URMA Write and Read

导言

本文把 URMA 官方样例中的核心 WRITE 路径收缩成一个教学 Case。目标不是复制几百行初始化代码,而是让零基础读者看懂:程序要准备哪些资源、每个结构体字段是什么意思、一条请求如何提交和确认完成。文中的核心代码按官方接口整理;初始化和带外交换用伪代码表示,实际编译时应以所使用 UMDK 版本的头文件与官方样例为准。

系列位置

  1. URMA Mental Model:对象与数据路径
  2. URMA Write and Read:最小读写程序
  3. URMA Completion and Concurrency:完成、顺序与并发
  4. SHMEM UDMA Programming:对称内存与 put 接口
  5. AIV UDMA Direct Drive:st_dev、多 QP 与 Relay

源码截面

核心 WRITE/READ 结构核对到 openeuler/umdk@a2e11613f0f8174c0170413952ac4a5364a99a70urma_sample.c。文中重命名并收缩了样例上下文,但保留了 SGE → SG → RW WR → post → poll 主线。

Case 目标

运行两个进程:

  • Server 注册一块缓冲区,并等待 Client 写入。
  • Client 把字符串 hello urma 单边写入 Server 缓冲区。
  • 两端通过 TCP socket 交换 Jetty、Segment 和 token 等控制信息。
  • 真正的数据传输由 URMA WRITE 完成,不经过这条 socket。
1
2
控制面:Client ←──── socket 交换描述符 ────→ Server
数据面:Client local buffer ── URMA WRITE ──→ Server buffer

socket 只是教学样例中的带外通道。它解决“如何认识对端资源”,不负责搬运 hello urma

资源分别是什么类型

官方样例通常会定义自己的 context_t,把 URMA 资源和应用状态集中保存。**context_t 是样例结构,不是 URMA 公共类型。**

下面是一份便于阅读的精简版:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 接受的导入句柄。这两个字段表达的是同一远端资源在不同阶段的形态,不能相互替代。

初始化流程

完整初始化涉及较多属性结构。对初学者,先按下面的依赖顺序理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 教学伪代码:函数参数应以当前 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, ...);

顺序背后的原因是:

  1. 没有 Context,就无法创建归属于设备的队列和端点。
  2. 没有 JFC,Jetty 就没有完成项的落点。
  3. 本地内存必须先注册,才能形成可交换的 Segment 描述。
  4. 必须先拿到对端描述,才能导入远端 Jetty 和 Segment。
  5. 所有依赖就绪后,才有条件构造 WRITE。

构造一条 WRITE

下面的函数保留了官方样例的关键结构。错误处理被收缩,便于先读懂主线:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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 段:准备本地数据

1
snprintf((char *)ctx->va, msg_size, "hello urma");

数据先写入已经注册过的本地缓冲区。WRITE 的源地址不是字符串常量地址,而是 ctx->va

第 2 段:描述源 SGE

1
2
3
.addr = (uint64_t)ctx->va,
.len = msg_size,
.tseg = ctx->local_tseg,

这三个字段共同表达:“从本地已注册 Segment 中,以 ctx->va 为起点读取 msg_size 字节”。如果地址落在 Segment 范围之外,或 Segment 权限不符合要求,请求会失败。

第 3 段:描述目标 SGE

1
2
3
.addr = ctx->remote_seg.ubva.va,
.len = msg_size,
.tseg = ctx->import_tseg,

目标地址来自对端交换过来的 Segment 描述,目标句柄则来自本端的导入结果。二者必须指向同一远端注册区域。

第 4 段:把 SGE 组成 SG

1
2
.sge = &src_sge,
.num_sge = 1,

本例只有一段连续内存,所以源、目标各使用一个 SGE。SG 仍然存在,是因为接口也要支持多段不连续内存。

第 5 段:构造读写描述

1
2
3
4
urma_rw_wr_t rw = {
.src = src_sg,
.dst = dst_sg,
};

rw 只表达数据从哪里到哪里,还没有说明操作是 WRITE 还是 READ。

第 6 段:构造 WR

1
2
3
4
5
.opcode = URMA_OPC_WRITE,
.tjetty = ctx->t_jetty,
.user_ctx = ctx->rid,
.rw = rw,
.next = NULL,
  • **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 后还要检查:

  1. cr.status 是否为成功状态。
  2. cr.user_ctx 是否等于本次请求的 rid

如何改成 READ

READ 的数据方向与 WRITE 相反:

1
2
WRITE:本地 src ──→ 远端 dst
READ :远端 src ──→ 本地 dst

因此改动有两类:

1
2
3
4
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,可以看清数据结构;业务代码则可根据控制需求选择更简洁的接口。

常见失败点

  1. 把普通指针直接当远端地址:远端地址必须来自对端交换的 Segment 描述。
  2. 收到描述却没有导入:WR 中要使用 import_tsegt_jetty
  3. 源或目标越界:地址加长度必须落在相应 Segment 范围内。
  4. 权限不匹配:注册和导入属性需要允许相应 READ/WRITE 操作。
  5. 没有请求完成项:未设置 complete_enable 却等待对应 CR。
  6. 把提交成功当作传输完成:必须按完成语义等待。
  7. 直接复制不同版本样例:结构字段和参数要与本机安装的 UMDK 头文件匹配。

建议练习

先运行官方 urma_sample,确认环境和 EID 正常;再只修改消息内容、长度和请求 ID。环境打通后,再尝试把 WRITE 改成 READ。这样能把“环境问题”和“WR 构造问题”分开。

参考资料

Author

Shaojie Tan

Posted on

2026-09-02

Updated on

2026-09-02

Licensed under