RoCE UC Receive 实现代码结构与原理分析

分析对象:MLNX_OFED 24.10-5.1.6.1 源码包(与 analysis-02 同源)

  • 用户态:rdma-core-2410mlnx54/providers/mlx5/
  • 内核态:mlnx-ofed-kernel-24.10.OFED.24.10.5.1.6.1/drivers/infiniband/hw/mlx5/

UC 接收由两个互补动作组成:预投递接收缓冲(ibv_post_recv) 与 收割接收完成(ibv_poll_cq)。两者都在用户态完成,绕过内核。


1. 总体结构

复制代码
发送方 UC SEND ──RoCEv2 报文──▶ 接收方 HCA
                                   │ ① 按 BTH 目的 QPN 定位 QP(st=UC)
                                   │ ② 校验 PSN 连续性(QPC.next_rcv_psn)
                                   │ ③ 从 RQ 取一个接收 WQE(预投递的 SGE 数组)
                                   │ ④ DMA 把消息负载写入 SGE 指向的内存
                                   │ ⑤ 写 CQE 到 CQ(opcode=RESP_SEND*,含 byte_cnt/IMM)
                                   ▼
应用:ibv_poll_cq ──▶ mlx5_parse_cqe ──▶ handle_responder ──▶ ibv_wc(RECV, byte_len, wr_id)
       │                                                              │
       └── ibv_post_recv:把接收 WQE(data seg 数组) 填进 RQ,更新 DBR ◀┘(补投,保持 RQ 有水)

与发送侧(analysis-02)对照:发送 = 在 SQ 放"发送描述符"并敲 MMIO 门铃;接收 = 在 RQ 放"缓冲区描述符",只写 host 内存里的 doorbell 记录(DBR),无 MMIO 门铃------HCA 有包到达时才按 DBR 取接收 WQE。


2. 用户态实现(rdma-core mlx5 provider)

2.1 投递接收:mlx5_post_recv()(providers/mlx5/qp.c:3826-3909)

c 复制代码
mlx5_spin_lock(&qp->rq.lock);
ind = qp->rq.head & (qp->rq.wqe_cnt - 1);        // RQ 环形缓冲写指针
for (nreq = 0; wr; ++nreq, wr = wr->next) {
    if (mlx5_wq_overflow(&qp->rq, nreq, recv_cq))  → ENOMEM;   // RQ 满
    if (wr->num_sge > qp->rq.qp_state_max_gs)      → EINVAL;   // SGE 超限
    scat = get_recv_wqe(qp, ind);                  // buf.buf + rq.offset + (ind << wqe_shift) (qp.c:65)
    for (i = 0, j = 0; i < wr->num_sge; ++i)       // 每个 SGE → 一个 16B data seg
        set_data_ptr_seg(scat + j++, wr->sg_list + i, 0);
    if (j < qp->rq.max_gs) {                       // 终止段:硬件识别 WQE 结束
        scat[j].byte_count = 0;
        scat[j].lkey       = htobe32(MLX5_INVALID_LKEY);
        scat[j].addr       = 0;
    }
    qp->rq.wrid[ind] = wr->wr_id;                  // 完成时回填给应用
    ind = (ind + 1) & (qp->rq.wqe_cnt - 1);
}
qp->rq.head += nreq;
udma_to_device_barrier();                          // 保证描述符先落内存
qp->db[MLX5_RCV_DBR] = htobe32(qp->rq.head & 0xffff);  // ★ 仅写 DBR,无 MMIO

RQ 接收 WQE 格式(与 SQ 的 ctrl+data 不同,RQ 无控制段):

复制代码
┌ data seg 0 (16B): byte_count / lkey / addr  ← SGE 0(接收缓冲 0)
├ data seg 1 (16B): ...                       ← SGE 1
├ data seg N (16B): byte_count=0 / lkey=MLX5_INVALID_LKEY / addr=0  ← 终止段
└(可选签名段,T10-DIF 场景,qp->wq_sig 时位于最前)

