NVMe驱动详解系列_第一部:NVMe驱动初始化与注销-阿里云开发者社区

开发者社区> binarydady> 正文

NVMe驱动详解系列_第一部:NVMe驱动初始化与注销

简介:
+关注继续查看

 

 

 

 

 

 

NVMe驱动详解系列

第一部: NVMe驱动初始化与注销

作者:perftrace@gmail.com

 

 

 

 

 

 

 

 

 

 

 

 

 

1     NVMe驱动详解之一源码和编译

本系列主要针对linux系统中自带的NVMe驱动,进行详细的分析和学习,从而掌握NVMe以及PCI相关知识。文中所使用的源码是linux4.17.2。

需要提醒的是,阅读本系列文章需要一些linux内核模块、pci总线、内核数据结构以及设备驱动模型相关知识,当然作者会尽全力将内容写得简单易懂,使得读者不需要太深奥的知识。大家可以先尝试看如果阅读遇到问题再回去补充知识也可以的,或者直接邮件给我perftrace@gmail.com。

            我们直接进入正题。

开篇我们先来看下源码的位置和编译方法。

1.1     模块源码

            NVMe相关的代码位于内核源码树的drivers/nvme中,我们可以看到主要有两个文件夹一个是host,一个targets.

     其中targets是用于实现将本系统中的nvme设备作为磁盘导出,供其他服务器或者系统使用的功能。而文件夹host是实现NVMe磁盘供本系统自己使用,不会对外提供,这意味着外部系统不能通过网络或者光纤来访问我们的NVMe磁盘。如果配置NVMe target还需要工具nvmetcli工具:

     http://git.infradead.org/users/hch/nvmetcli.git

     我们这个系列主要针对host,关于target将来有机会再做进一步分析。

            所以后续所有文件都是位于drviers/nvme/host中。

1.2     模块诞生

先来看下drviers/nvme/host目录中的Makefile,具体如下。我们发现根据内核中的参数配置,最多会有5个模块。

# SPDX-License-Identifier: GPL-2.0

 

ccflags-y                               += -I$(src)

 

obj-$(CONFIG_NVME_CORE)                 += nvme-core.o

obj-$(CONFIG_BLK_DEV_NVME)              += nvme.o

obj-$(CONFIG_NVME_FABRICS)              += nvme-fabrics.o

obj-$(CONFIG_NVME_RDMA)                 += nvme-rdma.o

obj-$(CONFIG_NVME_FC)                   += nvme-fc.o

 

nvme-core-y                             := core.o

nvme-core-$(CONFIG_TRACING)             += trace.o

nvme-core-$(CONFIG_NVME_MULTIPATH)      += multipath.o

nvme-core-$(CONFIG_NVM)                 += lightnvm.o

nvme-core-$(CONFIG_FAULT_INJECTION_DEBUG_FS)    += fault_inject.o

 

nvme-y                                  += pci.o

nvme-fabrics-y                          += fabrics.o

nvme-rdma-y                             += rdma.o

nvme-fc-y                               += fc.o

            其中ccflags-y是编译标记,会被正常的cc调用,指定了$(CC)编译时候的选项,这里只是将内核源码的头文件包含进去。 其中$(src)是指向内核根目录中Makefile所在的目录,包含模块需要的一些头文件。当然,这里其实我们可以不用纠结或者理睬它。

我们看下决定模块是否编译的5个配置参数:

l   NVME_CORE:这是一个被动的选项。该选项在BLK_DEV_NVME, NVME_RDMA, NVME_FC使能时候会自动选上,是nvme核心基础。对应的代码是core.c,产生的模块是nvme-core.ko。另外。这里需要注意的是,如果使能了配置:TRACING,NVME_MULTIPATH,NVM,FAULT_INJECTION_DEBUG_FS,那么模块nvme-core.ko会合入trace.c,multipath.c,lightnvm.c和fault_inject.c文件,这些是NVMe驱动的特点可选择是否开启。

l   BLK_DEV_NVME:这个选项开启后会自动选上NVME_CORE,同时自身依赖pci和block.这个产生nvme.ko驱动用于直接将ssd链接到pci或者pcie.对应的代码是nvme.c和pci.c,产生的模块是nvme.ko.

l   CONFIG_NVME_FABRICS:是这个被动选项。被NVME_RDMA和NVME_FC选择(当然,还有一些其他条件需要满足)。主要用于支持FC协议。

l   CONFIG_NVME_RDMA:这个驱动使得NVMe over Fabric可以通过RDMA传输(该选项还依赖于CONFIG_INFINIBAND)。该选项会自动使能NVME_CORE和NVME_FABRICS,SG_POOL

l   CONFIG_NVME_FC:这个驱动使得NVMe over Fabric可以在FC传输。该选项会自动使能NVME_CORE和NVME_FABRICS,SG_POOL

 

模块

依赖

源码文件

nvme-core.ko

-

nvme-core.c, trace.c, multipath.c, lightnvm.c, fault_inject.c

nvme.ko

nvme-core

pci.c

nvme-fabirc.ko

-

fabrics.c

nvme-rdma.ko

nvme-core,nvme-fabirc,sp_pool

rdma.c

nvme-fc.ko

nvme-core,nvme-fabirc,sp_pool

fc.c

            配置完毕后,可以在内核代码根目录中执行make命令产生驱动。

#make M=drivers/nvme/host

            编译后会产生所配置的驱动模块,我们本系列只覆盖nvme.ko这个驱动模板,当然另一个nvme-core.ko必须的。编译后可以通过make modules_install来安装。

            然后可以通过modprobe nvme来加载驱动。

2     NVMe驱动详解之二PCI驱动注册

我们现在使用的NVMe设备都是基于PCI的,所以最后设备需要连接到内核中的pci总线上才能够使用。这也是为什么在上篇配置nvme.ko时会需要pci.c文件,在Makefile中有如下这一行的:

nvme-y    += pci.o

本篇主要针对pci.c文件的一部分内容进行分析(其实本系列涉及的代码内容就是在pci.c和nvme-core.c两个文件中)。

2.1     驱动注册上

我们先来看下驱动的注册和注销,其实就是模块的初始化和退出函数,如下。

   module_init(nvme_init);

   module_exit(nvme_exit);

可知模块注册函数是nvme_init,非常简单,就是一个pci_register_driver函数。

static int __init nvme_init(void)

{

        return pci_register_driver(&nvme_driver);

}

            注册了nvme_driver驱动,参数为结构体nvme_driver,该结构体类型是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_dev_pm_ops,

        },

        .sriov_configure = nvme_pci_sriov_configure,

        .err_handler    = &nvme_err_handler,

};

            我们可以从结构体nvme_driver中得知,驱动的名字是nvme;初始化函数是nvme_probe,该函数负责在驱动加载时候探测总线上的硬件设备;设备与驱动的关联表为nvme_id_table,通过这个表内核可以知道哪些设备是通过这个驱动来工作的;probe函数,该函数用来初始化设备;remove函数,当前驱动从内核移除时候被调用;shutdown函数用于关闭设备;错误处理句柄nvme_err_handler;以及nvme的sriov操作函数。

            而pci_register_driver是个宏,其实是__pci_register_driver函数,该函数会通过调用driver_register将要注册的驱动结构体放到系统中设备驱动链表中,将其串成了一串。这里要注意的是pci_driver中包含了device_driver,而我们的驱动nvme_driver就是pci_driver类型。

struct pci_driver {

          struct list_head        node;  

      ……

        struct device_driver    driver;

        struct pci_dynids       dynids;

};

            我们来具体看下这个__pci_register_driver,函数会参数也就是将驱动代码中nvme_driver结构体值赋值给nvme_driver中device_driver这个通用结构体。

