mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
5929 字
17 分钟
RDMA 与远程直接内存访问
2025-06-15

某 AI 训练集群在 8 台 GPU 服务器间做 AllReduce 操作,基于 TCP 的参数同步每次迭代要 200 微秒,而改用 RDMA 后降到 20 微秒。训练时间从一周缩短到一天。RDMA 让”内存到内存”的零拷贝传输成为可能。

一、引言:RDMA——绕过远端 CPU 的零拷贝通信#

在传统网络编程中,一次数据传输需要经历完整的内核协议栈:应用程序调用 send(),数据从用户缓冲区拷贝到内核空间,内核封装 TCP/IP 协议头,再通过 DMA 交给网卡发送;接收端网卡通过 DMA 将数据写入内核缓冲区,内核解析协议头、拷贝数据到用户空间,最后应用程序才能在 recv() 中读到数据。整个过程涉及两次数据拷贝至少两次上下文切换,发送端和接收端的 CPU 都深度参与。

RDMA(Remote Direct Memory Access)彻底改变了这一范式:零拷贝(zero-copy)、内核旁路(kernel bypass)、CPU 卸载(CPU offload)三位一体,使得一台机器可以直接读写另一台机器的内存,而远端 CPU 完全不参与数据搬运。这就是”远程直接内存访问”的含义——远程(Remote)、直接(Direct)、内存访问(Memory Access)。

本章将深入 RDMA 的完整技术栈:从三种传输协议的对比,到 Verbs API 编程模型的核心抽象(PD/MR/QP/CQ/SRQ),再到 QP 状态机、RDMA CM 连接管理、单边操作的零拷贝原理,以及性能调优与动手实践。

二、RDMA 是什么?#

传统网络的数据路径#

在传统 TCP/IP 网络中,一次数据传输的完整路径如下:

发送端:
应用缓冲区 → [CPU 拷贝] → 内核 socket 缓冲区 → [协议栈封装] → sk_buff
→ [CPU 拷贝到 DMA 映射区] → NIC DMA 读取 → 网络发送
接收端:
NIC DMA 写入 → [中断通知 CPU] → 内核协议栈解析 → [CPU 拷贝] → 应用缓冲区

这条路径的问题:

  • 两次数据拷贝:用户态→内核态、内核态→NIC DMA 缓冲区(发送方向);NIC DMA 缓冲区→内核态、内核态→用户态(接收方向)
  • 上下文切换send()/recv() 系统调用触发用户态↔内核态切换
  • 协议栈处理开销:TCP/IP 协议头封装/解析、校验和计算、拥塞控制等全部由 CPU 完成
  • 中断开销:接收端每个数据包(或每批数据包)都需要中断通知 CPU

RDMA 的数据路径#

RDMA 将数据传输的路径缩短到极致:

发送端:
应用缓冲区(已注册 MR)→ NIC 直接 DMA 读取 → 网络发送
接收端:
NIC DMA 直接写入应用缓冲区(已注册 MR)→ 完成 CQE 通知

关键差异:

  1. 零拷贝:数据直接在应用缓冲区和 NIC 之间通过 DMA 传输,不经过内核缓冲区
  2. 内核旁路:数据路径完全绕过操作系统内核,不需要系统调用
  3. CPU 卸载:传输协议由 RNIC(RDMA NIC / RNIC)硬件实现,远端 CPU 完全不参与数据搬运
  4. 单边操作:RDMA Write / RDMA Read / Atomic 操作只需发起端 CPU 参与,远端 CPU 完全无感知

传统网络 vs RDMA 数据路径对比#

graph TB subgraph 传统TCP_IP["传统 TCP/IP 数据路径"] direction TB SA1["发送端应用缓冲区"] -->|CPU 拷贝| SK1["内核 Socket 缓冲区"] SK1 -->|协议栈封装| SKB1["sk_buff"] SKB1 -->|DMA| SNIC1["发送端 NIC"] SNIC1 -->|网络| RNIC1["接收端 NIC"] RNIC1 -->|中断 + DMA| RK1["内核 Socket 缓冲区"] RK1 -->|CPU 拷贝| RA1["接收端应用缓冲区"] end subgraph RDMA数据路径["RDMA 数据路径"] direction TB SA2["发送端应用缓冲区<br>(已注册 MR)"] -->|RNIC 直接 DMA 读取| SNIC2["发送端 RNIC"] SNIC2 -->|网络| RNIC2["接收端 RNIC"] RNIC2 -->|RNIC 直接 DMA 写入| RA2["接收端应用缓冲区<br>(已注册 MR)"] end style 传统TCP_IP fill:#fff3e0,stroke:#e65100 style RDMA数据路径 fill:#e8f5e9,stroke:#2e7d32
Note

RDMA 的”零拷贝”是真正的零拷贝——数据从发送端应用缓冲区到接收端应用缓冲区,全程不经过任何中间缓冲区。这与 sendfile()splice() 的”零拷贝”不同,后者只是避免了用户空间的拷贝,数据仍然经过内核缓冲区。

单边操作:远端 CPU 完全不参与#

RDMA 最革命性的特性是单边操作(one-sided operations)。在 RDMA Write、RDMA Read 和 Atomic 操作中:

  • 发起端:CPU 构造 Work Request,提交到 QP 的 Send Queue
  • 网络:RNIC 硬件处理传输协议
  • 远端:RNIC 硬件直接读写内存,CPU 完全不参与,甚至不知道发生了什么

这意味着远端 CPU 可以继续执行其他计算任务,网络 I/O 与计算真正实现了并行。这对于分布式存储、数据库、机器学习等场景至关重要——网络延迟不再占用 CPU 周期。

三、三种 RDMA 传输#

RDMA 技术有三种主流传输协议,它们在物理介质、部署成本和性能特征上各有差异,但向上都提供统一的 Verbs API 接口。

InfiniBand#

InfiniBand(IB)是 RDMA 的”原生”传输协议,从物理层到传输层完全为 RDMA 设计:

  • 专用网络:使用 InfiniBand 交换机和线缆,不兼容以太网
  • 极致延迟:端到端延迟约 0.5~1μs,是三种传输中最低的
  • 高带宽:当前 HDR(High Data Rate)可达 200Gbps,NDR 可达 400Gbps+
  • 内置服务质量:支持 SL(Service Level)、VL(Virtual Lane)等 QoS 机制
  • 子网管理:由 Subnet Manager(SM)管理路由和地址分配

InfiniBand 的典型部署场景是高性能计算(HPC)超算中心,如 Top500 超算中的互连网络。

RoCEv2#

RoCEv2(RDMA over Converged Ethernet version 2)是目前数据中心中最广泛部署的 RDMA 传输:

  • 运行在以太网之上:使用 UDP/IP 封装,可部署在标准以太网基础设施上
  • 封装格式[Ethernet][IP][UDP][IB Transport][Payload],UDP 目标端口 4791
  • 需要无损网络:依赖 PFC(Priority Flow Control)防止丢包,配合 DCQCN 拥塞控制
  • 性能接近 IB:端到端延迟约 1~2μs,带宽取决于以太网速率(100Gbps/200Gbps/400Gbps)
  • 部署成本远低于 IB:复用现有以太网交换机和线缆
Warning

