当前位置: 首页 > news >正文

【RDMA】RDMA read和write编程实例(verbs API)

 WRITE|READ编程(RDMA read and write with IB verbs)

(本文讲解的示例代码在:RDMA read and write with IB verbs | The Geek in the Corner)


将 RDMA 与verbs一起使用非常简单:首先注册内存块,然后交换内存描述符,然后进行读/写操作。注册是通过调用 ibv_reg_mr() 来完成的,它将内存块固定(从而防止它被交换出)并返回一个包含 uint32_t key的结构 ibv_mr * ,允许远程访问注册的内存。然后必须通过某种带外机制与对等方交换此key以及块的地址。然后,对方可以在调用 ibv_post_send() 时使用key和地址来post  RDMA 读和写请求。

一些代码可能有指导意义:

/* PEER 1 */

/* PEER 1 */const size_t SIZE = 1024;char *buffer = malloc(SIZE);
struct ibv_mr *mr;
uint32_t my_key;
uint64_t my_addr;mr = ibv_reg_mr(pd, buffer, SIZE, IBV_ACCESS_REMOTE_WRITE);my_key = mr->rkey;
my_addr = (uint64_t)mr->addr;/* exchange my_key and my_addr with peer 2 */

/* PEER 2 */ 

/* PEER 2 */const size_t SIZE = 1024;char *buffer = malloc(SIZE);
struct ibv_mr *mr;
struct ibv_sge sge;
struct ibv_send_wr wr, *bad_wr;
uint32_t peer_key;
uint64_t peer_addr;mr = ibv_reg_mr(pd, buffer, SIZE, IBV_ACCESS_LOCAL_WRITE);/* get peer_key and peer_addr from peer 1 */strcpy(buffer, "Hello!");memset(&wr, 0, sizeof(wr));sge.addr = (uint64_t)buffer;
sge.length = SIZE;
sge.lkey = mr->lkey;wr.sg_list = &sge;
wr.num_sge = 1;
wr.opcode = IBV_WR_RDMA_WRITE;wr.wr.rdma.remote_addr = peer_addr;
wr.wr.rdma.rkey = peer_key;ibv_post_send(qp, &wr, &bad_wr);

PEER 1 的 ibv_reg_mr() 的最后一个参数 IBV_ACCESS_REMOTE_WRITE 指定我们希望对PEER  2 具有对位于缓冲区的内存块的写访问权限。

在实践中使用它更复杂。本文附带的示例代码连接两台主机,交换内存区域密钥keys、读取或写入远程内存,然后断开连接。顺序如下: 

  1. 初始化上下文并注册内存区域。
  2. 建立连接。
  3. 使用之前帖子中描述的发送/接收模型在对等点之间交换内存区域密钥keys。
  4. 后读/写操作。
  5. 断开。

连接的每一端都有两个线程:处理连接事件的主线程和轮询完成队列(CQ)的线程。为了避免死锁和竞争条件,我们安排了我们的操作,以便一次只有一个线程发布工作请求。为了详细说明上面的顺序,在建立连接后,

客户端将:

  1. 在 MSG_MR 消息中发送其 RDMA 内存区域密钥keys。
  2. 等待服务器的 MSG_MR 消息(包含其 RDMA 密钥keys)。
  3. 发布 RDMA 操作。
  4. 通过发送 MSG_DONE 消息通知服务器它已准备好断开连接。
  5. 等待来自服务器的 MSG_DONE 消息。
  6. 断开。

第一步发生在 RDMA 连接事件处理程序线程的上下文中,但第二步到第六步发生在verbs  CQ 轮询线程的上下文中。(verbs =verbs api =verbs 库)

服务端的操作顺序类似:

  1. 等待客户端的 MSG_MR 消息及其 RDMA 密钥。
  2. 在 MSG_MR 消息中发送其 RDMA 密钥。
  3. 发布 RDMA 操作。
  4. 通过发送 MSG_DONE 消息通知客户端它已准备好断开连接。
  5. 等待来自客户端的 MSG_DONE 消息。
  6. 断开。

这里所有六个步骤都发生在verbs  CQ 轮询线程的上下文中。等待 MSG_DONE 是必要的,否则我们可能会在对等方(peer)的 RDMA 操作完成之前关闭连接。在(服务端)发送 MSG_DONE 之前,我们不必等待 RDMA 操作 完成——InfiniBand 规范要求 requests  将按照它们发布的顺序启动 处理。这意味着在 RDMA 操作完成之前,对等方(peer)不会收到 MSG_DONE。

