linux内核源码分析 - nvme设备的初始化

本文深入探讨Linux内核中NVMe设备的初始化过程,包括驱动加载、NVMe数据结构和设备初始化。重点讲解了如何识别NVMe设备、nvme_probe函数的工作,以及如何建立硬件与驱动之间的传输通道,包括admin queue和io queue的初始化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【好文推荐】

浅析linux内核网络协议栈--linux bridge

深入理解SR-IOV和IO虚拟化

virtio-net 实现机制【一】(图文并茂)

驱动的加载

驱动加载实际就是module的加载,而module加载时会对整个module进行初始化,nvme驱动的module初始化函数为nvme_init(),如下:

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_dev_pm_ops,
    },
    .err_handler    = &nvme_err_handler,
};

static int __init nvme_init(void)
{
    int result;

    /* 初始化等待队列nvme_kthread_wait,此等待队列用于创建nvme_kthread(只允许单进程创建nvme_kthread) */
    init_waitqueue_head(&nvme_kthread_wait);

    /* 创建一个workqueue叫nvme */
    nvme_workq = create_singlethread_workqueue("nvme");
    if (!nvme_workq)
        return -ENOMEM;

    /* 在内核中注册新的一类块设备驱动,名字叫nvme,注意这里只是注册,表示kernel支持了nvme类的块设备,返回一个major,之后所有的nvme设备的major都是此值 */
    result = register_blkdev(nvme_major, "nvme");
    if (result < 0)
        goto kill_workq;
    else if (result > 0)
        nvme_major = result;

    /* 注册一些通知信息 */
    nvme_nb.notifier_call = &nvme_cpu_notify;
    result = register_hotcpu_notifier(&nvme_nb);
    if (result)
        goto unregister_blkdev;

    /* 注册pci nvme驱动 */
    result = pci_register_driver(&nvme_driver);
    if (result)
        goto unregister_hotcpu;
    return 0;

 unregister_hotcpu:
    unregister_hotcpu_notifier(&nvme_nb);
 unregister_blkdev:
    unregister_blkdev(nvme_major, "nvme");
 kill_workq:
    destroy_workqueue(nvme_workq);
    return result;
}

这里面其实最重要的就是做了两件事,一件事是register_blkdev,注册nvme这类块设备,返回一个major,另一件事是注册了nvme_driver,注册了nvme_driver后,当有nvme设备插入后系统后,系统会自动调用nvme_driver->nvme_probe去初始化这个nvme设备.这时候可能会有疑问,系统是如何知道插入的设备是nvme设备的呢,注意看struct pci_driver nvme_driver这个结构体,里面有一个nvme_id_table,其内容如下:

/* Move to pci_ids.h later */
#define PCI_CLASS_STORAGE_EXPRESS    0x010802

static const struct pci_device_id nvme_id_table[] = {
    { PCI_DEVICE_CLASS(PCI_CLASS_STORAGE_EXPRESS, 0xffffff) },
    { 0, }
};

再看看PCI_DEVICE_CLASS宏是如何定义的

#define PCI_DEVICE_CLASS(dev_class,dev_class_mask) \
     .class = (dev_class), .class_mask = (dev_class_mask), \
     .vendor = PCI_ANY_ID, .device = PCI_ANY_ID, \
     .subvendor = PCI_ANY_ID, .subdevice = PCI_ANY_ID

也就是当pci class为PCI_CLASS_STORAGE_EXPRESS时,就表示是nvme设备,并且这个是写在设备里的,当设备插入host时,pci driver(并不是nvme driver)回去读取这个值,然后判断它需要哪个驱动去做处理.

nvme数据结构

现在假设nvme.ko已经加载完了(注册了nvme类块设备,并且注册了nvme driver),这时候如果有nvme盘插入pcie插槽,pci会自动识别到,并交给nvme driver去处理,而nvme driver就是调用nvme_probe去处理这个新加入的设备.

在说nvme_probe之前,先说一下nvme设备的数据结构,首先,内核使用一个nvme_dev结构体来描述一个nvme设备, 一个nvme设备对应一个nvme_dev,nvme_dev如下:

