土法炼钢 · 系统与基础设施

【SPDK 用户态存储】用户态 NVMe 驱动:Controller、Qpair 与 Poll Group

文章导航

分类入口
storagelinux
标签入口
#spdk#nvme#qpair#poll-group#userspace-driver#zero-copy#v26.01

目录

上一篇把设备交到用户态进程。本篇钉驱动库本身:谁拥有 controller、I/O 如何进 SQ、完成如何出 CQ、admin 与 I/O 队列如何分工——以及库为何自称 entirely passive。

常见误区:以为链上 lib/nvme 就会自动起线程收完成;或以为多个线程可以无协调地敲同一 qpair。官方文档把这两点写死了——违反第二点的结果是 undefined behavior,且驱动故意不加锁来「帮你检查」。

协议字段与命令集演进仍以 storage/03 为准;本篇只写 SPDK 实现路径与失败模式

本文是「SPDK / 用户态存储栈」系列第 4 篇(共 16 篇)。→ 系列目录

篇目 核心内容
第 3 篇 · 环境与绑盘 VFIO / 大页
第 4 篇 · NVMe 驱动 controller、qpair、poll group
第 5 篇 · DMA / mempool 缓冲与零拷贝约束
第 6 篇 · bdev 核心 块抽象如何包住驱动

版本锚定:SPDK v26.01 NVMe Driverspdk.io/doc/nvme.html);公共头 include/spdk/nvme.h;实现目录 lib/nvme/ @ tag v26.01。示例命令语义来自官方文档;无本机直通 NVMe 则不粘贴伪造 identify / perf / RPC 输出。不把官方 perf 相对 fio 的倍数抄成本机结论。


一、库的定位:被动、零拷贝、可单独链接

NVMe Driver 引言钉四句:

  1. 这是可直接链进应用的 C 库
  2. 提供对 NVMe SSD 的 direct、zero-copy 数据路径。
  3. Entirely passive:不拉线程;只在应用调用时做事。
  4. 通过映射 PCI BAR 做 MMIO;I/O 经 queue pair 异步提交,完成模型「与 Linux libaio 不完全不像」。

后来同一套 API 也能 spdk_nvme_probe() 到远端 NVMe-oF——传输变了,调用形态尽量不变。本篇聚焦本机 PCIe;远端 host 侧细节到第 10 篇。

与第 2 篇的关系:你可以在自写循环里调 spdk_nvme_qpair_process_completions(),也可以注册 poller 包一层;驱动不替你选。把完成泵挂错线程,和跨线程共享 qpair 一样,都属于集成错误而非「盘性能问题」。

flowchart TD
  APP["Application / bdev / poller"]
  PROBE["spdk_nvme_probe"]
  CTRLR["spdk_nvme_ctrlr"]
  ADMIN["Admin qpair"]
  IOQ["I/O qpair(s)"]
  PG["poll group optional"]
  DEV["NVMe SSD BAR / queues"]
  APP --> PROBE --> CTRLR
  CTRLR --> ADMIN
  CTRLR --> IOQ
  APP -->|"submit ns_cmd_*"| IOQ
  APP -->|"process_completions"| IOQ
  IOQ --> PG
  APP -->|"poll_group_process_completions"| PG
  IOQ --> DEV
  ADMIN --> DEV

二、附着:spdk_nvme_probe 与 controller / namespace

关键入口(文档 Public Interface 表):

函数 角色
spdk_nvme_probe() 按 transport ID 枚举并按回调决定是否 attach
spdk_nvme_ctrlr_get_ns() 取 namespace 句柄
spdk_nvme_ctrlr_alloc_io_qpair() 分配 I/O 提交/完成队列对
spdk_nvme_ns_cmd_read/write(及 *v、带 metadata 变体等) 提交 NVM 命令
spdk_nvme_qpair_process_completions() 抽完成并触发回调
spdk_nvme_ctrlr_process_admin_completions() 抽 admin 完成
spdk_nvme_ctrlr_cmd_admin_raw() / spdk_nvme_ctrlr_cmd_io_raw() 原始命令逃逸口