为简洁起见(并说明它们几乎相同),此示例的代码合并了上一组帖子中的许多客户端和服务器代码(common.c中),客户端 (rdma-client) 和服务器 (rdma-server) 继续运行不同的 RDMA 连接管理器循环处理事件(RDMA connection manager event loops),但它们相同的verbs 代码部分——轮询 CQ、发送消息、发布 RDMA 操作等共用一份代码。

我们也使用相同代码进行RDMA 读取和写入操作,因为它们非常相似。 rdma-server 和 rdma-client 将““read” or “write” 作为它们的第一个命令行参数。


让我们从 rdma-common.c 的顶部开始,它包含客户端和服务器通用的verbs 代码。我们首先定义我们的消息结构体。我们将使用它来在节点之间传递 RDMA 内存区域 (MR) 密钥并发出我们已完成的信号。

struct message 
{enum {MSG_MR,MSG_DONE} type;union {struct ibv_mr mr;} data;
};

我们的连接结构体已扩展,包括用于 RDMA 操作的内存区域以及对等方(peer)的 MR 结构和两个状态变量:

struct connection 
{struct rdma_cm_id *id;struct ibv_qp *qp;int connected;struct ibv_mr *recv_mr;struct ibv_mr *send_mr;struct ibv_mr *rdma_local_mr;struct ibv_mr *rdma_remote_mr;struct ibv_mr peer_mr;struct message *recv_msg;struct message *send_msg;char *rdma_local_region;char *rdma_remote_region;enum {SS_INIT,SS_MR_SENT,SS_RDMA_SENT,SS_DONE_SENT} send_state;enum {RS_INIT,RS_MR_RECV,RS_DONE_RECV} recv_state;
};

完成处理程序(completion handler)使用 send_state 和 recv_state 这两个状态枚举变量来确保对等点(peer)之间的消息和 RDMA 操作的正确顺序。

该结构体由 build_connection() 初始化:

void build_connection(struct rdma_cm_id *id)
{struct connection *conn;struct ibv_qp_init_attr qp_attr;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->id = id;conn->qp = id->qp;conn->send_state = SS_INIT;conn->recv_state = RS_INIT;conn->connected = 0;register_memory(conn);post_receives(conn);
}

由于我们使用 RDMA read操作,我们必须在 struct rdma_conn_param 中设置initiator_depth 和responder_resources。这些控制并行的 RDMA read请求的数量(These control the number of simultaneous outstanding RDMA read requests):

void build_params(struct rdma_conn_param *params)
{memset(params, 0, sizeof(*params));params->initiator_depth = params->responder_resources = 1;params->rnr_retry_count = 7; /* infinite retry */
}

将 rnr_retry_count 设置为 7 表示我们希望网卡在对端回复  receiver-not-ready (RNR) 错误时无限期地重新发送。当在对端发布相应的接收请求( receive request)之前发布发送请求(send request )时,会发生 RNR。

使用 send_message() 函数post 发送:

void send_message(struct connection *conn)
{struct ibv_send_wr wr, *bad_wr = NULL;struct ibv_sge sge;memset(&wr, 0, sizeof(wr));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_msg;sge.length = sizeof(struct message);sge.lkey = conn->send_mr->lkey;while (!conn->connected);TEST_NZ(ibv_post_send(conn->qp, &wr, &bad_wr));
}

send_mr() 封装了这个函数,并被 rdma-client 用来将它的 MR 发送到服务器,提示服务器发送它的 MR 作为响应,从而启动 RDMA 操作:

void send_mr(void *context)
{struct connection *conn = (struct connection *)context;conn->send_msg->type = MSG_MR;memcpy(&conn->send_msg->data.mr, conn->rdma_remote_mr, sizeof(struct ibv_mr));send_message(conn);
}

完成处理程序( completion handler)完成大部分工作。它维护 send_state 和 recv_state,根据需要回复消息和发布 RDMA 操作:

