3. 多级页表结构:x86_64 四级页表、ARMv8 页表、页表层级与地址翻译
多级页表是现代操作系统实现虚拟内存的核心数据结构。它解决了线性页表占用连续大块物理内存的问题,通过层级索引将页表拆分为多个小粒度页面,按需分配。本章深入剖析 x86_64 的四级页表与 ARMv8 的多级页表结构,并展示地址翻译的完整路径。
3.1 多级页表的设计动机
在 64 位地址空间中,若使用单级页表(线性页表),每个进程需要维护一个包含所有虚拟页到物理页映射的数组。以 4KB 页、48 位虚拟地址为例,页表项数量为 2³⁶(约 687 亿项),每项 8 字节,单张页表需占用 512GB 内存,这在物理上不可行。
多级页表通过以下方式解决该问题:
- 按需分配:仅对实际使用的虚拟地址范围分配页表层级节点。
- 层级索引:将虚拟地址拆分为多个位段,每段索引一级页表。
- 共享大页:高层级页表项可直接映射大块连续内存(2MB、1GB),减少页表层级深度。
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 位 | 页内偏移 | 定位物理页内的字节 |
地址翻译流程:
- 从 CR3 寄存器获取 PML4 表的物理基地址。
- 使用虚拟地址的 PML4 索引(位 47:39)在 PML4 表中查找表项,获取 PDPT 表的物理地址。
- 使用 PDPT 索引(位 38:30)在 PDPT 表中查找表项,获取 PD 表的物理地址。
- 使用 PD 索引(位 29:21)在 PD 表中查找表项,获取 PT 表的物理地址。
- 使用 PT 索引(位 20:12)在 PT 表中查找表项,获取物理页帧号。
- 将物理页帧号与页内偏移(位 11:0)拼接,得到最终物理地址。
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) |
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 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
通过分析页表遍历事件,可以判断当前工作负载的页表层级效率,并指导大页配置或内存布局优化。