0%

深入理解计算机系统-虚拟内存

1 虚拟内存

一个系统中的进程是与其他进程共享CPU和主存资源的。在之前我们明白一个进程会有4GB的内存,但实际中不可能为一个进程真实的分配这么多,否则对于8G的主存,只要两个进程就占满了。因此,计算机系统的设计者们引入了一中很聪明的做法——虚拟内存

虚拟内存是硬件异常、硬件地址翻译、主存和磁盘文件和内核软件的完美交互:

1.1 虚拟寻址和地址空间

在计算机中每个字节都存在它唯一的地址,该地址就是物理地址,我们将通过真实物理地址进行寻址的方法称为物理地址

而虚拟地址,是指由CPU生成一个虚拟地址(VA)来访问主存,如下图,这个虚拟地址在送到内存之前会经过MMU内存管理单元来翻译成物理地址,在这里会结合到后面的页表来进行翻译。通过虚拟地址寻址的方式就是虚拟寻址。现在计算绝大部分都是使用寻你寻址

地址空间

  • 虚拟地址空间:虚拟地址空间一般以地址总线条数为基准,如32位的位\(2^{32}=4GB\)
  • 物理地址空间:对应与实际内存挡住的M字节,一般来说,物理地址空间由进程运行过程中所实际使用的内存大小所决定

物理地址空间和虚拟地址空间是对应关系的,即由虚拟地址找到真实的物理地址。

1.2 虚拟内存是作为缓存的工具

在概念上,虚拟内存是指存放在磁盘上的N字节大小的内存区域,每个字节都要相应的虚拟地址,作物寻址索引。和存储结构的缓存一样,磁盘的数据也被分割成块,在这类为做区别称之为虚拟页,每个虚拟页大小由计算机系统决定为\(P=2^p\),与之对应的是物理页,其大小也应该为P

在任何时刻,我们都不能实际真实一股脑的为进程分配全部的存储空间,因此虚拟页面分为三个不相交的子集:

  • 未分配的:VM系统还没有分配的页,此时未分配的块没有任何数据和它们管理,因此此时不会占有任何磁盘空间
  • 缓存的:当前缓存的虚拟页在占有磁盘中的空间,当然由于缓存到内存。内存当中也会占有一定内存空间
  • 维缓存的:在磁盘中占用空间,但并未缓存在内存中,当然不占用内存(此时当CPU要寻址该页当中一个数据时,由于未缓存在内存中,就会造成缺页异常。)

为与存储结构的缓存做区分L1、L2、L3的缓存机制称为SRAM缓存,主存和磁盘之间的缓存即虚拟内存缓存称为DRAM缓存

1.3 页表

虚拟内存系统必须以某种方法来判断一个虚拟页是否在主存DRAM上,如果时,还必须缺点在DRAM的哪个位置。若不命中,还必须知道它在磁盘的哪个位置,同时还必须用页调度算法在物理内存中牺牲一个页进行替换。

上述的功能由软硬件共同结合作用,包括操作系统软件、MMU和重要的数据结构——页表页表上是虚拟地址和物理页的关系,它常驻在主存上。每次地址翻译硬件将一个虚拟地址转为物理地址时,都会读取页表。页表的结构如下,页表就是页表条目PTE数组 在页表条目中会有三种情况,

  • 缓存的:已经缓存的有效位会置1,同时后面存储着相应的物理页号。
  • 未缓存的:有效位置0,后面存储着在磁盘的地址。
  • 未分配:有效位为0,后面是null

1.4 页命中和缺页

  • 页命中:在上图中,当cpu想读包含在VP2中的虚拟内存的一个字时,VP2此时已被缓存在主存中,地址翻译硬件此时将虚拟地址的作为一个索引来定位页表的PTE2,并从内存中读取它。因为设置了有效位,地址翻译硬件知道VP2此时已经缓存在主存中。所有使用PTE中的物理页号来该数据的构造物理地址。

    • 步骤:
      • 第一步:处理器生成一个虚拟地址VA,并把它传送给MMU
      • 第二步:MMU用VA生成寻址索引PTEA来定位PTE,从高速缓存/主存得到它
      • 第三步:高速缓存/主存MMU返回一个PTE
      • 第四步:MMU利用PTE和VA构造物理地址,并把它传送给高速缓存
      • 第五步:高数缓存/主存返回所请求的的数据给处理器
  • 缺页:此时若CPU要使用VP3该页中某块的某个数据,由于主存并没有缓存该物理页,因此页表的PTE有效位为0,就会触发缺页异常,执行缺页处理程序,进行页面调度替换后再返回原来执行的指令\(I_{cur}\)重新执行该虚拟的地址上的数据请求。在缺页异常处理程序,该程序会选择一个牺牲页作为替换VP3,假如选择了VP4作为替换页,若VP4修改了,那么内核执行写回操作,将他复制回磁盘。
    • 步骤:
      • 第一步和第三步:与上述一样
      • 第四步:PTE的有效位是零,所以MMU回触发缺页异常,传递给CPU中的控制到操作系统内核中的缺页异常处理程序。
      • 第五步:缺页异常处理程序确定物理内存的牺牲页,如果这个页面已经被修改,则把它换出到磁盘
      • 第六步:缺页异常处理程序调入新的页面,并且更新页表对应的PTE
      • 第七步:缺页异常处理程序返回到原来的进程,再次执行导致缺页的指令。

