4. Linux 内核 NVMe 驱动架构:核心数据结构、模块初始化、探测与移除
说实话,很多人拿到 NVMe 驱动源码,第一反应就是找入口函数。但我觉得,先搞清楚内核怎么组织这块代码,后面才不会抓瞎。这一章我们就从三个角度切入:内核里 NVMe 驱动用的那些核心数据结构,驱动怎么注册进系统,以及设备热插拔时它怎么响应。
4.1 核心数据结构一览
NVMe 驱动的实现依赖几个关键结构体,它们串联了整个 I/O 路径。我习惯先把它们打在脑子里,再去看代码,效率会高很多。
| 结构体名称 | 作用域 | 核心字段(列举) | 备注 |
|---|---|---|---|
nvme_dev | 一个 NVMe 控制器实例 | bar, irq, ctrl, queues | 对应 PCI 设备 probe 时创建 |
nvme_ctrl | 控制器抽象层 | admin_q, fabrics_q, ctrl_config | 分离了 PCIe 与 RDMA/FC 共享部分 |
nvme_queue | 单个提交/完成队列对 | sq_cmds, cqes, q_depth | 实际存放命令和完成槽位 |
nvme_ns | 命名空间实例 | ns_id, disk, lba_shift | 每个 namespace 对应一个块设备 |
request_queue | 块层 I/O 请求队列 | make_request_fn, q_used | 内核通用块层结构,由驱动填充 |
这些结构体之间的引用关系,我画了一张图帮你快速建立印象:
我的个人习惯:看驱动源码时,先 grep 这些结构体的定义和引用,再配合 pahole 查看布局,能快速理解内存分布。
4.2 模块初始化流程
驱动以内核模块的形式存在。入口是 module_init(nvme_pci_driver_init),它做的事情其实很固定——注册一个 PCI 驱动。
static int __init nvme_pci_driver_init(void)
{
return pci_register_driver(&nvme_pci_driver);
}
module_init(nvme_pci_driver_init);
嗯,就这么几行。但背后发生的事情不少:
- PCI 驱动注册:填充
pci_driver结构体,指定id_table(支持哪些 VID/DID),以及probe、remove、suspend、resume回调。 - 动态分配主设备号:内核自动分配,驱动无需自己申请。
- 初始化互斥锁和工作队列:比如
nvme_workq用来调度异步 admin 命令。
我曾经踩过一个坑:自己写的 NVMe 驱动没有注册 id_table,导致设备插上后 probe 根本没被调用。排查了半天才发现是 VID/DID 不匹配。所以,id_table 必须跟你的硬件完全对应,尤其是 OEM 定制型号。
注意: 新版本内核(5.x+)还增加了 nvme_reset_work 作为延迟重置的工作项,如果你定制的是早期版本,记得小心这部分差异。
4.3 探测与移除细节
4.3.1 probe:设备发现时发生了什么?
nvme_pci_probe 是整个驱动的核心入口。当 PCI 子系统发现一个匹配的设备时,就会调用它。流程可以概括为:
- 启用 PCI 设备:调用
pci_enable_device,随后pci_request_mem_regions获取 BAR 空间。 - 映射 MMIO:将 BAR0 映射为虚拟地址,存放在
dev->bar里。这里我建议你用ioremap_wc开启写合并,对 NVMe 的提交队列写入性能有明显提升。 - 分配中断:先尝试 MSI-X,不行就回退到 MSI。内核提供了
pci_enable_msix和pci_enable_msi。 - 创建管理队列(admin queue):这是 NVMe 规范要求的第一个队列对,用于执行 Admin 命令(如创建 I/O 队列、获取 log page)。
- 初始化控制器:通过 Admin 命令读取控制器能力(CC/ CAP/ VS),设置队列深度和仲裁机制。
- 创建 I/O 队列:根据硬件能力创建若干个 I/O 队列对,每个队列绑定到一个 CPU 核心以获得局部性。
- 扫描命名空间:发送
Identify命令,获取每个 namespace 信息,并注册块设备(nvme_ns_alloc后调用add_disk)。
为什么我说要留意中断分配的顺序?因为我在某次适配国产 NVMe 卡时,发现它虽然宣称支持 MSI-X,但实际只实现了 2 个向量。当时我调用 pci_enable_msix 时分配了 8 个,结果内核直接返回错误。后来改成先读取实际可用向量数,再分配。
4.3.2 remove:优雅的清理
设备移除时,nvme_pci_remove 被调用。具体步骤:
- 设置
dev->ctrl.state为DELETING,阻止新的 I/O 提交。 - 等待所有正在处理的命令完成(
nvme_cancel_request+ 超时回收)。 - 删除所有命名空间对应的块设备(
del_gendisk)。 - 释放中断、取消注册。
- 释放 MMIO 映射、释放队列内存、释放
nvme_dev结构体。
这里有个容易忽视的点:先清理 I/O 队列,再清理 Admin 队列。如果你反过来,Admin 队列先被销毁,那么 I/O 队列的完成处理函数中如果还想发送 Admin 命令(比如写 log),就会触发非法访问。我在某个内核 4.19 版本上调试时就因为顺序问题导致 oops,后来严格按“I/O → Admin → 资源”的顺序重构才稳定。
总结一下:NVMe 驱动的 probe/remove 遵循典型的 PCI 驱动模式,但多了 NVMe 特有的队列创建和命名空间发现步骤。理解 nvme_dev → nvme_ctrl → nvme_queue → nvme_ns 的层次关系,是掌握整章的基础。