URMA Completion and Concurrency
两个环形队列
SQ 和 CQ 都可先看成带索引的环形数组:
1 | SQ:软件写 WQE 设备读 WQE |
正常情况下,软件可以填写尚未发布的 SQ 槽位,设备同时消费更早发布的槽位。这就是队列带来的生产者—消费者并行。
关键约束是:
- 软件不能覆盖设备尚未消费的 WQE。
- 软件必须先把完整 WQE 写到设备可见位置,再更新 PI 和 Doorbell。
- 应用不能把尚未完成的本地缓冲区提前复用。
- 软件消费 CQE 后,要按实现要求推进 CQ 的消费状态。
所以,“SQ 与 WQE 能否同时写”并不是准确问法。SQ 是容器,软件写的是 SQ 中尚且空闲的 WQE 槽位。
提交、执行与完成
一次请求至少经历三个不同时间点:
- 提交成功:URMA 接口接受 WR,Provider 已将其放入发送路径。
- 设备执行:UDMA 设备取到 WQE,并执行数据传输。
- 完成可见:设备生成 CQE,应用从 JFC 得到成功 CR。
1 | post 返回 设备传输 poll 得到 CR |
post 成功不能代替 poll 成功。尤其是非阻塞操作,源缓冲区的复用时间、目标数据的消费时间,都必须遵守接口定义的完成语义。
用 user_ctx 找回请求
多个请求同时在途时,不能只凭“我先提交了谁”来猜当前完成项对应谁。更稳妥的做法是给每条 WR 分配唯一上下文:
1 | for (uint64_t i = 0; i < request_count; ++i) { |
user_ctx 是应用关联信息。它能帮助找到请求,但不提供互斥、顺序或重试能力。
一次提交多条 WR
WR 的 next 字段可用于串起链表:
1 | wr0.next = &wr1; |
这能减少调用开销,但要处理部分失败:
bad_wr == NULL:没有报告失败节点。bad_wr != NULL:它指向第一条未成功提交的 WR;前面的 WR 可能已经进入 SQ。
四种“同时写”
| 场景 | 是否可行 | 原因与条件 |
|---|---|---|
| 软件填写新 WQE,设备消费旧 WQE | 可以 | 环形队列本来就为生产者和消费者并行设计,但不能越过容量边界 |
| 一个 SQ 中存在多条未完成 WQE | 可以 | 这就是队列深度的意义;仍要处理信用、完成和缓冲区生命周期 |
| 多个 Host 线程调用同一 Jetty 提交 | 取决于 Provider 配置 | 公共接口可被并发调用,但具体 Provider 需要内部锁,或要求调用者在 lock-free 模式自行串行化 |
| 多个 AIV 直接更新同一 QP 的 head 和 Doorbell | 不可以直接这样做 | 共享 head、SQ 槽位和 Doorbell,若无专门同步会产生覆盖与丢更新 |
Host 多线程
在常规 Host 路径中,应用调用 URMA 接口,Provider 负责构造 WQE。当前 UDMA Provider 的常规配置会在提交临界区使用自旋锁,大致保护以下步骤:
1 | 加锁 |
因此,多个线程可以发起调用,但同一 SQ 的实际发布步骤仍需要串行化。如果启用 Provider 的 lock-free 配置,锁被移除不表示冲突消失,而是互斥责任转移给调用者。
最容易维护的策略是:
- 单个 QP 由一个提交线程拥有;或者
- 多线程共享时在更高层明确加锁;或者
- 为独立流量分配不同 QP。
AIV 多核直驱
AIV 直驱绕开 Host Provider,由 Device 代码直接构造 WQE、推进 head 并写 Doorbell。多个 AIV 若同时使用同一 QP,可能出现:
1 | AIV 0 读取 head = 8 |
最终至少有一条 WQE 被覆盖,而且 head 只前进了一次。这不是 payload 地址是否相同的问题,而是队列元数据本身发生竞争。
SHMEM UDMA 的常见并行方式是给不同 AIV 分配不同 QP,使每个 AIV 独占自己的 SQ/CQ/head/Doorbell。第五篇会给出对应代码。
多 QP 也不自动解决数据竞争
多 QP 解决的是队列生产者冲突,并不自动解决业务数据冲突:
1 | QP 0 ── WRITE 100 bytes ──┐ |
两条路径若同时写同一远端地址,最终值取决于实际执行与可见顺序。正确做法通常是让不同 QP 操作不重叠的数据片段,或者在协议层建立明确同步。
同理,不同 QP 之间不能仅凭提交先后推断远端可见顺序。需要跨 QP 依赖时,应使用完成、signal、barrier 或上层协议表达。
顺序与完成不是一回事
可以先用两个问题区分:
- 顺序问题:操作 B 会不会在操作 A 之后被观察?
- 完成问题:操作 A 是否已经执行到可以安全复用资源的程度?
Fence 类语义主要约束操作顺序,Quiet 或完成项主要回答在途操作是否完成。某个实现可能让 Fence 比规范要求更强,但可移植代码不应依赖这种偶然增强。
对于“先写数据,再通知对端”的协议,最直接的表达不是两次无关联的普通写,而是使用带顺序保证的 signal 操作,或者显式建立写完成与通知之间的依赖。本系列第四篇会用 putmem_signal 展开。
队列满时怎么办
SQ 深度有限。生产速度持续高于设备消费速度时,提交会遇到信用不足或队列满。处理策略包括:
- 轮询并回收完成项。
- 限制同时在途请求数。
- 使用批量投递降低单次调用开销,但仍遵守队列容量。
- 在真正存在独立流量时增加 QP,而不是无条件扩大并发。
完成队列也必须及时消费。只投递、不轮询,最终可能让完成资源耗尽。
缓冲区复用规则
对于非阻塞 WRITE,可用一条保守规则避免大部分错误:
1 | 写入本地源缓冲区 |
READ 则是:
1 | 提交 READ |
如果为了性能提前复用,必须确认所用接口给出的更精确完成定义,而不能靠时间延迟猜测。
本篇结论
- WQE 可以排在 SQ 中并保持多条在途,这是正常工作方式。
- 软件与设备可以分别操作不同的队列位置,前提是索引、容量和可见性协议正确。
- 同一 Host Jetty 的多线程提交需要 Provider 或调用者串行化关键更新。
- 多个 AIV 不应无同步地直驱同一 QP,常见方案是一核一 QP。
- 多 QP 只隔离队列状态,不隔离远端数据地址。
- 提交成功、顺序成立、操作完成是三个不同判断。
参考资料
URMA Completion and Concurrency