入门示例目录:examples/nvme/,官方推荐从 hello_world 开始;识别工具 build/bin/spdk_nvme_identifyGetting Started)。本环境无盘则只保留路径,不贴输出。

Hotplug 语义(驱动层):周期性再 probe 可发现新盘;可提供 remove_cb。热拔时访问 BAR 可能 SIGBUS——驱动安装处理并把 BAR 重映射到占位区,使飞行中 I/O 以错误完成而不是进程崩溃。细节以 NVMe Hotplug 节为准。生产上仍应把「拔盘」当成控制面事件:停提交、抽干、再拆 qpair,而不是只依赖 SIGBUS 兜底。


三、Admin vs I/O 队列

NVMe 规范区分 admin 队列与 I/O 队列;SPDK 映射为:

排障时把「identify 卡住」和「读 I/O 卡住」分到不同完成泵上,避免只轮询 I/O qpair 却忘了 admin。把 admin 完成泵塞进与 I/O 相同的 poller 里通常可行,但要用计数与超时把两类延迟分开看,否则一次慢 identify 会被误读成「盘读延迟」。

Admin 路径上,文档承认:部分 admin 命令在所用 API 下可能涉及数据拷贝;这与数据面「无 I/O 数据缓冲拷贝」的零拷贝叙事不矛盾——零拷贝声明针对的是 NVM 读写载荷路径。


四、Qpair:并行单位与单线程独占

4.1 提交与完成

nvme_ns_cmd_* 把请求写成 SQE 提交后立即返回;应用必须对仍有未完成 I/O 的 qpair 泵完成。忘记 process_completions 的经典症状是:提交一段时间后队列满/挂起,CPU 却在空转别的事——完成从未被收。

4.2 无锁与线程规则

官方 Scaling Performance

扩展建议:固定线程池,每线程独占至少一条 qpair,并常把线程钉到核;文档甚至在叙事里把 “CPU core” 与 “thread” 互换使用。进一步把应用数据也按线程切分,用消息把请求打到拥有者——与第 2 篇 message passing 对齐。

关于「一条 qpair 是否够跑满盘」:规范允许多到上千队列,消费级常见几十到上百;文档工程判断是——多数设备用单 qpair 也能跑到标称性能,多 qpair 的价值常在线程扩展与隔离,而不是「队列数正比于 IOPS」。这是官方定性叙述,不是本机 benchmark。

4.3 内部内存(量级,非本机实测)

文档 NVMe Driver Internal Memory Usage


五、Poll group:多 qpair 的一泵多路

当一线程盯多条 qpair(或多远程连接)时,逐个 process_completions 可工作,但管理繁琐。Poll group 把多条 qpair 聚合成一次泵完成:

API(nvme.h / 文档) 作用
spdk_nvme_poll_group_create() 创建组(现代版本附带 fd group 以管中断事件)
spdk_nvme_poll_group_add() / remove() 加入/移除 qpair
spdk_nvme_poll_group_process_completions() 轮询组内所有 qpair 完成
spdk_nvme_poll_group_get_fd_group() 取 fd group(中断模式集成)
spdk_nvme_poll_group_wait() 等待组内中断事件并处理

v26.01 变更脉络(changelog):移除未使用的 spdk_nvme_qpair_get_optimal_poll_group() 与旧式 spdk_nvme_poll_group_get_fd();改为经 get_fd_group() 再取 fd。写代码时以 v26.01 头文件为准,不要抄过时博客。

默认数据面叙事仍是轮询;interrupt mode 是降低 CPU 税的另一条轴(本机 NVMe 与 NVMe-oF RDMA 的中断细节见官方 NVMe Interrupt Mode 与第 9 篇),本篇只要求:poll group 同时服务「多队列一泵」与「中断等待」两种集成方式。


六、零拷贝在本层的精确含义

「Zero-copy」在 SPDK NVMe 文档里指:

自动意味着:

与内核路径对照:内核可能在 bio/bounce、页缓存(非 Direct)等处拷贝;O_DIRECT+io_uring 减少缓存拷贝但仍经内核驱动。SPDK 把「谁提供缓冲、谁泵完成」拉进同一进程——延迟结构变了,责任也变了。


