NVMe 驱动走读
把前面学到的知识放进一个真实 in-tree 驱动:drivers/nvme/host/pci.c
为什么选 NVMe 做走读样本?
前面各页讲的是"零件":设备驱动的 API、MSI-X 硬件层、复位与错误恢复。而 drivers/nvme/host/pci.c 是这些零件在一根主线上的真实组合:它是 PCI 设备驱动、用 BAR 做 MMIO、用 MSI-X 建多队列中断、支持 FLR/错误恢复——读一遍它的 probe 流程,胜过十个玩具示例。本页按执行顺序走读关键函数(源码行号基于 master,随版本略有漂移)。
第 0 步:设备怎么匹配到 NVMe 驱动
/* drivers/nvme/host/pci.c(节选) */
static const struct pci_device_id nvme_id_table[] = {
{ PCI_DEVICE_CLASS(PCI_CLASS_STORAGE_EXPRESS, 0xffffff), }, /* 类码匹配 */
{ PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x2001), /* 特定 VID/DID */
.driver_data = NVME_QUIRK_IGNORE_DEV_SUBNQN, },
/* ... 若干厂商/compat 条目,带 quirk 标志 */
{ 0, }
};
MODULE_DEVICE_TABLE(pci, nvme_id_table);
static struct pci_driver nvme_driver = {
.name = "nvme",
.id_table = nvme_id_table,
.probe = nvme_probe,
.remove = nvme_remove,
.shutdown = nvme_shutdown,
.err_handler = &nvme_err_handler, /* 错误恢复回调组(见 error-handling 页) */
.sriov_configure = nvme_pci_sriov_configure,
};
要点:类码匹配(Class Code 0x01/0x08/0x02 = NVM Express 控制器)让任何未绑定的 NVMe 控制器都能被接管,无需逐个 VID/DID 添加——这是"通用类驱动"的标准手法;driver_data 携带 quirk 位,处理硬件缺陷。
第 1 步:nvme_probe 的执行顺序
/* drivers/nvme/host/pci.c: nvme_probe()(骨架,行号 ~3763) */
static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
dev = nvme_pci_alloc_dev(pdev, id); /* 1. 分配设备结构+初始化 ctrl */
nvme_add_ctrl(&dev->ctrl); /* 2. 注册到 nvme-core(此时尚无队列) */
nvme_dev_map(dev); /* 3. pci_request_mem_regions + pci_iomap BAR0 */
nvme_pci_alloc_iod_mempool(dev); /* 4. DMA 内存池(PRP/SGL 描述符) */
nvme_pci_enable(dev); /* 5. 使能设备并"启动控制器":见下 */
nvme_alloc_admin_tag_set(...); /* 6. 建 admin tag set(blk-mq 基础设施) */
nvme_change_ctrl_state(..., NVME_CTRL_CONNECTING);
/* 7. 建 Admin 队列 → 发 Identify/Set Features 等管理命令 */
/* 8. nvme_setup_io_queues():按 CPU 数申请 MSI-X 与 IO 队列 */
/* 9. 注册块设备(nvme-core 完成) */
}
5. nvme_pci_enable:PCI 使能与控制器启动
/* nvme_pci_enable()(行号 ~3209) */
pci_enable_device_mem(pdev); /* 1. 使能 Memory Space(BAR 可访问) */
pci_set_master(pdev); /* 2. 置 Bus Master(允许 DMA) */
/* 3. 映射 BAR0 后校验控制器是否活了:CSTS == -1 说明 MMIO 读全 1 */
if (readl(dev->bar + NVME_REG_CSTS) == -1) {
result = -ENODEV; /* 控制器不可达(链路/复位未完成) */
goto disable;
}
/* 4. 配置并使能控制器(写 NVME_REG_CC) */
writel(NVME_CC_ENABLE | NVME_CC_CSS_NVM | NVME_CC_IOSQES | NVME_CC_IOCQES | shn, ...);
/* 5. 等待 NVME_CSTS_RDY 置位(超时则失败) */
while (!(readl(dev->bar + NVME_REG_CSTS) & NVME_CSTS_RDY)) { msleep(1); ... }
/* 6. 关中断/复位控制器,为队列建立做准备 */
逐个对应前面学过的概念:pci_enable_device_mem() 与 pci_set_master() 是设备驱动页讲的 Command 寄存器配合;寄存器偏移(CAP=0x0、VS=0x8、CC=0x14、CSTS=0x1C、AQA=0x24、ASQ=0x28、ACQ=0x30,见 include/linux/nvme.h)是 NVMe 规范固定的控制器寄存器布局——注意它是 BAR0 内的 MMIO,与 PCI 配置空间(偏移 0x00–0xFFF)完全两个空间。
"CSTS 读回全 1"为什么是经典判据?
PCIe 配置读失败返回全 1(Vendor ID = 0xFFFF);MMIO 读全 1 同理表示"目标不可达"(链路没起来/复位没释放/地址窗没配对)。这个判据在几乎所有 PCIe 驱动里都会出现,也是错误处理页里"设备不可达"的软件表征。
第 2 步:Admin 队列 → IO 队列 → MSI-X 多队列
NVMe 的队列是"内存里的一对环形缓冲"(SQ 提交队列 + CQ 完成队列),控制器通过 DB(Doorbell)寄存器感知提交。驱动先建一个 Admin 队列(经 AQA/ASQ/ACQ 寄存器告诉控制器位置),用它发 Identify 命令了解控制器能力,再建 IO 队列:
/* nvme_setup_io_queues()(行号 ~2960):按 CPU 数决定队列数 */
/* 1. 申请 MSI-X 向量(每队列一个向量 = 中断亲和的基础) */
nr_io_queues = min(maxcpus, dev->max_qid);
result = pci_alloc_irq_vectors(pdev, 1, nr_io_queues, PCI_IRQ_ALL_TYPES);
if (result < 0) { /* 回退:更少队列 */ }
/* 2. 逐个建立:分配 SQ/CQ 内存(DMA 一致性内存)、写 DB 寄存器 */
static int nvme_create_queue(struct nvme_queue *nvmeq, int qid, bool polled)
{
nvmeq->cq_vector = qid - 1;
result = adapter_alloc_cq(dev, qid, nvmeq); /* 建完成队列 + 请求 IRQ */
result = adapter_alloc_sq(dev, qid, nvmeq); /* 建提交队列 */
/* 写 NVME_REG_DBS 中的 doorbell,通知控制器队列已就绪 */
}
/* 3. 中断处理:MSI-X handler 只做"取完成项" */
static irqreturn_t nvme_irq(int irq, void *data)
{
nvmeq = data;
/* 从 CQ 取完成项,唤醒相应请求;按需触发 blk-mq 软中断 */
}
对应前面学过的MSI-X 硬件层:pci_alloc_irq_vectors() 完成后,每个队列的中断地址/数据已写入设备 MSI-X Table;队列数 = 向量数,配合 CPU 亲和性实现"每个 CPU 处理自己的队列"——这是 NVMe 百万级 IOPS 的软件基础。多队列的核-队列映射规则(nvmeq->cq_vector 与 blk_mq 的映射)从设计上避免跨核竞争。
对照表:理论概念在真实驱动里的落点
| 理论概念(本站页面) | NVMe 驱动中的落点 |
|---|---|
| Command 寄存器 Memory Space / Bus Master(设备驱动) | pci_enable_device_mem() + pci_set_master() |
| BAR 映射与 MMIO(同上) | nvme_dev_map() → pci_iomap() BAR0;控制器寄存器读写全部走这里 |
| DMA 一致性内存(同上) | 队列内存、PRP/SGL 列表用 dma_alloc_coherent()/dma_map_sg() |
| MSI-X 与多队列(同上 MSI 硬件层) | pci_alloc_irq_vectors() 按 CPU 数申请;每队列一个向量 |
| 错误恢复 pci_error_handlers(错误处理) | nvme_err_handler:error_detected 冻结队列 → slot_reset 重初始化 → resume 恢复服务 |
| FLR 与复位(同上) | nvme_reset_ctrl() 流程中触发设备复位并重建队列 |
| SR-IOV(虚拟化) | sriov_configure 回调;NVMe VF 直通到虚拟机 |
| 性能调优(性能调优) | 队列深度 vs 设备 MQES(CAP 寄存器低 16 位);MRRS 影响读性能 |
拿自己的驱动对照自查
- probe 顺序:先
pci_enable_device→pci_request_regions→pci_iomap→pci_set_master→ 分配中断 → 建队列?顺序错了会出现"BAR 读全 1""中断不来"等怪象。 - MMIO 可达性校验:读一个已知非全 0/全 1 的寄存器判断设备是否真的可达(学 NVMe 的 CSTS 判据)。
- 中断向量与队列一一对应:MSI-X 向量数少于队列数时,是否优雅回退并共享向量?
- 错误恢复回调:是否实现了
pci_error_handlers,还是让设备"出错即死"? - remove/shutdown:停队列 → 关中断(
pci_free_irq_vectors)→ 释放 DMA →pci_iounmap→pci_release_regions→pci_disable_device,逆序且完整?