RDMA-Toy-1 PingPong和概念梳理

概念对齐

verbs 是 RDMA 的底层编程接口,目的是通过一组 ibv_* 调用把用户读写任务(WR)提交到队列(QP),网卡按这些请求在不经过内核协议栈的前提下执行 SEND/RECV、RDMA READ/WRITE、原子操作 等动作,并把结果以完成事件(CQE)的形式回报给应用。

▸ 类比:BSD sockets 是用 TCP/UDP 传字节的系统调用集合,verbs 是用 RDMA 传内存的系统调用集合。sockets 交给内核协议栈;verbs 交给 RNIC 的硬件队列。

InfiniBand verbs 规范定义 HCA 能执行的动作(post、poll、create、modify等),因此在 Linux 用户态库中呈现为一组 ibv_* API。而不同厂商通过 provider(如 libmlx5、librxe)实现背后的具体硬件/软件行为,但对应用暴露统一的 verbs 抽象。

  • 用户态库:libibverbs(rdma-core)+ 各 provider(libmlx5, librxe)。
  • 字符设备:/dev/infiniband/uverbsX、rdma_cm 等承接与内核的 ioctl/事件通道。

这也是为什么混装发行版包与第三方 OFED 会报错“符号找不到”,因为应用链接的 lib 库与内核驱动/provider 实现必须匹配

关键对象:

  • Device / Context:网卡设备与用户态句柄(ibv_open_device);
  • PD(Protection Domain):保护域,用于把 QP、MR、CQ 等资源组合;
  • MR(Memory Region):注册内存,获得 lkey/rkey 以供 DMA 访问(ibv_reg_mr);
  • CQ(Completion Queue):完成队列,硬件把执行结果写成 CQE,使用 ibv_poll_cq 取出;
  • QP(Queue Pair):一对发送队列 SQ 与接收队列 RQ。是一个可靠连接 RC 的典型状态机:RESET→INIT→RTR→RTS;
  • WR(Work Request):用户指令,如 post a SEND 或 do RDMA WRITE;
  • SGE(Scatter/Gather Entry):描述这条 WR 要访问的内存片段(地址、长度、lkey)。
  • WQE:WR 进入硬件队列后的条目形式;
  • WC(Work Completion):CQ 里读到的一条完成记录(含 status、wr_id 等)。
  • HCA(Host Channel Adapter):主机侧的 RDMA 适配器,硬件或软件实现,提供 QP/CQ/MR 等verbs能力,并执行 DMA。
  • lkey(local key):本机访问某块注册内存(MR)时必须携带的访问令牌;
  • rkey(remote key):远端对这块 MR 进行 RDMA READ/WRITE 时必须提供的 授权令牌,要通过握手显式告诉对端。

RDMA 设备在主机侧的抽象,InfiniBand里叫 HCA;以太网 RoCE 场景是 RNIC。在 Linux verbs 里,应用通过 ibv_open_device() 打开 HCA/RNIC 得到上下文(ibv_context*):

  • 暴露 队列对(QP)、完成队列(CQ)、保护域(PD)、注册内存(MR) 等资源;
  • 把应用通过 verbs post 的工作请求(WR)下发到硬件队列,并通过 DMA 直接访问内存;
  • 把执行结果写入 CQE 供应用 ibv_poll_cq() 读取。

实现形态:

  • 硬件 HCA:如 Mellanox/NVIDIA ConnectX 系列;
  • 软件 HCA:如 Soft-RoCE(rxe)

数据通路:

  1. 发现/打开设备:ibv_get_device_list → ibv_open_device;
  2. 分配资源:ibv_alloc_pd、ibv_create_cq、ibv_create_qp;
  3. 注册内存:ibv_reg_mr(拿到 lkey/rkey);
  4. 建链:把 QP 走状态机到 RTS(连接信息可用 RDMA-CM 交换,或 out-of-band);
  5. 收发与完成:
    1. ibv_post_recv 贴接收缓冲;
    2. 发端 ibv_post_send(opcode 可选 IBV_WR_SEND/WRITE/READ/ATOMIC 等);
    3. 循环 ibv_poll_cq 取 WC,检查 status==IBV_WC_SUCCESS。

▸ RDMA-CM 与 verbs 的关系
RDMA-CM(如 rping、perftest 的 -R)只负责找到RDMA对端;verbs 接口负责具体的数据通路行为。

