
简介这份资源是面向RDMA初学者与高性能网络开发者的C语言编程示例源码包围绕《RDMA C编程示例详解》展开帮助读者在缺少硬件文档的情况下理解RDMA通信的核心机制。包内共18个文件以8个.c源文件、3个.h头文件为主体辅以3个Makefile构建脚本、3个txt说明与1个md文档压缩包约19KB体量轻便却覆盖了从基础客户端/服务端到读写、文件传输的递进式示例。内容按模块组织包含基础客户端服务端交互、read-write读写操作以及file-transfer文件传输等场景涉及队列对、完成队列、内存区域注册与工作请求提交等关键概念便于对照代码理解libibverbs接口的实际用法。目前已有271人学习适合希望快速上手RDMA编程、搭建可运行实验环境并掌握资源管理与排错思路的开发者参考。1. 从一份 RDMA 源码包说起谁需要把 Verbs 编程啃到能跑手里这份the-geek-in-the-corner-master.zip解压后目录结构很直白01_basic-client-server、02_read-write、03_file-transfer三个递进示例外加README.md、LICENSE.txt和若干Makefile。它不是框架也不是库就是一套用 C 直接调 libibverbs 的最小可运行集合。如果你正在做高性能存储、分布式训练参数同步或者单纯被ibv_post_send那一串参数绕得头晕这份源码的价值在于它把「建 QP、注册 MR、提交 WR、轮询 CQ」这条链路拆成了三个能单独编译运行的阶段而不是一上来就丢给你一个几千行的完整项目。我见过太多人卡在 RDMA 的第一公里文档读了一堆QP 状态机背得滚瓜烂熟真到写代码时连ibv_create_qp返回 NULL 都不知道从哪查。这份源码解决的就是这个断层——它让你先跑通一个客户端-服务端握手再理解单边 READ/WRITE 的内存语义最后落到文件传输这个真实场景。适合谁有 C 基础、懂 socket 编程、想从 TCP 过渡到 RDMA 的工程师也适合需要快速验证网卡和驱动是否正常的运维同学。不适合零网络编程基础的人因为里面没有教你什么是端口、什么是字节序。2. 三个目录的递进逻辑从握手到单边操作再到文件传输2.1 为什么是 01、02、03 这个顺序很多人拿到源码包习惯直接翻03_file-transfer觉得文件传输才是「有用」的结果被里面交织的 QP 状态切换和 MR 注册搞得一头雾水。这份源码的编号不是随便排的它对应 RDMA 编程的三个认知台阶。01_basic-client-server只做一件事建立连接。服务端创建rdma_cm_id绑定到某个 IP 和端口进入监听客户端发起连接请求双方在RDMA_CM_EVENT_ESTABLISHED事件后各自创建 QP 并过渡到RTS状态。这个阶段不传任何用户数据目的是让你看清rdma_cm这套连接管理接口和裸ibverbs的区别——前者帮你处理了路由和握手后者只管队列对本身。02_read-write在此基础上引入内存注册和单边操作。服务端注册一块 MR把rkey和地址通过带内消息发给客户端客户端直接对这块远端内存发起IBV_WR_RDMA_READ或IBV_WR_RDMA_WRITE。这里的关键认知转变是数据搬运不再经过远端 CPU远端甚至不知道有人读了它的内存。rdma-server.c和rdma-client.c里对ibv_sge、ibv_send_wr的填充顺序值得逐行对照。03_file-transfer把前两步合起来加上文件分块、偏移量管理和完成队列的批量收割。sequence.txt和results.txt这两个文件不是代码是运行时的输入输出记录用来验证传输完整性。rdma-common.c和rdma-common.h把重复的资源创建、错误检查、连接建立抽成公共函数这是从示例走向可维护代码的信号。2.2 编译前必须确认的三件事在敲make之前有三件事没确认后面全是玄学报错。第一网卡是否支持 RDMA。用ibv_devices列出设备如果输出为空说明要么没插支持 RoCE 的网卡要么驱动没加载。常见的是 Mellanox 系列ibstat能看到端口状态为Active才算就绪。第二libibverbs-dev和librdmacm-dev是否安装。这份源码的 Makefile 里链接了-libverbs -lrdmacm缺任何一个都会在链接阶段报undefined reference。Debian/Ubuntu 系用apt install libibverbs-dev librdmacm-devRHEL 系用yum install libibverbs-devel librdmacm-devel。第三rdma-common.h里的宏定义是否和你的环境匹配。比如MAX_MSG_SIZE、QP_DEPTH这些值源码给的是保守配置但在高吞吐场景下需要根据网卡能力调整。先按默认值跑通再谈调优。# 确认 RDMA 设备列表输出应为网卡名称如 mlx5_0 ibv_devices # 查看端口状态State 应为 Active物理状态为 LinkUp ibstat # 安装依赖以 Ubuntu 为例 sudo apt update sudo apt install -y libibverbs-dev librdmacm-dev build-essential这三条命令的输出直接决定了后面能不能跑。ibv_devices为空时不要急着改代码先查dmesg | grep -i mlx看驱动加载情况。ibstat里端口状态是Down的话检查网线、交换机端口配置或者 RoCE 的 VLAN 设置。2.3 编译与运行的最小闭环进入01_basic-client-server目录make之后会生成server和client两个可执行文件。先在一个终端跑服务端再在另一个终端跑客户端。服务端会打印监听的 IP 和端口客户端需要把这两个参数传进去。# 终端 1启动服务端监听 192.168.1.100 的 7471 端口 cd 01_basic-client-server make ./server 192.168.1.100 7471 # 终端 2启动客户端连接服务端 ./client 192.168.1.100 7471服务端输出RDMA_CM_EVENT_ESTABLISHED后客户端也会打印同样的事件说明 QP 已经进入RTS连接建立成功。这个阶段没有数据传输但它是后面所有操作的地基。如果卡在这里九成是 IP 不可达或者防火墙拦了rdma_cm使用的端口。注意rdma_cm的端口和 TCP 端口不是一回事但防火墙规则如果默认拒绝所有入站照样会挡。02_read-write的运行方式类似但服务端会多打印一行注册的 MR 地址和rkey。客户端拿到这些信息后发起 READ服务端的内存内容会被读到客户端缓冲区。你可以先在服务端用malloc填充一段字符串客户端读完后打印出来对比。03_file-transfer则需要指定一个待传输的文件路径服务端和客户端要对同一个文件名达成一致传输完成后用md5sum比对两端文件。3. 把 QP 状态机和 MR 注册讲透代码里那几行到底在干什么3.1 QP 状态迁移的四个关键节点RDMA 的 QP 不是创建出来就能直接发数据的它必须经过RESET → INIT → RTR → RTS四个状态。这份源码在rdma-common.c里把每个状态的修改封装成了独立函数但很多人调ibv_modify_qp时只改一个字段结果返回EINVAL。RESET是创建后的默认状态此时 QP 不能收发任何东西。INIT状态需要设置qp_access_flags决定这个 QP 允许哪些操作——IBV_ACCESS_REMOTE_WRITE、IBV_ACCESS_REMOTE_READ、IBV_ACCESS_LOCAL_WRITE这几个标志位必须按需打开。如果你只做 READ却忘了给远端 MR 加IBV_ACCESS_REMOTE_READ客户端会收到REMOTE_ACCESS_ERR。RTR状态要填dest_qp_num、rkey、ah_attr或者path_mtu。在 RoCE 环境下ah_attr的填充和 InfiniBand 不同is_global要设为 1grh里的sgid_index要和网卡的 GID 表对应。源码里用rdma_cm拿到的ah_attr直接填进去省去了手动构造的麻烦这也是推荐用rdma_cm而不是裸ibverbs建连的原因之一。RTS状态设置sq_psn和timeout。sq_psn是发送队列的起始包序列号两端必须一致或者按规则递增否则会出现RETRY_EXC_ERR。源码里用rand()生成一个初始值并通过带内消息交换这是常见做法。// rdma-common.c 中 QP 状态迁移的典型片段 // 迁移到 INIT设置访问权限和端口 memset(qp_attr, 0, sizeof(qp_attr)); qp_attr.qp_state IBV_QPS_INIT; qp_attr.pkey_index 0; qp_attr.port_num port; qp_attr.qp_access_flags IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ; if (ibv_modify_qp(qp, qp_attr, IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS)) { perror(Failed to modify QP to INIT); return -1; }这段代码里IBV_QP_STATE等掩码告诉驱动「我要改哪些字段」没在掩码里出现的字段即使填了也会被忽略。这是ibv_modify_qp最容易翻车的地方——你以为改了其实没生效。每次调用后检查返回值不要假设一定成功。3.2 MR 注册不是 malloc 完就能 RDMA 读RDMA 网卡只能访问已经注册过的内存区域这是硬件层面的限制。ibv_reg_mr做两件事把虚拟地址翻译成物理地址并锁页同时生成一个rkey供远端引用。源码里rdma-common.c的register_mr函数封装了这个过程。注册时的access_flags决定了这块内存允许被怎么操作。如果服务端要接受远端的 WRITE必须加IBV_ACCESS_REMOTE_WRITE如果只允许远端 READ加IBV_ACCESS_REMOTE_READ就够了。IBV_ACCESS_LOCAL_WRITE是给本地写操作用的比如接收缓冲区。漏掉某个标志位对应的操作会在完成队列里返回IBV_WC_REM_ACCESS_ERR。// 注册 MR 并打印 rkey供远端引用 struct ibv_mr *mr ibv_reg_mr(pd, buf, size, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ); if (!mr) { perror(ibv_reg_mr failed); return NULL; } printf(MR registered: addr%p, rkey0x%x\n, buf, mr-rkey);pd是保护域从ibv_alloc_pd获得它把 QP 和 MR 绑定在一起只有同一个 PD 下的 QP 才能访问对应的 MR。源码里每个连接创建一个 PD这是简单场景的做法高并发下可以共享 PD但要注意资源竞争。3.3 完成队列轮询别用 sleep 等结果提交 WR 之后操作不会立即完成。ibv_post_send返回 0 只代表请求成功放入了发送队列不代表数据已经到达。真正的完成通知在 CQ 里需要用ibv_poll_cq轮询。源码里poll_completion函数是一个while循环不断调用ibv_poll_cq直到返回非零值。返回的wc结构里status字段是判断成功与否的唯一依据。IBV_WC_SUCCESS才是成功其他值对应不同的错误类型。wc.opcode告诉你完成的是什么操作wc.wr_id是你提交 WR 时设置的标识用来区分多个未完成请求。// 轮询 CQ 直到拿到一个完成事件 struct ibv_wc wc; int n; do { n ibv_poll_cq(cq, 1, wc); } while (n 0); if (wc.status ! IBV_WC_SUCCESS) { fprintf(stderr, Completion failed: %s\n, ibv_wc_status_str(wc.status)); return -1; }这里有个性能陷阱忙轮询会占满一个 CPU 核心。在延迟敏感场景下这是必要的但在文件传输这种吞吐优先的场景可以在循环里加usleep(1)或者用ibv_req_notify_cq配合事件通道。源码为了简洁用了纯忙轮询实际部署时要根据场景调整。4. 避坑与排查跑不通时先看这五个地方4.1 现象ibv_reg_mr返回 NULLerrno为ENOMEM原因通常是ulimit -l限制太小。RDMA 注册内存需要锁页默认的 locked memory 限制可能只有 64KB而你要注册的缓冲区远大于这个值。解决方法是临时提高限制ulimit -l unlimited或者修改/etc/security/limits.conf永久生效。注意这个限制对每个进程生效容器环境下还要看容器的--ulimit参数。4.2 现象连接建立后第一次 READ 就返回IBV_WC_REM_ACCESS_ERR先检查远端 MR 的access_flags是否包含IBV_ACCESS_REMOTE_READ。如果确认加了再看rkey是否传对了。rkey是一个 32 位值通过带内消息传输时如果用了ntohl转换而两端字节序不一致就会变成一个无效值。源码里用memcpy直接拷贝结构体避免了手动转换的坑但如果你自己改成了逐字段发送这里很容易翻车。4.3 现象ibv_modify_qp到 RTR 时报EINVAL九成是ah_attr没填对。RoCE 环境下ah_attr.is_global必须为 1ah_attr.grh.sgid_index要指向一个有效的 GID。用show_gids命令查看网卡的 GID 表选一个 RoCE v2 的条目。如果sgid_index填了 0 但 0 号 GID 是 RoCE v1而交换机只支持 v2就会在 RTR 阶段失败。源码里通过rdma_cm自动获取ah_attr绕过了这个问题但如果你手动构造务必核对 GID 类型。4.4 现象文件传输完成后md5sum不一致先看results.txt里记录的传输字节数是否和源文件大小一致。如果少了检查文件分块的循环边界——03_file-transfer里按固定块大小读取最后一块可能不足整块如果代码里固定按整块发送就会多读越界或者少发数据。另外接收端的缓冲区如果没有按实际接收长度截断写入文件时会多出脏数据。源码里用wc.byte_len来确认实际传输长度这个字段必须用上。4.5 现象程序运行一段时间后卡死ibv_poll_cq永远返回 0检查 QP 是否进入了错误状态。用ibv_query_qp查询qp_state如果是IBV_QPS_ERR说明之前有操作失败了但没处理。常见原因是发送队列深度不够ibv_post_send返回ENOMEM但被忽略后续操作全部阻塞。源码里QP_DEPTH设为 16在高吞吐场景下需要增大到 128 或更高。另外每次ibv_post_send后都要检查返回值非零时打印errno并清理。提示RDMA 编程里最耗时的往往不是写代码而是确认环境。ibv_devinfo看固件版本ibv_query_port看端口能力ethtool -S看丢包统计这三板斧能解决大部分「代码没问题但跑不通」的情况。5. 从能跑到好用把示例改造成可复用的传输模块5.1 把rdma-common抽成静态库三个目录里重复的代码不少创建 PD、注册 MR、建连、轮询 CQ。与其每次复制粘贴不如把rdma-common.c编译成librdma-common.a头文件里只暴露必要的结构体和函数声明。这样02和03的 Makefile 只需要链接这个静态库代码量能减少三分之一。# 编译静态库的 Makefile 片段 CC gcc CFLAGS -Wall -O2 -g LIB librdma-common.a OBJS rdma-common.o $(LIB): $(OBJS) ar rcs $ $^ rdma-common.o: rdma-common.c rdma-common.h $(CC) $(CFLAGS) -c $ -o $ clean: rm -f $(OBJS) $(LIB)ar rcs创建静态库r表示插入c表示创建s表示生成索引。链接时用-L. -lrdma-common注意库名要去掉lib前缀和.a后缀。这样改造后新增一个示例只需要写业务逻辑基础设施代码不用动。5.2 用事件通道替代忙轮询忙轮询在低延迟场景下是合理的但文件传输这种场景下CPU 空转浪费严重。ibv_create_comp_channel创建一个完成通道ibv_req_notify_cq请求通知然后用ibv_get_cq_event阻塞等待。收到事件后重新武装 CQ再轮询取完成项。// 创建完成通道并绑定到 CQ struct ibv_comp_channel *comp_channel ibv_create_comp_channel(context); struct ibv_cq *cq ibv_create_cq(context, CQ_DEPTH, NULL, comp_channel, 0); // 请求通知 ibv_req_notify_cq(cq, 0); // 阻塞等待事件 struct ibv_cq *ev_cq; void *ev_ctx; ibv_get_cq_event(comp_channel, ev_cq, ev_ctx); ibv_ack_cq_events(ev_cq, 1); // 确认事件 ibv_req_notify_cq(ev_cq, 0); // 重新武装ibv_ack_cq_events必须调用否则事件计数不会减少达到上限后通道会失效。这个细节在源码的忙轮询版本里不存在但改成事件驱动后就是必选项。事件通道适合吞吐优先、延迟不敏感的场景如果追求极致低延迟还是忙轮询更稳。5.3 验证传输完整性的三个层次改完代码怎么确认没引入新问题第一层用md5sum比对源文件和目标文件这是最基本的。第二层在传输前后打印 MR 的rkey和地址确认没有复用已释放的 MR。第三层用ibv_query_qp在传输结束后查询 QP 状态确认没有进入ERR状态。我自己的习惯是每次改完rdma-common.c先跑01确认建连正常再跑02确认单边读写正常最后跑03传一个 100MB 左右的文件并比对哈希。这三步走完基本能覆盖大部分回归问题。从那以后我每次动公共代码都强制走一遍这个流程省去了很多「改一处崩三处」的后悔药。希望帮到你。本文还有配套的精品资源点击获取