Featured image of post RDMA 技术及其编程方法(二):编程指导

RDMA 技术及其编程方法(二):编程指导

本文是关于 RDMA 技术及其编程方法的指导。文章主要介绍了 libibverbs 的简介和 Verbs API 的详解,包括 Verbs 对象创建层次和两个动态库。此外,还介绍了 Connection Manager 的建立过程和抽象类型 RDMACM。最后,文章通过解析被动方和主动方的 RDMACM 程序,以及基于 RDMA 的 client-server 程序的实战,来帮助读者更好地理解和应用 RDMA 技术。

# RDMA 技术及其编程方法(二):编程指导

# 一、libibverbs 简介

  • libibverbs 由 Roland Dreier 自 2006 年开始开发和维护,实际上是*nix 中的 Verbs API 标准
    • 开源
    • Verbs 的核心部分自 2005 年起集成到 Linux 内核中–内核 2.6.11
    • Inbox in several *nix distributions
    • 目前有多个硬件供应商提供的级别较低的库
  • 对所有启用 RDMA 的传输协议使用相同的 API
    • InfiniBand: 支持 RDMA 的网络体系结构
      • 需要支持它的网卡和 InfiniBand 交换机。
    • RoCE:基于以太网/IP 帧的 RDMA 数据包封装
      • 需要支持它的网卡和标准以太网交换机
    • iWARP:提供基于流控制传输协议(SCTP)和传输控制协议(TCP)的 RDMA
      • 需要支持它的网卡和标准以太网交换机
  • libibverbs 是完全线程安全的
    • libibverbs 本身是线程安全的
    • 用户态空间低级驱动程序库也是线程安全的
    • 应用程序可以在多线程中使用 RDMA 资源

危险

销毁一个线程中的资源并在另一个线程中使用它将导致 segmentation fault,这个问题在非多线程代码中也会发生

  • 使用 libibverbs 的基本须知
    tips说明
    头文件引入#include<infiniband/verbs.h>
    编译时链接-libverbs
    所有的 input structures 需要为 zeroed使用 memset()或结构初始化、如果该 structure 有扩充的需求,则零值将保留遗留行为
    大多数资源句柄都是指针,因此使用错误的句柄可能会导致分段错误使用 NULL 检查句柄
    返回指针的 Verbs 成功时返回有效值,失败时返回 NULL检查返回值
    返回整形变量的 Verbs 如果成功则返回零,如果成功则返回-1 或 errno检查返回值

# 二、Verbs API 详解

# 1. 简介

  • 在内核和用户态空间均可使用
  • Verbs 中的类
    • 资源管理:Qps、CQs、SRQs 等等
    • WR 处理:post send, 轮询 CQ 等等
    • 内存注册
    • 地址句柄
  • Verbs 中的操作
    • Device 操作
    • 上下文操作
    • PD 操作
    • QP bringup
    • 活跃 QP 操作

# 2. Verbs 对象创建层次

  1. 获取 devide 列表
  2. 打开请求的 device
  3. 查询 device 功能
  4. 分配 PD 内存空间
  5. 注册内存域 MR
  6. 关联并创建完成队列 CQ
  7. 创建 QP
  8. Bring up a QP
  9. Post WR 并且轮询 CQ
  10. 清理资源
    20230724235009
20230724235116

# 3. 两个动态库

  • libibverbs.so
    • 用于直接通过用户态空间访问 InfiniBand 硬件的库
    • Infiniband(根据 Infiniband 规范)和 iWarp(iWARP 动词规范)的 RDMA Verbs 的实现
    • 它处理创建、修改、查询和销毁资源的控制路径,如保护域(PD)、完成队列(CQ)、队列对(QP)、共享接收队列(SRQ)、地址句柄(AH)、内存区域(MR)
    • 它还处理发送和接收发布到 QPS 和 SRQ 的数据,使用轮询和完成事件从 CQs 获取完成
  • librdmacm.so
    • 用户态空间的 RDMA 连接管理器
    • 使用 Socket 语义的 RDMA(InfiniBand、ROCE 和 iWARP)通信管理库