int __pci_register_driver(struct pci_driver *drv, struct module *owner,

                          const char *mod_name)

{

        /* initialize common driver fields */

        drv->driver.name = drv->name;//赋值为”nvme”

        drv->driver.bus = &pci_bus_type;//设置为pci_bus_type,是个结构体

        drv->driver.owner = owner;//驱动的拥有者

        drv->driver.mod_name = mod_name;//device_driver中的名字,为系统中的KBUILD_NAME

        drv->driver.groups = drv->groups;//驱动代码中并未赋值

                                          

        spin_lock_init(&drv->dynids.lock);//获取自旋锁

        INIT_LIST_HEAD(&drv->dynids.list);//初始化设备驱动中的节点元素,用于在链表中串起来

 

        /* register with core */

        return driver_register(&drv->driver);//调用driver_register注册驱动。

}

            参数中基本都是比较直白的,唯独pci_bus_type是由充满玄机的,故而把它列出来如下。结构图中定义了很多和总线相关的函数,这些函数其实是由pci总线驱动提供的,位于drivers/pci/pci-driver.c文件,这些内容我们在后续会有说明,这里先让它们露个脸和大家打个照面。

struct bus_type pci_bus_type = {

        .name           = "pci",

        .match          = pci_bus_match,

        .uevent         = pci_uevent,

        .probe          = pci_device_probe,

        .remove         = pci_device_remove,   

        .shutdown       = pci_device_shutdown,

        .dev_groups     = pci_dev_groups,

        .bus_groups     = pci_bus_groups,

        .drv_groups     = pci_drv_groups,

        .pm             = PCI_PM_OPS_PTR,

        .num_vf         = pci_bus_num_vf,

        .force_dma      = true,

};

总之呢,这个pci_register_driver函数主要作用就是传递驱动相关参数,并调用driver_register。接下我们看下driver_register.

2.2     驱动注册中

继续驱动注册,上面讲到driver_register函数。该函数实现驱动注册到总线,参数就是一个需要注册的device_driver。

int driver_register(struct device_driver *drv)

{      

        int ret;

        struct device_driver *other;

 

        BUG_ON(!drv->bus->p);//检测device_driver->driver_private,开始应该是为NULL

 

        if ((drv->bus->probe && drv->probe) ||

            (drv->bus->remove && drv->remove) ||

            (drv->bus->shutdown && drv->shutdown))

                printk(KERN_WARNING "Driver '%s' needs updating - please use "

                        "bus_type methods\n", drv->name);

 

        other = driver_find(drv->name, drv->bus);

        if (other) {

                printk(KERN_ERR "Error: Driver '%s' is already registered, "

                        "aborting...\n", drv->name);

                return -EBUSY;

        }

 

        ret = bus_add_driver(drv);

        if (ret)

                return ret;

        ret = driver_add_groups(drv, drv->groups);

        if (ret) {

                bus_remove_driver(drv);

                return ret;

        }

        kobject_uevent(&drv->p->kobj, KOBJ_ADD);

 

        return ret;

}

函数会先检测device_driver->driver_private,开始应该是为NULL,不然就panic了。

然后判断总线和驱动都是否都定义了probe,remobe,shutdown函数,因为总线中已有这三个函数的定义,所以device_driver中并不需要了,如果出现则打印输出警告。

            接着通过driver_find函数,在需要注册总线上查找是否已经存在相同名字的驱动了,如果存在,那就停止注册。函数会调用kset_find_obj来查找, 传入的参数是要注册的驱动名字和总线结构体。如果找到则返回驱动。

struct device_driver *driver_find(const char *name, struct bus_type *bus)

{

        struct kobject *k = kset_find_obj(bus->p->drivers_kset, name);

        struct driver_private *priv;

 

        if (k) {

                /* Drop reference added by kset_find_obj() */

                kobject_put(k);

                priv = to_driver(k);

                return priv->driver;

        }

        return NULL;

}

            其中bus->p的结构体是subsys_private,其中变量driver_kset是表示和总线相关的驱动,其类型是kset, kset通过其中的list成员组成链表。

            kset_find_obj的函数如下,先获取一个自旋锁,然后在列表中遍历查找,这里用了列表遍历函数list_for_each_entry,每次获取总线中的一个代表驱动的kobject,然后通过kobject_name获得节点中项的名字(驱动名字),然后与要注册的驱动名字对比,如果相等则返回该kobject,否则返回NULL,最后释放自旋锁。其中kset_find_obj函数如下。

struct kobject *kset_find_obj(struct kset *kset, const char *name)

{

        struct kobject *k;

        struct kobject *ret = NULL;

 

        spin_lock(&kset->list_lock);

       

        list_for_each_entry(k, &kset->list, entry) {

                if (kobject_name(k) && !strcmp(kobject_name(k), name)) {

                        ret = kobject_get_unless_zero(k);

                        break;

                }

        }      

               

        spin_unlock(&kset->list_lock);

        return ret;

}

            到此是确定系统中要么已经存在同名驱动退出注册,要么是系统可以继续注册。如果可以继续注册,那么需要继续执行driver_register函数中的代码片段如下,我们在下小节中详解。

        ret = bus_add_driver(drv);

        if (ret)

                return ret;

        ret = driver_add_groups(drv, drv->groups);

        if (ret) {

                bus_remove_driver(drv);

                return ret;

        }

        kobject_uevent(&drv->p->kobj, KOBJ_ADD);

 

2.3     驱动注册下

接下去,才是真正实现将驱动设备添加到总线中的过程。该函数是bus_add_driver。

int bus_add_driver(struct device_driver *drv)

{

        struct bus_type *bus;

        struct driver_private *priv;

        int error = 0;

 

        bus = bus_get(drv->bus);

        if (!bus)

                return -EINVAL;

 

        pr_debug("bus: '%s': add driver %s\n", bus->name, drv->name);

 

        priv = kzalloc(sizeof(*priv), GFP_KERNEL);

        if (!priv) {

                error = -ENOMEM;

                goto out_put_bus;

        }

        klist_init(&priv->klist_devices, NULL, NULL);

        priv->driver = drv;

        drv->p = priv;

        priv->kobj.kset = bus->p->drivers_kset;

        error = kobject_init_and_add(&priv->kobj, &driver_ktype, NULL,

                                     "%s", drv->name);

        if (error)

                goto out_unregister;

 

        klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);

        if (drv->bus->p->drivers_autoprobe) {

                if (driver_allows_async_probing(drv)) {

                        pr_debug("bus: '%s': probing driver %s asynchronously\n",

                                drv->bus->name, drv->name);

                        async_schedule(driver_attach_async, drv);

                } else {

                        error = driver_attach(drv);

                        if (error)

                                goto out_unregister;

                }

        }

        module_add_driver(drv->owner, drv);

 

        error = driver_create_file(drv, &driver_attr_uevent);

        if (error) {

                printk(KERN_ERR "%s: uevent attr (%s) failed\n",

                        __func__, drv->name);

        }

        error = driver_add_groups(drv, bus->drv_groups);

        if (error) {

                /* How the hell do we get out of this pickle? Give up */

                printk(KERN_ERR "%s: driver_create_groups(%s) failed\n",

                        __func__, drv->name);

        }

 

        if (!drv->suppress_bind_attrs) {

                error = add_bind_files(drv);

                if (error) {

                        /* Ditto */

                        printk(KERN_ERR "%s: add_bind_files(%s) failed\n",

                                __func__, drv->name);

                }

        }

 

        return 0;

 

out_unregister:

        kobject_put(&priv->kobj);

        /* drv->p is freed in driver_release()  */

        drv->p = NULL;

out_put_bus:

        bus_put(bus);

        return error;

}

