1. 程式人生 > >linux核心原始碼分析

linux核心原始碼分析

本文基於3.18.3核心的分析,nvme裝置為pcie介面的ssd,其驅動名稱為nvme.ko,驅動程式碼在drivers/block/nvme-core.c.

驅動的載入

  驅動載入實際就是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_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 <