Linux虚拟内存

为了更有效的管理内存并且少出错,现代操作系统提供了一种对主存的抽象概念,叫做虚拟内存(VM)。虚拟内存提供了三个重要的能力:
1.它将主存(物理内存)看成是一个存储在磁盘上的地址空间的高速缓存,在主存中只保留活动区域,并且根据需要在磁盘和主存之间来回交换数据。通过这种方式它高效的使用了主存。
2.他为每个进程提供了一致的地址空间,从而简化了内存的管理。
3.它保护了每个进程的地址空间不被其他进程破坏。
这篇文章里就带大家了解一下虚拟内存是怎样实现上面的三种重要的能力的。

寻址与地址空间

在实际介绍虚拟内存的功能与实现之前我们必须先来大致了解一下物理寻址,虚拟寻址,物理地址空间,虚拟地址空间的概念。

物理寻址与虚拟寻址

计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组,每个字节都有一个唯一的物理地址。第一个字节物理地址为0,下一个字节为1,在下一个为2,以此类推。不带任何存储器抽象的直接使用物理地址的方式就称作物理寻址(Physical addressing)

如上图展示了一个物理寻址的示例。该示例的上下文是一条加载指令,它读取从物理地址4处开始的4字节。当CPU执行这条加载指令时,会生成一个有效的物理地址,通过内存总线,把它传递给主存,主存取出物理地址4处开始的4字节,并将它返回给CPU。早期的PC使用的是物理寻址,现代也有部分具有特殊用途的计算机系统会采用这种寻址方式。但是这种寻址方式对于现代的多道程序设计系统却不适用。所以现代处理器使用的是一种称为虚拟寻址(Virtual addressing)的寻址方式,参见下图。

使用虚拟地址,CPU通过生成一个虚拟地址(Virtual Address,VA)来访问主存。这个虚拟地址在被送到内存之前先被转换为了适当的物理地址。将一个虚拟地址转换成物理地址的过程叫做地址翻译。该地址翻译由一个CPU芯片上的叫做内存管理单元(Memory Management Unit,MMU)的专用硬件完成。MMU利用存放在主存中的查询表来动态的翻译虚拟地址。这个查询表的内容由操作系统管理,并且整个地址翻译的过程是系统自己完成的,不需要应用程序员操作。

物理地址空间与虚拟地址空间

地址空间是一个非负整数地址的有序集合.
如:

1
{0,1,2,3,...}

如果地址空间中的整数是连续的,我们说它是一个线性地址空间(在这里我们假设地址空间总是连续的)。在一个带有虚拟内存的系统中,CPU从一个有N=pow(2,n)个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间(virtual address space)

1
{0,1,2,...,N-1}

一个地址空间的大小是由表示最大地址所需要的位数来描述的。例如,一个包含N=2的n次方个地址的虚拟地址空间就叫做n位地址空间。

一个系统还有还有物理地址空间(physical address space),对应与系统中物理内存的M个字节:

1
{0,1,2,...,M-1}

在了解了物理地址空间和虚拟地址空间之后,我们现在应该要建立这样一个认知:每个数据对象可以有多个独立的地址,其中每个地址都选自不同的地址空间。这就是虚拟内存的基本思想。主存中的每字节都有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。

好了,在了解了与内存有关的概念之后,我们开始进入到虚拟内存的功能与实现的讨论了。

作为缓存的实现

在文章开头我们描述虚拟内存提供的三个重要能力中提到的第一个能力就是:虚拟内存将主存看作是存储在磁盘上的地址空间的缓存。下面我们就详细的讨论一下,这是如何实现的。

什么是虚拟内存

在概念上来讲,虚拟内存被组织为由存放在磁盘上的N个连续的字节大小的单元组成的数组。每个字节都有唯一的虚拟地址,作为到数组的索引(现在我们知道了虚拟内存虽然叫做内存,但其实它并不在内存中,而是磁盘中的一块空间)。虚拟内存与物理内存之间构成了一个缓存系统。虚拟内存位于较低层,物理内存位于较高层。虚拟内存上的数据可以被缓存到主存中。VM系统将虚拟内存分割为称为虚拟页(Virtual Page)的大小固定的块,每个虚拟页的大小为P=pow(2,p)字节。类似的,物理内存被分割为物理页(Physical Page),大小也为P字节,物理页也被称为页帧。在虚拟内存与物理内存之间交换的单元并不是一个一个的字节,而正是这些P字节大小的块。

