乐趣区

关于rust:Datenlord-Rust实现RDMA异步编程一基于epoll实现RDMA-异步操作

RDMA 是一套高性能网络协议栈,多用于高性能计算、高性能存储畛域。RDMA 的 library 是用 C 实现的,然而没有很好用的 Rust 的 binding,不不便 Rust 开发者应用。于是咱们正在封装一层合乎 Rust 格调、便于 Rust 开发者应用的 RDMA Rust binding。特地的,异步编程是近几年很受关注的编程形式,用 Rust 异步编程来实现 IO 操作,能够防止操作系统的过程上下文切换,进步性能,而且 Rust 的异步编程框架也在逐渐成熟和欠缺。本系列文章探讨下如何用异步的形式实现 RDMA 的操作。本文先探讨下如何基于 Linux 的 epoll 机制实现 RDMA 异步操作,后续的文章再探讨如何用 Rust 异步编程来实现 RDMA 异步操作。

RDMA 操作简介

RDMA 的编程模型是基于音讯的形式来实现网络传输,并且用队列来治理待发送的音讯和接管到的音讯。RDMA 的网络传输相干操作基本上都是跟队列相干的操作:比方把要发送的音讯放入发送队列,音讯发送实现后在实现队列里放一个发送实现音讯,以供用户程序查问音讯发送状态;再比方接管队列里收到音讯,也要在实现队列里放个接管实现音讯,以供用户程序查问有新音讯要解决。

由下面的形容能够看出 RDMA 的队列分为几种:发送队列 Send Queue (SQ),接管队列 Receive Queue(RQ),和实现队列 Completion Queue (CQ)。其中 SQ 和 RQ 统称工作队列 Work Queue (WQ),也称为 Queue Pair (QP)。此外,RDMA 提供了两个接口,ibv_post_send 和 ibv_post_recv,由用户程序调用,别离用于发送音讯和接管音讯:

  • 用户程序调用 ibv_post_send 把发送申请 Send Request (SR)插入 SQ,成为发送队列的一个新的元素 Send Queue Element (SQE);
  • 用户程序调用 ibv_post_recv 把接管申请 Receive Request (RR)插入 RQ,成为接管队列的一个新元素 Receive Queue Element (RQE)。

SQE 和 RQE 也统称工作队列元素 Work Queue Element (WQE)。

当 SQ 里有音讯发送实现,或 RQ 有接管到新音讯,RDMA 会在 CQ 里放入一个新的实现队列元素 Completion Queue Element (CQE),用以告诉用户程序。用户程序有两种同步的形式来查问 CQ:

  • 用户程序调用 ibv_cq_poll 来轮询 CQ,一旦有新的 CQE 就能够及时失去告诉,然而这种轮询形式很耗费 CPU 资源;
  • 用户程序在创立 CQ 的时候,指定一个实现事件通道 ibv_comp_channel,而后调用 ibv_get_cq_event 接口期待该实现事件通道来告诉有新的 CQE,如果没有新的 CQE,则调用 ibv_get_cq_event 时产生阻塞,这种办法比轮询要节俭 CPU 资源,然而阻塞会升高程序性能。

对于 RDMA 的 CQE,有个须要留神的中央:对于 RDMA 的 Send 和 Receive 这种双边操作,发送端在发送实现后能收到 CQE,接收端在接管实现后也能收到 CQE;对于 RDMA 的 Read 和 Write 这种单边操作,比方节点 A 从节点 B 读数据,或节点 A 往节点 B 写数据,只有发动 Read 和 Write 操作的一端,即节点 A 在操作完结后能收到 CQE,另一端节点 B 齐全不会感知到节点 A 发动的 Read 或 Write 操作,节点 B 也不会收到 CQE。

Linux epoll 异步机制简介

Linux 的 epoll 机制是 Linux 提供的异步编程机制。epoll 专门用于解决有大量 IO 操作申请的场景,查看哪些 IO 操作就绪,使得用户程序不用阻塞在未就绪 IO 操作上,而只解决就绪 IO 操作。epoll 比 Linux 之前的 select 和 poll 这两种异步机制要弱小,epoll 特地适宜有大量 IO 操作的场景,比方 RDMA 的场景,每个 RDMA 节点同时有很多队列,用于大量传输数据,那么就能够用 epoll 来查问每个 CQ,及时取得 RDMA 音讯的发送和接管状况,同时防止同步形式查问 CQ 的毛病,要么用户程序耗费大量 CPU 资源,要么用户程序被阻塞。

Linux 的 epoll 机制提供了三个 API 接口:

  • epoll_create 用于创立 epoll 实例,返回 epoll 实例的句柄;
  • epoll_ctl 用于给 epoll 实例减少、批改、删除待查看的 IO 操作事件;
  • epoll_wait 用于查看每个通过 epoll_ctl 注册到 epoll 实例的 IO 操作,看每个 IO 操作是否就绪 / 有冀望的事件产生。

