引言
InfiniBand/RDMA 提供了两种截然不同的数据传输模式:Send/Recv 和 Read/Write。这两种模式在底层实现机制、CPU 参与度、对端感知性和传输控制策略方面存在根本性差异。
核心差异对比
基本特性对比
| 维度 |
Send/Recv |
Read/Write |
| 操作码 |
IBV_WR_SEND |
IBV_WR_RDMA_WRITE
IBV_WR_RDMA_READ |
| CPU 参与 |
接收端必须参与(post recv) |
对端 CPU 不参与 |
| 对端感知 |
完全感知(CQ 事件) |
不感知(无事件) |
| 流控机制 |
有(RNR 机制) |
无 |
| 传输速率控制 |
受接收端节制 |
不受对端节制 |
| 内存注册要求 |
发送端和接收端都需要 |
发起端和对端都需要(对端需设置 REMOTE 权限) |
| 远程地址信息 |
不需要 |
需要(remote_addr + rkey) |
| 完成事件 |
双方都有 |
仅发起端有 |
| 使用复杂度 |
需要双方协调 |
只需发起端操作 |
| 性能 |
高(硬件加速) |
最高(零拷贝,无 CPU 参与) |
工作机制对比
| 步骤 |
Send/Recv |
Read/Write |
| 1. 准备工作 |
接收端预先 post recv |
对端注册内存并传递 R_Key |
| 2. 发起操作 |
发送端 post send |
发起端 post RDMA Read/Write |
| 3. 数据传输 |
HCA 匹配 Receive WR 并写入缓冲区 |
HCA 直接访问远程内存 |
| 4. 完成通知 |
双方 CQ 都有完成事件 |
仅发起端 CQ 有完成事件 |
| 5. 对端状态 |
接收端知道数据到达 |
对端 CPU 完全无感知 |
CPU 参与度与对端感知性
| 方面 |
Send/Recv |
Read/Write |
| 接收端操作 |
必须预先 post recv |
无需任何操作 |
| 接收端 CPU |
必须参与,处理 CQ 事件 |
完全不参与 |
| 接收端控制 |
可以控制接收速率 |
无法控制传输速率 |
| 对端 CPU 状态 |
需要处理接收事件 |
可以执行其他任务或休眠 |
| 数据到达感知 |
完全感知(CQ 事件) |
不感知 |
| 传输时机控制 |
可以控制接收时机 |
无法控制传输时机 |
| 数据来源信息 |
知道数据来源(QP 编号) |
不知道数据来源 |
| 数据大小信息 |
知道数据大小 |
不知道传输大小 |
| 传输完成感知 |
通过 CQ 事件知道 |
无法直接知道 |
| 同步机制 |
通过 CQ 事件同步 |
需要额外机制(Send/Recv 或轮询) |
传输速率控制
| 方面 |
Send/Recv |
Read/Write |
| 流控机制 |
RNR(Receiver Not Ready)机制 |
无流控机制 |
| 速率控制方 |
接收端控制(通过 post recv 频率) |
发起端完全控制 |
| RNR 处理 |
发送端收到 RNR NACK,等待重试 |
不适用 |
| 对端限制能力 |
可以限制接收速率 |
无法限制传输速率 |
| 潜在风险 |
接收端可能过载(但可通过流控避免) |
对端可能被数据淹没 |
| 性能特点 |
受接收端处理能力限制 |
可达到网络带宽上限 |
RNR 机制参数:
| 参数 |
说明 |
典型值 |
rnr_retry |
RNR 重试次数(0-7,7 表示无限重试) |
7 |
min_rnr_timer |
最小 RNR 等待时间(单位:655.36 微秒) |
0x12 |
1 2 3
| attr.rnr_retry = 7; attr.min_rnr_timer = 0x12;
|
API 与代码示例
关键函数
ibv_post_recv()
接收端必须预先调用此函数提交 Receive WR:
1 2 3 4 5
| #include <infiniband/verbs.h>
int ibv_post_recv(struct ibv_qp *qp, struct ibv_recv_wr *wr, struct ibv_recv_wr **bad_wr);
|
| 参数 |
说明 |
qp |
队列对句柄 |
wr |
Receive Work Request 链表头 |
bad_wr |
如果提交失败,返回失败的 WR 指针 |
返回值:成功返回 0,失败返回错误码
ibv_post_send()
发送端调用此函数提交 Send WR 或 RDMA Read/Write WR:
1 2 3
| int ibv_post_send(struct ibv_qp *qp, struct ibv_send_wr *wr, struct ibv_send_wr **bad_wr);
|
| 参数 |
说明 |
qp |
队列对句柄 |
wr |
Send Work Request 链表头 |
bad_wr |
如果提交失败,返回失败的 WR 指针 |
数据结构
Receive Work Request
1 2 3 4 5 6
| struct ibv_recv_wr { uint64_t wr_id; struct ibv_recv_wr *next; struct ibv_sge *sg_list; int num_sge; };
|
Send Work Request
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| struct ibv_send_wr { uint64_t wr_id; struct ibv_send_wr *next; struct ibv_sge *sg_list; int num_sge; enum ibv_wr_opcode opcode; int send_flags; union { struct { uint64_t remote_addr; uint32_t rkey; } rdma; } wr; uint32_t imm_data; };
|
Scatter/Gather Element
1 2 3 4 5
| struct ibv_sge { uint64_t addr; uint32_t length; uint32_t lkey; };
|
操作码与标志位
| 操作码 |
说明 |
使用场景 |
IBV_WR_SEND |
Send/Recv 模式 |
需要接收端确认的消息传递 |
IBV_WR_RDMA_WRITE |
RDMA Write |
将本地数据写入远程内存 |
IBV_WR_RDMA_WRITE_WITH_IMM |
RDMA Write with Immediate |
写入数据并通知对端 |
IBV_WR_RDMA_READ |
RDMA Read |
从远程内存读取数据 |
| 标志位 |
说明 |
使用场景 |
IBV_SEND_FENCE |
栅栏标志,确保顺序 |
需要保证操作顺序 |
IBV_SEND_SIGNALED |
请求完成通知 |
需要知道操作完成 |
IBV_SEND_SOLICITED |
请求立即通知(用于 Send with Immediate) |
Send with Immediate |
IBV_SEND_INLINE |
内联发送(小数据直接放在 WR 中) |
小数据快速发送 |
Send/Recv 代码示例
接收端代码
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
| char *recv_buffer = malloc(BUFFER_SIZE); struct ibv_mr *recv_mr = ibv_reg_mr(pd, recv_buffer, BUFFER_SIZE, IBV_ACCESS_LOCAL_WRITE);
struct ibv_sge recv_sge; recv_sge.addr = (uintptr_t)recv_buffer; recv_sge.length = BUFFER_SIZE; recv_sge.lkey = recv_mr->lkey;
struct ibv_recv_wr recv_wr, *bad_recv_wr; memset(&recv_wr, 0, sizeof(recv_wr)); recv_wr.wr_id = (uintptr_t)recv_buffer; recv_wr.sg_list = &recv_sge; recv_wr.num_sge = 1;
if (ibv_post_recv(qp, &recv_wr, &bad_recv_wr)) { fprintf(stderr, "Failed to post receive WR\n"); return -1; }
struct ibv_wc wc; int ne; do { ne = ibv_poll_cq(cq, 1, &wc); } while (ne == 0);
if (wc.status != IBV_WC_SUCCESS) { fprintf(stderr, "Receive failed with status: %s\n", ibv_wc_status_str(wc.status)); return -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
| char *send_buffer = malloc(BUFFER_SIZE); memcpy(send_buffer, data_to_send, data_size); struct ibv_mr *send_mr = ibv_reg_mr(pd, send_buffer, BUFFER_SIZE, IBV_ACCESS_LOCAL_WRITE);
struct ibv_sge send_sge; send_sge.addr = (uintptr_t)send_buffer; send_sge.length = data_size; send_sge.lkey = send_mr->lkey;
struct ibv_send_wr send_wr, *bad_send_wr; memset(&send_wr, 0, sizeof(send_wr)); send_wr.wr_id = (uintptr_t)send_buffer; send_wr.opcode = IBV_WR_SEND; send_wr.send_flags = IBV_SEND_SIGNALED; send_wr.sg_list = &send_sge; send_wr.num_sge = 1;
if (ibv_post_send(qp, &send_wr, &bad_send_wr)) { fprintf(stderr, "Failed to post send WR\n"); return -1; }
struct ibv_wc wc; int ne; do { ne = ibv_poll_cq(cq, 1, &wc); } while (ne == 0);
|
Read/Write 代码示例
RDMA Write 操作
1 2 3 4 5 6 7 8 9 10 11 12 13
| struct ibv_send_wr write_wr, *bad_wr; memset(&write_wr, 0, sizeof(write_wr));
write_wr.wr_id = (uintptr_t)local_buffer; write_wr.opcode = IBV_WR_RDMA_WRITE; write_wr.send_flags = IBV_SEND_SIGNALED; write_wr.sg_list = &sge; write_wr.num_sge = 1; write_wr.wr.rdma.remote_addr = remote_addr; write_wr.wr.rdma.rkey = remote_rkey;
ibv_post_send(qp, &write_wr, &bad_wr);
|
RDMA Read 操作
1 2 3 4 5 6 7 8 9 10 11 12
| struct ibv_send_wr read_wr, *bad_wr; memset(&read_wr, 0, sizeof(read_wr));
read_wr.wr_id = (uintptr_t)local_buffer; read_wr.opcode = IBV_WR_RDMA_READ; read_wr.send_flags = IBV_SEND_SIGNALED; read_wr.sg_list = &sge; read_wr.num_sge = 1; read_wr.wr.rdma.remote_addr = remote_addr; read_wr.wr.rdma.rkey = remote_rkey;
ibv_post_send(qp, &read_wr, &bad_wr);
|
内存注册要求
1 2 3 4 5 6 7 8 9
| struct ibv_mr *remote_mr = ibv_reg_mr(pd, remote_buffer, BUFFER_SIZE, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_WRITE);
|
同步机制
Send/Recv 同步机制
关键问题:ibv_post_recv 必须在 ibv_post_send 之前吗?
答案:不是必须在 ibv_post_send 之前调用,但必须在数据到达之前 post recv。
调用顺序说明
| 场景 |
说明 |
是否可行 |
| 预先 post recv |
在发送端 post send 之前,接收端预先 post recv |
✅ 推荐做法 |
| 同时调用 |
接收端和发送端同时调用(不同线程/进程) |
✅ 可行,但需要确保 recv 先完成 |
| 延迟 post recv |
发送端先 post send,接收端后 post recv |
⚠️ 可能导致 RNR 错误 |
工作原理
接收队列(RQ)机制:
- 接收端调用
ibv_post_recv() 将 Receive WR 提交到接收队列(RQ)
- 当数据包到达时,HCA 硬件从 RQ 中取出一个 Receive WR
- 如果 RQ 中没有 Receive WR,HCA 返回 RNR(Receiver Not Ready)NACK
时序要求:
- 不是要求
ibv_post_recv() 必须在 ibv_post_send() 之前调用
- 而是要求数据包到达时,RQ 中必须有可用的 Receive WR
- 由于网络延迟,通常可以预先 post recv
代码示例
场景 1:预先批量 post recv(推荐)
1 2 3 4 5 6 7 8 9 10
| for (int i = 0; i < BATCH_SIZE; i++) { struct ibv_recv_wr recv_wr, *bad_wr; ibv_post_recv(qp, &recv_wr, &bad_wr); }
ibv_post_send(qp, &send_wr, &bad_send_wr);
|
场景 2:RNR 错误处理
1 2 3 4 5 6 7 8 9 10 11
| struct ibv_wc wc; ibv_poll_cq(cq, 1, &wc);
if (wc.status == IBV_WC_RNR_RETRY_EXC_ERR) { fprintf(stderr, "RNR retry exceeded\n"); } else if (wc.status == IBV_WC_REM_OP_ERR) { fprintf(stderr, "Remote operation error, possibly RNR\n"); }
|
最佳实践
| 实践 |
说明 |
原因 |
| 预先批量 post recv |
在连接建立后立即批量提交多个 Receive WR |
避免 RNR 错误,提高性能 |
| 保持 RQ 中有足够的 WR |
接收完成后立即重新 post recv |
确保持续接收能力 |
| 使用应用层同步 |
如果必须延迟 post recv,使用同步机制 |
避免 RNR 错误 |
| 合理设置 RNR 参数 |
设置合适的 rnr_retry 和 min_rnr_timer |
给接收端时间 post recv |
Read/Write 同步机制
由于 Read/Write 操作对端无感知,需要额外的同步机制来保证数据一致性和操作顺序。
同步问题
| 问题 |
说明 |
影响 |
| 数据写入完成通知 |
对端不知道数据何时写入完成 |
可能读取到未完成的数据 |
| 数据读取时机 |
对端不知道何时被读取 |
可能在修改时被读取,导致数据不一致 |
| 操作顺序保证 |
多个 RDMA 操作的顺序 |
可能乱序执行,导致逻辑错误 |
| 并发访问 |
本地 CPU 和远程 RDMA 同时访问 |
可能导致数据竞争 |
同步机制对比
| 机制 |
说明 |
适用场景 |
| RDMA Write with Immediate |
写入数据的同时发送立即数据通知 |
需要通知对端数据已写入 |
| Send/Recv 通知 |
通过 Send/Recv 发送完成通知 |
需要确认和流控 |
| 内存屏障(Fence) |
使用 IBV_SEND_FENCE 保证顺序 |
需要保证操作顺序 |
| 原子操作 |
使用原子操作作为同步点 |
需要细粒度同步 |
| 版本号/双缓冲 |
使用版本号或双缓冲机制 |
需要检测数据变化或无锁读取 |
在写入数据的同时,发送立即数据通知对端:
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
| struct ibv_send_wr write_wr, *bad_wr; memset(&write_wr, 0, sizeof(write_wr));
write_wr.wr_id = (uintptr_t)local_buffer; write_wr.opcode = IBV_WR_RDMA_WRITE_WITH_IMM; write_wr.send_flags = IBV_SEND_SIGNALED; write_wr.sg_list = &sge; write_wr.num_sge = 1; write_wr.wr.rdma.remote_addr = remote_addr; write_wr.wr.rdma.rkey = remote_rkey; write_wr.imm_data = htonl(NOTIFICATION_FLAG);
ibv_post_send(qp, &write_wr, &bad_wr);
struct ibv_recv_wr recv_wr, *bad_recv_wr;
ibv_post_recv(qp, &recv_wr, &bad_recv_wr);
struct ibv_wc wc; ibv_poll_cq(cq, 1, &wc); if (wc.opcode == IBV_WC_RECV_RDMA_WITH_IMM) { uint32_t imm_data = ntohl(wc.imm_data); }
|
内存屏障(Fence)机制
使用 IBV_SEND_FENCE 保证操作顺序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| struct ibv_send_wr wr[3], *bad_wr;
wr[0].opcode = IBV_WR_RDMA_WRITE; wr[0].send_flags = 0; wr[0].next = &wr[1];
wr[1].opcode = IBV_WR_RDMA_WRITE; wr[1].send_flags = IBV_SEND_FENCE; wr[1].next = &wr[2];
wr[2].opcode = IBV_WR_RDMA_WRITE; wr[2].send_flags = IBV_SEND_SIGNALED; wr[2].next = NULL;
ibv_post_send(qp, &wr[0], &bad_wr);
|
Fence 的作用:
- 确保 Fence 之前的所有操作在 Fence 之后的操作之前完成
- 保证操作的全局顺序(跨多个 QP)
- 适用于需要严格顺序的场景
原子操作同步
使用原子操作作为同步点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| struct ibv_send_wr atomic_wr, *bad_wr; memset(&atomic_wr, 0, sizeof(atomic_wr));
atomic_wr.opcode = IBV_WR_ATOMIC_CMP_AND_SWP;
atomic_wr.send_flags = IBV_SEND_SIGNALED; atomic_wr.sg_list = &sge; atomic_wr.num_sge = 1; atomic_wr.wr.atomic.remote_addr = remote_flag_addr; atomic_wr.wr.atomic.rkey = remote_rkey; atomic_wr.wr.atomic.compare_add = 0; atomic_wr.wr.atomic.swap = 1;
ibv_post_send(qp, &atomic_wr, &bad_wr);
|
| 操作码 |
说明 |
用途 |
IBV_WR_ATOMIC_CMP_AND_SWP |
Compare and Swap |
条件更新标志位 |
IBV_WR_ATOMIC_FETCH_AND_ADD |
Fetch and Add |
计数器操作 |
Read 时的并发安全
问题:如果 Read 时对端 CPU 正在修改数据,可能导致读取到不一致的数据。
解决方案:
版本号机制:
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
| struct data_with_version { uint64_t version; char data[BUFFER_SIZE]; };
void update_data(struct data_with_version *buf) { prepare_new_data(buf->data); __sync_synchronize(); __sync_add_and_fetch(&buf->version, 1); }
uint64_t old_version = 0; do { uint64_t version_before = read_version(); __sync_synchronize(); read_data(buffer); __sync_synchronize(); uint64_t version_after = read_version(); if (version_before == version_after && version_before != old_version) { break; } old_version = version_before; } while (1);
|
双缓冲机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| struct double_buffer { char buffer[2][BUFFER_SIZE]; volatile int active_index; };
void update_data(struct double_buffer *db) { int write_index = 1 - db->active_index; prepare_data(db->buffer[write_index]); __sync_synchronize(); __sync_lock_test_and_set(&db->active_index, write_index); }
void read_data(struct double_buffer *db) { int read_index = db->active_index; rdma_read(db->buffer[read_index]); }
|
同步机制选择建议
| 场景 |
推荐机制 |
原因 |
| 需要通知对端数据已写入 |
RDMA Write with Immediate |
高性能 + 通知 |
| 需要严格顺序 |
Fence + Send/Recv |
保证操作顺序 |
| 需要检测数据变化 |
版本号机制 |
可以检测并发修改 |
| 需要无锁读取 |
双缓冲机制 |
避免锁竞争 |
| 需要细粒度控制 |
原子标志位 |
精确控制读写时机 |
应用与实践
应用场景对比
| 场景类型 |
Send/Recv |
Read/Write |
| 请求-响应模式 |
✅ 适合(RPC、数据库查询) |
❌ 不适合 |
| 需要流控 |
✅ 适合(接收端处理能力有限) |
❌ 不适合 |
| 需要确认 |
✅ 适合(事务提交、状态同步) |
❌ 不适合 |
| 小数据频繁交互 |
✅ 适合 |
❌ 不适合 |
| 协议实现 |
✅ 适合(分布式一致性协议) |
❌ 不适合 |
| 高性能计算(HPC) |
❌ 不适合 |
✅ 适合(科学计算、数值模拟) |
| AI 训练 |
❌ 不适合 |
✅ 适合(参数同步,对端 CPU 繁忙) |
| 存储系统 |
❌ 不适合 |
✅ 适合(块存储、文件系统) |
| 批量数据传输 |
❌ 不适合 |
✅ 适合(大数据传输) |
| 零拷贝需求 |
❌ 不适合 |
✅ 适合(避免 CPU 参与) |
混合使用策略
在实际应用中,可以混合使用两种模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
struct metadata { uint64_t data_addr; uint32_t data_rkey; size_t data_size; } meta; send_metadata(&meta);
rdma_write_data(meta.data_addr, meta.data_rkey, data_buffer, meta.data_size);
send_completion_notification();
|
| 用途 |
使用的模式 |
原因 |
| 元数据交换 |
Send/Recv |
需要确认和流控 |
| 数据传输 |
Read/Write |
需要最高性能 |
| 完成通知 |
Send/Recv |
需要确认 |
性能优化
| 优化策略 |
Send/Recv |
Read/Write |
| 批量操作 |
批量提交 Receive WR |
批量提交 Read/Write WR |
| Signaling 策略 |
周期性 Signaling(每 N 个请求) |
避免频繁 Signaling |
| 内存对齐 |
建议对齐 |
强烈建议对齐(64 字节) |
| 队列大小 |
合理设置 max_recv_wr |
合理设置 max_send_wr |
| RNR 参数 |
合理设置 rnr_retry 和 min_rnr_timer |
不适用 |
Send/Recv 性能优化代码:
1 2 3 4 5 6 7 8 9
| for (int i = 0; i < BATCH_SIZE; i++) { post_recv_wr(); }
if (count % 64 == 0) { send_wr.send_flags |= IBV_SEND_SIGNALED; }
|
Read/Write 性能优化代码:
1 2 3 4 5 6 7 8 9
| struct ibv_send_wr *wr_list = build_wr_list(); ibv_post_send(qp, wr_list, &bad_wr);
write_wr.send_flags = 0;
posix_memalign((void**)&buffer, 64, size);
|
注意事项
| 注意事项 |
Send/Recv |
Read/Write |
| 必须预先 post recv |
✅ 必须 |
❌ 不需要 |
| 及时处理 CQ 事件 |
✅ 必须(避免 CQ 溢出) |
✅ 必须(仅发起端) |
| 合理设置队列大小 |
✅ 重要 |
✅ 重要 |
| 内存访问权限 |
✅ 需要 |
✅ 需要(对端需设置 REMOTE 权限) |
| R_Key 安全 |
❌ 不需要 |
✅ 重要(需要安全传递) |
| 同步机制 |
✅ 通过 CQ 事件 |
✅ 需要额外机制 |
| 内存边界检查 |
✅ 需要 |
✅ 需要(避免越界访问) |
| 并发安全 |
✅ 需要应用层同步 |
✅ 需要应用层同步 |
总结
Send/Recv 和 Read/Write 代表了 InfiniBand/RDMA 的两种不同设计哲学:
| 模式 |
设计哲学 |
核心特点 |
| Send/Recv |
协作和流控 |
接收端参与,有流控机制,适合需要确认的场景 |
| Read/Write |
极致性能和 CPU 卸载 |
对端 CPU 不参与,无流控,适合高性能场景 |
在实际应用中,应根据具体场景的需求,灵活选择或组合使用这两种模式,充分发挥 InfiniBand/RDMA 的性能优势。