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

news2024/11/25 2:30:27

驱动的加载

驱动加载实际就是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)回去读取这个值,然后判断它需要哪个驱动去做处理.

【文章福利】小编推荐自己的Linux内核技术交流群: 【977878001】整理一些个人觉得比较好得学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100进群领取,额外赠送一份 价值699的内核资料包(含视频教程、电子书、实战项目及代码)

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

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_tail;
    /* 驱动已经处理完成的cmd位置,当cq_head == sq_tail时,表示cmd队列为空,当sq_tail == cq_head - 1时表示cmd队列已满 */
    u16 cq_head;
    /* 此nvme queue在此nvme设备中的queue id 
     * 0: 控制命令队列
     */
    u16 qid;
    /* 初始设为1,主要用于判断命令是否完成,当cqe.status & 1 != cq_phase时,表示命令还没有完成
     * 当每次sq_tail == q_depth时,此值会取反
     */
    u8 cq_phase;
    u8 cqe_seen;
    /* 初始设为1 */
    u8 q_suspended;
    /* CPU亲和性,用于设置此nvme queue能够在哪些CPU上做中断和中断处理 */
    cpumask_var_t cpu_mask;
    struct async_cmd_info cmdinfo;
    /* 实际就是cmdinfo,此包含d_depth个cmdinfo,一个cmdid表示一个cmdinfo,当对应的bit为0时,表示此槽位空闲,为1时表示此槽位存有cmd 
     * 空闲的cmdinfo的默认完成回调函数都是special_completion
     * 其内存结构如下
     *                      d_depth bits                                       d_depth cmdinfo
     *   (每个bit一个cmdid,用于表示此cmdinfo是空闲还是被占用)              (d_depth个struct nvme_cmd_info)
     * |                                                      |                                                   |
     */
    unsigned long cmdid_data[];
};

nvme_queue是nvme驱动最核心的数据结构,它是nvme驱动和nvme设备通信的桥梁,重点也要围绕nvme_queue来说,之前也说过,一个nvme设备有多个nvme_queue(一个admin queue,至少一个io queue),每个nvme_queue是独立的,它们有

  • 自己对应的中断(irq)
  • 自己的submission queue(sq),用于将struct nvme command发送给nvme设备,并且最多能存dev->d_depth个nvme command
  • 自己的completion queue(cq),用于nvme设备将完成的命令信息(struct nvme_completion)发送给host,并且最多能存dev->d_depth个nvme_completion.
  • 自己的cmdinfo,用于描述一个nvme command.(struct nvme_cmd_info)

可以把sq想象成一个struct nvme_command sq[dev->d_depth]的数组,而cq为struct nvme_completion cq[dev->d_depth]的数组.

struct nvme_command主要用于存储一个nvme命令,包括io命令,或者控制命令,当初始化好一个struct nvme_command后,直接将其下发给nvme设备,nvme设备就会根据它来执行对应操作,其结构如下:

struct nvme_command {
    union {
        struct nvme_common_command common;
        struct nvme_rw_command rw;
        struct nvme_identify identify;
        struct nvme_features features;
        struct nvme_create_cq create_cq;
        struct nvme_create_sq create_sq;
        struct nvme_delete_queue delete_queue;
        struct nvme_download_firmware dlfw;
        struct nvme_format_cmd format;
        struct nvme_dsm_cmd dsm;
        struct nvme_abort_cmd abort;
    };
};


struct nvme_format_cmd {
    __u8            opcode;
    __u8            flags;
    __u16            command_id;  
    __le32            nsid;
    __u64            rsvd2[4];
    __le32            cdw10;
    __u32            rsvd11[5];
};

联合体里面就是nvme支持的所有种类的命令,我随便取了一个nvme_format_cmd,可以看看里面的变量,只要将这些变量设置正确,传给nvme设备,nvme是能够执行这个命令的.

再看看struct nvme_completion,它用于描述完成的命令

struct nvme_completion {
    __le32    result;        /* Used by admin commands to return data */
    __u32    rsvd;
    __le16    sq_head;    /* how much of this queue may be reclaimed */
    __le16    sq_id;        /* submission queue that generated this entry */
    __u16    command_id;    /* of the command which completed */
    __le16    status;        /* did the command fail, and if so, why? */
};

按之前说的,我们把sq和cq想象成两个数组,比如驱动之前将一个nvme_format_cmd放到了sq[10]中,设备对这个nvme_format_cmd命令做了处理,这时候设备就会返回一个nvme_completion,并且把这个nvme_completion放入到cq[6](这里的index为6是假设,实际上我认为一个nvme_command对应一个nvme_completion,如果这个假设成立的话,正常情况这里应该也是为10),并且产生一个中断,在nvme queue的中断处理中,会获取到这个nvme_completion,并通过nvme_completion->sq_head就能够获取到sq[10]中的nvme_format_cmd.这样sq和cq就能够完全联系起来了.

对于驱动来说,一个命令应该是由两部分组成:

  1. 命令的格式,要通过怎样的格式发送给硬件,硬件能够识别.
  2. 命令的额外信息.

对于第一点,实际上就是nvme_command来做,而对于第二点,就需要用nvme_cmd_info来保存了,nvme_cmd_info也是一个数组,根据d_depth来分配长度(因为sq和cq都是根据d_depth来分配长度),并且nvme_queue还会维护一个nvme_cmd_info的used_bitmap,用来表示哪个nvme_cmd_info数组中哪个cmd_info已经被占用,nvme_cmd_info如下:

struct nvme_cmd_info {
    nvme_completion_fn fn;    // 命令完成后的回调函数
    void *ctx;                // 命令的信息,不同命令使用不同结构来描述,所以这里只提供一个指针
    unsigned long timeout;    // 命令允许的超时时间
    int aborted;            // 命令是否作废
};

