RDMA 基础概念

一、基础概念

1. QP — Queue Pair

RDMA 通信的基础,其中包含了一个 RQ 和一个 SQ。基于连接的 QP 只能一对一交流,而基于数据报的非连接通信能实现 QP 一对多通信。

2. RQ — Receive Queue

存储了接收的 WQE。

3. SQ — Send Queue

存储了发送的 WQE。

4. CQ — Completion Queue

存放通信完成的信号,其中存放的每个实体较说 CQE — Completion Queue Entry。

5. WQE — Work Queue Elements

用于下发操作指令,一般是放置在 RQ 或者 SQ 中,其大小受多种因素影响。

6. MR — Message Region

用于记录从主机内存中分配来的内存,可用于之后传输数据和接收数据使用。

二、动作原语 (Verbs)

1. 内存交互原语

  • Read
  • Write

这类原语需要提前知道对端主机里要被操作的内存地址(相对地址,基于virtual address),同时它无需对端主机 CPU 的参与。

2. 消息交互原语

  • Send
  • Receive

这类原语无需知道地址,Send 操作会将消息写到对端通过 Receive 操作声明的内存中,对端声明地址时需要 CPU 参与。

三、传输方式

1. 基于连接

RC — Reliable Connected

NIC 会使用确认机制来保证按顺序传递消息。

UC — Unreliable Connected

不可靠连接不保证消息顺序,但 InfiniBand 提供了链路层的流控制,保证极少丢包。

2. 无连接的

UD — Unreliable Datagram

使用较少,但可以实现广播

3. 对 Verbs 的支持

![RDMA 传输方式](/images/rdma/Untitled 30.png)

四、传输流程

这里使用 Send 和 Recv 作为示例进行介绍,常规 RDMA 编程还需提前建立连接、协商地址等过程,这里进行了省略,详细可参考 RDMA_CA 的使用。

1. 开辟内存空间

RDMA 操作开始于“搞”内存。当你在对内存进行操作的时候,就是告诉内核这段内存名花有主了,主人就是你的应用程序。因此我们要先注册一个内存区域(MR)。一旦 MR 注册完毕,我们就可以使用这段内存来做任何 RDMA 操作。

1
2
3
4
5
6
7
8
9
struct ibv_mr {
struct ibv_context *context;
struct ibv_pd *pd;
void *addr;
size_t length;
uint32_t handle;
uint32_t lkey; // 本地操作的凭据
uint32_t rkey; // 远端操作的凭据
};

2. 创建 WQE (send / recv)

有了内存下面就可以进行传输了,首先发送端应该在 SQ 中创建一个 WQE 或者叫 Work Request,并使用 sg_list 指定需要传输的缓冲区地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct ibv_send_wr {
uint64_t wr_id; /* User defined WR ID */
struct ibv_send_wr *next; /* Pointer to next WR in list, NULL if last WR */
struct ibv_sge *sg_list; /* Pointer to the s/g array */
int num_sge; /* Size of the s/g array */
enum ibv_wr_opcode opcode; /* Operation type */
int send_flags; /* Flags of the WR properties */
uint32_t imm_data; /* Immediate data (in network byte order) */
union {
struct {
uint64_t remote_addr; /* Start address of remote memory buffer */
uint32_t rkey; /* Key of the remote Memory Region */
} rdma;
struct {
uint64_t remote_addr; /* Start address of remote memory buffer */
uint64_t compare_add; /* Compare operand */
uint64_t swap; /* Swap operand */
uint32_t rkey; /* Key of the remote Memory Region */
} atomic;
struct {
struct ibv_ah *ah; /* Address handle (AH) for the remote node address */
uint32_t remote_qpn; /* QP number of the destination QP */
uint32_t remote_qkey; /* Q_Key number of the destination QP */
} ud;
} wr;
};

对应的接收端也要创建一个 WQE 用于接收。

1
2
3
4
5
6
struct ibv_recv_wr {
uint64_t wr_id;
struct ibv_recv_wr *next;
struct ibv_sge *sg_list;
int num_sge;
};

RDMA编程中,SGL(Scatter/Gather List)是最基本的数据组织形式。 SGL 是一个数组,该数组中的元素被称之为 SGE(Scatter/Gather Element),每一个 SGE 就是一个 Data Segment (数据段)。RDMA 支持 Scatter/Gather 操作,具体来讲就是 RDMA 可以支持一个连续的 Buffer 空间,进行 Scatter 分散到多个目的主机的不连续的 Buffer 空间。Gather 指的就是多个不连续的 Buffer 空间可以 Gather 到目的主机的一段连续的 Buffer 空间。

1
2
3
4
5
6
struct ibv_sge {
uint64_t addr; // 数据段所在的虚拟内存的起始
// 地址 (Virtual Address of the Data Segment (i.e. Buffer))
uint32_t length; // 数据段长度(Length of the Data Segment)
uint32_t lkey; // 该数据段对应的L_Key (Key of the local Memory Region)
}

3. 添加任务

之后需要将任务添加到对应的 QP 中,让网卡去执行,需要使用 ibv_post_send() 函数

1
2
3
int ibv_post_send(struct ibv_qp *qp, 
struct ibv_send_wr *wr,
struct ibv_send_wr **bad_wr);

4. 网卡执行

根据 sg_list 字段网卡驱动便可以知道需要操作的内存地址,之后即可进行内存操作,同时为了防止不同主机操作本不属于它的内存,又出现了 PD 这个概念。通过 PD 可以将 MR 与 QP 进行绑定,通过网卡驱动硬件上防止内存被错误访问到。

![Protection Domain](/images/rdma/Untitled 31.png)

网卡检验 PD 后便可以通过 SGL 组合内存,进行数据传输。

![Gather Step of RDMA](/images/rdma/Untitled 32.png)