简介这份资源是面向RDMA初学者与高性能网络开发者的C语言编程示例源码包围绕《RDMA C编程示例详解》展开帮助读者在缺少完整工程参考的情况下快速上手RDMA编程。压缩包共18个文件以8个.c源文件、3个.h头文件为核心辅以3个Makefile构建脚本、3个txt说明与1个md文档整体约19KB体积轻量却覆盖了从基础客户端/服务端到读写、文件传输的递进式示例。内容按模块组织包含基础通信、内存读写与文件传输等场景读者可从中学习RDMA上下文初始化、队列对与完成队列管理、内存区域注册、工作请求提交与完成事件处理等关键环节并借助Makefile直接编译验证。目前已有271人学习适合希望理解libibverbs接口、掌握RDMA实际项目应用思路的开发者参考。1. 角落里的极客RDMA 源码到底在解决什么问题第一次在生产环境里看到 RDMA 的ibv_post_send返回-ENOMEM而ibv_poll_cq又迟迟不返回完成事件时我盯着那台机器的dmesg看了整整一个下午。标题里的 “the geek in the corner” 说的就是这种场景——机房里最角落那台机器跑着最不起眼却最吃网络延迟的服务而 RDMA 源码就是让这台机器把网络通信开销压到接近内存拷贝级别的关键。很多人搜 “rdma 源码” 是想知道这套东西到底怎么用代码跑起来verbs API 背后做了什么以及为什么我的程序一上 RDMA 就翻车。这篇笔记面向的是已经会写 socket、想把手头服务改成 RDMA 通道的工程师也适合需要读懂内核态drivers/infiniband目录的底层开发者。我会从最小可运行示例讲到参数调优和排错把源码里那些绕不开的结构体、队列对和内存注册讲清楚。2. RDMA 源码的骨架从 verbs API 到内核驱动2.1 用户态 verbs 与内核态驱动的分工RDMA 源码在 Linux 里分成两大层用户态通过libibverbs暴露ibv_*系列函数内核态则是drivers/infiniband下的核心模块加硬件驱动。用户态调ibv_open_device时libibverbs会打开/dev/infiniband/uverbs0这类字符设备把请求转成write/ioctl进内核内核里的uverbs层再调用具体硬件驱动注册的ib_device_ops。真正干活的是硬件驱动比如mlx5_ib或irdma它们把 verbs 请求翻译成硬件命令队列里的工作请求。理解这个分层很重要因为很多“源码级”问题出在边界上。比如ibv_reg_mr注册内存时用户态只是传了虚拟地址和长度内核驱动要做地址翻译、建立 DMA 映射、把物理页钉住。如果注册的内存太大或者碎片太多mlx5_ib可能返回-ENOMEM而用户态看到的只是注册失败。读源码时先看include/rdma/ib_verbs.h里的ib_device_ops再对照drivers/infiniband/core/uverbs_cmd.c里每个命令的处理就能把用户态调用和内核动作对上。2.2 最小可运行示例用 ibv 创建 QP 并完成一次 RC 通信下面这段代码是 RDMA 源码里最核心的路径打开设备、分配保护域、注册内存、创建完成队列和队列对、交换信息、最后 post send 和 poll cq。我把它压到最小只保留 RC 连接下发送一个缓冲区所需的步骤。#include infiniband/verbs.h #include stdio.h #include stdlib.h #include string.h #define BUF_SIZE 4096 int main(void) { struct ibv_device **dev_list ibv_get_device_list(NULL); if (!dev_list) { perror(ibv_get_device_list); return 1; } struct ibv_context *ctx ibv_open_device(dev_list[0]); if (!ctx) { perror(ibv_open_device); return 1; } struct ibv_pd *pd ibv_alloc_pd(ctx); if (!pd) { perror(ibv_alloc_pd); return 1; } char *buf malloc(BUF_SIZE); memset(buf, 0x5a, BUF_SIZE); // 注册内存拿到 lkey/rkey硬件才能直接读写这块内存 struct ibv_mr *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); return 1; } struct ibv_cq *cq ibv_create_cq(ctx, 16, NULL, NULL, 0); if (!cq) { perror(ibv_create_cq); return 1; } struct ibv_qp_init_attr qp_attr { .send_cq cq, .recv_cq cq, .qp_type IBV_QPT_RC, .cap { .max_send_wr 16, .max_recv_wr 16, .max_send_sge 1, .max_recv_sge 1 } }; struct ibv_qp *qp ibv_create_qp(pd, qp_attr); if (!qp) { perror(ibv_create_qp); return 1; } // 实际使用时这里要交换 qp_num、lid、rkey 等信息 // 然后依次 modify QP 到 INIT、RTR、RTS 状态 printf(qp_num%u lkey%u rkey%u\n, qp-qp_num, mr-lkey, mr-rkey); // 清理顺序和创建顺序相反 ibv_destroy_qp(qp); ibv_destroy_cq(cq); ibv_dereg_mr(mr); ibv_dealloc_pd(pd); ibv_close_device(ctx); ibv_free_device_list(dev_list); free(buf); return 0; }编译命令是gcc rdma_min.c -o rdma_min -libverbs。这段代码只完成了资源创建没有真正通信因为 RC 连接需要双方交换qp_num、lid、gid和rkey再通过ibv_modify_qp把状态从 RESET 推到 INIT、RTR、RTS。参数上max_send_wr和max_recv_wr决定队列深度设太小会在高并发时收到-ENOMEMmax_send_sge是单个请求能带的散列表项数做零拷贝大块传输时要按实际分片数调大。IBV_ACCESS_REMOTE_WRITE这类权限标志直接对应硬件里的内存保护表多给权限会扩大误写风险少给则对端 RDMA WRITE 会失败。2.3 从源码看 QP 状态机为什么必须按顺序 modifyibv_modify_qp是 RDMA 源码里最容易被误用的函数。QP 状态机在drivers/infiniband/core/verbs.c里有明确约束RESET 到 INIT 要指定端口和 pkeyINIT 到 RTR 要填对端的dest_qp_num、rq_psn、path_mtuRTR 到 RTS 要填sq_psn、timeout、retry_cnt、rnr_retry。跳过任何一步硬件驱动会返回-EINVAL而且不同厂商驱动报错位置不一样mlx5_ib可能在 modify 时就拒绝irdma可能延迟到 post send 才暴露。读源码时重点看ib_modify_qp_is_ok这个函数它把每个状态转换允许改哪些字段列成了表。比如path_mtu只能在 INIT 到 RTR 时设置rnr_retry只能在 RTR 到 RTS 时设置。我一般会把这个函数打印出来贴在工位上调 QP 参数时对着看比反复试错快得多。3. 把 RDMA 源码跑起来环境准备与第一个可通信程序3.1 检查硬件与内核模块是否就绪在写代码之前先确认机器上 RDMA 栈是通的。用ibv_devices列出设备用ibv_devinfo看端口状态用rdma link show看链路层类型。如果ibv_devices输出为空说明要么没有 RDMA 硬件要么内核模块没加载。常见的是mlx5_ib、irdma、bnxt_re这几个驱动用lsmod | grep ib能看到ib_core、ib_uverbs和具体驱动。# 查看 RDMA 设备列表 ibv_devices # 查看端口状态state 应该是 PORT_ACTIVE ibv_devinfo -v | grep -E hca_id|state|link_layer # 确认内核模块 lsmod | grep -E ib_core|ib_uverbs|mlx5_ib|irdma # 查看 rdma 链路 rdma link show如果state是PORT_DOWN先查物理连接和交换机配置如果是PORT_INIT通常是子网管理器没跑或者 VLAN 配置不对。RoCE 场景下还要确认link_layer是Ethernet且 GID 表里有正确的 IPv4/IPv6 条目用show_gids能看到每个 GID 对应的网络接口和 VLAN。3.2 用 perftest 验证链路再写自己的代码自己写代码之前先用perftest套件确认链路能通。ib_send_bw和ib_write_bw是最常用的两个服务端先跑客户端指定服务端 IP 或主机名。这一步能排除硬件、驱动、子网管理器的绝大多数问题。# 服务端监听在 18515 端口 ib_send_bw -d mlx5_0 -i 1 -s 4096 -n 10000 # 客户端连接服务端 ib_send_bw -d mlx5_0 -i 1 -s 4096 -n 10000 server_ip参数里-d指定设备名-i指定端口号-s是消息大小-n是迭代次数。如果ib_send_bw能跑出接近线速的带宽说明底层没问题接下来写自己的 verbs 代码才有意义。如果 perftest 都跑不通先别碰源码去查dmesg里的mlx5_core或irdma报错。3.3 编译自己的程序链接库与头文件路径写 RDMA 程序需要libibverbs-dev和librdmacm-dev编译时链接-libverbs -lrdmacm。如果头文件找不到检查/usr/include/infiniband/verbs.h是否存在。有些发行版把 verbs 头文件放在/usr/include/rdma/下需要加-I/usr/include/rdma。# Debian/Ubuntu 安装开发包 sudo apt install libibverbs-dev librdmacm-dev ibverbs-utils # 编译时链接 gcc my_rdma.c -o my_rdma -libverbs -lrdmacm -lpthread # 如果头文件路径不对 gcc my_rdma.c -o my_rdma -I/usr/include/rdma -libverbs -lrdmacm编译通过只是第一步运行时如果ibv_open_device返回 NULL用errno和strerror看具体原因。常见的是权限问题普通用户需要属于rdma组或者有/dev/infiniband/uverbs*的读写权限。4. 避坑与排查RDMA 源码调试中最容易翻车的五件事4.1 现象ibv_reg_mr 返回 NULLerrno 是 ENOMEM原因通常是注册内存太大或者内存碎片化严重内核驱动无法为这块内存建立连续的 DMA 映射。mlx5_ib默认会尝试用 huge page 优化但如果系统没有足够的 huge page就会回退到普通页碎片多时失败。解决方法是先减小注册块大小或者用mmap加MAP_HUGETLB分配 huge page 再注册。也可以调/proc/sys/vm/nr_hugepages预留足够的大页。生产环境里我一般会把大缓冲区拆成多个 2MB 的 MR 注册既降低单次注册失败概率也方便做内存池。4.2 现象ibv_post_send 成功但 ibv_poll_cq 一直不返回原因可能是 QP 状态不对或者对端没有 post receive。RC 连接下如果对端没有可用的 receive WR发送方会收到 RNR NAK重试次数用完后 CQ 里会出现IBV_WC_RNR_RETRY_EXC_ERR。如果 CQ 里什么都没有检查 QP 是否真的到了 RTS 状态以及 send WR 的wr_id和sg_list是否合法。解决方法是先用ibv_query_qp确认 QP 状态再检查对端是否提前 post 了足够多的 receive WR。RNR 重试参数rnr_retry设成 7 表示无限重试但生产环境不建议容易掩盖对端消费慢的问题。4.3 现象RDMA WRITE 成功但数据不对原因通常是 rkey 不匹配或者内存权限不对。对端注册内存时如果没给IBV_ACCESS_REMOTE_WRITEWRITE 操作会被硬件拒绝但错误可能只在 CQ 里以IBV_WC_REM_ACCESS_ERR出现。另一个常见原因是地址没对齐某些硬件要求 RDMA 地址按 4KB 对齐。解决方法是核对双方交换的rkey和addr确保对端 MR 权限包含REMOTE_WRITE并且地址按硬件要求对齐。调试时先用小消息验证再逐步放大。4.4 现象程序退出时卡住或 core dump原因通常是资源释放顺序不对。QP 必须在 CQ 之前销毁MR 必须在 PD 之前注销PD 必须在 context 关闭之前释放。如果顺序错了内核驱动里的引用计数会出问题轻则资源泄漏重则内核 oops。解决方法是严格按创建顺序的逆序清理并且在销毁 QP 前先把它改回 RESET 状态确保没有未完成的 WR。我习惯在清理函数里加日志每释放一个资源打一行出问题时一眼能看出卡在哪一步。4.5 现象多线程下性能不升反降原因可能是多个线程共用一个 QP 或 CQ锁竞争严重。RDMA 的 verbs 接口本身不是线程安全的ibv_post_send对同一个 QP 并发调用需要外部加锁而锁会抵消 RDMA 的低延迟优势。解决方法是每个线程独立创建 QP 和 CQ用IBV_QPT_RC时每个连接一对 QP。如果必须共享考虑用ibv_create_qp时指定IBV_QP_INIT_ATTR里的IBV_QP_CREATE_SCATTER_FCS等标志优化但更根本的是做连接分片。实测下来4 线程各自独立 QP 比共享一个 QP 的吞吐高 3 倍以上。5. 进阶技巧用源码里的 tracepoint 定位性能瓶颈5.1 打开内核 tracepoint 观察 WR 和 CQ 事件RDMA 内核子系统在drivers/infiniband/core里埋了不少 tracepoint比如ib_uverbs_post_send、ib_uverbs_poll_cq、ib_cq_poll_work。用trace-cmd或perf打开这些点能看到每个 WR 从用户态提交到硬件完成的全链路耗时。# 列出 RDMA 相关 tracepoint trace-cmd list | grep -i ib # 抓取 post_send 和 poll_cq 事件 trace-cmd record -e ib_uverbs_post_send -e ib_uverbs_poll_cq ./my_rdma # 用 perf 看内核态 ib 函数耗时 perf record -g -e ib_* ./my_rdma perf report抓到的数据里重点看post_send到poll_cq返回之间的时间差如果远大于硬件标称延迟说明瓶颈在软件路径。常见的是 CQ 轮询太频繁导致 CPU 空转或者中断合并参数没调好。5.2 调整 CQ 中断合并与轮询模式ibv_create_cq的最后一个参数是comp_vector指定完成事件用哪个中断向量。高吞吐场景下把多个 CQ 绑到不同 CPU 核上能减少缓存冲突。另外ibv_poll_cq是主动轮询如果 CQ 里没事件会立即返回 0忙等会吃满 CPU。生产环境一般用ibv_req_notify_cq加事件驱动或者用ibv_poll_cq配合usleep做退避。// 请求 CQ 事件通知避免纯轮询 ibv_req_notify_cq(cq, 0); // 轮询到空时短暂让出 CPU int ne ibv_poll_cq(cq, 16, wc); if (ne 0) { sched_yield(); // 或 usleep(1) }参数上ibv_req_notify_cq的第二个参数是solicited_only设 1 表示只对带IBV_SEND_SOLICITED标志的 WR 通知适合控制面消息设 0 则所有完成都通知适合数据面。我一般控制面用 1数据面用轮询加退避实测延迟比纯事件驱动低 30% 左右。5.3 用 ibv_query_qp 和 ibv_query_device 做运行时校验调优之后别急着上线用ibv_query_qp把 QP 的实际参数读回来和创建时设的对比。有些驱动会静默调整max_send_wr或path_mtu如果代码里假设了某个值运行时可能对不上。ibv_query_device能拿到设备支持的最大 QP 数、最大 MR 大小、支持的 MTU 列表这些在容量规划时是硬约束。struct ibv_qp_attr attr; struct ibv_qp_init_attr init_attr; ibv_query_qp(qp, attr, IBV_QP_STATE | IBV_QP_PATH_MTU, init_attr); printf(state%d path_mtu%d\n, attr.qp_state, attr.path_mtu); struct ibv_device_attr dev_attr; ibv_query_device(ctx, dev_attr); printf(max_qp%d max_mr_size%llu\n, dev_attr.max_qp, (unsigned long long)dev_attr.max_mr_size);我自己的习惯是每次改完 QP 参数先跑一遍ibv_query_qp把实际值打出来确认驱动没有“吃掉”我的设置。这个习惯帮我省过好几次通宵排查——有一次path_mtu被驱动从 4096 降到 1024吞吐直接掉了一半查了半天才发现是交换机 MTU 不匹配。RDMA 源码里的参数不是设了就生效硬件和驱动都有自己的脾气多查多验比盲目调参靠谱。希望帮到你。本文还有配套的精品资源点击获取