# RDMA 基本元素
本文欢迎非商业转载,转载请注明出处。
声明:仅用于收藏,便于阅读
― Savir, 知乎专栏:3. RDMA 基本元素
RDMA 技术中经常使用缩略语,很容易让刚接触的人一头雾水,本篇的目的是讲解 RDMA 中最基本的元素及其含义。
我将常见的缩略语对照表写在前面,阅读的时候如果忘记了可以翻到前面查阅。
# WQ
Work Queue 简称 WQ,是 RDMA 技术中最重要的概念之一。WQ 是一个储存工作请求的队列,为了讲清楚 WQ 是什么,我们先介绍这个队列中的元素 WQE(Work Queue Element,工作队列元素)。
# WQE
WQE 可以认为是一种“任务说明”,这个工作请求是软件下发给硬件的,这份说明中包含了软件所希望硬件去做的任务以及有关这个任务的详细信息。比如,某一份任务是这样的:“我想把位于地址 0x12345678 的长度为 10 字节的数据发送给对面的节点”,硬件接到任务之后,就会通过 DMA 去内存中取数据,组装数据包,然后发送。
WQE 的含义应该比较明确了,那么我们最开始提到的 WQ 是什么呢?它就是用来存放“任务书”的“文件夹”,WQ 里面可以容纳很多 WQE。有数据结构基础的读者应该都了解,队列是一种先进先出的数据结构,在计算机系统中非常常见,我们可以用下图表示上文中描述的 WQ 和 WQE 的关系:
WQ 这个队列总是由软件向其中增加 WQE(入队),硬件从中取出 WQE,这就是软件给硬件“下发任务”的过程。为什么用队列而不是栈?因为进行“存”和“取“操作的分别是软件和硬件,并且需要保证用户的请求按照顺序被处理在 RDMA 技术中,所有的通信请求都要按照上图这种方式告知硬件,这种方式常被称为“Post”。
# QP
Queue Pair 简称 QP,就是“一对”WQ 的意思。
# SQ 和 RQ
任何通信过程都要有收发两端,QP 就是一个发送工作队列和一个接受工作队列的组合,这两个队列分别称为 SQ(Send Queue)和 RQ(Receive Queue)。我们再把上面的图丰富一下,左边是发送端,右边是接收端:
WQ 怎么不见了?SQ 和 RQ 都是 WQ,WQ 只是表示一种可以存储 WQE 的单元,SQ 和 RQ 才是实例。
SQ 专门用来存放发送任务,RQ 专门用来存放接收任务。在一次 SEND-RECV 流程中,发送端需要把表示一次发送任务的 WQE 放到 SQ 里面。同样的,接收端软件需要给硬件下发一个表示接收任务的 WQE,这样硬件才知道收到数据之后放到内存中的哪个位置。上文我们提到的 Post 操作,对于 SQ 来说称为 Post Send,对于 RQ 来说称为 Post Receive。
需要注意的是,在 RDMA 技术中通信的基本单元是 QP,而不是节点。如下图所示,对于每个节点来说,每个进程都可以使用若干个 QP,而每个本地 QP 可以“关联”一个远端的 QP。我们用“节点 A 给节点 B 发送数据”并不足以完整的描述一次 RDMA 通信,而应该是类似于“节点 A 上的 QP3 给节点 C 上的 QP4 发送数据”。
每个节点的每个 QP 都有一个唯一的编号,称为 QPN(Queue Pair Number),通过 QPN 可以唯一确定一个节点上的 QP。
# SRQ
Shared Receive Queue 简称 SRQ,意为共享接收队列。概念很好理解,就是一种几个 QP 共享同一个 RQ 时,我们称其为 SRQ。以后我们会了解到,使用 RQ 的情况要远远小于使用 SQ,而每个队列都是要消耗内存资源的。当我们需要使用大量的 QP 时,可以通过 SRQ 来节省内存。如下图所示,QP2~QP4 一起使用同一个 RQ:
# CQ
Completion Queue 简称 CQ,意为完成队列。跟 WQ 一样,我们先介绍 CQ 这个队列当中的元素——CQE(Completion Queue Element)。可以认为 CQE 跟 WQE 是相反的概念,如果 WQE 是软件下发给硬件的“任务书”的话,那么 CQE 就是硬件完成任务之后返回给软件的“任务报告”。CQE 中描述了某个任务是被正确无误的执行,还是遇到了错误,如果遇到了错误,那么错误的原因是什么。
而 CQ 就是承载 CQE 的容器——一个先进先出的队列。我们把表示 WQ 和 WQE 关系的图倒过来画,就得到了 CQ 和 CQE 的关系:
每个 CQE 都包含某个 WQE 的完成信息,他们的关系如下图所示:
下面我们把 CQ 和 WQ(QP)放在一起,看一下一次 SEND-RECV 操作中,软硬件的互动(图中序号顺序不表示实际时序):
2022/5/23:下图及后面的列表顺序有修改,将原来第 2 条的“接收端硬件从 RQ 中拿到任务书,准备接收数据”移动到“接收端收到数据,进行校验后回复 ACK 报文给发送端”之后,并且修改了描述,现在为第 6 条。
这里我犯了错误的点是 RQ 和 SQ 不同,是一个“被动接收”的过程,只有收到 Send 报文(或者带立即数的 Write 报文)时硬件才会消耗 RQ WQE。感谢 @连接改变世界 的指正。
- 接收端 APP 以 WQE 的形式下发一次 RECV 任务到 RQ。
- 发送端 APP 以 WQE 的形式下发一次 SEND 任务到 SQ。
- 发送端硬件从 SQ 中拿到任务书,从内存中拿到待发送数据,组装数据包。
- 发送端网卡将数据包通过物理链路发送给接收端网卡。
- 接收端收到数据,进行校验后回复 ACK 报文给发送端。
- 接收端硬件从 RQ 中取出一个任务书(WQE)。
- 接收端硬件将数据放到 WQE 中指定的位置,然后生成“任务报告”CQE,放置到 CQ 中。
- 接收端 APP 取得任务完成信息。
- 发送端网卡收到 ACK 后,生成 CQE,放置到 CQ 中。
- 发送端 APP 取得任务完成信息。
注释
NOTE: 需要注意的一点是,上图中的例子是可靠服务类型的交互流程,如果是不可靠服务,那么不会有步骤 5 的 ACK 回复,而且步骤 9 以及之后的步骤会在步骤 5 之后立即触发。关于服务类型以及可靠与不可靠,我们将在《RDMA 基本服务类型》一文中讲解。
至此,通过 WQ 和 CQ 这两种媒介,两端软硬件共同完成了一次收发过程。
# WR 和 WC
说完了几个 Queue 之后,其实还有两个文章开头提到的概念没有解释,那就是 WR 和 WC(不是 Water Closet 的缩写)。
WR 全称为 Work Request,意为工作请求;WC 全称 Work Completion,意为工作完成。这两者其实是 WQE 和 CQE 在用户层的“映射”。因为 APP 是通过调用协议栈接口来完成 RDMA 通信的,WQE 和 CQE 本身并不对用户可见,是驱动中的概念。用户真正通过 API 下发的是 WR,收到的是 WC。
WR/WC 和 WQE/CQE 是相同的概念在不同层次的实体,他们都是“任务书”和“任务报告”。于是我们把前文的两个图又加了点内容:
# 代码示例
最后,下面是一个简单的例子,展示了如何使用 libibverbs 来创建一个 QP,然后通过这个 QP 来发送一次数据。这是一个非常简单的例子,只是为了让读者对上文中的概念有一个直观的认识。
#include <infiniband/verbs.h>
int main() {
struct ibv_context *ctx;
struct ibv_pd *pd;
struct ibv_cq *cq;
struct ibv_qp *qp;
struct ibv_mr *mr;
struct ibv_sge sge;
struct ibv_send_wr wr;
struct ibv_send_wr *bad_wr;
struct ibv_wc wc;
ctx = ibv_open_device();
pd = ibv_alloc_pd(ctx);
cq = ibv_create_cq(ctx, 100, NULL, NULL, 0);
qp = ibv_create_qp(pd, NULL, NULL);
mr = ibv_reg_mr(pd, buf, size, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE);
sge.addr = (uintptr_t)buf;
sge.length = size;
sge.lkey = mr->lkey;
wr.wr_id = 1;
wr.sg_list = &sge;
wr.num_sge = 1;
wr.opcode = IBV_WR_SEND;
wr.send_flags = IBV_SEND_SIGNALED;
wr.next = NULL;
ibv_post_send(qp, &wr, &bad_wr);
ibv_poll_cq(cq, 1, &wc);
return 0;
}
# 总结
好了,我们用 IB 协议[1]3.2.1 中的 Figure 11 这张图总结一下本篇文章的内容:
用户态的 WR,由驱动转化成了 WQE 填写到了 WQ 中,WQ 可以是负责发送的 SQ,也可以是负责接收的 RQ。硬件会从各个 WQ 中取出 WQE,并根据 WQE 中的要求完成发送或者接收任务。任务完成后,会给这个任务生成一个 CQE 填写到 CQ 中。驱动会从 CQ 中取出 CQE,并转换成 WC 返回给用户。
基础概念就介绍到这里,下一篇将介绍 RDMA 的几种常见操作类型。
# 参考文献
[1]《IB Specification Vol 1-Release-1.3-2015-03-03》