RoCEv2 的无损网络配置是性能的关键保障。如果以太网交换机未正确配置 PFC 和 ECN,丢包会导致 RDMA 性能急剧下降——因为 RDMA 传输层没有 TCP 那样的重传机制,丢包恢复的开销远高于 TCP。

iWARP#

iWARP(Internet Wide Area RDMA Protocol)将 RDMA 运行在 TCP/IP 之上:

  • 运行在 TCP 之上:使用标准 TCP 连接作为传输层
  • 可在任何 IP 网络上运行:包括广域网,无需特殊交换机配置
  • 开销最高:TCP 协议栈的开销使得延迟和 CPU 占用高于 IB 和 RoCEv2
  • 生态有限:硬件支持较少,主要在特定存储场景中使用

iWARP 的优势在于网络兼容性——它可以在路由器、防火墙等标准 IP 网络设备之间传输,不需要专用的无损以太网配置。

三种传输对比#

特性InfiniBandRoCEv2iWARP
物理介质专用 IB 线缆标准以太网任何 IP 网络
封装格式原生 IB 帧UDP/IP + IB TransportTCP/IP + DDP
端到端延迟~0.5-1μs~1-2μs~2-5μs
带宽HDR 200Gbps, NDR 400Gbps+取决于以太网(100G/200G/400G)取决于以太网
交换机专用 IB 交换机标准以太网交换机(需 PFC/ECN)标准以太网交换机
部署成本高(专用设备)中(复用以太网)低(复用以太网)
无损要求原生无损需配置 PFC/DCQCN不需要(TCP 重传)
路由支持子网内(SM 管理)IP 路由IP 路由
生态成熟度HPC 领域成熟数据中心主流有限
典型场景HPC 超算云计算、分布式存储广域 RDMA、存储
graph LR subgraph 应用层 APP["Verbs API<br>ibv_post_send / ibv_poll_cq"] end subgraph 传输层 IB["InfiniBand<br>原生 RDMA 传输"] ROCE["RoCEv2<br>UDP/IP 封装"] IWARP["iWARP<br>TCP/IP 封装"] end subgraph 链路层 IBL["IB 链路层<br>专用交换机"] ETH["以太网链路层<br>标准交换机 + PFC"] IPNET["IP 网络<br>标准路由器"] end APP --> IB APP --> ROCE APP --> IWARP IB --> IBL ROCE --> ETH IWARP --> IPNET style APP fill:#e8eaf6,stroke:#283593 style IB fill:#e1bee7,stroke:#6a1b9a style ROCE fill:#c8e6c9,stroke:#2e7d32 style IWARP fill:#fff9c4,stroke:#f9a825
Note

三种传输协议向上都提供统一的 Verbs API——应用程序不需要为不同的传输协议编写不同的代码。选择哪种传输主要取决于基础设施条件和性能需求,而非编程接口的差异。

四、Verbs API 编程模型#

Verbs API 是 RDMA 编程的核心接口,由 libibverbs 提供传输操作,librdmacm 提供连接管理。理解 Verbs 的关键在于掌握其核心抽象:Protection Domain、Memory Region、Queue Pair、Completion Queue 和 Shared Receive Queue。

核心对象关系#

graph TB PD["Protection Domain<br>保护域 — 内存隔离边界"] PD --> MR1["Memory Region 1<br>lkey / rkey"] PD --> MR2["Memory Region 2<br>lkey / rkey"] PD --> QP1["Queue Pair 1<br>Send Queue + Receive Queue"] PD --> QP2["Queue Pair 2<br>Send Queue + Receive Queue"] PD --> SRQ["Shared Receive Queue<br>共享接收队列"] CQ1["Completion Queue A"] CQ2["Completion Queue B"] QP1 -->|"发送完成通知"| CQ1 QP1 -->|"接收完成通知"| CQ2 QP2 -->|"发送完成通知"| CQ1 QP2 -->|"接收完成通知"| CQ2 SRQ -->|"接收完成通知"| CQ2 QP1 -.->|"关联 SRQ"| SRQ QP2 -.->|"关联 SRQ"| SRQ style PD fill:#e8eaf6,stroke:#283593 style CQ1 fill:#e1bee7,stroke:#6a1b9a style CQ2 fill:#e1bee7,stroke:#6a1b9a style SRQ fill:#fff9c4,stroke:#f9a825

Protection Domain(PD):内存隔离边界#

Protection Domain 是 RDMA 内存隔离的基本单位。所有 MR 和 QP 都必须关联到同一个 PD,才能互相访问:

// 创建 Protection Domain
struct ibv_pd *pd = ibv_alloc_pd(context);
if (!pd) {
perror("ibv_alloc_pd");
exit(1);
}
// 释放 Protection Domain
ibv_dealloc_pd(pd);

PD 的核心规则:

  • 同一 PD 下的 MR 可以被同一 PD 下的 QP 访问
  • 不同 PD 之间的 MR 和 QP 相互隔离
  • 一个 PD 可以关联多个 MR 和多个 QP
  • PD 本身不涉及数据传输,只是访问控制的边界

Memory Region(MR):注册内存供远程访问#

MR 是 RDMA 编程中最关键的概念之一。应用程序必须先注册内存区域,RNIC 才能对其进行 DMA 操作:

// 注册 Memory Region
void *buf = malloc(BUFFER_SIZE);
struct ibv_mr *mr = ibv_reg_mr(pd, buf, BUFFER_SIZE,
IBV_ACCESS_LOCAL_WRITE | // 本地写权限
IBV_ACCESS_REMOTE_WRITE | // 远端写权限
IBV_ACCESS_REMOTE_READ | // 远端读权限
IBV_ACCESS_REMOTE_ATOMIC); // 远端原子操作权限
if (!mr) {
perror("ibv_reg_mr");
exit(1);
}
// mr 包含两个关键密钥:
// mr->lkey — 本地访问密钥(Local Key),提交 Work Request 时使用
// mr->rkey — 远程访问密钥(Remote Key),远端进行 RDMA 操作时使用
printf("lkey: 0x%x, rkey: 0x%x\n", mr->lkey, mr->rkey);
// 注销 Memory Region
ibv_dereg_mr(mr);

MR 注册时内核做了什么:

  1. 锁定物理页面:调用 get_user_pages() 锁定 MR 对应的物理页面,防止被换出
  2. 建立 DMA 映射:将物理地址映射到 RNIC 可访问的 DMA 地址
  3. 生成访问密钥:RNIC 硬件生成 lkey 和 rkey,用于后续访问控制
  4. 注册到 RNIC:将 MR 的虚拟地址→物理地址→DMA 地址映射关系写入 RNIC 的内存翻译表(Memory Translation Table, MTT)
Warning

MR 注册是昂贵的操作——涉及内核系统调用、页面锁定和 DMA 映射。不要频繁注册/注销 MR。最佳实践是在程序启动时注册大块内存,在整个生命周期内复用。使用 Huge Pages 可以显著降低 MR 注册的开销和 MTT 表项数量。

Queue Pair(QP):发送与接收队列#

QP 是 RDMA 通信的基本端点,每个 QP 包含一个 Send Queue(SQ)和一个 Receive Queue(RQ):