现在来说说nvme驱动怎么把nvme_command,nvme_completion和nvme_cmd_info联系起来,以上面的nvme_format_cmd为例,假设nvme驱动要发送一个nvme_format_cmd命令,那么先会从nvme_cmd_info的used_bitmap中获取一个空闲的nvme_cmd_info(包括这个cmd_info对应的index,实际就是nvme_cmd_info的数组下标,也称为cmdid),然后根据nvme_format_cmd驱动需要做的事情和信息,来初始化这个nvme_cmd_info,将nvme_format_cmd中的command_id设置为cmdid,发送nvme_format_cmd给nvme设备,nvme设备处理完毕后,发送nvme_format_cmd对应的nvme_completion给host,host获取到此nvme_comletion,从command_id中获取到cmdid,根据获取到的cmdid就能够获取到对应的nvme_cmd_info了.也就是说,在将命令发送给nvme设备时,要将cmd_info对应的cmd_id也一并传下去,之后命令返回时,nvme设备也会将这个cmd_id传回来,这样就能够将三者对应联系起来了.

nvme设备初始化

之前也说了,nvme驱动加载好后,如果有新的nvme设备加入,那么会通过nvme_probe来初始化这个nvme设备,我们先看看nvme_probe这个函数.

/* 当插入一个nvme设备时,会通过此函数进行nvme设备的初始化 */
static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    int result = -ENOMEM;
    /* nvme设备描述符 */
    struct nvme_dev *dev;

    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;
    /* nvme用的是msi/msix中断,这里应该是是按numa内的CPU个数来分配entry数量,entry是msix_entry */
    dev->entry = kcalloc(num_possible_cpus(), sizeof(*dev->entry),
                                GFP_KERNEL);
    if (!dev->entry)
        goto free;
    /* struct nvme_queue,数量是numa内的CPU个数+1 */
    dev->queues = kcalloc(num_possible_cpus() + 1, sizeof(void *),
                                GFP_KERNEL);
    if (!dev->queues)
        goto free;
    /* unsigned short的数组,每个CPU占一个 */
    dev->io_queue = alloc_percpu(unsigned short);
    if (!dev->io_queue)
        goto free;

    /* 初始化namespace链表 */
    INIT_LIST_HEAD(&dev->namespaces);
    /* reset work的调用函数 */
    dev->reset_workfn = nvme_reset_failed_dev;
    INIT_WORK(&dev->reset_work, nvme_reset_workfn);
    INIT_WORK(&dev->cpu_work, nvme_cpu_workfn);
    dev->pci_dev = pdev;
    pci_set_drvdata(pdev, dev);
    /* 分配一个ID,保存到dev->instance里,实际上第一个加入的nvme设备,它的instance为0,第二个加入的nvme设备,instance为1,以此类推 */
    result = nvme_set_instance(dev);
    if (result)
        goto free;

    /* 主要创建两个dma pool,一个是4k大小(prp list page),一个是256B大小(prp list 256) */
    result = nvme_setup_prp_pools(dev);
    if (result)
        goto release;

    kref_init(&dev->kref);
    /* 1.做bar空间的映射,映射地址存放到nvme_dev->bar 
      * 2.当此设备是系统中第一个加载的nvme设备或者nvme_thread没有启动时,就会启动一个nvme_thread
     * 3.初始化nvme的io queue(主要)
     */
    result = nvme_dev_start(dev);
    if (result) {
        if (result == -EBUSY)
            goto create_cdev;
        goto release_pools;
    }

    /* 分配request queue和disk,执行完此函数后,在/dev/下就有此nvme设备了 */
    result = nvme_dev_add(dev);
    if (result)
        goto shutdown;

 create_cdev:
     /* 这里开始分配一个对应的混杂设备,可以理解为字符设备,主要用于应用层用ioctl接口来操作此nvme设备 
      * 这个字符设备的名字为nvme%d
      */
    scnprintf(dev->name, sizeof(dev->name), "nvme%d", dev->instance);
    dev->miscdev.minor = MISC_DYNAMIC_MINOR;
    dev->miscdev.parent = &pdev->dev;
    dev->miscdev.name = dev->name;
    dev->miscdev.fops = &nvme_dev_fops;
    result = misc_register(&dev->miscdev);
    if (result)
        goto remove;

    dev->initialized = 1;
    return 0;

 remove:
    nvme_dev_remove(dev);
    nvme_free_namespaces(dev);
 shutdown:
    nvme_dev_shutdown(dev);
 release_pools:
    nvme_free_queues(dev, 0);
    nvme_release_prp_pools(dev);
 release:
    nvme_release_instance(dev);
 free:
    free_percpu(dev->io_queue);
    kfree(dev->queues);
    kfree(dev->entry);
    kfree(dev);
    return result;
}

nvme_probe函数主要做如下几件事情:

  1. 为中断创建msi/msix的entry,按CPU的数量进行entry的分配,为什么要按照CPU数量进行分配,因为每个io queue会占用一个.而整个系统io queue最大值也就是possible_cpus.
  2. 分配possible个cpus+1的queue结构体,possible应该是系统最大能够插入的cpu核个数,其不等于online_cpus,注意这里是possible_cpus+1,而中断的msi/msix的entry个数为possible_cpus,而每个queue会用一个entry,这样不是就会导致有一个queue是没有entry用的吗?实际上admin queue和第一个io queue会共用entry0.
  3. 分配instance,实际上就是一个nvme id,从0开始依次递增.
  4. 分配两个dma pool,一个pool中的元素大小为4k,一个是256B,这两个pool都是用于数据传输时做dma分配用的.
  5. 调用nvme_dev_start和nvme_dev_add,这两个是主要函数,之后重点看这两个函数.

nvme_dev_start和nvme_dev_add是负责不同的初始化,简单点说,nvme_dev_start是将硬件和驱动的联系进行初始化,当nvme_dev_start执行完成后,此nvme设备实际已经能够通过驱动正常使用了,但实际操作系统还是无法使用此设备,原因是需要nvme_dev_add函数将此设备注册到操作系统中,实际就是注册对应的gendisk和request queue,这样在/dev/和操作系统中都能过对此nvme设备进行操作.

nvme_dev_start

nvme_dev_start函数主要是做硬件方面与驱动方面的传输通道的初始化和硬件的一些初始化,实际主要就是建立admin queue和io queue,并且为这些queue绑定到各自的irq上.

/* 1.做bar空间的映射,映射地址存放到nvme_dev->bar 
 * 2.当此设备是系统中第一个加载的nvme设备或者nvme_thread没有启动时,就会启动一个nvme_thread
 * 3.初始化nvme的io queue
 */
