4. 页表与地址翻译:Stage1翻译、Stage2翻译、混合翻译模式、页表格式与属性

地址翻译,说白了就是SMMU最核心的活儿。CPU给你一个虚拟地址,你得把它变成物理地址,然后才能去内存里拿数据。但SMMU的翻译比CPU的MMU复杂得多——它要处理两层翻译,还要考虑安全和非安全世界的隔离。

我记得刚接触SMMU时,看到Stage1和Stage2这两个概念,第一反应是:这不就是嵌套页表吗?后来踩了几个坑才明白,事情没那么简单。今天咱们就把这块彻底掰开揉碎。

4.1 Stage1翻译:虚拟地址到中间物理地址

Stage1翻译,负责把设备发过来的虚拟地址(VA)转成中间物理地址(IPA)。这个IPA是什么?你可以把它理解成“虚拟的物理地址”——对设备来说它像物理地址,但对系统来说它还是虚拟的。

关键点:Stage1的页表格式和CPU的页表几乎一模一样。如果你熟悉ARMv8的页表,那Stage1对你来说就是老朋友。

Stage1支持多种页表粒度:

  • 4KB页:最常用,兼容性好,但页表占用内存大
  • 16KB页:某些场景下减少TLB miss
  • 64KB页:大页,适合高性能场景,我曾在某存储控制器项目中用过

页表级数也分三种:

级数 地址空间 适用场景
3级 40位VA 大多数嵌入式设备
4级 48位VA 服务器、高性能计算
5级 52位VA 超大地址空间场景

我的经验:别一上来就上5级页表。级数越多,翻译延迟越大。我见过有人为了“未来扩展”选了5级,结果性能惨不忍睹。除非你真的需要52位地址空间,否则4级足够了。

4.2 Stage2翻译:中间物理地址到物理地址

Stage2翻译,把IPA转成真正的物理地址(PA)。这层翻译是SMMU独有的,CPU的MMU没有这个。为什么需要它?因为虚拟化。

在虚拟化场景下,Hypervisor给每个虚拟机分配了一段IPA空间。设备通过Stage1拿到IPA,再通过Stage2拿到真正的PA。这样Hypervisor就能灵活控制设备能访问哪些物理内存。

Stage2的页表格式和Stage1略有不同:

  • Stage2的页表项里没有XN(Execute Never)位——因为设备不需要执行代码
  • Stage2多了HAP(Hardware Access Permission)位,用于控制访问权限
  • Stage2支持VMSA(Virtual Memory System Architecture)的页表属性

注意:Stage2的页表基地址寄存器是TTBR2,不是TTBR1。我见过有人把Stage1的基地址配到Stage2里,结果系统直接挂掉。这个坑我踩过,别重蹈覆辙。

4.3 混合翻译模式:两阶段齐上阵

混合翻译模式,就是Stage1和Stage2同时工作。设备发一个VA,先过Stage1变成IPA,再过Stage2变成PA。整个过程对设备完全透明。

为什么叫“混合”?因为你可以自由组合:

  • 只开Stage1:设备直接访问IPA空间,适合非虚拟化场景
  • 只开Stage2:设备发的是IPA,直接翻译成PA,适合透传场景
  • 两阶段全开:设备发VA,经过两层翻译得到PA,适合虚拟化场景

我画了一张流程图,帮你理清这几种模式的关系:

SMMU地址翻译模式流程图 设备发起地址访问 选择翻译模式(SMMU_CR0) 仅Stage1 VA → IPA 仅Stage2 IPA → PA 两阶段全开 VA → IPA → PA 物理地址(PA) 说明: • 翻译模式通过 SMMU_CR0 寄存器的 SMMUEN 和 S2P 位组合选择 • 两阶段全开时,Stage1和Stage2的页表可以独立配置,互不干扰

4.4 页表格式与属性详解

