一、前言
RDMA(Remote Direct Memory Access,远程直接内存访问)是一种高性能网络传输技术,它允许应用程序直接访问远程主机的内存,而无需经过操作系统内核,从而大幅降低网络传输延迟并提高吞吐量。
本文解决的问题:
- 在没有真实 RDMA 硬件(如 Mellanox 网卡)的情况下体验 RDMA
- 使用 Soft-RoCE(RXE)软件模拟 RDMA 设备
- 完整的源码下载、编译和测试流程
- 提供完整可运行的 RDMA 客户端 / 服务端代码示例
适用场景:
- 学习和开发 RDMA 应用程序
- 在虚拟机或普通服务器上测试 RDMA 功能
- 快速验证 RDMA 程序的逻辑正确性
二、环境准备
2.1 系统要求
-
操作系统:Ubuntu 22.04 LTS(推荐)
-
内核版本:5.15+(Soft-RoCE 需要)
-
内存:至少 4GB
-
网络:普通以太网卡即可
检查系统版本
lsb_release -a
检查内核版本
uname -r
2.2 安装基础依赖
# 更新系统包
sudo apt-get update
# 安装基础开发工具
sudo apt-get install -y build-essential git cmake gcc g++ make autoconf automake libtool
# 安装RDMA核心库和工具
sudo apt-get install -y rdma-core libibverbs-dev librdmacm-dev ibverbs-utils rdmacm-utils
# 安装Soft-RoCE所需的包
sudo apt-get install -y ibacm infiniband-diags perftest
# 安装其他依赖
sudo apt-get install -y libnl-3-dev libnl-route-3-dev pandoc doxygen
# 安装Python依赖(用于构建文档)
sudo apt-get install -y python3-dev python3-pip
2.3 验证基础安装
# 检查安装的工具版本
ibv_devinfo --help
# 查看已安装的包
dpkg -l | grep rdma
dpkg -l | grep ibverbs
三、配置 Soft-RoCE(RXE)
3.1 什么是 Soft-RoCE
Soft-RoCE(也称为 RXE)是一个软件实现的 RoCE(RDMA over Converged Ethernet)设备。它允许在没有 RDMA 硬件的普通服务器上运行 RDMA 应用程序。
优势:
- 无需专用 RDMA 硬件
- 支持标准以太网
- 完整的 RDMA Verbs API 支持
- 适合开发、测试和学习
限制:
- 性能不如真实 RDMA 硬件
- CPU 开销较高
- 延迟较高
3.2 加载 RDMA 内核模块
# 加载RDMA核心模块
sudo modprobe rdma
# 加载Soft-RoCE模块
sudo modprobe rdma_rxe
# 验证模块已加载
lsmod | grep rdma
输出示例:
rdma_rxe 163840 0
ib_uverbs 163840 0
rdma_cm 163840 0
rdma_core 163840 1 rdma_cm
ib_core 450560 6 rdma_cm,rdma_rxe,ib_uverbs,rdma_core
3.3 创建 RXE 设备
方法一:使用 rxe_cfg 工具(推荐)
# 查看可用网络接口
ip link show
# 使用rxe_cfg创建RXE设备
# 假设你的网卡是eth0或ens33
sudo rxe_cfg start
# 添加RXE设备(将eth0替换为你的网卡名)
sudo rxe_cfg add eth0
# 或者
sudo rxe_cfg add ens33
# 检查RXE设备状态
sudo rxe_cfg status
方法二:手动配置
如果系统没有 rxe_cfg,可以手动创建:
# 查看网卡名称
ip link show
# 创建RXE设备(将eth0替换为你的网卡名)
echo "eth0" | sudo tee /sys/module/rdma_rxe/parameters/add
# 查看创建的设备
ls -l /sys/class/infiniband/
3.4 验证 RXE 设备
# 查看RDMA设备列表
ibv_devices
输出示例:
device node GUID
------ ----------------
rxe0 505400fffe123456
# 查看设备详细信息
ibv_devinfo
输出示例:
hca_id: rxe0
transport: InfiniBand (0)
fw_ver: 0.0.0
node_guid: 5054:00ff:fe12:3456
sys_image_guid: 5054:00ff:fe12:3456
vendor_id: 0x0000
vendor_part_id: 0
hw_ver: 0x0
phys_port_cnt: 1
port: 1
state: PORT_ACTIVE (4)
max_mtu: 4096 (5)
active_mtu: 1024 (3)
sm_lid: 0
port_lid: 0
port_lmc: 0x00
link_layer: Ethernet
3.5 设置开机自动加载(可选)
# 编辑modules配置文件
sudo tee /etc/modules-load.d/rdma.conf << EOF
rdma
rdma_rxe
EOF
# 创建启动脚本
sudo tee /etc/network/if-up.d/rxe-setup << 'EOF'
#!/bin/bash
# 等待网络接口就绪
sleep 5
# 加载模块
modprobe rdma_rxe
# 查找第一个非lo的接口
IFACE=$(ip link show | grep -v lo | grep -v DOWN | awk '/^[0-9]/{print $2}' | sed 's/://' | head -n1)
if [ -n "$IFACE" ]; then
rxe_cfg add $IFACE 2>/dev/null || true
fi
EOF
sudo chmod +x /etc/network/if-up.d/rxe-setup
四、完整的 RDMA 客户端 / 服务端程序
本节提供完整的、可运行的 RDMA 客户端 / 服务端代码,使用 Socket + Verbs API 方式实现,在 Soft-RoCE 环境下稳定可靠。
4.1 为什么选择 Socket + Verbs API?
RDMA CM 的问题:
- RDMA CM(Connection Manager)在 Soft-RoCE 环境下存在路由解析问题
rdma_resolve_route()经常返回 "No such device" 错误- 兼容性较差
Socket + Verbs API 的优势:
- 使用传统 TCP Socket 交换连接信息(QPN、GID、rkey 等)
- 直接使用 Verbs API 管理 QP 状态
- 在 Soft-RoCE 环境下稳定可靠
- 更清晰地展示 RDMA 工作原理
传统 TCP/IP 需要 CPU 把数据从应用内存拷贝到内核缓冲区、再封装成以太网帧发送,数据经过多次拷贝和内核处理;RDMA/RoCE 网卡直接通过 DMA 访问应用内存、硬件自动封装以太网帧并传输,数据全程零拷贝、完全绕过内核,实现微秒级延迟和近 100Gbps 带宽;Soft-RoCE 则是让 CPU 用软件模拟 RDMA 网卡的功能,虽然同样提供零拷贝和内核旁路的数据通路,但 "封装以太网帧" 这一步由 CPU 完成、而非硬件卸载,所以性能约为硬件 RDMA 的 10-20%,但好处是无需专用网卡、普通以太网就能跑,适合开发测试学习。三者区别本质:封装以太网帧的是 CPU(传统 TCP + 多次拷贝)、RDMA 硬件(零拷贝 + 最高性能)、还是软件模拟(零拷贝 + 中等性能)。
4.2 创建项目目录
# 创建项目目录
mkdir -p ~/RDMA
cd ~/RDMA
4.3 服务端代码 (rdma_server.c)
cpp
/*
* RDMA Server - 使用 Socket + Verbs API(Soft-RoCE 完全兼容版)
*
* 编译: gcc rdma_server.c -o server -libverbs
* 运行: ./server [device_name]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <infiniband/verbs.h>
#define PORT 8888
#define BUF_SIZE 1024
/* QP 状态转换函数 */
int qp_to_init(struct ibv_qp *qp, int port_num) {
struct ibv_qp_attr attr;
memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_INIT;
attr.pkey_index = 0;
attr.port_num = port_num;
attr.qp_access_flags = IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ;
int flags = IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS;
return ibv_modify_qp(qp, &attr, flags);
}
int qp_to_rtr(struct ibv_qp *qp, uint32_t remote_qpn, union ibv_gid *remote_gid, int port_num) {
struct ibv_qp_attr attr;
memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_RTR;
attr.path_mtu = IBV_MTU_1024;
attr.dest_qp_num = remote_qpn;
attr.rq_psn = 0;
attr.max_dest_rd_atomic = 1;
attr.min_rnr_timer = 12;
attr.ah_attr.dlid = 0;
attr.ah_attr.sl = 0;
attr.ah_attr.port_num = port_num;
attr.ah_attr.is_global = 1;
attr.ah_attr.grh.dgid = *remote_gid;
attr.ah_attr.grh.hop_limit = 1;
attr.ah_attr.grh.sgid_index = 0;
int flags = 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;
return ibv_modify_qp(qp, &attr, flags);
}
int qp_to_rts(struct ibv_qp *qp) {
struct ibv_qp_attr attr;
memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_RTS;
attr.sq_psn = 0;
attr.max_rd_atomic = 1;
attr.retry_cnt = 7;
attr.rnr_retry = 7;
attr.timeout = 14; /* Soft-RoCE 需要设置 timeout */
int flags = IBV_QP_STATE | IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC |
IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY | IBV_QP_TIMEOUT;
return ibv_modify_qp(qp, &attr, flags);
}
int main(int argc, char *argv[]) {
const char *dev_name = argc > 1 ? argv[1] : "rxe0";
struct ibv_device **dev_list = NULL;
struct ibv_context *ctx = NULL;
struct ibv_pd *pd = NULL;
struct ibv_cq *cq = NULL;
struct ibv_qp *qp = NULL;
struct ibv_mr *mr = NULL;
char *buf = NULL;
int listen_sock = -1, client_sock = -1;
printf("[Server] 启动 (设备: %s)\n", dev_name);
/* 获取设备列表 */
int num_devices;
dev_list = ibv_get_device_list(&num_devices);
if (!dev_list || num_devices == 0) {
fprintf(stderr, "[Server] 没有找到RDMA设备\n");
return 1;
}
/* 查找指定设备 */
struct ibv_device *target_dev = NULL;
for (int i = 0; i < num_devices; i++) {
if (strcmp(ibv_get_device_name(dev_list[i]), dev_name) == 0) {
target_dev = dev_list[i];
break;
}
}
if (!target_dev) {
fprintf(stderr, "[Server] 未找到设备 %s\n", dev_name);
ibv_free_device_list(dev_list);
return 1;
}
printf("[Server] 打开设备: %s\n", ibv_get_device_name(target_dev));
/* 打开设备 */
ctx = ibv_open_device(target_dev);
if (!ctx) {
perror("ibv_open_device");
goto cleanup;
}
/* 分配 PD */
pd = ibv_alloc_pd(ctx);
if (!pd) {
perror("ibv_alloc_pd");
goto cleanup;
}
/* 创建 CQ */
cq = ibv_create_cq(ctx, 16, NULL, NULL, 0);
if (!cq) {
perror("ibv_create_cq");
goto cleanup;
}
/* 创建 QP */
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.cap.max_send_wr = 8;
qp_attr.cap.max_recv_wr = 8;
qp_attr.cap.max_send_sge = 1;
qp_attr.cap.max_recv_sge = 1;
qp_attr.qp_type = IBV_QPT_RC;
qp = ibv_create_qp(pd, &qp_attr);
if (!qp) {
perror("ibv_create_qp");
goto cleanup;
}
printf("[Server] QP 创建成功, QPN: 0x%06x\n", qp->qp_num);
/* QP 转换到 INIT 状态 */
if (qp_to_init(qp, 1) != 0) {
fprintf(stderr, "[Server] QP 状态转换到 INIT 失败\n");
goto cleanup;
}
printf("[Server] QP 状态: INIT\n");
/* 分配并注册内存 */
buf = calloc(1, BUF_SIZE);
if (!buf) {
perror("calloc");
goto cleanup;
}
mr = ibv_reg_mr(pd, buf, BUF_SIZE,
IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ);
if (!mr) {
perror("ibv_reg_mr");
goto cleanup;
}
printf("[Server] MR 注册成功, rkey: 0x%08x\n", mr->rkey);
/* 获取 GID */
union ibv_gid gid;
ibv_query_gid(ctx, 1, 0, &gid);
/* 创建 TCP socket 监听 */
listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock < 0) {
perror("socket");
goto cleanup;
}
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(PORT);
if (bind(listen_sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind");
goto cleanup;
}
if (listen(listen_sock, 1) < 0) {
perror("listen");
goto cleanup;
}
printf("[Server] 监听端口 %d...\n", PORT);
/* 等待客户端连接 */
client_sock = accept(listen_sock, NULL, NULL);
if (client_sock < 0) {
perror("accept");
goto cleanup;
}
printf("[Server] 客户端已连接\n");
/* 交换信息 */
struct {
uint32_t qpn;
uint32_t rkey;
uint64_t vaddr;
uint8_t gid[16];
} local_info, remote_info;
local_info.qpn = qp->qp_num;
local_info.rkey = mr->rkey;
local_info.vaddr = (uint64_t)(uintptr_t)buf;
memcpy(local_info.gid, gid.raw, 16);
/* 先发送,再接收 */
send(client_sock, &local_info, sizeof(local_info), 0);
recv(client_sock, &remote_info, sizeof(remote_info), 0);
printf("[Server] 收到客户端信息: QPN=0x%06x, rkey=0x%08x\n",
remote_info.qpn, remote_info.rkey);
/* QP 转换到 RTR 状态 */
if (qp_to_rtr(qp, remote_info.qpn, (union ibv_gid *)remote_info.gid, 1) != 0) {
fprintf(stderr, "[Server] QP 状态转换到 RTR 失败\n");
goto cleanup;
}
printf("[Server] QP 状态: RTR\n");
/* QP 转换到 RTS 状态 */
if (qp_to_rts(qp) != 0) {
fprintf(stderr, "[Server] QP 状态转换到 RTS 失败\n");
goto cleanup;
}
printf("[Server] QP 状态: RTS\n");
/* 发布接收请求 */
struct ibv_sge sge;
struct ibv_recv_wr recv_wr, *bad_wr = NULL;
sge.addr = (uint64_t)(uintptr_t)buf;
sge.length = BUF_SIZE;
sge.lkey = mr->lkey;
recv_wr.wr_id = 0;
recv_wr.next = NULL;
recv_wr.sg_list = &sge;
recv_wr.num_sge = 1;
if (ibv_post_recv(qp, &recv_wr, &bad_wr) != 0) {
perror("ibv_post_recv");
goto cleanup;
}
printf("[Server] 等待接收消息...\n");
/* 轮询 CQ */
struct ibv_wc wc;
while (ibv_poll_cq(cq, 1, &wc) == 0) {
usleep(1000);
}
if (wc.status == IBV_WC_SUCCESS && wc.opcode == IBV_WC_RECV) {
printf("\n========================================\n");
printf("[Server] 收到消息: %s\n", buf);
printf("========================================\n");
} else {
printf("[Server] 接收失败: %s\n", ibv_wc_status_str(wc.status));
}
sleep(1);
cleanup:
printf("[Server] 清理资源\n");
if (client_sock >= 0) close(client_sock);
if (listen_sock >= 0) close(listen_sock);
if (qp) ibv_destroy_qp(qp);
if (mr) ibv_dereg_mr(mr);
if (buf) free(buf);
if (cq) ibv_destroy_cq(cq);
if (pd) ibv_dealloc_pd(pd);
if (ctx) ibv_close_device(ctx);
if (dev_list) ibv_free_device_list(dev_list);
return 0;
}
4.4 客户端代码 (rdma_client.c)
cpp
/*
* RDMA Client - 使用 Socket + Verbs API(Soft-RoCE 完全兼容版)
*
* 编译: gcc rdma_client.c -o client -libverbs
* 运行: ./client [server_ip] [device_name]
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <infiniband/verbs.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8888
#define BUF_SIZE 1024
/* QP 状态转换函数 */
int qp_to_init(struct ibv_qp *qp, int port_num) {
struct ibv_qp_attr attr;
memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_INIT;
attr.pkey_index = 0;
attr.port_num = port_num;
attr.qp_access_flags = IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ;
int flags = IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS;
return ibv_modify_qp(qp, &attr, flags);
}
int qp_to_rtr(struct ibv_qp *qp, uint32_t remote_qpn, union ibv_gid *remote_gid, int port_num) {
struct ibv_qp_attr attr;
memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_RTR;
attr.path_mtu = IBV_MTU_1024;
attr.dest_qp_num = remote_qpn;
attr.rq_psn = 0;
attr.max_dest_rd_atomic = 1;
attr.min_rnr_timer = 12;
attr.ah_attr.dlid = 0;
attr.ah_attr.sl = 0;
attr.ah_attr.port_num = port_num;
attr.ah_attr.is_global = 1;
attr.ah_attr.grh.dgid = *remote_gid;
attr.ah_attr.grh.hop_limit = 1;
attr.ah_attr.grh.sgid_index = 0;
int flags = 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;
return ibv_modify_qp(qp, &attr, flags);
}
int qp_to_rts(struct ibv_qp *qp) {
struct ibv_qp_attr attr;
memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_RTS;
attr.sq_psn = 0;
attr.max_rd_atomic = 1;
attr.retry_cnt = 7;
attr.rnr_retry = 7;
attr.timeout = 14; /* Soft-RoCE 需要设置 timeout */
int flags = IBV_QP_STATE | IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC |
IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY | IBV_QP_TIMEOUT;
return ibv_modify_qp(qp, &attr, flags);
}
int main(int argc, char *argv[]) {
const char *server_ip = argc > 1 ? argv[1] : SERVER_IP;
const char *dev_name = argc > 2 ? argv[2] : "rxe0";
struct ibv_device **dev_list = NULL;
struct ibv_context *ctx = NULL;
struct ibv_pd *pd = NULL;
struct ibv_cq *cq = NULL;
struct ibv_qp *qp = NULL;
struct ibv_mr *mr = NULL;
char *buf = NULL;
int sock = -1;
printf("[Client] 连接服务器: %s:%d (设备: %s)\n", server_ip, PORT, dev_name);
/* 获取设备列表 */
int num_devices;
dev_list = ibv_get_device_list(&num_devices);
if (!dev_list || num_devices == 0) {
fprintf(stderr, "[Client] 没有找到RDMA设备\n");
return 1;
}
/* 查找指定设备 */
struct ibv_device *target_dev = NULL;
for (int i = 0; i < num_devices; i++) {
if (strcmp(ibv_get_device_name(dev_list[i]), dev_name) == 0) {
target_dev = dev_list[i];
break;
}
}
if (!target_dev) {
fprintf(stderr, "[Client] 未找到设备 %s\n", dev_name);
ibv_free_device_list(dev_list);
return 1;
}
printf("[Client] 打开设备: %s\n", ibv_get_device_name(target_dev));
/* 打开设备 */
ctx = ibv_open_device(target_dev);
if (!ctx) {
perror("ibv_open_device");
goto cleanup;
}
/* 分配 PD */
pd = ibv_alloc_pd(ctx);
if (!pd) {
perror("ibv_alloc_pd");
goto cleanup;
}
/* 创建 CQ */
cq = ibv_create_cq(ctx, 16, NULL, NULL, 0);
if (!cq) {
perror("ibv_create_cq");
goto cleanup;
}
/* 创建 QP */
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.cap.max_send_wr = 8;
qp_attr.cap.max_recv_wr = 8;
qp_attr.cap.max_send_sge = 1;
qp_attr.cap.max_recv_sge = 1;
qp_attr.qp_type = IBV_QPT_RC;
qp = ibv_create_qp(pd, &qp_attr);
if (!qp) {
perror("ibv_create_qp");
goto cleanup;
}
printf("[Client] QP 创建成功, QPN: 0x%06x\n", qp->qp_num);
/* QP 转换到 INIT 状态 */
if (qp_to_init(qp, 1) != 0) {
fprintf(stderr, "[Client] QP 状态转换到 INIT 失败\n");
goto cleanup;
}
printf("[Client] QP 状态: INIT\n");
/* 分配并注册内存 */
buf = malloc(BUF_SIZE);
if (!buf) {
perror("malloc");
goto cleanup;
}
memset(buf, 0, BUF_SIZE);
strcpy(buf, "Hello RDMA! Soft-RoCE 连接成功!");
mr = ibv_reg_mr(pd, buf, BUF_SIZE,
IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ);
if (!mr) {
perror("ibv_reg_mr");
goto cleanup;
}
printf("[Client] MR 注册成功, rkey: 0x%08x\n", mr->rkey);
/* 获取 GID */
union ibv_gid gid;
ibv_query_gid(ctx, 1, 0, &gid);
/* 连接服务器 */
sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
goto cleanup;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
inet_pton(AF_INET, server_ip, &addr.sin_addr);
if (connect(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("connect");
goto cleanup;
}
printf("[Client] 已连接到服务器\n");
/* 交换信息 */
struct {
uint32_t qpn;
uint32_t rkey;
uint64_t vaddr;
uint8_t gid[16];
} local_info, remote_info;
local_info.qpn = qp->qp_num;
local_info.rkey = mr->rkey;
local_info.vaddr = (uint64_t)(uintptr_t)buf;
memcpy(local_info.gid, gid.raw, 16);
/* 先接收,再发送 */
recv(sock, &remote_info, sizeof(remote_info), 0);
send(sock, &local_info, sizeof(local_info), 0);
printf("[Client] 收到服务端信息: QPN=0x%06x, rkey=0x%08x\n",
remote_info.qpn, remote_info.rkey);
/* QP 转换到 RTR 状态 */
if (qp_to_rtr(qp, remote_info.qpn, (union ibv_gid *)remote_info.gid, 1) != 0) {
fprintf(stderr, "[Client] QP 状态转换到 RTR 失败\n");
goto cleanup;
}
printf("[Client] QP 状态: RTR\n");
/* QP 转换到 RTS 状态 */
if (qp_to_rts(qp) != 0) {
fprintf(stderr, "[Client] QP 状态转换到 RTS 失败\n");
goto cleanup;
}
printf("[Client] QP 状态: RTS\n");
printf("[Client] 连接建立成功!\n");
/* 发送消息 */
struct ibv_sge sge;
struct ibv_send_wr send_wr, *bad_wr = NULL;
sge.addr = (uint64_t)(uintptr_t)buf;
sge.length = strlen(buf) + 1;
sge.lkey = mr->lkey;
memset(&send_wr, 0, sizeof(send_wr));
send_wr.wr_id = 1;
send_wr.next = NULL;
send_wr.sg_list = &sge;
send_wr.num_sge = 1;
send_wr.opcode = IBV_WR_SEND;
send_wr.send_flags = IBV_SEND_SIGNALED;
printf("[Client] 发送消息: %s\n", buf);
if (ibv_post_send(qp, &send_wr, &bad_wr) != 0) {
perror("ibv_post_send");
goto cleanup;
}
/* 轮询 CQ */
struct ibv_wc wc;
while (ibv_poll_cq(cq, 1, &wc) == 0) {
usleep(1000);
}
if (wc.status == IBV_WC_SUCCESS && wc.opcode == IBV_WC_SEND) {
printf("[Client] 发送成功!\n");
} else {
printf("[Client] 发送失败: %s\n", ibv_wc_status_str(wc.status));
}
sleep(1);
cleanup:
printf("[Client] 清理资源\n");
if (sock >= 0) close(sock);
if (qp) ibv_destroy_qp(qp);
if (mr) ibv_dereg_mr(mr);
if (buf) free(buf);
if (cq) ibv_destroy_cq(cq);
if (pd) ibv_dealloc_pd(pd);
if (ctx) ibv_close_device(ctx);
if (dev_list) ibv_free_device_list(dev_list);
return 0;
}
4.5 Makefile
CC = gcc
CFLAGS = -g -Wall -O2
LIBS = -libverbs
.PHONY: all clean
all: server client
server: rdma_server.c
$(CC) $(CFLAGS) -o $@ $< $(LIBS)
client: rdma_client.c
$(CC) $(CFLAGS) -o $@ $< $(LIBS)
clean:
rm -f server client
4.6 编译程序
# 保存上述代码到对应文件后,编译
cd ~/RDMA
make
# 或者单独编译
gcc -g -Wall -O2 -o server rdma_server.c -libverbs
gcc -g -Wall -O2 -o client rdma_client.c -libverbs
编译成功后,目录结构如下:
RDMA/
├── rdma_server.c # 服务端源码
├── rdma_client.c # 客户端源码
├── Makefile # 编译脚本
├── server # 服务端可执行文件
└── client # 客户端可执行文件
五、运行测试
5.1 单机测试(回环模式)
在同一台机器上打开两个终端:
终端 1 - 启动服务端:
cd ~/RDMA
./server rxe0
终端 2 - 启动客户端:
服务端输出示例:
[Server] 启动 (设备: rxe0)
[Server] 打开设备: rxe0
[Server] QP 创建成功, QPN: 0x000011
[Server] QP 状态: INIT
[Server] MR 注册成功, rkey: 0x000003cc
[Server] 监听端口 8888...
[Server] 客户端已连接
[Server] 收到客户端信息: QPN=0x000012, rkey=0x000004c9
[Server] QP 状态: RTR
[Server] QP 状态: RTS
[Server] 等待接收消息...
========================================
[Server] 收到消息: Hello RDMA! Soft-RoCE 连接成功!
========================================
[Server] 清理资源
客户端输出示例:
[Client] 连接服务器: 127.0.0.1:8888 (设备: rxe0)
[Client] 打开设备: rxe0
[Client] QP 创建成功, QPN: 0x000012
[Client] QP 状态: INIT
[Client] MR 注册成功, rkey: 0x000004c9
[Client] 已连接到服务器
[Client] 收到服务端信息: QPN=0x000011, rkey=0x000003cc
[Client] QP 状态: RTR
[Client] QP 状态: RTS
[Client] 连接建立成功!
[Client] 发送消息: Hello RDMA! Soft-RoCE 连接成功!
[Client] 发送成功!
[Client] 清理资源
5.2 双机测试
如果两台机器都配置了 Soft-RoCE,可以进行跨机器测试:
服务器 A:
# 查看本机IP
ip addr show
# 启动服务端
./server rxe0
服务器 B:
# 连接服务器A(假设IP为192.168.1.100)
./client 192.168.1.100 rxe0
5.3 测试成功标志
测试成功的标志:
- 服务端显示 "QP 状态: RTS"
- 客户端显示 "QP 状态: RTS" 和 "连接建立成功!"
- 服务端收到消息:
Hello RDMA! Soft-RoCE 连接成功! - 客户端显示 "发送成功!"
六、程序执行流程详解
6.1 RDMA 核心概念
队列对(Queue Pair, QP):
- RDMA 通信的基本单元
- 包含发送队列(SQ)和接收队列(RQ)
- 每个 QP 有唯一的 QPN(QP Number)
完成队列(Completion Queue, CQ):
- 用于通知操作完成
- 包含完成队列元素(CQE)
- 支持轮询和事件通知两种模式
内存区域(Memory Region, MR):
- 注册的内存缓冲区
- 具有远程访问权限
- 包含内存密钥(lkey/rkey)
保护域(Protection Domain, PD):
- 资源隔离的容器
- QP 和 MR 必须属于同一 PD
GID(Global Identifier):
- RDMA 网络中的全局标识符
- 用于 RoCE 网络中的路由
6.2 连接建立流程
┌─────────────────────────────────────────────────────────────────┐
│ 连接建立时序图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Server Client │
│ │ │ │
│ │ 1. 打开设备,创建资源 │ │
│ │ 2. QP: RESET → INIT │ │
│ │ 3. 监听TCP端口 │ │
│ │◄──────────────────────────────────│ TCP连接请求 │
│ │ 4. 接受TCP连接 │ │
│ │ │ │
│ │ 5. 发送本地信息(QPN,GID,rkey) │ │
│ │──────────────────────────────────►│ │
│ │ │ │
│ │ 6. 接收对端信息 │ │
│ │◄──────────────────────────────────│ 发送本地信息 │
│ │ │ │
│ │ 7. QP: INIT → RTR │ 7. QP: INIT → RTR │
│ │ (配置对端QPN, GID) │ (配置对端QPN, GID)│
│ │ │ │
│ │ 8. QP: RTR → RTS │ 8. QP: RTR → RTS │
│ │ (设置发送参数) │ (设置发送参数) │
│ │ │ │
│ │ RDMA连接建立完成! │ │
│ │ │ │
│ │ 9. ibv_post_recv() │ │
│ │ │ 10. ibv_post_send() │
│ │◄──────────────────────────────────│ RDMA SEND操作 │
│ │ 11. 轮询CQ,收到数据 │ │
│ │ │ 12. 轮询CQ,发送完成 │
│ │ │ │
└─────────────────────────────────────────────────────────────────┘
6.3 QP 状态转换详解
QP状态转换图:
RESET → INIT → RTR → RTS → SQD → SQE → ERROR
↑ ↓
└───────────────────────────┘
各状态说明:
┌─────────┬────────────────────────────────────────────────────┐
│ 状态 │ 说明 │
├─────────┼────────────────────────────────────────────────────┤
│ RESET │ 初始状态,QP刚创建 │
│ INIT │ 已初始化,可以发布接收请求,设置端口和访问权限 │
│ RTR │ Ready to Receive,配置了对端信息,可以接收数据 │
│ RTS │ Ready to Send,可以发送数据 │
│ SQD │ Send Queue Drained,发送队列已清空 │
│ SQE │ Send Queue Error,发送队列错误 │
│ ERROR │ 错误状态 │
└─────────┴────────────────────────────────────────────────────┘
关键状态转换参数:
1. RESET → INIT:
- port_num: 端口号
- pkey_index: 分区键索引
- qp_access_flags: 访问权限 (IBV_ACCESS_REMOTE_WRITE/READ)
2. INIT → RTR:
- dest_qp_num: 对端QP号
- ah_attr.grh.dgid: 对端GID
- path_mtu: 路径MTU
- rq_psn: 接收PSN
- max_dest_rd_atomic: 最大入站RDMA Read
3. RTR → RTS:
- sq_psn: 发送PSN
- max_rd_atomic: 最大出站RDMA Read
- retry_cnt: 重试次数
- rnr_retry: RNR重试次数
- timeout: 超时参数 (Soft-RoCE必须设置!)
6.4 关键代码解析
1. 信息交换结构体
cpp
struct {
uint32_t qpn; // QP号,用于对端配置RTR状态
uint32_t rkey; // 远程内存密钥,用于RDMA Read/Write
uint64_t vaddr; // 虚拟地址,用于RDMA操作的远程地址
uint8_t gid[16]; // 全局标识符,用于RoCE路由
} local_info, remote_info;
2. QP 状态转换到 RTR 的关键配置
cpp
attr.dest_qp_num = remote_qpn; // 对端QP号
attr.ah_attr.is_global = 1; // 使用全局路由(RoCE必须)
attr.ah_attr.grh.dgid = *remote_gid; // 对端GID
attr.ah_attr.grh.sgid_index = 0; // 本地GID索引
3. Soft-RoCE 的 timeout 设
cpp
// Soft-RoCE必须设置timeout,否则RTS转换会失败
attr.timeout = 14; // 约8.4秒超时 (4.096μs × 2^14)
6.5 RDMA Verbs API 详解
控制通路 API:
| API | 功能 | 说明 |
|---|---|---|
ibv_get_device_list() |
获取设备列表 | 返回可用 RDMA 设备 |
ibv_open_device() |
打开设备 | 获取设备上下文 |
ibv_alloc_pd() |
分配 PD | 创建保护域 |
ibv_create_cq() |
创建 CQ | 创建完成队列 |
ibv_create_qp() |
创建 QP | 创建队列对 |
ibv_modify_qp() |
修改 QP 状态 | 状态转换 |
ibv_reg_mr() |
注册 MR | 注册内存区域 |
ibv_query_gid() |
查询 GID | 获取全局标识符 |
数据通路 API:
| API | 功能 | 说明 |
|---|---|---|
ibv_post_send() |
发布发送请求 | 发起 RDMA 操作 |
ibv_post_recv() |
发布接收请求 | 准备接收数据 |
ibv_poll_cq() |
轮询 CQ | 检查完成状态 |
ibv_wc_status_str() |
状态字符串 | 错误信息转换 |
七、Soft-RoCE 性能优化
7.1 调整 MTU
# 查看当前MTU
ip link show eth0
# 设置更大的MTU(需要网卡支持)
sudo ip link set eth0 mtu 9000
# 验证MTU设置
ip link show eth0
7.2 CPU 亲和性设置
# 查看CPU核心
lscpu
# 设置进程CPU亲和性
taskset -c 0,1 ./server rxe0
7.3 内存锁定限制
# 查看当前限制
ulimit -l
# 设置更大的内存锁定限制
ulimit -l unlimited
# 永久设置(添加到~/.bashrc)
echo "ulimit -l unlimited" >> ~/.bashrc
7.4 内核参数调优
# 编辑sysctl配置
sudo tee -a /etc/sysctl.conf << EOF
# RDMA优化参数
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.core.rmem_default = 33554432
net.core.wmem_default = 33554432
net.core.netdev_max_backlog = 5000
EOF
# 应用配置
sudo sysctl -p
八、常见问题排查
8.1 问题:ibv_devices 显示 "Function not implemented"
原因: 系统中没有 RDMA 设备(硬件或 Soft-RoCE)
解决方案:
# 1. 加载RDMA模块
sudo modprobe rdma_rxe
# 2. 创建RXE设备
sudo rxe_cfg add eth0
# 3. 验证
ibv_devices
8.2 问题:创建 RXE 设备失败
原因: 内核不支持或模块未加载
解决方案:
# 检查内核版本(需要5.0+)
uname -r
# 检查模块是否存在
modinfo rdma_rxe
# 如果模块不存在,安装额外的内核模块
sudo apt-get install linux-modules-extra-$(uname -r)
8.3 问题:QP 状态转换失败
原因: Soft-RoCE 需要设置 timeout 参数
解决方案:
确保代码中包含 timeout 设置:
attr.timeout = 14; // Soft-RoCE必须设置!
8.4 问题:连接超时
原因: 网络配置或防火墙问题
解决方案:
# 1. 检查网络连通性
ping 192.168.1.100
# 2. 检查防火墙
sudo ufw status
sudo ufw disable # 临时关闭防火墙测试
# 3. 检查GID配置
ibv_devinfo -v
8.5 问题:权限不足
原因: 用户没有访问 RDMA 设备的权限
解决方案:
# 方法1:添加用户到rdma组
sudo usermod -a -G rdma $USER
# 需要重新登录生效
# 方法2:修改设备权限
sudo chmod 666 /dev/infiniband/uverbs*
# 方法3:使用sudo运行
sudo ./server rxe0
8.6 问题:编译错误
原因: 缺少依赖
解决方案:
# 安装完整的开发依赖
sudo apt-get install -y \
build-essential \
libibverbs-dev \
librdmacm-dev
# 重新编译
make clean && make
九、RDMA 架构原理详解
9.1 控制通路与数据通路
RDMA 架构将操作分为两个独立的通路:
控制通路(Control Path):
┌─────────────────────────────────────┐
│ 应用程序 │
│ ↓ ibv_* API │
│ 用户态驱动 (libibverbs) │
│ ↓ ioctl │
│ 内核态驱动 (ib_core) │
│ ↓ │
│ 硬件固件 │
└─────────────────────────────────────┘
操作:设备打开、PD分配、MR注册、CQ/QP创建、QP状态修改
特点:低频操作,需要内核参与
目的:资源初始化和配置
数据通路(Data Path):
┌─────────────────────────────────────┐
│ 应用程序 │
│ ↓ ibv_post_send/poll_cq │
│ 用户态驱动 (libibverbs) │
│ ↓ 直接访问硬件寄存器 │
│ RDMA网卡硬件 │
└─────────────────────────────────────┘
操作:数据发送、接收、完成通知
特点:高频操作,旁路内核
目的:零拷贝数据传输
9.2 分层架构图
┌────────────────────────────────────────────────────────┐
│ 应用程序层 │
│ RDMA应用程序 (使用Verbs API) │
├────────────────────────────────────────────────────────┤
│ 用户态层 │
│ ┌──────────────────┐ ┌──────────────────────────┐ │
│ │ 控制通路 │ │ 数据通路 │ │
│ │ libibverbs │ │ libibverbs │ │
│ │ librdmacm │ │ (直接访问硬件) │ │
│ └──────────────────┘ └──────────────────────────┘ │
├────────────────────────────────────────────────────────┤
│ 内核态层 │
│ ┌──────────────────────────────────────────────────┐ │
│ │ RDMA核心子系统 (ib_core) │ │
│ │ - 设备管理 - PD管理 - MR管理 │ │
│ │ - CQ管理 - QP管理 - 安全控制 │ │
│ └──────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Soft-RoCE驱动 (rdma_rxe) │ │
│ │ - 软件实现的RDMA设备 │ │
│ │ - UDP封装 (RoCEv2) │ │
│ └──────────────────────────────────────────────────┘ │
├────────────────────────────────────────────────────────┤
│ 硬件层 │
│ 普通以太网卡 (Soft-RoCE) │
│ 或 RDMA网卡 (真实硬件) │
└────────────────────────────────────────────────────────┘
9.3 Soft-RoCE 工作原理
Soft-RoCE数据包封装 (RoCEv2):
┌─────────────────────────────────────────┐
│ IP Header │
│ Source/Dest IP Address │
├─────────────────────────────────────────┤
│ UDP Header │
│ Source Port: 4791 (RoCE) │
│ Dest Port: 4791 │
├─────────────────────────────────────────┤
│ IB BTH (Base Transport Header) │
│ - Opcode (操作类型: SEND/WRITE/READ) │
│ - PSN (包序列号) │
│ - QPN (队列对号) │
├─────────────────────────────────────────┤
│ IB RETH (RDMA Extended Header) │
│ - VA (虚拟地址) │
│ - Rkey (远程密钥) │
│ - DMA Length (数据长度) │
├─────────────────────────────────────────┤
│ Payload (负载数据) │
├─────────────────────────────────────────┤
│ ICRC (完整性校验) │
└─────────────────────────────────────────┘
传输过程:
1. 应用程序调用ibv_post_send()
2. 用户态驱动构造WQE(工作队列元素)
3. Soft-RoCE驱动处理WQE,封装为RoCEv2数据包
4. 通过标准UDP/IP协议栈发送
5. 接收端Soft-RoCE驱动解析数据包
6. 写入目标内存区域
7. 生成CQE(完成队列元素)