1. UC SEND/RECEIVE 的完整交互过程图示
下面用左右两纵列 画一个 UC SEND/RECEIVE 的完整交互图。
左列:发送方 Client/Sender;右列:接收方 Server/Receiver。
│ 中间箭头表示交互;带外 TCP 用于交换 QP 信息;RoCEv2 数据通道 用于真正的 SEND 报文。
text
发送方 (Client/Sender) 接收方 (Server/Receiver)
────────────────────────────────────────────────────────────────────────────────────
【0】本地资源创建(双方独立,无交互)
ibv_get_device_list / ibv_open_device ibv_get_device_list / ibv_open_device
ibv_alloc_pd ibv_alloc_pd
ibv_reg_mr(buf_S) -> lkey_S ibv_reg_mr(buf_R) -> lkey_R
ibv_create_cq() ibv_create_cq()
ibv_create_qp(qp_type=IBV_QPT_UC) -> qp_S ibv_create_qp(qp_type=IBV_QPT_UC) -> qp_R
【1】带外 TCP 控制通道:交换连接信息
│ 发送: QPN_S, GID_S, PSN_S, MTU │
│ ───────────────────────────────────────────────────────> │
│ 回复: QPN_R, GID_R, PSN_R, MTU │
│ <─────────────────────────────────────────────────────── │
│ │
【2】QP 状态迁移:INIT -> RTR -> RTS
发送方 qp_S: 接收方 qp_R:
INIT -> RTR: INIT -> RTR:
dest_qp_num = QPN_R dest_qp_num = QPN_S
ah_attr.grh.dgid = GID_R ah_attr.grh.dgid = GID_S
path_mtu = 交换的 MTU path_mtu = 交换的 MTU
rq_psn = PSN_R rq_psn = PSN_S
RTR -> RTS: RTR -> RTS:
sq_psn = PSN_S sq_psn = PSN_R
(可发送 SEND) (可接收/也可发送)
【3】接收方必须先张贴 RECEIVE,否则报文会被静默丢弃
ibv_post_recv(qp_R, &recv_wr)
recv_wr.wr_id = RECV_WRID
recv_wr.sg_list -> buf_R
recv_wr.sg_list[0].lkey = lkey_R
【4】发送方张贴 SEND
ibv_post_send(qp_S, &send_wr)
send_wr.opcode = IBV_WR_SEND
send_wr.wr_id = SEND_WRID
send_wr.sg_list -> data_S
send_wr.sg_list[0].lkey = lkey_S
send_wr.send_flags = IBV_SEND_SIGNALED
【5】RoCEv2 数据通道:发送 SEND 报文
│ UDP/IP/Eth + BTH: opcode = SEND_ONLY / SEND_LAST │
│ Dest QPN = QPN_R, PSN = ..., payload = data_S │
│ ───────────────────────────────────────────────────────> │
│ │
│ RNIC 查 RQ:
│ 若匹配到 recv_wr:
│ DMA 写入 buf_R
│ 生成 CQE: IBV_WC_RECV
│ 若未匹配:
│ 静默丢弃
│ UC 无 ACK、无重传
【6】完成队列 CQ 通知
ibv_poll_cq(cq_S) -> CQE ibv_poll_cq(cq_R) -> CQE
opcode = IBV_WC_SEND opcode = IBV_WC_RECV
wr_id = SEND_WRID wr_id = RECV_WRID
⚠ 仅表示本地 RNIC 已发出, 处理 buf_R 中的数据
不保证接收方已收到
【7】可选:应用层确认 / 超时重传
│ <─────────────────────────────────────────────────────── │
│ 应用层 ACK:TCP 或另一次 SEND │
│ │
【8】资源释放
ibv_destroy_qp / ibv_destroy_cq / ibv_dereg_mr 同样按顺序释放
ibv_dealloc_pd / ibv_close_device
图中关键点
- UC 是面向连接的 :必须先通过带外 TCP 交换
QPN / GID / PSN / MTU,再把 QP 从INIT -> RTR -> RTS。 - SEND/RECEIVE 是双边操作 :接收方必须先
ibv_post_recv(),发送方的SEND才有地方落数据。 - SEND 不需要交换 rkey :数据写到接收方哪个缓冲区,由接收方的
recv_wr.sg_list决定,不是发送方指定远端地址。 - UC 不可靠 :没有 ACK/NACK、没有重传;如果接收方 RQ 中没有
RECV WR,或者网络丢包,报文会被丢弃。 - 发送方
IBV_WC_SEND不等于对端收到:它只表示本地 RNIC 已经把报文发出。要确认对端处理成功,需要应用层自己做 ACK、超时和重传。
2. UC SEND/RECEIVE 的完整交互过程分析
UC(Unreliable Connection,不可靠连接)的 SEND/RECEIVE 是一套双边操作 。与单边操作(如 RDMA WRITE)不同,双边的 SEND/RECEIVE 要求接收方必须预先在接收队列(RQ)中张贴 RECEIVE 请求,为即将到达的数据准备好"落脚点"。
一个完整的交互流程可以分为以下五个阶段。
🧱 阶段一:资源创建与初始化(两端各自独立完成)
在建立连接之前,通信双方(Client 和 Server)都需要在本地完成一系列资源分配与初始化工作,这些操作是独立进行的。
- 获取设备列表 :通过
ibv_get_device_list()获取可用的 RDMA 设备(如mlx5_0)。 - 打开设备 :使用
ibv_open_device()打开选定的设备,获取设备上下文ibv_context。 - 分配保护域(PD) :调用
ibv_alloc_pd()分配一个保护域。PD 是内存区域(MR)和队列对(QP)的容器,用于实现内存保护。 - 注册内存区域(MR) :使用
ibv_reg_mr()注册将要用于发送和接收的内存缓冲区。注册后,网卡才能直接读写这些内存。返回的ibv_mr结构体中包含了后续操作所需的lkey(本地密钥) 和rkey(远程密钥)。 - 创建完成队列(CQ) :调用
ibv_create_cq()创建完成队列。当发送或接收请求完成后,相应的完成事件(CQE)会被放入 CQ 中,供应用程序查询。 - 创建队列对(QP) :调用
ibv_create_qp()创建 QP。关键是在ibv_qp_init_attr结构体中指定qp_type = IBV_QPT_UC,并关联之前创建的发送 CQ 和接收 CQ。
🔗 阶段二:连接建立与状态迁移(双方协作)
UC 是面向连接的,一个 QP 必须与远端的另一个 QP 建立点对点连接。此阶段双方需要通过带外通道(如 TCP socket) 交换信息,并驱动本地 QP 完成状态迁移。
信息交换:双方通过 TCP 等可靠通道交换各自的 QP 连接信息,核心包括:
- QPN(Queue Pair Number):本地 QP 的编号。
- GID(Global Identifier):RoCEv2 中的全局标识符,用于寻址。
- PSN(Packet Sequence Number):初始包序列号,用于检测丢包和乱序。
QP 状态迁移:RDMA 规范要求 QP 必须按顺序经过一系列状态才能进行通信。对于 UC,主要涉及以下两个关键状态:
- INIT → RTR(Ready to Receive) :调用
ibv_modify_qp()将 QP 从 INIT 状态迁移到 RTR 状态。此时需要配置远端信息,包括dest_qp_num(远端 QPN) 、ah_attr(包含远端 GID 的地址句柄) 、path_mtu等。进入 RTR 后,QP 就可以接收 来自远端的SEND报文了。 - RTR → RTS(Ready to Send) :再次调用
ibv_modify_qp(),将 QP 从 RTR 状态迁移到 RTS 状态。此阶段需要设置sq_psn(发送队列的初始 PSN) 。进入 RTS 后,QP 就可以发送SEND报文了。
注意 :由于 UC 是不可靠的,状态迁移中不需要设置
timeout、retry_cnt等 RC 所必需的重传参数,这是它与 RC 在配置上的显著区别。
📨 阶段三:数据交换(SEND/RECEIVE 核心流程)
连接建立后,真正的数据交换才开始。由于是双边操作,接收方必须"先发制人",抢先张贴 RECEIVE 请求。
接收方:张贴 RECEIVE 请求(ibv_post_recv)
接收方应用程序调用 ibv_post_recv(),将一个接收工作请求(Receive WR)放入 QP 的接收队列(RQ)中。这个请求告诉网卡:"如果收到一个 SEND 消息,请把它的数据放到我指定的这个内存缓冲区里。"
c
// 构建接收请求 (Receive WR)
struct ibv_recv_wr recv_wr = {
.wr_id = RECV_WRID, // 用户自定义的请求ID
.sg_list = &recv_sge, // 指向 scatter/gather 列表
.num_sge = 1,
.next = NULL
};
// 张贴到接收队列
ibv_post_recv(qp, &recv_wr, &bad_wr);
发送方:张贴 SEND 请求(ibv_post_send)
发送方应用程序调用 ibv_post_send(),将一个发送工作请求(Send WR)放入 QP 的发送队列(SQ)中。关键是要将 opcode 设置为 IBV_WR_SEND。
c
// 构建发送请求 (Send WR)
struct ibv_send_wr send_wr = {
.wr_id = SEND_WRID,
.sg_list = &send_sge, // 指向要发送的数据
.num_sge = 1,
.opcode = IBV_WR_SEND, // ** 关键:指定为 SEND 操作 **
.send_flags = IBV_SEND_SIGNALED, // 请求发送完成通知
.next = NULL
};
// 张贴到发送队列
ibv_post_send(qp, &send_wr, &bad_wr);
硬件交互与不可靠性
发送方的网卡(RNIC)从 SQ 中取出 SEND 请求,将数据打包成 RoCEv2 报文(UDP 封装),通过网络发送出去。
接收方的网卡收到报文后,会查找 RQ 中是否有已张贴的 RECEIVE 请求:
- 如果找到 :网卡直接将数据负载写入
RECEIVE请求所指定的内存缓冲区,并生成一个完成事件(CQE)放入接收 CQ。 - 如果找不到 :由于 UC 的不可靠 特性,该报文会被静默丢弃 。接收方不会收到任何通知,发送方也不会收到任何确认或重传,数据就永久丢失了。这是 UC 与 RC 最本质的区别------UC 将错误处理的职责完全交给了应用程序。
✅ 阶段四:完成通知(Completion Notification)
应用程序通过轮询(busy-polling)或等待事件(waiting for events)的方式检查完成队列(CQ),以获知操作是否完成。
- 接收方 :当
RECEIVE操作完成(数据已写入内存)后,CQ 中会出现一个 CQE。其opcode为IBV_WC_RECV。 - 发送方 :如果
SEND请求的send_flags中设置了IBV_SEND_SIGNALED,当发送操作被网卡处理后,CQ 中也会出现一个 CQE。其opcode为IBV_WC_SEND。
应用程序通过 ibv_poll_cq() 取出 CQE,并根据 wr_id 区分是哪个请求完成了。
重要提示 :在 UC 中,发送方收到
IBV_WC_SEND完成事件仅表示本地网卡已成功将报文发出 ,并不保证接收方已收到。接收方是否收到,只能通过应用程序层的逻辑(如接收方回复一个确认消息)来间接判断。
🧹 阶段五:资源释放
通信结束后,应用程序需要按顺序释放所有资源:
ibv_destroy_qp():销毁 QP。ibv_destroy_cq():销毁 CQ。ibv_dereg_mr():注销内存区域。ibv_dealloc_pd():释放保护域。ibv_close_device():关闭设备。
💎 总结
UC SEND/RECEIVE 的完整交互过程,其核心逻辑可以概括为:
接收方必须"先下车"(
post_recv),发送方才能"投递"(post_send);而一旦"投递"失败(丢包),由于 UC 没有"回执"机制,发送方无从知晓,接收方也静默丢弃,一切错误处理都依赖于上层应用自身的超时与重传逻辑。
这与 RC 形成了鲜明对比:RC 的 SEND/RECEIVE 由硬件保证按序可靠送达,而 UC 则是一个"尽力而为"的双边通信原语,用可靠性换取了更低的协议开销和更少的 QP 资源占用。