内核态关键数据结构:QP、CQ、MR、PD、AH 及其管理

RDMA 内核态数据通路的性能上限,很大程度上取决于核心对象在内核中的组织方式与生命周期管理。与用户态通过 verbs API 操作抽象句柄不同,内核态驱动直接面对 struct ib_qp、struct ib_cq、struct ib_mr、struct ib_pd、struct ib_ah 等真实结构体。理解这些结构体的字段布局、相互引用关系以及内核中的注册/注销流程,是优化数据通路的前提。

核心原理:对象层次与所有权模型

RDMA 内核对象并非彼此孤立,而是存在严格的层次归属关系。最顶层是 设备(struct ib_device),每个设备维护一个保护域(PD)列表。PD 是资源的“容器”与隔离边界:QP、MR、AH 都必须归属于某个 PD,且只有同属一个 PD 的 QP 与 MR 才能用于内存注册和收发操作。CQ 虽然不完全受 PD 约束,但在语义上通常与 QP 的完成队列绑定。

从数据通路视角看,一次发送操作涉及的对象链为:QP → WR → MR(内存描述)→ AH(路径描述)→ CQ(完成通知)。内核驱动在 post_send 时,需要快速解析这些对象之间的指针关系,避免多次间接查找。因此,结构体设计上大量使用直接指针引用而非句柄查找。

关键设计原则:内核态对象通过 ib_alloc_pd()、ib_create_qp() 等接口创建后,返回的是可直接解引用的内核指针,而非用户态那种需要 idr 映射的用户句柄。这种设计减少了每次操作时的 ID 转换开销,但要求驱动开发者严格管理对象生命周期,防止悬垂指针。
struct ib_device(设备全局) 维护 PD 列表、设备能力、操作函数集 struct ib_pd(保护域) local_dma_lkey / unsafe_global_rkey device->list struct ib_qp(队列对) qp_state RESET→INIT→RTR→RTS send_cq / recv_cq 指向 CQ 的指针 pd 所属保护域 qp_num / qp_type RC/UC/UD 标识 qp->pd struct ib_cq(完成队列) cqe 环形缓冲 深度可配置 notify 机制 中断/轮询模式 comp_handler(回调函数) 完成事件上报入口 qp->send_cq struct ib_mr(内存区域) iova / length IOVA 地址与长度 rkey / lkey 远端/本地访问密钥 dma 映射(sg_list) 物理页散列表 mr->pd(同一 PD 内共享) struct ib_ah(地址句柄) dgram sgid_index / dlid av(地址向量) 端口/流控/速率 ah->pd ⚠ 生命周期管理核心:所有对象必须在其 PD 销毁前释放;CQ 可在 QP 销毁后独立存在,但 QP 不可引用已销毁的 CQ

方法流程:对象创建与注册路径

以内核态创建 QP 为例,典型调用链如下:

// 1. 分配保护域
struct ib_pd *pd = ib_alloc_pd(device, 0);

// 2. 创建完成队列(发送/接收各一)
struct ib_cq *send_cq = ib_create_cq(device, comp_handler, NULL, NULL, cq_depth, 0);
struct ib_cq *recv_cq = ib_create_cq(device, comp_handler, NULL, NULL, cq_depth, 0);

// 3. 填充 QP 初始化属性
struct ib_qp_init_attr init_attr = {
    .send_cq = send_cq,
    .recv_cq = recv_cq,
    .cap = { .max_send_wr = 256, .max_recv_wr = 256,
             .max_send_sge = 4, .max_recv_sge = 4 },
    .qp_type = IB_QPT_RC,
    .sq_sig_type = IB_SIGNAL_ALL_WR,
};

// 4. 创建 QP(内部会调用 device->create_qp 回调)
struct ib_qp *qp = ib_create_qp(pd, &init_attr);

// 5. 注册内存区域
struct ib_mr *mr = ib_reg_user_mr(pd, start_addr, length, access_flags);

// 6. 对于 UD 服务类型,还需创建 AH
struct rdma_ah_attr ah_attr = { .dlid = remote_lid, .port_num = 1 };
struct ib_ah *ah = rdma_create_ah(pd, &ah_attr, NULL);