七、多进程、CUSE 与工具边界(提前知会)

这些不改变「单进程数据面」主叙事,但解释了为何绑盘后仍可能看到「半个设备节点世界」。


八、设计谱系与开放问题

谱系:NVMe 多队列规范 → 用户态一对一映射 qpair → 与 DPDK 式轮询合并。学术/工程争论点:

  1. 被动库 vs 自带线程:组合灵活,但错误集成(忘泵完成、跨线程打同一 qpair)由应用承担。
  2. 单 qpair 是否够:官方偏向「多数盘单队列可满性能」;应用仍可能为隔离与多核扩展开多队列——目标要写清。
  3. 轮询 vs 中断:低负载 CPU 税 vs 尾延迟;26.01 线在 interrupt 相关 API 上有演进,需按负载实证。

开放问题:ZNS/KV 等命令集在用户态驱动暴露后,如何在 bdev 层不退化成「又一个 ioctl 垃圾桶」(第 6–7、16 篇)。


九、最小正确集成清单(应用侧)

在写 bdev 或业务代码之前,用下面清单自检——每条都对应文档硬约束,而不是风格偏好:

  1. 环境:目标 BDF 已在 VFIO/UIO;大页足够;进程有权访问设备(第 3 篇)。
  2. 附着spdk_nvme_probe(或等价 attach 路径)成功;对每个要用的 ns 取柄。
  3. 队列:为每个将泵完成的线程分配私有 I/O qpair;不要跨线程共享同一 qpair。
  4. 缓冲:所有载荷来自 DMA 安全分配(第 5 篇);提交与完成回调之间缓冲存活。
  5. 完成泵:在同一线程的循环/poller 中定期 process_completions(或经 poll group);admin 路径别忘了 admin 完成泵。
  6. 错误:热拔、超时、队列满要有回调/返回码分支;不要假设「提交成功即永完成」。
  7. 关停:先停提交,抽干完成,再释放 qpair/detach;自定义 shutdown_cb 时按 app 框架要求调用 spdk_app_stop()

少任何一条,故障都会打扮成「盘慢」或「SPDK 不稳」。官方 hello_world / perf 是对照实现;读它们时重点看完成泵与 qpair 亲和,不要只抄参数行。perf 相对 fio 更「少开销」的上游叙述,只说明基准工具本身会吃掉用户态优势——选型时应用等价工具,而不是混用口径。

再补一条易漏项:命名空间与 LBA 格式(扇区大小、元数据、PI)必须在提交前读自 identify,而不是假设 512B。协议细节归 storage/03;实现上用错格式的命令会以设备错误回来,看起来像驱动 bug。fused 操作(compare-and-write 等)还有「同队列相邻提交、相同 LBA 范围」等规范约束,文档 Fused operations 节给出了调用顺序要求——本篇不展开业务场景,只提醒:高级命令不是「换个 opcode」那么简单。

源码阅读入口保持目录级:lib/nvme/ 按传输与 PCIe 路径分文件,公共契约在 include/spdk/nvme.h。跟调用栈时以 v26.01 tag 为准,避免把主干上已删的 poll group 辅助 API 抄进生产代码。


十、参考资料

规范 / 官方文档(A)

源码(A)

站内对照

实验台账


十一、小结

  1. NVMe 库被动:不拉线程;完成必须由应用/poller 泵。
  2. Qpair 是并行单位,且单线程独占;无锁是特性,越界是 UB。
  3. Admin 与 I/O 完成通道分离;排障时分开看。
  4. Poll group 聚合多 qpair 轮询,并承接中断模式 fd 集成(API 以 v26.01 为准)。
  5. 零拷贝指载荷无驱动侧 bounce,不免除 DMA 分配与对齐义务。
  6. 集成清单把 env、qpair 亲和、完成泵、缓冲寿命写成可勾选项——比「再调一轮队列深度」优先。

上一篇:环境与绑盘 · 系列目录 · 下一篇:DMA 与 mempool

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。


By .