# 三、Connection Manager

# 1. 连接的建立

  • 基于 Infiniband 通信管理(CM)协议(在通用服务接口(GSI)上定义的协议 QP:QP1)

  • 提供以下服务

    • 在对等 RC 和 QP 之间交换必要的参数,使它们为通信做好准备
      • 初始化器请求连接到远程上的服务 ID(服务 ID 映射)
      • 类 TCP 握手:请求/响应/即用消息
    • 查找给定服务 ID 的远程 UD 和 QP 序号
      • 服务 ID 请求/响应消息
    • 加载备用路径
  • 连接管理器(Connection Manager,CM)是一个用户态空间的库,它提供了一个通用的接口,用于在 RDMA 网络中建立连接。它可以用于建立连接,也可以用于查找远程 QP 的地址,以便在不建立连接的情况下发送数据。

    • 需要在对等 QP 之间交换信息
    • 负责 RC、UC、RD 连接的建立
    • 应用程序使用 SA 来获取其他信息(例如路径记录)
    • SIDR 用于 UD

# 2. CM 的抽象类型(RDMACM)

  • 类似于 Socket 连接模式的语义
  • 对 IB 和 ROCE 都使用基于 IP 的寻址模式
说明
rdma_create/destroy_idcreates/destroys a connection identifier (equivalent to a socket)
rdma_create/destroy_qpallocate/destroy a qp for communication
rdma_bind_addrset local port to listen on
rdma_resolve_addrobtain local RDMA device to reach remote address
rdma_resolve_routedetermine route to remote address
rdma_get_src_portquery local port
rdma_get_local_addrquery local ip
rdma_get_peer_addrquery remote ip
rdma_connect/disconnectconnect/disconnect rc qps, or resolve service id to qp for ud qps
rdma_listenlisten for incoming connections
rdma_accept/rejectaccept/reject incoming connection requests
rdma_create/destroy_event_channelallocate/destroy an event channel
rdma_get_cm_eventget next event
rdma_ack_cm_eventacknowledge event(s) to rdmacm
rdma_join/leave_multicastjoin/leave multicast addresses
  • 使用 rdmacm 的基本须知
    tips说明
    头文件引入#include<rdma/rdma_cma.h>
    编译时链接-lrdmacm

# 四、 RDMACM 程序解析——被动方

流程如下:

  1. 创建事件 channel,以便我们可以接收 rdmacm 事件,如连接请求和连接建立通知。
  2. 创建连接 ID 并绑定到地址。
  3. 创建 Listener 并返回端口/地址。
  4. 等待连接请求
  5. 创建 PD、CQ 和 Send-Receive QP
  6. 接受连接请求
  7. 等待建立连接
  8. 视情况发布操作

# 1. 创建事件 channel

  • 打开用于报告通信事件的 channel。异步事件将通过事件 channel 报告给用户,对应方法为struct rdma_event_channel * rdma_create_event_channel(void)
  • 事件 channel 用于定向 rdma_cm_id 上的所有事件。对于许多客户端来说,单个事件 channel 可能就足够了,然而,当管理大量的连接或 cm_id 时,用户可能会发现将不同 cm_id 的事件定向到不同的 channel 进行处理是有用的。
  • 必须通过调用rdma_destroy_event_channel 销毁所有创建的事件 channel。用户应该调用rdma_get_cm_event 来检索事件 channel 上的事件。
struct rdma_event_channel *channel = rdma_create_event_channel();
if (!channel) {
    perror("rdma_create_event_channel");
    return -1;
}
struct rdma_cm_event* event;
// 此处会阻塞,直到有事件发生
int err = rdma_get_cm_event(channel, &event);
if (err) {
    perror("rdma_get_cm_event");
    return err;
}
// 中间处理代码...
rdma_destroy_event_channel(channel);
  • 每个事件 channel 都映射到一个文件描述符。可以像使用和操作任何其他 FD 一样使用和操作关联的文件描述符,以更改其行为。

