4. Stream Table 与 Stream ID:Stream Table 的作用、Stream ID 的分配与配置

好,咱们进入第四章。这一章聊的是 SMMU 里一个非常核心的概念——Stream Table 和 Stream ID。

说实话,我刚接触 SMMU 那会儿,最头疼的就是这个 Stream ID。它到底是个什么东西?为什么要有它?怎么配?踩过几次坑之后,我才真正搞明白。今天我把这些经验掰开了讲给你听。

4.1 为什么需要 Stream Table?

先想一个问题:系统里那么多设备(GPU、Display、USB、Ethernet...),它们都要访问内存。SMMU 怎么知道哪个设备该用哪套页表?

答案就是——Stream Table。

你可以把它理解成一张“设备-配置”的映射表。每个设备发起的 DMA 事务,都会带一个 Stream ID。SMMU 拿到这个 ID 后,去 Stream Table 里查一下,就知道该用哪套页表、走哪个上下文。

我习惯把 Stream Table 比作一个字典:

  • Key 是 Stream ID
  • Value 是 Stream Table Entry(STE),里面存了页表指针、配置属性

没有这张表,SMMU 就是个瞎子。

核心要点:Stream Table 是 SMMU 的“设备注册中心”。每个设备必须先在这里“登记”,才能正常做 DMA。

4.2 Stream ID 从哪来?

Stream ID 不是凭空产生的。它由系统总线(比如 AMBA ACE/CHI)在事务层携带。具体来说:

  • 设备发起读/写请求时,总线会带上一个 Stream ID 字段
  • SMMU 在接收事务时,从总线协议中提取这个 ID
  • 然后用它去索引 Stream Table

我记得有一次调试一个 PCIe 设备,发现它的 Stream ID 总是配不对。后来查了 PCIe 的 ATS 规范才发现,PCIe 的 Stream ID 是通过 Requester ID(Bus:Device:Function)映射过来的。嗯,这个坑我后面会细讲。

4.3 Stream Table 的结构

Stream Table 在内存中是一块连续的区域。每个条目(STE)的大小是固定的,通常是 64 字节(Arm SMMUv3 规范)。

它的布局大致如下:

偏移 字段 说明
0x00 Config STE 配置,包括是否有效、安全状态等
0x08 S1ContextPtr Stage-1 页表基地址指针
0x10 S2ContextPtr Stage-2 页表基地址指针
0x18 S1CDMax Stage-1 上下文描述符数量
0x20 S1ContextPtr(高32位) 64位地址的高32位
... ... 其他配置字段

你看,每个 STE 里最关键的就是那两个指针:S1ContextPtr 和 S2ContextPtr。它们指向了真正的页表结构。

个人经验:我建议你在固件初始化时,把 Stream Table 放在一个 4KB 对齐的物理地址上。这样 SMMU 硬件访问时效率最高。我曾经因为没对齐,导致多了一次总线延迟,性能掉了 5%。

4.4 Stream ID 的分配策略

Stream ID 怎么分?这取决于你的系统设计。常见的做法有几种:

  1. 静态分配:每个设备固定一个 ID,写在硬件里。简单,但不灵活。
  2. 动态分配:系统启动时,固件扫描设备拓扑,动态分配 ID。灵活,但需要管理。
  3. 基于拓扑的分配:用总线位置(比如 PCIe BDF)直接作为 Stream ID。这是最常用的方式。

我个人比较推荐第三种。为什么呢?因为调试方便。你看到一个 Stream ID,马上就能反推出它在总线上的位置,定位问题快很多。

举个例子:

// 伪代码:基于 PCIe BDF 生成 Stream ID
uint16_t stream_id = (bus << 8) | (device << 3) | function;
// 比如 Bus 0x01, Device 0x02, Function 0x00
// Stream ID = 0x01 << 8 | 0x02 << 3 | 0x00 = 0x0110