伪代码描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

ctx = ibv_open_device(dev);
pd = ibv_alloc_pd(ctx);
cq = ibv_create_cq(ctx, CQ_DEPTH, ...);
qp = ibv_create_qp(pd, { .send_cq=cq, .recv_cq=cq, .cap={...}, .qp_type=IBV_QPT_RC });

mr_recv = ibv_reg_mr(pd, recv_buf, SZ, IBV_ACCESS_LOCAL_WRITE);
mr_send = ibv_reg_mr(pd, send_buf, SZ, IBV_ACCESS_LOCAL_WRITE|IBV_ACCESS_REMOTE_WRITE|IBV_ACCESS_REMOTE_READ);

// (连接信息通过 RDMA-CM 或自定义握手拿到)

// 接收
post_recv(recv_buf, lkey);

// 发送一个 SEND
post_send(send_buf, lkey, IBV_WR_SEND);

// 轮询 CQ 直到拿到 WC
while (!done) {
n = ibv_poll_cq(cq, &wc);
if (n > 0 && wc.status == IBV_WC_SUCCESS) handle(wc);
}

PingPong 实验

为了能够深入理解RDMA的机制,笔者尝试动手实现了一个极简版本的PingPong代码,如下:

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cerrno>
#include <getopt.h>
#include <string>
#include <algorithm>
#include <cstdarg> // <-- for va_list, va_start, va_end

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h> // sysconf

#include <infiniband/verbs.h>
#include <rdma/rdma_cma.h>

static const size_t kMsgSizeDefault = 1024;
static const int kCQDepth = 256;
static const int kQPDepth = 128;

static const uint64_t WRID_RECV = 0xBEEF;
static const uint64_t WRID_SEND = 0xCAFE;

struct Options {
bool is_server = false;
std::string ip = "10.10.0.1";
uint16_t port = 19999;
int iters = 10;
bool verbose = false;
size_t msg_size = kMsgSizeDefault;
};

struct Ctx {
rdma_event_channel* ec = nullptr;
rdma_cm_id* id = nullptr;
ibv_pd* pd = nullptr;
ibv_cq* cq = nullptr;
ibv_qp* qp = nullptr;
ibv_mr* mr_send = nullptr;
ibv_mr* mr_recv = nullptr;
char* send_buf = nullptr;
char* recv_buf = nullptr;
};

static void die(const char* msg) {
perror(msg);
std::exit(EXIT_FAILURE);
}
static void diex(const char* msg, int err) {
std::fprintf(stderr, "%s: %s (%d)\n", msg, strerror(err), err);
std::exit(EXIT_FAILURE);
}

static void vlog(bool on, const char* fmt, ...) {
if (!on) return;
va_list ap;
va_start(ap, fmt);
std::vfprintf(stdout, fmt, ap);
std::fputc('\n', stdout);
va_end(ap);
}

static const char* wc_opcode_str(int op) {
switch (op) {
case IBV_WC_SEND: return "SEND";
case IBV_WC_RECV: return "RECV";
case IBV_WC_RDMA_WRITE: return "RDMA_WRITE";
case IBV_WC_RDMA_READ: return "RDMA_READ";
default: return "OTHER";
}
}

static void post_one_recv(Ctx& ctx, size_t msg_size) {
ibv_sge sge{};
sge.addr = reinterpret_cast<uint64_t>(ctx.recv_buf);
sge.length = (uint32_t)msg_size;
sge.lkey = ctx.mr_recv->lkey;

ibv_recv_wr wr{};
wr.wr_id = WRID_RECV;
wr.sg_list = &sge;
wr.num_sge = 1;

ibv_recv_wr* bad = nullptr;
int rc = ibv_post_recv(ctx.qp, &wr, &bad);
if (rc) diex("ibv_post_recv", rc);
}

static void post_one_send(Ctx& ctx, size_t len) {
ibv_sge sge{};
sge.addr = reinterpret_cast<uint64_t>(ctx.send_buf);
sge.length = (uint32_t)len;
sge.lkey = ctx.mr_send->lkey;

ibv_send_wr wr{};
wr.wr_id = WRID_SEND;
wr.sg_list = &sge;
wr.num_sge = 1;
wr.opcode = IBV_WR_SEND;
wr.send_flags = IBV_SEND_SIGNALED;

ibv_send_wr* bad = nullptr;
int rc = ibv_post_send(ctx.qp, &wr, &bad);
if (rc) diex("ibv_post_send", rc);
}

