本文以 RC QP、verbs 接口和 mlx5 为主线,分析发送和接收请求的提交流程、完成事件的数据处理过程。除特别说明外,“本端”表示发起操作的一端,“远端”表示 RC 连接的另一端。
1. 架构
下图展示了基于 Linux mlx5 驱动的 RDMA 数据收发与完成事件通知架构。整体分为用户态、内核态和 HCA 硬件三个部分,以 QP(Queue Pair)、CQ(Completion Queue)和 EQ(Event Queue) 为核心,描述 RDMA 数据请求的提交、硬件执行、完成状态记录及事件通知机制。
RDMA 直接在用户态访问硬件队列,从而实现了高性能数据传输。正常的数据收发和 CQ 轮询不需要内核参与;内核主要负责资源管理、中断处理以及完成事件通知。
- 消费者(Consumer)
- RDMA 应用程序,负责发送数据、接收数据及处理完成事件。
- 通过 RDMA Verbs 接口操作 QP、CQ 等资源,支持轮询和事件通知两种处理方式。
- libibverbs
- 提供统一的 RDMA Verbs 用户态接口,屏蔽不同 HCA 设备的实现差异。
- 将应用的收发和完成管理请求分发给对应的设备 Provider,并管理完成事件通道。
- mlx5 Provider
- Mellanox/NVIDIA HCA 的用户态驱动实现,负责将 Verbs 操作转换为设备相关的队列和寄存器操作。
- 管理用户态 QP、CQ,构建 WQE,并通过 DBR(Doorbell Record)和 UAR(User Access Region) 与 HCA 交互。
- 直接访问 CQE(Completion Queue Entry),完成硬件执行状态的获取与解析。
- Linux 内核态(RDMA Core / mlx5 驱动)
- 负责 RDMA 硬件资源的创建、配置和管理,并建立用户态与硬件之间的资源映射。
- 通过 MSI-X 中断和 EQ 处理 HCA 上报的完成事件,并根据 CQ 标识找到对应的内核 CQ 对象。
- 通过 Completion Channel 将完成通知传递到用户态,支持应用采用事件驱动方式处理 CQ。
- HCA 硬件(Host Channel Adapter)
- RDMA 数据传输的硬件执行主体,负责解析和执行 SQ/RQ 中的 WQE,完成网络数据收发和 DMA 操作。
- 通过 QP/QPC 管理收发队列执行状态,通过 CQ/CQC 管理完成状态,通过 EQ/EQC 管理异步事件与中断通知。
- 将执行结果写入 CQE,并在满足通知条件时通过 EQ 和 MSI-X 向主机上报完成事件。
整体架构实现了用户态直接提交数据传输请求和获取结果、HCA 执行 RDMA 操作、内核处理异步完成通知的职责划分,从而减少数据路径上的内核开销,同时支持高性能轮询与中断驱动两种完成处理机制。
2. 请求提交
应用通过 Verbs 接口创建数据路径对象、发布 WR,并从 CQ 取得执行结果。
2.1 接口定义
| 接口定义 | 功能 |
|---|---|
int ibv_post_send(struct ibv_qp *qp, struct ibv_send_wr *wr, struct ibv_send_wr **bad_wr) | 把一个或多个发送 WR 发布到 SQ。 |
int ibv_post_recv(struct ibv_qp *qp, struct ibv_recv_wr *wr, struct ibv_recv_wr **bad_wr) | 把一个或多个接收 WR 发布到 RQ。 |
struct ibv_comp_channel *ibv_create_comp_channel(struct ibv_context *context) | 为指定设备 context 创建 completion channel,成功返回 channel,失败返回NULL并设置errno。应用随后在创建 CQ 时传入该 channel,以关联完成通知。 |
int ibv_destroy_comp_channel(struct ibv_comp_channel *channel) | 销毁 completion channel,关闭其 fd 并释放用户态对象。仍有关联 CQ 时返回EBUSY,成功返回 0。 |
int ibv_poll_cq(struct ibv_cq *cq, int num_entries, struct ibv_wc *wc) | 从 CQ 获取 WC(work completions)。 |
int ibv_req_notify_cq(struct ibv_cq *cq, int solicited_only) | 为 CQ 请求一次性通知:solicited_only=0时任意新 CQE 均可触发;非零时仅带 Solicited 标记的接收完成或错误完成触发。 |
int ibv_get_cq_event(struct ibv_comp_channel *channel, struct ibv_cq **cq, void **cq_context) | 从 completion channel 获取产生完成事件的 CQ,请求完成结果通过ibv_poll_cq获取。 |
void ibv_ack_cq_events(struct ibv_cq *cq, unsigned int nevents) | 确认已经成功取得的完成事件。 |
2.2 示例
以下是 Verbs 流程片段,省略初始化细节。
步骤 1:创建资源。
- 设备列表用于选择 RDMA 设备并取得 context;列表本身在打开设备后即可释放。
- PD 隔离访问域,MR 把业务缓冲区注册到该 PD,后续 SGE 使用 MR 返回的
lkey。 - completion channel 提供事件 fd,CQ 保存完成记录(CQE),QP 引用 PD 和 CQ;QP 创建后先迁移到 INIT。
devices=ibv_get_device_list(&num_devices);context=ibv_open_device(devices[0]);ibv_free_device_list(devices);pd=ibv_alloc_pd(context);mr=ibv_reg_mr(pd,buffer,buffer_length,IBV_ACCESS_LOCAL_WRITE);channel=ibv_create_comp_channel(context);cq=ibv_create_cq(context,16,cq_context,channel,0);qp=ibv_create_qp(pd,&qp_init_attr);structibv_qp_attrinit_attr={/* 将新创建的 QP 从 RESET 迁移到 INIT。 */.qp_state=IBV_QPS_INIT,/* 使用 P_Key 表的第 0 项。 */.pkey_index=0,/* 指定 QP 使用的本地物理端口。 */.port_num=ib_port,/* 本例只使用 SEND/RECV,不开放远端 RDMA/Atomic 访问。 */.qp_access_flags=0,};/* 指定本次 ibv_modify_qp() 应用 init_attr 中的四个属性。 */intinit_mask=IBV_QP_STATE|IBV_QP_PKEY_INDEX|IBV_QP_PORT|IBV_QP_ACCESS_FLAGS;ibv_modify_qp(qp,&init_attr,init_mask);步骤 2:交换参数并连接 QP。
ibv_query_port()和ibv_query_gid()取得本端路径信息,QP 本身提供 QPN;应用还要为连接选择 PSN。- 两端通过 TCP、RDMA CM 或其他控制面交换 QPN、PSN、LID/GID 和 MTU。该交换不属于正常 RDMA 数据路径,代码中只保留占位注释。
- 应用根据远端信息填好
rtr_attr和rts_attr,依次把 RC QP 迁移到 RTR、RTS。
ibv_query_port(context,ib_port,&port_attr);if(gid_index>=0)ibv_query_gid(context,ib_port,gid_index,&gid);/* 控制面交换连接参数,并据此准备 rtr_attr 和 rts_attr。 */ibv_modify_qp(qp,&rtr_attr,rtr_mask);ibv_modify_qp(qp,&rts_attr,rts_mask);步骤 3:接收和发送。
ibv_req_notify_cq()在完成事件产生前 arm CQ,避免应用错过完成事件通知。- 接收端先用
ibv_post_recv()提供 RQ 缓冲区;控制面确认对端 RQ 已准备好后,发送端再发布 SEND。 - 两个 post 接口返回成功只表示 WR 已发布。SEND/RECV 的执行结果仍要通过 WC 判断。
ibv_req_notify_cq(cq,0);ibv_post_recv(qp,&recv_wr,&bad_recv_wr);/* 控制面确认对端也已经发布 RECV。 */ibv_post_send(qp,&send_wr,&bad_send_wr);步骤 4:等待事件并轮询完成。
ibv_get_cq_event()阻塞读取 completion channel,返回需要检查的 CQ 和用户 context。ibv_ack_cq_events()更新用户态确认计数;随后重新 arm CQ,为下一次通知建立条件。ibv_poll_cq()才真正消费 CQE 并生成 WC。循环须排空 CQ,按wr_id和 QP 识别请求,先检查status,成功后再读取opcode等字段。
for(;;){if(ibv_get_cq_event(channel,&event_cq,&event_context)!=0)break;ibv_ack_cq_events(event_cq,1);if(ibv_req_notify_cq(event_cq,0)!=0)break;while((n=ibv_poll_cq(event_cq,wc_capacity,wc))>0)process_wc(wc,n);if(n<0||exchange_finished)break;}步骤 5:释放资源。
- 应用必须等预期 WC 全部返回并停止并发提交,再按对象依赖关系逆序销毁 QP、CQ、channel、MR、PD 和 context。
- 销毁 CQ 前必须确认所有已经由
ibv_get_cq_event()取得的事件,否则ibv_destroy_cq()可能等待未完成的事件确认。
/* 仅在相关设备访问已终止后清理;轮询失败不能直接视为全部完成。 */ibv_destroy_qp(qp);ibv_destroy_cq(cq);ibv_destroy_comp_channel(channel);ibv_dereg_mr(mr);ibv_dealloc_pd(pd);ibv_close_device(context);3. 发送流程
3.1 提交 WQE
发送过程把应用 WR 转换为 SQ WQE,再通过 DBR 和 UAR 发布请求。下图聚焦 SEND、RDMA READ 和 WRITE 的 CPU 提交路径,展开 WQE 编码、发布顺序与 MMIO 实现;设备执行与各操作的数据流见 3.2 节。
Verbs 分发与 SQ 锁共同确定请求入口和生产者顺序。
ibv_post_send()最终调用_mlx5_post_send(),后者持有 SQ 锁,检查 opcode、队列容量、SGE 数量和 QP 能力。图中省略了mlx5_post_send()函数。mlx5_wq_overflow()先读取 head/tail,接近容量上限时在 CQ 锁下重新读取完成进度;失败时bad_wr标出本批第一个未接受的请求。WQE 编码把操作、地址权限和数据来源转换为 HCA 可读的描述符。
_mlx5_post_send()以 16 B DS 统计长度,以 64 B WQEBB 推进cur_post;设备多字节字段采用大端格式。provider 另在sq.wrid[]和sq.wqe_head[]中分别保存应用的wr_id和 SQ 请求序号,供 CQ 轮询时识别请求并推进 SQ tail。- opcode 决定是否编码远端地址段。
set_raddr_seg()为 RDMA READ 和 WRITE 编码远端地址与rkey;SEND 使用远端 RQ 提供的数据目的地。 - inline 与非 inline 决定 payload 的保存位置。
set_data_inl_seg()检查 inline 长度,把 SGE 内容用memcpy()复制进主机 SQ,跨 SQ 尾部时分两段复制,并写入 inline 标志和长度。非 inline 的set_data_ptr_seg()编码byte_count、lkey和本地 MR 地址,零长度 SGE 被跳过。 - control segment 使 HCA 能识别完整 WQE。
opmod_idx_opcode编码 opmod、cur_post低 16 位和 mlx5 opcode,qpn_ds编码 QPN 与最终 DS 数;fm_ce_se保存 fence、signaled 和 solicited 控制,send_ieth()提供 immediate/invalidate 信息。启用 WQE signature 时,wq_sig()调用calc_sig()对指定范围逐字节异或并取反。
- opcode 决定是否编码远端地址段。
post_send_db()建立 WQE、DBR 与 WC MMIO 之间的发布顺序。nreq为零时直接返回;WR 正确时推进sq.head,再依次发布 DBR、写门铃、刷新 WC 写。Send DBR 发布 WQE 对应的生产者位置。
udma_to_device_barrier()位于 WQE 写入和 DBR 更新之间:x86_64 使用带memoryclobber 的空汇编,即编译器屏障;ARM64 使用DMB OSHST。随后把cur_post & 0xffff以大端格式写入主机内存中的MLX5_SND_DBR。x86_64 的保证适用于普通 cacheable store;应用使用 non-temporal store 或特殊内存类型时,需在调用 verbs 前自行执行SFENCE。WC 写入前的屏障保证 DBR 先于新的 UAR 写入。
mmio_wc_start()宏展开为mmio_flush_writes(),在 x86_64 执行SFENCE,在 ARM64 执行DSB ST,把此前 DBR/既有 MMIO 访问排在新的 WC UAR 写之前。BlueFlame 通过有序 MMIO 复制直接携带 WQE。 当 BF 已启用、本批只有一个 WR、UAR 有效、inline 或
prefer_bf成立且 WQE 长度符合窗口限制时,调用mlx5_bf_copy(),复制长度为align(size * 16, 64)。函数按 64 B 调用mmio_memcpy_x64()。x86_64 每块使用八次递增地址的mmio_write64_be(),最终经 relaxed 64-bit atomic store 落到一条MOV存储指令。ARM64 进入_mmio_memcpy_x64_64b(),通常使用vld4q_u64()/vst4q_u64(),编译启用__ARM_FEATURE_LS64且运行时aarch64_has_ls64为真时采用__arm_st64b()(ST64B,目标要求 64 B 对齐)。普通门铃通过一次 64-bit MMIO store 提交 control segment 的前 64 bit。 BF 条件不满足时,
mmio_write64_be()向bf->reg + bf->offset写入门铃。mmio_write64_be()将该访问实现为atomic_store_explicit(),目标类型为_Atomic(uint64_t),内存序为memory_order_relaxed。- x86_64 用一条普通
MOV完成 64-bit 存储。 对当前按 8 B 对齐的访问,编译器通常生成MOV r/m64, r64。 - ARM64 的普通 64-bit 门铃写使用
STR。 对相同的自然对齐 relaxed store,典型映射为STR Xt, [Xn],由 64-bit 数据寄存器写入目标地址。这里是单次标量存储,与 BlueFlame 64 B 复制采用的 NEON/ST64B分支不同。 - 地址映射决定 MMIO 目标,外围屏障保证写入顺序。 目标虚拟地址映射到设备 BAR 中的 UAR,访问按该映射的内存类型进入设备写入路径。
_be参数已经按设备大端格式编码,访问器原样保存其位模式,不再执行字节交换。relaxed store 负责这次 64-bit 写入,前后的mmio_wc_start()和mmio_flush_writes()负责 WC 排序与刷新。
- x86_64 用一条普通
WC 刷新使共享窗口中的门铃按临界区的执行顺序发出。 写门铃后,
mmio_flush_writes()在 x86_64 执行SFENCE、在 ARM64 执行DSB ST,促使本 CPU 的 WC 写及时发出。共享 UAR 时,刷新位于 BF 锁内,防止后一 CPU 的门铃先于前一 CPU 尚未刷出的门铃到达设备;随后通过bf->offset ^= bf->buf_size切换窗口。
3.2 执行
ibv_send_wr经 provider 翻译为设备格式的 SQ WQE。HCA 从 control segment 取得 opcode 和队列位置,从 data segment 取得本地 MR 访问地址与lkey,从 remote address segment 取得远端地址与rkey。MKey 将 MR 地址转换为 DMA 地址,并检查长度边界和访问权限;remote write、remote read 和 remote atomic 权限分别授权远端 WRITE、READ 和 Atomic。
| 操作 | HCA 的业务数据访问 | 远端 RQ 参与方式 | 远端正常 WC | 发起端正常 WC |
|---|---|---|---|---|
| SEND | DMA 读本端 SGE,经 RC 写入远端 RQ SGE | 消耗一个 RQ WQE | IBV_WC_RECV | IBV_WC_SEND |
| WRITE | DMA 读本端 SGE,写remote_addr/rkey指定的远端 MR | 直接访问 MR | — | IBV_WC_RDMA_WRITE |
| WRITE with immediate | 数据写远端 MR,immediate value 进入远端 receive WC | 消耗一个 RQ WQE | IBV_WC_RECV_RDMA_WITH_IMM | IBV_WC_RDMA_WRITE |
| READ | 读取远端 MR,DMA 写回本端 SGE | 直接访问 MR | — | IBV_WC_RDMA_READ |
| Atomic | 对远端 8 字节位置执行原子操作,DMA 写回原值 | 直接访问 MR | — | IBV_WC_COMP_SWAP或IBV_WC_FETCH_ADD |
3.3 提交边界
wr->next允许一次提交多个 WR。mlx5 在持锁区间逐项构造 WQE;遇到队列溢出、SGE 超限或 opcode 超出支持范围时,接口返回非零错误码,并让bad_wr指向第一个尚未发布的 WR。此前成功构造的 WQE 继续执行,bad_wr因而成为调用者处理剩余 WR 的起点。
IBV_SEND_INLINE适用于 SEND 与 RDMA WRITE,它把 payload 复制进 WQE,ibv_post_send()成功后即可复用原始发送缓冲区。非 inline WQE 保存 SGE 地址、长度和lkey,MR 与缓冲区的设备所有期持续到对应完成条件成立。selective signaling 通常周期性插入 signaled WR;同一 SQ 上后续 signaled WR 的成功 WC 可作为此前未 signaled WR 已完成的进度点,据此回收关联资源。
4. 接收流程
4.1 提交 WQE
接收过程把描述符放入 RQ,再通过 Receive DBR 发布生产者位置。下图上半部分展示 provider 的函数树,下半部分独立展示报文到达后的 HCA 数据流。
mlx5_post_recv()将可写缓冲区编码到固定 stride 的 RQ 槽位。 RQ 锁串行化生产者,容量和 SGE 检查保证描述符能够放入当前队列。- 定位槽位时同时考虑环形索引和 WQE stride。 首个索引由
rq.head & (rq.wqe_cnt - 1)得到,get_recv_wqe()按缓冲区基址、rq.offset和index << rq.wqe_shift定位;每接受一个 WR 后按队列掩码推进索引。mlx5_wq_overflow()必要时在 CQ 锁下复核 tail,SGE 数量还受rq.qp_state_max_gs限制,失败项通过bad_wr返回。 - data segment 保存 HCA 写入缓冲区所需的长度、key 和地址。
set_data_ptr_seg()将非零长度 SGE 的byte_count、lkey和addr编成大端设备字段;零长度 SGE 被跳过。实际段数少于rq.max_gs时,mlx5_post_recv()直接写入零长度、MLX5_INVALID_LKEY和零地址的结束哨兵。 - 可选 signature 覆盖描述符与队列身份。 启用
wq_sig时先清零槽位并保留 signature segment,完成数据段后由set_sig_seg()组合 WQE 范围、QPN 和传入索引的校验值。其calc_sig()实现是逐字节异或后取反;签名用于设备格式校验,独立于应用请求标识。
- 定位槽位时同时考虑环形索引和 WQE stride。 首个索引由
软件保存接收请求的
wr_id,供完成时交还应用。 provider 把它写入rq.wrid[]。CQ 轮询根据 CQE 的 WQE counter 找回该值并推进 RQ tail;硬件 RQ WQE 只保存设备所需的描述符。Receive DBR 在 WQE 完整写入后发布新进度。 成功编码至少一个 WR 后,provider 推进
rq.head,通过 DMA 写屏障和 DBR 完成交接。- 屏障保证 producer index 对应的描述符先对设备可见。
udma_to_device_barrier()在 x86_64 使用编译器屏障,在 ARM64 使用DMB OSHST;其适用内存类型与发送路径相同。随后将rq.head & 0xffff转为大端,写入主机内存中的MLX5_RCV_DBR。 - 普通 RC RQ 以 DBR 结束发布。 HCA 在入站处理时依据 RQ 进度取得 WQE,该路径不调用
mmio_wc_start()、mmio_wc_spinlock()或mmio_flush_writes()。共享实现中的 Raw Packet/underlay QP 在尚未到 RTR 时暂缓写 DBR,这项状态条件不改变普通 RC 主线。
- 屏障保证 producer index 对应的描述符先对设备可见。
HCA 根据可用的 RQ WQE 处理接收请求。 对 SEND,设备取得并消费 WQE、检查 MKey、DMA 写入接收 payload,然后在满足完成条件后写 receive CQE;数据写入与完成发布具有先后关系。RQ 为空时,RC 返回 RNR NAK,重试行为由 QP 参数控制;WRITE with immediate 的数据目的地差异在 4.2 节说明。
4.2 接收数据
SEND 的 payload 写入 RQ WQE 指定的 SGE,完成 WC 的byte_len表示实际接收长度。WRITE with immediate 也消耗一个 RQ WQE,业务数据写入发送者给出的remote_addr/rkey;RQ 在该操作中提供通知所需的资源,receive WC 通过IBV_WC_WITH_IMM和imm_data交付 immediate value。普通 WRITE、READ 和 Atomic 直接以远端 MR 为操作对象。
RC 的 RNR 表示接收端暂时缺少可用 RQ WQE。发送端按 QP 的 RNR 重试参数再次发送;重试耗尽时,发起端得到IBV_WC_RNR_RETRY_EXC_ERR。接收端在消费 receive WC 后及时补充 RQ WQE,可以持续提供消息接收资源。
4.3 缓冲区所有权
ibv_post_recv()成功后,RQ WQE 引用的缓冲区进入设备所有期。HCA 完成 DMA 写并产生 CQE 后,ibv_poll_cq()解析 responder CQE、恢复wr_id、推进 RQ tail,receive WC 随即把缓冲区所有权交还应用,供应用处理和复用。
RECV 使用注册内存 SGE 为 HCA 提供可写缓冲区,每个非空 SGE 都引用具备相应访问条件的 MR。mlx5 同时接受num_sge == 0的接收请求,此时 WQE 不包含有效数据段,并写入零长度、MLX5_INVALID_LKEY和零地址的终止项,表示接收缓冲区容量为零。该请求仍占用 RQ 槽位,软件保留其wr_id;终止项标记数据段结束,不要求 HCA 访问无效 MR。接收 WR 链也可能部分提交:bad_wr指向第一个尚未发布的请求项,之前的 RQ WQE 已经生效,调用者从bad_wr开始处理剩余请求。
零长度接收 WQE 的使用场景
为 WRITE with immediate 提供接收完成资源。 该操作会消费接收端的一个 RQ WQE,但业务数据直接写入发送端指定的 remote_addr/rkey,不需要接收 WQE 提供 payload 缓冲区。应用通过接收完成 WC 获取 wr_id、IBV_WC_RECV_RDMA_WITH_IMM 和 imm_data。WRITE 的业务数据长度可以非零。
接收零 payload 的 SEND 通知或握手消息。 零长度 SEND 和零 payload 的 SEND with immediate 均可消费该 WQE 并产生接收完成。对于 SEND with immediate,应用可通过 WC 获取 imm_data,无需为 immediate data 提供 SGE 缓冲区。
提供一个容量为零的有效接收请求。 该 WQE 已存在于 RQ 中,与 RQ 为空不同。它不能用于丢弃普通 SEND 携带的非零 payload;此类消息因超出接收缓冲区容量而产生长度错误。WQE 被消费后,若还需接收后续消息,应用应按协议及时提交新的 Receive WQE;新 WQE 仅用于匹配后续消息,不会接收或恢复此前已经发生长度错误的消息。
5. 完成事件
下图展示 HCA、内核和应用之间的 CQ 通知交付,以及应用轮询 CQ 获取 WC 的流程。
应用先建立 CQ 通知,再等待完成事件。 通知配置由用户态 provider 直接提交,等待事件则通过 channel fd 进入内核。
- 发布通知条件。
ibv_req_notify_cq()经mlx5_arm_cq()更新主机 arm DBR,再向已映射的 UAR 写入 doorbell。 - 读取完成事件。
ibv_get_cq_event()调用read(channel->fd),经ib_uverbs_comp_event_read()到达ib_uverbs_event_read();事件队列为空时,wait_event_interruptible()使读取线程在ev_queue->poll_wait上等待,已有事件时可直接返回。这里的 fd 属于 completion channel,由 uverbs 提供文件操作,并非 Linuxeventfd对象。
- 发布通知条件。
HCA 发布请求结果与完成通知。 HCA 先通过 DMA 向 CQ Buffer 写入 CQE,在 CQ 已 armed 且通知条件满足时向关联 EQ Buffer 写入携带 CQN 的 EQE,再在 EQ 通知条件满足时触发 MSI-X。CQE 保存请求结果,EQE 标识需要处理的 CQ;一次 CQ 通知可以对应多个 CQE。
内核通过 EQ 找到 CQ,再经 CQ 回调找到应用使用的 channel。 IRQ 处理与延后的 tasklet 处理分属两个执行阶段。
- IRQ 与 EQ 的关联来自预先建立的 notifier 绑定。
irq_int_handler()调用 IRQ notifier chain,mlx5_eq_comp_int()从 notifier 对象取得绑定的 completion EQ;next_eqe_sw()检查 EQE owner,随后执行dma_rmb(),再按 EQE.CQN 经mlx5_eq_cq_get()查找 CQ。用户 CQ 通过mlx5_add_cq_to_tasklet()入队,并按待处理列表状态调用tasklet_schedule()请求调度;IRQ 路径随后通过eq_update_ci()更新 EQ consumer index 并重新 arm EQ。该绑定不要求在所有配置下 MSI-X vector 与 EQ 都一一对应。 - tasklet 把 CQ 通知转换为 channel 中可读取的事件。
mlx5_cq_tasklet_cb()经mlx5_ib_cq_comp()调用ib_uverbs_comp_handler(),后者通过内核 CQ 回调的cq_context取得 channel 的ev_queue,将用户 CQ handle 加入event_list,再调用wake_up_interruptible()唤醒poll_wait上的等待线程。
- IRQ 与 EQ 的关联来自预先建立的 notifier 绑定。
获取产生完成事件的 CQ。 线程被唤醒后调用
ib_uverbs_event_read()读取事件,同时增加已报告事件计数,最后获取 CQ handle。libibverbs 根据该 handle 找到 CQ 及其应用 context,调用mlx5_cq_event()推进用户态 arm 序列号,再将 CQ 和 context 返回给应用。ibv_ack_cq_events()累加已确认事件计数,用于 CQ 生命周期管理;CQE 和 EQE 的消费进度则通过各自的 consumer 体现。应用在读取 CQ 前重新 arm,使下一轮等待具备通知条件。 应用依次执行 ack、
ibv_req_notify_cq()和循环ibv_poll_cq(),直到 poll 返回 0。- 完成事件读取与资源回收。 provider 检查 CQE owner,确认可消费后执行
udma_from_device_barrier()再读取内容;mlx5_parse_cqe()解析 WC 并推进关联队列的回收进度,update_cons_index()更新主机内存中的 CQ consumer DBR。这段路径直接访问用户映射的 CQ Buffer,无需进入 uverbs。 - 获取请求完成结果。 循环轮询 CQ,provider 解析 CQE 并返回 WC;channel 事件只提示检查 CQ。
- 完成事件读取与资源回收。 provider 检查 CQE owner,确认可消费后执行
5.1 创建完成通道
ibv_create_comp_channel()为指定 context 创建接收 CQ 通知的 completion channel。
channel fd。
context->cmd_fd对应打开 context 时已打开的/dev/infiniband/uverbsN;ioctl-write 使用RDMA_VERBS_IOCTL,通过UVERBS_METHOD_INVOKE_WRITE携带IB_USER_VERBS_CMD_CREATE_COMP_CHANNEL。legacy write 与 ioctl-write 共用同一创建路径,返回的channel->fd与命令 fd 分开。独立内核事件对象、匿名文件和事件队列。 内核对象属于创建它的 context;匿名文件名为
[infinibandevent],访问模式为O_RDONLY,fd 带有O_CLOEXEC标志。事件队列保存关联 CQ 的通知,等待队列支持应用通过read()、poll()等接口读取和轮询事件。channel 是 uverbs 软件资源,创建它无需新建 HCA EQ。预留和安装 fd。 预留 fd、初始化对象以及向用户态复制 fd 编号;创建成功后,框架通过
fd_install()建立进程 fd 到匿名文件的映射,提交完成后 ioctl 才成功返回。文件的private_data关联内核事件对象,供后续事件访问使用。同一 context 可创建多个独立的 channel,每个 CQ 最多关联其中一个。 多个 CQ 可以共享同一 channel,应用通过 CQ 创建时的关联关系决定通知的接收通道。
- 创建成功后会得到一个 channel 和一个新 fd。 各 channel 拥有独立的事件队列;
dup()只复制 fd 引用,仍共享原文件和事件队列。 - channel 数量取决于可用资源。 context 创建的 channel 数量通常没有上限,实际受进程
RLIMIT_NOFILE、系统文件资源和内存限制。 - 完成事件只投递到 CQ 关联的 channel。
ibv_create_cq()的channel参数指定 CQ 关联的 channel。CQ 已 arm 且满足通知条件时,内核将通知放入该 channel 的事件队列,不会向同一 context 的其他 channel 广播或自动分流。若创建 CQ 时传入NULL,该 CQ 不通过 channel 交付通知,应用直接轮询 CQ 获取 WC。 - 销毁 channel 前需释放关联 CQ 的引用。
refcnt记录关联 CQ 的数量,应用应先销毁 CQ,再销毁 channel。事件读取见 5.2 节,请求结果的获取见 5.4 节。
- 创建成功后会得到一个 channel 和一个新 fd。 各 channel 拥有独立的事件队列;
5.2 读取完成事件
ibv_get_cq_event()读取 channel,成功后返回有完成事件的 CQ 及其应用 context,具体请求完成结果仍由 CQ 轮询获取。
- 阻塞属性。 阻塞模式下等待事件;非阻塞模式下通过
errno报告EAGAIN。应用也可先用poll()或epoll()等待 fd 可读。 - 获取产生完成事件的 CQ。 返回的 CQ 指针和
cq_context标识通知对象及应用私有上下文。 - 用户态序列号。 mlx5 provider 的
arm_sn随完成事件读取递增,其低两位参与后续 CQ 通知配置,见 5.3 节。
5.3 配置 CQ 通知
ibv_req_notify_cq()配置一次性 CQ 完成通知。
doorbell 编码。
mlx5_arm_cq()取arm_sn低两位和 consumer index 低 24 位,并按solicited_only选择 solicited 或 all-event 命令;高 32 位组成 arm word,低 32 位保存 CQN。最终组成 64 位 doorbell。DBR。
MLX5_CQ_ARM_DB写入大端格式的 arm word,记录本次 CQ arm 的序列号、消费进度和通知模式。保证 DBR 先于 UAR 写入。 该写前屏障展开为
mmio_flush_writes():x86_64 执行SFENCE,ARM64 执行DSB ST。更新 UAR CQ doorbell。
mmio_write64_be()向ctx->cq_uar_reg + MLX5_CQ_DOORBELL写入大端格式的 doorbell,底层atomic_store_explicit()使用memory_order_relaxed。- x86_64 用 64-bit
MOV写入。 指令写出已编码的 doorbell 位模式。 - ARM64 用
STR Xt, [Xn]写入。 指令写出同一位模式。
- x86_64 用 64-bit
刷出 UAR CQ doorbell。 x86_64 执行
SFENCE,ARM64 执行DSB ST。UAR 写入后执行写屏障;当该 UAR 映射为 WC 时,还会促使 WC 写缓冲中的内容刷新出去。
5.4 轮询 CQ
ibv_poll_cq()从用户映射的 CQ 内存取得 WC,并回收关联 SQ、RQ 和 CQ 的槽位。
查找有效 CQE。
- 判定 CQE。 opcode 必须不是
MLX5_CQE_INVALID,owner 必须与当前环形队列轮次匹配,避免重复消费旧 CQE。 - DMA 屏障。
udma_from_device_barrier()在 x86_64 使用LFENCE,在 ARM64 使用DMB OSHLD,确保判定 CQE opcode 和 owner 的操作完成后,才能读取 CQE 内容。
- 判定 CQE。 opcode 必须不是
解析 CQE 并回收队列。 CQE 中的资源标识和 WQE counter 定位请求,provider 从
wrid[]取回应用的wr_id;不同类型决定返回字段及回收对象。- request CQE 回收 SQ。 WC 保留原请求的
wr_id和操作类型;sq.wqe_head[]保存的 SQ 请求序号用于推进 tail。READ/Atomic 还提供结果长度,inline-scatter 数据需复制到请求的本地缓冲区。 - responder CQE 交还接收缓冲区。 普通 RC RQ 完成包含
wr_id、长度、receive opcode 及 immediate/invalidate 信息,并推进 RQ tail。存在 inline-scatter 时,CPU 按接收数据段容量回填 payload,完成复制后才交付 WC;容量不足时报告长度错误。 - error CQE 报错并回收请求。 WC 保存映射后的
status和vendor_err,SQ 或 RQ 的消费进度仍会推进。错误字段的有效范围与后续处理见 5.6 节。
- request CQE 回收 SQ。 WC 保留原请求的
consumer DBR 更新 CQ 消费进度。
MLX5_CQ_SET_CI保存大端格式的 consumer index 低 24 位,向设备归还已消费槽位;completion-channel 事件确认使用独立的软件计数。轮询返回本次取得的 WC 数量或轮询错误。
5.5 通知竞态
可靠的事件循环采用“首次等待前 arm,事件返回后确认并重新 arm,最后排空 CQ”的顺序。重新 arm 位于最终空轮询之前,使这个窗口内到达的完成由本次轮询消费或触发下一次事件。
重新 arm 与排空 CQ 之间到达的 CQE 可能同时被本次轮询看到并产生下一次 event,因此后续 event 读取成功时,CQ 可能已经被前一次循环排空,ibv_poll_cq()返回 0 属于正常结果。一个 event 也可能覆盖多个 CQE,应用以 event 作为唤醒提示,以 WC 作为请求完成的处理单位。
solicited_only非零时,ibv_req_notify_cq()仅对带 Solicited 标记的接收完成或错误完成请求通知,其他 CQE 仍按正常规则产生。应用可用poll()或epoll()等待 channel fd;fd 可读表示ev_queue中存在 CQ 事件,实际请求结果由 CQ 轮询取得。
5.6 错误完成
错误可能发生在发布阶段或执行阶段。发布阶段的容量、参数和 opcode 检查通过 API 返回值与bad_wr报告;执行阶段的本地保护错误、远端访问错误、重试耗尽等通过 WCstatus和vendor_err报告。
严重错误可能使 QP 进入 error 状态。尚未完成的 WR 随后以IBV_WC_WR_FLUSH_ERR返回;flush completion 表示该请求已经终止,相关软件资源进入错误回收路径。处理第一个错误时,应用先保留 QP、CQ 和 MR,排空所有可能的 completion 并停止提交请求,再按对象依赖顺序销毁。
CQ overrun 是另一类资源错误:HCA 产生 CQE 的速度超过软件处理速度时,完成信息可能丢失,关联 CQ/QP 进入错误状态。应用通过合适的 CQ 容量、消费速率和 signaling 频率控制 CQ 压力,并周期性提交 signaled WR 形成回收进度和错误检查点。
6. 总结
- 用户态收发数据与轮询 CQ。 mlx5 provider 在用户态 QP/CQ 队列内存上编码 WQE、发布门铃并读取 CQE,HCA 负责发送和接收数据;内核负责管理资源,以及通过 EQ、中断和 completion channel 交付完成事件。
- 提交 WQE。 provider 通过屏障、DBR 及发送门铃控制 WQE 的发布顺序。
- 操作类型决定数据落点与 RQ 用途。 SEND 消耗远端 RQ WQE 并写入其缓冲区;WRITE、READ 和 Atomic 直接访问远端 MR。WRITE with immediate 的数据写入远端 MR,同时消耗 RQ WQE 以产生接收完成事件;零长度接收 WQE 不需要提供 payload 缓冲区。
- CQE 提供完成信息。 provider 根据 CQE 找到请求,利用软件保存的
wr_id恢复请求,并推进 SQ/RQ/CQ 的消费进度。WC 的status报告执行结果。 - 完成事件通知。 CQ 通知采用一次性 arm;事件模式通过 completion channel 唤醒应用,应用确认事件、重新 arm 并读取所有 CQE。一个事件可能对应多个 CQE,请求完成结果以轮询得到的 WC 为准。
7. 参考资料
7.1 源码位置
| 层与功能 | 主要位置 |
|---|---|
| 用户态示例 | rdma-core/libibverbs/examples/rc_pingpong.c |
| verbs WR、WC 与公开入口 | rdma-core/libibverbs/verbs.h、rdma-core/libibverbs/verbs.c |
| mlx5 provider 操作表 | rdma-core/providers/mlx5/mlx5.c |
| 用户态 SQ/RQ 提交与门铃 | rdma-core/providers/mlx5/qp.c、rdma-core/providers/mlx5/wqe.h |
| 用户态 DMA 与 WC MMIO 屏障 | rdma-core/util/udma_barrier.h、rdma-core/util/mmio.h |
| completion channel 创建与事件读取 | rdma-core/libibverbs/verbs.c、cmd_write.h、cmd_fallback.c、cmd_ioctl.c |
| 用户态 CQ 轮询与 arm | rdma-core/providers/mlx5/cq.c |
| mlx5_ib 用户 CQ 完成回调 | linux/drivers/infiniband/hw/mlx5/cq.c |
| 内核 completion channel 创建与 fd 提交 | linux/drivers/infiniband/core/uverbs_cmd.c、uverbs_ioctl.c、uverbs_std_types_device.c、uverbs_std_types.c、rdma_core.c;linux/include/rdma/uverbs_std_types.h |
| uverbs completion event | linux/drivers/infiniband/core/uverbs_main.c |
| 进程 fd 分配与安装 | linux/fs/file.c |
| mlx5 completion EQ 与 CQ 分发 | linux/drivers/net/ethernet/mellanox/mlx5/core/eq.c、linux/drivers/net/ethernet/mellanox/mlx5/core/cq.c |
| 内核 EQ doorbell 与架构屏障 | linux/drivers/net/ethernet/mellanox/mlx5/core/lib/eq.h、linux/arch/x86/include/asm/barrier.h、linux/arch/arm64/include/asm/barrier.h |
| mlx5 设备格式与门铃 | linux/include/linux/mlx5/device.h、linux/include/linux/mlx5/doorbell.h |
7.2 接口与规范
libibverbs,
ibv_post_send(3)、ibv_post_recv(3):rdma-core/libibverbs/man/ibv_post_send.3、rdma-core/libibverbs/man/ibv_post_recv.3,说明 WR 链、bad_wr、inline 与缓冲区所有权。libibverbs,
ibv_poll_cq(3):rdma-core/libibverbs/man/ibv_poll_cq.3,说明 WC 字段、状态与轮询返回值。libibverbs,
ibv_req_notify_cq(3)、ibv_get_cq_event(3):rdma-core/libibverbs/man/ibv_req_notify_cq.3.md、rdma-core/libibverbs/man/ibv_get_cq_event.3,说明 CQ arm、事件确认及额外事件竞态。libibverbs,
ibv_create_comp_channel(3)、ibv_destroy_comp_channel(3):rdma-core/libibverbs/man/ibv_create_comp_channel.3,说明 completion channel 的创建、返回值和销毁条件。Linux Kernel Documentation,Userspace verbs access:linux/Documentation/infiniband/user_verbs.rst,说明用户态 verbs、内核资源和直接数据路径的边界。
Mellanox Technologies,Mellanox Adapters Programmer’s Reference Manual (PRM), Rev 0.53:rdma-core/docs/analysis/rdma-stack/Mellanox Adapters Programmer’s Reference Manual Rev 0.53.pdf,第 2.4 节与第 8 章说明 HCA operation、WQ/WQE、CQ/CQE、EQ 及可靠传输完成语义。
University of Cambridge,C/C++11 mappings to processors:提供 relaxed atomic store 到 x86_64
MOV与 AArch64STR的指令映射参考;MMIO 顺序另依据本地访问器、屏障实现及设备接口语义。