RDMA 乒乓示例:CX5 <-> CX6 DX(QSFP28 直连)

两台服务器,一台插 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

运行前检查(两台机器都做)

  1. 端口是 Ethernet 模式且已 LinkUp:

    bash 复制代码
    ibstat            # State: Active, Link layer: Ethernet
  2. 有 RoCE v2 的 GID(程序会自动挑选,也可用 -g 手工指定):

    bash 复制代码
    show_gids         # 找一行 VER=v2 的,记下 INDEX
  3. 给直连口配上 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)

  1. 建链 :两端用普通 TCP(管理通道)互换 QPN + GID,然后各自把 RC QP
    RESET -> INIT -> RTR -> RTS 推进,RDMA 通道打通。
  2. 预挂 RECV :收端先 ibv_post_recv() 把"接收任务单"挂到 RQ,
    否则对端 SEND 到达时网卡无处安放数据。
  3. 发送 :发端把 128 字节填好,ibv_post_send()(= 填 SEND WQE 到 SQ +
    写门铃),网卡 DMA 取数、封包、发出。
  4. 完成 :网卡把数据 DMA 进收端 recv_buf 并写 CQE;双方各自
    ibv_poll_cq() 轮询 CQ 拿回执,收端逐字节校验后进入下一轮。
  5. 全程 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
相关推荐
Eloudy2 小时前
cpu rdma 与 gpunetio 的关系
gpu·rdma·roce·doca
HHFQ7 小时前
Linux RDMA命令行工具使用手册
rdma
gwf2161 天前
AI RDMA网络的光互联:CPO与硅光子技术前瞻——基于芯片设计验证与系统级协同的深度剖析
rdma·dpu·cpo·ibgda·ai集群网络·硅光子·光互联
gwf2162 天前
RDMA在联邦学习与跨DC训练中的应用:从协议栈到芯片微架构的深度解析
rdma·nccl·硬件加速·rocev2·dcqcn·gpudirect·ai集群网络
gwf2164 天前
AI RDMA流量工程:自适应路由与动态负载均衡 —— 架构篇:从芯片RTL到集群拓扑的算网协同设计
rdma·拥塞控制·dpu·ai集群·gpudirect·rail-optimized·自适应路由
gwf2166 天前
NVMe/RDMA传输层协议深度解析:RDMA原理、Queue Pair映射、内核实现与性能全栈剖析
linux内核·ssd·nvme·性能调优·rdma·存储协议·nvme/rdma
gwf2167 天前
800G RNIC芯片设计挑战:PCIe Gen6与DMA引擎架构 —— 面向AI超大规模集群的硬件实现深度解析
芯片设计·rdma·rnic·gpudirect·pciegen6·dma引擎·mrc协议
gwf2169 天前
AI多租户集群的RDMA隔离:PD/MR/ACL硬件实现 —— 面向大模型训练/推理的硅级安全架构
芯片设计·rdma·nccl·ai集群·gpudirect·多租户隔离·sr-iov
gwf2169 天前
NVLink与RDMA融合:Scale-Up/Scale-Out统一互联架构深度解析
rdma·nvlink·nccl·dpu·rocev2·aiinfra·gpudirect