static void wait_one_cqe(Ctx& ctx, ibv_wc& wc, bool verbose) {
for (;;) {
int n = ibv_poll_cq(ctx.cq, 1, &wc);
if (n < 0) die("ibv_poll_cq");
if (n == 0) continue;
if (wc.status != IBV_WC_SUCCESS) {
std::fprintf(stderr,
"CQE error: status=%d wr_id=0x%lx opcode=%d(%s)\n",
wc.status, (unsigned long)wc.wr_id, wc.opcode, wc_opcode_str(wc.opcode));
std::exit(EXIT_FAILURE);
}
if (verbose) {
std::printf("[cq] wr_id=0x%lx opcode=%d(%s)\n",
(unsigned long)wc.wr_id, wc.opcode, wc_opcode_str(wc.opcode));
}
return;
}
}

static void usage(const char* prog) {
std::fprintf(stderr,
"Usage: %s (-s|-c) -a <ipv4> [-p <port>] [-n <iters>] [-m <msg_size>] [-v]\n"
" -s server mode\n"
" -c client mode\n"
" -a <ipv4> bind/connect address (IPv4)\n"
" -p <port> TCP port for control channel (default 19999)\n"
" -n <iters> ping-pong iterations (default 10)\n"
" -m <bytes> message buffer size (default 1024)\n"
" -v verbose logging\n",
prog);
}

