今天想认真聊聊 SMMUv3 的硬件 PTW。以前我提到 SMMU 就说“IOMMU 嘛,给 DMA 做地址翻译”,直到在某块新平台的调试中,网络控制器 DMA 偶发访问异常,我才发现自己对“翻译到底是谁做的、在哪一层做、为什么改了页表还不起效”这些细节并没有真正吃透。这篇文章是那次排查之后的完整复盘。
SMMUv3 里的硬件 PTW(Page Table Walk,页表遍历)指的是:当一个 IO 事务需要地址翻译而翻译结果不在缓存里时,SMMU 不需要打断 CPU、不需要让驱动软件去一级一级读页表,而是由硬件自己把多级页表从头走到尾,最终得出物理地址。这套机制是 IOMMU 高性能的关键,也是虚拟化场景下 IO 设备隔离的基石。如果你在做设备驱动、IOMMU 子系统适配、虚拟化 DMA 透传,或者只是被一段 DMA 内存损坏问题折磨,这篇文章应该对你有用。
1. 从“软件查页表”到“硬件走查”:PTW 解决的真问题
1.1 没有 SMMU 时,DMA 是一盘散沙
在没有 SMMU 的系统里,外设发出的 DMA 地址就是物理地址。设备驱动说“我要访问 0x8000_0000”,DMA 就会直接读写那个物理位置。优点很简单:无翻译、无延迟。缺点也很致命:要么信任所有设备驱动,要么自己维护一份“这块内存不能被 DMA 碰”的地址排除名单。
虚拟化场景更麻烦。给客户机分配一个直通设备时,如果设备 DMA 能直接访问物理内存,客户机里一个写坏的描述符就可能改写宿主机内存。设备直通要安全,就必须让客户机里的驱动操作的是“看起来属于自己”的地址,而不是真正的物理地址。
所以 SMMU 的核心工作,是把 DMA 地址变成 IO 虚拟地址(IOVA)。设备发出的地址不再直接落到内存,SMMU 负责把 IOVA 翻译成系统物理地址。翻译需要查页表,而“谁去查页表”就是一个绕不开的架构选择。
1.2 软件参与页表遍历,代价比想象中大
早期或者简化实现里,翻译缺失经常要走软件路径:TLB 未命中后,通过中断或命令队列通知软件,软件去内存里逐级读页表,算出物理地址,再写回 TLB。听上去每笔 DMA 只需要一次额外处理,但现代高吞吐网卡有几十个队列,每个队列每秒可以产生上百万个 DMA 请求。如果每次 miss 都要触发一次软件 walk,CPU 开销会瞬间拉满,DMA 延迟也会剧烈抖动。
软件遍历还有一个工程问题:页表在内存里,软件要读取它,就得保证自己运行在正确的地址空间、正确的页表层级、正确的权限下,而这些代码本身还可能被调度、被中断打断。一次遍历往往要串行读好几个内存地址,任何一个环节慢,整条设备 I/O 路径就慢下来。硬件 PTW 把这件事收进片上状态机,目的就是去掉这条串行软件链路。
1.3 硬件 PTW 带来的三个直接收益
- 第一是延迟可控。翻译缺失时硬件自动从根表逐级读内存,无需 CPU 参与,大多数情况就是一次或两次内存读,加上一次 TLB 填充。
- 第二是并发。越来越多的 SMMU 实现会配备不止一个 PTW 引擎,不同 Stream、甚至多个队列的 DMA 请求可以并行 walk,互不阻塞。软件遍历做不到这种并发度。
- 第三是兼容标准页表软件栈。SMMUv3 的 PTW 直接吃标准的 LPAE 多级页表格式,内核 IOMMU 子系统里
io-pgtable框架支持这种格式,驱动侧创建和销毁页表的逻辑可以完全复用。
其实还有第四点:硬件 PTW 在 walk 过程中可以做中间级缓存和预取,这一点放到第三章细说。理解这点,才算真正理解硬件 PTW 的性能优势。
2. 一次 IO 事务从提交到命中:STE、CD 和多级描述符的实际路径
2.1 设备怎么找到自己的页表
一个 IO 事务进入 SMMU 后,SMMU 先拿事务携带的 StreamID 去查 Stream Table。STE(Stream Table Entry)是第一步的关键。STE 允许三种策略:bypass、abort、translate。如果配置成 translate,SMMU 才会真的开始页表遍历。
具体从哪张页表开始,取决于是否开启两级翻译。只使用 stage2 时,STE 里直接放 S2TTB(stage2 页表基地址)和 VMID;需要 stage1 时,STE 里的配置会指向一份 CD(Context Descriptor),CD 里再放 TTB0/TTB1、ASID、地址宽度等字段。支持 SubstreamID 时,一个 Stream 下还能再细分多个 substream,和 PCIe 的 PASID 概念对应。
所以“硬件 PTW 的根”不在某个固定寄存器里,而在 STE/CD 这套间接配置里。这也是排查时最容易被忽略的地方:你以为页表挂上了,实际上 SMMU 连根表地址都取错了。
2.2 多级页表的遍历规则
以 4KB granule、48 位输入地址为例,硬件把虚拟地址切成 9+9+9+9+12 五个部分,前四级分别是 Level0 到 Level3 的索引,最后 12 位是页内偏移。PTW 先从根表取 Level0 描述符,再根据描述符类型判断:
- 如果描述符是 table 类型,继续用下一段地址索引去取 Level1 描述符;
- 如果是 block 类型,说明这一大块地址直接映射到一段连续物理内存,也就是大页映射;
- 到了 Level3,看到 page 描述符,翻译完成。
每个描述符里除了输出地址,还带着权限、缓存属性、访问标志位等。硬件 PTW 会逐级检查,属性不合法就立刻终止并产生事件。
这个过程和 CPU 侧 MMU 的页表遍历几乎一模一样,把多次内存访问变成了硬件内部操作。区别在于,CPU 侧的硬件 walk 是为单核多线程服务的,SMMU 侧的 walk 则服务于多个设备,缺页时可能有多个请求在等同一个描述符,因此硬件里通常还有对同一缓存行的合并逻辑,避免重复读内存。
2.3 一次翻译究竟要读几次内存
很多人以为一次翻译就是读“页表”一次,实际未必。用一个刚建立的映射做例子:
- 如果 TLB 没有命中、中间级也没有缓存,48 位地址从 Level0 走到 Level3,理论上要读 4 次内存;
- 如果中间某一级命中了 walk cache,通常能省掉若干次;
- 如果映射用了 2MB 大页,Level0 之后直接得到 block 描述符,只需要读更少的次数。
这个“读几次内存”的差异直接决定了 DMA 首次访问的延迟。某些对延迟敏感的存储设备,头几个 I/O 的时延偏差就是从这来的,也是后面讲调优时为什么要优先用大页的原因。
2.4 CMDQ:软件和硬件 PTW 之间的调度界面
这里要强调一个容易混淆的点:软件并不会“启动”一次 PTW。设备 DMA 请求到达 SMMU、翻译不在缓存时,PTW 是自己开始的。软件和硬件的真正交互界面是命令队列(CMDQ)。
CMDQ 里最常见的是 TLBI 和 SYNC。TLBI 的作用是告诉 SMMU:某段范围内的缓存失效,下次访问不要再用旧结果;SYNC 则是让队列里之前的命令必须执行完毕后再继续。所以软件能控制的不是“怎么翻译”,而是“什么时候让缓存里的旧结果失效”。这个语义理解到位,很多奇怪的 DMA 问题都能定位了。
3. TLB 与中间缓存:页表更新失效的脏数据陷阱
3.1 TLB 里存的是什么
TLB 缓存的是完整翻译结果:一个 IOVA 对应哪个物理地址、什么权限、什么属性。设备访问是高频的,TLB 命中时不需要再做任何页表遍历,直接拿结果去访问内存。
要注意 TLB entry 通常还带 ASID/VMID 标签,用来区分不同地址空间。stage1 用 ASID、stage2 用 VMID,缓存里同一个虚拟地址的翻译结果可能同时存在,互不串扰。
问题恰恰在这里:软件改了页表,如果 TLB 里还留着旧映射,那么设备下一次访问依然会被翻译到旧物理地址。这跟 CPU 侧改页表不刷 TLB 是一个道理。差别在于 CPU 刷 TLB 是一条特权指令,而 SMMU 刷 TLB 要走 CMDQ 下发;CPU 是自己主动失效自己的缓存,SMMU 则是软件发命令让另一个硬件单元去失效,时序上要更小心。
3.2 walk cache:比 TLB 更容易忽视的一层
有些实现除了 TLB,还把 PTW 过程中读到的中间级描述符放在专门的 walk cache 里。比如 Level1 的表项、Level2 的表项,单独缓存。
这意味着什么?即使你把 TLB 失效了,但 walk cache 里残留的中间描述符仍可能被 PTW 使用,然后得到一条错误的完整路径。要命的是,如果中间级页表被复用或释放,walk cache 里留着旧地址,下一次 walk 可能读到一个已经被挪作他用的内存区域,轻则翻译错误,重则读到非法描述符产生 fault。
所以正确理解是:只要页表内容变化了,就必须做相应范围的 TLBI;TLBI 不仅负责 TLB,也会驱动硬件把相关中间缓存一并作废。不要因为“我只改了二级页表,TLB 里又没存这一层”就不发命令。
3.3 TLBI 命令的正确顺序
一个安全顺序应该是:
- 在内存中更新页表描述符;
- 通过内存屏障确保页表更新的可见性;
- 通过 CMDQ 下发对应范围的 TLBI;
- 下发 SYNC,确认失效完成;
- 之后才允许释放或复用被替换的页表内存。
第 5 步最容易被省略。不少人 unmap 之后忘了 TLBI+SYNC,于是某天设备偶发 TRANSLATION fault。等排查到的时候,页表内存可能已经被其他驱动覆盖,连 fault 本身的地址都难以解释。
再补一个批量技巧:如果一次要解除几百个映射,不必每个 unmap 都发一条 TLBI。可以一边批量更新页表,一边把需要失效的 VA 范围记下来,然后一次性下发多条 TLBI,最后统一 SYNC。这样 CMDQ 不会被频繁撑满,性能上明显更稳。
3.4 缓存一致性与“写页表”的隐性要求
还有一层是内存属性。PTW 读页表本质是一次系统互联上的内存读。如果页表所在内存是 normal cacheable 并已被 CPU 写入缓存,而 SMMU 与 CPU 之间没有或只有部分缓存一致性支持,那么 PTW 可能读到旧内容。
很多现代 SoC 上 SMMU 与 CPU 是 coherenct 的,页表写在 cache 里也能被 PTW 看到。但不代表所有平台都会保证这一点。遇到“页表明明改了,SMMU 还是老样子”的时候,不要只怀疑 TLBI,先看页表更新有没有做 clean/write back,或者干脆把页表放 non-cacheable 区域做对照实验。这个坑我在某个新平台的调试中确实碰到过,当时浪费了不少时间。
4. Stage1/Stage2 与安全属性:硬件 PTW 如何守住隔离边界
4.1 Stage1/Stage2 真的会查两遍吗
虚拟化场景下,客户机和虚拟化层各有一张页表。客户机管 stage1:设备看到的 IOVA 转 IPA;虚拟化层管 stage2:IPA 转最终 PA。
SMMUv3 开启 nested 翻译时,硬件 PTW 先按 stage1 走一遍,得出 IPA,再用 IPA 作为输入按 stage2 走第二遍,得出 PA。这个过程完全由硬件完成,对客户机和虚拟化层都是透明的。代价是翻译路径变长,如果每级都不命中缓存,stage1 加 stage2 最多会读很多次内存。所以裸金属场景或者不需要虚拟化的场景,通常会关闭 stage1,直接让 STE 里配 stage2 甚至 bypass,换取更低的 DMA 时延。这个取舍要在性能和隔离之间自己权衡。
4.2 Secure/Non-Secure 是 PTW 的一条硬边界
SMMU 要防护的不只是“地址对不对”,还有“谁有资格读哪块内存”。ARM 体系里有 Secure/Non-Secure 的世界划分;Non-Secure 世界运行的组件,不应该有能力让 DMA 直接读到 Secure 世界的内存。
STE 里记录了 stream 对应的安全状态。硬件 PTW 在遍历页表时,也会根据 stream 的安全属性决定能访问哪段地址空间。如果配置把一个 Non-Secure stream 的 STE 指向了 Secure 内存里的页表,硬件不会“尝试之后失败”,而是会更早地在配置层面判定不合法,阻止翻译发生。这其实是把 DMA 变成一种“带权限的内存访问”的关键机制。
实际产品里,安全侧固件通常会把 SMMU 的某些 stream 配置成 abort 或限制在明确的安全地址范围。调试时如果看到设备怎么都拿不到地址,先检查 stream 的安全属性和 STE 配置是否匹配,优先于去分析页表内容。
4.3 权限位不匹配时,硬件去哪报告
页表描述符里除了地址,还有读写许可、访问标志等。一个设备对 read-only 页发起写 DMA,或者对不存在映射的地址发起访问,PTW 都会在相应阶段停止,并产生一个 event。SMMUv3 的事件队列里会记录类型、StreamID、SubstreamID、地址等信息。
常见的事件类型包括:地址翻译失败、权限失败、地址宽度超出限制、页表格式非法等。每种类型的排查方向完全不同:
- 翻译失败,先怀疑页表有没有建好、STE/CD 是否生效;
- 权限失败,先看描述符的 AP 位和事务的读写方向;
- 地址宽度失败,往往和设备自身寻址能力、输入地址宽度配置有关。
拿到 event record 时,不要只看“SMMU 报错了”,把 type、地址、StreamID 三个字段同时拿出来,往往能直接定位到驱动里的某一行配置。
5. 实际排错与调优:我在项目中遇到的 PTW 问题
5.1 我建议的排错第一站:Event Queue
遇到 DMA 异常,我的习惯是先读 Event Queue,而不是先翻驱动代码。原因是 event record 是硬件的直接现场。
要看的信息有这么几项:StreamID(哪个设备)、event type(哪类失败)、翻译阶段(stage1 还是 stage2)、出错的地址。用这些信息回查设备树或驱动的 stream 配置,很快能确定方向。
一个常见假象:设备已经被复位或者驱动已经被卸载,但 Event Queue 里还堆着之前的 fault。所以排错时要先确认记录的时间或单调递增计数,别拿死亡现场当现行犯。
5.2 用 SMMU 的 PMU 定位性能问题
功能通了之后就是性能。SMMUv3 有 PMCG(Performance Monitor Counter Group),可以统计 TLB 命中/未命中、PTW 次数等。如果你发现某个设备吞吐上不去,又不能解释为 CPU 瓶颈,建议先看几个计数:
- TLB miss 率是否异常高;
- PTW 次数是否和设备 I/O 频率成正比;
- 大页映射是否能明显降低 miss 率。
调优动作通常是两个方向:一个是把 IOVA 空间尽量做成连续大块,让映射命中 block 描述符;另一个是减少 map/unmap 频率,让热路径上的 TLB 长期命中。我见过某些驱动为了省内存,每笔 DMA 都用小块映射,结果 TLB miss 率超过一半,改成预分配环形区域后性能直接翻倍。
5.3 让我花时间最长的几个坑
这里挑几个我在实际调试中真正踩过、或者陪别人查了很久的典型问题:
| 现象 | 根因 | 处理 |
|---|---|---|
| 偶发 TRANSLATION fault | unmap 后没有做 TLBI+SYNC 就释放页表内存 | 先补齐失效命令,再释放内存 |
| DMA 访问到了错误物理地址 | STE 被配置成 bypass,页表形同虚设 | 检查 STE 的 translate 策略 |
| DMA 性能突然暴跌 | stage1+stage2 双重 walk,且页表全是 4KB | 用 2MB 大页或关闭不必要的 stage1 |
| 修改页表后长时间不生效 | walk cache 残留,或页表写后未做 clean | 发对应 TLBI,并检查页表内存属性 |
每个坑单拎出来都不难,难的是组合场景。比如“偶发 TRANSLATION fault”再加上“设备刚好在卸载时触发”,如果不了解 TLBI 生命周期,很容易误判成硬件不稳定。
5.4 一个可以直接拿去用的检查顺序
我的建议是配置阶段、运行阶段、异常阶段分开查。
配置阶段:
- 确认 STE 基址、CD 基址写入正确,且按硬件要求的对齐方式配置;
- 确认 STE 的 translate/bypass/abort 策略符合预期;
- 确认 CD 和 STE 里的地址宽度、ASID/VMID 没有冲突。
运行阶段:
- 验证页表内存可见性,必要时做 clean/write back;
- 大量更新页表时,使用批量 TLBI 加单次 SYNC;
- 用 PMCG 观察 TLB miss 曲线,及时优化映射块大小。
异常阶段:
- 第一时间读 Event Queue,记录 StreamID、type、地址;
- 反查 STE/CD 配置,确认是不是配置层问题;
- 确认没有未失效的 TLB/walk cache,再执行内存释放。
这套顺序看起来朴素,但能把大多数 SMMUv3 问题压缩到一到两个排查轮次。别上来就怀疑 SMMU 硬件坏了,绝大多数问题都出在页表和命令队列的时序上。
这篇文章写到这,其实我最想表达的一点是:SMMUv3 的硬件 PTW 不是一个简单的“硬件帮你查表”开关,它是把 CPU 侧 MMU 的各种局部性设计搬到了 IO 侧,并且把页表一致性的责任明确交到了软件手里。最初我配完 STE 就以为完事了,后来经历的几次问题都跟页表生命周期的顺序有关。有一次恰好把 TLBI 放在 DMA 完成回调之后执行,排了很久才发现是顺序错位,页表已经释放,PTW 还在走查。如果你也在调类似问题,不妨先按上面的顺序把 Event Queue、TLBI、walk cache 这条线索理一遍。硬件 PTW 也许不会让你眼前一亮,但把这块机制吃透之后,处理 DMA 相关的怪问题会从容很多。