要点:

  1. 纯零拷贝 :data seg 里的 lkey 来自 ibv_reg_mr() 注册的接收内存,HCA 收到包后按 lkey 做地址翻译直接 DMA 写入应用缓冲。
  2. 无 MMIO 门铃 :与 SQ 不同,RQ 投递只更新 host 内存中的 doorbell record(qp->db[MLX5_RCV_DBR],MLX5_RCV_DBR=0,mlx5dv.h:70)。HCA 在需要接收 WQE 时自行 DMA 读取 DBR------因为"有包才消费",无需每次投递都打扰网卡。Raw Packet QP 在未到 RTR 状态时甚至不写 DBR(qp.c:3900-3903),防止非法状态收包。
  3. 批量投递一次屏障:循环内填所有 WQE,循环外一次 barrier + 一次 DBR 更新。
  4. SRQ 变体 (srq.c:257 mlx5_post_srq_recv):多个 QP 共享一个 SRQ 的接收缓冲池;WQE 组织成链表 (mlx5_wqe_srq_next_seg.next_wqe_index 指向下一个空闲 WQE),数据段格式相同。

2.2 收割完成:ibv_poll_cq → handle_responder()

调用链(providers/mlx5/cq.c):

复制代码
mlx5_poll_cq (cq.c:1392)
  └─ poll_cq (cq.c:997)            持 cq->lock,循环 ne 次
       └─ mlx5_poll_one (cq.c:981)
            ├─ mlx5_get_next_cqe (cq.c:536)   按 cons_index 取下一条 CQE,
            │      检查 ownership bit(op_own&1),udma_from_device_barrier()
            └─ mlx5_parse_cqe (cq.c:712)      按 CQE opcode 分发
                 ├─ MLX5_CQE_REQ*        → 发送完成(见 analysis-02)
                 └─ MLX5_CQE_RESP_SEND*  → ★ 接收完成 handle_responder (cq.c:257)
       └─ update_cons_index (cq.c:159)   dbrec[MLX5_CQ_SET_CI]=cons_index,归还 CQE 给硬件

CQE 结构(mlx5dv.h:1168 struct mlx5_cqe64,64B)关键字段:

字段 含义
op_own 高 4 位 = CQE opcode(RESP_SEND 等),bit0 = ownership(软件/硬件翻转标志)
byte_cnt 本条消息实际收到的字节数(可小于预投递缓冲总长)
wqe_counter 被消耗的接收 WQE 序号(定位 wrid)
sop_drop_qpn 低 24 位 = 本地 QPN
flags_rqpn 源 QPN、GRH 标志、SL
imm_inval_pkey immediate data / 待失效 rkey / pkey(复用字段)
slid、ml_path、hds_ip_ext 源 LID、路径、L3/L4 校验和状态

CQE opcode(mlx5dv.h:1127-1138)→ handle_responder()(cq.c:257-328)的翻译:

c 复制代码
wc->byte_len = be32toh(cqe->byte_cnt);           // 实收长度
wqe_ctr = be16toh(cqe->wqe_counter) & (wq->wqe_cnt - 1);
wc->wr_id  = wq->wrid[wqe_ctr];                  // 找回投递时的 wr_id
++wq->tail;                                      // 释放该接收 WQE