static int nvme_dev_start(struct nvme_dev *dev)
{
    int result;
    bool start_thread = false;

    /* 主要做bar空间的映射,映射地址存放到nvme_dev->bar,并且从bar空间获取nvme设备的d_queue,d_queue是queue中允许的最大cmd数量 */
    result = nvme_dev_map(dev);
    if (result)
        return result;

    /* 初始化控制命令队列,中断处理函数为nvme_irq */
    result = nvme_configure_admin_queue(dev);
    if (result)
        goto unmap;

    spin_lock(&dev_list_lock);
    /* 当此设备是系统中第一个加载的nvme设备或者nvme_thread没有启动时,就会启动一个nvme_thread */
    if (list_empty(&dev_list) && IS_ERR_OR_NULL(nvme_thread)) {
        start_thread = true;
        nvme_thread = NULL;
    }
    list_add(&dev->node, &dev_list);
    spin_unlock(&dev_list_lock);

    if (start_thread) {
        /* 在此nvme设备的加载上下文中创建nvme_thread */
        nvme_thread = kthread_run(nvme_kthread, NULL, "nvme");
        wake_up(&nvme_kthread_wait);
    } else
        /* 非创建nvme_thread的nvme设备就会在这里等待nvme_thread创建完成 */
        wait_event_killable(nvme_kthread_wait, nvme_thread);

    if (IS_ERR_OR_NULL(nvme_thread)) {
        result = nvme_thread ? PTR_ERR(nvme_thread) : -EINTR;
        goto disable;
    }

    /* 初始化nvme的io queue,此为nvme_queue,一个nvme设备至少一个admin queue,一个io queue */
    result = nvme_setup_io_queues(dev);
    if (result && result != -EBUSY)
        goto disable;

    return result;

 disable:
    nvme_disable_queue(dev, 0);
    nvme_dev_list_remove(dev);
 unmap:
    nvme_dev_unmap(dev);
    return result;
}

需要注意,d_queue默认是1024,驱动会通过此nvme设备的pci bar空间获取到设备支持的d_queue,并取两者的最小值作为此设备所有queue的d_queue,d_queue是queue中允许存放的cmd数量最大值.

d_queue获取到后,第一件事情是初始化admin queue,使用nvme_configure_admin_queue:

/* 初始化控制命令队列,中断处理函数为nvme_irq */
static int nvme_configure_admin_queue(struct nvme_dev *dev)
{
    int result;
    u32 aqa;
    u64 cap = readq(&dev->bar->cap);
    struct nvme_queue *nvmeq;

    /* 应该是告诉nvme设备禁止操作 
     * 实现方法是对bar空间的NVME_CC_ENABLEbit做操作,因为当前还没有做irq分配和使用,只能通过寄存器的方法做设置
     */
    result = nvme_disable_ctrl(dev, cap);
    if (result < 0)
        return result;

    /* 获取qid为0的nvme queue,实际上就是admin queue */
    nvmeq = raw_nvmeq(dev, 0);
    /* 如果不存在,则分配一个nvme queue的内存空间用于admin queue(qid 0) */
    /* 主要分配cq和sq的dma空间,大小为depth*(struct nvme_completion),depth*(struct nvme_command) 
     * 注意sq和cq的dma空间都必须使用dma_alloc_coherent来分配
     */
    if (!nvmeq) {
        nvmeq = nvme_alloc_queue(dev, 0, 64, 0);
        if (!nvmeq)
            return -ENOMEM;
    }

    aqa = nvmeq->q_depth - 1;
    aqa |= aqa << 16;

    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;

    /* 初始化sq和cq */
    writel(aqa, &dev->bar->aqa);
    writeq(nvmeq->sq_dma_addr, &dev->bar->asq);
    writeq(nvmeq->cq_dma_addr, &dev->bar->acq);
    writel(dev->ctrl_config, &dev->bar->cc);

    /* 应该是告诉nvme设备使能操作 */
    result = nvme_enable_ctrl(dev, cap);
    if (result)
        return result;

    /* 分配中断,这里主要分配cq的中断,中断处理函数为nvme_irq */
    result = queue_request_irq(dev, nvmeq, nvmeq->irqname);
    if (result)
        return result;

    spin_lock_irq(&nvmeq->q_lock);
    /* 初始化cq和sq */
    nvme_init_queue(nvmeq, 0);
    spin_unlock_irq(&nvmeq->q_lock);
    return result;
}


/* 分配cq和sq的dma空间,大小为depth*(struct nvme_completion),depth*(struct nvme_command) */
static struct nvme_queue *nvme_alloc_queue(struct nvme_dev *dev, int qid,
                            int depth, int vector)
{
    struct device *dmadev = &dev->pci_dev->dev;
    unsigned extra = nvme_queue_extra(depth);
    struct nvme_queue *nvmeq = kzalloc(sizeof(*nvmeq) + extra, GFP_KERNEL);
    if (!nvmeq)
        return NULL;

    /* cq的dma区域,存放completion cmd的地方 */
    nvmeq->cqes = dma_alloc_coherent(dmadev, CQ_SIZE(depth),
                    &nvmeq->cq_dma_addr, GFP_KERNEL);
    if (!nvmeq->cqes)
        goto free_nvmeq;
    memset((void *)nvmeq->cqes, 0, CQ_SIZE(depth));

    /* sq的dma区域,存放submission cmd的地方 */
    nvmeq->sq_cmds = dma_alloc_coherent(dmadev, SQ_SIZE(depth),
                    &nvmeq->sq_dma_addr, GFP_KERNEL);
    if (!nvmeq->sq_cmds)
        goto free_cqdma;

    if (qid && !zalloc_cpumask_var(&nvmeq->cpu_mask, GFP_KERNEL))
        goto free_sqdma;

    nvmeq->q_dmadev = dmadev;
    nvmeq->dev = dev;
    snprintf(nvmeq->irqname, sizeof(nvmeq->irqname), "nvme%dq%d",
            dev->instance, qid);
    spin_lock_init(&nvmeq->q_lock);
    nvmeq->cq_head = 0;
    nvmeq->cq_phase = 1;
    /* 当sq中的cmdinfo满时,会将进程加入到此waitqueue做等待 */
    init_waitqueue_head(&nvmeq->sq_full);
    /* sq_cong_wait是用于加入到sq_full,当sq_full唤醒sq_cong_wait时,实际上是唤醒了nvme_thread */
    init_waitqueue_entry(&nvmeq->sq_cong_wait, nvme_thread);
    bio_list_init(&nvmeq->sq_cong);
    INIT_LIST_HEAD(&nvmeq->iod_bio);
    /* 当前sq_tail位置,是nvme设备上的一个寄存器,存在于bar空间中 
     * 发送命令流程: cmd放入sq_cmds,sq_head++,更新sq_head到此q_db,nvme设置会感知到,然后dma sq cmds,并处理sq cmd.
     */
    nvmeq->q_db = &dev->dbs[qid * 2 * dev->db_stride];
    /* 1024或者nvme设备支持的最大值 */
    nvmeq->q_depth = depth;
    /* admin queue为0,io queue从0~io queue count */
    nvmeq->cq_vector = vector;
    /* queue id, admin queue为0, io queue为1~ io_queue_count+1 */
    nvmeq->qid = qid;
    nvmeq->q_suspended = 1;
    /* nvme设备的queue_count++ */
    dev->queue_count++;
    rcu_assign_pointer(dev->queues[qid], nvmeq);

    return nvmeq;

 free_sqdma:
    dma_free_coherent(dmadev, SQ_SIZE(depth), (void *)nvmeq->sq_cmds,
                            nvmeq->sq_dma_addr);
 free_cqdma:
    dma_free_coherent(dmadev, CQ_SIZE(depth), (void *)nvmeq->cqes,
                            nvmeq->cq_dma_addr);
 free_nvmeq:
    kfree(nvmeq);
    return NULL;
}



