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;