switch (cqe->op_own >> 4) {
case MLX5_CQE_RESP_WR_IMM:    // =1 对端 RDMA_WRITE_WITH_IMM 落地(也消耗一个接收 WQE!)
    wc->opcode = IBV_WC_RECV_RDMA_WITH_IMM;
    wc->wc_flags |= IBV_WC_WITH_IMM;
    wc->imm_data = cqe->imm_inval_pkey;          // 提取 IMMETH 中的 32bit 立即数
    break;
case MLX5_CQE_RESP_SEND:      // =2 普通 SEND
    wc->opcode = IBV_WC_RECV;
    break;
case MLX5_CQE_RESP_SEND_IMM:  // =3 SEND_WITH_IMM
    wc->opcode = IBV_WC_RECV;  wc->wc_flags |= IBV_WC_WITH_IMM;
    wc->imm_data = cqe->imm_inval_pkey;
    break;
case MLX5_CQE_RESP_SEND_INV:  // =4 SEND_WITH_INV
    wc->opcode = IBV_WC_RECV;  wc->wc_flags |= IBV_WC_WITH_INV;
    wc->invalidated_rkey = be32toh(cqe->imm_inval_pkey);  // 对端要求失效的 rkey
    break;
}
wc->slid = ...; wc->sl = ...; wc->src_qp = flags_rqpn & 0xffffff;   // 对端信息
g = (flags_rqpn >> 28) & 3;  wc->wc_flags |= g ? IBV_WC_GRH : 0;    // RoCE 带 GRH

附加处理:

  • 接收 inline scatter (cq.c:289-294):若 HCA 把小包负载直接塞进 CQE(op_own & MLX5_INLINE_SCATTER_32/64),mlx5_copy_to_recv_wqe()(qp.c:104)再把它拷进预投递的缓冲------CQE 即数据源,省一次 DMA。
  • 校验和卸载 (cq.c:280-281, 249-255):RoCE 场景 HCA 验证 IPv4+L4 checksum,全对则置 IBV_WC_IP_CSUM_OK。
  • 错误接收 (cq.c:881-887 MLX5_CQE_RESP_ERR):mlx5_handle_error_cqe()(cq.c:340)按 syndrome 映射,UC 接收端最常见的是 MLX5_CQE_SYNDROME_LOCAL_LENGTH_ERR → IBV_WC_LOC_LEN_ERR(对端消息比预投递缓冲还长) ;UC 不会出现的:TRANSPORT_RETRY_EXC_ERR、RNR_RETRY_EXC_ERR(RC 专有)。
  • 事件通知 (cq.c:1730 mlx5_arm_cq):写 CQ doorbell record + mmio_write64_be(ctx->cq_uar_reg + MLX5_CQ_DOORBELL, ...) 武装中断,配合 ibv_get_cq_event 做事件驱动接收。

2.3 QP-EX 收包接口

mlx5_qp_fill_wr_pfns()(qp.c:3559)对包括 UC 在内的 QP 注册 ibqp->wr_recv = mlx5_send_wr_recv*(qp-ex 形式投递),与 send 共用同一套 RQ 机制。


3. 内核态镜像(mlx5_ib)

内核消费者(存储类 ULP 等)使用同构实现:

文件:行 内容
wr.c:1220 mlx5_ib_post_recv() 与用户态同构:锁 RQ → 逐 WR 填 data seg → 终止段用 dev->mkeys.terminate_scatter_list_mkey (内核专用终止 mkey,用户态用 MLX5_INVALID_LKEY)→ wmb() → *qp->db.db = rq.head(同样只写 DBR,无 MMIO)
cq.c:170 handle_responder() 内核版完成解析:MLX5_CQE_RESP_WR_IMM → IB_WC_RECV_RDMA_WITH_IMM、RESP_SEND → IB_WC_RECV(附带 L3/L4 csum 检查)、RESP_SEND_IMM/INV;同样回填 byte_len/src_qp/GRH
main.c ops 表 .post_recv = mlx5_ib_post_recv_nodrain
drivers/infiniband/core/uverbs_cmd.c:2294 ib_uverbs_post_recv 兼容路径(非直通报备接口)

3.1 QPC 中的接收侧状态(rdma-core .../mlx5_ifc.h:3672-3790,与内核 IFA 布局一致)

固件 QPC 内维护的接收相关字段,是 UC 接收行为的硬件上下文:

