3. SMMU核心数据结构:Stream Table、Context Descriptor、Stage 1/2 页表

好,咱们进入正题。SMMU 这东西,说白了就是个地址翻译和权限管控的硬件模块。你写驱动也好,调性能也罢,最终都要跟它的数据结构打交道。我个人习惯把这三块——Stream Table、Context Descriptor、Stage 1/2 页表——称为 SMMU 的「三驾马车」。搞不懂它们,调试的时候你连日志都看不懂。

3.1 Stream Table:设备身份的「户口本」

每个连接到 SMMU 的设备,都有一个唯一的 Stream ID。硬件发起事务时,会拿着这个 ID 去查 Stream Table。你可以把它理解成派出所的户籍系统——先找到这个人,再看他的权限和配置。

Stream Table 有两种组织形式:线性和 2级。我遇到过不少工程师一上来就选 2 级,觉得省内存。其实对于设备数量少于 64 个的场景,线性表更简单,查表延迟也更低。

特性 线性 Stream Table 2级 Stream Table
内存占用 连续大块,浪费可能多 按需分配,节省内存
查表延迟 固定,一次访存 两次访存,延迟略高
适用场景 设备少(<64),性能敏感 设备多(>256),内存受限
配置复杂度 低,基地址+大小即可 高,需配置 L1/L2 基址
我的经验: 在 SoC 原型验证阶段,先用线性表跑通功能。等软件栈稳定了,再根据实际设备数量决定是否切到 2 级。别一开始就给自己找麻烦。

Stream Table 的每个条目叫 STE(Stream Table Entry)。STE 里最重要的字段就是 Context Descriptor 指针。它指向了下一步要用的配置结构体。

3.2 Context Descriptor:翻译规则的「指挥中心」

Context Descriptor(CD)是 SMMU 里承上启下的关键。它告诉硬件:这个设备用哪个页表?是 Stage 1 还是 Stage 2?要不要做嵌套翻译?

CD 里有两个核心指针:

  • TTB0/TTB1:Stage 1 页表的基地址。TTB0 通常用于进程地址空间,TTB1 用于内核或全局映射。
  • VTTB:Stage 2 页表的基地址。在虚拟化场景下,由 Hypervisor 管控。

我记得有一次调试 GPU 驱动,发现某个上下文切换后地址翻译全乱套了。查了两天才定位到——CD 里的 ASID(Address Space ID)字段没更新。硬件用 ASID 来区分 TLB 条目属于哪个进程,你忘了改,那可不就乱了吗?

避坑指南: 我曾经在配置 CD 时,把 TTB0 和 TTB1 的地址写反了。结果用户态进程访问内核地址空间,直接触发 Permission Fault。所以,写 CD 之前一定要 double-check 基地址和 ASID 的对应关系。

3.3 Stage 1/2 页表:两层翻译的「分工协作」

这是 SMMU 最核心的能力——两阶段地址翻译。Stage 1 由 OS 管理,负责虚拟地址(VA)到中间物理地址(IPA)的映射。Stage 2 由 Hypervisor 管理,负责 IPA 到真实物理地址(PA)的映射。

为什么要搞两层?说白了就是为了虚拟化。Guest OS 管自己的进程,Hypervisor 管物理内存。两边各管各的,互不干扰。

关键点: 两阶段翻译不是必须同时开启的。非虚拟化场景下,只用 Stage 1 就够了。只有当你跑 KVM 或 Xen 时,才需要 Stage 2。

页表的层级结构(4KB 页为例):

  1. L0/L1 表:覆盖大范围,每个条目指向下一级表。
  2. L2 表:中等粒度,每个条目指向 L3 表或直接映射 2MB 块。
  3. L3 表:最细粒度,每个条目映射 4KB 页。

我建议你在调优时重点关注 块映射(Block Mapping)。如果设备访问的是连续大块内存(比如显示缓冲区),直接用 L2 的 2MB 块映射,能大幅减少页表遍历次数和 TLB 压力。

3.4 三者的协作流程

来,我们画个图,把这三块串起来。设备发起一次内存访问,SMMU 内部是怎么一步步找到物理地址的?

SMMU 地址翻译流程(三驾马车协作) 设备 Stream ID Stream Table Context Descriptor Stage 1 页表 Stage 2 页表 物理地址 PA 流程说明 1. 设备发起事务 2. 硬件查 Stream Table 3. 获取 Context Descriptor 4. 执行 Stage 1/2 翻译

流程很清晰:

  1. 设备发起 DMA 请求,携带 Stream ID。
  2. SMMU 用 Stream ID 查 Stream Table,找到对应的 STE。
  3. STE 指向 Context Descriptor,CD 里存着页表基地址和配置。
  4. 硬件先走 Stage 1 页表(VA→IPA),再走 Stage 2 页表(IPA→PA)。
  5. 最终拿到物理地址,访问内存。
调优小技巧: 如果你发现 TLB 缺失率偏高,可以检查一下 Stage 1 页表的块映射比例。我习惯用 perf stat -e smmu_tlb_miss 先看整体缺失率,再用 smmu_debugfs 导出页表统计信息,分析大页使用情况。

3.5 关键配置参数速查

最后,我整理了一张常用配置参数表。调试的时候对照着看,能省不少时间。

参数 所属结构 作用 常见错误
StreamID 事务请求 标识设备 ID 超出范围,查不到 STE
STE.S1ContextPtr Stream Table 指向 Stage 1 CD 指针未对齐,触发异常
CD.TTB0 Context Descriptor Stage 1 页表基址 地址写错,翻译全乱
CD.ASID Context Descriptor 标识地址空间 未更新,TLB 命中错误
Stage1/2 页表 页表层级 VA→IPA→PA 映射 权限位设置错误,触发 Fault
再强调一次: 调试时如果遇到 Translation Fault,先别急着翻页表。先确认 Stream ID 对不对,再查 STE 和 CD 的指针是否有效。我见过太多人一上来就盯着页表看半天,结果发现是 STE 的配置根本没生效。