4、Linux NVMe驱动框架:驱动入口与模块初始化、PCIe probe流程、中断处理机制

各位好,今天我们聊聊NVMe驱动的骨架——驱动入口、PCIe probe流程,还有中断处理。这三个东西,说白了就是驱动从“被加载”到“开始干活”的完整路径。我刚开始接触NVMe驱动时,总觉得代码量太大,摸不着头脑。后来发现,只要把这三条主线理清楚,剩下的都是血肉填充。

4.1 驱动入口与模块初始化

每个内核驱动都有一个入口点。NVMe驱动也不例外。它的入口函数是 nvme_init(),在模块加载时被调用。

我个人习惯,看驱动先看module_init。这能告诉你驱动是怎么注册到内核的。NVMe驱动走的是PCIe子系统,所以它注册了一个 pci_driver 结构体。

static struct pci_driver nvme_driver = {
    .name       = "nvme",
    .id_table   = nvme_id_table,
    .probe      = nvme_probe,
    .remove     = nvme_remove,
    .shutdown   = nvme_shutdown,
    .driver     = {
        .pm = &nvme_pm_ops,
    },
    .sriov_configure = nvme_sriov_configure,
};

static int __init nvme_init(void)
{
    int ret;

    ret = nvme_core_init();
    if (ret)
        return ret;

    ret = pci_register_driver(&nvme_driver);
    if (ret)
        nvme_core_exit();

    return ret;
}
module_init(nvme_init);

这里有个细节:nvme_core_init() 先于 pci_register_driver() 调用。为什么?因为核心层初始化了字符设备、sysfs接口、还有内存分配器。这些是probe流程中要用到的基础设施。我在项目中遇到过有人把顺序搞反,结果probe时分配内存失败,查了半天才发现是初始化顺序的问题。

关键点: NVMe驱动采用分层设计。nvme_init() 负责初始化核心层,然后注册PCI驱动。核心层提供通用服务,PCI层处理硬件交互。

4.2 PCIe probe流程

当内核发现一个NVMe设备时,会调用 nvme_probe()。这个函数是驱动的心脏。它负责把PCI设备变成一个可用的块设备。

probe流程大致分几步:

  1. 启用PCI设备——调用 pci_enable_device(),让设备可以响应IO和内存访问。
  2. 设置DMA掩码——告诉设备能用多大的地址空间。64位设备设64位掩码,32位设备设32位掩码。
  3. 映射BAR空间——NVMe设备通常用BAR0作为门铃寄存器。通过 pci_ioremap_bar() 映射到内核虚拟地址空间。
  4. 分配队列——创建管理队列(Admin Queue)和IO队列。管理队列用来发命令,IO队列用来传数据。
  5. 注册块设备——调用 nvme_alloc_admin_tags() 和 nvme_init_ctrl(),最终生成 /dev/nvme0 这样的设备节点。
static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct nvme_dev *dev;
    int result;

    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    result = pci_enable_device_mem(pdev);
    if (result)
        goto out_free_dev;

    result = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
    if (result)
        result = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));

    result = nvme_setup_prp_pools(dev);
    if (result)
        goto out_disable_device;

    result = nvme_init_ctrl(&dev->ctrl, &pdev->dev, &nvme_pci_ctrl_ops,
                   id->driver_data);
    if (result)
        goto out_release_prp_pools;

    pci_set_drvdata(pdev, dev);

    return 0;

out_release_prp_pools:
    nvme_release_prp_pools(dev);
out_disable_device:
    pci_disable_device(pdev);
out_free_dev:
    kfree(dev);
    return result;
}

嗯,这里要注意:nvme_setup_prp_pools() 是在 nvme_init_ctrl() 之前调用的。PRP(Physical Region Page)是NVMe用来描述数据缓冲区的结构。没有PRP池,后续的队列初始化就没法分配DMA缓冲区。我曾经在调试一个老版本内核时,发现PRP池分配失败导致probe卡死,最后定位到是DMA掩码设置不对——设备只支持32位,但代码默认设了64位。