复制代码
st[0x8]              服务类型:UC=0x1(与发送共用)
next_rcv_psn[0x18]   ★ 期望接收的下一个 PSN ------ 收包 PSN 校验基准
rmsn[0x18]           接收消息序号(消息边界跟踪)
cqn_rcv[0x18]        接收方向绑定的 CQ
rq_type[0x3]         普通 RQ / SRQ / CRQ / 零长 RQ
dbr_addr[0x40]       ★ RQ doorbell record 的 host 物理地址(HCA 读它取新 WQE)
hw_rq_counter/sw_rq_counter   硬件/软件 RQ 消耗计数(调试与一致性)
min_rnr_nak[0x5]     RNR NAK 定时器 ------ ★ RC 专用,UC 不使用
last_acked_psn       ACK 跟踪 ------ RC 专用,UC 不使用

RQ 缓冲本身在创建 QP 时由 qp.c(内核)/ verbs.c(用户态)按 log_rq_size/log_rq_stride 划好,经 CREATE_QP 命令把起始地址交给固件。


4. UC Receive 原理

4.1 接收模型:预投递缓冲 + 按序消耗

RDMA 接收是生产者-消费者 模型:应用必须提前 把足够大的空闲缓冲(SGE 数组)投进 RQ;HCA 每完整收到一条消息就按投递顺序 消耗一个接收 WQE,把负载 DMA 进去,再用 CQE 告知"哪个 wr_id 的缓冲被填了多少字节"。缓冲复用靠应用 poll 到完成后补投 (repost)------perftest 的主循环即如此(perftest_resources.c:3303/3844/4131/4728 等处 ibv_post_recv 补投)。

4.2 报文分流与 QP 定位(RoCE 特有)

RoCEv2 报文到达后:HCA 先按 Eth/IP/UDP(dport=4791) 接收并校验(可选 L3/L4 csum 卸载 → IBV_WC_IP_CSUM_OK),剥出 IB BTH,用 BTH 里的 dest QPN 找到目标 QP 的 QPC ;QPC st=UC 决定按 UC 语义处理。源信息(源 QPN、SL、GRH 存在位)由硬件抄进 CQE,这就是 handle_responder() 里 wc->src_qp、IBV_WC_GRH 的来源。

4.3 PSN 校验:UC 的"不可靠"在接收端的体现

QPC 的 next_rcv_psn 是期望序号。每条 UC 消息按 Send First/Middle/Last/Only 携带递增 PSN:

  • PSN 连续 → 正常重组、写缓冲、产生 CQE;
  • PSN 出现空洞(中间丢包)→ UC 硬件静默丢弃失序包,不发 NAK、不通知对端重传(QPC 里 UC 没有 RNR/ACK 计时器,见 3.1);应用层只能感知到"某条消息永远没来";
  • 消息边界由 BTH opcode(First/Middle/Last/Only)划分;多包消息的各分段按序写入同一个接收 WQE 的连续 SGE 空间,byte_cnt 汇总整条消息长度。

这正是 UC 与 RC 接收的本质差异:RC 收端发现 PSN 空洞会回 NAK(PSN sequence error) 触发对端重传,并对"无接收缓冲"回 RNR NAK(min_rnr_nak 定时退避);UC 两者都没有 ------没缓冲、丢包都是直接丢。因此 UC 应用的铁律是 RQ 永远保持有水,否则消息无声丢失。

4.4 IMM / INV 的处理

  • 对端 SEND_WITH_IMM:HCA 剥出 IMMETH 头中的 32bit 立即数放进 CQE imm_inval_pkey,完成标记 IBV_WC_WITH_IMM;
  • 对端 RDMA_WRITE_WITH_IMM:RDMA 写本身不碰 RQ(直接落地到远端内存),但 IMM 部分必须消耗一个接收 WQE 来承载通知------这就是 IBV_WC_RECV_RDMA_WITH_IMM(byte_len=0)存在的意义,也是 UC 接收端要为 RDMA_WRITE+IMM 预留缓冲的原因;
  • 对端 SEND_WITH_INV:CQE 带 IBV_WC_WITH_INV + invalidated_rkey,应用应随后使该 rkey 对应的 MR 失效(与发送侧 IBV_WR_LOCAL_INV 配合的零拷贝流控手法)。

4.5 CQE 所有权与环形 CQ 机制

