上一篇把设备交到用户态进程。本篇钉驱动库本身:谁拥有 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 Driver(spdk.io/doc/nvme.html);公共头
include/spdk/nvme.h;实现目录lib/nvme/@ tag v26.01。示例命令语义来自官方文档;无本机直通 NVMe 则不粘贴伪造 identify / perf / RPC 输出。不把官方 perf 相对 fio 的倍数抄成本机结论。
一、库的定位:被动、零拷贝、可单独链接
NVMe Driver 引言钉四句:
- 这是可直接链进应用的 C 库。
- 提供对 NVMe SSD 的 direct、zero-copy 数据路径。
- Entirely passive:不拉线程;只在应用调用时做事。
- 通过映射 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_identify(Getting
Started)。本环境无盘则只保留路径,不贴输出。
Hotplug 语义(驱动层):周期性再 probe
可发现新盘;可提供 remove_cb。热拔时访问 BAR
可能 SIGBUS——驱动安装处理并把 BAR 重映射到占位区,使飞行中
I/O 以错误完成而不是进程崩溃。细节以 NVMe Hotplug
节为准。生产上仍应把「拔盘」当成控制面事件:停提交、抽干、再拆
qpair,而不是只依赖 SIGBUS 兜底。
三、Admin vs I/O 队列
NVMe 规范区分 admin 队列与 I/O 队列;SPDK 映射为:
- Admin:控制器管理、identify、firmware、特性等;完成走
spdk_nvme_ctrlr_process_admin_completions()。 - I/O qpair:数据面读写、DSM、flush
等;每对含 SQ+CQ;完成走
spdk_nvme_qpair_process_completions()或经 poll group 聚合。
排障时把「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 上提交。
- Qpair 不含锁或原子;同一时刻只有一个线程能使用某一 qpair。
- 驱动不强制检查(加检查就要加锁);违反 → undefined behavior。
扩展建议:固定线程池,每线程独占至少一条 qpair,并常把线程钉到核;文档甚至在叙事里把 “CPU core” 与 “thread” 互换使用。进一步把应用数据也按线程切分,用消息把请求打到拥有者——与第 2 篇 message passing 对齐。
关于「一条 qpair 是否够跑满盘」:规范允许多到上千队列,消费级常见几十到上百;文档工程判断是——多数设备用单 qpair 也能跑到标称性能,多 qpair 的价值常在线程扩展与隔离,而不是「队列数正比于 IOPS」。这是官方定性叙述,不是本机 benchmark。
4.3 内部内存(量级,非本机实测)
文档 NVMe Driver Internal Memory Usage:
- 数据面无「I/O 数据缓冲池」藏在驱动里——载荷缓冲由调用方提供(且须 DMA 安全,第 5 篇)。
- 每条 I/O 命令用 tracker 跟踪;tracker 固定 4096 bytes;条数受用户请求的队列深度与控制器 MQES 约束,故每 I/O 队列 tracker 内存上界约为 \((MQES+1)\times 4\,\mathrm{KiB}\)。
- SQE 64B、CQE 16B;队列本身另计。部分控制器支持 CMB,SQ 可放在控制器内存——由能力与用户选项决定。
五、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 文档里指:
- 驱动不为读写载荷维护中间 bounce buffer;
- 设备 DMA 直达调用方提供的缓冲;
- 因此缓冲必须满足 DMA/PRP(或 SGL)约束(第 5 篇)。
它不自动意味着:
- 从 socket 到盘的端到端零拷贝;
- admin 路径无拷贝;
- 可以传入任意堆指针。
与内核路径对照:内核可能在 bio/bounce、页缓存(非
Direct)等处拷贝;O_DIRECT+io_uring
减少缓存拷贝但仍经内核驱动。SPDK
把「谁提供缓冲、谁泵完成」拉进同一进程——延迟结构变了,责任也变了。
七、多进程、CUSE 与工具边界(提前知会)
- Multi Process:同
shm_id的主/从进程可共享关键结构并建各自 qpair;核 mask 不得重叠;块级协调仍是应用责任。动机常是管理工具附着到长跑进程。细节见官方 NVMe Multi Process。 - CUSE:可把 controller/namespace 暴露为
/dev/spdk/nvme*字符设备,便于部分nvme-cli/smartctl用法;不是完整内核块设备替代,sysfs 行为与 SCSI 翻译层有缺口。见官方 NVMe Character Devices。
这些不改变「单进程数据面」主叙事,但解释了为何绑盘后仍可能看到「半个设备节点世界」。
八、设计谱系与开放问题
谱系:NVMe 多队列规范 → 用户态一对一映射 qpair → 与 DPDK 式轮询合并。学术/工程争论点:
- 被动库 vs 自带线程:组合灵活,但错误集成(忘泵完成、跨线程打同一 qpair)由应用承担。
- 单 qpair 是否够:官方偏向「多数盘单队列可满性能」;应用仍可能为隔离与多核扩展开多队列——目标要写清。
- 轮询 vs 中断:低负载 CPU 税 vs 尾延迟;26.01 线在 interrupt 相关 API 上有演进,需按负载实证。
开放问题:ZNS/KV 等命令集在用户态驱动暴露后,如何在 bdev 层不退化成「又一个 ioctl 垃圾桶」(第 6–7、16 篇)。
九、最小正确集成清单(应用侧)
在写 bdev 或业务代码之前,用下面清单自检——每条都对应文档硬约束,而不是风格偏好:
- 环境:目标 BDF 已在 VFIO/UIO;大页足够;进程有权访问设备(第 3 篇)。
- 附着:
spdk_nvme_probe(或等价 attach 路径)成功;对每个要用的 ns 取柄。 - 队列:为每个将泵完成的线程分配私有 I/O qpair;不要跨线程共享同一 qpair。
- 缓冲:所有载荷来自 DMA 安全分配(第 5 篇);提交与完成回调之间缓冲存活。
- 完成泵:在同一线程的循环/poller 中定期
process_completions(或经 poll group);admin 路径别忘了 admin 完成泵。 - 错误:热拔、超时、队列满要有回调/返回码分支;不要假设「提交成功即永完成」。
- 关停:先停提交,抽干完成,再释放
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)
- SPDK, NVMe Driver @ v26.01:被动库、提交/完成、扩展、内存、Fabrics host、多进程、热插拔、CUSE。
- SPDK, nvme.h File Reference @ v26.01:poll group API。
- SPDK changelog @ v26.01:poll group fd API 变更。
- NVMe Base Specification(命令/队列语义)——协议细节外链 storage/03。
源码(A)
spdk/spdktag v26.01:lib/nvme/、include/spdk/nvme.h、examples/nvme/hello_world/。
站内对照
实验台账
- 无本机直通 NVMe;不粘贴伪造输出。官方 perf「相对 fio 最高约 2.6× IOPS/core」等语句保持为上游测量声明,不冒充本机结果。
十一、小结
- NVMe 库被动:不拉线程;完成必须由应用/poller 泵。
- Qpair 是并行单位,且单线程独占;无锁是特性,越界是 UB。
- Admin 与 I/O 完成通道分离;排障时分开看。
- Poll group 聚合多 qpair 轮询,并承接中断模式 fd 集成(API 以 v26.01 为准)。
- 零拷贝指载荷无驱动侧 bounce,不免除 DMA 分配与对齐义务。
- 集成清单把 env、qpair 亲和、完成泵、缓冲寿命写成可勾选项——比「再调一轮队列深度」优先。
→ 上一篇:环境与绑盘 · 系列目录 · 下一篇:DMA 与 mempool
读完这篇,下一步读什么
优先读同系列或同问题的下一篇,把单篇消费变成主题集群。
【SPDK 用户态存储】用户态存储全景:从 Reactor 到 NVMe-oF 的坐标系
定位 SPDK 相对内核块层、O_DIRECT+io_uring 与 NVMe 协议百科的生态位;钉住 I/O 路径、线程模型、设备独占、块抽象、远端传输五条坐标系,并给出 16 篇阅读路线。
【SPDK 用户态存储】DMA 与 Mempool:可 DMA 内存、对齐与零拷贝约束
解释为何 SPDK 数据缓冲必须经 DMA 安全分配:物理/IOVA 翻译、PRP 约束、hugepage 与 IOMMU;并说明 mempool 复用、对齐失败模式及零拷贝相对显式拷贝的边界。
【SPDK 用户态存储】bdev 模块边界:nvme / aio / malloc 与何时不该叠层
按后端与虚拟层两类划清 SPDK bdev 模块边界:nvme、aio、malloc、null、raid、lvol 各自保证什么;说明叠层何时伤害延迟与排障,并拒绝做成模块百科。
【SPDK 用户态存储】对照内核路径:SPDK、内核 NVMe 与 O_DIRECT+io_uring
从机制对照 SPDK 用户态轮询栈、内核 NVMe 块路径与 O_DIRECT+io_uring;写清共置约束与迁移税,外链 storage/79 与 io_uring 系列,不做跨方案延迟排行。