最后更新: 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。那么在创建过程中用户、内核、硬件是如何设计的?
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 根据配置计算SQ、RQ 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 孵化的操作系统深度观测项目。
- 关注微信公众号,或扫码加微信,邀请你加入用户群(请备注姓名+单位):