/* 初始化cq和sq */
static void nvme_init_queue(struct nvme_queue *nvmeq, u16 qid)
{
    struct nvme_dev *dev = nvmeq->dev;
    /* 大部分情况都是0 */
    unsigned extra = nvme_queue_extra(nvmeq->q_depth);

    nvmeq->sq_tail = 0;
    nvmeq->cq_head = 0;
    nvmeq->cq_phase = 1;
    nvmeq->q_db = &dev->dbs[qid * 2 * dev->db_stride];
    memset(nvmeq->cmdid_data, 0, extra);
    memset((void *)nvmeq->cqes, 0, CQ_SIZE(nvmeq->q_depth));
    /* 告诉设备取消处理当前设备中的io请求 */
    nvme_cancel_ios(nvmeq, false);
    nvmeq->q_suspended = 0;
    dev->online_queues++;
}

到这里admin queue已经初始化完成,可以通过对admin queue发送nvme控制命令来操作nvme设置.admin queue初始化完成后的结果如下:

  • qid为0就是admin queue,并且nvme_dev->queues[0]就是admin queue.
  • nvme_dev->entrys[0]是admin queue使用的.

admin queue初始化完成后,创建nvme_thread,此内核线程不会在初始化流程中使用,暂时先不看,接下来就是初始化io queue了.

初始化io queue是nvme_setup_io_queue函数

/* 初始化nvme设备的所有io queue */
static int nvme_setup_io_queues(struct nvme_dev *dev)
{
    struct nvme_queue *adminq = raw_nvmeq(dev, 0);
    struct pci_dev *pdev = dev->pci_dev;
    int result, i, vecs, nr_io_queues, size;

    /* 以CPU个数来分配io queue */
    nr_io_queues = num_possible_cpus();
    /* 此函数用于设置controller支持的io queue数量(通过发送NVME_FEAT_NUM_QUEUES命令),nvme driver最优的结果是cpus个数个io queue
     * 在服务器上nvme设备肯定不会支持那么多io queue,所以设置时controller最多只会设置自己支持的io queue,并返回自己支持的io queue个数
     * 最后我们选择最小的那个数作为io queue个数,因为也有可能CPU很少,controller支持的io queue很多
     */
    result = set_queue_count(dev, nr_io_queues);
    if (result < 0)
        return result;
    if (result < nr_io_queues)
        nr_io_queues = result;

    /* 4096 + ((nr_io_queues + 1) * 8 * dev->db_stride) */
    size = db_bar_size(dev, nr_io_queues);
    /* size过大,重新映射bar空间 */
    if (size > 8192) {
        iounmap(dev->bar);
        do {
            dev->bar = ioremap(pci_resource_start(pdev, 0), size);
            if (dev->bar)
                break;
            if (!--nr_io_queues)
                return -ENOMEM;
            size = db_bar_size(dev, nr_io_queues);
        } while (1);
        dev->dbs = ((void __iomem *)dev->bar) + 4096;
        adminq->q_db = dev->dbs;
    }

    /* Deregister the admin queue's interrupt */
    /* 释放admin queue的irq */
    free_irq(dev->entry[0].vector, adminq);

    for (i = 0; i < nr_io_queues; i++)
        dev->entry[i].entry = i;
    /* 每个io queue申请一个msix,如果不支持msix,则用msi */
    vecs = pci_enable_msix_range(pdev, dev->entry, 1, nr_io_queues);
    if (vecs < 0) {
        vecs = pci_enable_msi_range(pdev, 1, min(nr_io_queues, 32));
        if (vecs < 0) {
            vecs = 1;
        } else {
            for (i = 0; i < vecs; i++)
                dev->entry[i].vector = i + pdev->irq;
        }
    }

    /*
     * Should investigate if there's a performance win from allocating
     * more queues than interrupt vectors; it might allow the submission
     * path to scale better, even if the receive path is limited by the
     * number of interrupts.
     */
    nr_io_queues = vecs;
    dev->max_qid = nr_io_queues;

    /* 重新分配admin queue的irq */
    result = queue_request_irq(dev, adminq, adminq->irqname);
    if (result) {
        adminq->q_suspended = 1;
        goto free_queues;
    }

    /* Free previously allocated queues that are no longer usable */
    /* 释放多余的io queue */
    nvme_free_queues(dev, nr_io_queues + 1);
    /* 分配io queue需要的内存,并且分配对应的irq,最后设置CPU亲和性 */
    nvme_assign_io_queues(dev);

    return 0;

 free_queues:
    nvme_free_queues(dev, 1);
    return result;
}