# 2. 创建连接 ID

  • 创建用于跟踪通信信息的标识符,对应方法为int rdma_create_id(struct rdma_event_channel *channel, struct rdma_cm_id **id, void *context, enum rdma_port_space ps)
  • 输入参数:
    • channel:事件 channel
    • id:指向 rdma_cm_id 指针的指针,用于返回新创建的 rdma_cm_id
    • context:用户上下文,将在事件中返回给用户
    • ps:RDMA 端口空间,指定要使用的端口空间
  • rdma_cm_id 在概念上等同于用于 RDMA 通信的套接字。不同之处在于,RDMA 通信需要显式绑定到指定的 RDMA 设备,然后才能进行通信,并且大多数操作本质上是异步的。
  • 端口空间
    • RDMA_PS_TCP:提供可靠、面向连接的 QP 通信。与 TCP 不同,RDMA 端口空间提供基于消息的通信,而不是基于流的通信。
    • RDMA_PS_UDP:提供不可靠、无连接的 QP 通信。支持数据报和组播通信。
  • 销毁:在调用此函数并确认相关事件之前,用户必须释放任何与 rdma_cm_id 相关的 QP。
struct rdma_cm_id *listen_id;
int err = rdma_create_id(channel, &listen_id, NULL, RDMA_PS_TCP);
if (err) {
    perror("rdma_create_id");
    return err;
}
// 中间处理代码...
rdma_destroy_id(listen_id);

# 3. 绑定地址

  • 将源地址与 rdma_cm_id 相关联。对应方法为int rdma_bind_addr(struct rdma_cm_id *id, struct sockaddr *addr)
    • 地址中可以包含通配符。
    • 如果绑定到特定本地地址,则 rdma_cm_id 也将绑定到本地 RDMA 设备。
    • 通常,在调用rdma_listen 以绑定到特定端口号之前调用此函数,但也可以在调用rdma_resolve_addr 以绑定到特定地址之前在主动方调用该函数。
    • 如果用于绑定到端口 0,rdma_cm 将选择一个可用端口,可以使用rdma_get_src_port 检索该端口。
/*  sockaddr_in 是 IPV4 的地址结构体
*   AF_INET:IPV4
*   htons:将主机字节序转换为网络字节序(小端存储), 20079 是端口号
*   INADDR_ANY:表示任意地址
*/
struct sockaddr_in addr = {
    .sin_family = AF_INET,
    .sin_port = htons(20079),
    .sin_addr = { .s_addr = INADDR_ANY },
};
err = rdma_bind_addr(listen_id, (struct sockaddr *)&addr);
if (err) {
    perror("rdma_bind_addr");
    return err;
}

# 4. 创建 Listener,返回端口/地址

  • 初始化传入连接请求或数据报服务查找的 Listener。对应方法为int rdma_listen(struct rdma_cm_id *id, int backlog)
    • 侦听将被限制为本地绑定源地址
    • 在调用此函数之前,用户必须已通过调用rdma_bind_addrrdma_cm_id 绑定到本地地址。
    • 如果rdma_cm_id 绑定到特定的 IP 地址,则侦听将仅限于该地址和关联的 RDMA 设备。
    • 如果rdma_cm_id 仅绑定到 RDMA 端口号,则将在所有 RDMA 设备上进行侦听。
  • 返回已绑定到本地地址的rdma_cm_id 的本地端口号。对应方法为uint16_t rdma_get_src_port(struct rdma_cm_id *id)
  • 返回已绑定到本地设备的rdma_cm_id 的本地 IP 地址。对应方法为struct sockaddr * rdma_get_local_addr(struct rdma_cm_id *id)
  • 解析目的节点和服务地址,并返回建立通信所需的信息。提供与 getaddrinfo 等效的 RDMA 功能(配合rdma_create_ep 使用)。对应方法为int rdma_getaddrinfo (char *node, char *service, struct rdma_addrinfo *hints, struct rdma_addrinfo **res)
    • node: 可选,目的节点的主机名,或者点分十进制的 IPv4/IPv6 十六进制地址
    • service:地址的服务名称或端口号。
    • hints:一个包含有关调用方支持的服务类型的提示的 rdma_addrinfo 结构的引用。
    • res:指向包含响应信息的 rdma_addrinfo 结构的 LinkedList 的指针。
