NVMe驱动详解系列
第一部: NVMe驱动初始化与注销
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);通知用户驱动加载成功。
总的注册流程图如下:
高清图下载地址:
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函数。
至此,驱动注销结束了,其总的流程图如下:
(高清图下载地址:
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模块初始化过程流程如下:
高清图:
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);
}
高清图:
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_DEBUG:Enable dynamic printk() support
CONFIG_DEBUG_KERNEL : Kernel debugging
CONFIG_DEBUG_SLAB:Debug slab memory allocations
CONFIG_DEBUG_PAGEALLOC:Debug 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_*: 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驱动代码为主了。