/* 分配一个nvme queue,包括其需要的CQ和SQ空间和DMA地址 */
/* 通过admin queue告知nvme设备创建cq和sq,并且分配对应的irq */
static void nvme_create_io_queues(struct nvme_dev *dev)
{
    unsigned i, max;

    max = min(dev->max_qid, num_online_cpus());
    for (i = dev->queue_count; i <= max; i++)
        /* 分配一个nvme queue,包括其需要的CQ和SQ空间和DMA地址,注意这里第一个io queue使用的entry是0,也就是和admin queue共用 */
        if (!nvme_alloc_queue(dev, i, dev->q_depth, i - 1))
            break;

    max = min(dev->queue_count - 1, num_online_cpus());
    for (i = dev->online_queues; i <= max; i++)
        /* 通过admin queue告知nvme设备创建cq和sq,并且分配对应的irq */
        if (nvme_create_queue(raw_nvmeq(dev, i), i))
            break;
}


static int nvme_create_queue(struct nvme_queue *nvmeq, int qid)
{
    struct nvme_dev *dev = nvmeq->dev;
    int result;

    /* 通过admin queue将nvme_admin_create_cq命令发送给nvme设备,主要将当前queue的cq_dma地址和qid传给nvme设备,这样就能将cq关联起来 */
    result = adapter_alloc_cq(dev, qid, nvmeq);
    if (result < 0)
        return result;

    /* 通过admin queue将nvme_admin_create_sq命令发送给nvme设备,主要将当前queue的sq_dma地址和qid传给nvme设备,这样就能将sq关联起来 */
    result = adapter_alloc_sq(dev, qid, nvmeq);
    if (result < 0)
        goto release_cq;

    /* 为此queue创建一个irq */
    result = queue_request_irq(dev, nvmeq, nvmeq->irqname);
    if (result < 0)
        goto release_sq;

    spin_lock_irq(&nvmeq->q_lock);
    nvme_init_queue(nvmeq, qid);
    spin_unlock_irq(&nvmeq->q_lock);

    return result;

 release_sq:
    adapter_delete_sq(dev, qid);
 release_cq:
    adapter_delete_cq(dev, qid);
    return result;
}



/* 分配io queue需要的内存,并且分配对应的irq,最后设置CPU亲和性 */
static void nvme_assign_io_queues(struct nvme_dev *dev)
{
    unsigned cpu, cpus_per_queue, queues, remainder, i;
    cpumask_var_t unassigned_cpus;

    /* 分配一个nvme queue,包括其需要的CQ和SQ空间和DMA地址 */
    /* 告知nvme设备创建cq和sq,并且分配对应的irq */
    nvme_create_io_queues(dev);

    /* 获取queue的数量,其至少<=CPUS */
    queues = min(dev->online_queues - 1, num_online_cpus());
    if (!queues)
        return;

    /* 计算每个io queue的中断可以绑定到多少个CPU上,结果>=1 */
    cpus_per_queue = num_online_cpus() / queues;
    /* 剩余的CPU个数,因为queues <= cpus,当queues < cpus时,那么必然有一些queues绑定的cpus比其他的少一个,具体看下面的代码 */
    remainder = queues - (num_online_cpus() - queues * cpus_per_queue);
    if (!alloc_cpumask_var(&unassigned_cpus, GFP_KERNEL))
        return;

    /* 将所有可用的CPU的mask复制到unassigned_cpus */
    cpumask_copy(unassigned_cpus, cpu_online_mask);
    /* 获取第一个可用的CPU */
    cpu = cpumask_first(unassigned_cpus);
    /* 遍历所有的io queue,从1开始是因为0是admin queue */
    for (i = 1; i <= queues; i++) {
        /* 根据获取对应的io queue */
        struct nvme_queue *nvmeq = lock_nvmeq(dev, i);
        cpumask_t mask;

        /* 清除此io queue的cpumask */
        cpumask_clear(nvmeq->cpu_mask);
        /* 如果unassigned_cpus为0,表示没有CPU可以使用,则退出,之后会初始化nvme dev失败 */
        if (!cpumask_weight(unassigned_cpus)) {
            unlock_nvmeq(nvmeq);
            break;
        }

        /* 根据CPU ID.获取此CPU的cpumask */
        mask = *get_cpu_mask(cpu);
        /* 设置此io queue使用此CPU */
        nvme_set_queue_cpus(&mask, nvmeq, cpus_per_queue);
        /* 如果绑定的CPU个数少于cpus_per_queue,那么先绑定此CPU对应的超线程的其他CPU */
        if (cpus_weight(mask) < cpus_per_queue)
            nvme_add_cpus(&mask, unassigned_cpus,
                topology_thread_cpumask(cpu),
                nvmeq, cpus_per_queue);

        /* 如果绑定的CPU个数还少于cpus_per_queue,那么绑定此CPU对应的同一个socket上其他CPU */
        if (cpus_weight(mask) < cpus_per_queue)
            nvme_add_cpus(&mask, unassigned_cpus,
                topology_core_cpumask(cpu),
                nvmeq, cpus_per_queue);

        /* 如果绑定的CPU个数还少于cpus_per_queue,那么绑定此CPU对应的node上的所有CPU */
        if (cpus_weight(mask) < cpus_per_queue)
            nvme_add_cpus(&mask, unassigned_cpus,
                cpumask_of_node(cpu_to_node(cpu)),
                nvmeq, cpus_per_queue);

        /* 如果绑定的CPU个数还少于cpus_per_queue,那么绑定此CPU对应的node最近的node上的所有CPU */
        if (cpus_weight(mask) < cpus_per_queue)
            nvme_add_cpus(&mask, unassigned_cpus,
                cpumask_of_node(
                    nvme_find_closest_node(
                        cpu_to_node(cpu))),
                nvmeq, cpus_per_queue);

        /* 如果绑定的CPU个数还少于cpus_per_queue,那么绑定所有可用的CPU */
        if (cpus_weight(mask) < cpus_per_queue)
            nvme_add_cpus(&mask, unassigned_cpus,
                unassigned_cpus,
                nvmeq, cpus_per_queue);

        WARN(cpumask_weight(nvmeq->cpu_mask) != cpus_per_queue,
            "nvme%d qid:%d mis-matched queue-to-cpu assignment\n",
            dev->instance, i);

        /* 到这里,已经获取到了此queue对应绑定的CPU的cpumask,并且哪个CPU绑定哪个queue,已经写到nvme_dev->io_queue */
        
        /* 根据cpumask,设置中断的亲和性 */
        irq_set_affinity_hint(dev->entry[nvmeq->cq_vector].vector,
                            nvmeq->cpu_mask);
        /* 将这些绑定的CPU从unassigned_cpus中删除 */
        cpumask_andnot(unassigned_cpus, unassigned_cpus,
                        nvmeq->cpu_mask);
        /* cpu += 1 */
        cpu = cpumask_next(cpu, unassigned_cpus);
        /* 如果此时remainder为0了,那么从下一个queue开始,它绑定的cpus+1,也就是下一个及其之后的queue,绑定的CPUS都比之前的多1 */
        if (remainder && !--remainder)
            cpus_per_queue++;
        unlock_nvmeq(nvmeq);
    }
    WARN(cpumask_weight(unassigned_cpus), "nvme%d unassigned online cpus\n",
                                dev->instance);
    i = 0;
    cpumask_andnot(unassigned_cpus, cpu_possible_mask, cpu_online_mask);
    /* 如果还有剩余的可用CPU的情况,那么就从queue1开始依次绑到剩余这些CPU上 */
    for_each_cpu(cpu, unassigned_cpus)
        *per_cpu_ptr(dev->io_queue, cpu) = (i++ % queues) + 1;
    free_cpumask_var(unassigned_cpus);
}