// 等待连接请求的最大数量
int backlog = 10;
err = rdma_listen(listen_id, backlog);
if (err) {
    perror("rdma_listen");
    return err;
}

uint16_t port = rdma_get_src_port(listen_id);
port = ntohs(port);
printf("listening on port %u.\n", port);

struct sockaddr *local_addr = rdma_get_local_addr(listen_id);
if (local_addr->sa_family == AF_INET) {
    struct sockaddr_in *sin = (struct sockaddr_in *)local_addr;
    char ip[INET_ADDRSTRLEN];
    // 需要加上头文件 #include <arpa/inet.h>
    inet_ntop(AF_INET, &(sin->sin_addr), ip, INET_ADDRSTRLEN);
    printf("Local IP address is: %s\n", ip);
    printf("Local port is: %d\n", ntohs(sin->sin_port));
}

struct rdma_addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_flags = RAI_PASSIVE;
hints.ai_port_space = RDMA_PS_TCP;

struct rdma_addrinfo *res;
err = rdma_getaddrinfo(NULL, "20079", &hints, &res);

if (err) {
    perror("rdma_getaddrinfo");
    return err;
}
// do something with struct rdma_addrinfo *cur = res...

# 5. 等待连接请求

  • 检索通信事件。如果没有挂起的事件,默认情况下,调用将阻塞,直到接收到事件。对应方法为int rdma_get_cm_event(struct rdma_event_channel *channel, struct rdma_cm_event **event)
  • 通过修改与给定通道相关联的文件描述符,可以更改此函数的默认同步行为。
  • 所有报告的事件都必须通过调用rdma_ack_cm_event 进行确认。
  • rdma_cm_id 的销毁将被阻塞,直到相关事件被确认。
struct rdma_cm_event* event;
// 此处会阻塞,直到有事件发生
err = rdma_get_cm_event(channel, &event);

# 6. 创建 PD、CQ 和 Send-Receive QP

  • 分配与指定的rdma_cm_id 相关联的 QP,并将其转换为用于发送和接收。对应方法为int rdma_create_qp(struct rdma_cm_id *id, struct ibv_pd *pd, struct ibv_qp_init_attr *qp_init_attr)
  • 在调用此函数之前,rdma_cm_id 必须绑定到本地 RDMA 设备,且保护域 PD 必须用于同一设备。
  • 被分配给rdma_cm_id 的 QP 会由 librdmacm 自动转换状态.
  • 分配完毕后,QP 将准备就绪,处理接收信息的发布。如果 QP 未连接,它将准备好发布发送。
pd = ibv_alloc_pd(cm_client_id->verbs);
if (!pd) {
    perror("Failed to allocate a protection domain");
    return -1;
}

io_completion_channel = ibv_create_comp_channel(cm_client_id->verbs);
if (!io_completion_channel) {
    perror("Failed to create an I/O completion event channel");
    return -1;
}

cq = ibv_create_cq(cm_client_id->verbs, 10, NULL, io_completion_channel, 0);
if (!cq) {
    perror("Failed to create a completion queue");
    return -1;
}

ret = ibv_req_notify_cq(cq, 0);
if (ret) {
    perror("Failed to request notifications");
    return -1;
}

bzero(&qp_init_attr, sizeof(qp_init_attr));
qp_init_attr.qp_type = IBV_QPT_RC;
qp_init_attr.cap.max_send_wr = 10;
qp_init_attr.cap.max_recv_wr = 10;
qp_init_attr.cap.max_send_sge = 1;
qp_init_attr.cap.max_recv_sge = 1;
qp_init_attr.send_cq = cq;
qp_init_attr.recv_cq = cq;

