
简介这份资源是面向RDMA初学者与高性能网络开发者的C语言编程示例源码包围绕InfiniBand Verbs核心API展开帮助读者理解如何绕过内核直接进行远程内存访问。包内共18个文件以8个.c源文件、3个.h头文件、3个Makefile和3个txt说明为主另有README与LICENSE压缩包约19KB体量轻便却结构完整。内容按基础客户端-服务器、读写操作、文件传输三个层次递进覆盖RDMA上下文初始化、队列对与完成队列创建、内存区域注册、Work Request提交及Work Completion处理等关键环节并配有Makefile便于直接编译验证。已有271人学习适合希望从零掌握RDMA编程模型、为高性能计算与大数据传输场景打基础的开发者参考。1. 从一份 RDMA 源码包说起它到底能帮你省掉多少摸索时间如果你正在做高性能存储、分布式训练或者低延迟交易系统大概率绕不开 RDMA 这个词。但真正让人头疼的不是概念而是从零写一个能跑通的 RDMA 程序——光是ibv_open_device、ibv_alloc_pd、ibv_create_cq、ibv_create_qp这一串调用顺序就够劝退一批人。这份the-geek-in-the-corner-master源码包就是一份按学习曲线组织的 RDMA C 编程示例集从最基础的 client-server 通信到完整的文件传输三个递进目录把 verbs API 的核心链路串了一遍。它适合两类人一是刚接触 RDMA、需要一份能编译能跑的参考代码的开发者二是已经会用但想回头梳理 QP 状态机、MR 注册和 CQ 轮询细节的熟手。源码本身不依赖特定硬件厂商的封装库走的是标准 libibverbs 接口SoftRoCE 环境下也能跑通。2. 拆开目录看设计三个递进模块的技术选型与资源初始化2.1 为什么是 01/02/03 三层结构拿到一个源码包我习惯先看目录怎么分的。这份包的结构很直白the-geek-in-the-corner-master/ ├── 01_basic-client-server/ │ ├── server.c │ ├── client.c │ └── Makefile ├── 02_read-write/ │ ├── rdma-server.c │ ├── rdma-client.c │ ├── rdma-common.h │ ├── rdma-common.c │ └── Makefile ├── 03_file-transfer/ │ ├── sequence.txt │ └── results.txt ├── README.md └── LICENSE.txt01 目录是最小闭环只做连接建立和一次简单消息交换目的是让你把 RDMA 上下文初始化、PD 分配、CQ 创建、QP 创建和连接握手这条链路跑通。02 目录引入了rdma-common.c/h做公共封装把资源创建和销毁的重复代码抽出来同时加入了 RDMA Read/Write 操作这是真正体现 RDMA 价值的地方——远端 CPU 不参与数据搬运。03 目录则是文件传输场景的落地用sequence.txt和results.txt做传输验证。这个分层逻辑是合理的先跑通控制路径再跑数据路径最后做应用层封装。常见做法是直接拿 02 的代码改但如果你连 01 都没跑过遇到 QP 状态迁移失败时根本不知道从哪查。2.2 环境准备与依赖安装在编译之前先把依赖装好。以下命令适用于 Ubuntu/Debian 系# 安装 RDMA 核心库和开发头文件 sudo apt update sudo apt install -y libibverbs-dev librdmacm-dev rdma-core # 安装 perftest 工具集用于验证 RDMA 链路是否正常 sudo apt install -y perftest # 如果手头没有 IB 硬件加载 SoftRoCE 内核模块做软件模拟 sudo modprobe rdma_rxe sudo rdma link add rxe0 type rxe netdev eth0这里有几个参数需要说明。rdma_rxe是软件 RDMA 的实现netdev eth0指定绑定的网络接口你需要根据实际网卡名替换。加载后用rdma link show确认设备状态是 ACTIVE。perftest里的ib_send_bw和ib_write_bw是验证链路的标准工具后面排查问题时会反复用到。提示SoftRoCE 的性能远低于硬件 RDMA但用于功能验证和代码调试完全够用。如果你有 Mellanox 卡装完rdma-core后ibv_devices应该能直接列出设备。2.3 编译 01 目录并理解 Makefile 的链接逻辑进入 01 目录直接 makecd the-geek-in-the-corner-master/01_basic-client-server makeMakefile 里关键的链接参数通常是这样的CFLAGS -Wall -O2 -g LDFLAGS -libverbs -lrdmacm-libverbs提供 verbs API-lrdmacm提供 RDMA 连接管理CM相关的rdma_create_id、rdma_resolve_addr等函数。如果你只做 QP 手动握手而不走 CM 建链理论上可以去掉-lrdmacm但这份源码的 01 和 02 都用了 CM 来简化连接建立所以两个库都得链。编译通过后开两个终端。先跑 server./server再跑 client./client 127.0.0.1如果一切正常server 端会打印出收到的消息client 端会显示发送完成。这一步跑通意味着你的 RDMA 环境、编译工具链和基本 API 调用链路都没问题。2.4 02 目录的公共封装拆解02 目录的rdma-common.c把资源创建流程封装成了几个函数这是值得细看的部分。核心函数包括resources_init初始化struct resources把 ibv_context、pd、cq、qp 等指针置空resources_create打开设备、分配 PD、注册 MR、创建 CQ 和 QPresources_destroy逆序释放所有资源resources_create里的调用顺序不能乱// 1. 获取设备列表 ibv_get_device_list(num_devices); // 2. 打开设备 res-ib_ctx ibv_open_device(dev); // 3. 分配 Protection Domain res-pd ibv_alloc_pd(res-ib_ctx); // 4. 注册 Memory Region获取 rkey 和 lkey res-buf malloc(buf_size); res-mr ibv_reg_mr(res-pd, res-buf, buf_size, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ); // 5. 创建 Completion Queue res-cq ibv_create_cq(res-ib_ctx, cq_size, NULL, NULL, 0); // 6. 创建 Queue Pair res-qp ibv_create_qp(res-pd, qp_init_attr);ibv_reg_mr的 access flags 决定了这块内存允许哪些远端操作。IBV_ACCESS_REMOTE_WRITE允许远端写入IBV_ACCESS_REMOTE_READ允许远端读取IBV_ACCESS_LOCAL_WRITE允许本地写入。如果你只做 RDMA Read可以不设 REMOTE_WRITE但设多了不会报错设少了会在对端操作时返回IBV_WC_REM_ACCESS_ERR。QP 创建后处于 RESET 状态需要依次迁移到 INIT、RTR、RTS。这个状态机是 RDMA 编程里最容易翻车的地方下一章会专门讲。3. QP 状态机与内存注册RDMA 通信的核心链路怎么走通3.1 QP 三次状态迁移的参数含义QP 从 RESET 到 RTS 要经过三次ibv_modify_qp调用每次携带不同的属性掩码// 第一次RESET - INIT struct ibv_qp_attr attr {}; attr.qp_state IBV_QPS_INIT; attr.pkey_index 0; attr.port_num 1; 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); // 第二次INIT - RTR attr.qp_state IBV_QPS_RTR; attr.path_mtu IBV_MTU_1024; attr.dest_qp_num remote_qpn; // 对端 QP 号 attr.rq_psn 0; // 接收序列号 attr.max_dest_rd_atomic 1; attr.min_rnr_timer 12; attr.ah_attr.is_global 0; attr.ah_attr.dlid remote_lid; // 对端 LID attr.ah_attr.sl 0; attr.ah_attr.src_path_bits 0; attr.ah_attr.port_num 1; 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); // 第三次RTR - RTS attr.qp_state IBV_QPS_RTS; attr.timeout 14; attr.retry_cnt 7; attr.rnr_retry 7; attr.sq_psn 0; // 发送序列号 attr.max_rd_atomic 1; 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);几个参数值得展开说。path_mtu要和链路实际 MTU 匹配SoftRoCE 默认支持 1024硬件卡通常支持 4096。min_rnr_timer是接收端未准备好时的重试等待时间值越大等待越久。retry_cnt和rnr_retry都是 7 表示无限重试生产环境一般会调小以便快速失败。rq_psn和sq_psn是序列号起点两端必须约定一致否则会出现IBV_WC_RNR_RETRY_EXC_ERR或静默丢包。3.2 连接信息交换的两种方式RDMA 建链需要交换 QPN、LID、GID 和 rkey 等信息。这份源码用的是 RDMA CMCommunication Manager来简化这个过程// server 端监听 struct rdma_cm_id *listen_id; rdma_create_id(event_channel, listen_id, NULL, RDMA_PS_TCP); rdma_bind_addr(listen_id, (struct sockaddr *)addr); rdma_listen(listen_id, 1); // client 端解析地址并连接 rdma_create_id(event_channel, cm_id, NULL, RDMA_PS_TCP); rdma_resolve_addr(cm_id, NULL, (struct sockaddr *)addr, 2000); // 等待 RDMA_CM_EVENT_ADDR_RESOLVED rdma_resolve_route(cm_id, 2000); // 等待 RDMA_CM_EVENT_ROUTE_RESOLVED rdma_connect(cm_id, conn_param);CM 的好处是把 TCP 建链和 RDMA 参数交换揉在一起你不需要自己写 socket 来传 QPN 和 rkey。但代价是引入了事件循环代码结构会复杂一些。另一种常见做法是手动用 TCP socket 交换连接参数然后直接ibv_modify_qp到 RTR适合对建链延迟敏感的场景。3.3 内存注册的边界与 rkey 传递ibv_reg_mr返回的struct ibv_mr *里包含lkey和rkey。lkey 用于本地访问rkey 用于远端访问。在 RDMA Write 场景中发送方需要知道接收方的 rkey 和远端虚拟地址// 接收方把 mr-rkey 和 (uint64_t)mr-addr 发给发送方 // 发送方构造 RDMA Write WR struct ibv_sge sge { .addr (uint64_t)local_buf, .length data_len, .lkey local_mr-lkey }; struct ibv_send_wr wr { .wr_id 0, .sg_list sge, .num_sge 1, .opcode IBV_WR_RDMA_WRITE, .send_flags IBV_SEND_SIGNALED, .wr.rdma.remote_addr remote_addr, .wr.rdma.rkey remote_rkey }; ibv_post_send(qp, wr, bad_wr);这里有个容易忽略的点remote_addr必须落在对端注册的 MR 范围内否则硬件会返回IBV_WC_REM_ACCESS_ERR。MR 注册时如果用了IBV_ACCESS_REMOTE_WRITE对端才能写如果只设了IBV_ACCESS_REMOTE_READ对端写操作会被拒绝。3.4 轮询 CQ 与 WC 状态码解读提交 WR 后完成事件会进入 CQ。轮询代码如下struct ibv_wc wc; int ne; do { ne ibv_poll_cq(cq, 1, wc); } while (ne 0); if (wc.status ! IBV_WC_SUCCESS) { fprintf(stderr, WC error: %s\n, ibv_wc_status_str(wc.status)); return -1; }ibv_poll_cq返回 0 表示没有新完成事件返回正数表示取到了几个。wc.status是诊断问题的关键IBV_WC_SUCCESS正常IBV_WC_LOC_LEN_ERR本地长度错误IBV_WC_REM_ACCESS_ERR远端访问权限不足IBV_WC_RNR_RETRY_EXC_ERR接收端未就绪重试超限IBV_WC_RETRY_EXC_ERR发送重试超限。每个错误码对应的排查方向不同后面避坑章节会展开。4. 从 02 到 03文件传输场景的落地与验证4.1 文件分块传输的设计思路03 目录做的是文件传输核心问题是怎么把一个大文件切块通过 RDMA 发过去。常见做法是固定块大小比如 64KB 或 1MB循环提交 RDMA Write每块完成后轮询 CQ 确认。sequence.txt和results.txt应该是用来验证传输顺序和完整性的。一个简化的传输循环#define CHUNK_SIZE (1024 * 1024) // 1MB per chunk FILE *fp fopen(sequence.txt, rb); uint64_t offset 0; size_t nread; while ((nread fread(local_buf, 1, CHUNK_SIZE, fp)) 0) { struct ibv_sge sge { .addr (uint64_t)local_buf, .length nread, .lkey local_mr-lkey }; struct ibv_send_wr wr { .opcode IBV_WR_RDMA_WRITE, .send_flags IBV_SEND_SIGNALED, .sg_list sge, .num_sge 1, .wr.rdma.remote_addr remote_addr offset, .wr.rdma.rkey remote_rkey }; struct ibv_send_wr *bad_wr; ibv_post_send(qp, wr, bad_wr); // 等待完成 struct ibv_wc wc; while (ibv_poll_cq(cq, 1, wc) 0); if (wc.status ! IBV_WC_SUCCESS) { fprintf(stderr, chunk at offset %lu failed: %s\n, offset, ibv_wc_status_str(wc.status)); break; } offset nread; }这里每次只提交一个 WR 然后等完成吞吐量不是最优。生产环境一般会批量提交多个 WR用IBV_SEND_SIGNALED只标记最后一个减少 CQ 事件数量。但作为示例代码逐个等待更容易理解流程。4.2 用 perftest 验证链路带宽在跑自己的代码之前先用标准工具确认链路本身没问题# server 端 ib_write_bw -d rxe0 -a # client 端 ib_write_bw -d rxe0 -a 127.0.0.1-d指定设备名-a表示使用所有可用资源。输出会显示带宽和消息速率。SoftRoCE 环境下带宽通常在几 Gbps 级别硬件卡可以到 100Gbps 以上。如果 perftest 都跑不通自己的代码大概率也跑不通先排查环境。4.3 传输结果验证与数据一致性检查文件传完后用md5sum或diff对比源文件和目标文件md5sum sequence.txt # 在接收端 md5sum received_sequence.txt如果哈希不一致常见原因是传输过程中 WR 的remote_addr偏移算错或者 MR 注册范围没覆盖到实际写入区域。还有一种隐蔽情况是 CQ 轮询时误读了旧的 WC导致以为某块完成了实际没有。排查时可以在每次ibv_poll_cq后打印wc.wr_id和wc.opcode确认对应关系。5. 避坑与排查RDMA 编程中那些让人抓狂的报错5.1 ibv_modify_qp 返回 EINVAL现象调用ibv_modify_qp迁移 QP 状态时返回 -1errno 为 EINVAL。原因属性掩码和设置的字段不匹配。比如设置了attr.path_mtu但掩码里没加IBV_QP_PATH_MTU或者迁移到 RTR 时没提供IBV_QP_AV。解决对照ibv_modify_qp的手册页确认每个状态需要的属性掩码。RESET→INIT 需要IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGSINIT→RTR 需要加上IBV_QP_AV | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN | IBV_QP_RQ_PSNRTR→RTS 需要IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN。5.2 WC 返回 IBV_WC_REM_ACCESS_ERR现象RDMA Write 或 Read 完成后WC 状态是IBV_WC_REM_ACCESS_ERR。原因对端 MR 没有设置对应的 access flag。比如你做 RDMA Write但对端 MR 只注册了IBV_ACCESS_REMOTE_READ。解决检查对端ibv_reg_mr的 flags。RDMA Write 需要IBV_ACCESS_REMOTE_WRITERDMA Read 需要IBV_ACCESS_REMOTE_READ。另外确认remote_addr和length没有超出 MR 注册范围。5.3 SoftRoCE 下 ibv_devices 看不到设备现象ibv_devices输出为空或者只有rxe0但状态不是 ACTIVE。原因rdma_rxe模块没加载或者绑定的网络接口不对。解决先sudo modprobe rdma_rxe然后sudo rdma link add rxe0 type rxe netdev 你的网卡名。用ip addr确认网卡名用rdma link show确认状态。如果还是不行检查内核是否编译了CONFIG_RDMA_RXE。5.4 CQ 轮询死循环现象ibv_poll_cq一直返回 0程序卡死。原因WR 提交失败但没检查ibv_post_send的返回值或者 QP 状态不对导致 WR 根本没发出去。解决每次ibv_post_send后检查返回值和bad_wr。确认 QP 已经迁移到 RTS 状态。如果用的是IBV_SEND_SIGNALED确认没有漏设这个 flag否则完成事件不会进 CQ。5.5 传输大文件时性能骤降现象小文件传输正常大文件传输时带宽远低于预期。原因每次只提交一个 WR 就等待完成没有利用流水线。或者 MR 注册的内存不是对齐的导致硬件效率低。解决批量提交 WR用IBV_SEND_SIGNALED只标记每批的最后一个。MR 注册时用posix_memalign做 4KB 对齐。另外确认path_mtu设成了链路支持的最大值。6. 进阶技巧用 rdma-common 封装自己的 RDMA 模块把 02 目录的rdma-common.c/h抽出来稍作修改就能作为自己项目的基础模块。我一般会做这几件事第一把资源创建和销毁做成对称的init/destroy对确保任何一步失败都能安全回滚。resources_destroy里释放顺序要和创建顺序相反先销毁 QP再销毁 CQ然后注销 MR释放 PD最后关闭设备。第二把连接参数交换抽象成一个结构体struct rdma_conn_info { uint32_t qpn; uint32_t rkey; uint64_t addr; uint16_t lid; union ibv_gid gid; };这样无论是走 CM 还是手动 TCP 交换上层代码不用改。第三加一个带超时的 CQ 轮询函数避免死等int poll_cq_timeout(struct ibv_cq *cq, struct ibv_wc *wc, int timeout_ms) { struct timespec start, now; clock_gettime(CLOCK_MONOTONIC, start); while (1) { int ne ibv_poll_cq(cq, 1, wc); if (ne 0) return ne; clock_gettime(CLOCK_MONOTONIC, now); long elapsed (now.tv_sec - start.tv_sec) * 1000 (now.tv_nsec - start.tv_nsec) / 1000000; if (elapsed timeout_ms) return 0; } }第四在rdma-common.h里用宏定义把 MTU、CQ 深度、最大 WR 数等参数集中管理方便不同场景调参。比如低延迟场景把 CQ 深度设小、MTU 设大高吞吐场景把 CQ 深度设大、批量提交 WR。验证封装是否可靠我习惯用 perftest 做基准对比先用ib_write_bw跑出链路极限再用自己的代码跑同样的消息大小和队列深度看能到基准的百分之多少。如果差距超过 20%通常是 WR 提交策略或者 CQ 轮询效率有问题。从那以后我每次拿到新的 RDMA 代码都强制先跑一遍ibv_devices和ib_write_bw确认环境没问题再编译业务代码。这个习惯帮我省掉了至少一半的无效调试时间。希望帮到你。本文还有配套的精品资源点击获取