bus_add_driver函数的入参为device_driver,先通过bus_get获取总线,参数为bus_type->private_subsys->kset,是kset类型,最后会将对应的ojbect引用增加1。函数结束会调用bus_put来减少引用。

static struct bus_type *bus_get(struct bus_type *bus)

{

        if (bus) {

                kset_get(&bus->p->subsys);

                return bus;

        }

        return NULL;

 }

然后通过kzalloc(分配的空间都置0)分配结构体driver_private类型变量priv,并调用klist_init初始化设备列表,这里对应的将来驱动所能驱动的设备。

接着填充priv和驱动结构体drv,填充priv->driver=drv,以及drv->p=priv,相互指向。

设置priv->kobj.kset为总线的bus->p->drivers_kset

然后调用kobject_init_and_add函数,该函数初始化一个kojbect结构体,并加入到kobject架构中。其中kobject的对象就是priv->kobj,类型为driver_ktype,

static struct kobj_type driver_ktype = {

        .sysfs_ops      = &driver_sysfs_ops,

        .release        = driver_release,

};

         然后调用klist_add_tail(&priv->knode_bus, &bus->p->klist_drivers);将priv->knode_bus添加到总线的subsys_private->klist_drivers 链表中

            接着判断总线是否存在drivers_autoprobe函数,如果总线存在该函数,进一步则判断是否支持异步探测,主要是判断drv->probe_type变量,调用driver_attach_async或者driver_attach(async_schedule其实就是异步调用driver_attach_async函数),而driver_attach_async最后也是调用driver_attach。作者实测了一下,在pci总线中因为没有设置drv->probe_type为异步探测。函数driver_attach会调用bus_for_each_dev负责在总线上遍历设备,并将设备传递给函数会调用__driver_attach进行设备和驱动的匹配。

我们来看下__driver_attach函数,文件只列出该函数其他函数不再文中列出,大家可以查看源码。该函数其先调用driver_match_device,而driver_match_device则调用总线的match函数,pci总线的就是函数pci_bus_match。pci_bus_match先判断设备中的match_driver变量是否已经设备,如果设备说明已经和驱动匹配则无需匹配;否则调用pci_match_device (这个函数中会先通过override判断是否只绑定到指定驱动),先使用宏list_for_each_entry遍历驱动中动态id,显然后遍历静态id(驱动中的id_table表),如果匹配返回pci_device_id(如果设备设置了dev->override,且注册的驱动名字和设备需要的名字匹配,就算没找到也会也返回一个pci_device_id_any),这里注意的是pci_device_id 中class和classmask合计32位,实际有效的是class的16位,另外16位是为了掩盖pci_device中32位class中无效的16位。

     如果执行driver_match_device出错,并且返回错误是-EPROBE_DEFER,则需要调用driver_deferred_probe_add,来将设备通过dev->p->deferred_probe添加到deferred_probe_pending_list链表中。

     当返回pci_device_id后,如果设备没有绑定驱动, __driver_attach函数会调用driver_probe_device(调用该函数需要先获取设备锁),该函数负责将设备和驱动绑定。函数先判断设备dev->kobj.state_in_sysfs是否注册,然后调用really_probe(这里其实还会涉及linux电源管理的动作,此处为了简化问题暂时不展开)。

really_probe函数中,会设置dev->driver = drv将驱动赋值为设备,同时会调用driver_bound函数,将设备也绑定到驱动相关链表中,此外会调用总线的probe函数,如果总线没有probe函数则调用设备驱动的probe函数,当然really_probe函数中的学问还有很多,可以单独列一篇章来讲解。

static int __driver_attach(struct device *dev, void *data)

{

        struct device_driver *drv = data;

        int ret;

        /*

         * Lock device and try to bind to it. We drop the error

         * here and always return 0, because we need to keep trying

         * to bind to devices and some drivers will return an error

         * simply if it didn't support the device.

         *

         * driver_probe_device() will spit a warning if there

         * is an error.

         */

        ret = driver_match_device(drv, dev);

        if (ret == 0) {

                /* no match */

                return 0;

        } else if (ret == -EPROBE_DEFER) {

                dev_dbg(dev, "Device match requests probe deferral\n");

                driver_deferred_probe_add(dev);

        } else if (ret < 0) {

                dev_dbg(dev, "Bus failed to match device: %d", ret);

                return ret;

        } /* ret > 0 means positive match */

 

        if (dev->parent)        /* Needed for USB */

                device_lock(dev->parent);

        device_lock(dev);

        if (!dev->driver)

                driver_probe_device(drv, dev);

        device_unlock(dev);

        if (dev->parent)

                device_unlock(dev->parent);

 

        return 0;

}

然后bus_add_driver继续调用module_add_driver,该函数主要实现是sysfs_create_link在sysfs文件系统中创建相关文件。

   sysfs_create_link(&drv->p->kobj, &mk->kobj, "module");

   sysfs_create_link(mk->drivers_dir, &drv->p->kobj, driver_name);

            第一个sysfs_create_link调用在/sys/bus/pci/drivers/nvme中创建module指向/sys/module/nvme,第二个sysfs_create_link在目录/sys/module/nvme/drivers中创建pci:nvme链接,指向/sys/bus/pci/drivers/nvme驱动。

接着调用driver_create_file,通过sysfs_create_file函数,

sysfs_create_file(&drv->p->kobj, &attr->attr),在/sys/bus/pci/drivers/nvme中创建驱动的属性文件

然后调用driver_add_groups,通过 sysfs_create_groups函数创建总线中驱动的属性组。

            最后判断drv->suppress_bind_attrs是否设置,如果没有设置则调用add_bind_files创建绑定的属性文件。至此,bus_add_driver运行结束了,非常的繁杂,其函数调用链如下。对于我们掌握NMVe驱动来说是足够了,但是对于想要继续深入的同学其实还有很多细节并没有一一阐述,大家可以自己去hack。

            在driver_register函数的末尾,调用driver_add_groups,也是通过 sysfs_create_groups函数创建NVMe驱动的属性组。之前在bus_add_driver中是总线自己的属性组。

            最后调用kobject_uevent(&drv->p->kobj, KOBJ_ADD);通知用户驱动加载成功。

     总的注册流程图如下:

0cdae31a0274fab6790fa090ff3f0bde60ab5391

       高清图下载地址:

https://github.com/kernel-z/filesystem/blob/master/nvme-init-exit-0.1.png

终于,驱动注册完毕。

这片文章与其说是NVMe驱动注册,不如说是PCI设备驱动的注册。但是不管怎么样,我们终于一步一步的弄清楚了PCI驱动的注册大概流程,下面就看下NVMe驱动注销。

 

 

3     NVMe驱动详解之三PCI驱动注销

承接上篇PCI驱动注册,这篇我们来看下PCI驱动的注销。

3.1     驱动注销上

驱动的注销,其实就是模块的退出函数。

   module_exit(nvme_exit);

驱动注销函数:

static void __exit nvme_exit(void)

{

        pci_unregister_driver(&nvme_driver);

        flush_workqueue(nvme_wq);

        _nvme_check_size();

}

            pci_unregister_driver这个函数就是pci_register_driver的配对函数。注销的动作基本是注册动作的取反,把之前创建的文件分配的资源都进行回收,我们来看下其主要逻辑。

3.2     驱动注销中

pci_unregister_driver函数负责从已注册的pci驱动中注销一个pci驱动结构,同时触发驱动设备的remove函数,最后设置所驱动设备为无驱动状态。它的参数还是nvme驱动数据结构体nvme_driver,它会调用driver_unregister和pci_free_dynids。driver_unregister是和driver_register配对的函数,其参数为结构体device_driver,而pci_free_dynids负责删除驱动结构中drv->dynids.list中的节点,这些节点是动态添加的,所以在注册驱动的时候并不需要创建。

