SMMU 多级缓存架构:TLB、微 TLB、主 TLB 与 PTW Cache

各位好,我是老吴。今天咱们聊聊 SMMU 的缓存体系。说实话,很多做系统软件的朋友,一开始都觉得 SMMU 就是个地址翻译器,没啥大不了的。但等你真正碰到性能瓶颈,比如 IO 延迟突然飙升、吞吐量上不去的时候,你才会发现——问题十有八九出在缓存上。

我当年调一个 NVMe 驱动的性能,死活差 30%。最后抓出来,是 SMMU 的微 TLB 频繁失效,导致每次 IO 都要穿透到主 TLB 甚至内存里去查页表。嗯,从那以后,我再也不敢小看这玩意儿了。

为什么需要多级缓存?

说白了,就是速度与容量的博弈。CPU 的 MMU 有 L1、L2 TLB,SMMU 也一样。设备发起的 DMA 请求,如果每次都要去内存里翻页表,那延迟是灾难性的。

你想想看,一个 PCIe Gen4 的设备,带宽几十 GB/s。如果每次地址翻译都要等几百纳秒,那整个 IO 路径就废了。所以,SMMU 必须在硬件上做多级缓存,把最常用的翻译结果存起来。

核心原则: 越靠近设备的缓存,速度越快,容量越小。越靠近内存的缓存,容量越大,速度越慢。

四层缓存结构总览

我个人习惯把 SMMU 的缓存体系分成四层。注意,不是所有硬件都实现了全部四层,但主流的高性能 SMMU(比如 ARM 的 SMMUv3.2)基本都齐了。

层级 名称 容量 延迟 存储内容
L0 微 TLB (μTLB) 8~64 条 1~2 周期 最近使用的 1~2 个页表条目
L1 主 TLB (Main TLB) 512~2048 条 3~5 周期 完整的页表条目(含权限、属性)
L2 页表缓存 (PTW Cache) 8~64 KB 10~20 周期 页表中间层级(L1/L2/L3 页表项)
L3 系统内存 (DRAM) 无上限 100+ 周期 完整的页表结构

这张表你最好记下来。调优的时候,脑子里要时刻有这张图。

微 TLB (μTLB):第一道防线

微 TLB 是离设备最近的缓存。它很小,通常只有几十个条目。但它的速度极快,几乎不增加流水线延迟。

它的设计思路是:一个设备在短时间内,通常只访问连续的几页内存。比如一个网卡在收包,它可能连续写同一个 buffer 的不同偏移。这时候,微 TLB 就能完美命中。

我的经验: 微 TLB 的条目数通常与设备的 outstanding 请求数相关。如果你发现设备有大量乱序请求,微 TLB 太小就会频繁失效。我曾经在调试 GPU 驱动时,把微 TLB 从 16 条扩到 32 条,性能直接提升了 12%。

主 TLB (Main TLB):中坚力量

当微 TLB 失效时,请求会落到主 TLB。主 TLB 是真正的缓存主力。它存储的是完整的页表条目,包括物理地址、权限位、缓存属性、访问标志等。

主 TLB 通常采用全相联或组相联结构。我个人更倾向于组相联,因为它在面积和命中率之间取得了很好的平衡。

这里有个关键点:主 TLB 的条目是“合并”过的。什么意思?如果连续 4 个 4K 页映射到同一个 16K 大页,主 TLB 可以只存一条记录。这就是所谓的“块合并”或“超级条目”。

注意: 块合并不是自动的。它依赖于页表本身使用了大页(比如 2M、1G)。如果你在固件里配置的都是 4K 小页,那主 TLB 的命中率会大打折扣。我见过有人抱怨 SMMU 性能差,结果一看页表,全是 4K 页——这能快才怪。

页表缓存 (PTW Cache):隐藏的加速器

页表缓存,也叫 PTW Cache 或 Structure Cache。它不存最终的翻译结果,而是存页表本身的中间层级。

举个例子:一个 4 级页表查询,需要从内存里读 4 次。如果 PTW Cache 里已经缓存了 L1、L2 级的页表项,那硬件只需要读 L3 和 L4 两级。这能省掉一半的内存访问。

PTW Cache 的容量通常比主 TLB 大,但延迟也更高。它适合那些“页表结构复杂但变化不频繁”的场景。

我记得有一次,一个存储阵列的控制器,每次 IO 都要查 5 级页表(因为虚拟化嵌套)。没有 PTW Cache 的时候,延迟惨不忍睹。后来我们在 SMMU 固件里把 PTW Cache 开到最大,延迟直接降了 40%。

缓存之间的协作流程

下面这张图,是我自己画的 SMMU 缓存查询流程。你可以看到,一个地址翻译请求,是怎么一层层往下走的。

SMMU 多级缓存查询流程 SMMU 多级缓存查询流程 设备 DMA 请求 微 TLB (μTLB) 命中 失效 主 TLB (Main TLB) 命中 失效 PTW Cache 命中 失效 系统内存 (DRAM) 命中路径 • 微 TLB 命中:1~2 周期 • 主 TLB 命中:3~5 周期 • PTW Cache 命中:10~20 周期 • 内存访问:100+ 周期 返回物理地址

流程很简单:设备发起请求 → 查微 TLB → 命中则直接返回;失效则查主 TLB → 再失效则查 PTW Cache → 还失效就只能去内存里翻页表了。

每次命中,结果都会回填到上一级缓存。这叫“缓存填充”。所以,一次内存访问,可能会同时更新 PTW Cache、主 TLB 和微 TLB。

固件配置对缓存的影响

说了这么多,跟固件有什么关系?关系大了。SMMU 的缓存行为,很大程度上是由固件里的配置寄存器决定的。

  • TLB 条目数: 很多 SMMU 允许固件配置主 TLB 的条目数。如果你知道设备的工作集很大,就把它设大一点。
  • PTW Cache 大小: 这个通常跟页表级数有关。级数越多,PTW Cache 越有用。
  • 缓存策略: 是写通还是写回?是 LRU 还是随机替换?这些都会影响命中率。
  • 预取使能: 有些 SMMU 支持硬件预取。如果设备访问模式是顺序的,打开预取能大幅提升命中率。

我曾经踩过的坑: 有一次,我把 SMMU 的缓存策略配成了“写通”,结果每次 TLB 失效都会写回内存,导致总线拥堵。后来改成“写回”,问题解决。所以,别小看这些配置项,它们直接影响性能。

性能调优的核心指标

调优的时候,我主要看三个指标:

  1. TLB 命中率: 理想情况下,微 TLB 命中率应该在 90% 以上。如果低于 80%,说明工作集太大,或者页表配置不合理。
  2. PTW Cache 命中率: 这个应该在 95% 以上。如果频繁失效,说明页表结构变化太快,或者 PTW Cache 太小。
  3. 平均翻译延迟: 这个可以通过硬件计数器直接读到。如果延迟超过 50 个周期,就该检查缓存配置了。