一个理想内存(RAM)应该是:私有的、容量无限大的、速度无限快的、永久非易失的。但技术所限,并不存在这样的内存。
分层存储器体系包括:
- 若干兆(MB)快速、昂贵且易失性的高速缓存(cache);
- 数GB速度与价格适中且同样易失性的内存;
- 数TB低速、廉价、非易失性的磁盘存储;
- 诸如U盘等可移动存储装置。
操作系统中管理分层存储器体系的部分称为存储管理器,它的任务是有效地管理内存、即记录哪些内存是正在使用的,哪些内存是空闲的;在进程需要时为其分配内存,在进程使用完后释放内存。
地址空间
就像进程的概念创造了一类抽象的CPU以运行程序一样,地址空间为程序创造了一种抽象的内存。
地址空间是一个进程可用于寻址内存的一套地址集合。每个进程都有自己独立于其他进程的地址空间。
地址空间很常见,例如电话号码。地址空间可以不是数字的,一套“.com”的互联网域名也是地址空间。
给每个进程单独的地址空间的最简单方法就是利用基址寄存器和界限寄存器。程序的起始物理地址装载到基址寄存器中,程序的长度装载到界限寄存器中。
空闲内存管理
在动态分配内存时,操作系统必须对其管理。有两种方式可以跟踪内存使用情况:位图和空闲链表。
位图
内存被划分成小到几个字,大到几千字节的分配单元,每个分配单元对应位图中的一位,0表示空闲,1表示占用。内存和分配单元的大小决定了位图的大小。
链表
另一种记录内存使用情况的方法是:维护一个记录已分配内存段和空闲内存段的链表。链表的一个结点或许表示一个进程,或许表示两个进程间的一个空闲区。每个结点都包含以下域:空闲区(H)或进程(P)的指示标志、起始地址、长度和指向下一个结点的指针。
当按照地址顺序在链表中存放进程和空闲区时,有几种算法可以用来为新进程分配内存。
- 首次适配算法:存储管理器沿着链表进行搜索,直到找到一个足够大的空闲区。
- 下次适配算法:在首次适配的基础上,每次还记录当时的位置,以便在下次寻找空闲区时从上次结束的地方开始搜索。
- 最佳适配方法:搜索整个链表,找出能容纳进程的最小的空闲区。
虚拟内存
虽然存储器容量增长迅速,但是软件大小的增长更快。需要运行的程序往往大到内存无法容纳,而又需要系统能够支持多个程序同时运行。
和存储器层次结构中其他缓存一样,磁盘(较低层)上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。
虚拟内存的基本思想是:每个程序的地址空间被分割成多个被称作虚拟页面的块,每页有连续的地址范围。这些页被映射到物理内存中,但不是所有的页都要在内存中才能运行该程序。
每台计算机都有一个字长,指明整数和指针数据的标称大小。虚拟地址是以这样的一个字来编码的,所以字长决定的最重要的系统参数就是虚拟地址空间的最大大小。32位系统的虚拟地址空间是4GB。
类似的,物理内存中对应的单元称为页框、页帧,大小和虚拟页一样。
在任意时刻,虚拟页面的集合都分为三个不相交的子集:
- 未分配的:VM系统还未分配(或者创建)的页。未分配的块没有任何数据和它们相关联,因此也就不占用任何磁盘空间。
- 缓存的:当前缓存在物理存储器中的分配页。
- 未缓存的:没有缓存在物理存储器中的已分配页。
如图展示的是一个8个虚拟页的小虚拟存储器。虚拟页0和3还未分配,因此在磁盘上不存在。虚拟页1、4和6被缓存在物理存储器中。页2、5和7已经被分配了,但是当前并未缓存在主存中。
分页
64KB的虚拟地址空间和32KB的物理内存,拥有16个虚拟页面和8个页框。
内存管理单元(MMU)将页面映射到实际的页框中。
在上图中,当CPU读取包含在VP2中的虚拟存储器的一个字时,由于VP2是被缓存在DRAM中的,地址翻译硬件将虚拟地址作为一个索引来定位PTE2,并从存储器中读取它。因为设置了有效位,那么地址翻译硬件就知道VP2是缓存在存储器中的了。所以它使用PTE中的物理存储器地址,构造出这个字的物理地址。
页表
每个进程都需要自己的页表。
虚拟地址可以被分成虚拟页号(高位部分)和偏移量(低位部分)。例如,对于16位地址和4KB的页面大小,高4位可以指定16个虚拟页面中的一页,而低12位接着确定了所选页面中的字节偏移量(0~4095)。
页表的目的是把虚拟页面映射为页框。从数学的角度说,页表是一个函数,它的参数是虚拟页号,结果是物理页框号。通过这个函数可以把虚拟地址中的虚拟页面域替换成页框域,从而形成物理地址。
一个页表就是一个页表条目(Page Table Entry,PTE)的数组。虚拟地址空间中的每个页在页表中一个固定偏移量处都有一个PTE。为了我们的目的,我们将假设每个PTE是由一个有效位和一个n位地址字段组成的。有效位表明了该虚拟页当前是否被缓存在DRAM中。如果设置了有效位,那么地址字段就表示DRAM中相应的物理页的起始位置,这个物理页中缓存了该虚拟页。如果没有设置有效位,那么一个空地址表示这个虚拟页还未分配。否则,这个地址就指向该虚拟页在磁盘上的起始位置。
上图展示的是一个8个虚拟页和4个物理页的系统的页表。四个虚拟页VP1、VP2、VP3和VP7当前被缓存在DRAM中。两个页VP0和VP5还未被分配,而剩下的页VP3、VP6已经被分配的,但是当前还未被缓存。
缺页
DRAM缓存不命中称为缺页。下图展示的是缺页之前的页表状态。CPU引用VP3中的一个字时,VP3并未缓存在DRAM中。地址翻译硬件从存储器中读取PTE3,从有效位推断出VP3未被缓存,并且触发一个缺页异常。
缺页异常调用内核中的缺页异常处理程序,该应许会选择一个牺牲页,在此例中就是存放在PP3中的VP4。如果VP4已经被修改了,那么内核就会将它拷贝回磁盘。接着,内核从磁盘拷贝VP3到存储器中的PP3,更新PTE3,随后返回。当异常处理程序返回时,它会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重发送到地址翻译硬件。这时,VP3已经缓存在主存中了,那么页命中也能由地址翻译硬件正常处理了。
分页时的局部性
我们可能会怀疑虚拟存储器的效率,因为涉及到磁盘交互,不命中的处罚很大,我们会担心页面调度会破坏程序性能。但实际上,虚拟存储器工作得相当好,这主要归功于我们的老朋友局部性(locality)。
尽管整个运行过程中程序引用的不同页面的总数可能超出物理存储器总的大小,但是局部性原则保证了在任意时刻,程序将往往在一个较小的活动页面集合上工作,这个集合叫做工作集或常驻集。在初始开销,也就是将工作集页面调度到存储器中之后,接下来的针对这个工作集的引用将导致命中,而不会产生额外的磁盘流量。
只要我们的程序有好的时间局部性,虚拟存储器系统就能工作得相当好。但是,并不是所有的程序都能展现良好的时间局部性。如果工作集的大小超出了物理存储器的大小,那么程序将产生一种不幸的状态,叫做颠簸,这时页面将不断地换进换出。
虚拟存储器作为存储器保护的工具
现代操作系统都有方法来控制对存储器系统的访问。例如,不应该让进程修改它的只读文本段,也不应该允许它读或者修改任何内核中的代码,也不应该允许它读写其他进程的私有存储器。
在虚拟存储器中,我们通过PTE页表条目来对访问的页面进行控制。我们通过在PTE中添加访问标记,如SUP(表示运行在内核模式下才可访问页面),READ,WRITE等标记来控制。
如果某些指令违反这些许可条件,就会触发一个一般保护故障,Unix异常报告为“段错误”。
分页实现
利用TLB加速分页
任何分页系统都要考虑两个主要问题:
- 虚拟地址到物理地址的映射必须非常快;
- 如果虚拟地址空间很大,页表也会很大。
大多数程序总是对少量的页面进行多次的访问,因此只有很少的页表项会被反复读取,而其他的页表项很少被访问。
可以为计算机设置一个小型的硬件设备,将虚拟地址直接映射到物理地址,而不必再访问页表,这种设备称为翻译后备缓冲区(TLB)。
TLB是一个小的、虚拟内存的缓存。其中每一行都保存着一个由单个PTE组成的块。
软件的TLB也被广泛应用,当一个页面访问在内存中而不在TLB中时,将产生软失效,这时就要更新一下TLB;当页面既不在内存中也不在TLB中时,将产生硬失效,此时需要进行磁盘存取以装入该页面。
针对大内存的页表:多级页表
假设我们有一个32位的地址空间(虚拟地址空间4GB)、4KB的页面和一个4字节的PTE,那么即使应用程序只是用一小部分的虚拟地址空间,也需要一个4MB的页表驻留在存储器中。这对于多任务的计算机来说将会是极大的浪费,所以现代系统采用层次结构的多级页表来压缩页表。
假设32位虚拟地址空间被分为4KB的页,每个页表条目都是4字节。存储器的前2K个页面分配了代码和数据,接下来的6K个页面还未分配,再接下的1023个页面也未分配,接下来的1个页面分配给了用户栈。下图展示一个两级页表的层次结构。
一级页表的每个条目对应二级页表1024个条目,换句话说虚拟地址空间中一个4MB的片。这里总的虚拟地址是4GB,1024个一级页表条目就足够覆盖整个空间了。
如果片i中的每个页面都未分配,那么一级PTEi就为空,例如片2-7。然而,如果在片
i中至少有一个页是分配了的,那么一级PTEi就指向一个二级页表的基址。例如片0、1和8。
二级页表中的每个PTE都负责映射一个4KB的虚拟页面。
64位分页系统:倒排页表
在这种设计中,每个页框有一个表项,而不是每个虚拟页面有一个表项。
在此基础上,使用TLB。
进一步,对虚拟页面建立哈希表。如果哈希表中的槽数与机器中物理页面数一样多,那么哈希表的冲突列的平均长度将会是一个表项,一旦页框号被找到,新的<虚拟页号,物理页框>
对就会被装载到TLB中。
页面置换算法
页面置换算法有很多,最理想的是最优页面置换算法,但它不可能实现。实际中,最好的两种算法是老化算法和工作集时钟算法。
最优页面置换算法
这是一个理想的算法,实际中不可能实现:当发生缺页中断时,置换内存中最晚使用到的页面,从而把因为调入当前页面而发生的下一次缺页中断推迟得越久越好。