用RDMA实现文件传输

一、概述

我们上一篇博客介绍了RDMA的相关概念,本博客在此基础之上,使用 verbs 编程,实现 RDMA 文件传输(演示代码已开源(Github:rdma-sendfile)。

如果你还不了解QP、WR、MR等 RDMA基本要素,可以先花五分钟看看RDMA要素介绍。

RDMA有IB、RoCE、iWARP三种协议,然而,上层的用户接口都是Verbs,Verbs 可以看作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 的工作流程如下:

  1. 初始化:双方打开RDMA设备,分配PD、注册MR并获取对应的lkey,创建QP、CQ等
c 复制代码
//建链与初始化
static void initialize_connection(struct rdma_cm_id *cm_id) {
    struct ibv_pd *pd = ibv_alloc_pd(cm_id->verbs); //分配PD
    
    struct ibv_comp_channel *channel = ibv_create_comp_channel(cm_id->verbs); //创建完成事件通道
    struct ibv_cq *cq = ibv_create_cq(cm_id->verbs, 1024, NULL, channel, 0); //创建CQ
    
    struct ibv_qp_init_attr qp_attr = {
        .send_cq = cq,
        .recv_cq = cq,
        .qp_type = IBV_QPT_RC,
        .cap = {
            .max_send_wr = 1024,
            .max_recv_wr = 1024,
            .max_send_sge = 1,
            .max_recv_sge = 1
        }
    };
    rdma_create_qp(cm_id, pd, &qp_attr); //创建QP
    
    conn_manger_t *conn = calloc(1, sizeof(*conn));
    conn->pd = pd;
    conn->qp = cm_id->qp;
    conn->cq = cq;
    conn->channel = channel;
    
    posix_memalign((void **)&conn->recv_buffer, 4096, CTRL_BUF_SIZE);
    posix_memalign((void **)&conn->send_buffer, 4096, CTRL_BUF_SIZE);
    //注册MR
    conn->recv_mr = ibv_reg_mr(pd, conn->recv_buffer, CTRL_BUF_SIZE,
                               IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE);
    conn->send_mr = ibv_reg_mr(pd, conn->send_buffer, CTRL_BUF_SIZE,
                               IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ);
    
    cm_id->context = conn;
}
  1. 远端预先建构并发布recv WR,并将recv WR推入RQ中
c 复制代码
static int post_recv(conn_manger_t *conn) {
//构建recv WR
    struct ibv_sge sge;
    memset(&sge, 0, sizeof(sge));
    sge.addr = (uintptr_t)conn->recv_buffer;
    sge.length = CTRL_BUF_SIZE;
    sge.lkey = conn->recv_mr->lkey; //lkey用于操作本地的MR

    struct ibv_recv_wr recv_wr, *bad_recv_wr = NULL;
    memset(&recv_wr, 0, sizeof(recv_wr));
    recv_wr.wr_id = (uintptr_t)conn;
    recv_wr.sg_list = &sge;
    recv_wr.num_sge = 1;

    conn->recv_outstanding = 1;
    //将recv WR推送到RQ
    if (ibv_post_recv(conn->qp, &recv_wr, &bad_recv_wr)) {
        conn->recv_outstanding = 0;
        perror("ibv_post_recv");
        return -1;
    }
    return 0;
}

说明:SGE(Scatter/Gather Element),用于向硬件描述一块内存区域:

c 复制代码
struct ibv_sge {
    uint64_t addr;   // 内存缓冲区的起始虚拟地址
    uint32_t length; // 缓冲区长度(字节数)
    uint32_t lkey;   // 本地内存密钥(由注册MR时获得)
};
  1. 本机构建send WR(包含控制消息),将send WR推入SQ中
c 复制代码
static int post_send(conn_manger_t *conn, uint32_t length) {
   if (length == 0 || length > CTRL_BUF_SIZE)
       return -1;
//构建send WR 
   struct ibv_sge sge;
   memset(&sge, 0, sizeof(sge));
   sge.addr = (uintptr_t)conn->send_buffer;
   sge.length = length;
   sge.lkey = conn->send_mr->lkey; //lkey操作本地MR

   struct ibv_send_wr send_wr, *bad_send_wr = NULL;
   memset(&send_wr, 0, sizeof(send_wr));
   send_wr.wr_id = (uintptr_t)conn;
   send_wr.opcode = IBV_WR_SEND; //操作码:IBV_WR_SEND
   send_wr.send_flags = IBV_SEND_SIGNALED;
   send_wr.sg_list = &sge;
   send_wr.num_sge = 1;

   conn->send_outstanding = 1;
   //将send WR推送到SQ
   if (ibv_post_send(conn->qp, &send_wr, &bad_send_wr)) {
       conn->send_outstanding = 0;
       perror("ibv_post_send");
       return -1;
   }
   return wait_send_done(conn);
}
  1. RDMA硬件完成通信,产生WC
  2. 本机和远端轮询CQ,回收WC
c 复制代码
static int poll_cq_batch(conn_manger_t *conn) {
    struct ibv_wc wcs[POLL_BATCH];
    int n = ibv_poll_cq(conn->cq, POLL_BATCH, wcs); //轮询CQ,获取一批WC
		//遍历检查WC状态
    for (int i = 0; i < n; i++) {
        struct ibv_wc *wc = &wcs[i];
        if (wc->status != IBV_WC_SUCCESS) {
            conn->send_outstanding = 0;
            conn->recv_outstanding = 0;
            conn->write_inflight = 0;
            continue;
        }
		//操作码对应
        if (wc->opcode == IBV_WC_RDMA_WRITE) { //这个部分是单边write 消费WC用的,先不需要管
            int credits = (int)wc->wr_id;
            if (credits <= 0)
                credits = 1;
            conn->write_inflight -= credits;
            if (conn->write_inflight < 0)
                conn->write_inflight = 0;
        } else if (wc->opcode == IBV_WC_SEND) { //WC回收,计数清除
            conn->send_outstanding = 0;
        } else if (wc->opcode & IBV_WC_RECV) { //同上
            conn->recv_outstanding = 0;
        }
    }
    return n;
}

三、文件数据传输

我们通过双边模式交换控制消息(远端地址、rkey等)之后,现在主机(发送端)拥有了足够的信息执行单边write了,前面我们已经介绍了双边模式下使用的函数,其中许多都可以重复使用,比如PD分配与MR的注册、轮询CQ等。

单边数据write的过程如下:

  1. 分配 PD,注册待传输数据的 MR
  2. 用 chunks 批量构建send WR

将 MR 切成很多个 chunks(用SGE向RDMA硬件描述每块 chunk 的内存区域),将已经取得的远端地址remote_addr和rkey等写入WR中,批量推入SQ,多个WR inflight。

c 复制代码
static int post_rdma_file(conn_manger_t *conn) {
    size_t offset = 0;
    size_t size = conn->src_size;
    uint32_t lkey = conn->src_mr->lkey;	
    //用chunks批量构建WR
    while (offset < size) {
        //write窗口满了
        while (conn->write_inflight >= WR_WINDOW) { 
            cq_wait_progress(conn);
        }

        // 计算本次分块长度
        size_t remain = size - offset;
        uint32_t len = (remain > CHUNK_SIZE) ? CHUNK_SIZE : (uint32_t)remain;

        struct ibv_sge sge = {
            .addr = (uint64_t)(uintptr_t)(conn->src_buf + offset),
            .length = len,
            .lkey = lkey
        };

        struct ibv_send_wr send_wr = {
            .wr_id = 1,
            .opcode = IBV_WR_RDMA_WRITE, //操作码是IBV_WR_RDMA_WRITE
            .send_flags = IBV_SEND_SIGNALED,
            .sg_list = &sge,
            .num_sge = 1,
            .wr.rdma = {
                .remote_addr = conn->remote_addr + offset,
                .rkey = conn->remote_rkey
            }
        };

        conn->write_inflight++;
        ibv_post_send(conn->qp, &send_wr, NULL);

        offset += len;
    }

    while (conn->write_inflight > 0) {
        if (cq_wait_progress(conn) < 0)
            return -1;
    }
    return 0;
}
  1. RDMA硬件完成发送,产生WC
  2. 轮询CQ,回收WC
c 复制代码
//poll_cq_batch() 中处理单边write的分支
if (wc->opcode == IBV_WC_RDMA_WRITE) { 
     int credits = (int)wc->wr_id;
     if (credits <= 0)
         credits = 1;
     conn->write_inflight -= credits; //inflight的减去完成的WC数目
     if (conn->write_inflight < 0)
         conn->write_inflight = 0;
} 

四、测试

我使用了两台虚拟机,使用siw(Soft-iWARP) 。siw 是 Linux 内核中一个纯软件实现的 RDMA 设备驱动,它允许你在标准以太网卡上运行 RDMA 应用,无需专用的 RDMA 网卡硬件进行测试

以下结果仅供参考:

测试场景 吞吐量 结论
跨机 siw RDMA(官方 ib_write_bw) ~12 MB/s siw 跨机性能上限,受限于软件模拟 TCP/IP 协议栈
跨机 siw RDMA(demo, 8MB WRITE) ~9 MiB/s 与官方基准同一量级,代码实现无明显性能缺陷
相关推荐
Soneswy8 小时前
Linux系统篇--重定向与文件操作命令
linux
有毒的教程8 小时前
Linux底层存储路由配
linux·运维·网络
Linux系统运维工程师_郭主任9 小时前
运维人还在被拒offer?现在能考的,只有 RHCE 10.0。
linux
自养Agent10 小时前
MCP Server Boundary Audit Report
linux·服务器·人工智能·windows·大模型·llm
Coisinier11 小时前
docker命令管理镜像及容器
linux·运维·nginx·docker·容器·操作系统
A心有千千结11 小时前
通过 SSH 反向代理让服务器借用本机网络访问 GitHub 的最佳实践
linux·服务器·网络·git·ssh·github
书生执笔画浮沉11 小时前
Linux 内核模块为什么不能自己解决兼容性问题?
linux·c·kernel·update·灵感
15Moonlight12 小时前
Linux网络(03):Socket 编程 TCP
linux·网络·tcp/ip
广州灵眸科技有限公司12 小时前
单芯片同时跑 Linux 与 RT-Thread:RV1126B的AMP异构方案完整拆解
linux·运维·服务器
小雪崩12 小时前
嵌入式学习:Linux驱动开发总结
linux·驱动开发·学习