公众号之前的文章“Linux 4.4.220 PCI总线驱动分析”对PCIe做了详细分析,这篇文章挑选NVMe驱动,来分析其BIO层的数据流。之所以挑选NVMe是因为和XHCI以及AHCI相比,NVMe是最新的HCI,没有历史包袱,设计的非常简洁,驱动代码更加容易理解。
本文将分成三部分,第一部分分析drivers/NVMe/host/pci.c文件内的nvme_driver,包含NVMe设备探测,移除,关闭等操作,第二部分简单介绍NVMe相关的Open Channel SSD驱动,第三部分分析NVMeBIO层数据流。PART1和PART2已经在文章“Linux 4.4.220 NVMe驱动和BIOLayer分析(上)”(已经合并到此文,所以删除原文了)中介绍过,但写PART3的过程中对前两部分有些修改,所以这篇文章仍然包括了前两部分。
PART1
研究Linux NVMe驱动之前,先简单介绍下NVMe协议。
NVMe也叫NVM HCI,从名字可以看出它是一个host controller interface,协议对其介绍为:NVMExpress is a scalable host controller interface designed to address the needs of Enterprise and Client systems that utilize PCI Express based solid state drives。可以看出它专为基于PCIe的固态硬盘而设计。硬盘相关的HCI,公众号文章之前已经介绍了AHCI,NVMe在软件设计上和AHCI有类似之处,前者管理Admin/IO Submission用于存放IO命令和Data buffer信息,Admin/IO Completion Queue用于存放命令执行状态,以及doorbell用于触发命令执行,后者管理CommandList和RFIS以及CommandIssue都能和NVMe对应上。
但二者对应的外设差别非常大,主要体现在两点,1,AHCI和外设之间传输数据使用SATA协议,NVMe管理的SSD设备则使用PCIe协议。2,AHCI的对端外设计算机系统(一般是ARM SOC)上,使用的是AXI总线,而AHCI在X86系统里是一个PCIe类型的Function,X86下发的命令是通过PCIe总线传输,因此在AHCI和X86之间有一个设备,专门用于AXI总线和PCIe总线之间转换。而NVMe则不需要这个设备,因为SSD的SOC也使用了PCIe总线。因此二者的数据传输速率与软件协议栈优劣和物理层传输速率都相关,NVMe在这两方面均优于AHCI。
NVMe的框架和几个关键数据结构如下图所示。
NVM command和Admin command都必须包含以下字段:Command Dword0,Namespace Identifier,Metadata Pointer,PRPEntry 1,PRPEntry 2,SGLEntry 1,MetadataSGL Segment Pointer。DW10-DW15根据不同的命令,使用方式不同,比如Identify命令就只使用了DW10。DW0数据结构如图所示
Completion queue的数据结构如图所示
现在来看NVMe驱动开始的地方--nvme_init,如图所示
1,首先调用register_blkdev注册一个名字叫nvme的块设备,然后调用__register_chrdev注册一个名字叫nvme的字符设备,并为其注册nvme_dev_fops数据结构,如下图所示
其中最重要的成员是nvme_dev_ioctl,如下图所示
这个数据结构还有一个兄弟叫nvme_iotcl,作为对比,其数据结构如图所示
可以看出nvme_ioctl和nvme_dev_ioctl都包含了NVMe_IOCTL_SUBMIT_IO和NVMe_IOCTL_IO_CMD这两条case,它们调用nvme_user_cmd时传入的参数是nvme_passthru_cmd数据格式,即pass_thru命令。NVMe_IOCTL_SUBMIT_IO则是nvme_ioctl独有的,该case下,调用nvme_submit_io时传入参数则是nvme_user_io数据格式。块设备可以接收两种形式的命令,而字符设备只能解析第一种。
不论是nvme_user_cmd还是nvme_submit_io,他们调用的主要路径都是__nvme_submit_sync_cmd->blk_execute_rq->blk_execute_rq_nowait->blk_mq_insert_request->q->mq_ops->map_queue(nvme_mq_admin_ops将其注册为blk_mq_map_queue)->__blk_mq_insert_request->blk_mq_run_hw_queue->__blk_mq_run_hw_queue->q->mq_ops->queue_rq(nvme_mq_admin_ops将其注册为nvme_queue_rq)->nvme_submit_iod,最终回到NVMe驱动层将真正的IO命令打到SubmissionQueue中。调用过程根据是否是pass_thru命令略有不同。
2,调用nvme_init注册数据结构nvme _driver,如图所示,其注册过程调用路径为:module_init->nvme_init->pci_register_driver(&nvme_driver),
上图中PCI_CLASS_STORAGE_EXPRESS= 0x010802,NVMe Spec定义的classcode正是010802h,如图所示。
nvme_init是传给module_init的,而module_init=__initcall(x)=device_initcall,所以nvme_init会在内核调用do_initcalls()时被执行。
在device_initcall阶段,pci总线的初始化已经完成,pci总线上的所有设备都登记在册,当NVMe驱动注册时,会触发driver_attach->bus_for_each_dev->__driver_attach->driver_match_device,来查看新注册的驱动的id_table信息是否与已有的设备匹配,其classcode是010802h,此时启动__driver_attach->driver_probe_device->really_probe->drv->probe过程,调用nvme_driver的成员函数nvme_probe。
3, nvme_probe函数主体如下图所示
第一条nvme相关的动作是对dev->queues的初始化,下图是nvm