int main(int argc, char** argv) {
Options opt;
bool mode_set = false;

int c;
while ((c = getopt(argc, argv, "sca:p:n:m:v")) != -1) {
switch (c) {
case 's': opt.is_server = true; mode_set = true; break;
case 'c': opt.is_server = false; mode_set = true; break;
case 'a': opt.ip = optarg; break;
case 'p': opt.port = static_cast<uint16_t>(std::stoi(optarg)); break;
case 'n': opt.iters = std::max(1, std::stoi(optarg)); break;
case 'm': opt.msg_size = std::max(64, std::stoi(optarg)); break;
case 'v': opt.verbose = true; break;
default: usage(argv[0]); return 2;
}
}
if (!mode_set) { usage(argv[0]); return 2; }

setvbuf(stdout, nullptr, _IOLBF, 0);
std::printf("[*] mode=%s ip=%s port=%u iters=%d msg=%zu\n",
opt.is_server ? "server" : "client",
opt.ip.c_str(), opt.port, opt.iters, opt.msg_size);

Ctx ctx;
ctx.ec = rdma_create_event_channel();
if (!ctx.ec) die("rdma_create_event_channel");
if (rdma_create_id(ctx.ec, &ctx.id, nullptr, RDMA_PS_TCP)) die("rdma_create_id");

sockaddr_in sin{};
sin.sin_family = AF_INET;
sin.sin_port = htons(opt.port);
if (inet_pton(AF_INET, opt.ip.c_str(), &sin.sin_addr) != 1) die("inet_pton");

if (opt.is_server) {
std::puts("[*] server: bind + listen");
if (rdma_bind_addr(ctx.id, (sockaddr*)&sin)) die("rdma_bind_addr");
if (rdma_listen(ctx.id, 1)) die("rdma_listen");
std::puts("[*] server: waiting CONNECT_REQUEST");

rdma_cm_event* ev = nullptr;
if (rdma_get_cm_event(ctx.ec, &ev)) die("rdma_get_cm_event");
if (ev->event != RDMA_CM_EVENT_CONNECT_REQUEST) {
std::fprintf(stderr, "expected CONNECT_REQUEST, got %d\n", ev->event);
std::exit(EXIT_FAILURE);
}
vlog(opt.verbose, "[cm] CONNECT_REQUEST arrived");
rdma_cm_id* child = ev->id;
rdma_ack_cm_event(ev);
ctx.id = child;
} else {
rdma_cm_event* ev = nullptr;
if (rdma_resolve_addr(ctx.id, nullptr, (sockaddr*)&sin, 2000)) die("rdma_resolve_addr");
if (rdma_get_cm_event(ctx.ec, &ev)) die("rdma_get_cm_event");
vlog(opt.verbose, "[cm] got event=%d (expected ADDR_RESOLVED=0)", ev->event);
if (ev->event != RDMA_CM_EVENT_ADDR_RESOLVED) die("expected ADDR_RESOLVED");
rdma_ack_cm_event(ev);

if (rdma_resolve_route(ctx.id, 2000)) die("rdma_resolve_route");
if (rdma_get_cm_event(ctx.ec, &ev)) die("rdma_get_cm_event");
vlog(opt.verbose, "[cm] got event=%d (expected ROUTE_RESOLVED=1)", ev->event);
if (ev->event != RDMA_CM_EVENT_ROUTE_RESOLVED) die("expected ROUTE_RESOLVED");
rdma_ack_cm_event(ev);
}

// 资源创建
ctx.pd = ibv_alloc_pd(ctx.id->verbs);
if (!ctx.pd) die("ibv_alloc_pd");

ctx.cq = ibv_create_cq(ctx.id->verbs, kCQDepth, nullptr, nullptr, 0);
if (!ctx.cq) die("ibv_create_cq");

ibv_qp_init_attr qp_init{};
qp_init.qp_type = IBV_QPT_RC;
qp_init.send_cq = ctx.cq;
qp_init.recv_cq = ctx.cq;
qp_init.cap.max_send_wr = kQPDepth;
qp_init.cap.max_recv_wr = kQPDepth;
qp_init.cap.max_send_sge = 1;
qp_init.cap.max_recv_sge = 1;

if (rdma_create_qp(ctx.id, ctx.pd, &qp_init)) die("rdma_create_qp");
ctx.qp = ctx.id->qp;
vlog(opt.verbose, "[qp] created qp_num=0x%x", ctx.qp->qp_num);

// 分配并注册内存
long pagesz = sysconf(_SC_PAGESIZE);
if (pagesz <= 0) pagesz = 4096;
if (posix_memalign((void**)&ctx.send_buf, (size_t)pagesz, opt.msg_size)) die("posix_memalign send");
if (posix_memalign((void**)&ctx.recv_buf, (size_t)pagesz, opt.msg_size)) die("posix_memalign recv");
memset(ctx.send_buf, 0, opt.msg_size);
memset(ctx.recv_buf, 0, opt.msg_size);

int access = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE;
ctx.mr_send = ibv_reg_mr(ctx.pd, ctx.send_buf, opt.msg_size, access);
ctx.mr_recv = ibv_reg_mr(ctx.pd, ctx.recv_buf, opt.msg_size, access);
if (!ctx.mr_send || !ctx.mr_recv) die("ibv_reg_mr");

// 先接收,避免对端首包 RNR
post_one_recv(ctx, opt.msg_size);

// 连接
rdma_conn_param conn{};
conn.initiator_depth = 1;
conn.responder_resources = 1;
conn.retry_count = 7;
conn.rnr_retry_count = 7;

if (opt.is_server) {
if (rdma_accept(ctx.id, &conn)) die("rdma_accept");
} else {
if (rdma_connect(ctx.id, &conn)) die("rdma_connect");
}

rdma_cm_event* ev = nullptr;
if (rdma_get_cm_event(ctx.ec, &ev)) die("rdma_get_cm_event");
if (ev->event != RDMA_CM_EVENT_ESTABLISHED) die("expected ESTABLISHED");
rdma_ack_cm_event(ev);
std::puts("[*] connection ESTABLISHED");

// ping-pong 循环
ibv_wc wc;
for (int i = 0; i < opt.iters; ++i) {
if (!opt.is_server) {
size_t len = (size_t)std::snprintf(ctx.send_buf, opt.msg_size, "ping %d", i);
len = std::min(len + 1, opt.msg_size); // 带 '\0' 便于对端直接打印
post_one_send(ctx, len);
wait_one_cqe(ctx, wc, opt.verbose); // SEND 完成
wait_one_cqe(ctx, wc, opt.verbose); // RECV 完成
std::printf("[client] recv: %.*s\n",
(int)strnlen(ctx.recv_buf, opt.msg_size), ctx.recv_buf);
post_one_recv(ctx, opt.msg_size);
} else {
wait_one_cqe(ctx, wc, opt.verbose); // RECV 完成(收到对端 ping)
std::printf("[server] recv: %.*s\n",
(int)strnlen(ctx.recv_buf, opt.msg_size), ctx.recv_buf);
size_t len = (size_t)std::snprintf(ctx.send_buf, opt.msg_size, "pong %d", i);
len = std::min(len + 1, opt.msg_size);
post_one_send(ctx, len);
wait_one_cqe(ctx, wc, opt.verbose); // SEND 完成
post_one_recv(ctx, opt.msg_size);
}
}

rdma_disconnect(ctx.id);

ibv_dereg_mr(ctx.mr_send);
ibv_dereg_mr(ctx.mr_recv);
ibv_destroy_qp(ctx.qp);
ibv_destroy_cq(ctx.cq);
ibv_dealloc_pd(ctx.pd);
rdma_destroy_id(ctx.id);
rdma_destroy_event_channel(ctx.ec);

std::puts("done");
return 0;
}