1.5 多级页表

对于单级页表,无论是否分配,我们都要维护PTE,试想一下32位系统,页面大小是4KB和一个4字节的PTE,那么在内存中总需要用\(4GB/4KB*4byte=4MB\)的页表,对于64位来说更加复杂。内存是计算机很稀缺的资源,我们当然不希望内存因为存储页表而耗费太多的内存,虚拟内存并不是所有都会分配使用,因此多级页表利用这个特性解决这个问题的。

下面展示的是二级页表,在一级页表中只有1024个PTE,它常驻内存,接下啦是二级页表,二级页表当中只有在一级页表中能够确定分配的才会驻存在内存,其他的未分配则不会生成,因此这样就能够大大节约内存 上图中只占用了内存\(1024*4Byte*4=16KB\)

1.5 局部性和页面分配

上述的缺页异常,就是页未命中,不命中的惩罚代价是很大的,很可能会影响程序性能。然而实际上,虚拟内存工作的很好,这归公与我么之前提到的局部性原理

通过以页面形式进行缓存,来补偿未命中的惩罚。局部性原则保证了在任意时刻,程序将趋向于在一个较小的活动页面集合上活动,这个集合可称为常驻集合

分配页面

上述图中存在未分配的页面,当我们相要分配时,可使用mallocVP5的分配过程就是在磁盘上创建空间并更新PTE5.

1.6 虚拟内存的作用

虚拟内存和按需页面调度进主存的机制,不仅大大较少了主存的空间压力,也使得在8G主存的计算机上能够运行上百个进程。同样虚拟内存VM还简化了程序的连接、加载,代码和数据共享已经应用程序的内存分配。

  • 简化链接:独立的地址空间允许每个进程的内存映像使用相同的格式,且都是相同的虚拟起始地址,只要保证虚拟内存地址得顺序就可以,而不用管代码和数据实际存放在物理内存的何处。
  • 简化加载:虚拟内存还使得容易向内存中加载可执行文件和共享对象文件。把目标文件.txt.data加载到一个进程中,只要Linux加载器为代码和数据段分配对应虚拟页就可。
  • 简化共享:进程间共享内存更加简便,只要在每个进程得页表维护一个相同得PTE
  • 简化内存分配:当一个允许得用户进程想要额外得堆空间时,操作系统通过分配一个适当数字k的连续虚拟内存,并将它们映射到物理内存的K个任意物理页面即可(即虚拟内存上看时连续的,但实际物理上任意)

虚拟内存存在也使得进程无法去访问修改一个页表中不存在的地址,同时进程页表的设置不像上面那么简单,他还设置了三个许可位,如SUP\READ\WRITE,表示是否内核模式下才能访问,若过程序访问一个位置没有遵循上述条件,就会产生大家熟悉的段错误(Segmentation fault)

1.7 理解写时复制

一个对象被樱色到虚拟内存中的区域,要么作为共享对象,要么作为私有对象

  • 共享对象:如果一个进程将一个共享对象映射到它的虚拟地址空间的一个区域,那么这个进程对这个区域的任何写操作,对于那些也把这个共享对象映射到虚拟内存内存的其他进程来说也是可见的

  • 私有对象:即对象进程私有,本进程的修改对其他进程不可见。值得一提的是私有对象使用了一种读时共享、写时复制的技术,以此节约了内存。当一个进程试图写一个私有对象,这个写操作就会触发一个保护故障,当故障处理程序注意到保护异常由于进程写私有区域时,它就会在物理内存创建一个新副本,更新页表条目指向这个新的副本,然后恢复这个页面的写权限 >fork()函数也时采用了读时共享,写时复制的技术。