RDMA 关键技术研究:Queue Pairs

最后更新: 2026-04-08, 作者: HAO022

名词解释

  • PD Protection Domain 保护域, 在保护域中创建一组协同工作的对象(如QP, SQ …)。
  • QP Queue Pair 队列对
  • SQ Send Queue 发送队列
  • RQ Receive Queue 接收队列
  • CQ Completion Queue 完成队列
  • WQE Work Queue Entries, 向 RNIC 提交的 Work Requests (描述具体的发送或者接收信息)。

用户接口

用户态接口实现 libibverbs.so https://github.com/linux-rdma/rdma-core

  • ibv_get_device_list() 返回一组可用的 RDMA 设备列表
  • ibv_open_device() 打开设备创建设备上下文
  • ibv_alloc_pd() 根据设备上下文创建保护域
  • ibv_reg_mr() 在保护域中注册内存区域
  • ibv_create_cq 根据设备上下文创建完成队列
  • ibv_create_qp() 根据保护域和完成队列等参数创建 QP,接收队列,发送队列
  • ibv_post_recv() 向接收队列提交工作请求
  • ibv_post_send() 向发送队列提交工作请求
  • ibv_poll_cq() 轮训完成队列

QPs

QP 队列对,是数据交换的核心,包含发送队列 SQ,接收队列 RQ。那么在创建过程中用户、内核、硬件是如何设计的?

libverbs API:

struct ibv_qp *ibv_create_qp(struct ibv_pd *pd, struct ibv_qp_init_attr *qp_init_attr);

struct ibv_qp_init_attr {
	void *qp_context;
	struct ibv_cq *send_cq;
	struct ibv_cq *recv_cq;
	struct ibv_srq *srq;
	struct ibv_qp_cap cap;
	enum ibv_qp_type qp_type;
	int sq_sig_all;
};

struct ibv_qp_cap {
	uint32_t max_send_wr;
	uint32_t max_recv_wr;
	uint32_t max_send_sge;
	uint32_t max_recv_sge;
	uint32_t max_inline_data;
};

ibv_qp_init_attr 用于描述 QP 基础属性的结构体:

名称 描述
send_cq 与发送队列关联的完成队列 (通过 ibv_create_cq 创建,send_cq 和recv_cq 可以使用相同或者不同的 cq)
recv_cq 与接收队列关联的完成队列
cap 描述Queue Pair 大小等属性
qp_type - IBV_QPT_RC 面向连接的可靠传输
- IBV_QPT_UC 面向连接的不可靠传输
- IBV_QPT_UD 报文不可靠传输
- IBV_QPT_RAW_PACKET 允许用户自定义数据报文,包含L2

ibv_qp_cap 用于描述 QP 大小属性的结构体:

名称 描述
max_send_wr 发送队列可容纳的最大Work Requests 数量,可设置为 dev_cap.max_qp_wr
max_recv_wr 接收队列可容纳的最大Work Requests 数量,可设置为 dev_cap.max_qp_wr
max_send_sge 向发送队列提交的 Work Request 中最大分散聚合项的数量
max_recv_sge 向接收队列提交的 Work Request 中最大分散聚合项的数量

用户态实现:

ibv_create_qp verbs_context_ops.create_qp()

providers/mlx5/mlx5.c
struct verbs_context_ops mlx5_ctx_common_ops = { 非常庞大的函数集合
	.alloc_pd      = mlx5_alloc_pd,
	.create_qp     = mlx5_create_qp,
	.create_qp_ex  = mlx5_create_qp_ex, mlx5_create_qp 基础上可配置更多,更高级的参数
	...
}

mlx5_create_qp
	1. mlx5_calc_wq_size 根据配置计算SQRQ Work Queue WQE 需要的内存大小
		1. mlx5_calc_send_wqe
		2. mlx5_calc_rq_size
	2. mlx5_alloc_qp_buf 根据计算结果分配内存 wq_size
	3. mlx5_alloc_dbrec 分配 doorbell 内存资源
	4. ibv_cmd_create_qp_ex 和内核交互
		1. cmd.buf_addr ...
		2. cmd.db_addr ...
	5. 更新 attr 属性
mlx5_calc_sq_size
	1. mlx5_calc_send_wqe sq_overhead 根据 QP 类型选择控制结构体 + max_send_sge * mlx5_wqe_data_seg
	2. wq_size = roundup_pow_of_two(attr->cap.max_send_wr * wqe_size);
	3. qp->sq.wqe_cnt = wq_size / MLX5_SEND_WQE_BB;
	4. qp->sq.wqe_shift = ...;
	5. qp->sq.max_gs = cap.max_send_sge;
	6. qp->sq.max_post = 约等于 cap.max_send_wr,但需考虑对齐;

mlx5_calc_rq_size
	1. mlx5_calc_rcv_wqe
	2. qp->rq.wqe_cnt = ...;
	3. qp->rq.wqe_shift = ...;
	4. qp->rq.max_post = ...;

内核态实现: 内核并没有复杂的逻辑实现,直接调用驱动 ops.create_qp

struct ib_device_ops mlx5_ib_dev_ops = {
	.create_qp = mlx5_ib_create_qp, // ... create_user_qp
	...
}

create_user_qp
	1. ib_umem_get 对用户态虚拟地址进行:分配内存,锁页,dma 地址映射等
	2. mlx5_ib_populate_pas 填充 WQE MTT 表项
	3. mlx5_ib_db_map_user
		1. kmalloc 分配一个物理页
		2. 该物理页关联 db 虚拟地址
		3. ib_umem_get 对当前物理页锁页,dma 映射等
	4. mlx5_qpc_create_qp 构建 QPC Queue Pair Context 向硬件提交创建命令
		1. qpc.log_page_size
		2. qpc.page_offset
		3. qpc.log_sq_size
		4. qpc.pd
		5. qpc.uar_page
		6. qpc.dbr_addr

最终内存关系如图:

Posting Send

mlx5_post_send
        for (nreq = 0; wr; ++nreq, wr = wr->next) {
                // 1. 根据 wr 配置控制结构体 struct mlx5_wqe_ctrl_seg *;
                // 2. 根据 wr.sg_list[i] 配置数据结构体 struct mlx5_wqe_data_seg *;
                // mlx5 控制/数据结构体数据已经映射到 MTT
        }
        post_send_db(qp, ..., ctrl); // 门铃操作

思考

  • WQE Buf 最终会关联到 QP, 因此在逻辑上可以看做 QP 的一部分。
  • MTT 不仅仅在 Memory Region 注册过程中使用,在创建 QP 也有使用,主要作用是将为门铃功能传递过来的 WQE 虚拟地址转换为硬件可以访问的 IOVA/PA。这种方式避免了用户态和内核态上下文切换过程中拷贝 WQE,提升性能。

篇尾:

  • HUATUO 华佗是由滴滴开源并依托 CCF 孵化的操作系统深度观测项目。
  • 关注微信公众号,或扫码加微信,邀请你加入用户群(请备注姓名+单位):

微信