/* nvme设备描述符,描述一个nvme设备 */
struct nvme_dev {
    struct list_head node;
    /* 设备的queue,一个nvme设备至少有2个queue,一个admin queue,一个io queue,实际情况一般都是一个admin queue,多个io queue,并且io queue会与CPU做绑定 */
    struct nvme_queue __rcu **queues;
    /* unsigned short的数组,每个CPU占一个,主要用于存放CPU上绑定的io queue的qid,一个CPU绑定一个queues,一个queues绑定到1到多个CPU上 */
    unsigned short __percpu *io_queue;
    /* ((void __iomem *)dev->bar) + 4096 */
    u32 __iomem *dbs;
    /* 此nvme设备对应的pci dev */
    struct pci_dev *pci_dev;
    /* dma池,主要是以4k为大小的dma块,用于dma分配 */
    struct dma_pool *prp_page_pool;
    /* 也是dma池,但是不是以4k为大小的,是小于4k时使用 */
    struct dma_pool *prp_small_pool;
    /* 实例的id,第一个加入的nvme dev,它的instance为0,第二个加入的nvme,instance为1,也用于做/dev/nvme%d的显示,%d实际就是instance的数值 */
    int instance;
    /* queue的数量, 等于admin queue + io queue */
    unsigned queue_count;
    /* 在线可以使用的queue数量,跟online cpu有关 */
    unsigned online_queues;
    /* 最大的queue id */
    unsigned max_qid;
    /* nvme queue支持的最大cmd数量,为((bar->cap) & 0xffff)或者1024的最小值 */
    int q_depth;
    /* 1 << (((bar->cap) >> 32) & 0xf),应该是每个io queue占用的bar空间 */
    u32 db_stride;
    /*    初始化设置的值
     *    dev->ctrl_config = NVME_CC_ENABLE | NVME_CC_CSS_NVM;
     *    dev->ctrl_config |= (PAGE_SHIFT - 12) << NVME_CC_MPS_SHIFT;
     *    dev->ctrl_config |= NVME_CC_ARB_RR | NVME_CC_SHN_NONE;
     *    dev->ctrl_config |= NVME_CC_IOSQES | NVME_CC_IOCQES;
     */
    u32 ctrl_config;
    /* msix中断所使用的entry,指针表示会使用多个msix中断,使用的中断的个数与io queue对等,多少个io queue就会申请多少个中断
     * 并且让每个io queue的中断尽量分到不同的CPU上运行
     */
    struct msix_entry *entry;
    /* bar的映射地址,默认是映射8192,当io queue过多时,有可能会大于8192 */
    struct nvme_bar __iomem *bar;
    /* 其实就是块设备,一张nvme卡有可能会有多个块设备 */
    struct list_head namespaces;
    /* 对应的在/sys下的结构 */
    struct kref kref;
    /* 对应的字符设备,用于ioctl操作 */
    struct miscdevice miscdev;
    /* 2个work,暂时还不知道什么用 */
    work_func_t reset_workfn;
    struct work_struct reset_work;
    struct work_struct cpu_work;
    /* 这个nvme设备的名字,为nvme%d */
    char name[12];
    /* SN号 */
    char serial[20];
    char model[40];
    char firmware_rev[8];
    /* 这些值都是从nvme盘上获取 */
    u32 max_hw_sectors;
    u32 stripe_size;
    u16 oncs;
    u16 abort_limit;
    u8 vwc;
    u8 initialized;
};

在nvme_dev结构中,最最重要的数据就是nvme_queue,struct nvme_queue用来表示一个nvme的queue,每一个nvme_queue会申请自己的中断,也有自己的中断处理函数,也就是每个nvme_queue在驱动层面是完全独立的.nvme_queue有两种,一种是admin queue,一种是io queue,这两种queue都用struct nvme_queue来描述,而这两种queue的区别如下:

  • admin queue: 用于发送控制命令的queue,所有非io命令都会通过此queue发送给nvme设备,一个nvme设备只有一个admin queue,在nvme_dev中,使用queues[0]来描述.
  • io queue: 用于发送io命令的queue,所有io命令都是通过此queue发送给nvme设备,简单来说读/写操作都是通过io queue发送给nvme设备的,一个nvme设备有一个或多个io queue,每个io queue的中断会绑定到不同的一个或多个CPU上.在nvme_dev中,使用queues[1~N]来描述.

以上说的io命令和非io命令都是nvme命令,比如快层下发一个写request,nvme驱动就会根据此request构造出一个写命令,将这个写命令放入某个io queue中,当controller完成了这个写命令后,会通过此io queue的中断返回完成信息,驱动再将此完成信息返回给块层.明白了两种队列的作用,我们看看具体的数据结构struct nvme_queue

/* nvme的命令队列,其中包括sq和cq。一个nvme设备至少包含两个命令队列
 * 一个是控制命令队列,一个是IO命令队列
 */
struct nvme_queue {
    struct rcu_head r_head;
    struct device *q_dmadev;
    /* 所属的nvme_dev */
    struct nvme_dev *dev;
    /* 中断名字,名字格式为nvme%dq%d,在proc/interrupts可以查看到 */
    char irqname[24];    /* nvme4294967295-65535\0 */
    /* queue的锁,当操作nvme_queue时,需要占用此锁 */
    spinlock_t q_lock;
    /* sq的虚拟地址空间,主机需要发给设备的命令就存在这里面 */
    struct nvme_command *sq_cmds;
    /* cq的虚拟地址空间,设备返回的命令就存在这里面 */
    volatile struct nvme_completion *cqes;
    /* 实际就是sq_cmds的dma地址 */
    dma_addr_t sq_dma_addr;
    /* cq的dma地址,实际就是cqes对应的dma地址,用于dma传输 */
    dma_addr_t cq_dma_addr;
    /* 等待队列,当sq满时,进程会加到此等待队列,等待有空闲的cmd区域 */
    wait_queue_head_t sq_full;
    /* wait queue的一个entry,主要是当cmdinfo满时,会将它放入sq_full,而sq_full最后会通过它,唤醒nvme_thread */
    wait_queue_t sq_cong_wait;
    struct bio_list sq_cong;
    /* iod是读写请求的封装,可以看成是一个bio的封装,此链表有可能为空,比如admin queue就为空 */
    struct list_head iod_bio;
    /* 当前sq_tail位置,是nvme设备上的一个寄存器,告知设备最新的发送命令存在哪,存在于bar空间中 */
    u32 __iomem *q_db;
    /* cq和sq最大能够存放的command数量 */
    u16 q_depth;
    /* 如果是admin queue,那么为0,之后的io queue按分配顺序依次增加,主要用于获取对应的irq entry,因为所有的queue的irq entry是一个数组 */
    u16 cq_vector;
    /* 当完成命令时会更新,当sq_head == sq_tail时表示cmd queue为空 */
    u16 sq_head;
    /* 当有新的命令存放到sq时,sq_tail++,如果sq_tail == q_depth,那么sq_tail会被重新设置为0,并且cq_phase翻转 
     * 实际上就是一个环
     */
    u16 sq
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值