具体的 epoll 这三个接口的应用,前面联合代码示例来解说。这里先解释下 epoll 的 IO 事件查看规定。如下图所示,epoll 有两种查看规定:边际触发 Edge Trigger (ET),和电平触发 Level Trigger (LT)。边际触发和电平触发源自信号处理畛域。边际触发指信号一发生变化就触发事件,比方从 0 变到 1 就触发事件、或者从 1 到 0 就触发事件;电平触发指只有信号的状态处于特定状态就触发事件,比方高电平就始终触发事件,而低电平不触发事件。

对应到 epoll,电平触发指的是,只有 IO 操作处于特定的状态,就会始终告诉用户程序。比方当 socket 有数据可读时,用户程序调用 epoll_wait 查问到该 socket 有收到数据,只有用户程序没有把该 socket 上次收到的数据读完,每次调用 epoll_wait 都会告诉用户程序该 socket 有数据可读;即当 socket 处于有数据可读的状态,就会始终告诉用户程序。而 epoll 的边际触发指的是 epoll 只会在 IO 操作的特定事件产生后告诉一次。比方 socket 有收到数据,用户程序 epoll_wait 查问到该 socket 有数据可读,不论用户程序有没有读完该 socket 这次收到的数据,用户程序下次调用 epoll_wait 都不会再告诉该 socket 有数据可读,除非这个 socket 再次收到了新的数据;即仅当 socket 每次收到新数据才告诉用户程序,并不关怀 socket 以后是否有数据可读。

RDMA 实现队列 CQ 读取 CQE 的同步和异步办法

本节用 RDMA 读取 CQ 的操作为例展现如何基于 epoll 实现异步操作。先介绍下 RDMA 用轮询和阻塞的形式读取 CQ,再介绍基于 epoll 的异步读取 CQ 的办法。下文的代码仅作为示例,并不能编译通过。

RDMA 轮询形式读取 CQE

RDMA 轮询形式读取 CQ 非常简单,就是不停调用 ibv_poll_cq 来读取 CQ 里的 CQE。这种形式可能最快取得新的 CQE,间接用户程序轮询 CQ,而且也不须要内核参加,然而毛病也很显著,用户程序轮询耗费大量 CPU 资源。