源码分析

  1. RDMA-CM 建立
1
2
3
4
5
6
7
ctx.ec = rdma_create_event_channel();     // 事件通道
rdma_create_id(ctx.ec, &ctx.id, nullptr, RDMA_PS_TCP);

// server:rdma_bind_addr + rdma_listen → 等 CONNECT_REQUEST
// client:rdma_resolve_addr → ADDR_RESOLVED
// rdma_resolve_route → ROUTE_RESOLVED

RDMA-CM 目的是寻址与建链,数据面仍由 verbs 执行。在 RoCEv2 上,CM 实际走 UDP/4791 进行寻址;本 demo 额外使用一个 TCP 端口(默认 19999)作为参数交换的“控制通道”。

  1. 资源对象(PD / CQ / QP)初始化
1
2
3
4
5
6
7
8
9
10
11
12

ctx.pd = ibv_alloc_pd(ctx.id->verbs);
ctx.cq = ibv_create_cq(ctx.id->verbs, kCQDepth, nullptr, nullptr, 0);

ibv_qp_init_attr qp_init{};
qp_init.qp_type = IBV_QPT_RC;
qp_init.send_cq = ctx.cq;
qp_init.recv_cq = ctx.cq;
qp_init.cap = { .max_send_wr=kQPDepth, .max_recv_wr=kQPDepth, .max_send_sge=1, .max_recv_sge=1 };

rdma_create_qp(ctx.id, ctx.pd, &qp_init); // 后续 connect/accept 会自动把 QP 推到 RTS

RC(Reliable Connection) 模式下 QP 的状态机为 RESET→INIT→RTR→RTS。简单的,用 rdma_create_qp + rdma_connect/accept 让 CM 自动改 QP,避免手写 ibv_modify_qp。

  1. 先RECV
1
2
post_one_recv(ctx, msg_size);  // 进入循环前预投一条 RECV

RNR(Receiver Not Ready):对端发 SEND 而本端 RQ 没有可用 WQE 时,会返回 RNR NAK 并触发重试。先 RECV 是 RC 的标准做法。

  1. 建立链接
1
2
3
if (server) rdma_accept(ctx.id, &conn); else rdma_connect(ctx.id, &conn);
rdma_get_cm_event(...ESTABLISHED);

  • 客户端循环体:构造 “ping i” → post_one_send() → 等 SEND 完成 → 再等 RECV 完成(收到服务器 “pong i”);
  • 服务端:先等 RECV 完成(收到 “ping i”)→ 构造 “pong i” → SEND 完成 → 补一条 RECV 进入下一轮。
  1. 发送数据
1
2
3
4
5
size_t len = (size_t)
std::snprintf(ctx.send_buf, msg_size, "ping %d", i);
len = std::min(len + 1, msg_size); // '\0',便于打印
post_one_send(ctx, len);

  1. CQ 轮询
1
2
3
wait_one_cqe(ctx, wc, verbose);   // 轮询直到拿到 1 条 CQE
if (wc.status != IBV_WC_SUCCESS) { ...exit... }

这里保留了每条 SEND 必带 SIGNALED(每个发送有 CQE),为了便于观察。性能测试时可改成每 N 条 signal 一次。

  1. 优雅断开
1
2
3
4
5
6
7
8
9
rdma_disconnect(ctx.id);

ibv_dereg_mr(ctx.mr_send);
ibv_dereg_mr(ctx.mr_recv);
ibv_destroy_qp(ctx.qp);
ibv_destroy_cq(ctx.cq);
ibv_dealloc_pd(ctx.pd);
rdma_destroy_id(ctx.id);
rdma_destroy_event_channel(ctx.ec);

