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,适合虚拟化场景
我画了一张流程图,帮你理清这几种模式的关系:
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访问页表时会报错。