RDMA技术浅析

Posted yuanyun_elber

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RDMA技术浅析相关的知识,希望对你有一定的参考价值。

本章主要探讨RDMA软件相关的部分。

一、名词解释

首先解释一下几个名词:

  1. rdma-core

指开源RDMA用户态软件协议栈,包含用户态框架、各厂商用户态驱动、API帮助手册以及开发自测试工具等。

rdma-core在github上维护,我们的用户态Verbs API实际上就是它实现的。https://github.com/linux-rdma/rdma-core

代码目录结构如下:

其中比较重要的几个目录是:

  • libibverbs

以ibv为前缀,这里的ib并不代表infiniband协议,Verbs API支持IB/iWARP/RoCE三大RDMA协议,通过统一接口,让同一份RDMA程序程序可以无视底层的硬件和链路差异运行在不同的环境中。之所以有这个前缀,还是因为IB比较早。

Verbs API是一组用于使用RDMA服务的最基本的软件接口,也就是说业界的RDMA应用,要么直接基于这组API编写,要么基于在Verbs API上又封装了一层接口的各种中间件编写。

Verbs API向用户提供了有关RDMA的一切功能,典型的包括:注册MR、创建QP、Post Send、Poll CQ等等。

  • librdmacm

以rdma_为前缀,主要分为两个功能:

CMA(Connection Management Abstraction) (cma.c)

在Socket和Verbs API基础上实现的,用于CM建链并交换信息的一组接口。CM建链是在Socket基础上封装为QP实现,从用户的角度来看,是在通过QP交换之后数据交换所需要的QPN,Key等信息。

比如:

rdma_listen()用于监听链路上的CM建链请求。

rdma_connect()用于确认CM连接。

CM VERBS (rdma_verbs.h)

RDMA_CM也可以用于数据交换,相当于在verbs API上又封装了一套数据交换接口。

比如: 

   rdma_post_send  = ibv_post_send(qp,wr.opcode=IBV_WR_SEND,bad_wr)  
   rdma_post_read  = ibv_post_send(qp,wr.opcode=IBV_WR_RDMA_READ ,bad_wr) 
   rdma_post_write  = ibv_post_send(qp,wr.opcode=IBV_WR_RDMA_WRITE,bad_wr) 
  • provider

各个硬件厂商的驱动代码都在这里

如果要rdma编程,实现一个QP应用,那么关注前两个文件夹就可以了,作为我们硬件厂商来说,要关注provider这个目录下的驱动是怎么实现的。

下面我们会分别介绍这两个方面。

    2.kernel RDMA subsystem

指开源的Linux内核中的RDMA子系统,包含RDMA内核框架及各厂商的驱动。

RDMA子系统跟随Linux维护,是内核的的一部分。一方面提供内核态的Verbs API,一方面负责对接用户态的接口。

    3.OFED

全称为OpenFabrics Enterprise Distribution,是一个开源软件包集合,其中包含内核框架和驱动、用户框架和驱动、以及各种中间件、测试工具和API文档。

开源OFED由OFA组织负责开发、发布和维护,它会定期从rdma-core和内核的RDMA子系统取软件版本,并对各商用OS发行版进行适配。除了协议栈和驱动外,还包含了perftest等测试工具。

二、 RDMA编程

2.1 基本的通信编程

可以使用上文提到的ibverbs接口或者cma接口都可以

主要参考文档是:https://network.nvidia.com/sites/default/files/related-docs/prod_software/RDMA_Aware_Programming_user_manual.pdf

也可以在代码路径的man目录下查看api的说明。

下面附上一个简单的RDMA程序的大致接口调用流程,Client端的程序会发送一个SEND请求给Server端的程序,图中的接口上文中都有简单介绍。

需要注意的是图中的建链过程是为了交换对端的GID(相当于TCP中的IP),QPN(QP序号)等信息,可以通过传统的IBV接口实现,也可以通过本文中介绍的CMA接口实现。