loop {
    // 尝试读取一个 CQE
    poll_result = ibv_poll_cq(cq, 1, &mut cqe);
    if poll_result != 0 {// 解决 CQE}
}

RDMA 实现事件通道形式读取 CQE

RDMA 用实现事件通道读取 CQE 的形式如下:

  • 用户程序通过调用 ibv_create_comp_channel 创立实现事件通道;
  • 接着在调用 ibv_create_cq 创立 CQ 时关联该实现事件通道;
  • 再通过调用 ibv_req_notify_cq 来通知 CQ 当有新的 CQE 产生时从实现事件通道来告诉用户程序;
  • 而后通过调用 ibv_get_cq_event 查问该实现事件通道,没有新的 CQE 时阻塞,有新的 CQE 时返回;
  • 接下来用户程序从 ibv_get_cq_event 返回之后,还要再调用 ibv_poll_cq 从 CQ 里读取新的 CQE,此时调用 ibv_poll_cq 一次就好,不须要轮询。

RDMA 用实现事件通道读取 CQE 的代码示例如下:

// 创立实现事件通道
let completion_event_channel = ibv_create_comp_channel(...);
// 创立实现队列,并关联实现事件通道
let cq = ibv_create_cq(completion_event_channel, ...);

loop {
    // 设置 CQ 从实现事件通道来告诉下一个新 CQE 的产生
    ibv_req_notify_cq(cq, ...);
    // 通过实现事件通道查问 CQ,有新的 CQE 就返回,没有新的 CQE 则阻塞
    ibv_get_cq_event(completion_event_channel, &mut cq, ...);
    // 读取一个 CQE
    poll_result = ibv_poll_cq(cq, 1, &mut cqe);
    if poll_result != 0 {// 解决 CQE}
    // 确认一个 CQE
    ibv_ack_cq_events(cq, 1);
}

用 RDMA 实现事件通道的形式来读取 CQE,实质是 RDMA 通过内核来告诉用户程序 CQ 里有新的 CQE。事件队列是通过一个设施文件,/dev/infiniband/uverbs0(如果有多个 RDMA 网卡,则每个网卡对应一个设施文件,序号从 0 开始递增),来让内核通过该设施文件告诉用户程序有事件产生。用户程序调用 ibv_create_comp_channel 创立实现事件通道,其实就是关上上述设施文件;用户程序调用 ibv_get_cq_event 查问该实现事件通道,其实就是读取关上的设施文件。然而这个设施文件只用于做事件告诉,告诉用户程序有新的 CQE 可读,但并不能通过该设施文件读取 CQE,用户程序还要是调用 ibv_poll_cq 来从 CQ 读取 CQE。

用实现事件通道的形式来读取 CQE,比轮询的办法要节俭 CPU 资源,然而调用 ibv_get_cq_event 读取实现事件通道会产生阻塞,进而影响用户程序性能。

基于 epoll 异步读取 CQE

下面提到用 RDMA 实现事件通道的形式来读取 CQE,实质是用户程序通过事件队列关上 /dev/infiniband/uverbs0 设施文件,并读取内核产生的对于新 CQE 的事件告诉。从实现事件通道 ibv_comp_channel 的定义能够看出,外面蕴含了一个 Linux 文件描述符,指向关上的设施文件:

pub struct ibv_comp_channel {
    ...
    pub fd: RawFd,
    ...
}

于是能够借助 epoll 机制来查看该设施文件是否有新的事件产生,防止用户程序调用 ibv_get_cq_event 读取实现事件通道时(即读取该设施文件时)产生阻塞。

首先,用 fcntl 来批改实现事件通道里设施文件描述符的 IO 形式为非阻塞:

// 创立实现事件通道
let completion_event_channel = ibv_create_comp_channel(...);
// 创立实现队列,并关联实现事件通道
let cq = ibv_create_cq(completion_event_channel, ...);
// 获取设施文件描述符以后打开方式
let flags =
    libc::fcntl((*completion_event_channel).fd, libc::F_GETFL);
// 为设施文件描述符减少非阻塞 IO 形式
libc::fcntl((*completion_event_channel).fd,
    libc::F_SETFL,
    flags | libc::O_NONBLOCK
);

接着,创立 epoll 实例,并把要查看的事件注册给 epoll 实例:

use nix::sys::epoll;

// 创立 epoll 实例
let epoll_fd = epoll::epoll_create()?;
// 实现事件通道里的设施文件描述符
let channel_dev_fd = (*completion_event_channel).fd;
// 创立 epoll 事件实例,并关联设施文件描述符,// 当该设施文件有新数据可读时,用边际触发的形式告诉用户程序
let mut epoll_ev = epoll::EpollEvent::new(
    epoll::EpollFlags::EPOLLIN | epoll::EpollFlags::EPOLLET,
    channel_dev_fd
);
// 把创立好的 epoll 事件实例,注册到之前创立的 epoll 实例
epoll::epoll_ctl(
    epoll_fd,
    epoll::EpollOp::EpollCtlAdd,
    channel_dev_fd,
    &mut epoll_ev,
)

下面代码有两个留神的中央:

  • EPOLLIN 指的是要查看设施文件是否有新数据 / 事件可读;
  • EPOLLET 指的是 epoll 用边际触发的形式来告诉。

而后,循环调用 epoll_wait 查看设施文件是否有新数据可读,有新数据可读阐明有新的 CQE 产生,再调用 ibv_poll_cq 来读取 CQE:

let timeout_ms = 10;
// 创立用于 epoll_wait 查看的事件列表
let mut event_list = [epoll_ev];
loop {
    // 设置 CQ 从实现事件通道来告诉下一个新 CQE 的产生
    ibv_req_notify_cq(cq, ...);
    // 调用 epoll_wait 查看是否有冀望的事件产生
    let nfds = epoll::epoll_wait(epoll_fd, &mut event_list, timeout_ms)?;
    // 有冀望的事件产生
    if nfds > 0 {
        // 通过实现事件通道查问 CQ,有新的 CQE 就返回,没有新的 CQE 则阻塞
        ibv_get_cq_event(completion_event_channel, &mut cq, ...);
        // 循环读取 CQE,直到 CQ 读空
        loop {
            // 读取一个 CQE
            poll_result = ibv_poll_cq(cq, 1, &mut cqe);
            if poll_result != 0 {
                // 解决 CQE
                ...
                // 确认一个 CQE
                ibv_ack_cq_events(cq, 1);
            } else {break;}
        }
    }
}

下面代码有个要留神的中央,因为 epoll 是用边际触发,所以每次有新 CQE 产生时,都要调用 ibv_poll_cq 把 CQ 队列读空。思考如下场景,同时有多个新的 CQE 产生,然而 epoll 边际触发只告诉一次,如果用户程序收到告诉后没有读空 CQ,那 epoll 也不会再产生新的告诉,除非再有新的 CQE 产生,epoll 才会再次告诉用户程序。

总之,本文用 epoll 机制实现 RDMA 异步读取 CQE 的例子,展现了如何实现 RDMA 的异步操作。RDMA 还有相似的操作,都能够基于 epoll 机制实现异步操作。

对 Rust 和 RDMA 感兴趣的敌人,能够关注咱们的开源我的项目 https://github.com/datenlord/…

作者 | 王璞 
前期编辑 | 张汉东
转自《Rust Magazine 中文精选》

退出移动版