这里的关闭顺序很关键:断开链接 → 释放 MR/QP/CQ/PD 资源;

实验结果

  1. 编译
1
g++ -O3 -Wall rdma_pingpong.cc -o rdma_pingpong -lrdmacm -libverbs
  1. 运行

服务端(在 10.10.0.1 侧):

1
sudo ./rdma_pingpong -s -a 10.10.0.1 -v

客户端(另一侧连 10.10.0.1):

1
sudo ./rdma_pingpong -c -a 10.10.0.1 -v
  1. 输出

客户端输出:

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
sudo ./rdma_pingpong -c -a 10.10.0.1 -v

[*] mode=client ip=10.10.0.1 port=19999 iters=10 msg=1024
[cm] got event=0 (expected ADDR_RESOLVED=0)
[cm] got event=2 (expected ROUTE_RESOLVED=1)
[qp] created qp_num=0x2d
[*] connection ESTABLISHED
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[client] recv: pong 0
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[client] recv: pong 1
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[client] recv: pong 2
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[client] recv: pong 3
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[client] recv: pong 4
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[client] recv: pong 5
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[client] recv: pong 6
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[client] recv: pong 7
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[client] recv: pong 8
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[client] recv: pong 9
done

服务端输出:

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
sudo ./rdma_pingpong -s -a 10.10.0.1 -v

[*] mode=server ip=10.10.0.1 port=19999 iters=10 msg=1024
[*] server: bind + listen
[*] server: waiting CONNECT_REQUEST
[cm] CONNECT_REQUEST arrived
[qp] created qp_num=0x2e
[*] connection ESTABLISHED
[cq] wr_id=0xbeef opcode=128(RECV)
[server] recv: ping 0
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[server] recv: ping 1
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[server] recv: ping 2
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[server] recv: ping 3
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[server] recv: ping 4
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[server] recv: ping 5
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[server] recv: ping 6
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[server] recv: ping 7
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[server] recv: ping 8
[cq] wr_id=0xcafe opcode=0(SEND)
[cq] wr_id=0xbeef opcode=128(RECV)
[server] recv: ping 9
[cq] wr_id=0xcafe opcode=0(SEND)
done

实验结论

  • event=0 是 ADDR_RESOLVED,event=2 是 ROUTE_RESOLVED;
  • opcode=0(SEND) 表示本端发送完成 CQE,opcode=128(RECV) 表示本端收到对端消息的 CQE;
  • 客户端先 SEND 后 RECV,服务端先 RECV 后 SEND,是合理的 ping→pong 循环时序。

以客户端前 3 轮为例:

  1. SEND 完成(opcode=0, wr_id=0xCAFE),说明本地 SQ 中的 SEND WQE 已成功下发并确认,但不代表对端已经处理,只代表本端发送侧完成;
  2. RECV 完成(opcode=128, wr_id=0xBEEF),说明对端的 “pong i” 已到达并被 RQ 中的 RECV WQE 接住,本端从 CQ 中取到这条完成。
  3. “[client] recv: pong i” 被打印,说明读取 recv_buf,并立刻 post_one_recv(),让下一轮对端的 SEND 不会 RNR。

总结

本次 demo 用 RDMA-CM 简化了建链,用 SEND/RECV 实现了最简单的的 ping-pong实验,并且给出了 RDMA 相关内容的关键概念对齐。实验结果日志的每一行都对应一个机制节点:CM 事件(0/2/ESTABLISHED)→ QP 创建 → 首个 RECV 预投 → 循环中 客户端先 SEND 后 RECV、服务端先 RECV 后 SEND → 每次 RECV 完成后立刻补位。可以作为针对 RDMA 入门的极简了解。

下一篇中,笔者会:

  • 通过初始 SEND 交换“远端 addr + rkey + len”;
  • 构造 IBV_WR_RDMA_READ/WRITE 的 wr.wr.rdma.* 字段;
  • 设计 RECV 池与 Doorbell 批量 post,减少 RNR 与 CQ 压力;
  • 尝试把 RDMA 并入 io_uring 事件环。

RDMA-Toy-1 PingPong和概念梳理

https://devillove084.github.io/2025/09/20/RDMA-2/

作者

devillove084

发布于

2025-09-20

更新于

2025-09-21

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×