【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 */

cpp 复制代码
/* 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 */

cpp 复制代码
/* 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) 密钥并发出我们已完成的信号。

cpp 复制代码
struct message 
{
  enum {
    MSG_MR,
    MSG_DONE
  } type;
 
  union {
    struct ibv_mr mr;
  } data;
};

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

cpp 复制代码
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() 初始化:

cpp 复制代码
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_depthresponder_resources。这些控制并行的 RDMA read请求的数量(These control the number of simultaneous outstanding RDMA read requests):

cpp 复制代码
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 发送:

cpp 复制代码
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 操作:

cpp 复制代码
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 操作:

cpp 复制代码
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");
    else
      printf("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 的两个特定组合:

cpp 复制代码
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");
  else
    printf("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 地址/密钥:

cpp 复制代码
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。这意味着打印消息缓冲区并断开连接是安全的:

cpp 复制代码
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

Introduction to Programming Infiniband RDMA | Better Tomorrow with Computer Science


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

相关推荐
bandaoyu8 天前
【RDMA】 ZTR(Zero Touch RoCE)技术(无需配置PFC和ECN)
rdma
大桔骑士v10 天前
【RDMA学习笔记】1:RDMA(Remote Direct Memory Access)介绍
计算机网络·rdma
中古传奇1 个月前
【3.1 以太网RDMA优化--网卡缓存资源维度】
网络·缓存·rdma
Tassel_YUE1 个月前
SmartX分享:SMTX ZBS 中 RDMA 技术简介
分布式存储·rdma·技术分享·块存储·smartx
北冥有鱼被烹2 个月前
微知-DOCA SDK中如何编译一个sample?如何运行?(meson /tmp/xxx; meson compile -C /tmp/xxx)
rdma·dpu·doca
北冥有鱼被烹2 个月前
微知-ib_write_bw的各种参数汇总(-d -q -s -R --run_infinitely)
rdma·mellanox
KIDGINBROOK5 个月前
RDMA驱动学习(一)- 用户态到内核态的过程
rdma
羌俊恩5 个月前
Linux 常见的冷知识集锦
linux·rdma·posix
我想学LINUX6 个月前
【RDMA项目】如何使用rdma-core进行调用开发一个实战项目
服务器·网络·github·客户端·rdma·rdma-core·内存访问