4、StreamID与SubstreamID:StreamID的分配策略,SubstreamID在虚拟化中的应用

好,咱们进入第四章。StreamID 和 SubstreamID,这两个概念是 SMMU 寻址的基石。说白了,SMMU 怎么知道当前访问内存的设备是谁?就是靠 StreamID。而 SubstreamID 呢?它是虚拟化场景下,让同一个设备的不同虚拟机或进程能共享 SMMU 页表的关键。

我个人习惯把 StreamID 比作设备的“身份证号”,SubstreamID 则是这个设备上不同“工作上下文”的编号。搞不清楚这两个 ID 的分配,后面 TLB 性能调优根本无从谈起。我在项目中遇到过好几次,因为 StreamID 分配冲突,导致设备 DMA 访问直接挂死,查了两天才定位到问题。

4.1 StreamID:设备的硬件身份证

StreamID 通常由硬件总线接口(比如 PCIe Requester ID、AMBA Stream ID)直接生成,或者由固件在设备初始化时静态配置。它必须全局唯一,不能重复。

核心原则: 一个 StreamID 唯一标识一个“请求者”(Requestor)。在物理系统中,一个物理设备通常只有一个 StreamID。

4.1.1 StreamID 的分配策略

分配策略主要分两种:静态分配和动态分配。我建议在大多数嵌入式系统中使用静态分配,简单可靠。

策略 描述 适用场景 风险点
静态分配 固件在启动时,为每个设备分配固定的 StreamID。通常写在设备树或 ACPI 表中。 设备数量固定、拓扑简单的系统(如手机 SoC、嵌入式设备)。 灵活性差,硬件变更需要修改固件。
动态分配 系统运行时,由 SMMU 驱动或 hypervisor 动态分配 StreamID。设备热插拔场景常用。 PCIe 热插拔、虚拟化环境(VF 分配)。 分配冲突、资源耗尽、需要复杂的 ID 管理算法。

嗯,这里要注意。静态分配虽然简单,但一定要预留足够的 StreamID 空间。我曾经在一个项目里,因为只给每个 PCIe 端点分配了 8 位 StreamID,结果后来扩展了 3 个多功能设备,直接导致 ID 溢出。最后不得不改硬件设计,教训深刻。

4.1.2 StreamID 与 SMMU 配置的绑定

StreamID 一旦确定,就需要在 SMMU 的 Stream Table Entry (STE) 中配置对应的映射关系。每个 STE 包含了该 StreamID 的转换规则、页表基地址、权限控制等信息。

// 伪代码:配置 StreamID 对应的 STE
// 假设 StreamID = 0x5
smmu_ste_t *ste = get_ste_base() + 0x5;
ste->valid = 1;
ste->config = SMMU_CONFIG_BYPASS; // 或者 SMMU_CONFIG_TRANSLATE
ste->ttb0 = page_table_base_address;
ste->asid = 0x100; // 关联的 ASID
避坑指南: 我曾经在配置 STE 时,忘记设置 valid 位,结果设备 DMA 一直报错。SMMU 会直接丢弃无效 STE 对应的 StreamID 请求。所以,写完 STE 后,一定要回读验证。

4.2 SubstreamID:虚拟化下的上下文标识

SubstreamID 是 ARM SMMUv3 引入的概念。它允许一个物理设备(一个 StreamID)内部,区分来自不同虚拟机或不同进程的请求。你想想看,一个支持 SR-IOV 的网卡,它有多个虚拟功能(VF),每个 VF 属于不同的虚拟机。如果没有 SubstreamID,SMMU 怎么知道这个 DMA 请求是来自 VM1 还是 VM2?

SubstreamID 就是干这个的。它通常由设备在 DMA 描述符中携带,或者由系统软件在 PASID(Process Address Space ID)中编码。

4.2.1 SubstreamID 在虚拟化中的角色

在虚拟化场景下,SubstreamID 的核心作用是实现 共享虚拟内存(SVM) 和 设备直通(Device Passthrough)。

  • 设备直通: 将物理设备(或 VF)直接分配给虚拟机。此时,StreamID 标识物理设备,SubstreamID 标识虚拟机内部的进程或上下文。
  • SVM: 设备可以直接访问进程的虚拟地址空间。SubstreamID 就是进程的 PASID。SMMU 根据 SubstreamID 查找对应的进程页表。
重要: SubstreamID 不是必须的。如果设备不支持 PASID,或者系统不使用 SVM,那么 SubstreamID 可以忽略(通常设为 0)。但一旦启用,必须保证 SubstreamID 在同一个 StreamID 下是唯一的。

4.2.2 SubstreamID 的分配与管理

SubstreamID 的分配通常由 hypervisor 或设备驱动完成。我个人建议的分配策略是:

  1. 按虚拟机分配: 每个虚拟机分配一个唯一的 SubstreamID 范围。例如,VM1 使用 0x100-0x1FF,VM2 使用 0x200-0x2FF。
  2. 按进程分配: 在 SVM 场景下,每个进程分配一个 PASID(即 SubstreamID)。这个 PASID 由操作系统内核管理。
  3. 复用与回收: 当虚拟机或进程销毁时,对应的 SubstreamID 必须回收。否则会导致 ID 泄漏,最终耗尽资源。

我记得有一次调试一个虚拟化环境,发现某个 VF 的 DMA 请求总是被 SMMU 拒绝。查了半天,发现是 hypervisor 在分配 SubstreamID 时,给两个不同的 VF 分配了相同的 ID。嗯,这种低级错误,往往是最难查的。

4.3 StreamID 与 SubstreamID 的协同工作

在实际的 SMMU 查找过程中,StreamID 和 SubstreamID 是组合使用的。SMMU 先根据 StreamID 找到对应的 STE,然后根据 SubstreamID(如果存在)找到对应的 Substream Table Entry (SSTE) 或直接使用 STE 中的配置。

下面这张图展示了它们的关系:

StreamID 与 SubstreamID 协同工作流程图 StreamID 与 SubstreamID 协同工作流程 设备 DMA 请求 SMMU 提取 StreamID 查找 Stream Table Entry (STE) 有 SubstreamID? 是 查找 SSTE 否 使用 STE 默认配置 执行地址转换 StreamID 全局唯一 SubstreamID 在 StreamID 内唯一

4.4 实际项目中的分配建议

基于我多年的经验,我总结了几条 StreamID 和 SubstreamID 的分配原则,希望能帮你少走弯路:

  • 预留足够的 ID 空间: 设计阶段就要考虑未来扩展。StreamID 至少预留 16 位,SubstreamID 至少预留 20 位。
  • 使用连续的 ID 范围: 方便 SMMU 的 Stream Table 实现,也便于调试时快速定位。
  • 避免 ID 复用: 特别是在热插拔场景下,设备移除后必须释放 ID,新设备不能立即使用同一个 ID,要有冷却期。
  • 记录分配日志: 在固件中记录每次 StreamID/SubstreamID 的分配情况。我曾经靠这个日志,在 5 分钟内定位了一个 ID 冲突问题。
个人技巧: 我习惯在固件中维护一个“ID 分配表”,以数组形式存储。每次分配时,遍历数组检查是否已被占用。虽然简单,但非常有效。