为什么有了物理地址还需要虚拟地址呢?虚拟地址的优势在哪里?
总体来说,虚拟内存为每个进程提供了一个大的、一致的和私有的地址空间。通过一个很清晰的机制,虚拟内存提供了三个重要的能力:
- 它将主存看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保存活动区域,并根据需要在磁盘和主存之间来回传送数据,通过这种方式,它高效地使用了主存;
- 它为每个进程提供了一致的地址空间,从而简化了内存管理;
- 它保护了每个进程的地址空间不被其他进程破坏;
那么关于虚拟内存的一些细节又是什么呢?
物理寻址和虚拟寻址
-
物理寻址
计算机系统的主存都被组织成一个由 M 个连续的字节大小的单元组成的数组。每字节都有一个唯一的物理地址(Physical Address, PA)。CPU 访问内存的最自然的方式就是使用物理地址。当 CPU 执行一条从物理地址 A 处加载 N 字节的指令时,会生成一个有效的物理地址,通过内存总线,把它传递给主存。主存取出从物理地址 A 开始的 N 字节,并将其返回给 CPU,CPU 会将它存放再一个寄存器里。这就是物理寻址取数的流程。
-
虚拟寻址
现代处理器使用的基本上都是虚拟寻址(virtual addressing)。使用虚拟寻址,CPU 会通过生成一个虚拟地址(Virtual Address, VA)来访问主存,这个虚拟地址在被送到内存之前先转换成适当的物理地址。将一个虚拟地址转换为物理地址的任务叫做地址翻译(address translation)。就像异常处理一样,地址翻译需要 CPU 硬件和操作系统之间的紧密合作。CPU 芯片上叫做内存管理单元(Memory Management Unit, MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址,该表的内容由操作系统管理。
虚拟内存作为缓存的工具
在任意时刻,虚拟页面的集合都分为三个不相交的子集:
- 未分配的:VM 系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间
- 未缓存的:未缓存在物理内存中的已分配页
- 已缓存的:当前已缓存在物理内存中的已分配页
页表
同任何缓存一样,虚拟内存系统必须由某种方法来判定一个虚拟页是否缓存在 DRAM 中的某个地方。如果是,系统还必须确定这个虚拟页存放在哪个物理页中。如果不命中,系统必须判断这个虚拟页存放在磁盘的哪个位置,在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到 DRAM 中,替换这个牺牲页。
这些功能是由软硬件联合提供的,包括操作系统软件、MMU 中的地址翻译硬件和一个存放在物理内存中的页表(page table)的数据结构,页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。操作系统复制维护页表的内容,以及在磁盘与 DRAM 之间来回传送页。
页表就是一个页表条目(Page Table Entry, PTE)的数组。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个 PTE。n 位的虚拟地址包含两个部分:一个 p 位的虚拟页面偏移(Virtual Page Offset, VPO)和一个(n - p)位的虚拟页号(Virtual Page Number, VPN)。MMU 利用 VPN 来选择适当的 PTE。将 PTE 中的物理页号(Physical Page Number, PPN)和虚拟地址中的 VPO 串联起来,就得到相应的物理地址。因为物理和虚拟页面都是 P 字节的,所以物理页面偏移(Physical Page Offset, PPO)和 VPO 是相同的。
假设每个 PTE 由一个有效位(valid bit)和一个 n 位地址字段组成。有效位表明了该虚拟页当前是否被缓存在 DRAM 中。如果设置了有效位,那么地址字段就表示 DRAM 中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。
因为 DRAM 缓存是全相联的,所有任意物理页都可以包含任意虚拟页。
缺页
DRAM 缓存不命中称为缺页(page fault)。缺页异常调用内核中的缺页异常处理程序,该程序会选择一个在缓存中的页面对新页进行替换。
虽然页面不命中的开销非常高,但是局部性原子保证了在任意时刻,程序将趋向于在一个较小的活动页面(active page)集合上工作,这个集合叫做工作集(working set)或者常驻集合(resident set)。在初始开销,也就是将工作集页面调度到内存中后,接下来对这个工作集的引用将导致命中,而不会产生额外的磁盘流量。只要我们的程序有很好的局部性,虚拟内存系统就能很好地工作。但是,如果工作集的大小超过了物理内存的大小,那么程序将产生抖动(thrashing),这时页面将不断地换进换出。
可以用 Linux 的 getrusage 函数监测缺页的数量(以及许多其他的信息)。
虚拟内存作为内存管理的工具
按需页面调度和独立的虚拟地址空间的结合,对系统中内存的使用和管理造成了深远的影响。特别的,虚拟内存简化了链接和加载、代码和数据共享,以及应用程序的内存分配。
- 简化链接。独立的地址空间允许每个进程的内存映像使用相同的基本格式,而不管代码和数据实际存放在物理内存的何处。代码段从虚拟地址开始,数据段跟在代码段之后,中间有一段符合要求的对其空白。栈占据用户进程地址空间最高的部分,并向下生长。这样的一致性极大地简化了链接器的设计和实现,允许链接器生成完全链接的可执行文件,这些可执行文件是独立于物理内存中代码和数据的最终位置的。
- 简化加载。虚拟内存还使得容易向内存中加载可执行文件和共享对象文件。要把目标文件中 .text 和 .data 节加载到一个新创建的进程中,Linux 加载器为代码和数据段分配虚拟页,把它们标记为无效的(即未被缓存的),将页表条目指向目标文件中适当的位置。加载器从不从磁盘到内存实际复制任何数据。在每个页初次被引用时,要么是 CPU 取指令时引用的,要么是一条正在执行的指令引用一个内存位置时引用的,虚拟内存系统会按照需要自动地调入数据页。
- 简化共享。独立地址空间为操作系统提供了一个管理用户进程和操作系统自身之间共享的一致机制。一般而言,每个进程都有自己私有的代码、数据、堆以及栈区域,是不和其他进程共享的。在这种情况下,操作系统创建页表,将相应的虚拟页映射到不连续的物理页面。
- 简化内存分配。虚拟内存为向用户进程提供一个简单的分配额外内存的机制。当一个允许在用户进程中的程序要求额外的堆空间时(如调用 malloc 的结果),操作系统分配一个适当数字(例如 k)个连续的虚拟内存页面,并且将它们映射到物理内存中任意位置的 k 个任意的物理页面。由于页表工作的方式,操作系统没有必要分配 k 个连续的物理内存页面。页面可以随机地分散在物理内存中。
虚拟内存作为内存保护的工具
任何现代操作系统必须为操作系统提供手段来控制对内存系统的访问。不应该允许一个用户进程修改它的只读代码段。而且也不应该允许它读或者修改任何内核中的代码和数据结构。不应该允许它读或者写其他进程的私有内存,并且不允许它修改任何与其他进程共享的虚拟页面,除非所有的共享者都显式地允许它这么做。
提供独立的地址空间使得区分不同进程的私有内存变得容易。但地址翻译机制可以以一种自然的方式扩展到提供更好的访问控制。因为每次 CPU 生成一个地址时,地址翻译硬件都会读一个 PTE,所以通过在 PTE 上添加一些额外的许可位来控制对一个虚拟页面内容的访问十分简单。
地址翻译
当页面命中时,CPU 硬件执行的步骤:
- 处理器生成一个虚拟地址,并把它传送给 MMU;
- MMU 生成 PTE 地址,并从高速缓存/主存请求得到它;
- 高速缓存/主存向 MMU 返回 PTE;
- MMU 构造物理地址,并把它传送给高速缓存/主存;
- 高速缓存/主存返回所请求的数据字给处理器;
页面命中完全是由硬件来处理的,而处理缺页要求硬件和操作系统内核协作完成。
- 第 1-3 步:和页面命中的 1-3 步相同;
- 第 4 步:PTE 中的有效位是零,所以 MMU 触发了一次异常,传递 CPU 中的控制到操作系统内核中的缺页异常处理程序;
- 第 5 步:缺页处理程序确定出物理内存中的牺牲页,如果这个页面已经被修改了,则把它换出到磁盘;
- 第 6 步:缺页处理程序页面调入新的页面,并更新内存中的 PTE;
- 第 7 步:缺页处理程序返回到原来的进程,再次执行导致缺页的指令。CPU 将引起缺页的虚拟地址重新发送给 MMU。因为虚拟页面限制缓存在物理内存中,所以就会命中,在 MMU 执行了页面命中的步骤之后,主存就会将所请求字返回给处理器。