RDMA one-side 和 two-side原语
在RDMA传输中,SEND/RECEIVE是双边操作,即需要通信双方的参与,并且RECEIVE要先于SEND执行,这样对方才能发送数据,当然如果对方不需要发送数据,可以不执行RECEIVE操作,因此该过程和传统通信相似,区别在于RDMA的零拷贝网络技术和内核旁路,延迟低,多用于传输短的控制消息。WRITE/READ是单边操作,顾名思义,读/写操作是一方在执行,在实际的通信过程中,WRITE/READ操作是由active即客户端来执行的,而passive即服务器不需要执行任何操作。RDMA WRITE操作中,由客户端把数据从本地buffer中直接push到远程QP的虚拟空间的连续内存块中(物理内存不一定连续),因此需要知道目的地址(remote_addr)和访问权限(remote_key)。RDMA READ操作中,是客户端直接到远程的QP的虚拟空间的连续内存块中获取数据poll到本地目的buffer中,因此需要远程QP的内存地址和访问权限。单边操作多用于批量数据传输。
可以看出,在单边操作过程中,客户端需要知道远程QP的remote_addr(要读取或写入的地址)和remote_key,而这两个信息是通过SEND/REVEIVE操作来交换的,RDMA通信过程的大致流程如下:
1)初始化context,注册内存域
2)建立RDMA连接
3)通过SEND/RECEIVE操作,C/S交换包含RDMA memory region key的MSG_MR消息(一般是客户端先发送)
4)通过WRITE/READ操作,进行数据传输(单边操作)
5)发送MSG_DONE消息,关闭连接
相关函数
1、RDMA_CM API(<rdma/rdma_verbs.h>)中的有关WRITE/READ和SEND/RECEIVE涉及到的函数如下所示
发送RDMA WRITE请求,单边操作
int rdma_post_write(struct rdma_cm_id *id, void *context, void *addr, size_t length,
struct ibv_mr *mr, int flags, uint64_t remote_addr, uint32_t rkey);
- 1
- 2
把写WR发送到和rdma_cm_id关联的QP的SQ中,本地数据缓冲区中的数据就会被写到远程内存中。本地缓冲区和远程内存都是已经注册过的,才能和RDMA设备直接交互。
id就是调用者对应的rdma_cm_id,context就是用户自定义的context,addr是发送者的本地地址,就是发送数据缓冲区数组,这个数组应该不同于send/recv操作对应的缓冲区数组,length就是缓冲区数组长度,mr就是这个发送缓冲区数组对应的mr,也就是说这个发送缓冲区数组是要被RDMA读取的,所以需要注册,flags是发送标志,remote_addr就是远程的地址,即服务器地址,rkey就是访问远程地址的权限。相当于说,我是client,对应id,我要发送的数据在addr放着,长度length,这个发送的数据所在的内存已经在RDMA设备上注册过了,是mr,发送标志是flags,控制写操作,由于我是直接写到服务器,服务器什么都不做,所以我得知道存到哪,而这个地址是通过send/recv交换过的,所以我就把数据放到remote_addr地址处,当然这块地址也是已经注册过的,我有这个地址的钥匙相当于操作权限rkey,这个也是服务器给我的。write就相当于说客户端一个人通过网络把数据直接放到服务器的指定位置,发送之前需要获取对方地址和key。
发送RDMA READ请求,单边操作
int rdma_post_read (struct rdma_cm_id *id, void *context, void *addr, size_t length,
struct ibv_mr *mr, int flags, uint64_t remote_addr, uint32_t rkey);
- 1
- 2
把读WR发送到和rdma_cm_id关联的QP的SQ中,远程的数据将会被读到本地缓冲区中。本地和远程的数据缓冲区都是和RDMA设备直接打交道的,所以需要注册,有对应的mr。
id就是客户端的id,context是用户自定义,NULL应该可以,addr是本地目的地址,相当于说客户端去服务器取数据,存到这个addr中,已经注册过了,是mr,length是读操作的长度,flags是可选的标志位,用来控制读操作,remote_addr是要读取的服务器地址,已经注册过,rkey是和remote_addr相关联的key。
RDMA READ操作是客户端来进行,服务器无需操作,所以客户端需要知道服务器把数据放哪了,就是remote_addr,这个地址已经注册过了,并且返回了key,客户端还得知道这个远程地址的key,就是rkey,拿到数据之后还得存到客户端,存的目的地址就是addr,长度是length,这个目的地址已经注册了,是mr。
发送RDMA SEND请求,双边操作
int rdma_post_send (struct rdma_cm_id *id, void *context, void *addr, size_t length,
struct ibv_mr *mr, int flags);
- 1
- 2
id是本地id,context用户自定义,addr是发送缓冲区地址,length是缓冲区长度,缓冲区注册过了,是mr,flags标志位,控制发送请求。将addr的长度length的数据发到所连接的对方。
发送RDMA RECEIVE请求,双边操作
int rdma_post_recv (struct rdma_cm_id *id, void *context, void *addr, size_t length, struct ibv_mr *mr);
- 1
id是本地id,context用户自定义,addr是本地接收缓冲区地址,length是缓冲区长度,缓冲区注册过了,是mr。
2、Infiniband VERBS API中(<infiniband/verbs.h>)的有关WRITE/READ和SEND/RECEIVE涉及到的函数如下所示
把WR发送到QP的SQ中,根据send_wr中的opcode区分是SEND/WRITE/READ操作
int ibv_post_send(struct ibv_qp *qp, struct ibv_send_wr *wr, struct ibv_send_wr **bad_wr);
- 1
QP是从ibv_create_qp()返回的QP,wr是要发送到QP的SQ中的WR链表,bad_wr:指向它的指针将填充第一个处理失败的工作请求。返回0表示成功。struct ibv_send_wr结构体中有个成员sg_list,指定用于读取或写入的本地内存buffer,至于是读取还是写入,取决于操作码opcode,对于SEND/WRITE操作,sg_list指定要读取的内存buffer,对于READ操作,sg_list指定要写入的内存buffer。
RECEIVE操作,recv_wr中并没有操作码opcode,因为只有一个接收操作
int ibv_post_recv(struct ibv_qp *qp, struct ibv_recv_wr *wr, struct ibv_recv_wr **bad_wr);
- 1
WR是发送到QP的RQ中的WR,bad_wr指向它的指针将会用来填充第一个失败的处理请求。返回0表示成功。
可见,RDMA_CM API中,把四种常见的操作分割开来,而在Infiniband VERBS API中,SEND/READ/WRITE都是通过ibv_post_send()实现,而RECEIVE是通过ibv_post_recv()实现。
注意,无论是send/recv还是read/write,直观上看是数据直接发送,但其实是这些操作会对应一个注册过的内存,和RDMA直接交互,由RDMA直接读取和写入数据,避免应用内存和内核内存进行数据拷贝带来的开销。
比如send,发送的数据所在内存是已经注册过的,RNIC异步调度轮到相应的WQE的时候,发现是SEND操作,就会直接和注册过的内存交互,把数据发送出去。
比如recv,接收的缓冲区是注册过的,当RNIC异步调度轮到对应的WQE的时候,发现是RECV操作,RDMA就会直接把数据存到相应的地址中。
比如write,发送的数据所在的内存是已经注册过的,RNIC直接和这块内存交互,把数据写到服务器端的目的地址。
比如read,用于存储数据的接收内存是已经注册过的,RNIC直接通过网络交互,把服务器的数据直接拿过来,存到接收内存。
相当于说,通信过程需要操作的缓冲区都是注册过的,意味着可以和RDMA设备即RNIC进行直接数据交互,这样就提高了通信效率,降低延迟。send/recv和read/write操作中,都是RNIC直接和注册过的内存进行数据交互,低延迟、高吞吐量、低CPU占用率。