分析对象: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 时位于最前)
要点:
- 纯零拷贝 :data seg 里的
lkey来自ibv_reg_mr()注册的接收内存,HCA 收到包后按 lkey 做地址翻译直接 DMA 写入应用缓冲。 - 无 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),防止非法状态收包。 - 批量投递一次屏障:循环内填所有 WQE,循环外一次 barrier + 一次 DBR 更新。
- 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 立即数放进 CQEimm_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(消费者)共享的环形缓冲:
- 每条 CQE 的最后字节
op_own的 bit0 是 ownership 标志 ,每绕完一圈翻转一次;mlx5_get_next_cqe()只看该位即知"这条是否已被硬件写过",随后udma_from_device_barrier()保证先见标志再见内容; - 软件收割后
++cons_index,update_cons_index()写dbrec[MLX5_CQ_SET_CI]把槽位还给硬件------同样只写 DBR,不敲门铃; - 需要中断时
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. 小结
- 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}。 - 不可靠性全部落在接收端语义 :PSN 空洞丢包、无缓冲丢包,均静默丢弃、无 NAK、无重传(QPC 无 RNR/ACK 计时器字段被 UC 使用);应用必须保持 RQ 有水并容忍偶发丢失。
- IMM/INV 是连接型语义的组成部分 :
RDMA_WRITE_WITH_IMM也会消耗接收 WQE(IBV_WC_RECV_RDMA_WITH_IMM),SEND_WITH_INV携带待失效 rkey------这是 UC 上做"零拷贝推数据 + 通知"的常用模式。 - 性能机制与发送侧对称:kernel bypass、零拷贝 lkey、批量投递一次屏障、DBR 替代 MMIO 门铃、CQE ownership bit 无锁收割、校验和卸载与 inline scatter 小包优化。