0%

79:虚拟内存(1)

总是听到诸如页表,分页机制一类的词汇,听起来让人感到十分的复杂。实际上这些都是涉及到虚拟内存相关的只是。一直以来对这一部分的知识都是望而生畏,现在好好来理解一下它:

物理和虚拟寻址

计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有一个唯一的物理地址,物理地址从0开始依次设置。这是最简单自然的结构,我们把CPU以这个结构用来访问地址的方式称为物理寻址。早期的计算机和一些数字处理器和嵌入式设备仍然使用这种方式,

现代处理器则是使用一种被称为虚拟寻址的方式来进行寻址。使用虚拟寻址,CPU会生成一个虚拟地址(VA)来访问主存,VA被送到内存之前会先转换为适当的物理地址,这个过程叫做地址翻译。有一个专门的硬件单元来完成这个任务——内存管理单元(MMU)。原理是利用存放在主存中的查询表来动态翻译虚拟地址,这个表中的内容由操作系统管理。

地址空间

地址空间是非负整数地址的有序集合。如果地址空间中的整数时连续的,我们说它是一个线性地址空间,我们假定之后用到的所有地址空间都是线性的。在一个带虚拟内存的系统中,CPU从有一个有N=2^n个地址的地址空间中生成虚拟地址,则这个地址口空间称为虚拟地址空间

一个地址空间的大小是由表示最大地址所需要的位数来描述的。对于一个包含N=2^n个地址的虚拟地址空间,我们可以将其叫做为一个n位的地址空间。一个系统还带有一个物理地址空间,对应于系统中的物理内存的M个字节。

地址空间的概念实际上区分了两个概念:

  • 数据对象(字节)
  • 属性(地址)

所以我们应该意识到数据对象实际上可以有多个地址,只不过每一个地址都选自一个不同的地址空间,这就是我们虚拟空间所用到的概念。例如主存中的每一个字节都有有一个选自虚拟地址空间的虚拟地址和一个选自物理地址空间的物理地址。

虚拟内存作为缓存工具

虚拟地址实际上就是一个由存放在磁盘上的N个连续的字节大小的单元组成数组,每一个字节都有着一个对应的虚拟地址,作为对这个数组的索引。磁盘上数组的内容被缓存在主存中。和其他的缓存一样,磁盘上的数据被分隔成块,这些快作为磁盘和主存之间的传输单元。

VM系统将虚拟内存分割为虚拟页的大小固定的块,每个虚拟页的大小为P=2^p字节。物理内存也被分隔为同样大小的物理页,也称页帧。

在任意时刻,虚拟页处于以下中的一种状态:

  • 未分配:VM系统还没有创建的页。未分配的块不会有任何数据关联,不占用磁盘空间
  • 已缓存:当前已缓存在物理内存中的已分配页
  • 已分配:未缓存在物理内存中的已分配页

页表

在这里需要用到DRAM和SRAM的关系,可以查看存储器层次架构进行回顾。

和任何缓存一样,VM系统需要要一种方法来判定一个虚拟页是否被缓存在物理内存中的某个地方。如果命中,怎么确定这个虚拟页被存放在那个物理页中。如果不命中,系统需要判断虚拟页存放在磁盘的哪个位置,并在物理内存中选择有一个牺牲页,将虚拟页复制到这里,替换这个牺牲页。

通过操作系统软件、MMU和存放在物理内存中的页表,软硬联合,从而将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为一个物理地址的时候,都会读取页表。操作系统则负责维护页表中的内容,在磁盘和主存间来回传送页。页表的结构大致如下:

image.png

我们认识一下页表的基本数据结构,页表就是一个页表条目(PTE)的数组。虚拟地址空间中的每个页在页表中一个固定的偏移量处都有一个PTE(也就是说PTE的大小是固定的)。根据上面的这个简化模型,每个PTE实际上是由一个有效位和一个n位地址字段组成的:

  • 有效位表明该虚拟也当前是否被缓存在主存中。
  • n位地址字段,在有效位被设置的情况下,表示主存中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么这个地址指向该虚拟页在磁盘中的起始位置。

在上图中我们就可以看到虚拟页的三种状态:未分配、未缓存、已缓存。

页命中

当CPU想要读取包含在VP2中的虚拟内存的一个字时,地址翻译硬件会将虚拟地址作为一个索引来定位PTE2,然后再页表(内存)中读取它。因为设置了有效位,地址翻译硬件就会知道VP2被缓存在内存中,然后就会使用PTE中存储的物理内存地址,构造出这个字的物理地址。

image.png

缺页