void pci_unregister_driver(struct pci_driver *drv)

{

        driver_unregister(&drv->driver);

        pci_free_dynids(drv);

}

            我们来看下driver_unregister,该函数负责从系统中移除一个驱动,主要通过两个函数driver_remove_groups和bus_remove_driver来实现。

void driver_unregister(struct device_driver *drv)

{

        if (!drv || !drv->p) {

                WARN(1, "Unexpected driver unregister!\n");

                return;

        }      

        driver_remove_groups(drv, drv->groups);

        bus_remove_driver(drv);

}

driver_remove_groups函数调用sysfs_remove_groups,负责从sysfs文件系统中删除驱动的属性组,其实就是一些文件,为了不影响我们的理解,我们这里不深入展开。

3.3     驱动注销下

            我们再来看主要的函数,就是bus_remove_driver,这个函数和bus_add_driver配对。其中调用的函数其实也和bus_add_driver中逆着来的。

void bus_remove_driver(struct device_driver *drv)

{

        if (!drv->bus)

                return;

 

        if (!drv->suppress_bind_attrs)

                remove_bind_files(drv);

        driver_remove_groups(drv, drv->bus->drv_groups);

        driver_remove_file(drv, &driver_attr_uevent);

        klist_remove(&drv->p->knode_bus);

        pr_debug("bus: '%s': remove driver %s\n", drv->bus->name, drv->name);

        driver_detach(drv);

        module_remove_driver(drv);

        kobject_put(&drv->p->kobj);

        bus_put(drv->bus);

}

            我们一个一个来看下这些函数。

remove_bind_files函数取决于驱动中的参数drv->suppress_bind_attrs,如果为0,则调用该函数,因为再加载的时候也是调用了add_bind_files函数。remove_bind_files函数会调用driver_remove_file,然后调用sysfs_remove_file将驱动的属性文件删除。

driver_remove_groups会调用sysfs_remove_groups,最后调用sysfs_remove_group将总线的组属性移除。

driver_remove_file也是调用sysfs_remove_file函数。从sysfs中删除驱动属性文件。

klist_remove函数将驱动从总线中移除。

driver_detach(drv)将驱动管理的所有设备进行取消驱动关联。这个函数相对其他函数会稍微重要一点,所以在文中将其代码列出来如下。

void driver_detach(struct device_driver *drv)

{      

        struct device_private *dev_prv;

        struct device *dev;

       

        for (;;) {

                spin_lock(&drv->p->klist_devices.k_lock);

                if (list_empty(&drv->p->klist_devices.k_list)) {

                        spin_unlock(&drv->p->klist_devices.k_lock);

                        break;

                }

                dev_prv = list_entry(drv->p->klist_devices.k_list.prev,

                                     struct device_private,

                                     knode_driver.n_node);

                dev = dev_prv->device;

                get_device(dev);

                spin_unlock(&drv->p->klist_devices.k_lock);

                device_release_driver_internal(dev, drv, dev->parent);

                put_device(dev);

        }                                        

}

driver_detach函数是一个循环,非常符合逻辑,因为要将相关所有设备进行操作。然后先通过spin_lock获取驱动所管理的设备链的自旋锁,判断链路中是否有设备,如果没有设备则退出循环,并释放自旋锁。否则如果有设备,则在列表(device_driver->p->klist_devices.k_list)中获取device_private结构体,从而获得设备结构体(设备结构体为device_private的成员变量)。然后调用get_device增加设备的引用,并释放自旋锁,然后调用device_release_driver_internal函数。在device_release_driver_internal函数中,需要先锁住设备,然后调用__device_release_driver,这个函数是最终处理场所。在__device_release_driver中会调用driver_sysfs_remove删除sysfs中设备相关属性文件及软连接,最重要的一个点是会调用总线的remove函数,如果没有则调用驱动的remove函数。__device_release_drive函数通过kobject_uevent来通知用户。

最后通过函数put_device释放设备的引用。

module_remove_driver(drv)函数调用sysfs_remove_link函数将sysfs_create_link创建的软连接移除。

kobject_put(&drv->p->kobj)减少驱动的引用,如果已经没有其他引用则释放该结构。

bus_put(drv->bus)减少总线的一次引用,对应加载时候的bug_get函数。

至此,驱动注销结束了,其总的流程图如下:

