☰
RDMA源码实战:从verbs API到内核驱动与性能调优
2026/10/7 13:29:49 网站建设 项目流程

简介:这份资源是面向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决定队列深度,设太小会在高并发时收到-ENOMEM;max_send_sge是单个请求能带的散列表项数,做零拷贝大块传输时要按实际分片数调大。IBV_ACCESS_REMOTE_WRITE这类权限标志直接对应硬件里的内存保护表,多给权限会扩大误写风险,少给则对端 RDMA WRITE 会失败。

2.3 从源码看 QP 状态机:为什么必须按顺序 modify

ibv_modify_qp是 RDMA 源码里最容易被误用的函数。QP 状态机在drivers/infiniband/core/verbs.c里有明确约束:RESET 到 INIT 要指定端口和 pkey,INIT 到 RTR 要填对端的dest_qp_num、rq_psn、path_mtu,RTR 到 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 返回 NULL,errno 是 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_WRITE,WRITE 操作会被硬件拒绝,但错误可能只在 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 源码里的参数不是设了就生效,硬件和驱动都有自己的脾气,多查多验比盲目调参靠谱。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询