加载中…
个人资料
古猫先生-存储随笔
古猫先生-存储随笔
  • 博客等级:
  • 博客积分:0
  • 博客访问:35,951
  • 关注人气:10
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
相关博文
推荐博文
谁看过这篇博文
加载中…
正文 字体大小:

Linux NVMe驱动初始化解析

(2017-08-15 23:00:07)
标签:

杂谈

本文我们主要学习一下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),

.driver_data = NVME_QUIRK_IDENTIFY_CNS, },

{ PCI_DEVICE(0x1c58, 0x0003),

.driver_data = NVME_QUIRK_DELAY_BEFORE_CHK_RDY, },

{ PCI_DEVICE(0x1c5f, 0x0540),

.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()的风采吗?请听下回分解~​


​​​如果您也想针对存储行业分享自己的想法和经验,诚挚欢迎您的大作。

投稿邮箱:Memory_logger@163.com (投稿就有惊喜哦~)

如您有任何的建议与指正,敬请在文章底部留言,感谢您不吝指教。


更多精彩内容,敬请关注微信公众号: 存储随笔,Memory-logger.​​​​​

0

阅读 评论 收藏 转载 喜欢 打印举报/Report
  • 评论加载中,请稍候...
发评论

    发评论

    以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

      

    新浪BLOG意见反馈留言板 电话:4000520066 提示音后按1键(按当地市话标准计费) 欢迎批评指正

    新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 会员注册 | 产品答疑

    新浪公司 版权所有