一个nvme设备会有多个io queue,每个io queue会有自己的中断,并且nvme设备会将每个io queue的中断绑定到不同的CPU上(实际上并不是真正的做了绑定,注意irq_set_affinity_hint这个函数,它实际上是告知使用者,这个irq更适合在哪些CPU上做处理,但是kernel还是有可能将这个IRQ放到不属于这些CPU中的CPU去处理,不过如果在用户层使用了irqbalance命令,那么irqbalance会将这个IRQ放到这个函数设置的CPU上去处理.具体可以看/proc/irq中的值就能明白了,它改变的是smp_affinity_hint值,而非smp_affinity),就有了上面的代码.一般情况应该是一个io queue绑定到多个CPU上,比如CPU有16个,io queue有8个,那么io queue[0]绑定到cpu0,1上,io queue[1]绑定到cpu2,3上,依次类推.当io queue初始化完成后,一些需要注意的细节如下:

  • io queue使用的entry是从0开始的,也就是io queue0会与admin queue共用一个entry.
  • nvme_dev->queues是从1开始保存io queue.
  • queue的sq_dma,cq_dma和qid通过admin queue发送给nvme设备,nvme设备会将其做绑定.并且注意,在nvme_alloc_queue时,queue->q_db指向的位置是通过qid计算的,所以实际上,sq_dma,cq_dma,qid和q_db都能过联系起来了.

到这里,admin queue和io queue都初始化完成了,之后就是在块层注册设备的操作.

nvme_add_dev

static int nvme_dev_add(struct nvme_dev *dev)
{
    struct pci_dev *pdev = dev->pci_dev;
    int res;
    unsigned nn, i;
    struct nvme_ns *ns;
    struct nvme_id_ctrl *ctrl;
    struct nvme_id_ns *id_ns;
    void *mem;
    dma_addr_t dma_addr;
    int shift = NVME_CAP_MPSMIN(readq(&dev->bar->cap)) + 12;

    /* 分配一个一致性dma区域,注意大小是8192B,前4096B放盘的信息,后面4096B空闲,之后会使用 */
    mem = dma_alloc_coherent(&pdev->dev, 8192, &dma_addr, GFP_KERNEL);
    if (!mem)
        return -ENOMEM;

    /* 向controller发送一个identify命令,此命令会让controller将nvme卡的信息保存到mem这块一致性dma区域中 */
    res = nvme_identify(dev, 0, 1, dma_addr);
    if (res) {
        dev_err(&pdev->dev, "Identify Controller failed (%d)\n", res);
        res = -EIO;
        goto out;
    }

    /* 已经获取到信息,包括sn号,model,fw版本,用户可用容量等信息,注意,nn是表示此nvme物理盘生成多少个块设备 */
    ctrl = mem;
    /* 决定了生成多少个块设备 */
    nn = le32_to_cpup(&ctrl->nn);
    dev->oncs = le16_to_cpup(&ctrl->oncs);
    dev->abort_limit = ctrl->acl + 1;
    dev->vwc = ctrl->vwc;
    memcpy(dev->serial, ctrl->sn, sizeof(ctrl->sn));
    memcpy(dev->model, ctrl->mn, sizeof(ctrl->mn));
    memcpy(dev->firmware_rev, ctrl->fr, sizeof(ctrl->fr));
    if (ctrl->mdts)
        dev->max_hw_sectors = 1 << (ctrl->mdts + shift - 9);
    if ((pdev->vendor == PCI_VENDOR_ID_INTEL) &&
            (pdev->device == 0x0953) && ctrl->vs[3])
        dev->stripe_size = 1 << (ctrl->vs[3] + shift);

    id_ns = mem;
    for (i = 1; i <= nn; i++) {
        res = nvme_identify(dev, i, 0, dma_addr);
        if (res)
            continue;

        if (id_ns->ncap == 0)
            continue;

        /* 通过admin queue获取设备盘容量,lba大小等信息,存放到mem的后4096B中 */
        res = nvme_get_features(dev, NVME_FEAT_LBA_RANGE, i,
                            dma_addr + 4096, NULL);
        if (res)
            memset(mem + 4096, 0, 4096);

        /* 分配disk和request queue,一个块设备就是一个namespace */
        ns = nvme_alloc_ns(dev, i, mem, mem + 4096);
        /* 加入到nvme_dev->namespace链表中 */
        if (ns)
            list_add_tail(&ns->list, &dev->namespaces);
    }
    /* 将disk添加到系统中,这样用户就能在/dev/下面看到了 */
    list_for_each_entry(ns, &dev->namespaces, list)
        add_disk(ns->disk);
    res = 0;

 out:
    dma_free_coherent(&dev->pci_dev->dev, 8192, mem, dma_addr);
    return res;
}

此函数主要做几件事情:

  1. 获取nvme设备的信息.
  2. 根据nvme设备的信息,创建对应的namespace,一个namespace实际就是一个块设备
  3. 将创建的namespace加入到系统中的块设备中.

