2. RDMA编程模型:Verbs API概览、同步与异步模型、事件通知机制详解
好,咱们直接进入正题。RDMA编程,说白了就是跟网卡硬件打交道。你写的代码不是在CPU上跑逻辑,而是在指挥网卡直接搬数据。这个过程中,最核心的接口就是Verbs API。我刚开始接触这玩意儿的时候,看着那一堆ibv_开头的函数,说实话有点懵。但用久了你会发现,它其实就那几板斧。
2.1 Verbs API概览:你绕不开的那些函数
Verbs API是RDMA编程的基石。它定义了你如何创建连接、注册内存、发起数据传输。我个人习惯把Verbs API分成三大类:
- 资源管理类:创建PD(保护域)、MR(内存注册)、CQ(完成队列)、QP(队列对)。这些是基础设施,就像盖房子要先打地基。
- 连接管理类:建立QP状态机,从RESET到INIT,再到RTR和RTS。说白了就是让两端网卡互相认识。
- 数据传输类:ibv_post_send和ibv_post_recv。这是真正干活的地方。
举个例子,一个最简单的发送流程:
// 1. 注册内存
struct ibv_mr *mr = ibv_reg_mr(pd, buf, size, IBV_ACCESS_LOCAL_WRITE);
// 2. 准备发送WR
struct ibv_send_wr wr;
struct ibv_sge sge;
memset(&wr, 0, sizeof(wr));
wr.wr_id = 1;
wr.opcode = IBV_WR_SEND;
wr.send_flags = IBV_SEND_SIGNALED;
wr.sg_list = &sge;
wr.num_sge = 1;
sge.addr = (uint64_t)buf;
sge.length = size;
sge.lkey = mr->lkey;
// 3. 提交发送请求
struct ibv_send_wr *bad_wr;
ibv_post_send(qp, &wr, &bad_wr);
嗯,这里要注意。ibv_post_send是异步的,它只是把请求扔到了网卡的发送队列里。至于数据什么时候真的发出去了,你得去CQ里捞完成事件。
2.2 同步模型 vs 异步模型:别被名字骗了
很多新手会问:「RDMA到底支不支持同步?」我的回答是:Verbs API本身没有同步接口。所有数据传输都是异步的。所谓的同步模型,其实是你在用户态主动轮询CQ,等完成事件回来。
我见过不少项目,一开始图省事,用轮询的方式写:
// 同步轮询风格
ibv_post_send(qp, &wr, NULL);
while (1) {
struct ibv_wc wc;
int ne = ibv_poll_cq(cq, 1, &wc);
if (ne > 0) {
// 发送完成
break;
}
}
这种写法,说白了就是「假装同步」。CPU一直在空转,浪费了RDMA最大的优势——CPU卸载。你想想看,RDMA本来就是为了让CPU去干别的事,结果你在这儿死等,那还不如用传统TCP呢。
真正的异步模型长这样:
// 异步事件驱动风格
ibv_post_send(qp, &wr, NULL);
// 立刻返回,CPU去处理其他任务
do_other_work();
// 稍后通过事件通知或轮询来检查完成
我个人建议,生产环境一定要用异步模型。把CQ的轮询交给一个专门的线程,或者用事件通知机制来唤醒。这样CPU利用率能上去,延迟也能降下来。
2.3 事件通知机制详解:别让CPU空转
事件通知,就是让网卡在完成数据传输后主动通知你,而不是你一遍遍去问。Verbs API提供了两种方式:
| 机制 | 描述 | 适用场景 |
|---|---|---|
| ibv_poll_cq | 主动轮询CQ,非阻塞 | 低延迟、高频次场景 |
| ibv_req_notify_cq | 请求CQ事件通知,阻塞等待 | CPU资源紧张、低频次场景 |
我曾经在一个项目中踩过坑。当时为了追求极致延迟,用了纯轮询。结果CPU跑到100%,业务方投诉说其他服务都被饿死了。后来改成混合模式:平时轮询,空闲时切到事件通知。这样延迟只增加了不到1微秒,CPU占用却从100%降到了30%。
事件通知的典型用法:
// 请求通知
ibv_req_notify_cq(cq, 0);
// 然后阻塞等待
struct ibv_cq *ev_cq;
void *ev_ctx;
ibv_get_cq_event(channel, &ev_cq, &ev_ctx);
// 拿到事件后,再轮询CQ获取具体的完成项
struct ibv_wc wc;
ibv_poll_cq(cq, 1, &wc);
// 别忘了确认事件
ibv_ack_cq_events(cq, 1);
这里有个坑,我提醒一下:ibv_get_cq_event是阻塞的。如果你在中断上下文或者信号处理函数里调它,可能会出问题。另外,事件通知和轮询可以混用,但要注意竞态条件。我曾经遇到过事件通知来了,但轮询时CQ里啥也没有——那是因为另一个线程已经把完成项取走了。
2.4 实战建议:怎么选模型?
说了这么多,到底怎么选?我根据经验给个参考:
- 延迟敏感型(如高频交易):纯轮询。CPU核多,可以独占一个核跑轮询线程。
- 吞吐优先型(如分布式存储):事件通知 + 批量轮询。一次事件唤醒后,把CQ里的完成项一次性取完。
- 混合型(大多数场景):自适应策略。轮询一段时间没事件,就切到事件通知等。
你想想看,RDMA编程其实没那么玄乎。核心就是理解Verbs API的套路,然后根据业务场景选对模型。我见过太多人一上来就追求「纯异步」、「零拷贝」,结果代码写得复杂无比,性能却没上去。其实,先把基础API用对,比什么都重要。
好,这一章就到这儿。下一章我们会深入QP状态机,聊聊连接建立的细节。那个坑更多,到时候我慢慢讲。