Linux NVMe Driver学习笔记之2:初始化
上回,我们学习了Linux NVMe驱动的架构以及nvme_core_init的相关内容(),本文我们主要学习一下Linux NVMe驱动初始化过程中都做了哪些事情。
打开Pci.c找到初始化入口module_init(nvme_init):
static int __init nvme_init(void)
{
int result;
//1, 创建全局工作队列
nvme_workq = alloc_workqueue("nvme", WQ_UNBOUND | WQ_MEM_RECLAIM, 0);
if (!nvme_workq)
return -ENOMEM;
//2, 注册NVMe驱动
result = pci_register_driver(&nvme_driver);
if (result)
destroy_workqueue(nvme_workq);
return result;
}
从上面code来看,初始化的过程很简单,只进行了两步,code结构也太简单了吧。如果你对这点认真,那你就输了。不要被假象给蒙蔽咯,我们一步一步的拆开学习一下。
第一步:创建全局工作队列
创建Workqueue时,实际调用的函数是__alloc_workqueue_key(),这部分已经涉及到Linux底层的知识点,我们不展开详细学习了,还是主要针对nvme相关的内容具体解析。在这里只是结合__alloc_workqueue_key()函数中最开始的一段代码,学习一下Kernel的两种线程:
if ((flags & WQ_POWER_EFFICIENT) && wq_power_efficient)
flags |= WQ_UNBOUND;
在kernel中,有两种线程池,一种是线程池是per cpu的,也就是说,系统中有多少个cpu,就会创建多少个线程池,cpu x上的线程池创建的worker线程也只会运行在cpu x上。另外一种是unbound thread pool,该线程池创建的worker线程可以调度到任意的cpu上去。由于cache locality的原因,per cpu的线程池的性能会好一些,但是对power saving有一些影响。设计往往如此,workqueue需要在performance和power saving之间平衡,想要更好的性能,那么最好让一个cpu上的worker thread来处理work,这样的话,cache命中率会比较高,性能会更好。但是,从电源管理的角度来看,最好的策略是让idle状态的cpu尽可能的保持idle,而不是反复idle,working,idle again。
我们来一个例子辅助理解上面的内容。在t1时刻,work被调度到CPU A上执行,t2时刻work执行完毕,CPU A进入idle,t3时刻有一个新的work需要处理,这时候调度work到那个CPU会好些呢?是处于working状态的CPU B还是处于idle状态的CPU A呢?如果调度到CPU B上运行,那么,由于之前处理过work,其cache内容新鲜,处理起work当然是得心应手,速度很快,但是,这需要将CPU A从idle状态中唤醒。选择CPU B呢就不存在将CPU 从idle状态唤醒,从而获取power saving方面的好处。
了解了上面的基础内容之后,我们再来检视per cpu thread pool和unbound thread pool。当workqueue收到一个要处理的work,如果该workqueue是unbound类型的话,那么该work由unbound thread pool处理并把调度该work去哪一个CPU执行这样的策略交给系统的调度器模块来完成,对于scheduler而言,它会考虑CPU core的idle状态,从而尽可能的让CPU保持在idle状态,从而节省了功耗。因此,如果一个workqueue有WQ_UNBOUND这样的flag,则说明该workqueue上挂入的work处理是考虑到power saving的。如果workqueue没有WQ_UNBOUND flag,则说明该workqueue是per cpu的,这时候,调度哪一个CPU core运行worker thread来处理work已经不是scheduler可以控制的了,这样,也就间接影响了功耗。
这块涉及的详细内容,有兴趣的可以翻阅Linux相关书籍。我们接下来进入本文的重点。
第二步:注册NVME驱动
在初始化过程中,调用kernel提供的pci_register_driver()函数将nvme_driver注册到PCI Bus。问题来了,PCI Bus是怎么将nvme driver匹配到对应的NVMe设备的呢?
系统启动时,BIOS会枚举整个PCI Bus, 之后将扫描到的设备通过ACPI tables传给操作系统。当操作系统加载时,PCI Bus驱动则会根据此信息读取各个PCI设备的Header Config空间,从class code寄存器获得一个特征值。class code就是PCI bus用来选择哪个驱动加载设备的唯一根据。NVMe Spec定义NVMe设备的Class code=0x010802h, 如下图。
根据code来看,nvme driver会将class code写入nvme_id_table,
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_id_table的内容如下,
static const struct pci_device_id nvme_id_table[] = {
{ PCI_VDEVICE(INTEL, 0x0953),
.driver_data = NVME_QUIRK_STRIPE_SIZE |
NVME_QUIRK_DISCARD_ZEROES, },
{ PCI_VDEVICE(INTEL, 0x0a53),
.driver_data = NVME_QUIRK_STRIPE_SIZE |
NVME_QUIRK_DISCARD_ZEROES, },
{ PCI_VDEVICE(INTEL, 0x0a54),
.driver_data = NVME_QUIRK_STRIPE_SIZE |
NVME_QUIRK_DISCARD_ZEROES, },
{ PCI_VDEVICE(INTEL, 0x5845), /* Qemu emulated controller */
.driver_data = NVME_QUIRK_IDENTIFY_CNS, },
{ PCI_DEVICE(0x1c58, 0x0003), /* HGST adapter */
.driver_data = NVME_QUIRK_DELAY_BEFORE_CHK_RDY, },
{ PCI_DEVICE(0x1c5f, 0x0540), /* Memblaze Pblaze4 adapter */
.driver_data = NVME_QUIRK_DELAY_BEFORE_CHK_RDY, },
{ PCI_DEVICE_CLASS(PCI_CLASS_STORAGE_EXPRESS, 0xffffff) },
{ PCI_DEVICE(PCI_VENDOR_ID_APPLE, 0x2001) },
{ 0, }
};
咦,在上面的nvme_id_table里面怎么没有看到0x010802h呢?原来是最新的linux code将PCI_CLASS_STORAGE_EXPRESS定义放在了pci_ids.h里面了。
#define PCI_CLASS_STORAGE_EXPRESS 0x010802
pci_register_driver()函数将nvme_driver注册到PCI Bus之后,PCI Bus就明白了这个驱动是给NVMe设备(Class code=0x010802h)用的。
到这里,只是找到PCI Bus上面驱动与NVMe设备的对应关系。nvme_init执行完毕,返回后,nvme驱动就啥事不做了,直到pci总线枚举出了这个nvme设备,就开始调用nvme_probe()函数开始干活咯。再请出nvme_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_probe()的风采吗?请听下回分解~
更多推荐
所有评论(0)