三个高性能 I/O 框架——RDMA Verbs(网络)、io_uring(通用系统调用)、SPDK(存储)——应用的领域完全不同,但在底层队列数据结构的设计上,殊途同归。它们都不约而同地用了基于共享内存的环形缓冲区(Circular Ring Buffer)和生产者-消费者模型。
这篇文章逐一拆解三者的队列结构,再放到一起对比异同。
RDMA Verbs:硬件直接读你的内存
RDMA 的核心通信机制基于 Queue Pair(QP,队列对)。
一个 QP 包含两个队列:Send Queue(SQ,发送队列)和 Receive Queue(RQ,接收队列)。此外还必须绑定 Completion Queue(CQ,完成队列),多个 QP 可以共享同一个 CQ。
数据结构是驻留在主机内存中的环形数组。工作流程:
- 软件(用户态)作为生产者:应用程序构建 Work Request(WR),下发到 SQ 或 RQ,转换为硬件可读的 Work Queue Element(WQE),然后敲 Doorbell(写特定寄存器)通知网卡。
- 硬件(RNIC 网卡)作为消费者:收到 Doorbell 后,通过 DMA 直接读主机内存中的 WQE,执行网络传输。
- 完成通知:硬件完成后作为生产者,将 Completion Queue Element(CQE)通过 DMA 写入 CQ。软件轮询 CQ 或等中断来拿结果。
整个过程,用户态直接和网卡对话,内核完全旁路。
io_uring:内核学硬件的产物
io_uring 是 Linux 内核原生的异步 I/O 框架。它的设计思想非常直白:学硬件那套队列机制,重构系统调用的提交方式。
同样用环形缓冲区,通过 mmap 将内核和用户空间映射到同一块物理内存。两个独立队列:
- Submission Queue(SQ):提交队列
- Completion Queue(CQ):完成队列
SQ 有个特殊设计:它由两部分组成——存放具体任务信息的 SQE(Submission Queue Element)数组,加上一个存放 SQE 索引的环形数组。这一步间接引用换来了灵活性:一个 SQE 可以在队列中多次引用。
工作流程:
- 用户态作为生产者:填好 SQE,更新 SQ 的 Tail 指针。
- 内核作为消费者:发现 Head 和 Tail 不一致时,消费 SQE 执行实际系统调用。
- 完成通知:内核作为生产者,将结果写入 CQE 并更新 CQ 的 Tail;用户态检查 CQ 的 Head 消费结果。
io_uring 没有绕过内核——它仍然依赖内核处理逻辑。但 mmap 共享内存 + 环形队列 + 批量提交的组合,已经能把系统调用的开销压到极低。SQPOLL 模式甚至可以让内核线程替你轮询 SQ,把用户态的系统调用也省了。
SPDK:跟 SSD 控制器直接握手
SPDK(Storage Performance Development Kit)针对 NVMe 存储设备,核心是用户态轮询模式驱动(PMD)。
严格遵循 NVMe 规范定义的 SQ 和 CQ,同样是基于连续物理内存的环形缓冲区。通过 UIO/VFIO 将 DMA 内存地址和 NVMe 控制器的 PCIe 寄存器直接映射到进程地址空间。
工作流程:
- 软件(SPDK 用户态驱动)作为生产者:构建 64 字节的 NVMe 命令,放入 SQ,向 NVMe 控制器的 SQ Tail Doorbell 寄存器写新指针。
- 硬件(NVMe SSD 控制器)作为消费者:SSD 主控通过 DMA 抓取 SQ 中的命令,执行 NAND 闪存操作。
- 完成通知:SSD 执行完成后,将 16 字节的完成状态通过 DMA 写入 CQ 并触发中断。SPDK 通常屏蔽中断,改为轮询 CQ 的 Phase Tag 标志位。
拆到底:同与不同
它们为什么长得一样
| 共同点 | 说明 |
|---|---|
| SQ/CQ 分离的环形队列 | 天然适合批量提交和异步解耦 |
| 内存共享 + 零拷贝 | RDMA/SPDK 是应用内存直连硬件 DMA;io_uring 是用户态和内核共享内存 |
| 无锁化设计 | 通过 Head/Tail 指针的原子操作和内存屏障管理队列。单生产者单消费者场景下完全不需要锁 |
| 轮询为主 | 为追求低延迟,都鼓励或默认用轮询替代中断(RDMA 轮询 CQ、SPDK 轮询 Phase Tag、io_uring 的 SQPOLL 让内核线程替你轮询) |
它们分道扬镳的地方
| 维度 | RDMA Verbs | io_uring | SPDK |
|---|---|---|---|
| 队列消费者 | 硬件(RNIC 网卡) | 软件(Linux 内核) | 硬件(NVMe SSD 控制器) |
| Bypass 路径 | 完全旁路内核,软硬直连 | 旁路系统调用开销,仍走内核逻辑 | 完全旁路内核,软硬直连 |
| 数据元素结构 | WQE 极复杂,含 Scatter/Gather List 支持分散内存汇聚 | SQE 相对统一(64 bytes),映射各种 POSIX 调用参数 | NVMe Command 严格固定(64 bytes) |
| 状态机复杂度 | 极高。QP 必须走 RESET→INIT→RTR→RTS 严格状态转换 | 低。创建环形队列即可用,无硬件握手 | 中等。需复位控制器、分配 Admin 队列再创建 I/O 队列 |
| 通信媒介 | PCIe(CPU→网卡)+ 以太网/IB(网卡→网卡) | 纯 CPU 内存总线与内核通信 | PCIe(CPU→硬盘控制器) |
几点心得
这三种技术代表了高性能计算的同一个方向:避免数据拷贝、避免上下文切换、避免锁竞争。
RDMA 和 SPDK 走的是同一路线——应用层直接跟外设硬件(网卡/硬盘)对接,把内核彻底踢出数据路径。io_uring 选了一条更务实的路:保留内核这个中间人,但用硬件的队列思想重构了跟内核打交道的方式。
某种意义上,io_uring 是操作系统内核在向 RDMA 和 NVMe 的硬件队列设计学习。“既然硬件可以用环形缓冲 + Doorbell 实现高效异步,为什么系统调用不行?”
三种技术,同一种设计哲学。如果对其中某个框架有更深的一手经验,欢迎补充指正。