type
status
date
slug
summary
tags
category
icon
password
Property
Apr 15, 2023 03:24 PM
我们现在来解决分页引入的第二个问题:
页表太大
,因此消耗的内存太多。回想一下:通常系统中的每个进程都有一个页表!有一百个活动进程(在现代系统中并不罕见),就要为页表分配数百兆的内存!因此,要寻找一些技术来减轻这种沉重的负担。
但先看我们的关键问题:关键问题:如何让页表更小? 简单的基于数组的页表(通常称为线性页表)太大,在典型系统上占用太多内存。如何让页表更小? 关键的思路是什么?由于这些新的数据结构,会出现什么效率影响?
简单的解决方案:更大的页
补充:多种页大小
另外请注意,许多体系结构(例如 MIPS、SPARC、x86-64)现在都支持多种页大小
。通常使用一个小的(4KB 或 8KB)页大小。但是,如果一个“聪明的”应用程序请求它,则可以为地址空间的特定部分使用一个大型页(例如,大小为 4MB),从而让这些应用程序可以将常用的(大型的)数据结构放入这样的空间,同时只占用一个 TLB 项。这种类型的大页在数据库管理系统和其他高端商业应用程序中很常见。然而,多种页面大小的主要原因并不是为了节省页表空间。这是为了减少 TLB 的压力,让程序能够访问更多的地址空间而不会遭受太多的 TLB 未命中之苦。然而,正如研究人员已经说明[N+02]一样,采用多种页大小,使操作系统虚拟内存管理程序显得更复杂,因此,有时只需向应用程序暴露一个新接口,让它们直接请求大内存页,这样最容易。
这种方法的主要问题在于,大内存页会导致每页内的浪费,这被称为
内部碎片
(internal fragmentation)问题(因为浪费在分配单元内部)。因此,结果是应用程序会分配页,但只用每页的一小部分,而内存很快就会充满这些过大的页。因此,大多数系统在常见的情况下使用相对较小的页大小:4KB(如 x86)或 8KB(如 SPARCv9)。问题不会如此简单地解决。混合方法:分页和分段
在生活中,每当有两种合理但不同的方法时,你应该总是研究两者的结合,看看能否两全其美。我们称这种组合为
杂合
(hybrid)。
多年前,Multics 的创造者(特别是 Jack Dennis)在构建 Multics 虚拟内存系统时,偶然发现了这样的想法[M07]。具体来说,Dennis 想到将分页和分段相结合
,以减少页表的内存开销
。
假设我们有一个地址空间,其中堆和栈的使用部分很小。例如,我们使用一个16KB 的小地址空间和 1KB 的页(见图 20.1)。该地址空间的页表如表 20.1 所示。这个例子假定单个代码页(VPN 0)映射到物理页 10,单个堆页(VPN 4)映射到物理页 23,以及地址空间另一端的两个栈页(VPN 14 和 15)被分别映射到物理页 28 和 4。从图 20.1 中可以看到,大部分页表都没有使用,充满了无效的(invalid)项
。我们的杂合方法不是为进程的整个地址空间提供单个页表,而是为
每个逻辑分段
提供一个。在这个例子中,我们可能有 3 个页表,地址空间的代码、堆和栈部分各有一个。
现在,回忆在分段中,有一个基址(base)寄存器
,告诉我们每个段在物理内存中的位置,还有一个界限(bound)或限制(limit)寄存器
,告诉我们该段的大小。在杂合方案中,我们仍然在 MMU 中拥有这些结构。在这里,我们使用基址不是指向段本身,而是保存该段的页表的物理地址
。界限寄存器用于指示页表的结尾(即它有多少有效页)
。
我们通过一个简单的例子来澄清。假设 32 位虚拟地址空间包含 4KB 页面,并且地址空间分为 4 个段。在这个例子中,我们只使用 3 个段:一个用于代码,另一个用于堆,还有一个用于栈。
要确定地址引用哪个段,我们会用地址空间的前两位。假设 00 是未使用的段,01 是代码段,10 是堆段,11 是栈段。因此,虚拟地址如下所示:在硬件中,假设有 3 个基本/界限对,代码、堆和栈各一个。当进程正在运行时,每个段的基址寄存器都包含该段的线性页表的物理地址。因此,系统中的每个进程现在都有 3 个与其关联的页表。在上下文切换时,必须更改这些寄存器,以反映新运行进程的页表的位置。
在 T
LB 未命中
时(假设硬件管理的 TLB,即硬件负责处理 TLB 未命中),硬件使用分段位
(SN)来确定要用哪个基址和界限对。然后硬件将其中的物理地址与 VPN 结合起来,形成页表项
(PTE)的地址:这段代码应该看起来很熟悉,它与我们之前在线性页表中看到的几乎完全相同,唯一的区别是使用 3 个段基址寄存器中的一个,而不是单个页表基址寄存器。
杂合方案的关键区别在于,每个分段都有界限寄存器,每个界限寄存器保存了段中最大有效页的值。例如,如果代码段使用它的前 3 个页(0、1 和 2),则代码段页表将只有 3个项分配给它,并且界限寄存器将被设置为 3。内存访问超出段的末尾将产生一个异常,并可能导致进程终止。以这种方式,与线性页表相比,杂合方法实现了显著的内存节省。栈和堆之间未分配的页不再占用页表中的空间(仅将其标记为无效)。
但是,你可能会注意到,这种方法并非没有问题。
- 它仍然要求使用分段,分段并不像我们需要的那样灵活,因为它假定地址空间有一定的使用模式。例如,如果有一个
大而稀疏
的堆,仍然可能导致大量的页表浪费。
- 这种杂合导致
外部碎片
再次出现。尽管大部分内存是以页面大小单位管理的,但页表现在可以是任意大小(是 PTE 的倍数)。因此,在内存中为它们寻找自由空间更为复杂。
多级页表
如何
去掉
页表中的所有无效区域
,而不是
将它们全部保留
在内存中?将线性页表变成
树
的形式。我们将这种方法称为多级页表
(multi-level page table)假设
一个页
能存4个页表记录(页表项PTE)
,通过页目录
的每一项(PDE
)表示 4 个页表项组成的一个页
,只有启用
的页才分配空间。支持稀疏空间
的,不需要连续,这种间接方式,让我们能够将页表页放在物理内存的任何地方。其次,如果仔细构建,页表的每个部分都可以整齐地放入
一页
中,从而更容易管理内存。操作系统可以在需要分配或增长页表时简单地获取下一个空闲页。将它与一个简单的(非分页)线性页表相比①,后者仅是按 VPN 索引的 PTE 数组。用这样的结构,整个线性页表必须连续驻留
在物理内存中。对于一个大的页表(比如 4MB),找到如此大量的、未使用的连续空闲物理内存,可能是一个相当大的挑战。有了多级结构,我们增加了一个间接层(level of indirection),使用了页目录,它指向页表的各个部分。这种间接方式,让我们能够将页表页放在物理内存的任何地方。(上述表达式也能看出稀疏也是
有限制
的,至少被页目录索引 PageDirBase 限制了范围)当然比二级
更多层级
的页表也是允许的多级页表是
有成本
的。在 TLB 未命中
时,需要从内存加载两次
,才能从页表中获取正确的地址转换信息(一次用于页目录,另一次用于 PTE 本身),而用线性页表只需要一次加载。因此,多级表是一个时间—空间折中
(time-space trade-off)的小例子。另一个明显的缺点是
复杂性
。无论是硬件还是操作系统来处理页表查找(在 TLB 未命中时),这样做无疑都比简单的线性页表查找更复杂。详细的多级示例
为了更好地理解多级页表背后的想法,我们来看一个例子。设想一个大小为
16KB
的小地址空间,其中包含 64 个字节的页。因此,我们有一个 14 位的虚拟地址空间,VPN 有 8位,偏移量有 6 位。即使只有一小部分地址空间正在使用,线性页表也会有 28(256)个项。
图 20.3 展示了这种地址空间的一个例子。提示:对复杂性表示怀疑系统设计者应该谨慎对待让系统增加复杂性。好的系统构建者所做的就是:实现最小复杂性的系统,来完成手上的任务。例如,如果磁盘空间非常大,则不应该设计一个尽可能少使用字节的文件系统。同样,如果处理器速度很快,建议在操作系统中编写一个干净、易于理解的模块,而不是 CPU 优化的、手写汇编的代码。注意过早优化的代码或其他形式的不必要的复杂性。这些方法会让系统难以理解、维护和调试。正如 Antoine de Saint-Exupery 的名言:“完美非无可增,乃不可减。”他没有写的是:“谈论完美易,真正实现难。
在这个例子中,虚拟页 0 和 1 用于代码,虚拟页 4 和 5 用于
堆
,虚拟页 254 和 255 用于栈
。地址空间的其余页未被使用。要为这个地址空间构建一个两级页表,我们从完整的线性页表开始,将它分解成页大小的单元。回想一下我们的完整页表(在这个例子中)有 256 个项;假设每个 PTE 的大小是 4个字节。因此,我们的页大小为 1KB(256×4 字节)。鉴于我们有 64 字节的页,1KB 页表可以分为 16 个 64 字节的页,每个页可以容纳 16 个 PTE。
我们现在需要了解:如何获取 VPN,并用它来首先索引到页目录中,然后再索引到页表的页中。请记住,每个都是一组项。因此,我们需要弄清楚,如何为每个 VPN 构建索引。
我们首先索引到页目录。这个例子中的页表很小:256 个项,分布在 16 个页上。页目录需要为页表的每页提供一个项。因此,它有 16 个项。结果,我们需要 4 位 VPN 来索引目录。我们使用 VPN 的前 4 位,如下所示:
一旦从 VPN 中提取了页目录索引(简称 PDIndex),我们就可以通过简单的计算来找到页目录项(PDE)的地址:
这就得到了页目录,现在我们来看它,在地址转换上取得进一步进展。
如果页目录项标记为无效,则我们知道访问无效,从而引发异常。但是,如果 PDE 有效,我们还有更多工作要做。具体来说,我们现在必须从页目录项指向的页表的页中获取页表项(PTE)。要找到这个 PTE,我们必须使用 VPN 的剩余位索引到页表的部分:
这个页表索引(Page-Table Index,PTIndex)可以用来索引页表本身,给出 PTE 的地址:
请注意,从页目录项获得的页帧号(PFN)必须左移到位,然后再与页表索引组合,才能形成 PTE 的地址。
为了确定这一切是否合理,我们现在代入一个包含一些实际值的多级页表,并转换一个虚拟地址。让我们从这个例子的页目录开始(见表 20.2 的左侧)。
在该表中,可以看到每个页目录项(PDE)都描述了有关地址空间页表的一些内容。在这个例子中,地址空间里有两个有效区域(在开始和结束处),以及一些无效的映射。
在物理页 100(页表的第 0 页的物理帧号)中,我们有 1 页,包含 16 个页表项,记录了地址空间中的前 16 个 VPN。请参见表 20.2(中间部分)了解这部分页表的内容。
页表的这一页包含前 16 个 VPN 的映射。在我们的例子中,VPN 0 和 1 是有效的(代码段),4 和 5(堆)也是。因此,该表有每个页的映射信息。其余项标记为无效。
页表的另一个有效页在 PFN 101 中。该页包含地址空间的最后 16 个 VPN 的映射。具体见表 20.2(右侧)。
在这个例子中,VPN 254 和 255(栈)包含有效的映射。希望从这个例子中可以看出,多级索引结构可以节省多少空间。在这个例子中,我们不是为一个线性页表分配完整的 16页,而是分配 3 页:一个用于页目录,两个用于页表的具有有效映射的块。大型(32 位或64 位)地址空间的节省显然要大得多。
最后,让我们用这些信息来进行地址转换。这里是一个地址,指向 VPN 254 的第 0 个字节:
0x3F80
,或二进制的 11 1111 1000 0000
。
回想一下,我们将使用 VPN 的前 4 位来索引页目录。因此,1111 会从上面的页目录中选择最后一个(第 15 个,如果你从第 0 个开始)。这就指向了位于地址 101 的页表的有效页。
然后,我们使用 VPN 的下 4 位(1110)来索引页表的那一页并找到所需的 PTE。1110 是页面中的倒数第二(第 14 个)条,并告诉我们虚拟地址空间的页 254 映射到物理页 55。通过连接 PFN = 55(或十六进制 0x37)和 offset = 000000,可以形成我们想要的物理地址,并向内存系统发出请求:你现在应该知道如何构建两级页表,利用指向页表页的页目录。但遗憾的是,我们的工作还没有完成。我们现在要讨论,有时两个页级别是不够的!
超过两级
在至今为止的例子中,我们假定多级页表只有两个级别:一个页目录和几页页表。在某些情况下,更深的树是可能的(并且确实需要)。
让我们举一个简单的例子,用它来说明为什么更深层次的多级页表可能有用。在这个例子中,假设我们有一个 30 位的虚拟地址空间和一个小的(512 字节)页。因此我们的虚拟地址有一个 21 位的虚拟页号和一个 9 位偏移量。
请记住我们构建多级页表的目标:使页表的每一部分都能放入一个页。到目前为止,我们只考虑了页表本身。但是,如果页目录太大,该怎么办?
要确定多级表中需要多少级别才能使页表的所有部分都能放入一页,首先要确定多少页表项可以放入一页。鉴于页大小为 512 字节,并且假设 PTE 大小为 4 字节,你应该看到,可以在单个页上放入 128 个 PTE。当我们索引页表时,我们可以得出结论,我们需要 VPN的最低有效位 7 位(log2128)作为索引:
在上面你还可能注意到,多少位留给了(大)页目录:14。如果我们的页目录有 214个项,那么它不是一个页,而是 128 个,因此我们让多级页表的每一个部分放入一页目标失败了。为了解决这个问题,我们为树再加一层,将页目录本身拆成多个页,然后在其上添加另一个页目录,指向页目录的页。我们可以按如下方式分割虚拟地址:
现在,当索引上层页目录时,我们使用虚拟地址的最高几位(图中的 PD 索引 0)。该索引用于从顶级页目录中获取页目录项。如果有效,则通过组合来自顶级 PDE 的物理帧号和 VPN 的下一部分(PD 索引 1)来查阅页目录的第二级。最后,如果有效,则可以通过使用与第二级 PDE 的地址组合的页表索引来形成 PTE 地址。这会有很多工作。所有这些只是为了在多级页表中查找某些东西。
地址转换过程:记住 TLB
为了总结使用两级页表的地址转换的整个过程,我们再次以算法形式展示控制流(见
图 20.4)。该图显示了每个内存引用在硬件中发生的情况(假设硬件管理的 TLB)。
从图中可以看到,在任何复杂的多级页表访问发生之前,硬件首先检查 TLB。在命中
时,物理地址直接形成,而不像之前一样访问页表。只有在 TLB 未命中时,硬件才需要执
行完整的多级查找。在这条路径上,可以看到传统的两级页表的成本:两次额外的内存访
问来查找有效的转换映射。
反向页表
在反向页表(inverted page table)中,可以看到页表世界中更极端的空间节省。
保留了一
个页表
,其中的项代表系统的每个物理页
,而不是有许多页表(系统的每个进程一个)。页表项告诉我们哪个进程
正在使用此页,以及该进程的哪个虚拟页
映射到此物理页。
现在,要找到正确的项,就是要搜索这个数据结构。线性扫描是昂贵的,因此通常在此基础结构上建立散列表
,以加速查找。PowerPC 就是这种架构[JM98]的一个例子。
更一般地说,反向页表说明了我们从一开始就说过的内容:页表只是数据结构。你可以对数据结构做很多疯狂的事情,让它们更小或更大,使它们变得更慢或更快。多层和反向页表只是人们可以做的很多事情的两个例子。将页表交换到磁盘
到目前为止,我们一直假设页表位于内核拥有的物理内存中。即使我们有很多技巧来减小页表的大小,但是它仍然有可能是太大而无法一次装入内存。
一些系统将这样的页表放入
内核虚拟内存
(kernel virtual memory),从而允许系统在内存压力较大时,将这些页表中的一部分交换
(swap)到磁盘。小结
真正的页表不一定只是线性数组,而是更复杂的数据结构。这样的页表体现了
时间和空间上的折中
(表格越大,TLB 未命中可以处理得更快,反之亦然),因此结构的正确选择强烈依赖于给定环境的约束。
在一个内存受限
的系统中(像很多旧系统一样),小结构是有意义的。在具有较多内存,并且工作负载主动使用大量内存页的系统中,用更大的页表
来加速 TLB 未命中处理
,可能是正确的选择。有了软件管理的 TLB,数据结构的整个世界开放给了喜悦的操作系统创新者(提示:就是你)。参考
- 作者:GJJ
- 链接:https://blog.gaojj.cn/article/blog-25
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。