RDMA技术整理4 RDMA编程入门 连通测试 物理拓扑:服务器6和8之间有一根直连网线;两台服务器各有一张cx5网卡,支持RoCe
使用ibdev2netdev
查看设备:
使用show_gids
看看网卡支持的RoCe版本
这里的IPv4地址是通过nmtui
进行配置的
服务器8根据如上的信息使用ib_send_bw -d mlx5_1 -x 2
,作为信号的接收端;另一台服务器6使用sudo ib_send_bw -d mlx5_3 192.168.42.8 --report_gbits -F -x 2
进行信息的发送;(5_3是服务器6里面有ipv4地址并且up的设备)
测试结果:
接收端:
发送端:
RDMA 常用命令 参考自https://blog.csdn.net/bandaoyu/article/details/115798693
命令
操作
备注
ibv_devinfo
显示device信息(简略)
ibv_devinfo mlx5_0
显示设备mlx5_0的详细信息
ibv_devinfo -v
显示网卡信息(详细)
ibv_devices
列出device
ibvdev2netdev
显示device和网口的对应关系
mellonx的命令,intel的需要阅读用户说明自己根据他们的脚本编写类似的命令
show_gids
显示gid列表
mellonx的命令,intel的需要阅读用户说明自己根据他们的脚本编写类似的命令
show_drop
查看端口包丢弃情况
mellonx的命令,intel的需要阅读用户说明自己根据他们的脚本编写类似的命令
ibstatus
查看核更改网卡工作模式:Ethernet 或infiniband模式
ibv_asyncwatch
监视 InfiniBand 异步事件
iblinkinfo.pl 或 iblinkinfo
显示光纤网络中所有链路的链路信息
sminfo
用法sminfo –help;查询 IB SMInfo 属性
ibstat 或 ibsysstat
查询 InfiniBand 设备状态或 IB 地址上的系统状态
hca_self_test.ofed
RDMA网卡自测
mellonx
/etc/infiniband/info
Mellanox OFED 安装的信息
mellonx
cat /etc/infiniband/openib.conf
看自动加载的模块列表
mellonx
`lspci
grep Mellanox`
检查Mellanox网卡是否安装和版本
RDMA 编程入门 RDMA 编程入门1 这部分来自the-geek-in-the-corner 的01_basic-client-server ;
在上述配置中的运行方法和连通实验类似:
在两个服务器使用make;
服务器8作为server使用./server
,程序告知端口号是36436;
服务器6作为client使用./client 192.168.42.8 36436
;这里的IP地址和上面连通测试使用的IP地址一样
运行结果:
server8,服务端:
1 2 3 4 5 6 7 temp@R750-427Server8:~/worker/the-geek-in-the-corner/01_basic-client-server$ ./server listening on port 36436. received connection request. received message: message from active/client side with pid 21871 connected. posting send... send completed successfully. peer disconnected.
server6,客户端:
1 2 3 4 5 6 7 temp@R750-427Server6:~/worker/the-geek-in-the-corner/01_basic-client-server$ ./client 192.168.42.8 36436 address resolved. route resolved. connected. posting send... send completed successfully. received message: message from passive/server side with pid 14356 disconnected.
这部分代码的目标是连接两个应用程序,让他们能够交换数据。这部分的关键是QP pair和CP,连接的每一端都有发送-接受队列和一个完成队列。构建队列并且对他们相互连接的步骤如下:
创建保护域(关联队列对、完成队列、内存注册等)、完成队列和发送-接收队列对。
确定队列对的地址。
将地址传送到另一个节点(通过某些带外机制)。
将队列对转换为“随时可以接收”(RTR) 状态,然后转换为“准备发送”(RTS) 状态。
根据需要发布发送、接收等操作
主动端(请求端)和被动端(响应端)的具体步骤如下:
被动端:
创建一个事件通道,以便我们可以接收 rdmacm 事件,例如连接请求和连接建立的通知。
绑定到地址。
创建侦听器并返回端口/地址。
等待连接请求。
创建保护域、完成队列和发送-接收队列对。
接受连接请求。
等待建立连接。
根据需要发布操作。
主动端:
创建一个事件通道,以便我们可以接收 rdmacm 事件,例如地址解析、路由解析和连接建立的通知。
创建连接标识符。
解析对等方的地址,这会将连接标识符绑定到本地 RDMA 设备。
创建保护域、完成队列和发送-接收队列对。
解析到对等方的路由。
连接。等待建立连接。
根据需要发布操作。
双方将共享相当数量的代码 - 被动端的步骤 1、5、7 和 8 大致相当于主动端的步骤 1、4、7 和 8。一旦建立了连接,与套接字一样,双方都是对等的。利用连接需要我们在队列对上发布操作。接收操作(不出所料)发布在接收队列上。在发送队列上,我们发布发送请求、RDMA 读/写请求和原子操作请求。
Passive/Server side 上面交代了被动端设置的部分,现在是详细的代码部分。因为几乎所有的内容都是异步处理的;因此这项工作首先需要构建一个事件处理循环和一组时间处理器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <rdma/rdma_cma.h> #define TEST_NZ(x) do { if ( (x)) die("error: " #x " failed (returned non-zero)." ); } while (0) #define TEST_Z(x) do { if (!(x)) die("error: " #x " failed (returned zero/null)." ); } while (0) static void die (const char *reason) ; int main (int argc, char **argv) { return 0 ; } void die (const char *reason) { fprintf (stderr , "%s\n" , reason); exit (EXIT_FAILURE); }
接下来设置一个事件通道,创建一个ramdacm ID(相当于套接字),绑定之后再循环中等待事件,这部分是修改main()
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 static void on_event (struct rdma_cm_event *event) ; int main (int argc, char **argv) { struct sockaddr_in addr ; struct rdma_cm_event *event = NULL ; struct rdma_cm_id *listener = NULL ; struct rdma_event_channel *ec = NULL ; uint16_t port = 0 ; memset (&addr, 0 , sizeof (addr)); addr.sin_family = AF_INET; TEST_Z(ec = rdma_create_event_channel()); TEST_NZ(rdma_create_id(ec, &listener, NULL , RDMA_PS_TCP)); TEST_NZ(rdma_bind_addr(listener, (struct sockaddr *)&addr)); TEST_NZ(rdma_listen(listener, 10 )); port = ntohs(rdma_get_src_port(listener)); printf ("listening on port %d.\n" , port); 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); if (on_event(&event_copy)) break ; } rdma_destroy_id(listener); rdma_destroy_event_channel(ec); return 0 ; }
ec 是指向 rdmacm 事件通道的指针。listener是指向侦听器的 rdmacm ID 的指针。我们在创建它时指定了RDMA_PS_TCP
,这表明我们需要一个面向连接的可靠队列对。RDMA_PS_UDP
将指示无连接、不可靠的队列对。
然后,我们将此 ID 绑定到套接字地址。通过将端口(addr.sin_port
)设置为零,我们指示 rdmacm 选择一个可用端口。我们还指出,我们希望侦听任何可用 RDMA 接口/设备上的连接。
我们的事件循环从 rdmacm 获取事件,确认事件,然后对其进行处理。未能确认事件将导致rdma_destroy_id() 阻塞。连接的被动端的事件处理程序仅对三个事件感兴趣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static int on_connect_request (struct rdma_cm_id *id) ;static int on_connection (void *context) ;static int on_disconnect (struct rdma_cm_id *id) ; int on_event (struct rdma_cm_event *event) { int r = 0 ; if (event->event == RDMA_CM_EVENT_CONNECT_REQUEST) r = on_connect_request(event->id); else if (event->event == RDMA_CM_EVENT_ESTABLISHED) r = on_connection(event->id->context); else if (event->event == RDMA_CM_EVENT_DISCONNECTED) r = on_disconnect(event->id); else die("on_event: unknown event." ); return r; }
从上述代码结合前面的main函数break的条件可以看出来,这里只关注on_event
里面的三种事件,如果不是这三种就会继续循环(等待);
rdmacm 允许我们将 void *
上下文指针与 ID 相关联。我们将使用它来附加连接上下文结构:
1 2 3 4 5 6 7 8 9 struct connection { struct ibv_qp *qp ; struct ibv_mr *recv_mr ; struct ibv_mr *send_mr ; char *recv_region; char *send_region; };
它包含一个指向队列对(冗余,但略微简化了代码)、两个缓冲区(一个用于发送,另一个用于接收)和两个内存区域(用于发送/接收的内存必须“注册”到谓词库)的指针。当我们收到连接请求时,如果尚未构建动词上下文,我们首先构建该上下文。然后,在构建了连接上下文结构之后,我们预先发布了我们的接收信息(稍后会详细介绍),并接受连接请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 static void build_context (struct ibv_context *verbs) ;static void build_qp_attr (struct ibv_qp_init_attr *qp_attr) ;static void post_receives (struct connection *conn) ;static void register_memory (struct connection *conn) ; int on_connect_request (struct rdma_cm_id *id) { struct ibv_qp_init_attr qp_attr ; struct rdma_conn_param cm_params ; struct connection *conn ; printf ("received connection request.\n" ); build_context(id->verbs); build_qp_attr(&qp_attr); TEST_NZ(rdma_create_qp(id, s_ctx->pd, &qp_attr)); id->context = conn = (struct connection *)malloc (sizeof (struct connection)); conn->qp = id->qp; register_memory(conn); post_receives(conn); memset (&cm_params, 0 , sizeof (cm_params)); TEST_NZ(rdma_accept(id, &cm_params)); return 0 ; }
我们推迟构建谓词上下文,直到收到第一个连接请求,因为rdmacm listener ID 不一定绑定到特定的 RDMA 设备(以及关联的谓词上下文)。但是,我们收到的第一个连接请求将在
id->verbs` 处具有有效的谓词上下文结构。构建谓词上下文涉及设置静态上下文结构、创建保护域、创建完成队列、创建完成通道以及启动线程以从队列中提取完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 struct context { struct ibv_context *ctx ; struct ibv_pd *pd ; struct ibv_cq *cq ; struct ibv_comp_channel *comp_channel ; pthread_t cq_poller_thread; }; static void * poll_cq (void *) ; static struct context *s_ctx = NULL ; void build_context (struct ibv_context *verbs) { if (s_ctx) { if (s_ctx->ctx != verbs) die("cannot handle events in more than one context." ); return ; } s_ctx = (struct context *)malloc (sizeof (struct context)); s_ctx->ctx = verbs; TEST_Z(s_ctx->pd = ibv_alloc_pd(s_ctx->ctx)); TEST_Z(s_ctx->comp_channel = ibv_create_comp_channel(s_ctx->ctx)); TEST_Z(s_ctx->cq = ibv_create_cq(s_ctx->ctx, 10 , NULL , s_ctx->comp_channel, 0 )); TEST_NZ(ibv_req_notify_cq(s_ctx->cq, 0 )); TEST_NZ(pthread_create(&s_ctx->cq_poller_thread, NULL , poll_cq, NULL )); }
使用完成通道允许我们阻塞轮询器线程等待完成。我们创建完成队列,并将cqe
设置为 10(cqe
是ibv_create_cq
函数里面的第二个参数),表示我们希望在队列上留出 10 个条目的空间。此数字应设置得足够大,以便队列不会溢出。轮询器在通道上等待,确认完成,重新排列完成队列(使用 ibv_req_notify_cq()
),然后从队列中提取事件,直到没有事件留下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static void on_completion (struct ibv_wc *wc) ; void * poll_cq (void *ctx) { struct ibv_cq *cq ; struct ibv_wc wc ; while (1 ) { TEST_NZ(ibv_get_cq_event(s_ctx->comp_channel, &cq, &ctx)); ibv_ack_cq_events(cq, 1 ); TEST_NZ(ibv_req_notify_cq(cq, 0 )); while (ibv_poll_cq(cq, 1 , &wc)) on_completion(&wc); } return NULL ; }
回到我们的连接请求。构建动词上下文后,我们必须初始化队列对属性结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 void build_qp_attr (struct ibv_qp_init_attr *qp_attr) { 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 ; }
我们首先将结构清零,然后设置我们关心的属性。 send_cq
和 recv_cq
分别是发送和接收完成队列。qp_type
设置为表明我们想要一个可靠的、面向连接的队列对。队列对功能结构 qp_attr->cap
用于与动词驱动程序协商最小功能。在这里,我们请求十个挂起的发送和接收(在它们各自的队列中的任何时间),以及每个发送或接收请求一个分散/收集元素(SGE;实际上是一个内存位置/大小元组)。建立队列对初始化属性后,我们调用 rdma_create_qp() 来创建队列对。然后我们为我们的连接上下文结构(结构连接)分配内存,并为我们的发送和接收操作分配/注册内存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const int BUFFER_SIZE = 1024 ; void register_memory (struct connection *conn) { conn->send_region = malloc (BUFFER_SIZE); conn->recv_region = malloc (BUFFER_SIZE); TEST_Z(conn->send_mr = ibv_reg_mr( s_ctx->pd, conn->send_region, BUFFER_SIZE, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE)); TEST_Z(conn->recv_mr = ibv_reg_mr( s_ctx->pd, conn->recv_region, BUFFER_SIZE, IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE)); }
在这里,我们分配两个缓冲区,一个用于发送,另一个用于接收,然后用动词注册它们。我们指出需要对这些内存区域进行本地写入和远程写入访问。连接请求事件处理程序中的下一步(变得相当长)是预先发布接收。在接受连接之前必须发布接收工作请求 (WR) 的原因是,基础硬件不会缓冲传入消息 — 如果接收请求尚未发布到工作队列,则传入消息将被拒绝,对等方将收到接收器未就绪 (RNR) 错误。我将在另一篇文章中进一步讨论这个问题,但现在只需说必须在发送之前发布接收。我们将通过在接受连接之前发布收到的邮件,并在建立连接后发送发送来强制执行此操作。发布接收需要我们构建一个接收工作请求结构,然后将其发布到接收队列:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void post_receives (struct connection *conn) { struct ibv_recv_wr wr , *bad_wr = NULL ; struct ibv_sge 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; TEST_NZ(ibv_post_recv(conn->qp, &wr, &bad_wr)); }
(任意)wr_id
字段用于存储连接上下文指针。最后,完成所有这些设置后,我们已准备好接受连接请求。这是通过调用rdma_accept()来完成的。 我们需要处理的下一个事件是RDMA_CM_EVENT_ESTABLISHED,这表示已建立连接。这个处理程序很简单 - 它只是发布一个发送工作请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 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 passive/server side with pid %d" , getpid()); printf ("connected. posting send...\n" ); memset (&wr, 0 , sizeof (wr)); 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; TEST_NZ(ibv_post_send(conn->qp, &wr, &bad_wr)); return 0 ; }
这与我们用于发布接收的代码没有根本的不同,除了发送请求指定了操作码。此处,IBV_WR_SEND指示必须与对等体上的相应接收请求匹配的发送请求。其他选项包括 RDMA 写入、RDMA 读取和各种原子操作。在 wr.send_flags 中指定IBV_SEND_SIGNALED表示我们需要此发送请求的完成通知。
我们要处理的最后一个 rdmacm 事件是RDMA_CM_EVENT_DISCONNECTED,我们将在其中执行一些清理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int on_disconnect (struct rdma_cm_id *id) { struct connection *conn = (struct connection *)id ->context ; printf ("peer 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); return 0 ; }
我们所要做的就是处理从完成队列中提取的完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void on_completion (struct ibv_wc *wc) { if (wc->status != IBV_WC_SUCCESS) die("on_completion: status is not IBV_WC_SUCCESS." ); if (wc->opcode & IBV_WC_RECV) { struct connection *conn = (struct connection *)(uintptr_t )wc ->wr_id ; printf ("received message: %s\n" , conn->recv_region); } else if (wc->opcode == IBV_WC_SEND) { printf ("send completed successfully.\n" ); } }
回想一下,在 post_receives() 中,我们设置了对连接上下文结构的wr_id。就是这样!建造很简单,但不要忘记-lrdmacm。此处提供了被动端/服务器和主动端/客户端的完整代码。(在之后的部分作者讨论了优化)
参考资料