4、I/O页表格式:Stage-1翻译(VA->IPA)、Stage-2翻译(IPA->PA)、混合翻译模式、页表属性与权限控制
好,咱们今天来聊聊I/O页表的具体格式。说实话,这部分内容我第一次接触时也觉得头大——页表层级、翻译模式、属性位,一堆概念搅在一起。但后来我意识到,只要抓住一个核心问题:设备发起的DMA请求,到底是怎么一步步找到物理内存的?搞明白这个,剩下的都是细节。
4.1 为什么需要两阶段翻译?
先说说背景。在虚拟化场景下,虚拟机看到的地址空间是IPA(Intermediate Physical Address),不是真实的PA。设备要访问内存,得先知道这个IPA对应哪个PA。这就引出了两阶段翻译:
- Stage-1:把设备发出的VA(虚拟地址)翻译成IPA。这个阶段由Guest OS的IOMMU驱动来管理。
- Stage-2:把IPA翻译成真实的PA。这个阶段由Hypervisor/VMM来管理。
你想想看,如果没有Stage-2,虚拟机里的设备驱动直接操作物理地址,那整个系统就乱套了。我在一个ARM虚拟化项目中就遇到过这种情况——虚拟机里的NVMe驱动直接写死了物理地址,结果迁移后内存布局变了,数据全写到了别人的地盘上。嗯,从那以后我对两阶段翻译格外上心。
4.2 Stage-1翻译:VA到IPA
Stage-1的页表格式,说白了和CPU的页表非常像。以ARM SMMU为例,它支持多种页表粒度:4KB、16KB、64KB。我个人习惯用4KB粒度,兼容性最好。
页表层级通常是3级或4级。举个例子,4级页表的翻译过程:
VA[63:48] -> Level 0 Table (PGD)
VA[47:39] -> Level 1 Table (PUD)
VA[38:30] -> Level 2 Table (PMD)
VA[29:21] -> Level 3 Table (PTE)
VA[20:0] -> Page Offset
每一级页表项(Descriptor)都包含指向下一级表的基地址,以及一些控制位。到了最后一级,页表项里存的就是IPA的基地址了。
关键点:Stage-1翻译出来的IPA,对设备来说是透明的。设备只知道自己发出了VA,IOMMU负责把它变成IPA。至于这个IPA怎么变成PA,那是Stage-2的事。
4.3 Stage-2翻译:IPA到PA
Stage-2的页表结构和Stage-1类似,但多了几个重要的控制位。我记得第一次看SMMU spec时,被Stage-2的权限模型绕晕了——它不仅要管读写权限,还要管访问类型(比如是否允许缓存)。
Stage-2页表项的关键字段:
| 字段 | 说明 |
|---|---|
| PA[47:12] | 物理地址的高36位,低12位是页内偏移 |
| S2AP[1:0] | Stage-2访问权限:00=无权限,01=只读,10=只写,11=读写 |
| MemAttr[3:0] | 内存属性,比如是否可缓存、是否可共享 |
| XN | 禁止执行(对I/O设备来说,通常用不到) |
个人经验:配置Stage-2页表时,MemAttr字段特别容易踩坑。我曾经在一个项目中,设备DMA总是报错,查了两天才发现是Stage-2页表把内存属性设成了不可缓存,导致一致性协议出了问题。后来我养成了一个习惯:Stage-2的MemAttr一定要和Stage-1保持一致,除非你明确知道自己在做什么。
4.4 混合翻译模式
混合翻译模式,说白了就是Stage-1和Stage-2同时生效。设备发出VA,经过Stage-1变成IPA,再经过Stage-2变成PA。这种模式在虚拟化场景下是标配。
但这里有个坑:性能开销。每次DMA都要查两次页表,TLB miss的概率翻倍。我见过一些性能敏感的场景,比如高速网卡,混合翻译模式会导致吞吐量下降10%~20%。
怎么优化?
- 使用大页:比如2MB或1GB的页,减少页表层级,降低TLB miss率。
- 开启TLB预取:有些SMMU支持硬件预取,能提前加载页表项。
- 考虑直通模式:如果设备不需要虚拟化,可以绕过Stage-2,直接用Stage-1翻译到PA。
注意:混合翻译模式下,Stage-1和Stage-2的权限控制是叠加的。设备要访问某块内存,必须同时通过两阶段的权限检查。比如Stage-1允许读,但Stage-2禁止读,那设备还是读不了。这个逻辑有点像防火墙的规则——最严格的规则说了算。
4.5 页表属性与权限控制
页表属性这块,我重点说说几个容易混淆的点。
首先是访问权限。Stage-1和Stage-2都有自己的AP(Access Permission)位。Stage-1的AP控制设备对IPA的访问,Stage-2的AP控制IPA到PA的映射权限。两者必须配合好。
其次是内存属性。包括:
- Cacheability:是否允许缓存。对DMA来说,通常建议开启缓存,除非设备有特殊的一致性要求。
- Shareability:是否允许其他核或设备共享。非共享模式下,每个核有自己的缓存副本,容易产生一致性问题。
- Device属性:有些内存区域被标记为Device类型,比如MMIO寄存器。这种区域不允许缓存,也不允许推测访问。
我举个例子。有一次调试一个GPU驱动,发现DMA写入的数据总是被CPU读到旧值。查了半天,原来是页表把GPU的DMA缓冲区设成了Non-Cacheable,但CPU侧用的是Cacheable。两边不一致,导致数据不同步。解决方案很简单:把DMA缓冲区的页表属性改成Cacheable,并加上共享属性。
避坑指南:我曾经在配置页表属性时,把Device类型的内存误设成了Normal类型。结果设备访问MMIO寄存器时,SMMU做了缓存和重排,导致寄存器读写顺序错乱。嗯,从那以后我每次配置页表都会再三确认内存类型。
4.6 页表格式的SVG结构图
下面这张图展示了完整的I/O页表翻译流程。我画得比较简洁,但核心逻辑都在里面了。
4.7 小结
这一章的内容比较多,但核心就三件事:
- Stage-1:VA到IPA,由Guest OS管理。
- Stage-2:IPA到PA,由Hypervisor管理。
- 混合模式:两者叠加,权限取交集,属性要一致。
页表属性这块,我建议你多花点时间理解内存类型和权限控制。我在实际项目中见过太多因为属性配置不当导致的问题——数据不一致、性能下降、甚至系统崩溃。嗯,这些坑我都踩过,希望你能绕过去。
公众号:蓝海资料掘金营,微信deep3321