// 创建 Queue Pair
struct ibv_qp_init_attr qp_init_attr = {
.send_cq = cq, // 发送完成队列
.recv_cq = cq, // 接收完成队列(可与 send_cq 相同)
.qp_type = IBV_QPT_RC, // 可靠连接(Reliable Connection)
.cap = {
.max_send_wr = 256, // Send Queue 最大 Work Request 数
.max_recv_wr = 256, // Receive Queue 最大 Work Request 数
.max_send_sge = 4, // 每个 Send WR 最大 Scatter/Gather Element 数
.max_recv_sge = 4, // 每个 Recv WR 最大 Scatter/Gather Element 数
.max_inline_data = 64, // 内联数据最大字节数
},
.sq_sig_all = 0, // 仅在 WR 中指定时才产生完成通知
};
struct ibv_qp *qp = ibv_create_qp(pd, &qp_init_attr);
if (!qp) {
perror("ibv_create_qp");
exit(1);
}

QP 的类型:

QP 类型全称特性
IBV_QPT_RCReliable Connection可靠、有序、一对一连接,最常用
IBV_QPT_UCUnreliable Connection不可靠、有序、一对一连接
IBV_QPT_UDUnreliable Datagram不可靠、无序、一对多(组播)
IBV_QPT_XRCeXtended RC共享 QP,多进程复用同一连接

QP 的两个队列:

  • Send Queue(SQ):存放发送操作的 Work Request(WR),包括 Send、RDMA Write、RDMA Read、Atomic
  • Receive Queue(RQ):仅存放 Receive 操作的 WR,告诉 RNIC 接收数据后写入哪个缓冲区
Note

对于 RDMA Write、RDMA Read 和 Atomic 操作,远端不需要在 RQ 中预先 Post Receive——这些是单边操作,远端 RNIC 根据 rkey 和远程地址直接操作内存。只有 Send/Recv 双边操作需要远端预先 Post Receive。

Completion Queue(CQ):完成通知#

CQ 是 RDMA 的完成通知机制。当一个 WR 执行完毕后,RNIC 会在对应的 CQ 中产生一个 Completion Queue Entry(CQE):

// 创建 Completion Queue
struct ibv_cq *cq = ibv_create_cq(context, 256, NULL, NULL, 0);
if (!cq) {
perror("ibv_create_cq");
exit(1);
}
// 轮询 CQ 获取完成通知
struct ibv_wc wc;
int ne = ibv_poll_cq(cq, 1, &wc);
if (ne > 0) {
if (wc.status != IBV_WC_SUCCESS) {
fprintf(stderr, "完成状态错误: %s\n", ibv_wc_status_str(wc.status));
} else {
printf("WR 完成,opcode: %d, byte_len: %u\n", wc.opcode, wc.byte_len);
// wc.wr_id 是提交 WR 时设置的用户自定义标识
}
}
// 也可以使用完成通道(Completion Channel)实现事件驱动
struct ibv_comp_channel *channel = ibv_create_comp_channel(context);
struct ibv_cq *cq = ibv_create_cq(context, 256, NULL, channel, 0);
// 请求完成通知
ibv_req_notify_cq(cq, 0);
// 等待完成事件
struct ibv_cq *ev_cq;
void *ev_ctx;
ibv_get_cq_event(channel, &ev_cq, &ev_ctx);
// 处理完成后需要重新请求通知
ibv_ack_cq_events(ev_cq, 1);
ibv_req_notify_cq(cq, 0);

CQ 的两种使用模式:

  1. 轮询模式(Polling):反复调用 ibv_poll_cq(),延迟最低但 CPU 占用 100%
  2. 事件驱动模式(Event-driven):通过完成通道等待中断,CPU 占用低但延迟略高

高性能场景通常使用轮询模式,牺牲 CPU 占用换取最低延迟。

Shared Receive Queue(SRQ):共享接收缓冲区#

在大规模连接场景中,如果每个 QP 都预分配接收缓冲区,内存消耗巨大。SRQ 允许多个 QP 共享同一个接收队列:

// 创建 Shared Receive Queue
struct ibv_srq_init_attr srq_init_attr = {
.attr = {
.max_wr = 1024, // 最大 Receive WR 数
.max_sge = 4, // 每个 WR 最大 SGE 数
.srq_limit = 16, // 低水位线,触发事件通知
},
};
struct ibv_srq *srq = ibv_create_srq(pd, &srq_init_attr);
// 向 SRQ 投递 Receive WR
struct ibv_recv_wr wr, *bad_wr;
struct ibv_sge sge;
sge.addr = (uintptr_t)buf;
sge.length = BUFFER_SIZE;
sge.lkey = mr->lkey;
wr.next = NULL;
wr.wr_id = 1;
wr.sg_list = &sge;
wr.num_sge = 1;
ibv_post_srq_recv(srq, &wr, &bad_wr);
// 创建 QP 时关联 SRQ
struct ibv_qp_init_attr qp_init_attr = {
.srq = srq, // 关联 SRQ,QP 不再使用自己的 RQ
// ... 其他属性
};

SRQ 的优势:

  • 节省内存:N 个 QP 共享一个 SRQ,接收缓冲区总量从 N × per_qp_buffer 降为 total_shared_buffer
  • 灵活分配:接收缓冲区按需分配给活跃的 QP,而非静态预留
  • 适合大规模连接:分布式存储系统中数百个连接共享少量接收缓冲区

五、QP 状态机#

QP 在其生命周期中经历严格的状态转换,每个状态定义了 QP 可以执行的操作。理解 QP 状态机是 RDMA 编程的基础——状态转换错误是最常见的编程 bug 之一。

QP 状态与转换#

stateDiagram-v2 [*] --> RESET : ibv_create_qp() RESET --> INIT : ibv_modify_qp(IBV_QPS_INIT)<br>设置: port, pkey_index, access_flags INIT --> RTR : ibv_modify_qp(IBV_QPS_RTR)<br>设置: dest_qp_num, rq_psn, dgid, dlid, etc. RTR --> RTS : ibv_modify_qp(IBV_QPS_RTS)<br>设置: sq_psn, timeout, retry_cnt, rnr_retry RTS --> RTS : ibv_modify_qp() 修改属性 RTS --> SQD : ibv_modify_qp(IBV_QPS_SQD)<br>发送队列排空 SQD --> RTS : ibv_modify_qp(IBV_QPS_RTS)<br>重新激活 RTS --> ERR : 错误发生 / ibv_modify_qp(IBV_QPS_ERR) SQD --> ERR : 错误发生 RTR --> ERR : 错误发生 INIT --> ERR : 错误发生 ERR --> RESET : ibv_modify_qp(IBV_QPS_RESET) SQD --> SQE : 发送队列错误 SQE --> ERR : ibv_modify_qp(IBV_QPS_ERR) RESET --> [*] : ibv_destroy_qp()

各状态详解#

RESET 状态

  • QP 刚创建时的初始状态
  • 不能提交任何 WR
  • 所有队列被清空

INIT 状态

  • QP 已初始化,关联了端口和 PKey
  • 可以向 RQ 提交 Receive WR
  • 不能向 SQ 提交 Send WR
  • 不能发送或接收数据

RTR(Ready to Receive)状态

  • QP 已准备好接收数据
  • 必须设置远端 QP 号、PSN、GID/LID 等对端信息
  • 可以接收 Send 消息和 RDMA 操作
  • 仍不能发送数据