避坑指南: 如果你在移植NVMe驱动到新平台,一定要检查DMA掩码。很多嵌入式平台只支持32位DMA,设64位掩码会导致dma_alloc_coherent()返回NULL。

4.3 中断处理机制

NVMe设备支持两种中断模式:传统MSI/MSI-X和中断聚合(Interrupt Coalescing)。我个人更推荐MSI-X,因为它支持每个队列独立中断,负载均衡效果更好。

中断处理的核心是 nvme_irq() 和 nvme_irq_check()。前者是标准中断处理函数,后者是轮询模式下的快速检查。

static irqreturn_t nvme_irq(int irq, void *data)
{
    struct nvme_queue *nvmeq = data;
    struct nvme_dev *dev = nvmeq->dev;
    u16 start, end;
    u16 completed;

    if (nvmeq->cq_vector != irq)
        return IRQ_NONE;

    start = nvmeq->cq_head;
    end = nvme_process_cq(nvmeq);
    completed = end - start;

    if (completed) {
        nvmeq->cq_head = end;
        if (nvmeq->cq_phase)
            nvmeq->cq_phase = 0;
        else
            nvmeq->cq_phase = 1;

        if (nvmeq->dev->ctrl.queue_count > 1)
            nvme_ring_cq_doorbell(nvmeq);
    }

    return IRQ_HANDLED;
}

中断处理流程其实不复杂:

  • 硬件把完成队列(Completion Queue)的条目写进内存。
  • CPU收到中断,调用 nvme_irq()。
  • 函数遍历完成队列,处理每个完成的命令。
  • 处理完后,更新门铃寄存器,告诉硬件“我处理完了”。

这里有个性能关键点:nvme_process_cq() 会批量处理完成条目,而不是一个一个处理。这样可以减少门铃寄存器的写操作。门铃寄存器是MMIO空间,写一次开销不小。我在项目中遇到过,如果每个完成条目都写一次门铃,IOPS会下降30%以上。

注意: 中断上下文不能睡眠。NVMe的中断处理函数是原子上下文,不能调用可能睡眠的函数(比如kmalloc带GFP_KERNEL标志)。如果你需要在中断中分配内存,请用GFP_ATOMIC。

4.4 整体框架图

下面这张图展示了NVMe驱动的整体框架。从模块初始化到中断处理,每个模块的职责一目了然。

Linux NVMe驱动框架 模块初始化 nvme_init() PCIe Probe nvme_probe() 中断处理 nvme_irq() nvme_core_init() pci_register_driver() pci_enable_device() dma_set_mask() nvme_setup_prp_pools() nvme_init_ctrl() nvme_process_cq() nvme_ring_cq_doorbell() 三个核心模块:初始化 → Probe → 中断处理 每个模块内部有多个子步骤,按顺序执行

这张图把三个模块串起来了。初始化注册PCI驱动,Probe创建设备和队列,中断处理响应硬件完成事件。三者缺一不可。

4.5 小结

NVMe驱动的入口、probe和中断处理,是理解整个驱动的钥匙。你想想看,驱动无非就是“初始化硬件 → 注册到内核 → 响应中断”这三件事。NVMe只是把这三件事做得更精细、更高效而已。

我个人建议,初学者先别急着看IO路径和命令提交。先把probe流程走一遍,把中断处理函数读懂。这两块搞明白了,剩下的都是锦上添花。

核心要点回顾:
  • module_init注册PCI驱动,nvme_core_init提供基础设施
  • probe流程:使能设备 → 设置DMA → 映射BAR → 分配队列 → 注册块设备
  • 中断处理:批量处理完成队列,减少门铃写操作
  • MSI-X优于MSI,支持每个队列独立中断

好了,这一章就到这里。下一章我们深入IO路径,看看NVMe命令是怎么提交和完成的。

可可精品资料库