页表项(PTE)的格式,不同架构略有差异。我以ARM SMMUv3为例,给你拆解一下关键字段。

4.4.1 Stage1页表项格式

位域 名称 说明
[1:0] DESC 描述符类型:00=无效,01=页表,10=块,11=页
[4:2] ATTR_INDEX 属性索引,指向MAIR中的属性配置
[5] NS 非安全位:0=安全,1=非安全
[7:6] AP 访问权限:00=读写,01=只读,10/11=特权控制
[10] AF 访问标志位,硬件置1表示已访问
[11] SH 可共享属性:00=不可共享,01=保留,10=外设共享,11=内部共享
[52:48] PA 物理地址高位(52位地址空间时使用)

重点:AF位是个坑。硬件不会自动置AF位,需要软件在页表初始化时手动置1。否则第一次访问会产生Access Flag fault。我见过有人忘了这个,结果系统频繁报错,排查了半天。

4.4.2 Stage2页表项差异

Stage2的页表项和Stage1比,有几个关键区别:

  • 没有XN位:设备不需要执行权限控制
  • 多了HAP位:硬件访问权限,控制设备对内存的读写
  • MemAttr字段:内存属性,包括缓存策略、共享属性等
  • S2AP位:Stage2的访问权限,和Stage1的AP位协同工作

我整理了一个对比表,方便你理解:

属性 Stage1 Stage2
访问权限 AP[1:0] S2AP[1:0]
可执行 XN/PXN 不支持
内存属性 AttrIndex → MAIR MemAttr直接编码
共享属性 SH[1:0] S2SH[1:0]
访问标志 AF AF

4.5 页表配置实战要点

说完了理论,咱们聊聊实际配置时要注意什么。

4.5.1 页表基地址对齐

页表基地址必须按页表大小对齐。比如4KB页表,基地址必须4KB对齐。这个要求看似简单,但我见过有人把地址写错了,结果SMMU死活不工作。

我的习惯:配置页表基地址前,先打印出来看一眼。用printf("TTBR = 0x%lx\n", ttbr)确认对齐没问题。这招帮我省了不少调试时间。

4.5.2 缓存策略选择

页表本身也可以被缓存。SMMU有TLB和页表缓存,配置不当会导致性能问题。

  • Inner Shareable:多核场景下推荐,保证缓存一致性
  • Non-Shareable:单核或专用设备场景,性能更好
  • Outer Shareable:跨集群场景,但延迟较高

我曾经在一个项目中,把页表配成了Non-Shareable,结果多核访问时数据不一致,设备读到了脏数据。后来改成Inner Shareable才解决。

4.5.3 页表预填充

为了减少TLB miss,可以在初始化时预填充页表。SMMU支持TLB预加载命令,通过SMMU_CMD_QUEUE发送。

/* 预填充TLB示例代码 */
void smmu_tlb_prefill(uint64_t iova, uint64_t size) {
    struct smmu_cmd cmd;
    
    cmd.opcode = SMMU_CMD_TLBI_NSNH;
    cmd.tlbi.addr = iova;
    cmd.tlbi.size = size;
    
    smmu_send_cmd(&cmd);
}

注意:预填充不是万能的。如果设备访问模式变化频繁,预填充反而会浪费带宽。我建议先做性能分析,再决定要不要用。

4.6 避坑指南

最后,分享几个我踩过的坑:

  • 页表属性冲突:Stage1和Stage2的权限位是“与”的关系。Stage1允许读写,但Stage2只允许读,最终结果是只读。配置时一定要检查两层权限的叠加效果。
  • 页表更新后忘了刷TLB:修改页表后,必须发送TLB invalidate命令。否则SMMU可能还在用旧的缓存。我见过有人改了页表,结果设备访问的还是旧地址。
  • 页表内存类型:页表本身所在的内存,必须配置为Normal Non-Cacheable或Normal Cacheable。如果用Device类型,SMMU访问页表时会报错。