一、概述我们上一篇博客介绍了RDMA的相关概念本博客在此基础之上使用 verbs 编程实现 RDMA 文件传输演示代码已开源(Githubrdma-sendfile。如果你还不了解QP、WR、MR等 RDMA基本要素可以先花五分钟看看RDMA要素介绍。RDMA有IB、RoCE、iWARP三种协议然而上层的用户接口都是VerbsVerbs 可以看作HCA对主机提供的RDMA功能接口。RDMA 文件传输主要涉及两个部分的编码控制消息的传递文件数据的传输RDMA 提供了单边模式、双边模式两种工作模式对应send/recv、write/read 四种操作它们的特点和使用场景我们可以总结如下维度双边操作Send/Recv单边操作RDMA Read/Write远端CPU参与需要预先发布Recv WR不需要完全无感完成通知双方都产生CQE仅发起方产生CQE需要的信息只需本地缓冲区需要远端地址 rkey适用消息大小小消息控制类大消息数据类典型应用控制消息、元数据交换批量数据传输所以在我们的 RDMA 文件传输助手实现中控制消息的传递使用双边模式 send/recv而数据传输则使用单边的 write。主要的思路是先建链并使用双边模式传递控制消息然后复用链路进行单边模式 write 文件传输。二、控制消息传递控制消息: remote_addr、rkey要搞清楚控制消息传递如何实现我们先要搞清楚所谓的控制消息到底有什么我们最终需要通过单边模式write来实现大批量的数据传输主机写入远端内存的之前需要知道远端内存缓冲区地址和rkey这也就是RDMA文件传输助手中最关键的控制消息。双边模式并不需要提供远端地址和rkey就可以进行通信只需要远端预先发布recv WR我们可以根据这个特点进行元数据交换然后主机根据拿到的元数据进行单边write操作。RDMA 的 send和recv 的工作流程如下:初始化双方打开RDMA设备分配PD、注册MR并获取对应的lkey创建QP、CQ等//建链与初始化staticvoidinitialize_connection(structrdma_cm_id*cm_id){structibv_pd*pdibv_alloc_pd(cm_id-verbs);//分配PDstructibv_comp_channel*channelibv_create_comp_channel(cm_id-verbs);//创建完成事件通道structibv_cq*cqibv_create_cq(cm_id-verbs,1024,NULL,channel,0);//创建CQstructibv_qp_init_attrqp_attr{.send_cqcq,.recv_cqcq,.qp_typeIBV_QPT_RC,.cap{.max_send_wr1024,.max_recv_wr1024,.max_send_sge1,.max_recv_sge1}};rdma_create_qp(cm_id,pd,qp_attr);//创建QPconn_manger_t*conncalloc(1,sizeof(*conn));conn-pdpd;conn-qpcm_id-qp;conn-cqcq;conn-channelchannel;posix_memalign((void**)conn-recv_buffer,4096,CTRL_BUF_SIZE);posix_memalign((void**)conn-send_buffer,4096,CTRL_BUF_SIZE);//注册MRconn-recv_mribv_reg_mr(pd,conn-recv_buffer,CTRL_BUF_SIZE,IBV_ACCESS_LOCAL_WRITE|IBV_ACCESS_REMOTE_WRITE);conn-send_mribv_reg_mr(pd,conn-send_buffer,CTRL_BUF_SIZE,IBV_ACCESS_LOCAL_WRITE|IBV_ACCESS_REMOTE_READ);cm_id-contextconn;}远端预先建构并发布recv WR并将recv WR推入RQ中staticintpost_recv(conn_manger_t*conn){//构建recv WRstructibv_sgesge;memset(sge,0,sizeof(sge));sge.addr(uintptr_t)conn-recv_buffer;sge.lengthCTRL_BUF_SIZE;sge.lkeyconn-recv_mr-lkey;//lkey用于操作本地的MRstructibv_recv_wrrecv_wr,*bad_recv_wrNULL;memset(recv_wr,0,sizeof(recv_wr));recv_wr.wr_id(uintptr_t)conn;recv_wr.sg_listsge;recv_wr.num_sge1;conn-recv_outstanding1;//将recv WR推送到RQif(ibv_post_recv(conn-qp,recv_wr,bad_recv_wr)){conn-recv_outstanding0;perror(ibv_post_recv);return-1;}return0;}说明SGEScatter/Gather Element用于向硬件描述一块内存区域structibv_sge{uint64_taddr;// 内存缓冲区的起始虚拟地址uint32_tlength;// 缓冲区长度字节数uint32_tlkey;// 本地内存密钥由注册MR时获得};本机构建send WR包含控制消息将send WR推入SQ中staticintpost_send(conn_manger_t*conn,uint32_tlength){if(length0||lengthCTRL_BUF_SIZE)return-1;//构建send WRstructibv_sgesge;memset(sge,0,sizeof(sge));sge.addr(uintptr_t)conn-send_buffer;sge.lengthlength;sge.lkeyconn-send_mr-lkey;//lkey操作本地MRstructibv_send_wrsend_wr,*bad_send_wrNULL;memset(send_wr,0,sizeof(send_wr));send_wr.wr_id(uintptr_t)conn;send_wr.opcodeIBV_WR_SEND;//操作码IBV_WR_SENDsend_wr.send_flagsIBV_SEND_SIGNALED;send_wr.sg_listsge;send_wr.num_sge1;conn-send_outstanding1;//将send WR推送到SQif(ibv_post_send(conn-qp,send_wr,bad_send_wr)){conn-send_outstanding0;perror(ibv_post_send);return-1;}returnwait_send_done(conn);}RDMA硬件完成通信产生WC本机和远端轮询CQ回收WCstaticintpoll_cq_batch(conn_manger_t*conn){structibv_wcwcs[POLL_BATCH];intnibv_poll_cq(conn-cq,POLL_BATCH,wcs);//轮询CQ,获取一批WC//遍历检查WC状态for(inti0;in;i){structibv_wc*wcwcs[i];if(wc-status!IBV_WC_SUCCESS){conn-send_outstanding0;conn-recv_outstanding0;conn-write_inflight0;continue;}//操作码对应if(wc-opcodeIBV_WC_RDMA_WRITE){//这个部分是单边write 消费WC用的先不需要管intcredits(int)wc-wr_id;if(credits0)credits1;conn-write_inflight-credits;if(conn-write_inflight0)conn-write_inflight0;}elseif(wc-opcodeIBV_WC_SEND){//WC回收计数清除conn-send_outstanding0;}elseif(wc-opcodeIBV_WC_RECV){//同上conn-recv_outstanding0;}}returnn;}三、文件数据传输我们通过双边模式交换控制消息远端地址、rkey等之后现在主机发送端拥有了足够的信息执行单边write了前面我们已经介绍了双边模式下使用的函数其中许多都可以重复使用比如PD分配与MR的注册、轮询CQ等。单边数据write的过程如下分配 PD注册待传输数据的 MR用 chunks 批量构建send WR将 MR 切成很多个 chunks用SGE向RDMA硬件描述每块 chunk 的内存区域将已经取得的远端地址remote_addr和rkey等写入WR中批量推入SQ多个WR inflight。staticintpost_rdma_file(conn_manger_t*conn){size_toffset0;size_tsizeconn-src_size;uint32_tlkeyconn-src_mr-lkey;//用chunks批量构建WRwhile(offsetsize){//write窗口满了while(conn-write_inflightWR_WINDOW){cq_wait_progress(conn);}// 计算本次分块长度size_tremainsize-offset;uint32_tlen(remainCHUNK_SIZE)?CHUNK_SIZE:(uint32_t)remain;structibv_sgesge{.addr(uint64_t)(uintptr_t)(conn-src_bufoffset),.lengthlen,.lkeylkey};structibv_send_wrsend_wr{.wr_id1,.opcodeIBV_WR_RDMA_WRITE,//操作码是IBV_WR_RDMA_WRITE.send_flagsIBV_SEND_SIGNALED,.sg_listsge,.num_sge1,.wr.rdma{.remote_addrconn-remote_addroffset,.rkeyconn-remote_rkey}};conn-write_inflight;ibv_post_send(conn-qp,send_wr,NULL);offsetlen;}while(conn-write_inflight0){if(cq_wait_progress(conn)0)return-1;}return0;}RDMA硬件完成发送产生WC轮询CQ回收WC//poll_cq_batch() 中处理单边write的分支if(wc-opcodeIBV_WC_RDMA_WRITE){intcredits(int)wc-wr_id;if(credits0)credits1;conn-write_inflight-credits;//inflight的减去完成的WC数目if(conn-write_inflight0)conn-write_inflight0;}四、测试我使用了两台虚拟机使用siwSoft-iWARP 。siw 是 Linux 内核中一个纯软件实现的 RDMA 设备驱动它允许你在标准以太网卡上运行 RDMA 应用无需专用的 RDMA 网卡硬件进行测试以下结果仅供参考测试场景吞吐量结论跨机 perftest~12 MB/s官方工具测 siw 跨机性能上限受限于软件模拟 TCP/IP 协议栈实际传输~9 MiB/s与官方基准同一量级代码实现无明显性能缺陷