主要是通过nvme_alloc_ns函数来初始化一个namespace,一个namespace是一个块设备,一个块设备主要初始化两个结构,一个是gendisk,一个是request queue,当两个结构都初始化好后,调用add_disk()函数,这个块设备就会正式加入到系统中的块设备中.

static struct nvme_ns *nvme_alloc_ns(struct nvme_dev *dev, unsigned nsid,
            struct nvme_id_ns *id, struct nvme_lba_range_type *rt)
{
    struct nvme_ns *ns;
    struct gendisk *disk;
    int lbaf;

    if (rt->attributes & NVME_LBART_ATTRIB_HIDE)
        return NULL;

    ns = kzalloc(sizeof(*ns), GFP_KERNEL);
    if (!ns)
        return NULL;
    /* 分配一个request queue */
    ns->queue = blk_alloc_queue(GFP_KERNEL);
    if (!ns->queue)
        goto out_free_ns;
    ns->queue->queue_flags = QUEUE_FLAG_DEFAULT;
    /* 禁止合并操作,包括bio合并到request操作,两个request合并操作 */
    queue_flag_set_unlocked(QUEUE_FLAG_NOMERGES, ns->queue);
    /* 表示是一个ssd设备 */
    queue_flag_set_unlocked(QUEUE_FLAG_NONROT, ns->queue);
    queue_flag_clear_unlocked(QUEUE_FLAG_ADD_RANDOM, ns->queue);
    /* 绑定request queue的make_request_fn函数到nvme_make_request */
    blk_queue_make_request(ns->queue, nvme_make_request);
    ns->dev = dev;
    ns->queue->queuedata = ns;

    /* 分配一个gendisk结构,gendisk用于描述一个块设备 */
    disk = alloc_disk(0);
    if (!disk)
        goto out_free_queue;
    ns->ns_id = nsid;
    ns->disk = disk;
    lbaf = id->flbas & 0xf;
    ns->lba_shift = id->lbaf[lbaf].ds;
    ns->ms = le16_to_cpu(id->lbaf[lbaf].ms);
    /* 物理sector的大小,用户看到的逻辑sector大小一般是512B,而物理sector大小不同厂商不同定义,可能跟一个nand flash page一样,也可能小于一个nand flash page */
    blk_queue_logical_block_size(ns->queue, 1 << ns->lba_shift);
    /* 设备允许的一次request支持最大sector数量,request中的sector数量不能超过此值 */
    if (dev->max_hw_sectors)
        blk_queue_max_hw_sectors(ns->queue, dev->max_hw_sectors);
    if (dev->vwc & NVME_CTRL_VWC_PRESENT)
        blk_queue_flush(ns->queue, REQ_FLUSH | REQ_FUA);

    disk->major = nvme_major;
    disk->first_minor = 0;
    /* 此块设备的操作函数 */
    disk->fops = &nvme_fops;
    disk->private_data = ns;
    /* 将上面初始化好的request queue与gendisk联系一起 */
    disk->queue = ns->queue;
    disk->driverfs_dev = &dev->pci_dev->dev;
    /* 标记为允许扩展的设备,暂时不清楚什么意思 */
    disk->flags = GENHD_FL_EXT_DEVT;
    /* 在/dev/下显示的名字 */
    sprintf(disk->disk_name, "nvme%dn%d", dev->instance, nsid);
    /* 设置用户可用容量 */
    set_capacity(disk, le64_to_cpup(&id->nsze) << (ns->lba_shift - 9));

    /* 如果此nvme盘支持discard操作,则设置discard的一些初始参数,如discard必须以物理sector大小对齐 */
    if (dev->oncs & NVME_CTRL_ONCS_DSM)
        nvme_config_discard(ns);

    return ns;

 out_free_queue:
    blk_cleanup_queue(ns->queue);
 out_free_ns:
    kfree(ns);
    return NULL;
}

这里主要初始化gendisk和request queue,gendisk用于描述一个块设备,也就是当gendisk初始化好后,并调用add_disk(),就会在/dev/下出现一个此gendisk->name的块设备.而request_queue有什么用呢,注意看gendisk初始化时,会将gendisk->queue设置为一个初始化好的request_queue.对于request_queue,最重要的是初始化一个make_request_fn的函数指针,当有进程对此gendisk对应的块设备进行读写时,最终都会调用到gendisk的request_queue的make_request_fn所指的函数.在nvme驱动中,主要将request_queue的make_request_fn初始化为了nvme_make_request()函数,未来在说nvme设备的读写流程时,会详细说明此函数.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/52520.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

leetcode 746. 使用最小花费爬楼梯

文章目录题目思考代码和注释总结题目 给你一个整数数组 cost &#xff0c;其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用&#xff0c;即可选择向上爬一个或者两个台阶。 你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。 请你计算并返回达到…

Charles抓取接口报文并修改各种参数信息调试

1.首先介绍Charles面板 图上顶部工具栏常用介绍&#xff1a; 1是清除按钮&#xff1a;点击后将清空左侧抓取的接口列表&#xff0c;如果接口太多&#xff0c;可以点击该按钮清空列表&#xff0c;重新发起请求&#xff0c;一目了然&#xff1b; 2.是停止按钮&#xff1a;点击该按…

玩转redis(一)——基础数据结构

文章目录前言安装一、String1.常用命令和操作2.应用场景3.对应业务场景举例二、Hash1.常用命令和操作2.应用场景3.对应业务场景举例优缺点三、List1.常用命令和操作2.应用场景3.对应的业务场景举例四、Set1.常用的命令和操作2.应用场景3.对应的业务场景举例五、Zset1.常用的命令…

总结:SpringBoot内嵌Tomcat原理

一、介绍 一般我们启动web服务都需要单独的去安装tomcat&#xff0c;而Springboot自身却直接整合了Tomcat&#xff0c;什么原理呢&#xff1f; 二、原理 SpringBoot应用只需要引入spring-boot-starter-web中这个依赖&#xff0c;应用程序就默认引入了tomcat依赖&#xff0c;其…

[附源码]SSM计算机毕业设计疫情环境下的酒店管理系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【HAL库】STM32CubeMX开发----STM32F407----ETH+LAN8720A+LWIP----ping通