RTS(Ready to Send)状态

  • QP 已完全就绪,可以发送和接收
  • 必须设置本地 PSN、超时、重试计数等
  • 可以提交所有类型的 WR(Send、RDMA Write、RDMA Read、Atomic)

SQD(Send Queue Drained)状态

  • 发送队列正在排空,等待所有已提交的 Send WR 完成
  • 不接受新的 Send WR
  • 接收端仍正常工作
  • 通常用于修改 QP 属性而不影响接收

SQE(Send Queue Error)状态

  • 发送队列遇到错误
  • 接收队列可能仍正常
  • 只能转换到 ERR 状态

ERR 状态

  • QP 遇到不可恢复的错误
  • 所有未完成的 WR 产生错误 CQE
  • 只能转换回 RESET 状态

状态转换代码示例#

// RESET → INIT
struct ibv_qp_attr attr;
memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_INIT;
attr.port_num = 1; // 物理端口编号
attr.pkey_index = 0; // PKey 索引
attr.qp_access_flags = IBV_ACCESS_REMOTE_WRITE |
IBV_ACCESS_REMOTE_READ |
IBV_ACCESS_REMOTE_ATOMIC;
if (ibv_modify_qp(qp, &attr,
IBV_QP_STATE |
IBV_QP_PKEY_INDEX |
IBV_QP_PORT |
IBV_QP_ACCESS_FLAGS)) {
fprintf(stderr, "RESET → INIT 失败\n");
exit(1);
}
// INIT → RTR
memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_RTR;
attr.path_mtu = IBV_MTU_4096; // 路径 MTU
attr.dest_qp_num = remote_qpn; // 远端 QP 号
attr.rq_psn = 0; // 接收端起始 PSN
attr.max_dest_rd_atomic = 1; // 远端可接受的未完成 RDMA Read/Atomic 数
attr.min_rnr_timer = 12; // RNR (Receiver Not Ready) 定时器
// RoCEv2 需要设置 Global Routing 信息
attr.ah_attr.is_global = 1;
attr.ah_attr.dlid = remote_lid; // 远端 LID
attr.ah_attr.sl = 0; // Service Level
attr.ah_attr.src_path_bits = 0;
attr.ah_attr.port_num = 1;
attr.ah_attr.grh.dgid = remote_gid; // 远端 GID
attr.ah_attr.grh.sgid_index = 0; // 本地 GID 索引
attr.ah_attr.grh.hop_limit = 64;
if (ibv_modify_qp(qp, &attr,
IBV_QP_STATE |
IBV_QP_AV |
IBV_QP_PATH_MTU |
IBV_QP_DEST_QPN |
IBV_QP_RQ_PSN |
IBV_QP_MAX_DEST_RD_ATOMIC |
IBV_QP_MIN_RNR_TIMER)) {
fprintf(stderr, "INIT → RTR 失败\n");
exit(1);
}
// RTR → RTS
memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_RTS;
attr.timeout = 14; // 重传超时(4.096μs × 2^14 ≈ 67ms)
attr.retry_cnt = 7; // 重传次数
attr.rnr_retry = 7; // RNR 重试次数
attr.sq_psn = 0; // 发送端起始 PSN
attr.max_rd_atomic = 1; // 本端可发出的未完成 RDMA Read/Atomic 数
if (ibv_modify_qp(qp, &attr,
IBV_QP_STATE |
IBV_QP_TIMEOUT |
IBV_QP_RETRY_CNT |
IBV_QP_RNR_RETRY |
IBV_QP_SQ_PSN |
IBV_QP_MAX_QP_RD_ATOMIC)) {
fprintf(stderr, "RTR → RTS 失败\n");
exit(1);
}
Warning

QP 状态转换的顺序是严格的:RESET → INIT → RTR → RTS。跳过任何状态都会导致 ibv_modify_qp() 返回错误。此外,INIT → RTR 和 RTR → RTS 的属性设置必须完整——遗漏必需的属性标志也会导致失败。这是 RDMA 编程中最常见的错误来源之一。

六、RDMA CM:连接管理#

手动交换 QP 信息(QPN、PSN、GID、LID)并调用 ibv_modify_qp() 进行状态转换非常繁琐。RDMA CM(Connection Manager)库 librdmacm 封装了这一过程,提供了类似 socket API 的连接管理接口。

RDMA CM 核心 API#

函数作用
rdma_create_id()创建 RDMA CM 标识符
rdma_bind_addr()绑定地址
rdma_listen()监听连接
rdma_get_request()获取连接请求
rdma_accept()接受连接
rdma_connect()发起连接
rdma_disconnect()断开连接
rdma_destroy_id()销毁 CM 标识符

服务端代码骨架#

#include <rdma/rdma_cma.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFFER_SIZE 4096
struct context {
struct ibv_pd *pd;
struct ibv_mr *mr;
struct ibv_cq *cq;
struct ibv_qp *qp;
char *buf;
};
int main(int argc, char **argv)
{
struct rdma_cm_id *listen_id, *conn_id;
struct rdma_event_channel *ec;
struct rdma_cm_event *event;
struct context ctx;
// 1. 创建事件通道和监听 ID
ec = rdma_create_event_channel();
rdma_create_id(ec, &listen_id, NULL, RDMA_PS_TCP);
// 2. 绑定地址并监听
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(18515); // 监听端口
addr.sin_addr.s_addr = htonl(INADDR_ANY);
rdma_bind_addr(listen_id, (struct sockaddr *)&addr);
rdma_listen(listen_id, 1);
printf("服务端监听端口 %d\n", 18515);
// 3. 等待连接请求
rdma_get_cm_event(ec, &event);
if (event->event != RDMA_CM_EVENT_CONNECT_REQUEST) {
fprintf(stderr, "意外事件: %s\n", rdma_event_str(event->event));
exit(1);
}
conn_id = event->id;
rdma_ack_cm_event(event);
// 4. 在新连接上创建 PD、MR、CQ、QP
ctx.pd = ibv_alloc_pd(conn_id->verbs);
ctx.buf = malloc(BUFFER_SIZE);
ctx.mr = ibv_reg_mr(ctx.pd, ctx.buf, BUFFER_SIZE,
IBV_ACCESS_LOCAL_WRITE |
IBV_ACCESS_REMOTE_WRITE |
IBV_ACCESS_REMOTE_READ);
ctx.cq = ibv_create_cq(conn_id->verbs, 16, NULL, NULL, 0);
struct ibv_qp_init_attr qp_attr;
memset(&qp_attr, 0, sizeof(qp_attr));
qp_attr.send_cq = ctx.cq;
qp_attr.recv_cq = ctx.cq;
qp_attr.qp_type = IBV_QPT_RC;
qp_attr.cap.max_send_wr = 16;
qp_attr.cap.max_recv_wr = 16;
qp_attr.cap.max_send_sge = 1;
qp_attr.cap.max_recv_sge = 1;
// RDMA CM 自动处理 QP 创建和状态转换
rdma_create_qp(conn_id, ctx.pd, &qp_attr);
ctx.qp = conn_id->qp;
// 5. 接受连接
struct rdma_conn_param conn_param;
memset(&conn_param, 0, sizeof(conn_param));
conn_param.private_data = &ctx.mr->rkey; // 可传递私有数据
conn_param.private_data_len = sizeof(ctx.mr->rkey);
rdma_accept(conn_id, &conn_param);
// 6. 等待建立完成
rdma_get_cm_event(ec, &event);
rdma_ack_cm_event(event);
printf("连接建立成功!\n");
// ... 数据传输 ...
// 7. 清理
rdma_disconnect(conn_id);
ibv_destroy_qp(ctx.qp);
ibv_dereg_mr(ctx.mr);
ibv_destroy_cq(ctx.cq);
ibv_dealloc_pd(ctx.pd);
free(ctx.buf);
rdma_destroy_id(conn_id);
rdma_destroy_id(listen_id);
rdma_destroy_event_channel(ec);
return 0;
}

