3. StreamID 与上下文:StreamID 的概念、上下文描述符 (Context Descriptor)、STE (Stream Table Entry) 配置

好,咱们进入第三章。这一章要聊的东西,是 SMMU 里最核心的寻址逻辑——StreamID。说白了,就是 SMMU 怎么知道“谁”在发请求,以及该给这个“谁”用哪套页表。

我刚开始接触 SMMU 时,觉得 StreamID 不就是个编号嘛,有啥好讲的?后来在调试一个 PCIe 设备地址翻译错误时才发现,StreamID 配错了,整个系统都可能挂掉。嗯,这东西没那么简单。

3.1 StreamID 是什么?

StreamID,全称 Stream Identifier,是 SMMU 用来识别每个“数据流”的唯一标识。每个发起 DMA 请求的设备(比如网卡、GPU、NVMe 控制器),都会携带一个 StreamID。

你可以把它想象成设备的“身份证号”。SMMU 拿到这个 ID 后,才能去查对应的配置,决定怎么翻译地址、要不要做权限检查。

关键点:StreamID 通常由硬件线直接给出,不是软件分配的。比如 PCIe 设备的 StreamID 就是由 Requester ID(Bus:Device:Function)映射而来。

StreamID 的位宽是固定的,ARM 架构规定最大 20 位。但实际芯片里用多少位,取决于系统里挂了多少设备。我见过 8 位的,也见过 16 位的。

3.2 StreamID 到 STE 的映射

SMMU 拿到 StreamID 后,下一步就是找到对应的 STE(Stream Table Entry)。STE 里存着这个流的所有配置信息,包括:

  • 要不要翻译地址
  • 用哪套页表(Stage-1 还是 Stage-2)
  • 上下文描述符的指针
  • 安全属性、权限控制

映射方式有两种:

映射方式 说明 适用场景
线性映射 StreamID 直接作为 STE 数组的索引 设备数量少,StreamID 连续
二级映射 先查 L1 表,再查 L2 表,类似页表 设备数量多,StreamID 稀疏

我个人习惯用线性映射,简单直接。但如果你系统里有上百个设备,StreamID 又不连续,二级映射能省不少内存。

经验之谈:我曾经在一个项目里,设备 StreamID 从 0 到 0x3FF 都有,但实际只用了 20 多个。如果用线性映射,得分配 1024 个 STE 条目,每个 64 字节,就是 64KB。二级映射只需要几千字节。省下来的内存,给别的模块用不香吗?

3.3 STE 的结构与配置

STE 是 SMMU 固件配置的核心。每个 STE 固定 64 字节,里面塞满了各种控制位和指针。

咱们挑几个最重要的字段说说:

字段 位域 作用
Valid [0] STE 是否有效。无效的 STE 会导致 SMMU 报错
Config [1:2] 翻译模式:不翻译、Stage-1、Stage-2、Stage-1+2
S1ContextPtr [16:63] 指向 Stage-1 上下文描述符的指针
S2TTB [12:63] Stage-2 页表基地址
MTCFG [8:11] 内存属性配置,影响缓存策略

配置 STE 时,有几点要特别注意:

  • Valid 位最后置 1:先填好其他字段,最后再置 Valid。否则 SMMU 可能读到半成品。
  • 指针要对齐:S1ContextPtr 必须 64 字节对齐,否则 SMMU 直接忽略。
  • Config 不能乱设:如果设备不需要翻译,就设成“不翻译”,别画蛇添足。

避坑指南:我曾经在配置 STE 时,忘了把 Valid 位清零就修改其他字段。结果 SMMU 在修改过程中读到了 STE,直接报了个“STE 配置错误”的异常。排查了半天才发现是 Valid 位没处理好。记住:改 STE 前,先清 Valid,改完再置位。

3.4 上下文描述符 (Context Descriptor)

上下文描述符,简称 CD,是 Stage-1 翻译的配置入口。STE 里的 S1ContextPtr 指向的就是它。

CD 的结构和 STE 类似,也是 64 字节。它主要包含:

  • Stage-1 页表基地址 (TTB0, TTB1)
  • 地址空间大小 (T0SZ, T1SZ)
  • ASID (Address Space ID),用于 TLB 区分不同进程
  • 权限控制 (AP, XN, PXN)

为什么要有 CD?你想想看,STE 是设备级别的配置,一个设备可能跑多个进程。CD 就是进程级别的配置。STE 指向 CD,CD 再指向页表,这样就实现了“设备→进程→地址空间”的三级映射。

核心逻辑:StreamID → STE → CD → 页表。这是 SMMU 地址翻译的完整链路。

3.5 配置流程实战

说了这么多理论,咱们看看实际怎么配。下面是一个简化的配置流程:

  1. 分配 STE 和 CD 内存:必须从非缓存内存池分配,保证 SMMU 读到的是最新值。
  2. 初始化 STE:清 Valid,填 Config、S1ContextPtr 等字段。
  3. 初始化 CD:填页表基地址、ASID、地址空间大小。
  4. 置位 STE.Valid:让 SMMU 开始使用这个 STE。
  5. 刷新 SMMU 缓存:通过 CMD_SYNC 确保配置生效。

代码示例(伪代码):

// 配置 STE
ste->valid = 0;          // 先清 Valid
ste->config = STAGE1_ONLY;
ste->s1contextptr = cd_phys_addr;
ste->mtcfg = NORMAL_MEMORY;
dmb();                   // 保证写入顺序
ste->valid = 1;          // 最后置 Valid

// 配置 CD
cd->ttb0 = page_table_phys;
cd->t0sz = 39;           // 512GB 地址空间
cd->asid = 0x42;
cd->ap = RW_EL1;         // 读写权限

// 发送 CMD_SYNC
smmu_cmd_sync();

小技巧:配置完成后,可以用 SMMU 的调试寄存器读回 STE 和 CD 的内容,确认写入是否正确。我每次配完都会做这个检查,省得后面出问题再回头查。

3.6 知识结构图

下面这张图展示了 StreamID 到页表的完整映射链路:

StreamID → STE → CD → 页表 映射链路 PCIe 设备 StreamID STE CD Stage-1 页表 说明: 1. 设备发起 DMA 请求时携带 StreamID 2. SMMU 用 StreamID 索引 STE,获取设备级配置 3. STE 中的 S1ContextPtr 指向 CD,CD 再指向 Stage-1 页表

3.7 常见问题与排查思路

配置 StreamID 和 STE 时,最容易出问题的几个地方:

  • StreamID 越界:设备发出的 StreamID 超出了 STE 表范围,SMMU 直接报 C_BAD_STREAMID 错误。
  • STE 未初始化:Valid 位为 0,SMMU 访问时报错。
  • CD 指针错误:S1ContextPtr 指向了无效内存,或者没有 64 字节对齐。
  • 缓存一致性问题:STE/CD 在内存中更新了,但 SMMU 的缓存还是旧的。需要发 CMD_SYNC 或 CMD_TLBI。

重要提醒:如果你发现设备 DMA 请求一直报错,先查 StreamID 对不对。我遇到过好几次,硬件工程师把 PCIe 设备的 BDF 号改了,但固件里还是旧的 StreamID 映射表。两边一核对,问题就找到了。

嗯,这一章的内容就到这儿。StreamID、STE、CD 这三者的关系,是 SMMU 配置的基石。搞懂了它们,后面的多级缓存和性能调优才能有的放矢。