URMA Write and Read
导言
本文把 URMA 官方样例中的核心 WRITE 路径收缩成一个教学 Case。目标不是复制几百行初始化代码,而是让零基础读者看懂:程序要准备哪些资源、每个结构体字段是什么意思、一条请求如何提交和确认完成。文中的核心代码按官方接口整理;初始化和带外交换用伪代码表示,实际编译时应以所使用 UMDK 版本的头文件与官方样例为准。
系列位置
- URMA Mental Model:对象与数据路径
- URMA Write and Read:最小读写程序
- URMA Completion and Concurrency:完成、顺序与并发
- SHMEM UDMA Programming:对称内存与 put 接口
- AIV UDMA Direct Drive:
st_dev、多 QP 与 Relay
源码截面
核心 WRITE/READ 结构核对到 openeuler/umdk@a2e11613f0f8174c0170413952ac4a5364a99a70 的 urma_sample.c。文中重命名并收缩了样例上下文,但保留了 SGE → SG → RW WR → post → poll 主线。
Case 目标¶
运行两个进程:
- Server 注册一块缓冲区,并等待 Client 写入。
- Client 把字符串
hello urma单边写入 Server 缓冲区。 - 两端通过 TCP socket 交换 Jetty、Segment 和 token 等控制信息。
- 真正的数据传输由 URMA WRITE 完成,不经过这条 socket。
socket 只是教学样例中的带外通道。它解决“如何认识对端资源”,不负责搬运 hello urma。
资源分别是什么类型¶
官方样例通常会定义自己的 context_t,把 URMA 资源和应用状态集中保存。context_t 是样例结构,不是 URMA 公共类型。
下面是一份便于阅读的精简版:
typedef struct sample_context {
void *va; // 本地缓冲区的虚拟地址
urma_target_seg_t *local_tseg; // 注册本地内存后得到的句柄
urma_seg_t remote_seg; // 从对端收到的可交换 Segment 描述
urma_target_seg_t *import_tseg; // 导入 remote_seg 后得到的可用句柄
urma_jetty_t *jetty; // 本端 Jetty
urma_target_jetty_t *t_jetty; // 导入对端 Jetty 后得到的目标句柄
urma_jfc_t *jfc; // 本端完成队列
uint64_t rid; // 应用定义的请求 ID
} sample_context_t;
逐项理解:
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,完成时再取回。
描述与句柄
remote_seg 是从网络收来的描述,import_tseg 才是本端 Provider 接受的导入句柄。这两个字段表达的是同一远端资源在不同阶段的形态,不能相互替代。
初始化流程¶
完整初始化涉及较多属性结构。对初学者,先按下面的依赖顺序理解:
// 教学伪代码:函数参数应以当前 UMDK 头文件为准。
urma_init(...);
dev = find_device_and_eid(...);
ctx = urma_create_context(dev, eid_index);
jfc = urma_create_jfc(ctx, ...);
jetty = urma_create_jetty(ctx, jfc, ...);
va = aligned_alloc(...);
local_tseg = urma_register_seg(ctx, va, size, permissions, ...);
exchange_over_socket(local_jetty_desc, local_seg_desc, token);
t_jetty = urma_import_jetty(ctx, remote_jetty_desc, ...);
import_tseg = urma_import_seg(ctx, remote_seg_desc, remote_token, ...);
顺序背后的原因是:
- 没有 Context,就无法创建归属于设备的队列和端点。
- 没有 JFC,Jetty 就没有完成项的落点。
- 本地内存必须先注册,才能形成可交换的 Segment 描述。
- 必须先拿到对端描述,才能导入远端 Jetty 和 Segment。
- 所有依赖就绪后,才有条件构造 WRITE。
构造一条 WRITE¶
下面的函数保留了官方样例的关键结构。错误处理被收缩,便于先读懂主线:
static int post_one_write(sample_context_t *ctx)
{
const uint32_t msg_size = 64;
snprintf((char *)ctx->va, msg_size, "hello urma");
urma_sge_t src_sge = {
.addr = (uint64_t)ctx->va,
.len = msg_size,
.tseg = ctx->local_tseg,
};
urma_sge_t dst_sge = {
.addr = ctx->remote_seg.ubva.va,
.len = msg_size,
.tseg = ctx->import_tseg,
};
urma_sg_t src_sg = {
.sge = &src_sge,
.num_sge = 1,
};
urma_sg_t dst_sg = {
.sge = &dst_sge,
.num_sge = 1,
};
urma_rw_wr_t rw = {
.src = src_sg,
.dst = dst_sg,
};
urma_jfs_wr_t wr = {
.opcode = URMA_OPC_WRITE,
.tjetty = ctx->t_jetty,
.user_ctx = ctx->rid,
.rw = rw,
.next = NULL,
};
wr.flag.bs.complete_enable = 1;
wr.flag.bs.inline_flag = 0;
urma_jfs_wr_t *bad_wr = NULL;
int ret = urma_post_jetty_send_wr(ctx->jetty, &wr, &bad_wr);
if (ret != URMA_SUCCESS) {
return ret;
}
urma_cr_t cr;
do {
ret = urma_poll_jfc(ctx->jfc, 1, &cr);
} while (ret == 0);
if (ret < 0 || cr.status != URMA_CR_SUCCESS ||
cr.user_ctx != ctx->rid) {
return -1;
}
return 0;
}
第 1 段:准备本地数据¶
数据先写入已经注册过的本地缓冲区。WRITE 的源地址不是字符串常量地址,而是 ctx->va。
第 2 段:描述源 SGE¶
这三个字段共同表达:“从本地已注册 Segment 中,以 ctx->va 为起点读取 msg_size 字节”。如果地址落在 Segment 范围之外,或 Segment 权限不符合要求,请求会失败。
第 3 段:描述目标 SGE¶
目标地址来自对端交换过来的 Segment 描述,目标句柄则来自本端的导入结果。二者必须指向同一远端注册区域。
第 4 段:把 SGE 组成 SG¶
本例只有一段连续内存,所以源、目标各使用一个 SGE。SG 仍然存在,是因为接口也要支持多段不连续内存。
第 5 段:构造读写描述¶
rw 只表达数据从哪里到哪里,还没有说明操作是 WRITE 还是 READ。
第 6 段:构造 WR¶
opcode:把这条读写描述解释成 WRITE。tjetty:指定对端通信端点。user_ctx:给应用一个匹配请求与完成项的标识。rw:挂接刚才的源和目标 SG。next:本例不批量串接下一条 WR。
两个标志也很关键:
complete_enable = 1:请求设备生成完成项。inline_flag = 0:payload 位于 SGE 指向的内存,不内联放进 WQE。
第 7 段:提交 WR¶
这个函数把一条或一串 WR 交给 Jetty 的发送路径。Provider 会检查参数、选择 SQ 槽位、编码 WQE、更新生产者索引并通知设备。
若批量提交中途失败,bad_wr 用于指出第一条未成功提交的 WR。即使返回成功,也只代表已提交,不代表设备已完成远端写。
第 8 段:轮询完成¶
- 返回
0:当前没有完成项,可以继续轮询或采用事件机制等待。 - 返回正数:取到了相应数量的 CR。
- 返回负数:轮询过程出错。
拿到 CR 后还要检查:
cr.status是否为成功状态。cr.user_ctx是否等于本次请求的rid。
如何改成 READ¶
READ 的数据方向与 WRITE 相反:
因此改动有两类:
wr.opcode = URMA_OPC_READ;
rw.src = remote_sg; // 远端地址 + import_tseg
rw.dst = local_sg; // 本地地址 + local_tseg
轮询完成后,再读取本地目标缓冲区。不要在 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 头文件匹配。
建议练习
先运行官方 urma_sample,确认环境和 EID 正常;再只修改消息内容、长度和请求 ID。环境打通后,再尝试把 WRITE 改成 READ。这样能把“环境问题”和“WR 构造问题”分开。