客户端代码骨架#

#include <rdma/rdma_cma.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUFFER_SIZE 4096
int main(int argc, char **argv)
{
struct rdma_cm_id *conn_id;
struct rdma_event_channel *ec;
struct rdma_cm_event *event;
// 1. 创建事件通道和连接 ID
ec = rdma_create_event_channel();
rdma_create_id(ec, &conn_id, NULL, RDMA_PS_TCP);
// 2. 解析服务端地址并连接
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(18515);
inet_pton(AF_INET, argv[1], &addr.sin_addr);
rdma_resolve_addr(conn_id, NULL, (struct sockaddr *)&addr, 2000);
// 3. 等待地址解析完成
rdma_get_cm_event(ec, &event);
rdma_ack_cm_event(event);
// 4. 等待路由解析完成
rdma_get_cm_event(ec, &event);
rdma_ack_cm_event(event);
// 5. 创建 PD、MR、CQ、QP(与服务端类似)
struct ibv_pd *pd = ibv_alloc_pd(conn_id->verbs);
char *buf = malloc(BUFFER_SIZE);
struct ibv_mr *mr = ibv_reg_mr(pd, buf, BUFFER_SIZE,
IBV_ACCESS_LOCAL_WRITE |
IBV_ACCESS_REMOTE_WRITE |
IBV_ACCESS_REMOTE_READ);
struct ibv_cq *cq = ibv_create_cq(conn_id->verbs, 16, NULL, NULL, 0);
struct ibv_qp_init_attr qp_attr;
memset(&qp_attr, 0, sizeof(qp_attr));
qp_attr.send_cq = cq;
qp_attr.recv_cq = cq;
qp_attr.qp_type = IBV_QPT_RC;
qp_attr.cap.max_send_wr = 16;
qp_attr.cap.max_recv_wr = 16;
qp_attr.cap.max_send_sge = 1;
qp_attr.cap.max_recv_sge = 1;
rdma_create_qp(conn_id, pd, &qp_attr);
// 6. 发起连接
struct rdma_conn_param conn_param;
memset(&conn_param, 0, sizeof(conn_param));
rdma_connect(conn_id, &conn_param);
// 7. 等待连接建立
rdma_get_cm_event(ec, &event);
if (event->event != RDMA_CM_EVENT_ESTABLISHED) {
fprintf(stderr, "连接失败: %s\n", rdma_event_str(event->event));
exit(1);
}
rdma_ack_cm_event(event);
printf("连接建立成功!\n");
// ... 数据传输 ...
// 8. 清理
rdma_disconnect(conn_id);
rdma_destroy_qp(conn_id);
ibv_dereg_mr(mr);
ibv_destroy_cq(cq);
ibv_dealloc_pd(pd);
free(buf);
rdma_destroy_id(conn_id);
rdma_destroy_event_channel(ec);
return 0;
}

RDMA CM 连接建立流程#

sequenceDiagram participant C as 客户端 participant S as 服务端 C->>C: rdma_create_id() C->>C: rdma_resolve_addr() C->>C: rdma_resolve_route() C->>C: 创建 PD / MR / CQ / QP S->>S: rdma_create_id() S->>S: rdma_bind_addr() S->>S: rdma_listen() C->>S: rdma_connect() → CM Connect Request S->>S: 收到 CONNECT_REQUEST 事件 S->>S: 创建 PD / MR / CQ / QP S->>C: rdma_accept() → CM Connect Response C->>C: 收到 ESTABLISHED 事件 S->>S: 收到 ESTABLISHED 事件 Note over C,S: 连接建立完成,可以开始数据传输 C->>S: rdma_disconnect() → CM Disconnect S->>S: 收到 DISCONNECTED 事件 S->>S: rdma_disconnect()
Note

RDMA CM 在内部自动完成了 QP 的创建和 RESET → INIT → RTR → RTS 状态转换,以及 QP 信息的交换(QPN、PSN、GID/LID)。这大大简化了 RDMA 编程的复杂度。但在理解底层原理时,仍需掌握手动 ibv_modify_qp() 的流程。

七、数据传输操作#

RDMA 支持两大类数据传输操作:双边操作(Send/Recv)和单边操作(RDMA Write/RDMA Read/Atomic)。

Send / Recv:双边操作#

Send/Recv 是最基本的 RDMA 通信方式,类似传统 socket 的 send/recv,但绕过了内核:

  • Send 端:向 SQ 提交 Send WR,数据从本地 MR 发送
  • Recv 端:必须预先向 RQ 提交 Receive WR,指定接收缓冲区
  • 双方 CPU 都参与:发送端 CPU 提交 WR,接收端 CPU 必须预先 Post Receive
// 接收端:预先 Post Receive
struct ibv_recv_wr recv_wr, *bad_recv_wr;
struct ibv_sge recv_sge;
recv_sge.addr = (uintptr_t)recv_buf;
recv_sge.length = BUFFER_SIZE;
recv_sge.lkey = recv_mr->lkey;
recv_wr.wr_id = 1;
recv_wr.next = NULL;
recv_wr.sg_list = &recv_sge;
recv_wr.num_sge = 1;
ibv_post_recv(qp, &recv_wr, &bad_recv_wr);
// 发送端:Post Send
struct ibv_send_wr send_wr, *bad_send_wr;
struct ibv_sge send_sge;
send_sge.addr = (uintptr_t)send_buf;
send_sge.length = msg_size;
send_sge.lkey = send_mr->lkey;
send_wr.wr_id = 2;
send_wr.next = NULL;
send_wr.sg_list = &send_sge;
send_wr.num_sge = 1;
send_wr.opcode = IBV_WR_SEND;
send_wr.send_flags = IBV_SEND_SIGNALED; // 请求完成通知
ibv_post_send(qp, &send_wr, &bad_send_wr);
Warning

如果接收端没有预先 Post Receive,发送端的 Send WR 会触发 RNR(Receiver Not Ready)错误。RNIC 会根据 rnr_retrymin_rnr_timer 进行重试,超过重试次数后 QP 进入 ERR 状态。这是 RDMA 编程中另一个常见错误。

RDMA Write:单边写操作#

RDMA Write 是最常用的单边操作——发起端直接将数据写入远端内存,远端 CPU 完全不参与:

