跳转至

页表、TLB 与缺页

分页的抽象很简单:虚拟页号映射到物理页框号。但若为巨大虚拟地址空间保存一张平面数组,页表本身会消耗大量内存;若每次访存都先访问页表,性能又难以接受。多级页表和 TLB 分别解决空间与时间问题。

平面页表为什么太大

设虚拟地址宽度为 \(V\) 位,页内偏移为 \(p\) 位,则虚拟页数量为:

\[ N_{pages}=2^{V-p}. \]

若每个页表项大小为 \(E\) 字节,平面页表大小:

\[ S=2^{V-p}E. \]

例如 48 位有效虚拟地址、4 KiB 页、8 字节页表项:

\[ S=2^{48-12}\times8=512\ \text{GiB}. \]

每个进程都分配如此大的连续页表显然不可行,而多数进程只使用稀疏区域。

多级页表

多级页表把虚拟页号拆成多个索引,只为实际使用的分支分配下级表。

以概念上的四级结构为例:

虚拟地址
  | level-1 index
  | level-2 index
  | level-3 index
  | level-4 index
  | page offset

硬件从页表根开始,逐级读取页表项,最终得到物理页框。大块未映射区域只需上层无效项,不需要完整下级表。

代价是页表遍历需要多次内存访问。层数越多,稀疏空间开销越低,但 TLB miss 后的遍历路径更长。

TLB:翻译缓存

TLB 缓存近期虚拟页到物理页框的翻译和权限。CPU 访存时先查询 TLB:

  • 命中:直接组合物理页框与偏移。
  • 未命中:由硬件或软件遍历页表,填充 TLB 后重试。
  • 权限不满足或页表项无效:触发异常。

TLB 是缓存,因此有容量、组相联和替换问题。它缓存的是地址翻译,不是用户数据本身。

有效访问时间

用简化模型说明 TLB 的价值。设 TLB 查询时间为 \(t_T\),内存访问时间为 \(t_M\),单级页表,命中率为 \(h\)

\[ EAT=h(t_T+t_M)+(1-h)(t_T+2t_M). \]

未命中时多一次页表内存访问。真实多级页表、并行查找、缓存页表项和乱序执行会让模型更复杂,但结论不变:高 TLB 命中率对性能重要。

上下文切换与 TLB

不同进程可用同一虚拟页号映射不同物理页。切换地址空间时,旧 TLB 项不能误用于新进程。

处理方式包括:

  • 切换时刷新相关 TLB 项。
  • 给 TLB 项附加地址空间标识 ASID/PCID,使多个进程翻译共存。
  • 标记某些内核映射为全局项。

标识数量有限时仍需回收和刷新。多核系统修改页表后,还可能需要通知其他 CPU 失效其旧翻译,这就是 TLB shootdown,代价包括跨核通信和同步。

TLB miss 不等于缺页

这是最常见混淆之一:

TLB miss:翻译不在 TLB,但页表可能已有有效映射
page fault:当前访问不能由现有有效权限映射直接完成

TLB miss 通常由页表遍历解决,不需要磁盘,也不一定进入完整内核缺页处理。缺页异常可能只是分配零页或处理写时复制,也不一定访问磁盘。

缺页类型

合法的按需缺页

地址属于已建立虚拟区域,但物理页尚未分配。内核分配清零页、建立映射并重试指令。

文件页或交换页不驻留

需要从存储读取,线程通常睡眠等待 I/O。这类主要缺页延迟远高于纯内存处理。

写时复制缺页

页面存在但只读,写入方具有 COW 语义。内核复制页面并授予写权限。

权限或非法地址错误

访问不属于合法虚拟区域,或请求执行/写入权限不允许。内核无法透明修复,通常向进程报告错误信号。

“major/minor fault”的精确定义依系统而异,常见直觉是主要缺页需要存储 I/O,次要缺页可在内存中解决。

缺页处理路径

  1. CPU 保存出错指令地址、访问类型和异常原因。
  2. 内核根据故障虚拟地址查找所属虚拟内存区域。
  3. 检查地址范围和读写执行权限。
  4. 判断是按需分配、文件读取、交换换入、COW 还是非法访问。
  5. 必要时分配页框或启动 I/O;等待期间线程可阻塞。
  6. 更新页表和相关反向映射、计数与权限。
  7. 使旧 TLB 翻译失效。
  8. 返回并重新执行引发缺页的指令。

缺页处理器必须小心递归故障:内核在处理缺页时访问的元数据本身应处于可用状态,关键路径不能任意再次缺页或睡眠。

一个地址拆分例子

假设 32 位虚拟地址、4 KiB 页、两级页表,每级索引 10 位:

31          22 21          12 11           0
+--------------+--------------+--------------+
| directory 10 | page table 10| offset 12    |
+--------------+--------------+--------------+

每张下级页表有 \(2^{10}=1024\) 项。若页表项 4 字节,则一张下级表刚好 4 KiB,占一页。只为有映射的 4 MiB 虚拟区域分配对应下级表。

反向映射与页面回收

页表从虚拟地址找到物理页。页面回收却需要反过来知道“这个物理页被哪些地址空间映射”,以便撤销映射、写回或迁移。系统因此维护物理页元数据和某种反向映射信息。

共享页可能被许多进程映射,撤销或修改权限要同步多个页表和多个 CPU 的 TLB。虚拟内存性能问题常来自这些跨对象、跨核关系,而不只是一次哈希查找。

大页与多种页尺寸

多级页表的某一级可以直接映射更大物理区域,形成大页。大页减少遍历层级和 TLB 压力,但要求更大连续物理区域,回收、迁移和 COW 的粒度也更大。

透明大页尝试由系统自动合并和拆分,降低应用改造成本。自动策略并非总是收益:内存紧张、短生命周期或稀疏访问时,整理和缺页成本可能增加尾延迟。

页表安全

页表决定用户能访问什么,是关键保护结构。内核必须防止:

  • 用户映射内核物理页。
  • 可写页被意外同时作为特权代码执行。
  • 释放页仍保留可访问翻译。
  • 权限收紧后其他 CPU 继续使用旧 TLB 项。
  • 推测执行或微架构状态泄露跨域数据。

页表权限是必要边界,但现代系统还需针对侧信道和推测执行漏洞采取额外隔离与刷新策略。

观察与诊断

看到缺页数量高时,应继续区分:

  • 是进程启动阶段的正常按需加载,还是持续发生?
  • 是次要缺页、COW,还是需要 I/O 的主要缺页?
  • 工作集是否超过可用内存?
  • 是否因随机访问大数据导致 TLB miss?
  • 大页、预取或数据布局是否适合该访问模式?

单一“faults per second”无法直接证明内存不足。

自测

  1. 多级页表怎样节省稀疏地址空间的内存?
  2. TLB miss、次要缺页和主要缺页的成本为何不同?
  3. 地址空间标识怎样减少上下文切换后的 TLB 刷新?
  4. 修改页表后为什么可能要向其他 CPU 发送失效通知?
  5. 缺页处理完成后为何通常重试原指令?
参考思路

未使用的上层分支无需分配下级表。TLB miss 多为页表遍历,次要缺页在内存解决,主要缺页需存储 I/O。ASID 区分不同地址空间的翻译。其他 CPU 可能缓存旧项。合法缺页补齐了执行原指令所需映射,因此可以重试。