这篇文章紧接上回分解,在nvme_probe函数的最后一步调用nvme_reset_work进行reset操作,nvme_reset_work的主要工作可以概括如下几个步骤:
进入nvme_reset_work函数后先检查NVME_CTRL_RESETTING标志,来确保nvme_reset_work不会被重复进入。
调用nvme_pci_enable
调用nvme_configure_admin_queue
调用nvme_init_queue
调用nvme_alloc_admin_tags
调用nvme_init_identify
调用nvme_setup_io_queues
调用nvme_start_queues/nvme_dev_add之后,接着调用nvme_queue_scan
上篇文章中,我们解析了nvme_configure_admin_queue的内容,本文我们接着介绍nvme_reset_work中的其他函数。
1. 先来看看nvme_init_queue:
static void nvme_init_queue(struct nvme_queue *nvmeq, u16 qid)
{
struct nvme_dev *dev = nvmeq->dev;
spin_lock_irq(&nvmeq->q_lock);
nvmeq->sq_tail = 0;
nvmeq->cq_head = 0;
nvmeq->cq_phase = 1;
nvmeq->q_db = &dev->dbs[qid * 2 * dev->db_stride];
memset((void *)nvmeq->cqes, 0, CQ_SIZE(nvmeq->q_depth));
dev->online_queues++;
spin_unlock_irq(&nvmeq->q_lock);
}
nvme_init_queue做的事情比较简单,就是对之前nvme_configure_admin_queue函数中申请的queue进行初始化操作。在这个过程中,对SQ Tail, CQ Head以及CQ phase变量进行初始化赋值,然后通过q_db指向Doorbell寄存器。
有关SQ、CQ、Phase、Doorbell的详细解释请参考:
2. 看完nvme_init_queue, 我们再接着瞅瞅nvme_alloc_admin_tags:
static int nvme_alloc_admin_tags(struct nvme_dev *dev)
{
if (!dev->ctrl.admin_q) {
dev->admin_tagset.ops = &nvme_mq_admin_ops;
dev->admin_tagset.nr_hw_queues = 1;
dev->admin_tagset.queue_depth = NVME_AQ_BLKMQ_DEPTH - 1;
dev->admin_tagset.timeout = ADMIN_TIMEOUT;
dev->admin_tagset.numa_node = dev_to_node(dev->dev);
dev->admin_tagset.cmd_size = nvme_cmd_size(dev);
dev->admin_tagset.driver_data = dev;
if (blk_mq_alloc_tag_set(&dev->admin_tagset))
return -ENOMEM;
dev->ctrl.admin_q = blk_mq_init_queue(&dev->admin_tagset);
if (IS_ERR(dev->ctrl.admin_q)) {
blk_mq_free_tag_set(&dev->admin_tagset);
return -ENOMEM;
}
if (!blk_get_queue(dev->ctrl.admin_q)) {
nvme_dev_remove_admin(dev);
dev->ctrl.admin_q = NULL;
return -ENODEV;
}
} else
blk_mq_start_stopped_hw_queues(dev->ctrl.admin_q, true);
return 0;
}
这个函数是NVMe设备采用Multi-Queue(MQ)的核心函数,所以在展开解析这个函数之前,我们先聊聊Linux Multi-Queue Block Layer.
如之前NVME文章(NVMe系列专题之二:队列(Queue)管理)中介绍,多队列、原生异步、无锁是NVMe的最大特色,这些为高性能而生的设计迫使Linux Kernel在3.19抛弃了老的单队列Block Layer而转向Multi-Queue Block Layer. 这个Multi-Queue Block Layer的架构直接对应于NVMe的多队列设计,如下图:

所谓的Multi-Queue机制就是在多核CPU的情况下,将不同的block层提交队列分配到不同的CPU核上,以更好的平衡IO的工作负载,大幅提高SSD等存储设备的IO效率。Multi-Queue Block Layer长啥样子呢?画了个图,看一下:

Multi-Queue Block Layer分为两层,Software Queues和Hardware Dispatch Queues.
Softeware Queues是per core的,Queue的数目与协议有关系,比如NVMe协议,可以有最多64K对 IO SQ/CQ。Software Queues层做的事情如上图标识部分。
Hardware Queues数目由底层设备驱动决定,可以1个或者多个。最大支持数目一般会与MSI-X中断最大数目一样,支持2K。设备驱动通过map_queue维护Software Queues和Hardware Queues之间的对接关系。
需要强调一点,Hardware Queues与Software Queues的数目不一定相等,上图1:1 Mapping的情况属于最理想的情况。
到这里,Multi-Queue Block Layer基本理论我们就算回顾完毕了,我回过头来在看看nvme_alloc_admin_tags这个函数。
从上面的代码来看,主要分为三步:
对admin_tagset结构体初始化,在这个过程中特别提一下ops的赋值(后续会用到)。
static struct blk_mq_ops nvme_mq_admin_ops = {
.queue_rq = nvme_queue_rq,
.complete = nvme_complete_rq,
.init_hctx = nvme_admin_init_hctx,
.exit_hctx = nvme_admin_exit_hctx,
.init_request = nvme_admin_init_request,
.timeout = nvme_timeout,
};
接着调用blk_mq_alloc_tag_set分配tag set并与request queue关联,
然后调用blk_mq_init_allocated_queue对hardware queue和software queues进行初始化,并配置两者之间的mapping关系,最后将返回值传递给dev->ctrl.admin_q。
blk_mq_init_allocated_queue调用blk_mq_realloc_hw_ctxs,然调用blk_mq_init_hctx,最后调用set->ops->init_hctx,也就是nvme_admin_init_hctx。
也就是说,blk_mq_init_allocated_queue初始化最终调用的是nvme_admin_init_hctx:
static int nvme_admin_init_hctx(struct blk_mq_hw_ctx *hctx, void *data,
unsigned int hctx_idx)
{
struct nvme_dev *dev = data;
struct nvme_queue *nvmeq = dev->queues[0];
WARN_ON(hctx_idx != 0);
WARN_ON(dev->admin_tagset.tags[0] != hctx->tags);
WARN_ON(nvmeq->tags);
hctx->driver_data = nvmeq;
nvmeq->tags = &dev->admin_tagset.tags[0];
return 0;
}
从上面的code,可以发现,Hardware Queue初始化时,会将nvme_configure_admin_queue函数中申请的NVMe Queue(nvmeq)赋值给Hardware Queue的driver_data. 由此可知,NVMe Queue与Hardware Queue是一一对应的关系,这也是NVMe与Linux Multi-Queue Block Layer默契配合的关键之处。