// RDMA Write 示例
// 前提:已通过连接建立交换了远端的 rkey 和远程地址
struct ibv_send_wr wr, *bad_wr;
struct ibv_sge sge;
// 本地数据源
sge.addr = (uintptr_t)local_buf; // 本地 MR 缓冲区地址
sge.length = data_len; // 要写入的数据长度
sge.lkey = local_mr->lkey; // 本地 MR 的 lkey
memset(&wr, 0, sizeof(wr));
wr.wr_id = 100;
wr.next = NULL;
wr.sg_list = &sge;
wr.num_sge = 1;
wr.opcode = IBV_WR_RDMA_WRITE; // RDMA Write 操作
wr.send_flags = IBV_SEND_SIGNALED;
wr.wr.rdma.remote_addr = remote_addr; // 远端内存地址
wr.wr.rdma.rkey = remote_rkey; // 远端 MR 的 rkey
if (ibv_post_send(qp, &wr, &bad_wr)) {
fprintf(stderr, "RDMA Write 提交失败\n");
exit(1);
}
// 等待完成
struct ibv_wc wc;
while (ibv_poll_cq(cq, 1, &wc) == 0);
if (wc.status != IBV_WC_SUCCESS) {
fprintf(stderr, "RDMA Write 失败: %s\n", ibv_wc_status_str(wc.status));
}

RDMA Write 的零拷贝原理:

  1. 发送端 CPU 调用 ibv_post_send(),将 WR 提交到 QP 的 SQ
  2. 发送端 RNIC 通过 DMA 从本地 MR 读取数据
  3. RNIC 封装传输协议头,通过物理链路发送
  4. 接收端 RNIC 收到数据后,根据 rkey 查找 MR,验证访问权限
  5. 接收端 RNIC 通过 DMA 将数据直接写入 remote_addr 指定的内存位置
  6. 接收端 CPU 全程不参与——甚至不知道数据被写入了

RDMA Write with Immediate Data#

RDMA Write 还可以携带 4 字节的 Immediate Data:

wr.opcode = IBV_WR_RDMA_WRITE_WITH_IMM;
wr.imm_data = htonl(0x12345678); // 4 字节立即数
// 其余字段与 RDMA Write 相同

与普通 RDMA Write 的区别:

  • 普通 RDMA Write:远端不产生 CQE,远端 CPU 完全无感知
  • RDMA Write with Imm:远端 RQ 产生 CQE(消耗一个预 Post 的 Receive WR),远端 CPU 可以通过 CQE 得到通知

这解决了”远端如何知道数据已到达”的问题——用 Immediate Data 携带元数据(如消息长度、操作类型),远端 CPU 通过 CQE 事件感知数据到达。

RDMA Read:单边读操作#

RDMA Read 允许发起端直接从远端内存读取数据:

// RDMA Read 示例
struct ibv_send_wr wr, *bad_wr;
struct ibv_sge sge;
// 本地目标缓冲区(数据将读到这里)
sge.addr = (uintptr_t)local_buf; // 本地 MR 缓冲区地址
sge.length = data_len; // 要读取的数据长度
sge.lkey = local_mr->lkey; // 本地 MR 的 lkey
memset(&wr, 0, sizeof(wr));
wr.wr_id = 200;
wr.next = NULL;
wr.sg_list = &sge;
wr.num_sge = 1;
wr.opcode = IBV_WR_RDMA_READ; // RDMA Read 操作
wr.send_flags = IBV_SEND_SIGNALED;
wr.wr.rdma.remote_addr = remote_addr; // 远端内存地址
wr.wr.rdma.rkey = remote_rkey; // 远端 MR 的 rkey
if (ibv_post_send(qp, &wr, &bad_wr)) {
fprintf(stderr, "RDMA Read 提交失败\n");
exit(1);
}
// 等待完成
struct ibv_wc wc;
while (ibv_poll_cq(cq, 1, &wc) == 0);
if (wc.status != IBV_WC_SUCCESS) {
fprintf(stderr, "RDMA Read 失败: %s\n", ibv_wc_status_str(wc.status));
}
printf("成功从远端读取 %u 字节\n", wc.byte_len);

RDMA Read 的典型应用场景:

  • 分布式存储:客户端从存储节点读取数据块
  • RPC 框架:服务端读取客户端请求中的大块参数
  • 分布式数据库:节点间读取远程数据页
Note

RDMA Read 的吞吐量受 max_rd_atomic 参数限制——一个 QP 同时在途的 RDMA Read/Atomic 请求数不能超过此值。默认值通常为 1~4,需要根据应用场景调整。增大此值可以提高吞吐,但会消耗更多 RNIC 资源。

Atomic 操作:远程原子操作#

RDMA Atomic 操作在远端内存上执行原子性的读-修改-写操作:

// Compare and Swap (CAS)
struct ibv_send_wr wr, *bad_wr;
struct ibv_sge sge;
sge.addr = (uintptr_t)&local_result; // 存放 CAS 返回的原始值
sge.length = sizeof(uint64_t);
sge.lkey = local_mr->lkey;
memset(&wr, 0, sizeof(wr));
wr.wr_id = 300;
wr.next = NULL;
wr.sg_list = &sge;
wr.num_sge = 1;
wr.opcode = IBV_WR_ATOMIC_CMP_AND_SWP;
wr.send_flags = IBV_SEND_SIGNALED;
wr.wr.atomic.remote_addr = remote_addr;
wr.wr.atomic.rkey = remote_rkey;
wr.wr.atomic.compare_add = expected_value; // 期望值
wr.wr.atomic.swap = new_value; // 新值
ibv_post_send(qp, &wr, &bad_wr);
// Fetch and Add
memset(&wr, 0, sizeof(wr));
wr.wr_id = 301;
wr.next = NULL;
wr.sg_list = &sge;
wr.num_sge = 1;
wr.opcode = IBV_WR_ATOMIC_FETCH_AND_ADD;
wr.send_flags = IBV_SEND_SIGNALED;
wr.wr.atomic.remote_addr = remote_addr;
wr.wr.atomic.rkey = remote_rkey;
wr.wr.atomic.compare_add = increment; // 增量值
ibv_post_send(qp, &wr, &bad_wr);

Atomic 操作的语义:

  • Compare and Swap:如果远端地址的值等于 compare_add,则将其替换为 swap,返回原始值
  • Fetch and Add:将远端地址的值加上 compare_add,返回原始值

两种操作都是原子性的——RNIC 硬件保证在执行过程中不会被其他操作打断。这使得 RDMA Atomic 可以用于实现分布式锁、无锁数据结构、分布式计数器等。

操作类型对比#

操作类型远端 CPU远端 RQ典型用途
Send/Recv双边参与(Post Recv)消耗 Recv WR控制消息、RPC 请求/响应
RDMA Write单边不参与不消耗批量数据写入、日志复制
RDMA Write+Imm单边+通知CQE 通知消耗 Recv WR数据写入 + 通知远端
RDMA Read单边不参与不消耗远程数据读取
Atomic CAS单边不参与不消耗分布式锁、无锁数据结构
Atomic Fetch&Add单边不参与不消耗分布式计数器

七、性能特征与调优#

延迟与吞吐量#

RDMA 的性能远超传统 TCP/IP 网络:

指标InfiniBandRoCEv2TCP/IP(参考)
延迟(1 字节)~0.5-1μs~1-2μs~20-50μs
带宽HDR 200Gbps100-400Gbps取决于 CPU 和协议栈
CPU 占用< 5%< 10%30-100%
消息速率~100M msg/s~50M msg/s~1-5M msg/s

Huge Pages 与 MR 注册#

MR 注册时,RNIC 需要为每个 4KB 页面创建一个 MTT(Memory Translation Table)条目。对于大块内存,这会导致:

  • MTT 表项数量巨大,占用 RNIC 内存
  • 注册时间与页面数成正比
  • TLB miss 导致地址翻译延迟

