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 怎么分?这取决于你的系统设计。常见的做法有几种:
- 静态分配:每个设备固定一个 ID,写在硬件里。简单,但不灵活。
- 动态分配:系统启动时,固件扫描设备拓扑,动态分配 ID。灵活,但需要管理。
- 基于拓扑的分配:用总线位置(比如 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,我一般按这几步走:
- 分配内存:在系统内存中分配一块连续区域,大小 = STE 数量 × 64 字节
- 清零:把整块区域清零,避免残留数据导致误判
- 填充 STE:对每个设备,填写对应的 STE 条目
- 设置 SMMU 寄存器:把 Stream Table 的基地址写入 SMMU 的
SMMU_STRTAB_BASE寄存器 - 使能:设置
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 的关系:
这张图从左到右展示了完整的数据流:设备发起 DMA → 携带 Stream ID → SMMU 查 Stream Table → 找到 STE → 获取页表 → 完成翻译。你想想看,没有 Stream Table,这个链条就断了。
4.9 小结
这一章的内容,说白了就是两件事:
- Stream ID 是设备的“身份证”
- Stream Table 是查身份证的“户籍系统”