3. ARMv8-A 页表层级结构

ARMv8-A 架构采用多级页表(Multi-level Page Table)将虚拟地址(VA)转换为物理地址(PA)。页表的层级数及每级索引的位数,由页面大小(Page Size)和虚拟地址宽度共同决定。本章深入剖析 4 级页表(L0/L1/L2/L3)的硬件遍历过程、页表项(PTE)格式以及不同页面大小的配置差异。

核心概念:页表层级越多,单级页表占用的连续内存越小,但地址转换的访存次数也越多。ARMv8-A 支持 4KB、16KB、64KB 三种页面大小,分别对应不同的页表层级结构。

3.1 4 级页表结构(L0 → L1 → L2 → L3)

当使用 4KB 页面且虚拟地址宽度为 48 位时,ARMv8-A 采用标准的 4 级页表。虚拟地址被划分为 5 个部分:L0 索引、L1 索引、L2 索引、L3 索引和页内偏移(Page Offset)。

下图展示了 4 级页表的硬件遍历流程:

ARMv8-A 4级页表遍历流程 ARMv8-A 4级页表硬件遍历流程(4KB页面,48位VA) VA[47:39] VA[38:30] VA[29:21] VA[20:12] VA[11:0] PA[11:0] L0索引 L1索引 L2索引 L3索引 页内偏移 L0页表(TTBR0_EL1) L1页表 L2页表 L3页表 物理页(4KB) 说明:MMU从TTBR0_EL1(或TTBR1_EL1)获取L0基地址,逐级索引直到L3获取最终物理地址。
关键点:MMU 从 TTBR0_EL1(用户空间)或 TTBR1_EL1(内核空间)获取 L0 页表基地址。每级页表项指向下一级页表的物理基地址,直到 L3 页表项指向最终的 4KB 物理页。

3.2 页表项(PTE)格式

每个页表项(Page Table Entry, PTE)占用 8 字节(64 位)。其格式根据页表层级不同略有差异,但核心字段保持一致。下表列出了 L0/L1/L2 描述符(指向下一级页表)和 L3 描述符(指向物理页)的通用格式:

位域 宽度(位) 描述
[63:48] 16 物理地址高位(PA[47:32]),用于扩展物理地址
[47:12] 36 物理地址低位(PA[31:0]),4KB 对齐
[11:10] 2 访问权限(AP)位:控制读/写权限
[9] 1 非安全(NS)位:1=非安全空间
[8] 1 访问标志(AF):1=已访问,0=未访问(触发缺页异常)
[7:6] 2 共享属性(SH):00=非共享,01=不可预测,10=外部共享,11=内部共享
[5:4] 2 内存属性(AttrIndx):指向 MAIR_EL1 中的属性索引
[3] 1 非叶(nG):1=全局映射,0=非全局(进程特有)
[2] 1 可执行(UXN/PXN):0=可执行,1=不可执行
[1] 1 可写(Rd/Wr):0=只读,1=读写
[0] 1 有效位(Valid):0=无效,1=有效
注意:L0/L1/L2 描述符的位 [1] 和 [2] 含义不同:对于页表描述符,位 [1] 为 0 表示页表,为 1 表示块映射(Block Mapping);位 [2] 用于指示是否允许执行。L3 描述符的位 [1] 固定为 1,表示页面映射。

3.3 页面大小与层级关系

ARMv8-A 支持三种页面大小:4KB、16KB 和 64KB。页面大小直接影响页表层级数、每级索引位数以及块映射(Block Mapping)的大小。下表总结了不同配置下的关键参数:

页面大小 虚拟地址宽度 页表层级数 L0索引位数 L1索引位数 L2索引位数 L3索引位数 页内偏移位数
4KB 48 位 4 级 9 位 9 位 9 位 9 位 12 位
16KB 48 位 3 级 — 13 位 13 位 13 位 14 位
64KB 48 位 2 级 — — 14 位 14 位 16 位

