【RDMA】12. RDMA之Verbs|OFED
目录
CMA(Connection Management Abstraction)
Verbs直译过来是“动词”的意思,它在RDMA领域中有两种含义:
1) 由IB规范所描述的一组抽象定义,规定了各厂商的软硬件在各种Verbs下应该执行的动作或者表现出的行为,IB规范并未规定如何编程实现这些Verbs,在这种含义下,Verbs是与操作系统无关的。
举个例子,IB规范要求所有RDMA设备必须支持Create QP的行为(IB 规范11.2.5.1):
描述:
为指定的设备创建一个QP。
用户必须指定一组用于初始化QP的属性。
如果创建QP所需的属性有非法值或者缺失,那么应该返回错误,该QP不会被创建;如果成功, 那么返回该QP的指针和QPN。
……
输入:
设备指针;
SQ关联到的CQ;
RQ关联到的CQ,如果是XRC的INI QP,则可以不携带此参数;
……
输出:
新创建的QP的指针;
QP Number;
SQ的最大WR容量。
……
可以看出IB规范中的Verbs,就像教科书中对一个概念进行定义,讲的是“需要支持什么,但具体怎么实现我不做规定”。
2) 由OpenFabrics推动实现的一组RDMA应用编程接口(API)。既然是API,那么必然和运行的操作系统相关。Verbs API有Linux版本以及Windows版本(Windows版很久没有更新了)。
我们还是以Create QP为例,下文引用自Linux用户态Verbs API的帮助文档(ibv_create_qp(3): create/destroy queue pair):
名称:
ibv_create_qp - create a queue pair (QP)
概要:
#include <infiniband/verbs.h>
struct ibv_qp ibv_create_qp(struct ibv_pd pd, struct ibv_qp_init_attr *qp_init_attr);
描述:
ibv_create_qp()通过一个关联的PD创建一个QP,参数qp_init_attr是一个ibv_qp_init_attr类型的结构体,其定义在<infiniband/verbs.h>中。
struct ibv_qp_init_attr {
struct ibv_cq *send_cq; /* CQ to be associated with the Send Queue (SQ) */
struct ibv_cq *recv_cq; /* CQ to be associated with the Receive Queue (RQ) */
struct ibv_srq *srq; /* SRQ handle if QP is to be associated with an SRQ, otherwise NULL */
struct ibv_qp_cap cap; /* QP capabilities */
enum ibv_qp_type qp_type; /* QP Transport Service Type: IBV_QPT_RC, IBV_QPT_UC, or IBV_QPT_UD */
...
};
函数ibv_create_qp()会更新qp_init_attr->cap struct的内容,返回创建的QP所真正支持的规格……
返回值:
ibv_create_qp()返回被创建的QP的指针,或者在失败时返回NULL。QPN将在返回的指针所指向的结构体中。
可见Verbs API即是对IB规范中的Verbs定义的具体软件实现。
Verbs的第一种语义直接查阅IB规范的第11章即可,里面做了非常详细的描述。
本文介绍的是第二种语义,包含Verbs API是什么,如何和硬件产生交互,我们如何通过Verbs API来编写RDMA程序。如无特殊说明,下文中的Verbs均特指Verbs API。
相关名词解释
rdma-core
指开源RDMA用户态软件协议栈,包含用户态框架、各厂商用户态驱动、API帮助手册以及开发自测试工具等。
rdma-core在github上维护,我们的用户态Verbs API实际上就是它实现的。
kernel RDMA subsystem
指开源的Linux内核中的RDMA子系统,包含RDMA内核框架及各厂商的驱动。
RDMA子系统跟随Linux维护,是内核的的一部分。一方面提供内核态的Verbs API,一方面负责对接用户态的接口。
OFED
全称为OpenFabrics Enterprise Distribution,是一个开源软件包集合,其中包含内核框架和驱动、用户框架和驱动、以及各种中间件、测试工具和API文档。
开源OFED由OFA组织负责开发、发布和维护,它会定期从rdma-core和内核的RDMA子系统取软件版本,并对各商用OS发行版进行适配。除了协议栈和驱动外,还包含了perftest等测试工具。
下图为OFA给出的OFED的概览:
除了开源OFED之外,各厂商也会提供定制版本的OFED软件包,比如华为的HW_OFED和Mellanox的MLNX_OFED。这些定制版本基于开源OFED开发,由厂商自己测试和维护,会在开源软件包基础上提供私有的增强特性,并附上自己的配置、测试工具等。
以上三者是包含关系。无论是用户态还是内核态,整个RDMA社区非常活跃,框架几乎每天都在变动,都是平均每两个月一个版本。而OFED会定期从两个社区中取得代码,进行功能和兼容性测试后发布版本,时间跨度较大,以年为单位计。
Verbs API是什么
Verbs API是一组用于使用RDMA服务的最基本的软件接口,也就是说业界的RDMA应用,要么直接基于这组API编写,要么基于在Verbs API上又封装了一层接口的各种中间件编写。
Verbs API向用户提供了有关RDMA的一切功能,典型的包括:注册MR、创建QP、Post Send、Poll CQ等等。
对于Linux系统来说,Verbs的功能由rdma-core和内核中的RDMA子系统提供,分为用户态Verbs接口和内核态Verbs接口,分别用于用户态和内核态的RDMA应用。
结合上一部分的内容,我们给出一个OFED的全景:
广义的Verbs API主要由两大部分组成:
(mellanx又推出新接口:vma)
IB_VERBS
接口以ibv_xx(用户态)或者ib_xx(内核态)作为前缀,是最基础的编程接口,使用IB_VERBS就足够编写RDMA应用了。
比如:
- ibv_create_qp() 用于创建QP
- ibv_post_send() 用于下发Send WR
- ibv_poll_cq() 用于从CQ中轮询CQE
RDMA_CM
rdma_cm和Verbs的区别:
https://www.spinics.net/lists/linux-rdma/msg79591.html
Could I think it like below? I'm wondering how to use RDMA_CM API to make IB/RoCEv1 device work.
1) VPI provides lower layer control API for the devices which implement IB/RoCEv1/RoCEv2/iWARP
2) RDMA_CM use parts of VPI and other library API to make it convenient to use the devices which implement RoCEv2/iWARP
For exmaple, rdma_get_devices do some extra things e.g. ucma_set_af_ib_support, besides calling ibv_open_device.
以rdma_为前缀,主要分为两个功能:
CMA(Connection Management Abstraction)
在Socket和Verbs API基础上实现的,用于CM建链并交换信息的一组接口。CM建链是在Socket基础上封装为QP实现,从用户的角度来看,是在通过QP交换之后数据交换所需要的QPN,Key等信息。
比如:
- rdma_listen()用于监听链路上的CM建链请求。
- rdma_connect()用于确认CM连接。
CM VERBS
RDMA_CM也可以用于数据交换,相当于在verbs API上又封装了一套数据交换接口。
比如:
- rdma_post_read()可以直接下发RDMA READ操作的WR,而不像ibv_post_send(),需要在参数中指定操作类型为READ。
- rdma_post_ud_send()可以直接传入远端QPN,指向远端的AH,本地缓冲区指针等信息触发一次UD SEND操作。
上述接口虽然方便,但是需要配合CMA管理的链路使用,不能配合Verbs API使用。
Verbs API除了IB_VERBS和RDMA_CM之外,还有MAD(Management Datagram)接口等。
需要注意的是,软件栈中的Verbs API具体实现和IB规范中的描述并不是完全一致的。IB规范迭代较慢,而软件栈几乎每天都有变化,所以编写应用或者驱动程序时,应以软件栈API文档中的描述为准。
狭义的Verbs API专指以ibv_/ib_为前缀的用户态Verbs接口,因为RDMA的典型应用是在用户态,下文主要介绍用户态的Verbs API。
设计Verbs API的原因
传统以太网的用户,基于Socket API来编写应用程序;而RDMA的用户,基于Verbs API来编写应用程序。
Verbs API支持IB/iWARP/RoCE三大RDMA协议,通过统一接口,让同一份RDMA程序程序可以无视底层的硬件和链路差异运行在不同的环境中。
Verbs API所包含的内容
用户态Verbs API主要包含两个层面的功能:
为方便讲解,下面对各接口的形式做了简化,格式为"返回值1,返回值2 函数名(参数1, 参数2)"
1)控制面:
- 设备管理:
- device_listibv_get_device_list()
用户获取可用的RDMA设备列表,会返回一组可用设备的指针。
-
- device_contextibv_open_device(device)
打开一个可用的RDMA设备,返回其上下文指针(这个指针会在以后用来对这个设备进行各种操作)。
-
- device_attr, errno ibv_query_device(device_context*)
查询一个设备的属性/能力,比如其支持的最大QP,CQ数量等。返回设备的属性结构体指针,以及错误码。
- 资源的创建,查询,修改和销毁:
- pdibv_alloc_pd(device_context)
申请PD。该函数会返回一个PD的指针。
-
- mribv_reg_mr(pd, addr, length, access_flag)
注册MR。用户传入要注册的内存的起始地址和长度,以及这个MR将要从属的PD和它的访问权限(本地读/写,远端读/写等),返回一个MR的指针给用户。
-
- cq ibv_create_cq(device_context, cqe_depth, ...)
创建CQ。用户传入CQ的最小深度(驱动实际申请的可能比这个值大),然后该函数返回CQ的指针。
-
- qpibv_create_qp(pd, qp_init_attr)
创建QP。用户传入PD和一组属性(包括RQ和SQ绑定到的CQ、QP绑定的SRQ、QP的能力、QP类型等),向用户返回QP的指针。
-
- errnoibv_modiy_qp(qp, attr, attr_mask)
修改QP。用户传入QP的指针,以及表示要修改的属性的掩码和要修改值。修改的内容可以是QP状态、对端QPN、QP能力、端口号和重传次数等等。如果失败,该函数会返回错误码。
Modify QP最重要的作用是让QP在不同的状态间迁移,完成RST-->INIT-->RTR-->RTS的状态机转移后才具备下发Send WR的能力。也可用来将QP切换到ERROR状态。
-
- errnoibv_destroy_qp(qp)
销毁QP。即销毁QP相关的软件资源。其他的资源也都有类似的销毁接口。
- 中断处理:
- event_info, errnoibv_get_async_event(device_context)
从事件队列中获取一个异步事件,返回异步事件的信息(事件来源,事件类型等)以及错误码。
- 连接管理
- rdma_xxx()
用于CM建链,不在本文展开讲。
-
- ...
2)数据面:
- 下发WR
- bad_wr, errnoibv_post_send(qp, wr)
向一个QP下发一个Send WR,参数wr是一个结构体,包含了WR的所有信息。包括wr_id、sge数量、操作码(SEND/WRITE/READ等以及更细分的类型)。WR的结构会根据服务类型和操作类型有所差异,比如RC服务的WRITE和READ操作的WR会包含远端内存地址和R_Key,UD服务类型会包含AH,远端QPN和Q_Key等。
WR经由驱动进一步处理后,会转化成WQE下发给硬件。
出错之后,该接口会返回出错的WR的指针以及错误码。
-
- bad_wr, errno ibv_post_recv(qp, wr)
同ibv_post_send,只不过是专门用来下发RECV操作的WR的接口。
- 获取WC
- num, wc ibv_poll_cq(cq, max_num)
从完成队列CQ中轮询CQE,用户需要提前准备好内存来存放WC,并传入可以接收多少个WC。该接口会返回一组WC结构体(其内容包括wr_id,状态,操作码,QPN等信息)以及WC的数量。
使用Verbs API编写RDMA应用程序
查看接口定义
内核态
内核态Verbs接口没有专门的API手册,编程时需要参考头文件中的函数注释。声明这些接口的头文件位于内核源码目录中的:
.../include/rdma/ib_verbs.h
比如ib_post_send()接口:
函数注释中有明确介绍该函数的作用,输入、输出参数以及返回值。
用户态
有多种方法查阅用户态的Verbs API:
在线查阅最新man page
用户态的Verbs API手册跟代码在一个仓库维护,手册地址:
https://github.com/linux-rdma/rdma-core/tree/master/libibverbs/man
这里是按照Linux的man page格式编写的源文件,直接看源文件可能不太直观。有很多在线的man page网站可以查阅这些接口的说明,比如官方的连接:
https://man7.org/linux/man-pages/man3/ibv_post_send.3.html
也有一些其他非官方网页,支持在线搜索:
https://linux.die.net/man/3/ibv_post_send
查阅系统man page
如果你使用的商用OS安装了rdma-core或者libibverbs库,那么可以直接用man命令查询接口:
man ibv_post_send
查询Mellanox的编程手册
《RDMA Aware Networks Programming User Manual Rev 1.7》,最新版是2015年更新的。该手册写的比较详尽,并且附有示例程序,但是可能与最新的接口有一些差异。
https://www.mellanox.com/related-docs/prod_software/RDMA_Aware_Programming_user_manual.pdf
包含头文件
按需包含以下头文件:
#include <infiniband/verbs.h> // IB_VERBS 基础头文件
#include <rdma/rdma_cma.h> // RDMA_CM CMA 头文件 用于CM建链
#include <rdma/rdma_verbs.h> // RDMA_CM VERBS 头文件 用于使用基于CM的Verbs接口
编写应用
在RDMA中我们使用内核态驱动建立一个数据通道。我们称之为命令通道(Command Channel)。使用命令通道,我们能够建立一个数据通道(Data Channel),该通道允许我们在搬运数据的时候完全绕过内核。一旦建立了这种数据通道,我们就能直接读写数据缓冲区。
建立数据通道的API是一种称之为"verbs"的API。
下面附上一个简单的RDMA程序的大致接口调用流程,Client端的程序会发送一个SEND请求给Server端的程序,图中的接口上文中都有简单介绍。
需要注意的是图中的建链过程是为了交换对端的GID,QPN等信息,可以通过传统的Socket接口实现,也可以通过本文中介绍的CMA接口实现。
图中特意列出了多次modify QP的流程,一方面是把建链之后交互得到的信息存入QPC中(即QP间建立连接的过程),另一方面是为了使QP处于具备收/发能力状态才能进行下一步的数据交互。具体状态机的内容请回顾9. RDMA之Queue Pair
编译 & 执行
不在本文讨论范围内。
官方示例程序
rdma-core的源码目录下,为libibverbs和librdmacm都提供了简单的示例程序,大家编程时可以参考。
libibverbs
位于rdma-core/libibverbs/examples/目录下,都使用最基础的IB_VERBS接口实现,所以建链方式都是基于Socket的。
- asyncwatch.c 查询指定RDMA设备是否有异步事件上报
- device_list.c 列出本端RDMA设备列表
- devinfo.c 查询并打印本端RDMA设备详细信息,没有双端数据交互
- rc_pingpong.c 基于RC服务类型双端数据收发示例
- srq_pingpong.c 基于RC服务类型双端数据收发示例,与上一个示例程序的差异是使用了SRQ而不是普通的RQ。
- ud_pingpong.c 基于UD服务类型双端数据收发示例
- ud_pingpong.c 基于UC服务类型双端数据收发示例
- xsrq_pingpong.c 基于XRC服务类型双端数据收发示例
librdmacm
位于rdma-core/librdmacm/examples/目录下:
https://github.com/linux-rdma/rdma-core
- rdma_client/server.c 基础示例,通过CM建链并使用CM VERBS进行数据收发。
该目录剩下的程序就没有研究了。
参考文献
[1] RDMA Aware Networks Programming User Manual Rev 1.7
[2] part1-OFA_Training_Sept_2016