CQ 是 HCA(生产者)与 CPU(消费者)共享的环形缓冲:

  1. 每条 CQE 的最后字节 op_own 的 bit0 是 ownership 标志 ,每绕完一圈翻转一次;mlx5_get_next_cqe() 只看该位即知"这条是否已被硬件写过",随后 udma_from_device_barrier() 保证先见标志再见内容;
  2. 软件收割后 ++cons_index,update_cons_index() 写 dbrec[MLX5_CQ_SET_CI] 把槽位还给硬件------同样只写 DBR,不敲门铃;
  3. 需要中断时 mlx5_arm_cq() 才有一次真实的 MMIO 写(CQ doorbell,请求下一个完成事件通知)。

4.6 一次 UC 接收的完整时序

复制代码
① 应用 ibv_post_recv:RQ[ind] ← {data seg×N + 终止段},wrid[ind]=wr_id;
   barrier;DBR[RCV]=rq.head(无 MMIO)
② 对端 SEND 到达(RoCEv2: Eth/IP/UDP4791/BTH/payload/ICRC)
③ HCA:校验 L3/L4/ICRC → BTH.dest_qpn 定位 QPC(st=UC) → 读 DBR 发现有可用接收 WQE
④ HCA:PSN==next_rcv_psn? 是→按 lkey 翻译地址,DMA 负载入缓冲,next_rcv_psn++
       否→静默丢弃(无 NAK,不通知对端)
⑤ 消息收齐(Last/Only 包)→ 写 CQE{opcode=RESP_SEND, wqe_counter, byte_cnt, src_qp, GRH...}
   (若武装过 CQ,同时触发完成事件中断)
⑥ 应用 ibv_poll_cq → mlx5_parse_cqe → handle_responder:
   wc={opcode:IBV_WC_RECV, byte_len, wr_id, src_qp, wc_flags}
⑦ 应用处理数据 → 回到 ① 补投该缓冲

4.7 收 vs 发对称性速查

维度 发送 (SQ) 接收 (RQ)
WQE 内容 ctrl 段(opcode/QPN/signaled) + raddr + data 段 仅 data 段数组 + 终止段(无 ctrl)
通知 HCA doorbell record + MMIO 门铃(BF) 仅写 doorbell record,无 MMIO
软件指针 sq.head(投递)/ sq.tail(完成回收) rq.head(投递)/ rq.tail(完成回收,++tail)
完成 CQE MLX5_CQE_REQ(发出即完成) MLX5_CQE_RESP_SEND/IMM/INV/WR_IMM
完成语义 本地发出(无 ACK) 消息实收字节数 byte_cnt
UC 特有丢失场景 网络丢包(无重传) PSN 空洞静默丢弃;无缓冲静默丢弃

5. 关键代码位置索引

用户态 rdma-core-2410mlnx54/providers/mlx5/

位置 说明
qp.c:3826 mlx5_post_recv 接收 WQE 投递主函数(核心)
qp.c:65 get_recv_wqe RQ 环形缓冲地址计算(buf + rq.offset + ind<<wqe_shift)
qp.c:3868-3872 终止段(lkey=MLX5_INVALID_LKEY)
qp.c:3884-3904 barrier + 写 db[MLX5_RCV_DBR](无 MMIO)
srq.c:257 mlx5_post_srq_recv SRQ 链表式接收池
cq.c:1392 mlx5_poll_cq → cq.c:997 poll_cq → cq.c:981 mlx5_poll_one 完成收割调用链
cq.c:536 mlx5_get_next_cqe ownership bit 判定 + 屏障
cq.c:257 handle_responder 接收 CQE → ibv_wc 翻译(核心)
cq.c:810-837 mlx5_parse_cqe RESP 分支 RESP_SEND/IMM/INV/WR_IMM 分发
cq.c:104(→qp.c:104) mlx5_copy_to_recv_wqe 接收 inline scatter 拷贝
cq.c:340 mlx5_handle_error_cqe 错误 syndrome → ibv_wc_status
cq.c:1730 mlx5_arm_cq CQ 中断武装(唯一的 CQ 侧 MMIO)
mlx5dv.h:1168 struct mlx5_cqe64、1127-1138 CQE opcode、70 RCV/SND_DBR CQE 布局与常量
mlx5_ifc.h:3720-3790 QPC 接收侧字段(next_rcv_psn/rmsn/dbr_addr/rq_type...)

