4. 硬件基础:MMU(内存管理单元)地址转换机制(页表遍历)

MMU(Memory Management Unit,内存管理单元)是 CPU 内部负责将虚拟地址(VA)转换为物理地址(PA)的硬件模块。在 Linux 内核启动早期,MMU 处于关闭状态,CPU 直接使用物理地址执行指令;当内核完成页表初始化并开启 MMU 后,所有地址访问都必须经过页表遍历才能完成转换。页表遍历的效率直接决定了 TLB 缺失代价、内存访问延迟以及内核关键路径的性能。

4.1 页表遍历的基本流程

ARM64 架构采用4级页表(或 3 级/2 级,取决于页面大小和配置),每一级页表都是一个页表项(PTE)数组。MMU 从虚拟地址中提取各级索引,逐级查找物理地址。以 4KB 页面、4 级页表为例,遍历过程如下:

  1. TLB 查找:MMU 首先检查 TLB(Translation Lookaside Buffer)中是否缓存了该虚拟地址的转换结果。若命中(TLB hit),直接返回物理地址,无需遍历页表。
  2. 页表基址寄存器:若 TLB 未命中(TLB miss),MMU 读取当前进程的页表基址寄存器(如 ARM64 的 TTBR0_EL1),获得PGD(Page Global Directory)的物理基地址。
  3. 逐级索引提取:从虚拟地址中依次提取 PGD 索引、PUD 索引、PMD 索引、PTE 索引。
  4. 内存访问:MMU 对每一级页表执行读内存操作,获取下一级页表的物理地址或最终物理页帧号。
  5. 权限与属性检查:在最后一级 PTE 中,MMU 检查访问权限(读/写/执行)、用户/内核空间、缓存属性(可缓存/不可缓存、回写/直写)等。
  6. 结果填充 TLB:转换成功后,MMU 将结果写入 TLB,供后续快速查找。
性能关键点:一次 TLB miss 需要 4 次(4 级页表)或 3 次(3 级页表)内存访问才能完成地址转换。如果这些页表本身不在 CPU 缓存中(即页表缓存未命中),则每次页表访问都会触发一次完整的 cache line 填充,导致数十甚至上百个周期的延迟。

4.2 页表遍历的硬件加速机制

现代 CPU 通过以下硬件特性降低页表遍历的开销:

硬件特性 作用 对性能的影响
TLB(转换后备缓冲器) 缓存最近使用的 VA→PA 转换结果 命中率 >99% 时,地址转换几乎零开销;miss 时触发页表遍历
页表缓存(Page Table Cache) 缓存页表项本身(非转换结果) 减少页表遍历时的内存读取延迟
预取(Page Table Walker Prefetch) 硬件自动预取下一级页表 降低连续缺页或大块内存访问时的遍历延迟
大页(Huge Pages) 使用 2MB/1GB 页面,减少页表级数 从 4 级降为 3 级或 2 级,减少 TLB miss 后的遍历次数
ASID(地址空间标识符) 区分不同进程的 TLB 条目 避免进程切换时完全冲刷 TLB,减少 miss 率

4.3 页表遍历的延迟模型

一次完整的页表遍历延迟可以用以下公式估算:

遍历延迟 = TLB miss 延迟 + Σ(各级页表访问延迟)

其中:
- TLB miss 延迟 ≈ 8~15 个周期(硬件 walker 启动开销)
- 各级页表访问延迟 = 内存访问延迟(L1/L2/L3/DRAM)
  - L1 命中:3~5 周期
  - L2 命中:10~15 周期
  - L3 命中:30~50 周期
  - DRAM 访问:100~300 周期
避坑提示:在 Linux 内核启动早期,MMU 初始化阶段通常使用恒等映射(identity mapping),即虚拟地址等于物理地址。此时页表遍历虽然存在,但映射关系简单,且内核代码段通常被映射为不可缓存(Device-nGnRnE)或正常内存可缓存。如果页表属性配置错误(例如将页表本身映射为不可缓存),会导致每次页表遍历都直接访问 DRAM,启动速度急剧下降。

4.4 页表遍历的 SVG 流程图