ret = rdma_create_qp(cm_client_id, pd, &qp_init_attr);
if (ret) {
    perror("Failed to create QP");
    return -1;
}

client_qp = cm_client_id->qp;

# 7. 最后的操作

  1. Accept 请求连接
  2. 等待连接建立
  3. 发布操作
20230725224928

# 五、 RDMACM 程序解析——主动方

# 1. 创建事件 channel

  • 与被动方相同

# 2. 创建连接 ID

  • 与被动方相同

# 3. 绑定地址

  • 将目的地址和可选源地址从 IP 地址解析为 RDMA 地址。如果成功,则指定的 rdma_cm_id 将绑定到本地设备。对应方法为int rdma_resolve_addr (struct rdma_cm_id *id, struct sockaddr *src_addr, struct sockaddr *dst_addr, int timeout_ms)
  • 此方法用于将给定的目标 IP 地址映射到可用的 RDMA 地址。
  • IP 到 RDMA 地址的映射使用本地路由表或通过 ARP 完成。
  • 如果给定源地址,则将rdma_cm_id 绑定到该地址,就像调用rdma_ind_addr 一样。
  • 如果没有给出源地址,并且rdma_cm_id 尚未绑定到设备,则rdma_cm_id 将根据本地路由表绑定到源地址。
  • 在此方法调用之后,rdma_cm_id 将绑定到 RDMA 设备。
  • 该方法调用通常在调用 rdma_resolve_routerdma_connect 之前在主动方上进行。

注释

InfiniBand 特定

  • 此方法还会将目标 IP 地址和源 IP 地址(如果给定)映射到 GID。
  • 为了执行映射,IPoIB 必须同时在本地和远程节点上运行。

# 4. 创建 QP

  • 与被动方相同

# 5. 解析路由

  • 解析指向目标地址的 RDMA 路由,以建立连接。目标地址必须已通过调用 rdma_resolve_addr 解析。对应方法为int rdma_resolve_route (struct rdma_cm_id *id, int timeout_ms);

# 6. 建立连接

  • 对应方法为int rdma_connect (struct rdma_cm_id *id, struct rdma_conn_param *conn_param);

    • id:指向 rdma_cm_id 的指针
    • conn_param:指向 rdma_conn_param 结构的指针,包含连接参数
  • 对于 RDMA_PS_TCP 类型的 rdma_cm_id,该调用会向远程目的地发起连接请求

  • 对于 RDMA_PS_UDP 类型的 rdma_cm_id,它会启动对提供数据报服务的远程 QP 的查询

# 7. 最后的操作

  1. 等待连接建立
  2. 发布操作
20230725225927

20230725225943

# 六、实战:基于 RDMA 的 client-server 程序

# 1. server 端

  • 工作流程:
    1. 初始化 RDMA 资源
    2. 等待 client 连接
    3. 分配并固定服务器缓冲区 buffer
    4. 接受客户端连接
    5. 将有关本地服务器缓冲区的信息发送到客户端
    6. 等待断开连接

# 2. client 端

  • 工作流程:
    1. 初始化 RDMA 资源
    2. 连接 server
    3. 通过发送/接收 exchange 接收服务器端缓冲区信息
    4. 从(第一个)本地缓冲区向服务器缓冲区进行 RDMA 写入。
    5. 进行 RDMA 读取,将服务器缓冲区的内容读入第二个本地缓冲区。
    6. 比较第一缓冲区和第二缓冲区的内容,并进行匹配
    7. 断开连接

# 3. 项目实现

PKUcoldkeyboard
/
RDMA-examples
Waiting for api.github.com...
0
0
unkown
Waiting...

# 4. 补充:RDMA 应用程序标准流程

20230729185145

# 参考文献

  • Mellanox Technologies, Inc. (2017). RDMA Aware Networks Programming User Manual. Mellanox Technologies, Inc.
本博客已稳定运行
总访客数: Loading
总访问量: Loading
发表了 73 篇文章 · 总计 323.73k

使用 Hugo 构建
主题 StackJimmy 设计
基于 v3.27.0 分支版本修改