两台服务器,一台插 ConnectX-5 ,一台插 ConnectX-6 DX ,两块网卡用一根 QSFP28 线缆直连 (不经过交换机)。两端通过 RDMA RC(RoCE v2) 互相收发数据:
轮次 1: CX5 端 (initiator) ---> CX6 端 (responder) 128 个 0x01
轮次 2: CX6 端 (responder) ---> CX5 端 (initiator) 128 个 0x02
轮次 3: CX5 端 (initiator) ---> CX6 端 (responder) 128 个 0x03
... 奇数轮 CX5 发,偶数轮 CX6 发,填充值 = 轮次号 & 0xff
接收方每轮逐字节校验"128 个字节全部等于本轮的填充值",全部通过打印 PASS。
为什么用原生 verbs 而不是 DOCA
doca_rdma 库官方要求 ConnectX-6 Dx 及以上,CX5 跑不了 DOCA RDMA ;
而两端必须使用同一种编程框架,所以本项目用两端都支持的 libibverbs(rdma-core) 。
代码里的每个步骤(WQE / 门铃 / CQE / DMA)与 ../general_rdma.mc 讲的流水线一一对应。
编译
两台机器上都需要安装 rdma-core(libibverbs 开发文件),然后:
bash
cd rdma_project
make
运行前检查(两台机器都做)
-
端口是 Ethernet 模式且已 LinkUp:
bashibstat # State: Active, Link layer: Ethernet -
有 RoCE v2 的 GID(程序会自动挑选,也可用
-g手工指定):bashshow_gids # 找一行 VER=v2 的,记下 INDEX -
给直连口配上 IP(RoCE v2 需要;这条 TCP/IP 同时被用作"管理通道"交换建链信息):
bash# CX5 机器(网口名以实际为准,可用 show_gids 最后一列看到): sudo ip addr add 192.168.100.1/24 dev enp33s0f0np0 # CX6 机器: sudo ip addr add 192.168.100.2/24 dev enp33s0f1np1
运行
先在 CX6 机器上启动 responder (设备名按 ibv_devices 实际输出填):
bash
./rdma_pingpong -m responder -d mlx5_0
再在 CX5 机器上启动 initiator ,-s 指向 CX6 的 IP:
bash
./rdma_pingpong -m initiator -d mlx5_0 -s 192.168.100.2
期望输出(initiator 侧,共 10 轮):
================ RDMA 乒乓 (RC / RoCE v2) ================
角色 : initiator(CX5端),共 10 轮,每轮 128 字节
...
[轮次 1/10] 本端(initiator(CX5端)) -- 发送 128 x 0x01 --> 对端 [SEND 完成]
[轮次 2/10] 本端(initiator(CX5端)) <-- 收到 128 x 0x02 -- 对端 [校验 OK]
...
[轮次 9/10] 本端(initiator(CX5端)) -- 发送 128 x 0x09 --> 对端 [SEND 完成]
[轮次 10/10] 本端(initiator(CX5端)) <-- 收到 128 x 0x0a -- 对端 [校验 OK]
==========================================================
全部 10 轮完成,128 字节数据逐字节校验全部通过: PASS
常用选项:-r 轮数、-g GID下标、-i 网卡端口、-p TCP端口,详见 -h。
工作原理速览(对照 general_rdma.mc)
- 建链 :两端用普通 TCP(管理通道)互换
QPN + GID,然后各自把 RC QP
从RESET -> INIT -> RTR -> RTS推进,RDMA 通道打通。 - 预挂 RECV :收端先
ibv_post_recv()把"接收任务单"挂到 RQ,
否则对端 SEND 到达时网卡无处安放数据。 - 发送 :发端把 128 字节填好,
ibv_post_send()(= 填 SEND WQE 到 SQ +
写门铃),网卡 DMA 取数、封包、发出。 - 完成 :网卡把数据 DMA 进收端
recv_buf并写 CQE;双方各自
ibv_poll_cq()轮询 CQ 拿回执,收端逐字节校验后进入下一轮。 - 全程 CPU 只做"下单"和"看回执",128 字节数据不经过内核协议栈。
排错
| 现象 | 排查 |
|---|---|
端口状态不是 ACTIVE |
网线/光模块没插好;ibstat 看 State |
链路层不是 Ethernet |
端口在 IB 模式:sudo mlxconfig -d <mst设备> set LINK_TYPE_P1=2 后重启 |
找不到 RoCE v2 GID |
直连口没配 IP;或 show_gids 看 v2 行并用 -g 指定 |
| 等 SEND/RECV 完成超时 | 两端 GID 类型不一致;防火墙拦了 TCP 管理通道;dmesg/ibv_devinfo 进一步查 |
| 想跑 InfiniBand 模式 | 本项目按 RoCE v2 编写;IB 需要改用 LID 寻址且一端跑 opensm |
同机回环自测(开发调试用)
只有一台机器时,可以让两个进程走同一个端口做回环乒乓(流量在网卡内部回环):
bash
./rdma_pingpong -m responder -d mlx5_1 &
sleep 1
./rdma_pingpong -m initiator -d mlx5_1 -s 127.0.0.1
rdma_pingpong.c
c
/* SPDX-License-Identifier: MIT
*
* rdma_pingpong.c
*
* CX5 <-> CX6 DX,QSFP28 直连,RDMA RC (RoCE v2) 乒乓示例
* ---------------------------------------------------------------
* 行为约定(MSG_LEN 固定 128 字节):
* 轮次 1: initiator (CX5 端) ---> responder (CX6 端) 128 个 0x01
* 轮次 2: responder (CX6 端) ---> initiator (CX5 端) 128 个 0x02
* 轮次 3: initiator (CX5 端) ---> responder (CX6 端) 128 个 0x03
* ... 以此类推,接收方每轮校验"128 个字节全部等于 (轮次号 & 0xff)"。
*
* 工作流程(对照 general_rdma.mc 里讲的流水线):
* 1) 两端先通过普通 TCP(管理网口,或干脆就是直连口上配的 IP)
* 交换 RC QP 建链所需的信息:对端 QPN + 对端 GID;
* 2) 各自把 QP 从 RESET -> INIT -> RTR -> RTS,RDMA 通道建立;
* 3) 之后 CPU 只做两件事:填 WQE + 按门铃(post_send/post_recv),
* 以及轮询 CQ 拿回执(poll CQ);数据搬运完全由网卡 DMA 完成;
* 4) 全部轮次校验通过后打印 PASS。
*/
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <getopt.h>
#include <infiniband/verbs.h>
#include <netinet/in.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
/* ----------------------------- 常量 ----------------------------- */
#define MSG_LEN 128 /* 每轮消息长度(字节),按需求固定为 128 */
#define DEFAULT_TCP_PORT 18515 /* 交换建链信息的 TCP 端口(管理通道) */
#define DEFAULT_IB_PORT 1 /* 网卡端口号 */
#define DEFAULT_ROUNDS 10 /* 默认乒乓 10 轮 */
#define POLL_TIMEOUT_MS 15000 /* 轮询 CQ 的超时时间(毫秒) */
#define CONNECT_RETRY 30 /* initiator 连接 responder 的重试次数 */
/* ----------------------------- 类型 ----------------------------- */
enum role {
MODE_INITIATOR = 0, /* 先发的一方,跑在 CX5 机器上 */
MODE_RESPONDER = 1, /* 先收的一方,跑在 CX6 机器上 */
};
struct config {
enum role mode;
const char *server_ip; /* responder: 本地 bind 地址(NULL=0.0.0.0)
initiator: 对端( responder )IP,必填 */
int tcp_port;
const char *dev_name; /* RDMA 设备名,如 mlx5_0;NULL=取第一个 */
uint8_t ib_port;
int gid_idx; /* -1 表示自动挑选一个 RoCE v2 GID */
int rounds;
};
struct app {
struct config cfg;
struct ibv_context *ctx;
struct ibv_pd *pd;
struct ibv_cq *cq;
struct ibv_qp *qp;
struct ibv_mr *mr_send;
struct ibv_mr *mr_recv;
uint8_t *send_buf;
uint8_t *recv_buf;
struct ibv_port_attr port_attr;
union ibv_gid local_gid; /* 本端 RoCEv2 GID(放进 GRH 用) */
union ibv_gid remote_gid; /* 对端 GID(TCP 交换来的) */
int gid_idx;
uint32_t remote_qpn; /* 对端 QP 号(TCP 交换来的) */
};
/* 两端通过 TCP 交换的建链信息(RoCE 下 LID 用不上,只交换 QPN + GID) */
struct cm_con_data {
uint32_t qp_num; /* 网络字节序 */
uint8_t gid[16]; /* GID 原始字节 */
} __attribute__((packed));
/* ------------------------- 小工具函数 --------------------------- */
static void die(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "[ERROR] ");
vfprintf(stderr, fmt, ap);
fprintf(stderr, "\n");
va_end(ap);
exit(EXIT_FAILURE);
}
static const char *role_name(enum role r)
{
return r == MODE_INITIATOR ? "initiator(CX5端)" : "responder(CX6端)";
}
static const char *gid_str(union ibv_gid *gid, char *buf, size_t len)
{
if (inet_ntop(AF_INET6, gid->raw, buf, len) == NULL)
snprintf(buf, len, "<无法解析>");
return buf;
}
/* ---------------------- TCP 管理通道辅助 ------------------------ */
/* 说明:RDMA 建链前,两端必须互相知道对方的 QPN/GID。这里用最朴素的
* 方式------开一条普通 TCP 连接把这两个数字发过去。这条 TCP 走哪个网
* 都行:管理网口,或者 QSFP28 直连口上配的 IP 都可以。 */
static void write_full(int fd, const void *buf, size_t len)
{
const uint8_t *p = buf;
while (len > 0) {
ssize_t n = write(fd, p, len);
if (n <= 0)
die("TCP 写失败: %s", strerror(errno));
p += n;
len -= (size_t)n;
}
}
static void read_full(int fd, void *buf, size_t len)
{
uint8_t *p = buf;
while (len > 0) {
ssize_t n = read(fd, p, len);
if (n <= 0)
die("TCP 读失败: %s", n == 0 ? "对端关闭" : strerror(errno));
p += n;
len -= (size_t)n;
}
}
/* responder 侧:监听并接受一条 TCP 连接,返回已连接的 socket */
static int tcp_listen_accept(const char *bind_ip, int port)
{
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd < 0)
die("socket() 失败: %s", strerror(errno));
int one = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons((uint16_t)port),
};
if (bind_ip) {
if (inet_pton(AF_INET, bind_ip, &addr.sin_addr) != 1)
die("bind IP 格式不对: %s", bind_ip);
} else {
addr.sin_addr.s_addr = htonl(INADDR_ANY);
}
if (bind(lfd, (struct sockaddr *)&addr, sizeof(addr)) < 0)
die("TCP bind 端口 %d 失败: %s", port, strerror(errno));
if (listen(lfd, 1) < 0)
die("TCP listen 失败: %s", strerror(errno));
printf("[TCP] 管理通道监听中,等待 initiator 连接 (端口 %d) ...\n", port);
struct sockaddr_in peer;
socklen_t plen = sizeof(peer);
int cfd = accept(lfd, (struct sockaddr *)&peer, &plen);
if (cfd < 0)
die("TCP accept 失败: %s", strerror(errno));
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &peer.sin_addr, ip, sizeof(ip));
printf("[TCP] initiator 已从 %s 连上,管理通道建立\n", ip);
close(lfd);
return cfd;
}
/* initiator 侧:带重试地连接 responder,返回已连接的 socket */
static int tcp_connect_retry(const char *server_ip, int port)
{
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons((uint16_t)port),
};
if (inet_pton(AF_INET, server_ip, &addr.sin_addr) != 1)
die("对端 IP 格式不对: %s", server_ip);
for (int i = 1; i <= CONNECT_RETRY; i++) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0)
die("socket() 失败: %s", strerror(errno));
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) == 0) {
printf("[TCP] 已连上 responder %s:%d,管理通道建立\n",
server_ip, port);
return fd;
}
close(fd);
if (i == 1)
printf("[TCP] 连接 %s:%d 失败,每秒重试(请先启动 responder)...\n",
server_ip, port);
sleep(1);
}
die("连接 responder %s:%d 失败(重试 %d 次)", server_ip, port, CONNECT_RETRY);
return -1;
}
/* ------------------------ RDMA 资源建立 ------------------------- */
/* 自动挑选一个 RoCE v2 类型的 GID:优先选非链路本地地址(即配了 IP 的),
* 没有的话退而用 fe80:: 链路本地地址。 */
static void pick_rocev2_gid(struct app *a)
{
int fallback_idx = -1;
union ibv_gid fallback_gid;
for (int i = 0; i < a->port_attr.gid_tbl_len; i++) {
struct ibv_gid_entry e;
memset(&e, 0, sizeof(e));
if (ibv_query_gid_ex(a->ctx, a->cfg.ib_port, i, &e, 0) != 0)
continue;
if (e.gid_type != IBV_GID_TYPE_ROCE_V2)
continue;
static const uint8_t zero[16] = {0};
if (memcmp(e.gid.raw, zero, 16) == 0)
continue;
bool link_local = (e.gid.raw[0] == 0xfe &&
(e.gid.raw[1] & 0xc0) == 0x80);
if (!link_local) {
a->gid_idx = i;
a->local_gid = e.gid;
return;
}
if (fallback_idx < 0) {
fallback_idx = i;
fallback_gid = e.gid;
}
}
if (fallback_idx >= 0) {
a->gid_idx = fallback_idx;
a->local_gid = fallback_gid;
return;
}
die("端口 %d 上找不到任何 RoCE v2 GID,请用 `show_gids` 检查并用 -g 手工指定",
a->cfg.ib_port);
}
static void open_device(struct app *a)
{
int num = 0;
struct ibv_device **list = ibv_get_device_list(&num);
if (!list || num == 0)
die("没有发现任何 RDMA 设备(ibv_get_device_list)");
struct ibv_device *dev = NULL;
if (a->cfg.dev_name) {
for (int i = 0; i < num; i++)
if (strcmp(ibv_get_device_name(list[i]), a->cfg.dev_name) == 0)
dev = list[i];
if (!dev)
die("找不到名为 %s 的 RDMA 设备(可用 ibv_devices 查看)",
a->cfg.dev_name);
} else {
dev = list[0];
a->cfg.dev_name = ibv_get_device_name(dev);
}
a->ctx = ibv_open_device(dev);
if (!a->ctx)
die("ibv_open_device(%s) 失败", a->cfg.dev_name);
ibv_free_device_list(list);
if (ibv_query_port(a->ctx, a->cfg.ib_port, &a->port_attr) != 0)
die("ibv_query_port 失败(端口 %d 是否存在?)", a->cfg.ib_port);
if (a->port_attr.state != IBV_PORT_ACTIVE)
die("端口 %s:%d 状态不是 ACTIVE(网线插好了吗?可用 ibstat 查看)",
a->cfg.dev_name, a->cfg.ib_port);
if (a->port_attr.link_layer != IBV_LINK_LAYER_ETHERNET)
die("端口 %s:%d 链路层不是 Ethernet,本示例按 RoCE v2 编写",
a->cfg.dev_name, a->cfg.ib_port);
if (a->cfg.gid_idx >= 0) {
/* 用户手工指定了 GID 下标,直接取来用 */
if (ibv_query_gid(a->ctx, a->cfg.ib_port, a->cfg.gid_idx,
&a->local_gid) != 0)
die("ibv_query_gid(gid_idx=%d) 失败", a->cfg.gid_idx);
a->gid_idx = a->cfg.gid_idx;
} else {
pick_rocev2_gid(a);
}
}
static void create_qp_and_mrs(struct app *a)
{
a->pd = ibv_alloc_pd(a->ctx);
if (!a->pd)
die("ibv_alloc_pd 失败");
/* CQ:发送和接收共用一个完成队列,深度 32 绰绰有余 */
a->cq = ibv_create_cq(a->ctx, 32, NULL, NULL, 0);
if (!a->cq)
die("ibv_create_cq 失败");
/* RC(可靠连接)QP,只用 SEND/RECV 两种操作 */
struct ibv_qp_init_attr qia = {
.send_cq = a->cq,
.recv_cq = a->cq,
.cap = {
.max_send_wr = 16,
.max_recv_wr = 16,
.max_send_sge = 1,
.max_recv_sge = 1,
},
.qp_type = IBV_QPT_RC,
.sq_sig_all = 1, /* 每条 SEND 都要 CQE 回执,方便教学演示 */
};
a->qp = ibv_create_qp(a->pd, &qia);
if (!a->qp)
die("ibv_create_qp 失败");
/* QP: RESET -> INIT */
struct ibv_qp_attr attr = {
.qp_state = IBV_QPS_INIT,
.pkey_index = 0,
.port_num = a->cfg.ib_port,
.qp_access_flags = 0, /* SEND/RECV 不需要远端内存访问权限 */
};
if (ibv_modify_qp(a->qp, &attr,
IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT |
IBV_QP_ACCESS_FLAGS) != 0)
die("QP 切到 INIT 失败");
/* 注册发送/接收两块内存(MR),把 lkey 交给网卡当"钥匙" */
if (posix_memalign((void **)&a->send_buf, 64, MSG_LEN) != 0 ||
posix_memalign((void **)&a->recv_buf, 64, MSG_LEN) != 0)
die("posix_memalign 失败");
memset(a->send_buf, 0, MSG_LEN);
memset(a->recv_buf, 0, MSG_LEN);
a->mr_send = ibv_reg_mr(a->pd, a->send_buf, MSG_LEN,
IBV_ACCESS_LOCAL_WRITE);
a->mr_recv = ibv_reg_mr(a->pd, a->recv_buf, MSG_LEN,
IBV_ACCESS_LOCAL_WRITE);
if (!a->mr_send || !a->mr_recv)
die("ibv_reg_mr 失败");
}
/* QP: INIT -> RTR -> RTS(需要 TCP 交换来的对端 QPN/GID) */
static void connect_qp(struct app *a)
{
/* INIT -> RTR:告诉网卡"对端 QP 是谁、走哪条路径(GRH)" */
struct ibv_qp_attr attr;
memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_RTR;
attr.path_mtu = IBV_MTU_1024; /* 128B 消息,任何链路都够 */
attr.dest_qp_num = a->remote_qpn;
attr.rq_psn = 0;
attr.max_dest_rd_atomic = 1;
attr.min_rnr_timer = 12;
attr.ah_attr.is_global = 1; /* RoCE 必须带 GRH */
attr.ah_attr.sl = 0;
attr.ah_attr.src_path_bits = 0;
attr.ah_attr.port_num = a->cfg.ib_port;
attr.ah_attr.grh.dgid = a->remote_gid;
attr.ah_attr.grh.sgid_index = a->gid_idx;
attr.ah_attr.grh.hop_limit = 64;
attr.ah_attr.grh.traffic_class = 0;
if (ibv_modify_qp(a->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) != 0)
die("QP 切到 RTR 失败");
/* RTR -> RTS:此后才能 post_send */
memset(&attr, 0, sizeof(attr));
attr.qp_state = IBV_QPS_RTS;
attr.sq_psn = 0;
attr.timeout = 14;
attr.retry_cnt = 7;
attr.rnr_retry = 7;
attr.max_rd_atomic = 1;
if (ibv_modify_qp(a->qp, &attr,
IBV_QP_STATE | IBV_QP_SQ_PSN | IBV_QP_TIMEOUT |
IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY |
IBV_QP_MAX_QP_RD_ATOMIC) != 0)
die("QP 切到 RTS 失败");
}
/* --------------------------- 数据通路 --------------------------- */
/* 填一张 RECEIVE WQE 挂到 RQ:告诉网卡"有数据来就 DMA 到 recv_buf" */
static void post_recv(struct app *a)
{
struct ibv_sge sge = {
.addr = (uintptr_t)a->recv_buf,
.length = MSG_LEN,
.lkey = a->mr_recv->lkey,
};
struct ibv_recv_wr wr = {
.wr_id = 2,
.sg_list = &sge,
.num_sge = 1,
};
struct ibv_recv_wr *bad = NULL;
if (ibv_post_recv(a->qp, &wr, &bad) != 0)
die("ibv_post_recv 失败");
}
/* 填一张 SEND WQE 挂到 SQ 并按门铃:网卡会 DMA 读走 send_buf 发出去 */
static void post_send(struct app *a)
{
struct ibv_sge sge = {
.addr = (uintptr_t)a->send_buf,
.length = MSG_LEN,
.lkey = a->mr_send->lkey,
};
struct ibv_send_wr wr = {
.wr_id = 1,
.sg_list = &sge,
.num_sge = 1,
.opcode = IBV_WR_SEND,
.send_flags = IBV_SEND_SIGNALED,
};
struct ibv_send_wr *bad = NULL;
if (ibv_post_send(a->qp, &wr, &bad) != 0)
die("ibv_post_send 失败");
}
/* 轮询 CQ 拿回执(CQE),带超时,返回 0 表示拿到一条成功完成 */
static void poll_one(struct app *a, const char *what)
{
struct timespec start;
clock_gettime(CLOCK_MONOTONIC, &start);
for (;;) {
struct ibv_wc wc;
int n = ibv_poll_cq(a->cq, 1, &wc);
if (n < 0)
die("ibv_poll_cq 出错");
if (n > 0) {
if (wc.status != IBV_WC_SUCCESS)
die("%s 完成但状态异常: %s (vendor_err=0x%x)",
what, ibv_wc_status_str(wc.status),
wc.vendor_err);
/* byte_len 只对 RECV 型 CQE 有效,SEND 型是未定义值 */
if (wc.opcode == IBV_WC_RECV && wc.byte_len != MSG_LEN)
die("%s 字节数不对: %u(期望 %d)",
what, wc.byte_len, MSG_LEN);
return;
}
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long elapsed = (now.tv_sec - start.tv_sec) * 1000 +
(now.tv_nsec - start.tv_nsec) / 1000000;
if (elapsed > POLL_TIMEOUT_MS)
die("等待 %s 完成超时(%d ms),链路或对端异常",
what, POLL_TIMEOUT_MS);
}
}
/* --------------------------- 乒乓主循环 -------------------------- */
static void run_pingpong(struct app *a)
{
bool i_am_initiator = (a->cfg.mode == MODE_INITIATOR);
int total = a->cfg.rounds;
/* 关键:收端必须先于对端的 SEND 把 RECV WQE 挂好,
* 否则网卡收到数据无处安放(RNR)。第一张 RECV 在这里预挂。 */
post_recv(a);
for (int round = 1; round <= total; round++) {
uint8_t val = (uint8_t)(round & 0xff);
/* 奇数轮 initiator 发,偶数轮 responder 发 */
bool my_turn_to_send = ((round % 2) == 1) == i_am_initiator;
if (my_turn_to_send) {
memset(a->send_buf, val, MSG_LEN);
post_send(a);
poll_one(a, "SEND");
printf("[轮次 %2d/%d] 本端(%s) -- 发送 128 x 0x%02x --> 对端 [SEND 完成]\n",
round, total, role_name(a->cfg.mode), val);
} else {
poll_one(a, "RECV");
/* 校验:128 个字节必须全部等于本轮的填充值 */
for (int i = 0; i < MSG_LEN; i++)
if (a->recv_buf[i] != val)
die("轮次 %d 数据校验失败: 第 %d 字节是 0x%02x,期望 0x%02x",
round, i, a->recv_buf[i], val);
printf("[轮次 %2d/%d] 本端(%s) <-- 收到 128 x 0x%02x -- 对端 [校验 OK]\n",
round, total, role_name(a->cfg.mode), val);
/* 这张 RECV 已被消费;若后面还有要我收的轮次,补挂一张 */
if (round + 2 <= total)
post_recv(a);
}
}
}
/* ----------------------------- 收尾 ----------------------------- */
static void cleanup(struct app *a)
{
if (a->qp) ibv_destroy_qp(a->qp);
if (a->cq) ibv_destroy_cq(a->cq);
if (a->mr_send) ibv_dereg_mr(a->mr_send);
if (a->mr_recv) ibv_dereg_mr(a->mr_recv);
if (a->pd) ibv_dealloc_pd(a->pd);
if (a->ctx) ibv_close_device(a->ctx);
free(a->send_buf);
free(a->recv_buf);
}
/* --------------------------- 参数与 main ------------------------- */
static void usage(const char *prog)
{
printf(
"用法: %s -m <initiator|responder> [选项]\n"
"\n"
" -m, --mode <initiator|responder> 角色(必填)\n"
" initiator 跑在 CX5 端,先发 128 个 0x01\n"
" responder 跑在 CX6 端,回发 128 个 0x02\n"
" -s, --server-ip <IPv4> initiator: responder 的 IP(必填)\n"
" responder: 本地监听 IP(默认 0.0.0.0)\n"
" -p, --tcp-port <端口> 管理通道 TCP 端口(默认 %d)\n"
" -d, --device <设备名> RDMA 设备(默认取第一个,如 mlx5_0)\n"
" -i, --ib-port <端口> 网卡端口号(默认 %d)\n"
" -g, --gid-idx <下标> GID 下标(默认自动挑选 RoCE v2)\n"
" -r, --rounds <轮数> 乒乓轮数(默认 %d,>255 时填充值回绕)\n"
" -h, --help 显示本帮助\n",
prog, DEFAULT_TCP_PORT, DEFAULT_IB_PORT, DEFAULT_ROUNDS);
}
static void parse_args(int argc, char **argv, struct config *cfg)
{
static const struct option opts[] = {
{ "mode", required_argument, NULL, 'm' },
{ "server-ip", required_argument, NULL, 's' },
{ "tcp-port", required_argument, NULL, 'p' },
{ "device", required_argument, NULL, 'd' },
{ "ib-port", required_argument, NULL, 'i' },
{ "gid-idx", required_argument, NULL, 'g' },
{ "rounds", required_argument, NULL, 'r' },
{ "help", no_argument, NULL, 'h' },
{ NULL, 0, NULL, 0 },
};
*cfg = (struct config){
.mode = MODE_INITIATOR,
.server_ip = NULL,
.tcp_port = DEFAULT_TCP_PORT,
.dev_name = NULL,
.ib_port = DEFAULT_IB_PORT,
.gid_idx = -1,
.rounds = DEFAULT_ROUNDS,
};
bool mode_set = false;
int c;
while ((c = getopt_long(argc, argv, "m:s:p:d:i:g:r:h", opts, NULL)) != -1) {
switch (c) {
case 'm':
if (strcmp(optarg, "initiator") == 0)
cfg->mode = MODE_INITIATOR;
else if (strcmp(optarg, "responder") == 0)
cfg->mode = MODE_RESPONDER;
else
die("-m 只接受 initiator 或 responder");
mode_set = true;
break;
case 's': cfg->server_ip = optarg; break;
case 'p': cfg->tcp_port = atoi(optarg); break;
case 'd': cfg->dev_name = optarg; break;
case 'i': cfg->ib_port = (uint8_t)atoi(optarg); break;
case 'g': cfg->gid_idx = atoi(optarg); break;
case 'r': cfg->rounds = atoi(optarg); break;
case 'h': usage(argv[0]); exit(EXIT_SUCCESS);
default: usage(argv[0]); exit(EXIT_FAILURE);
}
}
if (!mode_set)
die("必须用 -m 指定角色(initiator/responder),-h 查看帮助");
if (cfg->mode == MODE_INITIATOR && !cfg->server_ip)
die("initiator 必须用 -s 指定 responder 的 IP");
if (cfg->rounds <= 0)
die("轮数必须为正整数");
}
int main(int argc, char **argv)
{
struct app a;
memset(&a, 0, sizeof(a));
parse_args(argc, argv, &a.cfg);
/* 第 1 步:打开设备、建 QP/MR(此时还是本机资源,未建链) */
open_device(&a);
create_qp_and_mrs(&a);
char gidbuf[INET6_ADDRSTRLEN];
printf("================ RDMA 乒乓 (RC / RoCE v2) ================\n");
printf("角色 : %s,共 %d 轮,每轮 %d 字节\n",
role_name(a.cfg.mode), a.cfg.rounds, MSG_LEN);
printf("设备/端口 : %s : %d\n", a.cfg.dev_name, a.cfg.ib_port);
printf("本地 GID : %s (gid_idx=%d)\n",
gid_str(&a.local_gid, gidbuf, sizeof(gidbuf)), a.gid_idx);
printf("本地 QPN : 0x%06x\n", a.qp->qp_num);
/* 第 2 步:TCP 管理通道交换 QPN + GID */
int sock = (a.cfg.mode == MODE_RESPONDER)
? tcp_listen_accept(a.cfg.server_ip, a.cfg.tcp_port)
: tcp_connect_retry(a.cfg.server_ip, a.cfg.tcp_port);
struct cm_con_data mine = {
.qp_num = htonl(a.qp->qp_num),
};
memcpy(mine.gid, a.local_gid.raw, 16);
struct cm_con_data peer;
write_full(sock, &mine, sizeof(mine));
read_full(sock, &peer, sizeof(peer));
a.remote_qpn = ntohl(peer.qp_num);
memcpy(a.remote_gid.raw, peer.gid, 16);
printf("对端 GID : %s\n", gid_str(&a.remote_gid, gidbuf, sizeof(gidbuf)));
printf("对端 QPN : 0x%06x\n", a.remote_qpn);
/* 第 3 步:QP 推进到 RTS,RDMA 通道正式打通 */
connect_qp(&a);
printf("[RDMA] QP 已进入 RTS,RDMA 通道建立完成\n");
/* 双方都在 RTS 之后再开始发数,避免对端还没就绪 */
write_full(sock, "R", 1);
char ready;
read_full(sock, &ready, 1);
printf("==========================================================\n");
/* 第 4 步:开始乒乓 */
run_pingpong(&a);
printf("==========================================================\n");
printf("全部 %d 轮完成,128 字节数据逐字节校验全部通过: PASS\n", a.cfg.rounds);
close(sock);
cleanup(&a);
return 0;
}
Makefile
bash
# CX5 <-> CX6 DX RDMA(RC/RoCEv2) 乒乓示例
# 依赖:rdma-core(libibverbs 头文件与库)
CC ?= cc
CFLAGS ?= -O2 -Wall -Wextra -g
LDLIBS ?= -libverbs
BIN := rdma_pingpong
all: $(BIN)
$(BIN): rdma_pingpong.c
$(CC) $(CFLAGS) -o $@ $< $(LDLIBS)
clean:
rm -f $(BIN)
.PHONY: all clean