3. 多级页表结构:x86_64 四级页表、ARMv8 页表、页表层级与地址翻译

多级页表是现代操作系统实现虚拟内存的核心数据结构。它解决了线性页表占用连续大块物理内存的问题,通过层级索引将页表拆分为多个小粒度页面,按需分配。本章深入剖析 x86_64 的四级页表与 ARMv8 的多级页表结构,并展示地址翻译的完整路径。

3.1 多级页表的设计动机

在 64 位地址空间中,若使用单级页表(线性页表),每个进程需要维护一个包含所有虚拟页到物理页映射的数组。以 4KB 页、48 位虚拟地址为例,页表项数量为 2³⁶(约 687 亿项),每项 8 字节,单张页表需占用 512GB 内存,这在物理上不可行。

多级页表通过以下方式解决该问题:

  • 按需分配:仅对实际使用的虚拟地址范围分配页表层级节点。
  • 层级索引:将虚拟地址拆分为多个位段,每段索引一级页表。
  • 共享大页:高层级页表项可直接映射大块连续内存(2MB、1GB),减少页表层级深度。
性能要点: 多级页表减少了 TLB 未命中时的内存访问次数,但每次地址翻译仍需多次内存读取(页表遍历)。页表层级越深,遍历开销越大,这是页表性能优化的核心矛盾。

3.2 x86_64 四级页表结构

x86_64 架构使用 48 位虚拟地址,采用四级页表结构:PML4(Page Map Level 4)、PDPT(Page Directory Pointer Table)、PD(Page Directory)、PT(Page Table)。每级页表包含 512 个表项(9 位索引),页表项大小为 8 字节,因此每级页表占 4KB(一个物理页)。

虚拟地址的位段划分如下:

位段 位范围 名称 作用
63:48 16 位 符号扩展 必须与第 47 位相同,否则触发异常
47:39 9 位 PML4 索引 选择 PML4 表中的表项
38:30 9 位 PDPT 索引 选择 PDPT 表中的表项
29:21 9 位 PD 索引 选择 PD 表中的表项
20:12 9 位 PT 索引 选择 PT 表中的表项
11:0 12 位 页内偏移 定位物理页内的字节

地址翻译流程:

  1. 从 CR3 寄存器获取 PML4 表的物理基地址。
  2. 使用虚拟地址的 PML4 索引(位 47:39)在 PML4 表中查找表项,获取 PDPT 表的物理地址。
  3. 使用 PDPT 索引(位 38:30)在 PDPT 表中查找表项,获取 PD 表的物理地址。
  4. 使用 PD 索引(位 29:21)在 PD 表中查找表项,获取 PT 表的物理地址。
  5. 使用 PT 索引(位 20:12)在 PT 表中查找表项,获取物理页帧号。
  6. 将物理页帧号与页内偏移(位 11:0)拼接,得到最终物理地址。
优化提示: 如果 PD 表项设置了 PS(Page Size)位(大页标志),则跳过 PT 层级,直接映射 2MB 页面。PDPT 表项也可设置 PS 位映射 1GB 页面。大页可减少页表层级深度和 TLB 压力。
x86_64 四级页表地址翻译流程 x86_64 四级页表地址翻译流程 虚拟地址 (48位) PML4 (9位) PDPT (9位) PD (9位) PT (9位) 偏移 (12位) PML4 表 (512项) PDPT 表 (512项) PD 表 (512项) PT 表 (512项) 物理页 (4KB) 物理地址 = 物理页帧号 + 页内偏移 说明:每级页表占用一个物理页(4KB),包含512个8字节表项。CR3寄存器指向当前进程的PML4表基地址。 大页(PS位)可跳过PT或PD层级,减少遍历深度。

3.3 ARMv8 页表结构

ARMv8-A 架构支持 4KB、16KB 和 64KB 页面粒度,以及 4 级或 3 级页表(取决于虚拟地址宽度和页面大小)。以最常见的 4KB 页面、48 位虚拟地址为例,ARMv8 使用四级页表:L0(Page/Granule Table)、L1、L2、L3。每级页表包含 512 个表项(9 位索引),表项大小为 8 字节。

ARMv8 页表与 x86_64 的关键差异:

特性 x86_64 ARMv8
页表层级命名 PML4 → PDPT → PD → PT L0 → L1 → L2 → L3
虚拟地址宽度 48 位(用户态) 48 位(用户态),支持 52 位扩展
页表项格式 统一格式,PS 位控制大页 描述符类型(块/表)控制层级
大页支持 2MB(PD 级)、1GB(PDPT 级) 2MB(L2 块)、1GB(L1 块)、512MB(L0 块)
页表基址寄存器 CR3 TTBR0_EL1(用户态)、TTBR1_EL1(内核态)
地址空间标识 PCID(Process Context ID) ASID(Address Space ID)
性能陷阱: ARMv8 的页表遍历在 L1 或 L2 层级遇到“块描述符”(Block Descriptor)时会提前终止,直接映射大块内存。如果页表配置不当(例如频繁使用 4KB 页面),会导致页表层级深度增加,TLB 未命中时遍历开销显著上升。
ARMv8 四级页表结构 ARMv8 四级页表结构(4KB 页面,48位虚拟地址) 虚拟地址 (48位) L0 (9位) L1 (9位) L2 (9位) L3 (9位) 偏移 (12位) L0 表 (512项) L1 表 (512项) L2 表 (512项) L3 表 (512项) 物理页 (4KB) L1 块描述符 → 1GB 页面 L2 块描述符 → 2MB 页面 ARMv8 页表项包含“描述符类型”字段:表描述符(继续遍历)或块描述符(直接映射大页)。

3.4 页表层级与地址翻译性能

页表层级深度直接影响地址翻译的延迟。每次页表遍历需要多次内存访问(每级一次),而内存访问本身存在数十纳秒的延迟。TLB 命中可完全避免遍历,但 TLB 未命中时,遍历开销成为性能瓶颈。

不同页面大小下的遍历深度对比:

页面大小 x86_64 遍历层级 ARMv8 遍历层级 内存访问次数 适用场景
4KB 4 级(PML4→PDPT→PD→PT) 4 级(L0→L1→L2→L3) 4 次 通用、细粒度内存管理
2MB 3 级(PML4→PDPT→PD,PS 位) 3 级(L0→L1→L2,块描述符) 3 次 数据库、大内存分配
1GB 2 级(PML4→PDPT,PS 位) 2 级(L0→L1,块描述符) 2 次 虚拟化、HPC、大页池
性能优化策略:
  • 大页(Huge Pages):减少页表层级深度,降低 TLB 未命中时的遍历开销。x86_64 支持 2MB(透明大页)和 1GB(显式大页)。
  • 页表缓存(Page Table Cache):部分 CPU 微架构会缓存最近使用的页表项,减少遍历次数。
  • TLB 预取:现代 CPU 在发生 TLB 未命中时,会预取相邻页表项,减少后续未命中。
  • 页表锁定:将频繁访问的页表项锁定在 L1/L2 缓存中,减少内存访问延迟。

3.5 页表遍历的硬件加速

现代 CPU 通过硬件页表遍历器(Hardware Page Table Walker)自动完成多级页表查找,无需软件介入。当 TLB 未命中时,硬件遍历器自动从 CR3/TTBR 寄存器读取基址,按虚拟地址位段逐级查找,并将结果填充到 TLB 中。

硬件遍历器的性能特征:

  • 并行遍历:部分 CPU 支持同时进行多个页表遍历(例如 Intel 的并行页表遍历器)。
  • 遍历缓存:微架构内部缓存最近使用的页表项(如 Intel 的 Paging Structure Caches),减少内存访问。
  • 预取:遍历器在访问一级页表时,会预取相邻表项到缓存中。
Perf 观测点: 使用 perf stat -e dTLB-load-misses,iTLB-load-misses,page_walker_loads 可以观测 TLB 未命中次数和页表遍历次数。高遍历次数通常意味着页表层级过深或大页使用不足。
# 使用 perf 统计页表遍历事件
perf stat -e dTLB-load-misses,page_walker_loads,page_walker_refs ./workload

# 输出示例
# 1,234,567  dTLB-load-misses
# 2,345,678  page_walker_loads
# 3,456,789  page_walker_refs

通过分析页表遍历事件,可以判断当前工作负载的页表层级效率,并指导大页配置或内存布局优化。