URMA Write and Read
Case 目标
运行两个进程:
- Server 注册一块缓冲区,并等待 Client 写入。
- Client 把字符串
hello urma单边写入 Server 缓冲区。 - 两端通过 TCP socket 交换 Jetty、Segment 和 token 等控制信息。
- 真正的数据传输由 URMA WRITE 完成,不经过这条 socket。
1 | 控制面:Client ←──── socket 交换描述符 ────→ Server |
socket 只是教学样例中的带外通道。它解决“如何认识对端资源”,不负责搬运 hello urma。
资源分别是什么类型
官方样例通常会定义自己的 context_t,把 URMA 资源和应用状态集中保存。**context_t 是样例结构,不是 URMA 公共类型。**
下面是一份便于阅读的精简版:
1 | typedef struct sample_context { |
逐项理解:
- **
void *va**:CPU 能读写的本地地址,尚不能单独证明 UDMA 有权访问。 - **
urma_target_seg_t *local_tseg**:本地缓冲区完成注册后的目标 Segment 句柄。 - **
urma_seg_t remote_seg**:适合经 socket 交换的远端资源描述,但还不能直接放入本端 WR。 - **
urma_target_seg_t *import_tseg**:本端导入远端描述后得到的可用目标句柄。 - **
urma_jetty_t *jetty**:本端创建的通信端点。 - **
urma_target_jetty_t *t_jetty**:对端 Jetty 在本端的导入句柄,提交 WRITE 时用它选目标端点。 - **
urma_jfc_t *jfc**:请求完成后,应用从这里轮询 CR。 - **
uint64_t rid**:应用自己分配的关联号,提交时放入user_ctx,完成时再取回。
初始化流程
完整初始化涉及较多属性结构。对初学者,先按下面的依赖顺序理解:
1 | // 教学伪代码:函数参数应以当前 UMDK 头文件为准。 |
顺序背后的原因是:
- 没有 Context,就无法创建归属于设备的队列和端点。
- 没有 JFC,Jetty 就没有完成项的落点。
- 本地内存必须先注册,才能形成可交换的 Segment 描述。
- 必须先拿到对端描述,才能导入远端 Jetty 和 Segment。
- 所有依赖就绪后,才有条件构造 WRITE。
构造一条 WRITE
下面的函数保留了官方样例的关键结构。错误处理被收缩,便于先读懂主线:
1 | static int post_one_write(sample_context_t *ctx) |
第 1 段:准备本地数据
1 | snprintf((char *)ctx->va, msg_size, "hello urma"); |
数据先写入已经注册过的本地缓冲区。WRITE 的源地址不是字符串常量地址,而是 ctx->va。
第 2 段:描述源 SGE
1 | .addr = (uint64_t)ctx->va, |
这三个字段共同表达:“从本地已注册 Segment 中,以 ctx->va 为起点读取 msg_size 字节”。如果地址落在 Segment 范围之外,或 Segment 权限不符合要求,请求会失败。
第 3 段:描述目标 SGE
1 | .addr = ctx->remote_seg.ubva.va, |
目标地址来自对端交换过来的 Segment 描述,目标句柄则来自本端的导入结果。二者必须指向同一远端注册区域。
第 4 段:把 SGE 组成 SG
1 | .sge = &src_sge, |
本例只有一段连续内存,所以源、目标各使用一个 SGE。SG 仍然存在,是因为接口也要支持多段不连续内存。
第 5 段:构造读写描述
1 | urma_rw_wr_t rw = { |
rw 只表达数据从哪里到哪里,还没有说明操作是 WRITE 还是 READ。
第 6 段:构造 WR
1 | .opcode = URMA_OPC_WRITE, |
- **
opcode**:把这条读写描述解释成 WRITE。 - **
tjetty**:指定对端通信端点。 - **
user_ctx**:给应用一个匹配请求与完成项的标识。 - **
rw**:挂接刚才的源和目标 SG。 - **
next**:本例不批量串接下一条 WR。
两个标志也很关键:
- **
complete_enable = 1**:请求设备生成完成项。 - **
inline_flag = 0**:payload 位于 SGE 指向的内存,不内联放进 WQE。
第 7 段:提交 WR
1 | urma_post_jetty_send_wr(ctx->jetty, &wr, &bad_wr); |
这个函数把一条或一串 WR 交给 Jetty 的发送路径。Provider 会检查参数、选择 SQ 槽位、编码 WQE、更新生产者索引并通知设备。
若批量提交中途失败,bad_wr 用于指出第一条未成功提交的 WR。即使返回成功,也只代表已提交,不代表设备已完成远端写。
第 8 段:轮询完成
1 | ret = urma_poll_jfc(ctx->jfc, 1, &cr); |
- 返回
0:当前没有完成项,可以继续轮询或采用事件机制等待。 - 返回正数:取到了相应数量的 CR。
- 返回负数:轮询过程出错。
拿到 CR 后还要检查:
cr.status是否为成功状态。cr.user_ctx是否等于本次请求的rid。
如何改成 READ
READ 的数据方向与 WRITE 相反:
1 | WRITE:本地 src ──→ 远端 dst |
因此改动有两类:
1 | wr.opcode = URMA_OPC_READ; |
轮询完成后,再读取本地目标缓冲区。不要在 READ 完成前假设数据已经到达。
为什么还有 urma_write()
URMA 同时提供简便接口和通用 WR 接口:
- **
urma_write()**:适合单条常见操作,内部帮忙组织一部分 WR 结构。 - **
urma_post_jetty_send_wr()**:适合显式控制 opcode、标志、链表和批量提交。
它们最终都要进入 Provider 的提交路径。学习时先手动构造一次 WR,可以看清数据结构;业务代码则可根据控制需求选择更简洁的接口。
常见失败点
- 把普通指针直接当远端地址:远端地址必须来自对端交换的 Segment 描述。
- 收到描述却没有导入:WR 中要使用
import_tseg和t_jetty。 - 源或目标越界:地址加长度必须落在相应 Segment 范围内。
- 权限不匹配:注册和导入属性需要允许相应 READ/WRITE 操作。
- 没有请求完成项:未设置
complete_enable却等待对应 CR。 - 把提交成功当作传输完成:必须按完成语义等待。
- 直接复制不同版本样例:结构字段和参数要与本机安装的 UMDK 头文件匹配。