RDMA技术浅析(四)
一、关于wq
今天聊聊rdma的重要的wq的概念。
在 RDMA(Remote Direct Memory Access)和 Mellanox MLX5 网卡驱动架构中,WQ 是 Work Queue(工作队列)的缩写。
它是软件(应用程序或驱动)与硬件网卡之间进行任务交互的核心数据结构。简单来说,WQ 是存放“待执行任务指令”的地方,网卡硬件会从这个队列中读取指令并执行相应的数据收发操作。
以下是关于 WQ 的详细解析:
1.1. WQ 的核心组成
一个完整的通信通道通常由一对 WQ组成,称为 QP(Queue Pair,队列对):
- SQ (Send Queue,发送队列):
- 作用:存储发送任务的指令。
- 生产者:软件(应用通过 Verbs API 提交 Send/Write/Read 请求)。
- 消费者:网卡硬件(从 SQ 中取出指令,构建数据包并发送)。
- RQ (Receive Queue,接收队列):
- 作用:存储接收缓冲区的描述信息,告诉网卡收到数据后存放到内存的哪个位置。
- 生产者:软件(应用预先 Post Receive Buffer,即“预留收货地址”)。
- 消费者:网卡硬件(收到数据后,根据 RQ 中的描述将数据 DMA 写入指定内存)。
注意:还有一种特殊的 SRQ (Shared Receive Queue,共享接收队列),允许多个 QP 共享同一个接收队列,以节省内存资源,常用于大规模连接场景。
1.2. WQ 的基本单元:WQE
WQ 是一个环形缓冲区(Ring Buffer),其中包含一个个最小的执行单元,称为 WQE (Work Queue Element,工作队列元素)。
- WQE 是什么:它是软件向硬件提交的一个“任务单”。
- 内容包含:
- 控制信息:操作类型(如 SEND, WRITE, READ)、消息长度、签名等。
- 数据指针:本地内存的地址(VA)、长度、LKey(内存注册密钥)。
- 传输信息:目标 QP 号、PSN(包序列号)、远程内存地址(对于 RDMA Write/Read)等。
- 对齐要求:在 MLX5 硬件中,为了优化 PCIe 传输效率,WQE 通常需要对齐到 64 字节(Cache Line 大小),不足部分会填充 Padding,这种对齐后的块有时被称为 WQEB (Work Queue Element Block)。
1.3. WQ 的工作流程(生产者-消费者模型)
WQ 机制遵循典型的异步生产者-消费者模型:
- 软件生产(Post):
- 应用程序调用
ibv_post_send或ibv_post_recv。 - 驱动将这些请求翻译成硬件能理解的 WQE 格式,写入到 WQ 的环形缓冲区中。
- 更新 WQ 的 Tail 指针(指向下一个可用位置)。
- 应用程序调用
- 通知硬件(Doorbell):
- 软件写一次“门铃寄存器”(Doorbell Register),通知网卡:“我有新任务了,快来处理”。
- 硬件消费(Fetch & Execute):
- 网卡硬件收到门铃信号后,通过 DMA 从内存中读取 WQE。
- 硬件解析 WQE,执行具体的数据搬运或发送操作。
- 更新 WQ 的 Head 指针(表示已处理到的位置)。
- 完成通知(Completion):
- 任务执行完毕后,硬件向 CQ (Completion Queue,完成队列) 写入一个 CQE (Completion Queue Element),告知软件任务已完成及结果状态。
1.4. 为什么需要 WQ?
- 解耦与异步:软件只需下发指令即可返回,无需等待数据真正发送完毕,极大提高了 CPU 效率。
- 批量处理:软件可以一次性提交多个 WQE(Batching),减少 Doorbell 触发次数,降低 PCIe 开销。
- 硬件卸载:复杂的协议封装(如 InfiniBand BTH 头、RoCEv2 UDP/IP 头)由硬件根据 WQE 中的信息自动完成,CPU 不参与数据拷贝和协议栈处理。
总结
WQ (Work Queue) 是 RDMA 通信的“任务调度中心”。
- SQ 负责“发货指令”,RQ 负责“收货地址预约”。
- WQE 是具体的“任务单”。
- 网卡硬件通过读取 WQ 中的 WQE 来执行零拷贝的数据传输,是实现高性能、低延迟网络通信的基础机制。
二、wq环形缓冲区
本质上,wq是一个环形缓冲区。
这个环形缓冲区,也就是Ring Buffer, 形式上在内存中的排布不一定是环形的,可能是线性分布的一块区域,之所以叫做环形缓冲区,是因为有头尾指针,循环累加,区别于队列或者堆栈而言的。这个东西,一般用来通信,比如Host驱动和设备端的固件需要交互消息,就可以用环形缓冲区来处理,名字千奇百怪,可能叫做Mailbox,可能叫做IPC。
一张普通网卡,常见的环形缓冲区就是数据包的环形缓冲区,通过dma_alloc_coherent申请,特点是物理地址连续,且保证缓存一致性。
rdma的wq包括sq、rq, 是存放wqe的环形缓冲区,这个东西就不适合用dma_alloc_coherent 来申请,因为比较大。所以,特点是虚拟地址连续,但是物理地址不一定连续。
下面讲讲wq的分配过程。
三、建立wq的过程
我们就以linux 7.0.0 rc2版本的代码来看看mlx5的rdma驱动程序是怎么申请一个wq的吧。
先上一个总体的调用关系表:
libmlx5 (用户态) ibv_create_qp()
│ ioctl: ucmd->buf_addr (用户WQ缓冲区虚拟地址)、ucmd->db_addr (doorbell页)
▼
mlx5_ib_create_qp() [ib/mlx5/qp.c]
│
├─► _create_user_qp() [qp.c:941]
│ │
│ ├─► ib_umem_get(dev, ucmd->buf_addr, buf_size, 0) [qp.c:1003]
│ │ └─ pin_user_pages_fast(FOLL_LONGTERM) [core/umem.c:235]
│ │ 锁定用户页 → 页的DMA地址存入 umem->sgt_append (sg_table)
│ │
│ ├─► mlx5_umem_find_best_quantized_pgoff() [qp.c:1009]
│ │ 选最大页大小 page_size,量化页内偏移 page_offset
│ │
│ ├─► ncont = ib_umem_num_dma_blocks(umem, page_size) [qp.c:1017]
│ │ 硬件需要的表项数
│ │
│ ├─► *in = kvzalloc(create_qp_in + ncont * 8) [qp.c:1022-1024]
│ │ 分配 mailbox 命令缓冲区,尾部预留 pas[] 数组
│ │
│ ├─► pas = MLX5_ADDR_OF(create_qp_in, *in, pas) [qp.c:1033]
│ │
│ ├─► mlx5_ib_populate_pas(umem, page_size, pas, 0) [qp.c:1035]
│ │ └─► rdma_umem_for_each_dma_block(umem, ...) [ib/mlx5/mem.c:45]
│ │ *pas = be64(页的DMA地址) ← 这一步就是"生成MTT表项"
│ │
│ └─ MLX5_SET(qpc, qpc, log_page_size / page_offset / [qp.c:1036-1040]
│ uar_page / dbr_addr …) QP上下文字段
│
▼
mlx5_qpc_create_qp() [ib/mlx5/qpc.c:246]
│ MLX5_SET(create_qp_in, in, opcode, MLX5_CMD_OP_CREATE_QP) [qpc.c:252]
│ 命令格式: [create_qp_in 头部][qpc][pas[0]..pas[ncont-1]]
▼
mlx5_cmd_exec(dev->mdev, in, inlen, out, outlen) [qpc.c:254]
│ → mlx5_cmd_invoke → cmd_alloc_ent
▼
写命令门铃 + DMA [eth/mlx5/core/cmd.c]
│ 命令缓冲区地址写入命令门铃寄存器 (PCIe MMIO)
│ 网卡固件 DMA 读走整个 create_qp_in(含 pas[] 页表)
▼
网卡固件 (HCA)
├─ 在 ICM 中建立 QP context
├─ 把 pas[] 复制/关联为该 QP 的 WQ 页表 (即硬件侧 MTT)
└─ 返回 create_qp_out { qpn } [qpc.c:260]
│
▼
create_resource_common() → 挂入 dev->qp_table (XArray, qpn→qp) [qpc.c:262]─── 运行时 ───
用户态投递 WQE → mmio_write64 到 uar_page 指向的 BF/doorbell (WC BAR)
硬件: QPN → QP context → WQ页表(来自pas[]) → DMA读WQE → 用lkey查MR的MTT → DMA读写数据
2.1 第 1 步:用户态传虚拟地址,内核 pin 成 umem
用户态创建qp的iverbs api是ibv_create_qp(), malloc 的缓冲区虚拟地址通过 ibv_create_qp 的 command 传下来(ucmd->buf_addr),陷入内核态后,通过ib的钩子函数,会调用mlx5_ib_create_qp(),这个函数会调用_create_user_qp函数,这里面的关键操作是ib_umem_get()
这个函数会 pin 住这些页,页列表存进 umem->sgt_append(scatter-gather table)。
2.2 页列表转成 MTT/KSM 项,写入网卡自己的页表
接下来,在 mlx5_ib_populate_pas()(mr.c)中,把 umem->sgt_append 里每个页的 DMA 地址(IOVA,经 IOMMU 翻译后的地址)填进一个连续的 PAS 数组——这就是 MTT(Memory Translation Table,每项 = 页的物理/DMA 地址)或 KSM(大页模式,描述 base+length)。
2.3 组装qpc的mailbox发给硬件
struct mlx5_ifc_create_qp_in_bits { opcode, uid, op_mod; // 命令头 input_qpn, opt_param_mask, ece; struct mlx5_ifc_qpc_bits qpc; // QP上下文 (~0x600 bits) wq_umem_offset / wq_umem_id / wq_umem_valid; u8 pas[][0x40]; // 变长: WQ页表 };其中包含了两个重要成员,一个是代表QP context 的mlx5_ifc_qpc_bits,是一个很大的结构体,这里不再赘述。
另外一个是pas,这个数组的每一项是一个8字节的大端地址,内容是该缓冲区的一个页。所以pas的角色是把用户虚拟地址上一段连续的缓冲区翻译成硬件视角的物理页清单,你也可以称之为页表。
所以从这个mailbox也可以看出,网卡硬件收到这个消息后,需要在ICM,也就是设备端的内存里“记住”的是,建立QP context,以及把pas数组复制为该QP的WQ页表。
四、如何理解wq是个环形缓冲区
从第三节的内容也可以看出,wq实际的空间是一个pas页表,也就是在物理地址上是分散的。可以这么理解:
pas 列出的页 = 环形缓冲区的全部物理空间
所谓的环形,其实是虚拟地址上的连续。
如下图所示