虚拟内存的状态

在任意的时候,位于虚拟内存中的虚拟页都位于以下三种状态之一:

  • 未分配的
    处于未分配状态的虚拟页是VM系统还未创建的页(严格来讲,他们此时并不能称之为虚拟页,它们是还未被纳入虚拟内存范围的磁盘块)。这些磁盘块没有任何数据与他们关联,因此它们不占用任何的磁盘空间。
  • 缓存的
    当前已缓存在物理内存中的已分配页。
  • 未缓存的
    当前未缓存在物理内存中的已分配页。


如上图展示了一个有8个虚拟页的小虚拟内存,虚拟页0,3还未分配,因此在磁盘上还不存在,虚拟页1,4,6被缓存在物理内存中,页面2,5,7已经被分配了,但是当前并未被缓存在主存中。

页表

同任何缓存系统一样,虚拟内存系统必须有某种方法来判定一个虚拟页面是否缓存在物理内存中。如果是,系统还必须确定这个虚拟页存放在哪个物理页面中。如果不命中,系统还必须确定这个虚拟页存放在磁盘的哪个位置,从而将该虚拟页面读入到物理内存中的某个物理页面中,如果物理内存中没有多余的物理页面,那么还将选择一个牺牲页将其移出(在这里移出的时候还要考虑该牺牲页是否已经更新,如果更新还要对应的更新虚拟页面里面的内容),从而腾出空间放置新读入的物理页面的数据。
上述的这些功能是由软硬件联合提供的,包括操作系统软件、MMU中的地址翻译硬件和一个存放在物理内存中的的叫做页表的数据结构。页表将虚拟页面映射到物理页面。每次地址翻译硬件将一个虚拟地址转换为物理地址的时候都要读取页表。

如下图展示了一个页表的基本基本组织结构:

页表就是一个由页表条目(Page Table Entry,PTE)组成的数组。虚拟地址空间中的每个页在页表的一个固定偏移量处都有一个PTE。为了简单起见,这里我们假设每个PTE是由一个有效位(valid bit)和一个n位地址字段组成的。有效位表明了该虚拟页面是否被缓存在物理内存中。如果设置了有效位,则地址字段存放的就是物理内存中相应的物理页面的起始地址;如果没有设置有效位,那么一个空地址表示这个虚拟页还未被分配,否则,这个地址指向该虚拟页面在磁盘上的起始位置。

页命中与缺页

还是以页表小节中的图片为例,考虑一下当CPU想要读包含在VP2中的虚拟内存中的一个字时会发生什么,由于VP2已经被缓存在物理内存中。地址翻译硬件将虚拟地址作为索引来定位PTE2,并且从物理内存中读取它(前面讲过页面位于物理内存中),检查其有效位,发现其已经设置了有效位,此时地址翻译硬件就知道该页面已经缓存在物理内存中,并且该有效位后面的bit表示的就是对应的物理页面的地址。上面描述的这个过程就是页命中的情形。

下面我们来考虑一下缓存不命中的情形(在虚拟内存的习惯说法中,缓存不命中称为缺页)。

如下是缺页之前我们的示例页表的状态:


CPU想要读包含在VP3中的虚拟内存中的数据,地址翻译硬件根据CPU给出的虚拟地址,定位到页表中的PTE3,然后在内存中读取PTE3的内容,检查发现有效位为0,这表明当前CPU请求的页面并没有白缓存到物理内存中,这时就发生了缺页异常。这个缺页异常会调用内核中的缺页异常处理程序,该程序会选择一个牺牲页,在次例中该牺牲页就是存放在PP3中的VP4。如果VP4已经被修改,那么内核就会把它赋值回磁盘。无论哪种情况,内核都会修改VP4中的页表条目,反映出VP4已经不再缓存在物理内存中这一事实。


接下来,内核从磁盘复制VP3到内存中的PP3,更新PTE3,此时我们的示例页表状态就成为上图这样子了。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重新发送到地址翻译硬件。但是现在VP3已经缓存在主存中了,那么页面也就能够由地址翻译硬件正常处理了。

在这里,我们有一个要注意的问题,在上面的叙述中,我们随机选择了一个页面作为牺牲页,但是在实际中为了获得更好的性能是不会随机选择的,这需要涉及到页面置换算法,我们将在后面的一篇文章中介绍。

内存管理的实现

内存保护的实现