图中特意列出了多次modify QP的流程,一方面是把建链之后交互得到的信息存入QPC中(即QP间建立连接的过程),另一方面是为了(转换状态)使QP处于具备收/发能力状态才能进行下一步的数据交互。

(假设A节点的某个QP要跟B节点的某个QP交换信息,除了要知道B节点的QP序号——QPN之外,还需要GID--(相当于TCP中的IP),在传统TCP-IP协议栈中,使用了家喻户晓的IP地址来标识网络层的每个节点。而IB协议中的这个标识被称为GID(Global Identifier,全局ID)

如果是用cma编程,从函数名上看,更像是socket编程了:

static int run_server(void)

	struct rdma_cm_id *listen_id;
	int i, ret;

	printf("cmatose: starting server\\n");
	ret = rdma_create_id(test.channel, &listen_id, &test, hints.ai_port_space);
	if (ret) 
		perror("cmatose: listen request failed");
		return ret;
	

	ret = get_rdma_addr(src_addr, dst_addr, port, &hints, &test.rai);
	if (ret) 
		printf("cmatose: getrdmaaddr error: %s\\n", gai_strerror(ret));
		goto out;
	

	ret = rdma_bind_addr(listen_id, test.rai->ai_src_addr);
	if (ret) 
		perror("cmatose: bind address failed");
		goto out;
	

	ret = rdma_listen(listen_id, 0);
	if (ret) 
		perror("cmatose: failure trying to listen");
		goto out;
	

	ret = connect_events();
	if (ret)
		goto out;

2.2 基于消息

和传统TCP/IP的socket编程还是有区别的,区别在于rdma基于消息,而传统TCP/IP基于流。

从transport modes可知RDMA通信主要有两种方式,分别是对应send/receive的双边模式和对应RDMA write/read的单边模式。

何为双边模式?因为完成一次通信过程需要两端CPU的参与,并且收端需要提前显式的下发WQE。下图是一次SEND-RECV操作的过程示意图。

WQE(work queue element),其实类似于传统网卡驱动中的描述符,最终指向内存中的一块buffer。

对于双边操作为例,主机A向主机B(下面简称A、B)发送数据的流程如下:

1.   首先,A和B都要创建并初始化好各自的QP,CQ

2.   A和B分别向自己的WQ中注册WQE,对于A,WQ=SQ,WQE描述指向一个等到被发送的数据;对于B,WQ=RQ,WQE描述指向一块用于存储数据的Buffer。

3.   A的RNIC异步调度轮到A的WQE,解析到这是一个SEND消息,从Buffer中直接向B发出数据。数据流到达B的RNIC后,B的WQE被消耗,并把数据直接存储到WQE指向的存储位置。

4.  AB通信完成后,A的CQ中会产生一个完成消息CQE表示发送完成。与此同时,B的CQ中也会产生一个完成消息表示接收完成。每个WQ中WQE的处理完成都会产生一个CQE。

双边操作与传统网络的底层Buffer Pool类似,收发双方的参与过程并无差别,区别在零拷贝、Kernel Bypass,实际上对于RDMA,这是一种复杂的消息传输模式,多用于传输短的控制消息。

单边操作,顾名思义,只需要一端的CPU操作,另一端CPU是无感的,(除非是个IRDMA_WRITE_WITH_IMM,带立即数的操作),如下图是write操作,read操作类似。

本端在准备阶段通过数据交互,获取了对端某一片可用的内存的地址和“钥匙”,相当于获得了这片远端内存的读写权限。

WRITE/READ操作中的目的地址和钥匙是如何获取的呢?通常可以通过我们刚刚讲过的SEND-RECV操作来完成。

2.3 具体的例子

我们可以看一下如下这个例子,看完这个例子,基本就对rdma编程常用的接口有所了解了。

InfiniBand, Verbs, RDMA | The Geek in the Corner

在这个例子中,需要把一个文件发送到对端,其流程如下,前两次双方的双向通信,目的是为了最后的单向通信服务的

该例子封装了收发消息的函数,本质上收发消息就是上文所述的send、recieve,我们一直说rdma是基于消息的,而tcp是基于流的,就是这个意思。

void send_message(struct connection *conn)

struct ibv_send_wr wr, *bad_wr = NULL;
struct ibv_sge sge;
memset(&wr, 0, sizeof(wr));
wr.wr_id = (uintptr_t)conn;
wr.opcode = IBV_WR_SEND;        //注意这个opcode,这个opcode就是传输模式
wr.sg_list = &sge;
wr.num_sge = 1;
wr.send_flags = IBV_SEND_SIGNALED;
sge.addr = (uintptr_t)conn->send_msg;
sge.length = sizeof(struct message);
sge.lkey = conn->send_mr->lkey;
while (!conn->connected);
TEST_NZ(ibv_post_send(conn->qp, &wr, &bad_wr));


void on_completion(struct ibv_wc *wc)    //CQ收到消息说明收发队列上有数据需要处理,cpu介入,调用此回调函数

  struct connection *conn = (struct connection *)(uintptr_t)wc->wr_id;
 //可以看出这个函数是深度定制的,根据状态机、消息的opcode、typde等等有着不同处理loop
  if (wc->status != IBV_WC_SUCCESS)
    die("on_completion: status is not IBV_WC_SUCCESS.");
 
  if (wc->opcode & IBV_WC_RECV) 
    conn->recv_state++;
 
    if (conn->recv_msg->type == MSG_MR)     //MSG_MR是此程序定制的消息类型,顾名思义就是为了传输内存信息的
      memcpy(&conn->peer_mr, &conn->recv_msg->data.mr, sizeof(conn->peer_mr));
      post_receives(conn); /* only rearm for MSG_MR */
 
      if (conn->send_state == SS_INIT) /* received peer's MR before sending ours, so send ours back */
        send_mr(conn);
    
 
   else 
    conn->send_state++;
    printf("send completed successfully.\\n");
  
 
  if (conn->send_state == SS_MR_SENT && conn->recv_state == RS_MR_RECV) 
    struct ibv_send_wr wr, *bad_wr = NULL;
    struct ibv_sge sge;
 
    if (s_mode == M_WRITE)
      printf("received MSG_MR. writing message to remote memory...\\n");
    else
      printf("received MSG_MR. reading message from remote memory...\\n");
 
    memset(&wr, 0, sizeof(wr));
 
    wr.wr_id = (uintptr_t)conn;
    wr.opcode = (s_mode == M_WRITE) ? IBV_WR_RDMA_WRITE : IBV_WR_RDMA_READ;
    wr.sg_list = &sge;
    wr.num_sge = 1;
    wr.send_flags = IBV_SEND_SIGNALED;
    wr.wr.rdma.remote_addr = (uintptr_t)conn->peer_mr.addr;
    wr.wr.rdma.rkey = conn->peer_mr.rkey;
 
    sge.addr = (uintptr_t)conn->rdma_local_region;
    sge.length = RDMA_BUFFER_SIZE;
    sge.lkey = conn->rdma_local_mr->lkey;
 
    TEST_NZ(ibv_post_send(conn->qp, &wr, &bad_wr));    //这个分支就是write、read的post了,后面不需要cpu参与了
 
    conn->send_msg->type = MSG_DONE;
    send_message(conn);
 
   else if (conn->send_state == SS_DONE_SENT && conn->recv_state == RS_DONE_RECV) 
    printf("remote buffer: %s\\n", get_peer_message_region(conn));
    rdma_disconnect(conn->id);
  

三、 RDMA和硬件接口

其实作为网卡芯片厂商,我们更为关注的还是provider文件夹下面的设备驱动部分。也就是rdma core如何把wqe下发到网卡,让网卡执行dma操作的。

以发送为例,数据面是下面这个函数

    ibv_post_send(qp, wr)

QP下发一个Send WR,参数wr是一个结构体,包含了WR的所有信息。包括wr_id、sge数量、操作码(SEND/WRITE/READ等以及更细分的类型)。

WR经由驱动进一步处理后,会转化成WQE下发给硬件。流程图如下:

以mlx5为例,mlx5发送的回调函数如下:

static inline int _mlx5_post_send(struct ibv_qp *ibqp, struct ibv_send_wr *wr,
				  struct ibv_send_wr **bad_wr)

	struct mlx5_qp *qp = to_mqp(ibqp);
	void *seg;
	struct mlx5_wqe_eth_seg *eseg;				//seg的意思是段,wqe中根据不同格式,分成不同段
	struct mlx5_wqe_ctrl_seg *ctrl = NULL;
	struct mlx5_wqe_data_seg *dpseg;
	struct mlx5_sg_copy_ptr sg_copy_ptr = .index = 0, .offset = 0;
	int nreq;
	int inl = 0;
	int err = 0;
	int size = 0;
	int i;
	unsigned idx;
	uint8_t opmod = 0;
	struct mlx5_bf *bf = qp->bf;
	void *qend = qp->sq.qend;
	uint32_t mlx5_opcode;
	struct mlx5_wqe_xrc_seg *xrc;
	uint8_t fence;
	uint8_t next_fence;
	uint32_t max_tso = 0;
	FILE *fp = to_mctx(ibqp->context)->dbg_fp; /* The compiler ignores in non-debug mode */

	mlx5_spin_lock(&qp->sq.lock);

	next_fence = qp->fm_cache;

	for (nreq = 0; wr; ++nreq, wr = wr->next) 
		if (unlikely(wr->opcode < 0 ||
		    wr->opcode >= sizeof mlx5_ib_opcode / sizeof mlx5_ib_opcode[0])) 
			mlx5_dbg(fp, MLX5_DBG_QP_SEND, "bad opcode %d\\n", wr->opcode);
			err = EINVAL;
			*bad_wr = wr;
			goto out;
		

		if (unlikely(mlx5_wq_overflow(&qp->sq, nreq,
					      to_mcq(qp->ibv_qp->send_cq)))) 
			mlx5_dbg(fp, MLX5_DBG_QP_SEND, "work queue overflow\\n");
			err = ENOMEM;
			*bad_wr = wr;
			goto out;
		

		if (unlikely(wr->num_sge > qp->sq.qp_state_max_gs)) 
			mlx5_dbg(fp, MLX5_DBG_QP_SEND, "max gs exceeded %d (max = %d)\\n",
				 wr->num_sge, qp->sq.max_gs);
			err = ENOMEM;
			*bad_wr = wr;
			goto out;
		

		if (wr->send_flags & IBV_SEND_FENCE)
			fence = MLX5_WQE_CTRL_FENCE;
		else
			fence = next_fence;
		next_fence = 0;
		idx = qp->sq.cur_post & (qp->sq.wqe_cnt - 1);
		ctrl = seg = mlx5_get_send_wqe(qp, idx);		//ctrl,seg都指向wqe的起始地址,seg是个累加指针
		*(uint32_t *)(seg + 8) = 0;						//先配置ctrl域
		ctrl->imm = send_ieth(wr);
		ctrl->fm_ce_se = qp->sq_signal_bits | fence |
			(wr->send_flags & IBV_SEND_SIGNALED ?
			 MLX5_WQE_CTRL_CQ_UPDATE : 0) |
			(wr->send_flags & IBV_SEND_SOLICITED ?
			 MLX5_WQE_CTRL_SOLICITED : 0);

		seg += sizeof *ctrl;							//seg指针累加
		size = sizeof *ctrl / 16;
		qp->sq.wr_data[idx] = 0;

		switch (ibqp->qp_type) 					//根据服务类型qp_type进行不同的处理,RC、UC、RD、UD等等
		case IBV_QPT_XRC_SEND:
			if (unlikely(wr->opcode != IBV_WR_BIND_MW &&
				     wr->opcode != IBV_WR_LOCAL_INV)) 
				xrc = seg;
				xrc->xrc_srqn = htobe32(wr->qp_type.xrc.remote_srqn);
				seg += sizeof(*xrc);
				size += sizeof(*xrc) / 16;
			
			/* fall through */
		case IBV_QPT_RC:
			switch (wr->opcode) 				//opcode决定了传输模式,在write/read模式下,需要配置remote addr,set_raddr_seg
			case IBV_WR_RDMA_READ:
			case IBV_WR_RDMA_WRITE:
			case IBV_WR_RDMA_WRITE_WITH_IMM:
				set_raddr_seg(seg, wr->wr.rdma.remote_addr,
					      wr->wr.rdma.rkey);
				seg  += sizeof(struct mlx5_wqe_raddr_seg);
				size += sizeof(struct mlx5_wqe_raddr_seg) / 16;
				break;

			case IBV_WR_ATOMIC_CMP_AND_SWP:
			case IBV_WR_ATOMIC_FETCH_AND_ADD:
				if (unlikely(!qp->atomics_enabled)) 
					mlx5_dbg(fp, MLX5_DBG_QP_SEND, "atomic operations are not supported\\n");
					err = EOPNOTSUPP;
					*bad_wr = wr;
					goto out;
				
				set_raddr_seg(seg, wr->wr.atomic.remote_addr,
					      wr->wr.atomic.rkey);
				seg  += sizeof(struct mlx5_wqe_raddr_seg);

				set_atomic_seg(seg, wr->opcode,
					       wr->wr.atomic.swap,
					       wr->wr.atomic.compare_add);
				seg  += sizeof(struct mlx5_wqe_atomic_seg);

				size += (sizeof(struct mlx5_wqe_raddr_seg) +
				sizeof(struct mlx5_wqe_atomic_seg)) / 16;
				break;

			case IBV_WR_BIND_MW:
				next_fence = MLX5_WQE_CTRL_INITIATOR_SMALL_FENCE;
				ctrl->imm = htobe32(wr->bind_mw.mw->rkey);
				err = set_bind_wr(qp, wr->bind_mw.mw->type,
						  wr->bind_mw.rkey,
						  &wr->bind_mw.bind_info,
						  ibqp->qp_num, &seg, &size);
				if (err) 
					*bad_wr = wr;
					goto out;
				

				qp->sq.wr_data[idx] = IBV_WC_BIND_MW;
				break;
			case IBV_WR_LOCAL_INV: 
				struct ibv_mw_bind_info	bind_info = ;

				next_fence = MLX5_WQE_CTRL_INITIATOR_SMALL_FENCE;
				ctrl->imm = htobe32(wr->invalidate_rkey);
				err = set_bind_wr(qp, IBV_MW_TYPE_2, 0,
						  &bind_info, ibqp->qp_num,
						  &seg, &size);
				if (err) 
					*bad_wr = wr;
					goto out;
				

				qp->sq.wr_data[idx] = IBV_WC_LOCAL_INV;
				break;
			

			default:
				break;
			
			break;

		case IBV_QPT_UC:
			switch (wr->opcode) 
			case IBV_WR_RDMA_WRITE:
			case IBV_WR_RDMA_WRITE_WITH_IMM:
				set_raddr_seg(seg, wr->wr.rdma.remote_addr,
					      wr->wr.rdma.rkey);
				seg  += sizeof(struct mlx5_wqe_raddr_seg);
				size += sizeof(struct mlx5_wqe_raddr_seg) / 16;
				break;
			case IBV_WR_BIND_MW:
				next_fence = MLX5_WQE_CTRL_INITIATOR_SMALL_FENCE;
				ctrl->imm = htobe32(wr->bind_mw.mw->rkey);
				err = set_bind_wr(qp, wr->bind_mw.mw->type,
						  wr->bind_mw.rkey,
						  &wr->bind_mw.bind_info,
						  ibqp->qp_num, &seg, &size);
				if (err) 
					*bad_wr = wr;
					goto out;
				

				qp->sq.wr_data[idx] = IBV_WC_BIND_MW;
				break;
			case IBV_WR_LOCAL_INV: 
				struct ibv_mw_bind_info	bind_info = ;

				next_fence = MLX5_WQE_CTRL_INITIATOR_SMALL_FENCE;
				ctrl->imm = htobe32(wr->invalidate_rkey);
				err = set_bind_wr(qp, IBV_MW_TYPE_2, 0,
						  &bind_info, ibqp->qp_num,
						  &seg, &size);
				if (err) 
					*bad_wr = wr;
					goto out;
				

				qp->sq.wr_data[idx] = IBV_WC_LOCAL_INV;
				break;
			

			default:
				break;
			
			break;

		case IBV_QPT_UD:
			set_datagram_seg(seg, wr);
			seg  += sizeof(struct mlx5_wqe_datagram_seg);
			size += sizeof(struct mlx5_wqe_datagram_seg) / 16;
			if (unlikely((seg == qend)))
				seg = mlx5_get_send_wqe(qp, 0);

			if (unlikely(qp->flags & MLX5_QP_FLAGS_USE_UNDERLAY)) 
				err = mlx5_post_send_underlay(qp, wr, &seg, &size, &sg_copy_ptr);
				if (unlikely(err)) 
					*bad_wr = wr;
					goto out;
				
			
			break;

		case IBV_QPT_RAW_PACKET:
			memset(seg, 0, sizeof(struct mlx5_wqe_eth_seg));
			eseg = seg;

			if (wr->send_flags & IBV_SEND_IP_CSUM) 
				if (!(qp->qp_cap_cache & MLX5_CSUM_SUPPORT_RAW_OVER_ETH)) 
					err = EINVAL;
					*bad_wr = wr;
					goto out;
				

				eseg->cs_flags |= MLX5_ETH_WQE_L3_CSUM | MLX5_ETH_WQE_L4_CSUM;
			

			if (wr->opcode == IBV_WR_TSO) 
				max_tso = qp->max_tso;
				err = set_tso_eth_seg(&seg, wr->tso.hdr,
						      wr->tso.hdr_sz,
						      wr->tso.mss, qp, &size);
				if (unlikely(err)) 
					*bad_wr = wr;
					goto out;
				

				/* For TSO WR we always copy at least MLX5_ETH_L2_MIN_HEADER_SIZE
				 * bytes of inline header which is included in struct mlx5_wqe_eth_seg.
				 * If additional bytes are copied, 'seg' and 'size' are adjusted
				 * inside set_tso_eth_seg().
				 */

				seg += sizeof(struct mlx5_wqe_eth_seg);
				size += sizeof(struct mlx5_wqe_eth_seg) / 16;
			 else 
				uint32_t inl_hdr_size =
					to_mctx(ibqp->context)->eth_min_inline_size;

				err = copy_eth_inline_headers(ibqp, wr->sg_list,
							      wr->num_sge, seg,
							      &sg_copy_ptr, 1);
				if (unlikely(err)) 
					*bad_wr = wr;
					mlx5_dbg(fp, MLX5_DBG_QP_SEND,
						 "copy_eth_inline_headers failed, err: %d\\n",
						 err);
					goto out;
				

				/* The eth segment size depends on the device's min inline
				 * header requirement which can be 0 or 18. The basic eth segment
				 * always includes room for first 2 inline header bytes (even if
				 * copy size is 0) so the additional seg size is adjusted accordingly.
				 */

				seg += (offsetof(struct mlx5_wqe_eth_seg, inline_hdr) +
						inl_hdr_size) & ~0xf;
				size += (offsetof(struct mlx5_wqe_eth_seg, inline_hdr) +
						inl_hdr_size) >> 4;
			
			break;

		default:
			break;
		

		if (wr->send_flags & IBV_SEND_INLINE && wr->num_sge) 
			int uninitialized_var(sz);

			err = set_data_inl_seg(qp, wr, seg, &sz, &sg_copy_ptr);
			if (unlikely(err)) 
				*bad_wr = wr;
				mlx5_dbg(fp, MLX5_DBG_QP_SEND,
					 "inline layout failed, err %d\\n", err);
				goto out;
			
			inl = 1;
			size += sz;
		 else 
			dpseg = seg;				//配置数据段的seg,set_data_ptr_seg,这里面主要就是本地的地址和lkey
			for (i = sg_copy_ptr.index; i < wr->num_sge; ++i) 			//支持scatter gather
				if (unlikely(dpseg == qend)) 
					seg = mlx5_get_send_wqe(qp, 0);
					dpseg = seg;
				
				if (likely(wr->sg_list[i].length)) 
					if (unlikely(wr->opcode ==
						   IBV_WR_ATOMIC_CMP_AND_SWP ||
						   wr->opcode ==
						   IBV_WR_ATOMIC_FETCH_AND_ADD))
						set_data_ptr_seg_atomic(dpseg, wr->sg_list + i);
					else 
						if (unlikely(wr->opcode == IBV_WR_TSO)) 
							if (max_tso < wr->sg_list[i].length) 
								err = EINVAL;
								*bad_wr = wr;
								goto out;
							
							max_tso -= wr->sg_list[i].length;
						
						set_data_ptr_seg(dpseg, wr->sg_list + i,
								 sg_copy_ptr.offset);
					
					sg_copy_ptr.offset = 0;
					++dpseg;
					size += sizeof(struct mlx5_wqe_data_seg) / 16;
				
			
		
		//至此wqe全部配完

		mlx5_opcode = mlx5_ib_opcode[wr->opcode];
		ctrl->opmod_idx_opcode = htobe32(((qp->sq.cur_post & 0xffff) << 8) |
					       mlx5_opcode			 |
					       (opmod << 24));
		ctrl->qpn_ds = htobe32(size | (ibqp->qp_num << 8));

		if (unlikely(qp->wq_sig))
			ctrl->signature = wq_sig(ctrl);

		qp->sq.wrid[idx] = wr->wr_id;
		qp->sq.wqe_head[idx] = qp->sq.head + nreq;
		qp->sq.cur_post += DIV_ROUND_UP(size * 16, MLX5_SEND_WQE_BB);

#ifdef MLX5_DEBUG
		if (mlx5_debug_mask & MLX5_DBG_QP_SEND)
			dump_wqe(to_mctx(ibqp->context), idx, size, qp);
#endif
	

out:
	qp->fm_cache = next_fence;
	post_send_db(qp, bf, nreq, inl, size, ctrl);	//doorbell通知硬件

	mlx5_spin_unlock(&qp->sq.lock);

	return err;

步骤0:用户首先将MD排队到TxQ中。然后,网络驱动程序准备设备特定的MD,该MD包含NIC的报头和指向有效负载的指针。

步骤1:使用8字节的原子写入内存映射位置,CPU(网络驱动程序)通知NIC消息已准备好发送。这叫做按门铃。RC使用MWr PCIe事务执行门铃。我们这里要介绍一下门铃机制, 在大多数HCA中,这是映射到进程地址空间的设备内存地址。我们在这份文件中称这个地址为“门铃”。

步骤2:门铃响后,网卡通过DMA读取MD。MRd PCIe事务执行DMA读取。

步骤3:NIC将使用另一个DMA读取(另一个MRd TLP)从注册的内存区域获取有效负载。请注意,在NIC可以执行DMA读取之前,必须将虚拟地址转换为物理地址。

下面两步,rdma write/read是不需要的。

步骤4:一旦网卡接收到有效载荷,它就会通过网络传输读取的数据。成功传输后,目标NIC接收确认(ACK)。

步骤5:接收到ACK后,NIC将DMA写入(使用MWr TLP)一个完成队列条目(CQE;动词中称为cookie;Mellanox InfiniBand中为64字节)到与TxQ关联的CQ。然后CPU将轮询此完成情况以取得进展。

接收和发送方向相反,不再赘述。

以上是关于RDMA技术浅析的主要内容,如果未能解决你的问题,请参考以下文章

RDMA技术浅析

RDMA over TCP的协议栈工作过程浅析

RDMA技术浅析

RDMA技术浅析

RDMA技术浅析

RDMA技术浅析