一、 概述
Verbs api是由OpenFabrics推动实现的一组RDMA应用编程接口(API),类似于socket api,它提供了一组函数和数据结构,用于管理RDMA通信和数据传输。verbs api一般由两部分构成:
IB_VERBS
以ibv_xx(用户态)或者ib_xx(内核态)为前缀,主要用于收发数据,当然也可用于连接管理,是基础的底层编程接口,程序可以完全基于IB_VERBS编写。
RDMA_CM
以rdma_xx为前缀,在IB_VERBS基础上封装实现的一组接口,主要用于管理连接,让通信双方可以确定和交换相关信息,RDMA_CM也提供了相应的接口用于收发数据。
二、 使用方法
下文将从基本的server与client连接,client向server发送信息来描述RDMA Verbs API的使用方法
1. server连接前准备
struct sockaddr_in addr;
struct rdma_cm_event *event = NULL;
struct rdma_cm_id *listener = NULL;
struct rdma_event_channel *ec = NULL;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
ec = rdma_create_event_channel();
rdma_create_id(ec, &listener, NULL, RDMA_PS_TCP);
rdma_bind_addr(listener, (struct sockaddr *)&addr);
// 第二个参数10代表传入连接请求的积压数为10
rdma_listen(listener, 10);
while (rdma_get_cm_event(ec, &event) == 0) {
struct rdma_cm_event event_copy;
memcpy(&event_copy, event, sizeof(*event));
rdma_ack_cm_event(event);
// ...
}
- rdma_create_event_channel()
创建用于报告事件的事件通道,client和server之间通过返回值rdma_event_channel结构体来传递事件信息。
- rdma_create_id()
创建用于跟踪通信信息的rdma_cm_id,类似于socket,通过关联的事件通道报告 rdma_cm_id 上的通信事件。参数RDMA_PS_TCP表示供可靠的,面向连接的 QP 通信。
- rdma_bind_addr()
将源地址与 rdma_cm_id 相关联,如果绑定到特定的本地地址,则 rdma_cm_id 也将绑定到本地 RDMA 设备。
- rdma_listen()
监听client端的连接请求。
- rdma_get_cm_event()
等待待处理的事件,如果没有事件则阻塞调用,直到收到事件。 必须通过调用 rdma_ack_cm_event来确认报告的所有事件。
- rdma_ack_cm_event()
释放通信事件,由rdma_get_cm_event分配的所有事件必须释放,成功获取和确认之间应该存在一对一的对应关系。
2. client连接前准备
struct addrinfo *addr;
struct rdma_cm_event *event = NULL;
struct rdma_cm_id *conn= NULL;
struct rdma_event_channel *ec = NULL;
ec = rdma_create_event_channel();
rdma_create_id(ec, &conn, NULL, RDMA_PS_TCP);
rdma_resolve_addr(conn, NULL, addr->ai_addr, TIMEOUT_IN_MS);
while (rdma_get_cm_event(ec, &event) == 0) {
struct rdma_cm_event event_copy;
memcpy(&event_copy, event, sizeof(*event));
rdma_ack_cm_event(event);
// ...
}
- rdma_create_event_channel()
与server端类似,建立事件通道
- rdma_create_id()
与server端类似,创建用于跟踪通信信息的rdma_cm_id。
- rdma_resolve_addr()
将目标源地址从 IP 地址解析为 RDMA 地址,rdma_cm_id 绑定到该地址。
- rdma_get_cm_event()
与server端类似。
- rdma_ack_cm_event()
与server端类似。
3. client发起连接请求
解析完IP地址后,事件通道会报告RDMA_CM_EVENT_ADDR_RESOLVED事件,之后还需要进行RDMA上下文创建和QP创建等准备工作,之后再解析RDMA路由。
//自定义的RDMA连接结构体
struct connection {
// 指向该连接使用的rdma id的指针
struct rdma_cm_id *id;
// 指向该连接使用的qp的指针
struct ibv_qp *qp;
// 指向接收队列内存区域的指针
struct ibv_mr *recv_mr;
// 指向发送队列内存区域的指针
struct ibv_mr *send_mr;
// 接收队列内存区域起始地址
char *recv_region;
// 发送队列内存区域起始地址
char *send_region;
// 已完成的请求数
int num_completions;
};
int on_addr_resolved(struct rdma_cm_id *id)
{
struct ibv_qp_init_attr qp_attr;
struct connection *conn;
// 建立RDMA上下文
build_context(id->verbs);
// 初始化QP属性
memset(qp_attr, 0, sizeof(*qp_attr));
qp_attr->send_cq = s_ctx->cq;
qp_attr->recv_cq = s_ctx->cq;
qp_attr->qp_type = IBV_QPT_RC;
qp_attr->cap.max_send_wr = 10;
qp_attr->cap.max_recv_wr = 10;
qp_attr->cap.max_send_sge = 1;
qp_attr->cap.max_recv_sge = 1;
rdma_create_qp(id, s_ctx->pd, &qp_attr);
id->context = conn = (struct connection *)malloc(sizeof(struct connection));
conn->id = id;
conn->qp = id->qp;
conn->num_completions = 0;
// 注册内存区域
register_memory(conn);
// 创建接收请求
post_receives(conn);
rdma_resolve_route(id, TIMEOUT_IN_MS);
return 0;
}
- rdma_create_qp()
分配与指定的 rdma_cm_id 相关联的 QP,并将其转换为发送和 接收。创建的 QP 的实际功能和属性将通过qp_attr 参数返回给用户。
- rdma_resolve_route()
将 RDMA 路由解析为目标地址以建立连接。
build_context
s_ctx = (struct context *)malloc(sizeof(struct context));
s_ctx->ctx = verbs;
s_ctx->pd = ibv_alloc_pd(s_ctx->ctx);
s_ctx->comp_channel =ibv_create_comp_channel(s_ctx->ctx);
s_ctx->cq = ibv_create_cq(s_ctx->ctx, 10, NULL,s_ctx->comp_channel, 0));
ibv_req_notify_cq(s_ctx->cq, 0);
- ibv_alloc_pd()
创建一个保护域(PD),返回指向保护域的指针。
- ibv_create_comp_channel()
创建完成通道,完成信道(Completion channel,CC) 是用户在新的完成队列事件(CQE)已被放置在完成队列(CQ)上时接收通知的机制。
- ibv_create_cq()
创建完成队列(CQ),返回指向CQ的指针。
- ibv_req_notify_cq()
为指示的完成队列(CQ)提供通知机制,当完成队列条目 (CQE)被放置在 CQ 上时,完成事件将被发送到与 CQ 相关联的完成信道(CC)。
register_memory
conn->send_region = malloc(BUFFER_SIZE);
conn->recv_region = malloc(BUFFER_SIZE);
// 分别为发送和接收注册内存区域
conn->send_mr = ibv_reg_mr(
s_ctx->pd,
conn->send_region,
BUFFER_SIZE,
0);
conn->recv_mr = ibv_reg_mr(
s_ctx->pd,
conn->recv_region,
BUFFER_SIZE,
IBV_ACCESS_LOCAL_WRITE);
- ibv_reg_mr()
注册一个内存区域(MR),将其与保护域(PD)相关联,并为其分配本地和远程密钥(lkey,rkey)。IBV_ACCESS_LOCAL_WRITE表示允许本地主机写访问权限。
post_receives
struct ibv_recv_wr wr, *bad_wr = NULL;
struct ibv_sge sge;
// 填充wr和sge
wr.wr_id = (uintptr_t)conn;
wr.next = NULL;
wr.sg_list = &sge;
wr.num_sge = 1;
sge.addr = (uintptr_t)conn->recv_region;
sge.length = BUFFER_SIZE;
sge.lkey = conn->recv_mr->lkey;
ibv_post_recv(conn->qp, &wr, &bad_wr);
- ibv_post_recv()
post一个receive请求,以便能够顺利接收client端send过来的数据。ibv_post_recv操作最好在链接建立之前进行,以避免通信的另一端在发送数据时出现RNR问题(receiver-not-ready)
建立连接
路由解析完成后,事件通道会报告RDMA_CM_EVENT_ROUTE_RESOLVED事件,然后就可以建立连接了。
int on_route_resolved(struct rdma_cm_id *id)
{
struct rdma_conn_param cm_params;
memset(&cm_params, 0, sizeof(cm_params));
rdma_connect(id, &cm_params);
return 0;
}
- rdma_connect()
启动活动连接请求,对于已连接的rdma_cm_id,会启动对远程目标的连接请求。请求发送后server端会收到RDMA_CM_EVENT_CONNECT_REQUEST事件,事件对象的内部可通过private_data属性来设置一些私有信息。
发送连接请求后,client继续阻塞调用rdma_get_cm_event,等待server端接收连接请求,接收成功会返回RDMA_CM_EVENT_ESTABLISHED事件。
4. server端accpet连接请求
收到client端的RDMA_CM_EVENT_CONNECT_REQUEST事件后,首先从事件对象中取出rdma_cm_id来作为与对端通信的唯一标识,accpet该请求。
int on_connect_request(struct rdma_cm_id *id)
{
struct ibv_qp_init_attr qp_attr;
struct rdma_conn_param cm_params;
// 自定义RDMA连接结构体,同client
struct connection *conn;
// 和client相同的创建RDMA上下文,QP,注册内存,post_receive等操作
// ...
memset(&cm_params, 0, sizeof(cm_params));
rdma_accept(id, &cm_params);
return 0;
}
- rdma_accept()
对client的连接请求进行接收
5. client端发送数据
int on_connection(void *context)
{
struct connection *conn = (struct connection *)context;
struct ibv_send_wr wr, *bad_wr = NULL;
struct ibv_sge sge;
// 将发送信息写入发送内存区域
snprintf(conn->send_region, BUFFER_SIZE, "message from active/client side with pid %d", getpid());
memset(&wr, 0, sizeof(wr));
// 填充wr和sge
wr.wr_id = (uintptr_t)conn;
wr.opcode = IBV_WR_SEND;
wr.sg_list = &sge;
wr.num_sge = 1;
wr.send_flags = IBV_SEND_SIGNALED;
sge.addr = (uintptr_t)conn->send_region;
sge.length = BUFFER_SIZE;
sge.lkey = conn->send_mr->lkey;
ibv_post_send(conn->qp, &wr, &bad_wr);
return 0;
}
- ibv_post_send()
将 WR 的链接列表发布到队列对(QP)的发送队列。此操作用于启动包括RDMA操作的所有通信。 WR列表的处理在第一个错误处停止,并且在 bad_wr 中返回指向错误WR的指针。
ibv_post_send处理结束后会向发送队列(SQ)提交一个工作请求(WQE),然后会通知网卡硬件进行后续发送处理。
6. sever端接收数据
server端在建立连接前已经执行过ibv_post_recv操作,可以通过ibv_poll_cq函数对CQ进行轮询来感知CQE生成。
struct ibv_cq *cq;
struct ibv_wc wc;
while (1) {
ibv_get_cq_event(s_ctx->comp_channel, &cq, &ctx);
// 参数1表示一次确认一个事件
ibv_ack_cq_events(cq, 1);
ibv_req_notify_cq(cq, 0);
while (ibv_poll_cq(cq, 1, &wc)) {
if (wc->status != IBV_WC_SUCCESS)
printf("on_completion: status is not IBV_WC_SUCCESS.");
if (wc->opcode & IBV_WC_RECV) {
// 该请求为接收请求
struct connection *conn = (struct connection *)(uintptr_t)wc->wr_id;
// 从接收内存区域中读取client端发送的信息
printf("received message: %s\n", conn->recv_region);
} else if (wc->opcode == IBV_WC_SEND) {
// 该请求为发送请求
printf("send completed successfully.\n");
}
}
}
- ibv_get_cq_event()
等待在指示的完成信道(CC)上有发送通知,这是一个阻塞操作,发送的每个通知必须使用 ibv_ack_cq_events 操作进行确认。ctx: 在 ibv_create_cq 中设置的用户提供的上下文对象。
- ibv_ack_cq_events()
确认从 ibv_get_cq_event 收到的事件。虽然从 ibv_get_cq_event 收到的每个通知都只为一个事件,但用户可以通过一次调用 ibv_ack_cq_events 来确认多个事件。
- ibv_poll_cq()
从完成队列(CQ)中检索 CQE,将CQE存储在工作完成(Work Complete, WC)数组中,返回WC数组中CQE的数量。
接收完对端传来的信息后,需要再次调用ibv_post_recv,以便接收下一个请求。
7. 断开连接
client端完成发送操作后,断开连接
//...
// conn为前文定义的connection结构体
rdma_disconnect(conn->id);
- rdma_disconnect()
断开连接,并将任何关联的 QP 转换为错误状态。成功断开连接后,将在连接的两端生成 RDMA_CM_EVENT_DISCONNECTED 事件。
事件通道获取到RDMA_CM_EVENT_DISCONNECTED事件后,执行RDMA内存释放相关操作。
int on_disconnect(struct rdma_cm_id *id)
{
struct connection *conn = (struct connection *)id->context;
printf("disconnected.\n");
rdma_destroy_qp(id);
ibv_dereg_mr(conn->send_mr);
ibv_dereg_mr(conn->recv_mr);
free(conn->send_region);
free(conn->recv_region);
free(conn);
rdma_destroy_id(id);
}
- rdma_destroy_qp()
销毁在 rdma_cm_id 上分配的 QP。
- ibv_dereg_mr()
释放内存注册区域(MR)。
- rdma_destroy_id()
销毁指定的 rdma_cm_id 并取消任何未完成的异步操作,调用前必须使用 rdma_destroy_qp 销毁关联的 QP。
最后client端关闭事件通道
rdma_destroy_event_channel(ec);
- rdma_destroy_event_channel()
关闭事件通道,释放与事件通道关联的所有资源,并关闭关联的文件描述符。
server端断开连接的方式与client端大致相同,区别在于server端在最后关闭通信通道前需要销毁监听器。
// ...
rdma_destroy_id(listener);
rdma_destroy_event_channel(ec);
三、 总结
上文详细讲述了使用RDMA Verbs API编写基本RDMA Send/Receive操作的方法,介绍了使用到的API及其作用,希望能对准备进行RDMA应用开发工作的人有所帮助。
附:RDMA事件状态表
RDMA_CM_EVENT_ADDR_RESOLVED |
地址解析(rdma_resolve_addr)已成功完成 |
RDMA_CM_EVENT_ADDR_ERROR |
地址解析(rdma_resolve_addr)失败 |
RDMA_CM_EVENT_ROUTE_RESOLVED |
路由解析(rdma_resolve_route)已成功完成 |
RDMA_CM_EVENT_ROUTE_ERROR |
路由解析(rdma_resolve_route)失败 |
RDMA_CM_EVENT_CONNECT_REQUEST |
在被动端生成新的连接请求 |
RDMA_CM_EVENT_CONNECT_RESPONSE |
在活动侧生成,对连接请求的成功响应。 |
RDMA_CM_EVENT_CONNECT_ERROR |
表示尝试建立或建立连接时发生错误。可以在连接的主动或被动侧生成 |
RDMA_CM_EVENT_UNREACHABLE |
在活动端生成,远程端点无法访问或无法响应连接请求 |
RDMA_CM_EVENT_REJECTED |
远程端点拒绝连接请求或响应 |
RDMA_CM_EVENT_ESTABLISHED |
已与远程端点建立连接 |
RDMA_CM_EVENT_DISCONNECTED |
断开连接 |
RDMA_CM_EVENT_DEVICE_REMOVAL |
已删除与 rdma_cm_id 关联的本地 RDMA 设备 |
RDMA_CM_EVENT_MULTICAST_JOIN |
多播加入操作(rdma_join_multicast)成功完成 |
RDMA_CM_EVENT_MULTICAST_ERROR |
加入多播组时发生错误,如果已加入该组,则在现有组中发生错误 |
RDMA_CM_EVENT_ADDR_CHANGE |
通过地址解析与该 ID 相关联的网络设备改变了其 HW 地址 |
RDMA_CM_EVENT_TIMEWAIT_EXIT |
与连接关联的 QP 已退出其 timewait 状态,现在可以重新使用 |