4. 页表格式与层级结构

页表这东西,说白了就是一张“地址翻译地图”。CPU看到的是虚拟地址,内存颗粒只认物理地址,中间全靠页表来搭桥。SMMU的页表和CPU的页表结构很像,但又有自己的脾气。今天咱们就把4级页表、3级页表、页表粒度和映射方式彻底聊透。

4.1 为什么需要多级页表?

你想想看,如果只用一级页表,一个4KB页、48位地址空间,需要多少页表项?2^36个,每个8字节,光页表就要512GB。这谁受得了?

多级页表的思路很简单:按需分配,逐级索引。就像查字典,先查部首,再查笔画,最后翻页码。不需要一次性把整本字典的索引都建好。

核心思想:多级页表用“树形结构”把线性页表拆成多段,每一级只保存下一级的物理地址。只有真正用到的页表级才会分配内存。

我在项目中遇到过一个大坑:某次IO设备访问的地址范围很稀疏,但用了4级页表后,页表内存反而比一级页表还大。后来一查,是页表分配器没做“按需分配”,直接把所有级都预分配了。嗯,这属于实现上的失误。

4.2 4级页表结构详解

ARMv8-A架构下,典型的4级页表长这样:

虚拟地址(48位)分解:
[63:48] 符号扩展
[47:39] L0索引 (9位) → 512个L0页表项
[38:30] L1索引 (9位) → 512个L1页表项
[29:21] L2索引 (9位) → 512个L2页表项
[20:12] L3索引 (9位) → 512个L3页表项
[11:0]  页内偏移 (12位) → 4KB页

每一级页表项(PTE)的大小是8字节。所以每级页表正好占一个4KB页(512 × 8 = 4096)。

页表级 索引位数 页表项数 覆盖地址范围 典型用途
L0 9 512 512GB / 项 顶级目录,指向L1
L1 9 512 1GB / 项 中间目录,指向L2
L2 9 512 2MB / 项 可映射大页(2MB)
L3 9 512 4KB / 项 叶子页表,映射4KB页

个人习惯:我一般把L0叫“根页表”,L3叫“叶子页表”。调试时这么说,团队里沟通效率高很多。

4.3 3级页表与页表粒度

不是所有场景都需要4级。如果地址空间只有39位,或者使用大页,可以缩减到3级甚至2级。

3级页表(39位地址):

[38:30] L1索引 (9位)
[29:21] L2索引 (9位)
[20:12] L3索引 (9位)
[11:0]  页内偏移

少了L0级,直接从L1开始。覆盖512GB地址空间,对大多数IO设备来说够用了。

页表粒度:

  • 4KB页:最细粒度,L3叶子项映射。适合随机小IO,但页表开销大。
  • 2MB页:L2项直接映射(块描述符),跳过L3。TLB覆盖范围扩大512倍。
  • 1GB页:L1项直接映射,跳过L2和L3。适合大块连续内存,比如GPU显存映射。

我曾经踩过的坑:在某个AI加速器项目中,我图省事把所有IO映射都用4KB页。结果TLB miss率飙到30%,性能惨不忍睹。后来换成2MB大页,TLB miss直接降到2%以下。记住:大页不是选项,是刚需。

4.4 页表项格式与属性

每个页表项(PTE)除了存下一级地址,还带了一堆属性位。SMMU的PTE和CPU的PTE基本兼容,但多了几个SMMU特有的字段。

典型的L3页表项(64位):
[63]    PXN        — 特权不可执行
[62]    XN         — 不可执行
[61:60] 保留
[59:56] 安全状态位
[55:52] 共享属性
[51:48] 访问权限
[47:12] 物理地址[47:12]  ← 4KB对齐的物理页基址
[11:9]  保留
[8]     AF         — 访问标志
[7:6]   保留
[5]     NS         — 非安全
[4:3]   AP[2:1]    — 访问权限
[2]     SH[1]      — 可共享性
[1]     PXNTable   — 表级PXN
[0]     Valid      — 有效位

关键属性解读:

  • Valid位:为0表示该页表项无效,访问会触发缺页异常。调试时经常遇到“页表配了但Valid没置1”的乌龙。
  • AF位:访问标志位。硬件在第一次访问时会置1,软件可以定期扫描做页表回收。
  • AP位:控制读写权限。SMMU里还要结合Stream的权限一起判断。
  • SH位:可共享性。IO设备之间的缓存一致性全靠这个位控制。

4.5 映射流程与TLB刷新

SMMU做地址翻译的流程,我习惯用“三级流水”来理解:

  1. Walk阶段:从根页表(L0/L1)开始,逐级索引,直到找到叶子PTE。
  2. 检查阶段:验证权限、安全状态、可共享性等属性。
  3. 转换阶段:取出物理地址,加上偏移,生成最终物理地址。

每次修改页表后,必须做TLB刷新。SMMU的TLB刷新命令和CPU不太一样:

// 全局TLB刷新(所有Stream)
SMMU_TLB_INVAL.write(0x1);

// 按ASID刷新(指定地址空间)
SMMU_TLB_INVAL.write(ASID << 32 | 0x1);

// 按VA刷新(指定虚拟地址)
SMMU_TLB_INVAL.write(VA & 0xFFFFFFFFFFFFF000 | 0x3);

我建议:批量修改页表时,先改完所有PTE,最后统一发一次TLB刷新。别改一个刷一次,性能会崩。我在一个NVMe驱动里见过这种写法,改一个4KB页就刷一次全局TLB,IOPS直接腰斩。

4.6 多级页表性能调优要点

实战中,页表配置对性能的影响非常大。我总结了几条经验:

调优方向 做法 效果 适用场景
页表级数 能用3级不用4级 减少一次内存访问 地址空间≤512GB
页粒度 优先用2MB/1GB大页 TLB覆盖范围扩大 大块连续DMA
页表缓存 开启PTW缓存 减少页表Walk延迟 页表频繁访问
TLB刷新 按ASID/VA刷新 避免全局刷新风暴 多Stream场景

注意:大页不是万能的。如果IO访问模式是随机4KB小IO,用2MB大页反而浪费内存(内部碎片)。我见过一个存储控制器,映射了2MB大页但实际只用了头4KB,剩下的2MB-4KB全浪费了。这种场景下,混合使用4KB和2MB页才是正解。

4.7 页表结构图

下面这张图展示了4级页表的完整Walk路径。我画图时习惯把“根”放在上面,叶子在下面,这样符合阅读习惯。

SMMU 4级页表Walk路径 SMMU 4级页表Walk路径 L0 根页表 L1 页表 L2 页表 2MB 大页映射 L3 叶子页表 物理页 (4KB) 说明:L2项可配置为块描述符,直接映射2MB大页,跳过L3级

4.8 实战建议

最后,给几个我在项目中反复验证过的建议:

  • 页表级数选型:先算地址空间大小。≤512GB用3级,>512GB用4级。别为了“统一”而浪费性能。
  • 大页策略:IO缓冲区≥2MB的,一律用2MB页。1GB页留给GPU或AI加速器。
  • 页表内存:放在NUMA离SMMU最近的节点上。页表Walk延迟能降30%。
  • 调试工具:用SMMU的页表Walk调试寄存器,可以单步跟踪翻译过程。我调试缺页异常时全靠它。

一句话总结:多级页表的核心是“用时间换空间”,但通过合理选择级数和粒度,可以把时间代价降到最低。页表配置不是一次性的,需要根据IO负载动态调整。