void on_completion(struct ibv_wc *wc)
{struct connection *conn = (struct connection *)(uintptr_t)wc->wr_id;if (wc->status != IBV_WC_SUCCESS)die("on_completion: status is not IBV_WC_SUCCESS.");if (wc->opcode & IBV_WC_RECV) {conn->recv_state++;if (conn->recv_msg->type == MSG_MR) {memcpy(&conn->peer_mr, &conn->recv_msg->data.mr, sizeof(conn->peer_mr));post_receives(conn); /* only rearm for MSG_MR */if (conn->send_state == SS_INIT) /* received peer's MR before sending ours, so send ours back */send_mr(conn);}} else {conn->send_state++;printf("send completed successfully.\n");}if (conn->send_state == SS_MR_SENT && conn->recv_state == RS_MR_RECV) {struct ibv_send_wr wr, *bad_wr = NULL;struct ibv_sge sge;if (s_mode == M_WRITE)printf("received MSG_MR. writing message to remote memory...\n");elseprintf("received MSG_MR. reading message from remote memory...\n");memset(&wr, 0, sizeof(wr));wr.wr_id = (uintptr_t)conn;wr.opcode = (s_mode == M_WRITE) ? IBV_WR_RDMA_WRITE : IBV_WR_RDMA_READ;wr.sg_list = &sge;wr.num_sge = 1;wr.send_flags = IBV_SEND_SIGNALED;wr.wr.rdma.remote_addr = (uintptr_t)conn->peer_mr.addr;wr.wr.rdma.rkey = conn->peer_mr.rkey;sge.addr = (uintptr_t)conn->rdma_local_region;sge.length = RDMA_BUFFER_SIZE;sge.lkey = conn->rdma_local_mr->lkey;TEST_NZ(ibv_post_send(conn->qp, &wr, &bad_wr));conn->send_msg->type = MSG_DONE;send_message(conn);} else if (conn->send_state == SS_DONE_SENT && conn->recv_state == RS_DONE_RECV) {printf("remote buffer: %s\n", get_peer_message_region(conn));rdma_disconnect(conn->id);}
}
Let’s examine on_completion() in parts. First, the state update:if (wc->opcode & IBV_WC_RECV) {conn->recv_state++;if (conn->recv_msg->type == MSG_MR) {memcpy(&conn->peer_mr, &conn->recv_msg->data.mr, sizeof(conn->peer_mr));post_receives(conn); /* only rearm for MSG_MR */if (conn->send_state == SS_INIT) /* received peer's MR before sending ours, so send ours back */send_mr(conn);}} else {conn->send_state++;printf("send completed successfully.\n");

如果完成的操作是接收操作(即,如果 wc->opcode 设置了 IBV_WC_RECV),则 recv_state 递增。

如果收到的消息是 MSG_MR,我们将收到的 MR 复制到我们的连接结构的 peer_mr 成员中,并重新准备接收槽。这对于确保我们在对端的 RDMA 操作完成后收到 MSG_DONE 消息是必要的。如果我们收到了对方的 MR 但还没有发送我们的(服务器就是这种情况),我们就调用 send_mr() 将我们的 MR 发给对方。更新 send_state 并不复杂。


接下来我们检查 send_state 和 recv_state 的两个特定组合:

if (conn->send_state == SS_MR_SENT && conn->recv_state == RS_MR_RECV){struct ibv_send_wr wr, *bad_wr = NULL;struct ibv_sge sge;if (s_mode == M_WRITE)printf("received MSG_MR. writing message to remote memory...\n");elseprintf("received MSG_MR. reading message from remote memory...\n");memset(&wr, 0, sizeof(wr));wr.wr_id = (uintptr_t)conn;wr.opcode = (s_mode == M_WRITE) ? IBV_WR_RDMA_WRITE : IBV_WR_RDMA_READ;wr.sg_list = &sge;wr.num_sge = 1;wr.send_flags = IBV_SEND_SIGNALED;wr.wr.rdma.remote_addr = (uintptr_t)conn->peer_mr.addr;wr.wr.rdma.rkey = conn->peer_mr.rkey;sge.addr = (uintptr_t)conn->rdma_local_region;sge.length = RDMA_BUFFER_SIZE;sge.lkey = conn->rdma_local_mr->lkey;TEST_NZ(ibv_post_send(conn->qp, &wr, &bad_wr));conn->send_msg->type = MSG_DONE;send_message(conn);} 
else if (conn->send_state == SS_DONE_SENT && conn->recv_state == RS_DONE_RECV) 
{printf("remote buffer: %s\n", get_peer_message_region(conn));rdma_disconnect(conn->id);
}

这些组合中的第一个是当我们既发送了我们的 MR 又收到了对方的 MR 时。这表明我们已准备好发布 RDMA 操作并发布 MSG_DONE。发布 RDMA 操作意味着构建 RDMA 工作请求(RDMA  work request)。这类似于发送工作请求( work request),除了我们指定 RDMA 操作码并传递对等方的 RDMA 地址/密钥:

wr.opcode = (s_mode == M_WRITE) ? IBV_WR_RDMA_WRITE : IBV_WR_RDMA_READ;wr.wr.rdma.remote_addr = (uintptr_t)conn->peer_mr.addr;
wr.wr.rdma.rkey = conn->peer_mr.rkey;

请注意,我们不需要为 remote_addr 使用 conn->peer_mr.addr(即remote_addr不一定非得等于 conn->peer_mr.addr?)— 如果我们愿意,我们可以使用落入 ibv_reg_mr() 注册的内存区域范围内的任何地址(如conn->peer_mr.addr+x,conn->peer_mr.addr+x在注册的内存区域范围内?)
第二个状态组合是 SS_DONE_SENT 和 RS_DONE_RECV,表明我们已经发送 MSG_DONE 并从对等方接收 MSG_DONE。这意味着打印消息缓冲区并断开连接是安全的:

printf("remote buffer: %s\n", get_peer_message_region(conn));
rdma_disconnect(conn->id);

 就是这样。如果一切正常,您应该在使用 RDMA 写入时看到以下内容:

$ ./rdma-server write

listening on port 47881.

received connection request.

send completed successfully.

received MSG_MR. writing message to remote memory...

send completed successfully.

send completed successfully.

remote buffer: message from active/client side with pid 20692

peer disconnected.

$ ./rdma-client write 192.168.0.1 47881

address resolved.

route resolved.

send completed successfully.

received MSG_MR. writing message to remote memory...

send completed successfully.

send completed successfully.

remote buffer: message from passive/server side with pid 26515

disconnected.

 当使用 RDMA read时:

$ ./rdma-server read

listening on port 47882.

received connection request.

send completed successfully.

received MSG_MR. reading message from remote memory...

send completed successfully.

send completed successfully.

remote buffer: message from active/client side with pid 20916

peer disconnected.

$ ./rdma-client read 192.168.0.1 47882

address resolved.

route resolved.

send completed successfully.

received MSG_MR. reading message from remote memory...

send completed successfully.

send completed successfully.

remote buffer: message from passive/server side with pid 26725

disconnected.

再次,示例代码可在此处获得。

Updated, Oct. 4: Sample code is now at the-geek-in-the-corner/02_read-write at master · tarickb/the-geek-in-the-corner · GitHub.

 infiniband概念空间分析 - 知乎

https://www.researchgate.net/figure/RDMA-Write_fig2_4245345

operation diagram: rdma read

operation diagram: rdma write

Introduction to Programming Infiniband RDMA | Better Tomorrow with Computer Science


更多,见:https://blog.csdn.net/bandaoyu/article/details/113125244


http://www.mrgr.cn/news/79132.html

相关文章:

  • React第十一节 组件之间通讯之发布订阅模式(自定义发布订阅器)
  • 微信小程序横滑定位元素案例代码
  • 【go】select 语句case的随机性
  • Python矩阵并行计算;CuPy-CUDA 实现显存加速:;在Python中实现显存加速或卸载;CuPy 和 NumPy 区别
  • compose组件库
  • java调用cmdsh命令
  • 流媒体之linux下离线部署FFmpeg 和 SRS
  • MongoDB集群的介绍与搭建
  • 【测试工具JMeter篇】JMeter性能测试入门级教程(七):JMeter断言
  • pset2 substitution.c
  • Linux内核__setup 宏的作用及分析
  • [go-redis]客户端的创建与配置说明
  • ansible自动化运维(二)ad-hoc模式
  • 网络层总结
  • 基于TensorFlow框架的线性回归实现
  • AI处理器组合--华为OD机试2024年E卷
  • 两个畸变矩阵相乘后还是一个2*2的矩阵,有四个畸变元素。1、畸变矩阵吸收了法拉第矩阵。2、畸变矩阵也给法拉第旋转角带来模糊(求解有多种可能)
  • Scala:正则表达式
  • Transformers在计算机视觉领域中的应用【第3篇:Swin Transformer——多层次的Vision Transformer】
  • 快速上手 RabbitMQ:使用 Docker 轻松搭建消息队列系统