从上表可以看出:

  • 4KB 页面:使用完整的 4 级页表,每级索引 9 位,共 512 个表项。页内偏移 12 位,覆盖 4KB 范围。
  • 16KB 页面:减少为 3 级页表(L1/L2/L3),每级索引 13 位,共 8192 个表项。页内偏移 14 位。
  • 64KB 页面:仅需 2 级页表(L2/L3),每级索引 14 位,共 16384 个表项。页内偏移 16 位。
性能权衡:更大的页面(如 64KB)可以减少页表层级数,降低 TLB 未命中率,但会增大内部碎片(Internal Fragmentation)。Linux 内核默认使用 4KB 页面,以平衡灵活性和内存利用率。

3.4 块映射(Block Mapping)

ARMv8-A 允许在 L0、L1 或 L2 级使用块映射(Block Mapping),将一大段连续虚拟地址直接映射到物理地址,跳过后续的页表层级。块映射的大小取决于当前层级:

  • L0 块映射:映射 512GB 区域(仅 4KB 页面配置)
  • L1 块映射:映射 1GB 区域(4KB 页面)或 2MB 区域(16KB 页面)
  • L2 块映射:映射 2MB 区域(4KB 页面)或 32MB 区域(16KB 页面)

块映射通过将页表项中的位 [1] 设置为 1 来标识。块映射描述符的格式与页表描述符类似,但物理地址字段覆盖更大的连续范围。

块映射与页表映射对比 块映射 vs 页表映射(4KB页面,L2级) L2页表(512个表项) L3页表(512个表项) 512个物理页(每个4KB) 总映射:2MB(512 × 4KB) L2块描述符(块映射) 连续物理区域(2MB) 总映射:2MB(单次映射) 块映射减少页表层级,降低TLB压力,适用于大块连续内存映射(如内核线性映射)。
实际应用:Linux 内核在启动阶段使用块映射(如 1GB 或 2MB 块)来映射整个物理内存,以减少页表占用和 TLB 未命中。用户空间进程通常使用 4KB 页面映射,以支持细粒度的内存管理(如页面置换、写时复制)。

3.5 页表基址寄存器(TTBR0_EL1 / TTBR1_EL1)

ARMv8-A 提供两个页表基址寄存器:

  • TTBR0_EL1:用于用户空间(EL0)的页表基址。通常每个进程拥有独立的用户页表。
  • TTBR1_EL1:用于内核空间(EL1)的页表基址。所有进程共享同一个内核页表。

虚拟地址的最高位(VA[47])决定使用哪个寄存器:

  • VA[47] = 0:使用 TTBR0_EL1(用户空间,地址范围 0x0000_0000_0000_0000 ~ 0x0000_7FFF_FFFF_FFFF)
  • VA[47] = 1:使用 TTBR1_EL1(内核空间,地址范围 0xFFFF_8000_0000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF)
注意:当切换进程时,内核必须更新 TTBR0_EL1 为新进程的用户页表基址,并执行 TLBI 指令使失效旧的 TLB 条目。内核页表(TTBR1_EL1)保持不变。

3.6 页表遍历与 TLB 缓存

MMU 在每次地址转换时,首先检查 TLB(Translation Lookaside Buffer)中是否缓存了对应的页表条目。如果 TLB 命中,则直接返回物理地址;如果 TLB 未命中,则启动页表遍历(Page Table Walk):

  1. 从 TTBR0_EL1 或 TTBR1_EL1 获取 L0 页表基地址。
  2. 使用 VA[47:39] 作为索引,从 L0 页表中读取 L0 描述符。
  3. 如果 L0 描述符是块映射,则直接计算物理地址;否则,从描述符中提取 L1 页表基地址。
  4. 使用 VA[38:30] 作为索引,从 L1 页表中读取 L1 描述符。
  5. 重复步骤 3-4,直到读取 L3 描述符(或遇到块映射)。
  6. 从 L3 描述符中提取物理页基地址,加上 VA[11:0] 得到最终物理地址。
  7. 将转换结果缓存到 TLB 中,供后续访问使用。
硬件加速:ARMv8-A 的 MMU 包含硬件页表遍历器(Hardware Page Table Walker),自动完成上述步骤,无需软件干预。遍历结果会写入 TLB,并更新页表项的访问标志(AF)和脏标志(DBM)。