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中文精选》