7. 地址转换
与 虚拟化 CPU 时类似,我们同样需要保证内存的虚拟化是高效、可控、灵活的。高效意味着我们需要硬件的支持;可控意味着操作系统必须确保应用程序只能访问自己的地址空间;此外灵活性要求程序能以任何方式访问自己的地址空间,这也方便了程序与系统的编写。因此,我们将同样采取 受限直接访问 的思路构建这一机制,即操作系统应尽量让程序自己运行,保持在关键点能够直接介入来保持对硬件的控制。
操作系统用以支持地址空间这一内存虚拟化的底层机制便是 基于硬件的地址转换。利用地址转换,硬件对每次内存访问进行处理时都会将指令中的虚拟地址转换为数据实际存储的物理地址。同样这一过程需要操作系统的介入:需要设置好硬件以完成正确的地址转换;需要记录内存的占用情况;需要在特定时机下介入。操作系统与硬件协作以实现 地址空间 的假象,来支持多个程序共享物理内存。
为了方便叙述,我们采用与 4. 进程调度策略 类似的方式:先提出一些不切实际的假设,讨论理想情况下的机制,随后逐步去掉这些假设,讨论现实情况中的地址转换与内存分配的机制。
我们对进程的内存空间作出如下假设:
- 地址空间必须在物理内存中连续。
- 地址空间不是很大(小于物理内存的大小)。
- 所有进程的地址空间大小相同。
基本想法:基于硬件的动态重定位
在这些假设下,解决方法似乎十分简单。最常见的一个想法即是 动态重定位(Dynamic Relocation)机制,又称 基址加界限(Base and Bound)机制。其思路很简单:每个 CPU 维护一对硬件寄存器:基址寄存器与界限寄存器。我们有时称 CPU 中负责地址转换的部分称作 内存管理单元(Memory Management Unit,MMU)。当程序运行时,操作系统决定该程序的地址空间在物理内存中的实际的起始地址,并将其记录在基址寄存器中;界限寄存器负责提供访问保护,操作系统决定程序的地址空间大小,并将其记录在界限寄存器中。由于进程的地址空间从零开始,则进程产生的所有 虚拟 内存引用都可以在加上基址寄存器的值后得到实际的物理地址。而当进程试图访问负的或超出地址空间的虚拟地址时,CPU 会触发异常并将控制权交给操作系统中负责越界的异常处理程序处理,操作系统可能会终止该进程。这便是地址转换技术。由于这种重定位在程序运行时发生,因此也被称作动态重定位。
早期的一些操作系统采用纯软件的定位方式,每当进程发出内存访问请求时,一个 加载程序 接手该程序,将虚拟地址重写为物理地址并访问内存。这被称作 静态重定位(Static Relocation)。
静态重定位存在许多问题:一大问题是 不提供访问保护,而这一般需要硬件支持;另一大问题是虚拟地址与物理地址的指向关系很难修改。
界限寄存器既可以存储地址空间的大小,也可以存储地址空间结束的物理地址,两种方法是等价的。
操作系统同时还需要跟踪内存的空闲情况以便能够为进程分配内存,最简单的方法就是 空闲列表,一个线性记录当前空闲内存范围的列表。
在前面的例子中,硬件可以介入内存访问以将指令中的虚拟地址转换为数据实际存储的物理地址。这表明介入是一个十分有用的设计思路。几乎所有良好定义的接口都应该提供介入机制以便增加功能或提升系统。介入的优点在于 透明,即完成介入不需要改动接口,而是可以在介入某个接口后在外部完成更多功能处理。
与 虚拟化 CPU 时类似,为了实现上面所述的机制,需要硬件与操作系统共同支持更多操作。硬件需要提供:

而操作系统需要实现如下的功能:

具体步骤很简单:
- 停止进程。
- 将地址空间拷贝到新位置
- 更新进程结构或 PCB 中基址寄存器的值。
与前面 CPU 的受限直接执行 结合起来,我们得到了一个在大多数情况下应用程序与操作系统、硬件的交互逻辑的表格:


可以看到,在这个过程中,在操作系统设置好硬件后,进程就可以自行在 CPU 上运行,虚拟地址的转换过程也完全由硬件处理。操作系统只会在时机合适(如中断发生时)才会介入,这完美符合我们一开始的设计思路。
然而,到现在我们所建立的只是一个基于理想环境的最基本的内存访问机制。即使不去掉我们在上面提出的几条假设。这个机制仍然存在内存利用率不高的问题。具体而言:实际运行的进程并不会有特别大的栈区或堆区,这导致堆栈中间大量的空闲内存被浪费了。这被称作 内部碎片(Internal Fragmentation),指的是已经分配的内存单元内部存在碎片(即未使用的内存空间)从而造成了浪费。下一节将提出该问题的一个解决方法,在 MMU 中为地址空间中的每个逻辑段分配一对基址与界限寄存器,即 分段。我们也将看到,分段也有自己的问题。