STM32CubeMX 下载和安装 详细教程 【HAL库】STM32CubeMX开发----STM32F407----目录 LAN8720A数据手册(中文英文) 前言 本次实验以 STM32F407VET6 芯片为MCU&#xff0c;使用 25MHz 外部时钟源。 以太网PHY层芯片为 LAN8720A&#xff0c;由MCU引脚 PA8 提供时钟。 LAN8720A引脚…

springboot整合mybatis实现增删改查

前言 在学习Springboot过程中&#xff0c;整合mybatis框架实现表数据的增删改查&#xff0c;话不多说&#xff0c;开始贴代码&#xff01; Spring Boot提供了一个名为spring-boot-starter-parent的工程&#xff0c;里面已经对各种常用依赖(并非全部)的版本进行了管理&#xff…

基于51单片机智能IC卡电表控制系统

资料编号&#xff1a;201 功能介绍&#xff1a; 采用51单片机作为主控CPU&#xff0c;使用按键进行模拟冲卡&#xff08;模拟缴费冲卡&#xff09;&#xff0c;通过按键来控制当前是否使用电力&#xff0c;并且LCD1602实时显示当前电力可用量剩余多少&#xff0c;当电力余额不…

3. JVM对象创建与内存分配机制

1. JVM对象创建过程详解 对象创建的主要流程 1.1 分配内存空间的方法 指针碰撞&#xff08;默认使用指针碰撞&#xff09; 如果JAVA堆中内存是绝对规整的&#xff0c;所有用过的内存都放在一边&#xff0c;空闲的内存放在另一边&#xff0c;中间放一个指针作为分界点&#xf…

Tableau长期免费使用的方法总结

目录方法一&#xff1a;使用Tableau Public第一步&#xff1a;在Product中点击Tableau Public第二步&#xff1a;向下滚动找到下载链接第三步&#xff1a;进入下载页面点击下载方法二&#xff1a;每一个可下载版本滚动试用14天第一步&#xff1a;**resource** → **support** →…

06【SpringMVC的Restful支持】

文章目录六、SpringMVC的Restful支持6.1 RESTFUL示例&#xff1a;6.2 基于restful风格的url6.3 基于Rest风格的方法6.4 配置HiddenHttpMethodFilter6.5 Restful相关注解六、SpringMVC的Restful支持 REST&#xff08;英文&#xff1a;Representational State Transfer&#xff…

.vcxproj.filters 误删后如何重建

背景&#xff1a; 今天碰到这样一种情况&#xff0c;我在删除这个VS文件夹下的.user文件时&#xff0c;不小心把.vcxproj.filters也删除了。当然为什么删.user呢&#xff0c;因为换电脑了。 删除之后&#xff0c;我发现&#xff1a;我的解决方案目录变成这样了&#xff1a; 对…

基于springboot企业客户信息反馈平台设计与实现的源码+文档

摘 要 网络的广泛应用给生活带来了十分的便利。所以把企业客户信息反馈管理与现在网络相结合&#xff0c;利用java技术建设企业客户信息反馈平台&#xff0c;实现企业客户信息反馈的信息化。则对于进一步提高企业客户信息反馈管理发展&#xff0c;丰富企业客户信息反馈管理经…

数据分析:从界定问题开始做数据分析?

一、引言 “界定问题”是数据分析工作流程的第一步,也是最重要的一步。再怎么强调“界定问题”的重要性都不为过,因为一旦没有把问题界定清楚,后续的工作很可能将会南辕北辙。而如果我们将问题界定清楚,就能针对性的制定解决方案。 1.什么是界定问题 界定问题是一个需求…

黄健翔质疑半自动越位技术?用「技术流」解读卡塔尔世界杯

昨天&#xff0c;喀麦隆3比3塞尔维亚的比赛&#xff0c;黄健翔发微博质疑「半自动越位识别技术」太慢&#xff0c;而且没有考虑观众的需求&#xff0c;严重影响看球的体验&#xff0c;巴西和瑞士的比赛&#xff0c;黄健翔连发三条微博&#xff0c;再次吐槽VAR技术对足球带来的伤…

Qt | QTextCodec类使用详解、GBK和UTF8编码互转、QString的toLocal8bit和toLatin1区别

Qt | QTextCodec类使用详解、GBK和UTF8编码互转、QString的toLocal8bit和toLatin1区别 目录Qt | QTextCodec类使用详解、GBK和UTF8编码互转、QString的toLocal8bit和toLatin1区别1、QTextCodec简介及使用1.1 编码之间的转换1.2 解决中文显示乱码问题2、QString的toLocal8bit和t…

Python——基本数据类型的转换

1、为什么需要左数据类型的转换 2、转换为整形&#xff1a; 浮点类型转换为整形&#xff1a; a 3.14 b int(a) print(b) 浮点类型转换成整形的规则是&#xff1a;保留小数点前面的数&#xff0c;去掉小数点和小数点后面的数。 字符串转换成整形&#xff1b; a 123 b int(a…

PHP怎么实现实时聊天?GatewayWorker+Thinkphp5即时通讯系统实现

PHP怎么实现实时聊天&#xff1f;GatewayWorkerThinkphp5即时通讯系统实现 一、项目介绍 后端技术&#xff1a;thinkphp5fastadmingatewayworker 前端技术&#xff1a;jqueryhtmlcsswebsocket 项目实现了简单的登录、注册功能&#xff0c;会员可进行后台管理&#xff0c;主界…

[OpenCV实战]52 在OpenCV中使用颜色直方图

颜色直方图是一种常见的图像特征&#xff0c;顾名思义颜色直方图就是用来反映图像颜色组成分布的直方图。颜色直方图的横轴表示像素值或像素值范围&#xff0c;纵轴表示该像素值范围内像素点的个数或出现频率。颜色直方图属于计算机视觉中的基础概念&#xff0c;其常常被应用于…

Ceres库中参数理解

1 参数含义 2 参考链接 Modeling Non-linear Least Squares — Ceres Solver (ceres-solver.org) Ceres详解&#xff08;二&#xff09; CostFunction_他人是一面镜子&#xff0c;保持谦虚的态度的博客-程序员宝宝_ceres costfunction - 程序员宝宝 (cxybb.com)