使用 Huge Pages 可以显著改善:

# 分配 Huge Pages
echo 1024 > /proc/sys/vm/nr_hugepages # 分配 1024 个 2MB Huge Pages
# 查看 Huge Pages 状态
cat /proc/meminfo | grep Huge
# 在程序中使用 Huge Pages
# 方式一:通过环境变量
export RDMAV_HUGEPAGES_SAFE=1
# 方式二:使用 mmap 分配 Huge Pages
void *buf = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);

Huge Pages 的效果:

  • 2MB 页面 vs 4KB 页面:MTT 条目减少 512 倍
  • 注册时间从秒级降到毫秒级
  • RNIC TLB 命中率大幅提升

NUMA 亲和性#

RDMA 性能高度依赖 NUMA 亲和性。RNIC 插在特定的 NUMA 节点上,如果应用运行在远端 NUMA 节点,DMA 传输需要跨越 QPI/UPI 互连,延迟增加 30-50%:

# 查看 RNIC 的 NUMA 节点
cat /sys/class/infiniband/mlx5_0/device/numa_node
# 将进程绑定到 RNIC 所在的 NUMA 节点
numactl --cpunodebind=0 --membind=0 ./rdma_app
# 查看 CPU 与 NUMA 节点的拓扑
numactl --hardware
lscpu | grep NUMA

最佳实践:

  • 应用线程:绑定到 RNIC 所在 NUMA 节点的 CPU 核心
  • MR 内存:从 RNIC 所在 NUMA 节点分配
  • CQ 轮询线程:与 QP 在同一 NUMA 节点
  • 中断亲和性:将 RNIC 中断绑定到同一 NUMA 节点的 CPU

Inline Data:小消息优化#

对于小消息(通常 < 64 字节),RDMA 支持将数据直接内联在 WQE(Work Queue Entry)中,省去一次 DMA 读取:

// 创建 QP 时设置 inline data 大小
qp_init_attr.cap.max_inline_data = 64;
// 提交 WR 时使用 inline 标志
wr.send_flags |= IBV_SEND_INLINE;
// 此时 sge.addr 指向的数据会被直接复制到 WQE 中
// 不需要 MR,不需要 DMA 读取

Inline Data 的适用场景:

  • RPC 请求头(通常几十字节)
  • 控制消息
  • 小于 max_inline_data 的消息
Note

Inline Data 消除了小消息的 DMA 读取延迟,但会增加 QP 的 WQE 大小,从而减少 SQ 可容纳的 WR 数量。对于大消息,不要使用 inline——数据会被复制到 WQE 中,反而增加了开销。

Doorbell Batching:减少 MMIO 写入#

每次 ibv_post_send() 都会产生一次 MMIO(Memory-Mapped I/O)写入——通知 RNIC 有新的 WR 需要处理。MMIO 写入是 uncached 的,延迟约 50-100ns,是 RDMA 延迟的重要组成部分。

Doorbell Batching 通过一次 MMIO 写入通知 RNIC 处理多个 WR:

// 链式提交多个 WR
struct ibv_send_wr wr1, wr2, wr3, *bad_wr;
// 设置 wr1, wr2, wr3 ...
wr1.next = &wr2;
wr2.next = &wr3;
wr3.next = NULL;
// 一次 ibv_post_send 提交 3 个 WR
// 只产生一次 Doorbell(一次 MMIO 写入)
ibv_post_send(qp, &wr1, &bad_wr);

其他调优参数#

# 调整 CQ 轮询批量大小
# 每次轮询处理多个 CQE,减少系统调用次数
int ne = ibv_poll_cq(cq, 32, wc_array); // 一次轮询最多 32
# 调整 QP 深度
# 增大 max_send_wr / max_recv_wr 允许更多在途请求
qp_init_attr.cap.max_send_wr = 1024;
qp_init_attr.cap.max_recv_wr = 1024;
# 启用自适应轮询
# 在低负载时退避,高负载时积极轮询
# 需要应用层实现

十、动手实践#

Practice 1:检查 RDMA 硬件#

# 查看系统中的 RDMA 设备
ibv_devinfo
# 输出示例:
# hca_id: mlx5_0
# transport: InfiniBand (0)
# fw_ver: 28.39.3000
# node_guid: 506b:4b03:00e8:7a40
# sys_image_guid: 506b:4b03:00e8:7a43
# vendor_id: 0x02c9
# vendor_part_id: 4123
# hw_ver: 0x0
# board_id: MT_0000000012
# phys_port_cnt: 1
# port: 1
# state: PORT_ACTIVE (4)
# max_mtu: 4096 (5)
# active_mtu: 4096 (5)
# sm_lid: 1
# port_lid: 2
# port_lmc: 0x00
# link_layer: InfiniBand
# 查看特定设备的端口状态
ibv_devinfo -d mlx5_0 -i 1
# 查看 GID 表(RoCEv2 需要)
show_gids
# 列出所有 RDMA 设备
ibv_devices

Practice 2:运行 rping(RDMA Ping)#

rping 是 RDMA 的 ping 工具,可以验证 RDMA 连接和测量延迟:

# 服务端(在一台机器上运行)
rping -s -a 0.0.0.0 -p 18515 -v
# 客户端(在另一台机器上运行)
rping -c -a <server_ip> -p 18515 -v -C 10
# 输出示例:
# rdma_ping: data 32 bytes 1 trips 10 ms
# rdma_ping: data 64 bytes 1 trips 10 ms
# rdma_ping: data 256 bytes 1 trips 10 ms
# rdma_ping: data 1024 bytes 1 trips 10 ms
# ...
# 使用 RDMA CM 测试(RoCEv2)
# 服务端
rping -s -a <server_ip> -p 18515 -v
# 客户端
rping -c -a <server_ip> -p 18515 -v

Practice 3:编写简单的 RDMA Write 程序#

以下是一个最小化的 RDMA Write 示例,展示核心编程模式:

#include <infiniband/verbs.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF_SIZE 4096
int main()
{
// 1. 获取设备列表
int num_devices;
struct ibv_device **dev_list = ibv_get_device_list(&num_devices);
if (!dev_list || num_devices == 0) {
fprintf(stderr, "未找到 RDMA 设备\n");
return 1;
}
// 2. 打开设备
struct ibv_context *ctx = ibv_open_device(dev_list[0]);
if (!ctx) {
fprintf(stderr, "打开设备失败\n");
return 1;
}
// 3. 创建 PD
struct ibv_pd *pd = ibv_alloc_pd(ctx);
// 4. 注册 MR
char *buf = malloc(BUF_SIZE);
struct ibv_mr *mr = ibv_reg_mr(pd, buf, BUF_SIZE,
IBV_ACCESS_LOCAL_WRITE |
IBV_ACCESS_REMOTE_WRITE |
IBV_ACCESS_REMOTE_READ);
printf("MR 注册成功: lkey=0x%x, rkey=0x%x\n", mr->lkey, mr->rkey);
// 5. 创建 CQ
struct ibv_cq *cq = ibv_create_cq(ctx, 16, NULL, NULL, 0);
// 6. 创建 QP
struct ibv_qp_init_attr qp_attr = {
.send_cq = cq,
.recv_cq = cq,
.qp_type = IBV_QPT_RC,
.cap = {
.max_send_wr = 16,
.max_recv_wr = 16,
.max_send_sge = 1,
.max_recv_sge = 1,
},
};
struct ibv_qp *qp = ibv_create_qp(pd, &qp_attr);
printf("QP 创建成功: qpn=0x%x\n", qp->qp_num);
// 7. 修改 QP 状态: RESET → INIT
struct ibv_qp_attr attr;
memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_INIT;
attr.port_num = 1;
attr.pkey_index = 0;
attr.qp_access_flags = IBV_ACCESS_REMOTE_WRITE |
IBV_ACCESS_REMOTE_READ;
ibv_modify_qp(qp, &attr,
IBV_QP_STATE | IBV_QP_PKEY_INDEX |
IBV_QP_PORT | IBV_QP_ACCESS_FLAGS);
// 在实际程序中,此处需要与对端交换 QP 信息
// 然后执行 INIT → RTR → RTS 状态转换
// ...
printf("RDMA 环境初始化完成\n");
// 清理
ibv_destroy_qp(qp);
ibv_destroy_cq(cq);
ibv_dereg_mr(mr);
ibv_dealloc_pd(pd);
ibv_close_device(ctx);
ibv_free_device_list(dev_list);
free(buf);
return 0;
}

编译:

# 编译 RDMA 程序
gcc -o rdma_write_demo rdma_write_demo.c -libverbs -lrdmacm
# 运行
./rdma_write_demo

Practice 4:使用 ib_write_bw 基准测试#

ib_write_bw 是 perftest 工具包中的 RDMA Write 带宽基准测试工具:

# 服务端
ib_write_bw -d mlx5_0
# 客户端
ib_write_bw -d mlx5_0 <server_ip>
# 输出示例:
# ---------------------------------------------------------------------------------------
# Dual-port : OFF Device : mlx5_0
# Number of qps : 1 Transport type : IB
# Connection type : RC Using SRQ : OFF
# TX depth : 128
# Mtu : 1024[B]
# Link type : IB
# Max msg size : 65536[B]
# ---------------------------------------------------------------------------------------
# 65536 5000 24123. 24118. 0.386
# 测试不同消息大小
ib_write_bw -d mlx5_0 -s 1 <server_ip> # 1 字节
ib_write_bw -d mlx5_0 -s 4096 <server_ip> # 4KB
ib_write_bw -d mlx5_0 -s 65536 <server_ip> # 64KB
# 测试延迟
ib_write_lat -d mlx5_0 <server_ip>
# 测试 RDMA Read 带宽
ib_read_bw -d mlx5_0 <server_ip>
# 测试 Send/Recv 带宽
ib_send_bw -d mlx5_0 <server_ip>
# 使用双向测试
ib_write_bw -d mlx5_0 -b <server_ip>
# 使用多 QP 测试
ib_write_bw -d mlx5_0 -n 4 <server_ip> # 4 个 QP
# 使用 Huge Pages
ib_write_bw -d mlx5_0 --use_hugepages <server_ip>

其他常用 perftest 工具:

工具测试内容
ib_write_bwRDMA Write 带宽
ib_write_latRDMA Write 延迟
ib_read_bwRDMA Read 带宽
ib_read_latRDMA Read 延迟
ib_send_bwSend/Recv 带宽
ib_send_latSend/Recv 延迟
ib_atomic_latAtomic 操作延迟

小结#

RDMA 通过零拷贝、内核旁路和 CPU 卸载三大核心技术,将网络 I/O 的性能推向了物理极限。本章深入了 RDMA 的完整技术栈:

  1. RDMA 的本质:绕过内核,RNIC 直接通过 DMA 访问应用内存,远端 CPU 可以完全不参与数据搬运——这是单边操作的革命性意义

  2. 三种传输协议:InfiniBand(原生 RDMA,极致性能)、RoCEv2(以太网上的 RDMA,数据中心主流)、iWARP(TCP 上的 RDMA,广域兼容)——向上统一提供 Verbs API

  3. Verbs API 编程模型:PD(内存隔离)、MR(注册内存供远程访问)、QP(通信端点)、CQ(完成通知)、SRQ(共享接收队列)——五大核心抽象构成了 RDMA 编程的基础

  4. QP 状态机:RESET → INIT → RTR → RTS 的严格状态转换,每一步都需要设置正确的属性——这是 RDMA 编程中最容易出错的地方

  5. RDMA CM:封装了 QP 创建、状态转换和信息交换,提供类似 socket API 的连接管理接口

  6. 数据传输操作:双边操作(Send/Recv)和单边操作(RDMA Write/Read/Atomic)——单边操作是 RDMA 的核心价值,远端 CPU 完全不参与

  7. 性能调优:Huge Pages、NUMA 亲和性、Inline Data、Doorbell Batching——每一项都可以显著提升性能

RDMA 技术正在从 HPC 走向更广泛的数据中心场景:分布式存储(NVMe-oF)、分布式数据库(PolarDB、TiDB)、机器学习(NCCL、GDR)、云网络(SR-IOV + RDMA)——理解 RDMA 的底层原理,是构建下一代高性能分布式系统的基础。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

RDMA 与远程直接内存访问
https://blog.souloss.com/posts/high-perf-networking/high-perf-networking-rdma-remote-direct-memory-access/
作者
Souloss
发布于
2025-06-15
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
SmartNIC 与 DPU
高性能网络 深入 SmartNIC 与 DPU——硬件卸载概念与收益、SmartNIC 架构(固定功能 vs 可编程)、DPU 产品矩阵(NVIDIA BlueField/AMD Pensando/Intel IPU)、OVS 硬件卸载(tc/rte_flow/ASAP²)、P4 编程入门——掌握硬件加速网络的完整技术栈。
2
DPDK 多核与并发模型
高性能网络 深入 DPDK 多核与并发模型——lcore 模型与 CPU 亲和性、Run-to-Completion 模型、Pipeline 模型与 rte_ring 跨核通信、原子操作与内存屏障、RCU 机制(rte_rcu_qsbr)、Eventdev 事件驱动框架——掌握多核数据平面编程的完整技术栈。
3
SPDK 与存储旁通
高性能网络 深入 SPDK 与存储旁通——SPDK 架构与 DPDK EAL 共享、用户态 NVMe 驱动(MMIO/SQ/CQ)、vhost-user 协议与共享内存、bdev 抽象层与模块、blobstore 持久化对象存储、iSCSI/NVMe-oF Target——掌握用户态存储的完整技术栈。
4
io_uring 与异步 IO 革命
高性能网络 深入 io_uring 架构——SQ/CQ 环形缓冲区布局与无锁交互、提交与完成流程、固定缓冲区与文件注册、网络 I/O 操作、multishot accept、SQPOLL 模式、与 epoll/AIO 的性能对比——掌握 Linux 异步 I/O 的终极方案。
5
OVS-DPDK 与虚拟交换
高性能网络 深入 OVS-DPDK 与虚拟交换——OVS-DPDK 架构与内核 OVS 对比、dpdkvhostuser 端口类型、vhost-user 协议与共享 virtio 环、virtio 前后端、VM-to-VM 交换路径、流分类(EMC/DPCLS/megaflows)、性能调优——掌握云环境虚拟网络加速的完整技术栈。