这样,你看到 Stream ID 0x0110,就知道是 Bus 1, Device 2, Function 0 的设备。

4.5 配置 Stream Table 的步骤

在固件里配置 Stream Table,我一般按这几步走:

  1. 分配内存:在系统内存中分配一块连续区域,大小 = STE 数量 × 64 字节
  2. 清零:把整块区域清零,避免残留数据导致误判
  3. 填充 STE:对每个设备,填写对应的 STE 条目
  4. 设置 SMMU 寄存器:把 Stream Table 的基地址写入 SMMU 的 SMMU_STRTAB_BASE 寄存器
  5. 使能:设置 SMMU_CR0 寄存器,使能 Stream Table

这里有个关键点:Stream Table 的基地址必须 64KB 对齐。这是 Arm SMMUv3 的硬性要求。我见过有人用 4KB 对齐,结果 SMMU 死活不工作,查了半天才发现是这个问题。

避坑指南:我曾经在一个项目里,因为 Stream Table 的物理地址跨越了 2MB 边界,导致某些 STE 访问时触发了总线错误。后来我强制把 Stream Table 放在一个连续的 2MB 区域内,问题就解决了。所以,尽量让 Stream Table 在一个大的连续物理区域内。

4.6 Stream ID 与 Substream ID

有些设备(比如 GPU)内部有多个上下文。这时候光靠一个 Stream ID 就不够了。SMMUv3 引入了 Substream ID 的概念。

你可以这样理解:

  • Stream ID:标识设备本身
  • Substream ID:标识设备内部的某个上下文

比如一个 GPU 有 8 个硬件队列,每个队列可以有自己的页表。那么:

  • GPU 的 Stream ID = 0x10
  • 队列 0 的 Substream ID = 0
  • 队列 1 的 Substream ID = 1
  • ...

在 STE 里,有一个字段叫 S1CDMax,它决定了这个设备支持多少个 Substream。每个 Substream 对应一个 Context Descriptor(CD),CD 里存了具体的页表信息。

说白了,Stream Table 是“设备级”的映射,而 Context Descriptor 是“上下文级”的映射。两层结构,灵活很多。

4.7 调试 Stream Table 的常用方法

调试 Stream Table 问题,我常用的手段:

  • 读 SMMU 寄存器:检查 SMMU_STRTAB_BASE 是否写对了
  • Dump 内存:把 Stream Table 的内存 dump 出来,人工检查 STE 内容
  • 触发 SMMU 事件:让设备做一次 DMA,看 SMMU 是否报错
  • 检查 C_BAD_STE 事件:如果 SMMU 报了这个事件,说明 STE 配置有问题

我记得有一次,一个设备老是报 C_BAD_STE。我 dump 了 STE 一看,发现 Config 字段的 Valid 位没置 1。嗯,这种低级错误,排查起来最费时间。

4.8 本章知识结构图

下面这张图,帮你理清 Stream Table 和 Stream ID 的关系:

Stream Table 与 Stream ID 结构图 Stream Table 与 Stream ID 结构图 Device A Device B Device C Stream ID = 0x01 Stream ID = 0x02 Stream ID = 0x03 Stream Table STE[0x01] → S1CtxPtr STE[0x02] → S1CtxPtr STE[0x03] → S1CtxPtr ... 更多 STE 页表 / CD Device A 页表 Device B 页表 Device C 页表 说明: 每个设备携带 Stream ID 访问 SMMU → SMMU 用 Stream ID 索引 Stream Table → 找到对应的 STE → 获取页表指针 → 完成地址翻译

这张图从左到右展示了完整的数据流:设备发起 DMA → 携带 Stream ID → SMMU 查 Stream Table → 找到 STE → 获取页表 → 完成翻译。你想想看,没有 Stream Table,这个链条就断了。

4.9 小结

这一章的内容,说白了就是两件事:

  • Stream ID 是设备的“身份证”
  • Stream Table 是查身份证的“户籍系统”