6.1810 / 6.1810 · 第 9 课 · rtm

透明大页

现代操作系统如何自动在 4KB 基础页与 MB 级超级页之间做出选择?本节课以 Navarro 等人(OSDI 2002)的经典论文为核心,探讨内核对透明大页(Transparent Superpages)的支持。

授课教师:rtm

日期:2025 年春季

难度:中等

阅读:Practical, Transparent O/S Support for Superpages (OSDI 2002)

经典操作系统分页

回顾:按需分配(Lazy Allocation)

上节课的重要结论:现代操作系统中,页分配通常是惰性的——远比 xv6 更加惰性:

好处是透明的快速启动:程序可以放心申请大量内存,而无需担心物理 RAM 的大小。

分页的初衷:让程序拥有比 RAM 更大的内存

分页(Paging)也称"换页到磁盘"(Paging to Disk),其历史可以追溯到内存非常昂贵的年代。即便在今天,Linux 仍然支持换页到磁盘,原因包括:

换页到磁盘的工作原理

假设一台机器只有 2 页物理内存,但程序想使用 4 页数据:

组件 内容
页表4 个 PTE
物理 RAM2 页(初始空闲)
磁盘4 页交换空间 + 1 页可执行文件

处理流程:

  1. 读 va 0 缺页:内核分配物理页 #1,从可执行文件读入,建立 PTE0 → pa1 映射
  2. 写 va 2 缺页:分配物理页 #0,置零,PTE2 → pa0
  3. 写 va 1 缺页:无空闲 RAM!选择一个非空闲物理页(如 pa0),将其内容写回磁盘(驱逐/换出),再初始化给 va 1
  4. 再次访问 va 2:需驱逐 pa1(va 0 的内容),再从磁盘换入 va 2

我们实际上把 RAM 当作磁盘的缓存,磁盘充当"后备存储"。

驱逐策略至关重要

如果驱逐的页面很快就会被使用,将引发又一次缺页和磁盘读取。一次磁盘读取约需 1 毫秒——相当于一百万条指令的时间!糟糕的驱逐策略会导致大量耗时的缺页异常。

常见驱逐策略

LRU(最近最少使用)

内核维护一个包含所有物理页号的 LRU 列表:

FreeBSD 的多列表方案(论文 §5.1)

三个 LRU 列表:

列表作用
Active最近被访问过的页面
Inactive从 Active 降级、尚未写回的页面
Cache已清除脏位、长时间未使用的干净页面

Cache 列表中的页面可直接用于缺页分配,保持 Cache 列表非空对快速处理缺页至关重要。

为什么驱逐干净页优于脏页?

干净页在磁盘上已有副本(来自之前的换出或只读可执行文件),重用前无需写回磁盘;脏页则必须先写回。驱逐干净页可显著降低缺页延迟。

LRU 是最好的策略吗?

LRU 本质上是一种预测:最近使用的页面很可能很快再次使用。多数情况下效果很好,但某些场景下表现极差:

反例:对巨大内存空间的重复顺序扫描——程序几乎会在每个页面上缺页,而 LRU 恰好会驱逐程序即将访问的页面!因此,某些系统采用随机替换策略。


TLB 与超级页动机

什么是 TLB?

Translation Lookaside Buffer (TLB) 是 CPU 内部的虚拟地址到物理地址转换的缓存,本质上是一张 (va, pa) 表。

为什么需要 TLB?

每次内存引用(加载、存储、指令取指)都需要 va→pa 转换。CPU 需要大约每周期完成一次(纳秒级)。

TLB 很小

由于必须极快,TLB 通常只有很少的条目:

TLB 的"覆盖范围"(Reach)

128 条目 × 4KB 页 = 半兆字节!
1024 条目 × 4KB 页 = 4 兆字节

对于拥有 16GB+ RAM 的机器和程序来说微不足道。任何常规使用超过此范围的程序都会 TLB 缺失。

TLB 缺失的代价

大页提升 TLB 覆盖范围

128 个 TLB 条目 × 2MB 页 = 1/4 GB!现代 CPU 确实拥有至少这么大的超级页。

大页的代价:内存浪费

如果程序只使用每个大页的一小部分,会浪费大量 RAM。因此,能够混合使用小页和大页是有吸引力的。


今日论文:透明大页支持

目标:内核自动在基础页和超级页之间选择,以提升 TLB 覆盖范围、减少 TLB 缺失的浪费时间。需要适应程序特征(包括行为变化)、兼顾大小程序、应对松紧内存压力,并透明地完成。

给进程分配 2MB 超级页本质上是一种预测:进程将在不久的将来较密集地使用这 2MB 空间。预测错误 → 浪费 RAM。核心张力:更少 TLB 缺失 vs. 浪费 RAM。

三个设计难题

1. 何时给应用分配超级页?

策略优点缺点
sbrk() 调用时 — 难以从参数猜测情况;sbrk(2MB) 不一定意味着会全部使用
2MB 区域内首次缺页时 知道它至少会用一些 RAM;立即获益 可能只用一小部分,浪费 RAM;假设超级页通常合适
区域内所有页都缺页时(论文方案) 确定使用了 100% 的 2MB 在超级页生效前让程序运行了相当长时间

2. 如何找到合适的 2MB 物理内存区域?

必须满足:连续、2MB 对齐、空闲。物理内存通常是"碎片化"的——已用和空闲页交错排列。

论文的精妙分配方案(§4.1)

  1. 新的 2MB 区域首次缺页时,创建一个普通 PTE,仅分配和初始化那一页
  2. 同时从 LRU Cache 列表预留 2MB 的页面(干净、近期未使用)
  3. 这些预留页暂不分配/初始化,仍保留其缓存内容
  4. 随着区域内更多页缺页,逐步分配预留页
  5. 当 2MB 区域内所有普通页都缺页后,提升为超级页

为什么这种安排是好的?

如果程序只使用 2MB 的一部分,预留页仍可作为缓存页被原始映射重用,避免了内存浪费。只有当程序确实使用整个 2MB 时,才提升为超级页。

3. 何时降级(Demote)超级页?

内存紧张时,小页可能占用更少 RAM。但:

方案:对超级页设置读保护,首次写入时降级,以获得更细粒度的 Dirty 位。

性能表现

论文 Table 1 显示,在各种真实基准测试中,超级页通常带来约 10% 的性能提升,有时更高。但请注意,该表的前提是"内存充足"。

现状

核心问题

收益往往相当适中,但代价(内存浪费)可能非常大。这正是透明大页设计持续演进的根源。


测验

Q1:TLB 的"覆盖范围"(reach)是什么?

Q2:为什么使用大页(超级页)?

Q3:论文的超级页分配方案中,"预留"页面但不立即分配的好处是什么?

Q4:为什么 LRU 页面置换策略对"重复顺序扫描巨大内存"的程序表现很差?

Q5:为什么内核有时需要降级(demote)超级页?

要点总结

TLB 是性能瓶颈

TLB 缺失代价极高(约 100 倍指令时间),大页能指数级提升覆盖范围。

预测与权衡

分配超级页是对程序行为的预测——预测错误会浪费 RAM,预测正确减少 TLB 缺失。

碎片化难题

物理内存碎片化使连续 2MB 空闲区域难以找到,需要预留或压缩策略。

仍在演进

透明大页设计持续变化,收益适中但代价可能很大,许多应用选择显式管理。

延伸阅读