内核态 mlnx-ofed-kernel-24.10.OFED.24.10.5.1.6.1/drivers/infiniband/hw/mlx5/

位置 说明
wr.c:1220 mlx5_ib_post_recv 内核接收投递(终止段用 terminate_scatter_list_mkey,wr.c:1269)
cq.c:170 handle_responder 内核接收完成解析(含 csum 检查置 IB_WC_IP_CSUM_OK)
cq.c:519-523 RESP opcode 分发表
core/uverbs_cmd.c:2294 ib_uverbs_post_recv 兼容路径

应用范例

位置 说明
perftest-24.10.0/src/perftest_resources.c:3303/3844/4131/4728 接收完成→处理→ibv_post_recv 补投的完整业务循环

6. 小结

  1. UC 接收 = 预投递 + 收割 :mlx5_post_recv 把"SGE 数组 + 终止段"写进 RQ 环形缓冲并更新 DBR(无 MMIO);HCA 按 BTH 目的 QPN 找到 QP、校验 PSN、DMA 写入缓冲、发 CQE;handle_responder 把 CQE 翻成 ibv_wc{RECV, byte_len, wr_id}。
  2. 不可靠性全部落在接收端语义 :PSN 空洞丢包、无缓冲丢包,均静默丢弃、无 NAK、无重传(QPC 无 RNR/ACK 计时器字段被 UC 使用);应用必须保持 RQ 有水并容忍偶发丢失。
  3. IMM/INV 是连接型语义的组成部分 :RDMA_WRITE_WITH_IMM 也会消耗接收 WQE(IBV_WC_RECV_RDMA_WITH_IMM),SEND_WITH_INV 携带待失效 rkey------这是 UC 上做"零拷贝推数据 + 通知"的常用模式。
  4. 性能机制与发送侧对称:kernel bypass、零拷贝 lkey、批量投递一次屏障、DBR 替代 MMIO 门铃、CQE ownership bit 无锁收割、校验和卸载与 inline scatter 小包优化。
相关推荐
Eloudy5 小时前
RoCE 的 UC SEND/RECEIVE 的完整交互过程分析
网络·rdma·roce
论文复现现场2 天前
8卡 A100 服务器哪里租?NVLink、RDMA 与 NCCL 验收指南
linux·服务器·php·分布式训练·rdma·nvlink·nccl
gwf2163 天前
RDMA事件与异步通知机制:CMA事件与设备事件深度解析
驱动开发·linux内核·rdma·rdma_cm·异构计算·异步事件·cve分析
cyf314 天前
RDMA 写操作带宽性能建模与参数敏感性分析
性能优化·rdma·rocev2
gwf2165 天前
QP状态机详解:Reset→Init→RTR→RTS与建链握手过程
性能调优·rdma·qp状态机·linux内核驱动·ib_modify_qp·cm建链·irdma
cyf315 天前
RDMA数据传输原理深入解读
udp·rdma·rocev2·bth·pertest
gwf2167 天前
Completion Queue(CQ)与中断处理:轮询、中断聚合与错误码 —— 面向AI集群的驱动级深度剖析
驱动开发·性能调优·pcie·rdma·ai集群·中断聚合·cq
gwf2168 天前
内核RDMA子系统(ib_core)架构与核心数据结构:从驱动视角解构数据通路
linux内核·rdma·网卡驱动·mlx5·gpudirect·ib_core·内核架构
gwf21610 天前
Verbs性能优化实战:Inline/Signaled/批处理/NUMA亲和全攻略
rdma·numa·io_uring·内核驱动·mlx5·verbs·hugepage