缺页实际上就是缓存不命中,同上图。CPU引用了VP3中的一个字,地址翻译硬件根据有效位发现VP3并没有被缓存在内存中,于是触发一个缺页异常。这个异常调用内核中的缺页异常处理程序,该程序会选择一个牺牲页。程序将牺牲页复制回硬盘中,并将VP3覆盖牺牲页。并修改页表中它们的状态。然后返回,并将导致缺页的虚拟地址重新发送给地址翻译硬件,此时页命中,可以被正确处理:

image.png

这个在磁盘和内存之间传送页的活动叫做页面调度,仅在不命中的情况下才进行调度的策略是按需页面调度,我们之后都会使用这个策略。

分配页面

image.png

这个过程展示了当操作系统分配一个新的虚拟内存页时,对我们的示例页表产生的影响。在这个过程中,系统在磁盘上创建了一个空间并更新PTE5,使它指向磁盘上这个新创建的页面。

局部性分析

对于虚拟内存的策略,我们可能会认为这是一个效率极低的方案,因为它的不命中惩罚很大。但是实际上,它有着良好的局部性。局部性保证了,在任意时刻中,程序将趋于一个较小的活动页面上工作,例如空间局部性,较大的页空间确保了很好的空间局部性,因为对于数据结构,程序是按序访问的;对于时间局部性,一段内存往往会被反复利用,所以有着良好的时间局部性。

当然如果出现了工作集大小超出内存大小的情况时,程序可能会发生抖动,页面会不停的换进换出,带来严重的不命中开销。

虚拟内存作为内存管理的工具

虚拟内存不仅有着很好的缓存性能,同时它也很好的简化了内存管理,为我们提供了一个很好的内存保护机制。

实际上,操作系统为每个进程提供了一个独立的页表,也就是一个独立的虚拟空间,下图很好的展示了这一点:

image.png

注意,这里可以看到多个虚拟页面实际上是可以映射到同一个共享物理页面上。

通过按需页面调度和独立的虚拟地址空间的结合,系统对内存的使用和管理被极大的简化,VM系统简化了链接和加载、代码和数据共享、以及应用程序的内存分配…

简化链接

独立的地址空间也允许每个进程的内存映像使用相同的基本格式,而不用考虑代码和数据实际上被存储在哪里。这样的一致性简化了链接器的设计和实现,允许链接器生成完全链接的可执行文件,这些可执行文件是独立于物理内存中代码和数据的最终位置的。

image.png

简化加载

虚拟内存简化了向内存中加载可执行文件和共享对象文件的过程。要把目标文件中.text.data节加载到一个新创建的进程中,Linux加载器会为代码段和数据段分配虚拟页,然后将其标为无效的(即未缓存)。而不是将其进行缓存,只有当页被引用到时,虚拟内存会按需调度这些页面。

将一组连续的虚拟页映射到任意一个文件的任意位置的表示法叫做内存映射我们会在之后涉及这些内容。

简化共享

一般而言,每个进程都有自己私有的代码,数据,堆栈等区域,这个其他进程是不共享的。操作系统为每个进程提供页表,将相应的虚拟页映射到不同的物理页面。也就是说,对于不同进程来说,尽管是同一个虚拟地址,但是实际上映射的是不同的物理地址。极大程度上简化了进程间私有的问题。

当然有时候进程间有也需要共享代码和数据,例如每个进程都调用相同的操作系统内核代码,操作系统会将不同进程中适当的虚拟页面映射到相同的物理页面,从而安排多个进程共享这部分代码的一个副本,而不是为每个进程都分配一个副本。

简化内存分配

虚拟内存为用户进程提供了一个简单的分配额外内存的机制。当一个运行在用户进程的程序要求有一个额外的堆空间时,操作系统只需要分配适当的连续的虚拟内存页面,并将其映射到物理内存中的物理页面就行了。通过页表,操作系统也不用分配连续的物理页。使得页面可以随机的分布在物理内存中,提高了碎片空间的可用性。

虚拟内存作为内存保护的工具

操作系统需要有手段来控制对内存系统的访问,不应该允许用户进程对其只读代码段进行修改,也不应该允许它修改内核中的代码和数据结构,不应该允许它读写其他进程的私有内存或是修改和其他进程共享的虚拟原页面。而虚拟内存能够很好的实现这个机制:

当每次CPU生成一个地址时,地址翻译硬件都会读一个PTE,我们可以通过有效位来判断这个页面的状态。我们也可以通过添加额外的许可页来控制对一个虚拟页面内容的访问。

image.png

例如图中的,SUP位表示进程是否必须在内核模式下才能访问此页。READ和WRITE位则控制对原页面的读写访问。不同进程的页表中对同一个页的访问权是不同的,以此可以实现对进程内存访问的控制。

如果一条指令违反了这些许可条件,那么CPU就会触发保护故障,将控制传递给异常处理程序。LInux Shell一般将这个异常报告为Segmentation fault