内核驱动在 ib_create_qp() 内部会执行以下关键步骤:

  1. 校验参数:检查 PD 是否有效、CQ 是否属于同一设备、QP 类型是否被设备支持。
  2. 调用硬件驱动回调:通过 device->create_qp() 让具体硬件驱动分配硬件队列资源。
  3. 建立内核对象关联:将 QP 加入 PD 的 qp_list,设置 qp->pd、qp->send_cq 等指针。
  4. 初始化状态机:将 QP 状态置为 IB_QPS_RESET,等待上层调用 ib_modify_qp() 迁移到 INIT→RTR→RTS。

案例场景:多 QP 共享 CQ 的典型配置

在 NVMe-oF 卸载场景中,通常一个完成队列会被多个 QP 共享,以减少中断数量和上下文切换。例如,一个 4 核 CPU 的服务器上,每个核创建一个 CQ,但每个核上运行 8 个 QP,所有 QP 的 send_cq 和 recv_cq 都指向本核的 CQ。这种设计下,ib_poll_cq() 可以一次性收集多个 QP 的完成事件,提高轮询效率。

对象 数量 归属关系 内核管理要点
PD 1 设备级 所有对象共享,提供地址保护域
QP 32 PD 内 每个 QP 独立状态机,需跟踪 qp_state
CQ 4 设备级 每核一个,被 8 个 QP 共享
MR 64 PD 内 每个 MR 维护 dma_map 和 rkey 分配
AH 8 PD 内 UD 服务类型使用,缓存路径信息

这种共享 CQ 的模式下,内核驱动必须保证 qp->send_cq 的并发安全。当多个 QP 同时完成操作时,CQ 的环形缓冲区写入需要加锁或使用无锁 CAS 操作。在实现层面,struct ib_cq 内部包含一个 lock 字段用于保护 cqe 的读写指针。

误区与复盘:对象生命周期管理的常见陷阱

典型误区 1:QP 销毁顺序错误。 某些驱动在销毁 QP 时,先释放了 CQ,导致 QP 的完成回调访问悬垂指针。正确顺序应为:先将 QP 迁移到 RESET 状态 → 销毁 QP → 最后销毁 CQ。内核中 ib_destroy_qp() 会检查是否有未完成的 WR,但不会检查 CQ 是否仍然有效。

典型误区 2:MR 的 lkey/rkey 生命周期管理。 在数据通路中,lkey 会被填充到发送 WR 的 sge 中。如果 MR 被提前注销,而硬件队列中仍有引用该 lkey 的 WR 未完成,将导致 DMA 访问非法地址。内核驱动在 ib_dereg_mr() 时,必须确保所有依赖该 MR 的 QP 已经完成相关操作,或者通过硬件同步机制等待 DMA 停止。

复盘建议:在实现内核模块时,建议使用引用计数(kref)管理 QP 和 MR。当 post_send 提交 WR 时,对 MR 增加引用计数;当 CQ 返回完成事件时,减少引用计数。只有当引用计数归零时,才允许真正释放内存。这种机制能有效避免 UAF(Use-After-Free)问题。

性能优化提示:在数据热路径上,应避免频繁调用 ib_reg_user_mr()。建议在模块初始化阶段预先注册大块内存并缓存 lkey/rkey,后续通过 ib_map_mr_sg() 或 ib_update_fast_reg_key() 复用 MR。内核态下,每次 MR 注册涉及 DMA 映射和页表锁定,开销可达数十微秒,而缓存命中后仅需纳秒级操作。

最后需要强调的是,内核态对象管理必须与硬件驱动的能力边界对齐。例如,某些网卡要求 QP 的 max_send_sge 不超过 4,而另一些则支持 16 个 SGE。在创建 QP 时,驱动应通过 ib_query_device() 获取设备能力,避免盲目使用固定值导致硬件拒绝。同时,AH 的创建在 RoCEv2 环境下需要额外指定 sgid_index 和 hop_limit,这些参数错误会导致数据包无法路由到对端。