用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_addrrkey等写入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 与官方基准同一量级,代码实现无明显性能缺陷
相关推荐
Discipline~Hai1 小时前
Linux网络编程05-sqlite3数据库
linux·c语言·网络·数据库·sqlite·linux应用软件编程
Huangjin007_1 小时前
【Linux 系统篇(十七)】进程(五) :进程切换、O(1) 调度算法
linux·运维·服务器
weixin_464307631 小时前
linux虚拟机断网
linux·运维·服务器
λqaq71 小时前
Docker 容器入门基础:Cloud Studio 在线 Linux 环境上手
linux·运维·docker
en.en..2 小时前
LVGL 普通函数指针与结构体函数指针原理
linux·windows·microsoft
byte轻骑兵2 小时前
【BlueZ 】Adapter 模块:蓝牙适配器(主设备)的初始化与核心逻辑
linux·人工智能·bluez·电脑蓝牙·嵌入式蓝牙
流浪0012 小时前
Linux系统篇27——通信(四):System V 消息队列 + 信号量:管道之后,Linux 还藏了哪两样 IPC
linux·运维
Java后端的Ai之路2 小时前
23、Python - 策略模式
linux·python·策略模式
tangwangbi12 小时前
Linux 系统配置文件:/etc/profile、~/.bashrc 和 ~/.bash_profile 三者之间的区别与作用
linux·运维·bash