现在我们解决了分页引入的第二个问题:页表太大,从而消耗了太多内存。让我们从一个线性的页表开始。正如你所看到的,线性页表变得非常大。再假设一个32位地址空间(2^32字节),有4KB(2^12字节)页和一个4字节的页表条目。因此,地址空间大致是这样的。 其中1百万虚拟页(2 ^32/2^12),乘以页表条目的大小,您可以看到我们的页表大小为4MB。还记得:我们通常在系统中的每个进程都有一个页表。有上百个活动进程(在现代系统中并不少见),我们将仅为页表分配几百兆字节的内存。因此,我们正在寻找一些技术来减轻这种沉重的负担。有很多,我们开始吧。但在我们的症结之前。
关键:如何使页表变小。
简单的基于数组的页面表(通常称为线性页表)太大了,在典型系统上占用了太多的内存。如何使页表更小?关键思想是什么?这些新的数据结构导致了什么效率低下!
20.1简单的解决方案:更大的页面.
我们可以用一种简单的方法减少页表的大小:使用更大的页面。再次使用我们的32位地址空间,但这次假设是16KB的页面。因此,我们将拥有一个18位的VPN加上14位的偏移量。假设每个PTE(4个字节)的大小相同,我们的线性页表中现在有218个条目,因此每个页面表的总大小为1MB。减少页表的4倍大小(毫不奇怪,减少确切地反映了4倍增加页面大小)。然而,这种方法的主要问题是,大页面会导致每个页面的浪费,这是一个被称为内部碎片的问题(因为垃圾是分配单元的内部)。因此,应用程序最终会分配页面,但只使用小块和小块,内存很快就会填满这些超大页。因此,大多数系统在普通情况下使用相对较小的页面大小:4KB(在x86中)或8KB(在SPARCv9中)。唉,我们的问题如此简单地解决不了。
20.2混合方法:分页和分段。
当你有两种合理但又不同的生活方式时,你应该经常检查两者的结合,看看你是否能获得这两个世界的精华。我们把这种组合称为混合。例如,当你可以把这两种食物混合在一起的时候,你可以把它们混合成一种可爱的混合饮料,叫做Reese s花生酱杯(M28)。年前,Multics的创建者(特别是Jack Dennis)在Multics虚拟内存系统的构建中偶然发现了这样一个想法[M07]。具体来说,Dennis提出了将分页和分割相结合的想法,以减少页表的内存开销。我们可以通过更详细地检查一个典型的线性页表来了解为什么会这样。假设我们有一个地址空间,其中堆和堆栈的使用部分是小的。对于这个示例,我们使用一个很小的16KB地址空间,其中有1KB的页面(图20.1)。这个地址空间的页表在图20.2中。
这个例子假定单一代码页(VPN 0)映射到物理页10,单一堆(VPN 4)物理页23页,和两个堆栈页另一端的地址空间(VPN 14和15)分别被映射到物理页28和4。从图中可以看到,大多数页表都是未使用的,满是无效的条目。真浪费。这是一个很小的16KB地址空间。想象一下一个32位地址空间的页表和所有潜在的浪费空间。实际上,不要想象这样的事情;这太可怕了。
因此,我们的混合方法:为什么没有一个单独的页表来处理整个进程的整个地址空间,为什么没有一个逻辑段呢?在本例中,我们可能有3个页表,一个用于处理地址空间的代码、堆和堆栈部分。现在,记住分段,我们有一个基本的寄存器告诉我们每个片段在物理内存中,以及一个限制或限制寄存器告诉我们说的片段的大小。在我们的混合动力系统中,我们仍然有MMU的结构;这里,我们使用的基础不是指向段本身,而是保存该段的页表的物理地址。边界寄存器用于指示页表的结束(即。,它有多少有效页面。)让我们做一个简单的例子来说明。假设有一个32位的虚拟地址空间,有4KB的页面,地址空间分成四个部分。我们将只使用三个片段:一个用于代码,一个用于堆,一个用于堆栈。为了确定地址引用的哪个段,我们将使用地址空间的前两个位。假设00是未使用的段,其中代码为01,堆为10,堆栈为11。因此,虚拟地址是这样的。
在硬件中,假设有三个基本/边界对,分别用于代码、堆和堆栈。当进程运行时,每个段的基本寄存器都包含该段的线性页表的物理地址;因此,系统中的每个进程现在都有三个与之相关联的页表。在上下文切换中,必须更改这些寄存器,以反映新运行进程的页表的位置。在TLB上(假设一个硬件管理的TLB,即:在硬件负责处理TLB错误的地方,硬件使用段位(SN)来确定要使用哪个基础和界限对。然后硬件将其中的物理地址和VPN结合起来,形成页面表条目的地址(PTE)。
这个序列应该看起来很熟悉;它实际上和我们之前看到的线性页表是一样的。当然,惟一的区别是使用了三个段基寄存器中的一个,而不是单页表基寄存器。我们的混合方案的关键区别是每段有一个边界寄存器;每个边界寄存器保存该段中最大有效页面的值。例如,如果代码段使用前三页(0、1和2),那么代码段页表只会有3个条目分配给它,并且设置边界寄存器到3。在段的末尾的内存访问将生成一个异常,并可能导致进程的终止。通过这种方式,我们的混合方法实现了与线性页表相比的显著的内存节省;堆栈和堆之间的未分配页不再占用页表中的空间(只是为了标记它们无效)。但是,正如您可能注意到的,这种方法并非没有问题。首先,它仍然要求我们使用分段;正如我们之前讨论过的,分割并不像我们希望的那样灵活,因为它假定了地址空间的某种使用模式;例如,如果我们有一个大的但很少使用的堆,我们仍然可以得到很多页表。第二,这种混合导致外部碎片再次出现。虽然大多数内存是在页面大小的单元中管理的,但现在页表可以是任意大小的(在PTEs的倍数中)。因此,在内存中为它们找到空闲空间要复杂得多。出于这些原因,人们继续寻找更好的方法来实现更小的页表。
20.3多级页表
不同的方法并不依赖于分割,而是攻击相同的问题:如何处理页表中的所有无效区域,而不是将它们全部保存在内存中?我们将此方法称为多级页表,因为它将线性页表变成了像树一样的东西。这种方法非常有效,许多现代系统都使用它(例如,x86 [BOH10])。我们现在详细描述这种方法。
多层页面表背后的基本思想很简单。首先,将页表切成页大小的单位;然后,如果一整页的页表条目无效,则不要分配该页表的所有内容。要跟踪页表的页面是否有效(如果有效,在内存中的位置),则使用一个名为“页目录”的新结构。因此,页面目录可以用于告诉您页表的页面所在位置,或者页面表的整个页面不包含有效页面。
图20.3显示了一个示例。在图的左边是经典的线性页表;尽管地址空间的大部分中间区域都无效,但我们仍然需要为这些区域分配页表空间(即:,中间两页的页表)页表有效(第一个和最后一个。)因此,页表的这两页就驻留在内存中。因此,您可以看到一种可视化多级别表的方法:它只会使线性页表的部分消失(为其他用途释放这些框架),并跟踪页面表中哪些页面被分配到页面目录中。
在一个简单的二级表中,页目录包含页表的每一页。它包含许多页目录条目(PDE)。一个PDE(最小)有一个有效位和一个页帧数(PFN),类似于PTE。但是,正如上面所暗示的,这个有效位的含义略有不同:如果PDE条目是有效的,那么它就意味着在页表中,入口指向(通过PFN)的页面至少有一个是有效的,即。在这个PDE的页面上至少有一个PTE,这个PTE的有效位被设置为1。如果PDE条目无效(即,等于零,PDE的其余部分没有定义。
与我们迄今为止看到的方法相比,多层页表有一些明显的优势。首先,可能最明显的是,多级表只按您使用的地址空间的数量分配页表空间;因此,它通常是紧凑的,并且支持稀疏地址空间。:第二,如果精心构造,页面表的每个部分都整齐地放在一个页面内,从而更容易管理内存;当需要分配或增加一个页表时,操作系统可以简单地获取下一个空闲页面。将其与简单的(非分页的)线性页面表2进行对比,它只是由VPN索引的一组PTEs;对于这样的结构,整个线性页表必须在物理内存中连续地驻留。对于一个大的页表(比如4MB),找到这样一大块未使用的连续的空闲物理内存可能是一个相当大的挑战。用多层次结构,我们通过使用页目录添加一个间接层,该目录指向页表的各个部分;这种间接方式允许我们将页表页面放置到我们想要的物理内存中。
应当指出的是,多层次的表格是有代价的;在TLB缺失时,需要从页表中获得正确的翻译信息(一个用于页面目录,另一个用于PTE本身),而只需要一个带有线性页表的加载。因此,多级表是一个时间空间权衡的小例子。我们想要更小的桌子,但不是免费的;尽管在常见的情况下(TLB命中),性能显然是相同的,但是TLB的缺失与这个较小的表的成本更高。另一个明显的负面因素是复杂性。无论是硬件还是操作系统处理页表查找(TLB缺失),这样做无疑比简单的线性页查找更复杂。我们通常愿意增加复杂性以提高性能或减少开销;在多级别表的情况下,为了节省宝贵的内存,我们使页表查找更加复杂。
一个详细的多层次的例子。
为了更好地理解多级页表背后的思想,我们来举个例子。想象一个大小为16KB的小地址空间,有64字节的页面。因此,我们有一个14位的虚拟地址空间,其中有8位用于VPN, 6位用于偏移。一个线性页表将有28(256)个条目,即使只有一小部分地址空间在使用中。图20.4给出了这样一个地址空间的示例。
例如,如果磁盘空间很丰富,那么您就不应该设计一个文件系统,该系统要尽可能地使用尽可能少的字节;类似地,如果处理器速度快,那么最好在操作系统中编写一个干净的、可理解的模块,而不是最优化的手工组装的代码。对于不必要的复杂性,在预先优化的代码或其他表单中保持警惕;这种方法使系统更难理解、维护和调试。正如安东尼·德·圣-埃克苏里(Antoine de Saint-Exupery)所写的那样:“当不再有任何东西需要补充时,完美终于实现了,但当不再有任何东西可以拿走的时候。他没有写的是:说一些关于完美的事情比真正实现它要容易得多。
在本例中,虚拟页0和1是代码,虚拟页4和5是堆,虚拟页254和255是堆栈;地址空间的其余部分未使用。为了为这个地址空间构建一个两级的页表,我们从完整的线性页表开始,并将其拆分为页面大小的单元。回想一下我们的完整表(在本例中)有256个条目;假设每个PTE的大小是4个字节。因此,我们的页表大小为1KB(256个字节)。如果我们有64字节的页面,那么1KB的页表可以划分为16个64字节的页面;每个页面可以容纳16个pte。我们现在需要了解的是如何使用VPN,并将其先应用到页面目录中,然后再进入到页表的页面中。记住,每个元素都是一个数组;因此,我们需要弄清楚的是如何从VPN的各个部分构造每个索引。让我们将第一个索引插入到页面目录中。这个示例中的页表很小:256个条目,分布在16个页面中。页目录需要页表每一页的一个条目;因此,它有16个条目。因此,我们需要4位VPN来索引到目录;我们使用VPN的前四位,如下所示。
旦我们从VPN中提取了page-directory索引(简称PDIndex),我们就可以使用它来找到一个简单计算的页面目录条目(PDE)地址:PDEAddr = PageDirBase + (PDIndex * sizeof(PDE))。这个结果出现在我们的页面目录中,我们现在检查它以在我们的翻译中取得进一步的进展。如果页目录条目被标记为无效,我们知道访问是无效的,从而引发异常。但是,如果PDE是有效的。我们还有很多工作要做。具体来说,我们现在必须从页面表中指向的页面目录中获取pagetable条目(PTE)。要找到这个PTE,我们必须使用VPN的剩余部分来索引页表的部分。
这个页表索引(简称PTIndex)可以用来索引到页表本身,给我们的PTE地址。PTEAddr =(PDE.PFN <<shift+ (PTIndex * sizeof(PTE)。请注意,从页目录条目获得的页帧数(PFN)必须在将其与页表索引结合以形成PTE的地址之前将其转换为位置。
为了查看这一切是否有意义,我们现在将填写一个具有一些实际值的多级页面表,并转换一个虚拟地址。让我们从这个示例的页面目录开始(图20.5的左侧)。在图中,您可以看到每个页面目录条目(PDE)描述了关于地址空间的页表的某个页面。在这个例子中,我们在地址空间中有两个有效的区域(在开始和结束时),以及在中间的一些无效的映射。
该页表包含了前16个VPNs的映射;在我们的示例中,VPNs 0和1是有效的(代码段),如4和5(堆)。因此,该表为每一页提供了映射信息。其余的条目被标记为无效。
页表的另一个有效页在PFN 101中找到。此页面包含地址空间最后16个VPNs的映射;详见图20.5(右)。例如,VPNs 254和255(堆栈)有有效的映射。希望我们从这个例子中可以看到,一个多层次的索引结构可以节省多少空间。在这个示例中,我们不为线性页表分配完整的16个页面,而是只分配三个:一个用于页面目录,另一个用于具有有效映射的页表的块。大的(32位或64位)地址空间的节省显然要大得多。最后,让我们使用这些信息来执行翻译。这里是一个地址,是指VPN 254: 0x3F80,或11 1111 1000000的二进制字节。回想一下,我们将使用VPN的前四位来索引到页面目录中。因此,1111将选择最后一个(如果您从第0页开始)将选择上面的页目录。这将指向位于地址101的页表的有效页。然后我们使用的下一个4位VPN(1110)指数到页面的页表,找到所需的PTE。1110年是倒数第二(14)页面上的条目,并告诉我们,254页的虚拟地址空间映射到物理页55。通过将PFN=55(或hex 0x37)和偏移量=000000连接起来,我们就可以形成我们想要的物理地址,并向内存系统发出请求:PhysAddr = (PTE.PFN << SHIFT) + offset=00 1101 1100 0000 = 0x0DC0。现在您应该知道如何构建一个两级的页表,使用指向页表的页目录。然而,不幸的是,我们的工作还没有完成。就像我们现在讨论的那样,有时两个层次的页表是不够的。
2.5More Than Two Levels
在我们的示例中,我们假设多级页面表只有两个级别:一个页面目录和一个页面表。在某些情况下,更深层的树是可能的(确实,需要)。让我们举一个简单的例子,并使用它来
说明为什么更深层的多级表是有用的。在这个例子中,假设我们有一个30位的虚拟地址空间和一个小的(512字节)页面。因此,我们的虚拟地址有一个21位的虚拟页面数组件和
9位偏移量。记住我们构建一个多层页面表的目标:使页面表的每个部分都适合于一个页面。到目前为止,我们只考虑了页表本身;但是,如果页面目录太大怎么办?
为了确定在一个多级别表中需要多少个级别,以使页面表的所有部分都适合于一个页面,我们首先要确定页面中包含多少页表条目。假设我们的页面大小为512字节,并且假设
PTE的大小为4个字节,您应该可以看到在一个页面上可以容纳128个PTE。当我们索引到页表的页面时,我们可以得出这样的结论:我们需要VPN中最不重要的7位(log2128)作为
索引。
您还可能注意到,上面的图中有多少位被放置到(大的)页目录:14。如果我们的页面目录有214个条目,它的范围不是一个页面而是128个,因此我们的目标是使每一个多层的
页面表都适合于一个页面消失。
为了解决这个问题,我们构建了树的另一个层次,将页面目录本身分割为多个页面,然后在其上添加另一个页面目录,以指向页面目录的页面。因此,我们可以将虚拟地址
分割如
现在,在索引上层页面目录时,我们使用虚拟地址的最顶端位(图中PD索引0);该索引可用于从顶级页面目录中获取页目录条目。如果有效,则通过将物理框架编号与顶级PDE和VPN的下一部分(PD Index 1)相结合,来咨询页面目录的第二级。
最后,如果有效,PTE地址可以通过使用页表索引和二级PDE的地址来形成。唷!那有很多工作要做。所有这些只是为了在一个多层的表中查找一些东西。
转换过程:记住TLB。
为了总结使用两级页表的地址转换的整个过程,我们再次以算法形式呈现控制流(图20.6)。这个图显示了在每个内存引用上硬件(假设一个硬件管理的TLB)发生了什么。正如您可以从图中看到的,在出现任何复杂的多级页表访问之前,硬件首先检查TLB。物理地址是直接形成的,不像以前那样直接访问页表。只有在TLB缺失时,硬件才需要执行完整的多级查找。这条路径上,您可以看到我们的传统两级页表的成本:两个额外的内存访问,以查找有效的转换。
20.4倒页表
在页表的世界里,有一种更极端的空间节省,就是倒页表。这里,我们没有很多页表(系统的每个进程),而是保留了一个单独的页表,其中包含系统的每个物理页面的条目。该条目告诉我们哪个进程使用这个页面,以及该进程的哪个虚拟页映射到这个物理页面。找到正确的条目现在是通过这个数据结构进行搜索的问题。线性扫描是昂贵的,因此一个哈希表通常建立在基础结构之上,以加速查找。PowerPC就是这种架构的一个例子[JM98]。更一般地,倒置的页表说明了我们从一开始就说过的:页表只是数据结构。你可以用数据结构做很多疯狂的事情,使它们变得更小或更大,使它们变得更慢或更快。多层和倒置的页表只是人们可以做的许多事情的两个例子。
20.5将页表交换到磁盘。
最后,我们讨论了最后一个假设的松弛。到目前为止,我们假定页表驻留在内核拥有的物理内存中。尽管我们有许多技巧来减少页表的大小,但是仍然有可能,它们可能太大而不能同时放入内存中。因此,有些系统在内核虚拟内存中放置这样的页表,从而允许系统在内存压力变得有点紧时将一些页表交换到磁盘。我们将在以后的章节(即VAX/VMS的案例研究)中详细讨论这个问题,一旦我们理解了如何移动页面。
20.6总结
现在我们已经看到了实际的页表是如何构建的;不一定是线性数组,而是更复杂的数据结构。这样的表存在于时间和空间中,表越大,TLB缺失的速度就越快,反之亦然,因此正确的结构选择在很大程度上依赖于给定环境的约束。
在内存受限的系统中(像许多旧系统一样),小型结构是有意义的;在一个具有足够内存和工作负载的系统中,使用大量的页面,加速TLB丢失的更大的表可能是正确的选择。通过软件的TLBs,数据结构的整个空间打开了操作系统创新者的喜悦(提示:that s you)。你能想出什么样的新结构?他们解决了什么问题?当你睡着的时候,想想这些问题,并梦想那些只有操作系统开发者才能实现的大梦想。