edc9df7d86bb87869fe75d278c4b402363096fc3

            (高清图下载地址:

https://github.com/kernel-z/filesystem/blob/master/nvme-init-exit-0.1.png

从以上分析来看,注销的过程和注册的过程是基本对立的过程,而且函数很多名字上也是配对的,这对我们理解有很大的帮助。虽然其中还有一些细节展开,但是对我们理解NVMe驱动注销我想已经够了的。

下一篇中,我们会对NVMe驱动所依赖的nvme-core模块进行注册和注销分析。

4     NVMe驱动详解之四nvme-core模块初始化

这个nvme-core模块是nvme模块所依赖的,我们可以在第一篇内核配置中知道,也可以在使用modinfo命令来观察发现。

    # modinfo  nvme

filename:       /lib/modules/4.17.2/kernel/drivers/nvme/host/nvme.ko

version:        1.0

license:        GPL

author:         Matthew Wilcox <willy@linux.intel.com>

srcversion:     727870919D954433442B206

alias:          pci:v0000106Bd00002003sv*sd*bc*sc*i*

alias:          pci:v0000106Bd00002001sv*sd*bc*sc*i*

alias:          pci:v*d*sv*sd*bc01sc08i02*

alias:          pci:v00001D1Dd00002807sv*sd*bc*sc*i*

alias:          pci:v00001D1Dd00001F1Fsv*sd*bc*sc*i*

alias:          pci:v0000144Dd0000A822sv*sd*bc*sc*i*

alias:          pci:v0000144Dd0000A821sv*sd*bc*sc*i*

alias:          pci:v00001C5Fd00000540sv*sd*bc*sc*i*

alias:          pci:v00001C58d00000023sv*sd*bc*sc*i*

alias:          pci:v00001C58d00000003sv*sd*bc*sc*i*

alias:          pci:v00008086d00005845sv*sd*bc*sc*i*

alias:          pci:v00008086d0000F1A5sv*sd*bc*sc*i*

alias:          pci:v00008086d00000A55sv*sd*bc*sc*i*

alias:          pci:v00008086d00000A54sv*sd*bc*sc*i*

alias:          pci:v00008086d00000A53sv*sd*bc*sc*i*

alias:          pci:v00008086d00000953sv*sd*bc*sc*i*

depends:        nvme-core

intree:         Y

name:           nvme

vermagic:       4.17.2 SMP mod_unload

parm:           use_threaded_interrupts:int

parm:           use_cmb_sqes:use controller's memory buffer for I/O SQes (bool)

parm:           max_host_mem_size_mb:Maximum Host Memory Buffer (HMB) size per controller (in MiB) (uint)

parm:           sgl_threshold:Use SGLs when average request segment size is larger or equal to this size. Use 0 to disable SGLs. (uint)

parm:           io_queue_depth:set io queue depth, should >= 2

            这意味着在加载nvme模块之前会提前加载nvme-core模块。

我们来看下nvme-core驱动的注册和注销:

module_init(nvme_core_init);

module_exit(nvme_core_exit);

     这个module_init和module_exit是linux内核模块的标准方式。

4.1     nvme_core_init

nvme_core模块初始化函数如下:

int __init nvme_core_init(void)

{

        int result = -ENOMEM;

 

        nvme_wq = alloc_workqueue("nvme-wq",

                        WQ_UNBOUND | WQ_MEM_RECLAIM | WQ_SYSFS, 0);

        if (!nvme_wq)

                goto out;

 

        nvme_reset_wq = alloc_workqueue("nvme-reset-wq",

                        WQ_UNBOUND | WQ_MEM_RECLAIM | WQ_SYSFS, 0);

        if (!nvme_reset_wq)

                goto destroy_wq;

 

        nvme_delete_wq = alloc_workqueue("nvme-delete-wq",

                        WQ_UNBOUND | WQ_MEM_RECLAIM | WQ_SYSFS, 0);

        if (!nvme_delete_wq)

                goto destroy_reset_wq;

 

        result = alloc_chrdev_region(&nvme_chr_devt, 0, NVME_MINORS, "nvme");

        if (result < 0)

                goto destroy_delete_wq;

 

        nvme_class = class_create(THIS_MODULE, "nvme");

        if (IS_ERR(nvme_class)) {

                result = PTR_ERR(nvme_class);

                goto unregister_chrdev;

        }   

 

        nvme_subsys_class = class_create(THIS_MODULE, "nvme-subsystem");

        if (IS_ERR(nvme_subsys_class)) {

                result = PTR_ERR(nvme_subsys_class);

                goto destroy_class;

        }   

        return 0;

 

destroy_class:

        class_destroy(nvme_class);

unregister_chrdev:

        unregister_chrdev_region(nvme_chr_devt, NVME_MINORS);

destroy_delete_wq:

        destroy_workqueue(nvme_delete_wq);

destroy_reset_wq:

        destroy_workqueue(nvme_reset_wq);

destroy_wq:

        destroy_workqueue(nvme_wq);

out:

        return result;

}

            我们来看下这个初始化函数的过程:

            首先调用alloc_workqueue,其实是个宏,该宏会调用__alloc_workqueue_key返回workqueue_struct结构体,初始化过程会创建三个工作队列,分别是nvme-wq,nvme-reset-wq,nvme-delete-wq。其中会调用workqueue_sysfs_register,最后队列会暴露出现在/sys/bus/workqueue/devices工作队列是延迟执行中使用最多到机制,分为unbound workqueue和bound workqueue。bound workqueue就是绑定到cpu上的,挂入到此队列中的work只会在相对应的cpu上运行。unbound workqueue不绑定到特定的cpu,而且后台线程池的数量也是动态的。此处三个工作队列都是unbound模式的。对于后续驱动使用来说,只要定义一个work,然后把work加入到workqueue就可以了。相比与tasklet机制,工作队列可以在不同 CPU 上同时运行。

            然后调用alloc_chrdev_region函数(会调用__register_chrdev_region函数),分配一个字符串设备nvme的范围,主设备号值随机,次设备号从0开始,范围为NVME_MINORS,并将第一个值(设备号)赋值给nvme_chr_devt。

            接着调用class_create宏,通过调用函数__class_create创建一个class结构体。函数中会调用__class_register,会将类进行注册,为后续设备注册准备。

            class_create会调用两次,创建两个类,一个nvme和一个nvme_subsystem。会在/sys/class中创建同名的两个目录,便于后续设备注册。

            nvme-core模块初始化过程流程如下:

6dedf783bb7b891a0007105b2ff618c3aca0aad8

高清图:

https://github.com/kernel-z/filesystem/blob/master/nvme-init-exit-0.1.png

 

4.2     nvme_core_exit

我们来看下注销过程,该过程是初始化的完全逆操作。

模块nvme_core注销函数如下:

void nvme_core_exit(void)

{

        ida_destroy(&nvme_subsystems_ida);

        class_destroy(nvme_subsys_class);

        class_destroy(nvme_class);

        unregister_chrdev_region(nvme_chr_devt, NVME_MINORS);

        destroy_workqueue(nvme_delete_wq);

        destroy_workqueue(nvme_reset_wq);

        destroy_workqueue(nvme_wq);

}

163864168d0943abc48645e29e6526aa63cb0ee0

高清图:

https://github.com/kernel-z/filesystem/blob/master/nvme-init-exit-0.1.png

其中ida_destory是释放所有和IDA关联的资源。IDA结构体是一个树状结构体,是内核工作的一个机制。 

这里先介绍一下IDR,IDR机制是内核中将一个整数ID号和指针关联在一起的机制。 如果使用数组进行索引,当ID号很大时,数组索引会占据大量的存储空间,如果使用链表,在总线上设备特别多的情况下,链表的查询效率不高。而IDR机制内部采用红黑树,可以很方便的将整数和指针关联起来,并且有很高的搜索效率。

IDA只是用来分配id,并不将某数据结构和id关联起来。 例如sd设备的设备名,如sda,驱动在生成设备文件的时候会向系统申请一个ida,也就是唯一id,然后把id映射成设备文件名。  

            在nvme-core中有使用到ida,所以在最后中需要释放。

            static DEFINE_IDA(nvme_subsystems_ida);

下一篇中,我们会对NVMe驱动加载和卸载过程的输出内容分析,加强对前两篇内容的理解。

5     NVMe驱动详解之五加载分析

内核配置可以参考《Linux开发环境内核配置》,主要是针对模块调试的那些选项,否则系统不会有详细的输出。例如:配置参数CONFIG_DYNAMIC_DEBUG后才能使能dev_dbg输出函数。

对于调试环境,内核配置的参数参考和说明如下:

CONFIG_DYNAMIC_DEBUGEnable dynamic printk() support

CONFIG_DEBUG_KERNEL
: Kernel debugging

CONFIG_DEBUG_SLABDebug slab memory allocations

CONFIG_DEBUG_PAGEALLOCDebug page memory allocations

CONFIG_DEBUG_SPINLOCK: Spinlock and rw-lock debugging: basic checks

CONFIG_DEBUG_INFO:Compile the kernel with debug info

CONFIG_MAGIC_SYSRQ
: Magic SysRq key

CONFIG_DEBUG_STACKOVERFLOW: Check for stack overflows

CONFIG_DEBUG_STACK_USAGE: Stack utilization instrumentation 

CONFIG_KALLSYMS: Load all symbols for debugging/ksymoops

CONFIG_IKCONFIG: Kernel .config support 

CONFIG_IKCONFIG_PROC: Enable access to .config through /proc/config.gz

CONFIG_ACPI_DEBUG: Debug Statements

CONFIG_DEBUG_DRIVER: Driver Core verbose debug messages 

CONFIG_SCSI_CONSTANTS: Verbose SCSI error reporting (kernel size += 36K)

CONFIG_INPUT_EVBUG: Event debugging

CONFIG_PROFILING: Profiling support

CONFIG_DEBUG_KOBJECT_RELEASE(这个选项可以不开,容易死机)

如果可以使能所有配置最好。

配置完毕并重新编译内核并重启。接着我们开始分析。

使用modprobe nvme加载NVMe驱动(关于如何编译请参考本系列第一个文章,对NVMe驱动源码位置和编译进行了详细描述),会出现如下输出。

<7>[30385.621786] device: 'nvme-wq': device_add

<7>[30385.621795] bus: 'workqueue': add device nvme-wq

<7>[30385.621804] PM: Adding info for workqueue:nvme-wq

<7>[30385.624172] device: 'nvme-reset-wq': device_add

<7>[30385.624184] bus: 'workqueue': add device nvme-reset-wq

<7>[30385.624196] PM: Adding info for workqueue:nvme-reset-wq

<7>[30385.624772] device: 'nvme-delete-wq': device_add

<7>[30385.624780] bus: 'workqueue': add device nvme-delete-wq

<7>[30385.624790] PM: Adding info for workqueue:nvme-delete-wq

<7>[30385.624839] device class 'nvme': registering

<7>[30385.624864] device class 'nvme-subsystem': registering

<7>[30385.625689] bus: 'pci': add driver nvme

<7>[30385.625702] bus: 'pci': driver_probe_device: matched device 0000:00:0e.0 with driver nvme

<7>[30385.625705] bus: 'pci': really_probe: probing driver nvme with device 0000:00:0e.0

<7>[30385.625712] devices_kset: Moving 0000:00:0e.0 to end of list

<7>[30385.625862] device: 'nvme0': device_add

<7>[30385.625886] PM: Adding info for No Bus:nvme0

<6>[30385.625949] nvme nvme0: pci function 0000:00:0e.0

<7>[30385.629882] driver: 'nvme': driver_bound: bound to device '0000:00:0e.0'

<7>[30385.629913] bus: 'pci': really_probe: bound device 0000:00:0e.0 to driver nvme

<7>[30385.628129] device: 'nvme-subsys0': device_add

<7>[30385.628150] PM: Adding info for No Bus:nvme-subsys0

<7>[30385.629458] device: '259:0': device_add

<7>[30385.629479] PM: Adding info for No Bus:259:0

<7>[30385.629537] device: 'nvme0n1': device_add

<7>[30385.629556] PM: Adding info for No Bus:nvme0n1

使用modprobe –r nvme卸载NVMe驱动,出现输出如下:

<7>[78541.046026] bus: 'pci': remove driver nvme

<7>[78541.048245] device: '259:0': device_unregister

<7>[78541.048262] PM: Removing info for No Bus:259:0

<7>[78541.049034] device: '259:0': device_create_release

<7>[78541.049572] PM: Removing info for No Bus:nvme0n1

<7>[78541.091916] PM: Removing info for No Bus:nvme0

<7>[78541.092007] PM: Removing info for No Bus:nvme-subsys0

<7>[78541.092067] driver: 'nvme': driver_release

<7>[78541.098693] device class 'nvme-subsystem': unregistering

<7>[78541.098773] class 'nvme-subsystem': release.

<7>[78541.098775] class_create_release called for nvme-subsystem

<7>[78541.098778] device class 'nvme': unregistering

<7>[78541.098792] class 'nvme': release.

<7>[78541.098794] class_create_release called for nvme

<7>[78541.098799] device: 'nvme-delete-wq': device_unregister

<7>[78541.098811] bus: 'workqueue': remove device nvme-delete-wq

<7>[78541.098831] PM: Removing info for workqueue:nvme-delete-wq

<7>[78541.099292] device: 'nvme-reset-wq': device_unregister

<7>[78541.099309] bus: 'workqueue': remove device nvme-reset-wq

<7>[78541.099312] PM: Removing info for workqueue:nvme-reset-wq

<7>[78541.099531] device: 'nvme-wq': device_unregister

<7>[78541.099547] bus: 'workqueue': remove device nvme-wq

<7>[78541.099549] PM: Removing info for workqueue:nvme-wq

            这里需要注意的是并不是所有的注册注销活动都会有debug信息,所以这些并不代表驱动注册和注销的全部。很多sysfs的操作是没有这些输出的,因为可以直接在系统/sys目录下观察的。

5.1     debug注册

下面我们将每行输出关联到内核中的代码函数中:

l   nvme-core模块中alloc_workqueue宏触发,调用__alloc_workqueue_key,其又调用workqueue_sysfs_register开启对sysfs文件系统的注册活动,接着调device_register,以及device_add函数,最后在devcie_add函数中调用,pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

<7>[30385.621786] device: 'nvme-wq': device_add

l   在device_add函数中,调用bus_add_device函数,将设备添加到总线中,其中有,pr_debug("bus: '%s': add device %s\n", bus->name, dev_name(dev));

<7>[30385.621795] bus: 'workqueue': add device nvme-wq

l   在device_add函数中调用device_pm_add,负责将设备添加到PM的激活设备列表中。其会调用如下:pr_debug("PM: Adding info for %s:%s\n",dev->bus ? dev->bus->name : "No Bus", dev_name(dev));

 

<7>[30385.621804] PM: Adding info for workqueue:nvme-wq

l   下面三行输出同上,都是创建工作队列过程中的输出,只是参数的区别。

<7>[30385.624172] device: 'nvme-reset-wq': device_add

<7>[30385.624184] bus: 'workqueue': add device nvme-reset-wq

<7>[30385.624196] PM: Adding info for workqueue:nvme-reset-wq

l   下面三行输出同上,都是创建工作队列过程中的输出,只是参数的区别。

<7>[30385.624772] device: 'nvme-delete-wq': device_add

<7>[30385.624780] bus: 'workqueue': add device nvme-delete-wq

<7>[30385.624790] PM: Adding info for workqueue:nvme-delete-wq

l   nvme-core模块中,class_create调用__class_register,在注册nvme类时候会调用,pr_debug("device class '%s': registering\n", cls->name);

 

<7>[30385.624839] device class 'nvme': registering

l   class_create调用__class_register,在注册nvme-subsystem类时候会调用pr_debug("device class '%s': registering\n", cls->name);

 

<7>[30385.624864] device class 'nvme-subsystem': registering

l   nvme模块中,bus_add_river函数中有如下pr_debug函数,pr_debug("bus: '%s': add driver %s\n", bus->name, drv->name);

    <7>[30385.625689] bus: 'pci': add driver nvme

l   nvme模块中,driver_probe_device函数中有如下pr_debug函数,pr_debug("bus: '%s': %s: matched device %s with driver %s\n",drv->bus->name, __func__, dev_name(dev), drv->name);

<7>[30385.625702] bus: 'pci': driver_probe_device: matched device 0000:00:0e.0 with driver nvme

l   在函数driver_probe_device中调用really_probe,pr_debug("bus: '%s': %s: probing driver %s with device %s\n",drv->bus->name, __func__, drv->name, dev_name(dev));

<7>[30385.625705] bus: 'pci': really_probe: probing driver nvme with device 0000:00:0e.0

l   really_probe函数会调用devices_kset_move_last将设备移动到devices_kset的末尾。pr_debug("devices_kset: Moving %s to end of list\n", dev_name(dev));

<7>[30385.625712] devices_kset: Moving 0000:00:0e.0 to end of list

,

l   nvme_probe函数会调用nvme_init_ctrl在函数nvme_init_ctrl(初始化NMVe控制结构体)中调用,cdev_device_add调用device_add,函数中有如下:pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

<7>[30385.625862] device: 'nvme0': device_add

l   在上行的上下文中,调用device_pm_add,pr_debug("PM: Adding info for %s:%s\n",dev->bus ? dev->bus->name : "No Bus", dev_name(dev));

<7>[30385.625886] PM: Adding info for No Bus:nvme0

l   在驱动代码的nvme_probe函数中,dev_info(dev->ctrl.device, "pci function %s\n", dev_name(&pdev->dev));

<6>[30385.625949] nvme nvme0: pci function 0000:00:0e.0

l driver_bound函数中当设备与驱动绑定结束后,pr_debug("driver: '%s': %s: bound to device '%s'\n", dev->driver->name, __func__, dev_name(dev));

<7>[30385.629882] driver: 'nvme': driver_bound: bound to device '0000:00:0e.0'

l   really_probe函数中当设备与驱动绑定结束后,pr_debug("bus: '%s': %s: bound device %s to driver %s\n",drv->bus->name, __func__, dev_name(dev), drv->name);

<7>[30385.629913] bus: 'pci': really_probe: bound device 0000:00:0e.0 to driver nvme

l   wake_up_new_task->nvme_reset_work->nvme_init_identify->__init_waitqueue_head->device_add。

<7>[30385.628129] device: 'nvme-subsys0': device_add

l   由device_add调用device_pm_add

<7>[30385.628150] PM: Adding info for No Bus:nvme-subsys0

l   下面这个输出的调用栈如下,通过nvme_scan_work来扫到设备。

<4>[    8.362018]  device_add+0x165/0x610

<4>[    8.362021]  device_create_groups_vargs+0xd3/0x100

<4>[    8.362024]  device_create_vargs+0x17/0x20

<4>[    8.362028]  bdi_register_va.part.16+0x23/0x180

<4>[    8.362030]  bdi_register+0x6c/0x80

<4>[    8.362033]  bdi_register_owner+0x2c/0x60

<4>[    8.362036]  __device_add_disk+0x285/0x4a0

<4>[    8.362038]  device_add_disk+0xe/0x10

<4>[    8.362041]  nvme_validate_ns+0x489/0x7d0 [nvme_core]

<4>[    8.362044]  ? blk_mq_free_request+0xfd/0x130

<4>[    8.362047]  ? __nvme_submit_sync_cmd+0x90/0xe0 [nvme_core]

<4>[    8.362049]  nvme_scan_work+0x230/0x300 [nvme_core]

<7>[30385.629458] device: '259:0': device_add

l   由device_add调用device_pm_add

<7>[30385.629479] PM: Adding info for No Bus:259:0

l   这个也是扫描设备得到的,调用堆栈同259:0设备的添加。

<7>[30385.629537] device: 'nvme0n1': device_add

由device_add调用device_pm_add

<7>[30385.629556] PM: Adding info for No Bus:nvme0n1

            这小节中,需要结合第二篇和第三篇中的注册流程图,能结合代码将是最好的了。

5.2     debug注销

l   nvme模块中pci_unregister_driver函数调用,driver_unregister,调用bus_remove_driver,pr_debug("bus: '%s': remove driver %s\n", drv->bus->name, drv->name);

<7>[78541.046026] bus: 'pci': remove driver nvme

l   driver_detach调用nvme_remove回调函数, 而nvme_remove之后的函数如下:nvme_remove->nvme_remove_namespace->nvme_ns_remove->del_gendisk->bdi_unregister->device_unregister,在device_unregister函数中,pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

<7>[78541.048245] device: '259:0': device_unregister

l   下面一条输出的函数调用栈如下:nvme_remove->nvme_remove_namespace->nvme_ns_remove->del_gendisk->bdi_unregister->device_unregister->device_pm_remove,在device_pm_remove函数中,pr_debug("PM: Removing info for %s:%s\n",dev->bus ? dev->bus->name : "No Bus", dev_name(dev));

<7>[78541.048262] PM: Removing info for No Bus:259:0

l   下面这个也是release函数,由kobject_delayed_cleanup函数调用。当device结构体引用为0后,会依次调用device结构中定义的release函数,或device_type中定义的release函数,或device所属的class中所定义的release函数,最后会吧device_private结构释放掉。在device_create->device_create_vargs->device_create_groups_vargs函数中会调用dev->release = device_create_release,所以最后会调用device_create_release。device_create_release函数,pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

 

<7>[78541.049034] device: '259:0': device_create_release

l   nvme_remove->nvme_remove_namespace->nvme_ns_remove->del_gendisk->device_del->device_pm_remove

<7>[78541.049572] PM: Removing info for No Bus:nvme0n1

l   nvme_remove->nvme_uninit_ctrl->cdev_device_del->device_del->device_pm_remove

<7>[78541.091916] PM: Removing info for No Bus:nvme0

l   下面这个输出,先触发device_release,因为在代码中设置了ctrl->device->release = nvme_free_ctrl,所以调用nvme_free_ctrl函数。而在nvme_free_ctrl中会间接调用nvme_destroy_subsystem。逻辑如下:nvme_free_ctrl-> nvme_put_subsystem->nvme_destroy_subsystem-> device_del-> device_pm_remove

 

<7>[78541.092007] PM: Removing info for No Bus:nvme-subsys0

l   下面这条记录,是由驱动对象引用减少为0后,内核中的工作队列延时执行的,其调用如下:

<4>[  243.031657]  driver_release+0x4b/0x70

<4>[  243.031660]  kobject_delayed_cleanup+0x6a/0x180

<4>[  243.031665]  process_one_work+0x13b/0x350

<4>[  243.031669]  worker_thread+0x45/0x3c0

<4>[  243.031672]  kthread+0xfd/0x130

<4>[  243.031674]  ? process_one_work+0x350/0x350

<4>[  243.031677]  ? kthread_bind+0x10/0x10

<4>[  243.031681]  ret_from_fork+0x35/0x40

 

<7>[78541.092067] driver: 'nvme': driver_release

l   nvme-core模块中nvme_exit函数中,函数class_destroy调用class_unregister,其中如下:pr_debug("device class '%s': unregistering\n", cls->name);

<7>[78541.098693] device class 'nvme-subsystem': unregistering

l   这个在class_unregister函数下,调用kset_unregister后,类的引用减少最后调用release函数释放。调用的函数栈如下:class_unregister-> kset_unregister-> kobject_put-> kobject_cleanup-> class_release。

<7>[78541.098773] class 'nvme-subsystem': release.

l   调用的函数栈如下:class_unregister-> kset_unregister-> kobject_put-> kobject_cleanup-> class_release-> class_create_release。在class_release函数中会调用class->class_release(class)来实现跳转(而在class_create中会设定cls->class_release = class_create_release)。class_create_release函数中有如下:pr_debug("%s called for %s\n", __func__, cls->name);

<7>[78541.098775] class_create_release called for nvme-subsystem

l   nvme-core模块中nvme_exit函数中,函数class_destroy调用class_unregister,其中如下:pr_debug("device class '%s': unregistering\n", cls->name);

<7>[78541.098778] device class 'nvme': unregistering

l   调用的函数栈如下:class_unregister-> kset_unregister-> kobject_put-> kobject_cleanup-> class_release

<7>[78541.098792] class 'nvme': release.

l   调用的函数栈如下:class_unregister-> kset_unregister-> kobject_put-> kobject_cleanup-> class_release-> class_create_release。在class_release函数中会调用class->class_release(class)来实现跳转(而在class_create中会设定cls->class_release = class_create_release)。class_create_release函数中有如下:pr_debug("%s called for %s\n", __func__, cls->name);

<7>[78541.098794] class_create_release called for nvme

 

l   最后是注销之前nvme-core注销过程中创建的三个队列。位于nvme-core模块中。destroy_workqueue函数调用device_unregister,其中有如下输出,输出的设备名字为nvme-delete-wq,其中有:pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

<7>[78541.098799] device: 'nvme-delete-wq': device_unregister

 

l   上行中的device_unregister会调用device_del, device_del函数调用bus_remove_device,bus_remove_device函数中pr_debug("bus: '%s': remove device %s\n",

                 dev->bus->name, dev_name(dev));

<7>[78541.098811] bus: 'workqueue': remove device nvme-delete-wq

l   device_del函数调用device_pm_remove,有输入下输出。

<7>[78541.098831] PM: Removing info for workqueue:nvme-delete-wq

l   destroy_workqueue函数调用device_unregister,在函数device_unregister中有如下输出,输出的设备名字为nvme-reset-wq,其中有pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

<7>[78541.099292] device: 'nvme-reset-wq': device_unregister

l   上行中的device_unregister会调用device_del, device_del函数调用bus_remove_device,bus_remove_device函数中

<7>[78541.099309] bus: 'workqueue': remove device nvme-reset-wq

l   device_del函数调用device_pm_remove,有输入下输出。

<7>[78541.099312] PM: Removing info for workqueue:nvme-reset-wq

l   在函数device_unregister中有如下输出,输出的设备名字为nvme-wq,其中有pr_debug("device: '%s': %s\n", dev_name(dev), __func__);

 

<7>[78541.099531] device: 'nvme-wq': device_unregister

l   上行中的device_unregister会调用device_del, device_del函数调用bus_remove_device,bus_remove_device函数中

<7>[78541.099547] bus: 'workqueue': remove device nvme-wq

l   device_del函数调用device_pm_remove,有输入下输出。

<7>[78541.099549] PM: Removing info for workqueue:nvme-wq

     如果看到有些晕的话,可以看下面的极其简化说明版。

5.3     简化版

5.3.1 注册

nvme-core注册队列,创建2个类:

<7>[30385.621786] device: 'nvme-wq': device_add

<7>[30385.621795] bus: 'workqueue': add device nvme-wq

<7>[30385.621804] PM: Adding info for workqueue:nvme-wq

<7>[30385.624172] device: 'nvme-reset-wq': device_add

<7>[30385.624184] bus: 'workqueue': add device nvme-reset-wq

<7>[30385.624196] PM: Adding info for workqueue:nvme-reset-wq

<7>[30385.624772] device: 'nvme-delete-wq': device_add

<7>[30385.624780] bus: 'workqueue': add device nvme-delete-wq

<7>[30385.624790] PM: Adding info for workqueue:nvme-delete-wq

<7>[30385.624839] device class 'nvme': registering

<7>[30385.624864] device class 'nvme-subsystem': registering

#nvme模块添加nvme驱动,并探测pci中nvme设备。

<7>[30385.625689] bus: 'pci': add driver nvme

<7>[30385.625702] bus: 'pci': driver_probe_device: matched device 0000:00:0e.0 with driver nvme

<7>[30385.625705] bus: 'pci': really_probe: probing driver nvme with device 0000:00:0e.0

<7>[30385.625712] devices_kset: Moving 0000:00:0e.0 to end of list

<7>[30385.625862] device: 'nvme0': device_add

<7>[30385.625886] PM: Adding info for No Bus:nvme0

<6>[30385.625949] nvme nvme0: pci function 0000:00:0e.0

<7>[30385.629882] driver: 'nvme': driver_bound: bound to device '0000:00:0e.0'

<7>[30385.629913] bus: 'pci': really_probe: bound device 0000:00:0e.0 to driver nvme

<7>[30385.628129] device: 'nvme-subsys0': device_add

<7>[30385.628150] PM: Adding info for No Bus:nvme-subsys0

<7>[30385.629458] device: '259:0': device_add

<7>[30385.629479] PM: Adding info for No Bus:259:0

<7>[30385.629537] device: 'nvme0n1': device_add

<7>[30385.629556] PM: Adding info for No Bus:nvme0n1

5.3.2 注销

#以下释放nvme设备,并注销NVMe驱动

<7>[78541.046026] bus: 'pci': remove driver nvme

<7>[78541.048245] device: '259:0': device_unregister

<7>[78541.048262] PM: Removing info for No Bus:259:0

<7>[78541.049034] device: '259:0': device_create_release

<7>[78541.049572] PM: Removing info for No Bus:nvme0n1

<7>[78541.091916] PM: Removing info for No Bus:nvme0

<7>[78541.092007] PM: Removing info for No Bus:nvme-subsys0

<7>[78541.092067] driver: 'nvme': driver_release

#以下在nvme-core模块中,通过nvme_exit->class_destroy->class_unregister,注销之前加载时候的两个类。

<7>[78541.098693] device class 'nvme-subsystem': unregistering

<7>[78541.098773] class 'nvme-subsystem': release.

<7>[78541.098775] class_create_release called for nvme-subsystem

<7>[78541.098778] device class 'nvme': unregistering

<7>[78541.098792] class 'nvme': release.

<7>[78541.098794] class_create_release called for nvme

#以下在nvme-core模块中,通过nvme_exit->destroy_workqueue->workqueue_sysfs_unregister->device_unregister->device_del来注销之前创建的三个工作队列完成。

<7>[78541.098799] device: 'nvme-delete-wq': device_unregister

<7>[78541.098811] bus: 'workqueue': remove device nvme-delete-wq

<7>[78541.098831] PM: Removing info for workqueue:nvme-delete-wq

<7>[78541.099292] device: 'nvme-reset-wq': device_unregister

<7>[78541.099309] bus: 'workqueue': remove device nvme-reset-wq

<7>[78541.099312] PM: Removing info for workqueue:nvme-reset-wq

<7>[78541.099531] device: 'nvme-wq': device_unregister

<7>[78541.099547] bus: 'workqueue': remove device nvme-wq

<7>[78541.099549] PM: Removing info for workqueue:nvme-wq

5.4     小结

这小节需要结合第二篇和第三篇中的注销流程图,如果能结合源代码将是最好的了。否则的话,收获会是很有效。

            其实,内核在每个关键阶段都是会有输出的,所以在打开内核相关debug配置选项后对于开发者是非常有帮助。

本节虽然还没涉及到nvme驱动的代码,但是通过实际加载分析输出,使得我们对第二篇中所讲述的内容有了更好的理解,当出现实际驱动加载问题时候可以快速定位到函数位置。

            以上几篇涉及的代码其实都是内核中已经存在的,例如pci总线,设备模型等,和NVMe驱动本身并且没有多大关系。从下部开始将主要以NVMe驱动代码为主了。

 

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
阿里云服务器怎么设置密码?怎么停机?怎么重启服务器?
如果在创建实例时没有设置密码,或者密码丢失,您可以在控制台上重新设置实例的登录密码。本文仅描述如何在 ECS 管理控制台上修改实例登录密码。
4152 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
4624 0
使用OpenApi弹性释放和设置云服务器ECS释放
云服务器ECS的一个重要特性就是按需创建资源。您可以在业务高峰期按需弹性的自定义规则进行资源创建,在完成业务计算的时候释放资源。本篇将提供几个Tips帮助您更加容易和自动化的完成云服务器的释放和弹性设置。
8271 0
阿里云服务器安全组设置内网互通的方法
虽然0.0.0.0/0使用非常方便,但是发现很多同学使用它来做内网互通,这是有安全风险的,实例有可能会在经典网络被内网IP访问到。下面介绍一下四种安全的内网互联设置方法。 购买前请先:领取阿里云幸运券,有很多优惠,可到下文中领取。
9519 0
windows server 2008阿里云ECS服务器安全设置
最近我们Sinesafe安全公司在为客户使用阿里云ecs服务器做安全的过程中,发现服务器基础安全性都没有做。为了为站长们提供更加有效的安全基础解决方案,我们Sinesafe将对阿里云服务器win2008 系统进行基础安全部署实战过程! 比较重要的几部分 1.
5535 0
阿里云服务器如何登录?阿里云服务器的三种登录方法
购买阿里云ECS云服务器后如何登录?场景不同,云吞铺子总结大概有三种登录方式: 登录到ECS云服务器控制台 在ECS云服务器控制台用户可以更改密码、更换系统盘、创建快照、配置安全组等操作如何登录ECS云服务器控制台? 1、先登录到阿里云ECS服务器控制台 2、点击顶部的“控制台” 3、通过左侧栏,切换到“云服务器ECS”即可,如下图所示 通过ECS控制台的远程连接来登录到云服务器 阿里云ECS云服务器自带远程连接功能,使用该功能可以登录到云服务器,简单且方便,如下图:点击“远程连接”,第一次连接会自动生成6位数字密码,输入密码即可登录到云服务器上。
17394 0
+关注
binarydady
深入底层,挖掘应用 Problem Shooter/Performance Analyzer
73
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载