下图展示了 ARM64 4 级页表遍历的完整路径,从虚拟地址输入到物理地址输出:

ARM64 4级页表遍历流程图 ARM64 4级页表遍历流程 虚拟地址 (VA) 输入 TLB 查找 TLB 命中? 物理地址输出 PGD 页表查找 PUD 页表查找 PMD 页表查找 PTE 页表查找 权限与属性检查 物理地址输出 说明 • 每级页表访问 = 1次内存读取 • 4级页表 = 4次内存访问 • 若页表不在缓存中,延迟显著增加

4.5 页表遍历与缓存系统的交互

页表遍历过程中,MMU 需要读取页表项数据。这些数据本身存储在内存中,因此页表遍历的性能高度依赖于缓存层次结构:

  • 页表项在 L1 缓存中:遍历延迟约 10~20 周期,几乎无性能损失。
  • 页表项在 L2 缓存中:遍历延迟约 30~50 周期,可接受。
  • 页表项在 L3 缓存中:遍历延迟约 80~120 周期,开始影响性能。
  • 页表项在 DRAM 中:遍历延迟约 300~600 周期,性能严重下降。
优化建议:在 Linux 内核启动阶段,页表通常位于内核映像的固定位置。如果内核映像被映射为回写(Write-Back)且可缓存(Cacheable),则页表遍历可以充分利用 CPU 缓存。建议将页表所在的内存区域配置为正常内存(Normal Memory)属性,并确保其位于直接映射区域内,避免额外的地址转换开销。

4.6 不同页面大小下的页表级数对比

页面大小 页表级数 TLB 覆盖范围 遍历内存访问次数 典型场景
4KB 4 级(PGD→PUD→PMD→PTE) 较小 4 次 通用内核、进程默认页表
64KB 3 级(PGD→PMD→PTE) 中等 3 次 服务器、大内存系统
2MB(大页) 3 级(PGD→PUD→PMD,PMD 直接映射) 较大 2 次(跳过 PTE) 数据库、HPC、虚拟化
1GB(巨页) 2 级(PGD→PUD,PUD 直接映射) 极大 1 次(跳过 PMD 和 PTE) 大型数据库、科学计算

4.7 页表遍历对内核启动性能的影响

在 Linux 内核启动过程中,MMU 初始化阶段需要完成以下与页表遍历相关的操作:

  1. 创建恒等映射页表:在开启 MMU 前,内核需要为当前执行地址创建恒等映射,确保开启 MMU 后指令指针(PC)仍然有效。
  2. 创建内核线性映射:将整个物理内存映射到内核虚拟地址空间(如 ARM64 的线性映射区域)。
  3. 创建设备 I/O 映射:为外设寄存器创建不可缓存的映射。

这些操作涉及大量的页表项写入和 TLB 维护指令(如 tlbi、dsb、isb)。如果页表遍历效率低下,会导致以下性能问题:

  • TLB 抖动:频繁的 TLB 失效和重填,增加启动延迟。
  • 页表缓存污染:大量页表项写入导致缓存行失效,影响后续正常内存访问。
  • 指令预取中断:MMU 配置变更时,CPU 流水线可能被冲刷,降低指令执行效率。
性能指标参考:在 ARM64 平台上,一次完整的 4 级页表遍历(所有页表项均在 L2 缓存中)大约需要 40~60 个 CPU 周期。如果页表项在 DRAM 中,延迟可高达 300~500 周期。内核启动阶段通常有数千次页表遍历操作,优化页表缓存命中率可以节省 数十万甚至上百万个周期。

4.8 页表遍历的软件优化策略

内核开发者可以通过以下方式减少页表遍历对性能的影响:

  • 使用大页映射:减少页表级数,降低 TLB miss 后的遍历次数。
  • 页表预分配:在启动早期预先分配并填充页表,避免运行时动态分配导致的额外延迟。
  • 缓存对齐:将页表结构对齐到缓存行边界,减少缓存行分裂。
  • 批量 TLB 维护:合并多个 TLB 失效操作,减少同步指令开销。
  • 页表属性优化:确保页表所在内存区域被映射为可缓存且回写属性,避免不可缓存访问。