天天看点

Linux中nvme驱动详解 1.   NVMe Command 2.   PCI总线 3.   单独编译NVME驱动 4.   注册和初始化 5.   NVMe的IO 6.   DMA 7.   参考

NVMe离不开PCIe,NVMe SSD是PCIe的endpoint。PCIe是x86平台上一种流行的bus总线,由于其Plug

and Play的特性,目前很多外设都通过PCI Bus与Host通信,甚至不少CPU的集成外设都通过PCI Bus连接,如APIC等。

  NVMe SSD在PCIe接口上使用新的标准协议NVMe,由大厂Intel推出并交由nvmexpress组织推广,现在被全球大部分存储企业采纳

1.  

NVMe Command

NVMe Host(Server)和NVMe

Controller(SSD)通过NVMe Command进行信息交互。NVMe Spec中定义了NVMe Command的格式,占用64字节。

NVMe Command分为Admin

Command和IO Command两大类,前者主要是用于配置,后者用于数据传输。

  NVMe Command是Host与SSD Controller交流的基本单元,应用的I/O请求也要转化成NVMe Command。

       详见《NVM_Express_Revision》

2.  

PCI总线

在系统启动时,BIOS会枚举整个PCI的总线,之后将扫描到的设备通过ACPI tables传给操作系统。当操作系统加载时,PCI Bus驱动则会根据此信息读取各个PCI设备的Header Config空间,从class code寄存器获得一个特征值。

class code是PCI bus用来选择哪个驱动加载设备的唯一根据。NVMe Spec定义的class code是010802h。NVMe SSD内部的Controller PCIe Header中class code都会设置成010802h。

Linux中nvme驱动详解 1.   NVMe Command 2.   PCI总线 3.   单独编译NVME驱动 4.   注册和初始化 5.   NVMe的IO 6.   DMA 7.   参考

所以,需要在驱动中指定class code为010802h,将010802h放入pci_driver nvme_driver的id_table。之后当nvme_driver注册到PCI Bus后,PCI Bus就知道这个驱动是给class

code=010802h的设备使用的。nvme_driver中有一个probe函数,nvme_probe(),这个函数才是真正加载设备的处理函数。

#define PCI_CLASS_STORAGE_EXPRESS       0x010802

static const struct pci_device_id

nvme_id_table[] = {

…….

{ PCI_DEVICE_CLASS(PCI_CLASS_STORAGE_EXPRESS, 0xffffff) },

……

};

3.  

单独编译NVME驱动

在老版本的源码中,可以在源码路径drivers/block中,增加Makefile内容如下,进行编译:

obj-$(CONFIG_BLK_DEV_NVME)     

+= nvme.o

nvme-objs := nvme-core.o nvme-scsi.o

PWD := $(shell pwd)

default:

        make -C

/usr/src/kernels/3.10.0-327.x86_64/ M=$(PWD) modules

clean:

        rm –rf *.o

*.ko

然后直接make 即可生成nvme.ko文件。

关于Makefile可以参考如下:

KERNELVER

?= $(shell uname -r)

KERNROOT

= /lib/modules/$(KERNELVER)/build

nvme:

    $(MAKE) -C $(KERNROOT)

M=`pwd`/drivers/block                                            

   $(MAKE) -C $(KERNROOT)

M=`pwd`/drivers/block clean

主要就两个文件:nvme-core.c和nvme-scsi.c。

       不过,最新的代码位于drivers/nvme/host中,主要是core.c和pci.c。

4.  

注册和初始化

我们知道首先是驱动需要注册到PCI总线。那么nvme_driver是如何注册的呢?

当驱动被加载时就会调用nvme_init(

drivers/nvme/host/pci.c

)函数。在这个函数中,调用了kernel的函数pci_register_driver,注册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,

这样PCI bus上就多了一个pci_driver

nvme_driver。当读到一个设备的class code是010802h时,就会调用这个nvme_driver结构体的probe函数, 也就是说当设备和驱动匹配了之后,驱动的probe函数就会被调用,来实现驱动的加载。

Probe函数主要完成四个工作:

1.映射设备的bar空间到内存虚拟地址空间

2.设置admin

queue;

3.添加nvme

namespace设备;

4.添加nvme

Controller,提供ioctl接口。

       PCIe的Header空间和BAR空间是PCIe的关键特性。Header空间是PCIe设备的通有属性,所有的PCIe

Spec功能和规范都在这里实现;BAR空间则是设备差异化的具体体现,BAR空间的定义决定了这个设备是网卡,SSD还是虚拟设备。BAR空间是Host和PCIe设备进行信息交互的重要介质,BAR空间的数据实际存储在PCIe设备上。Host这边给PCIe设备分配的地址资源,并不占用Host的内存资源。当读写BAR空间时,都需要通过PCIe接口(通过PCI TLP消息)进行实际的数据传输。

接着来看下nvme_driver结构体中的.probe函数nvme_probe。

static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id)

{

        int node, result = -ENOMEM;

        struct nvme_dev *dev;    

        unsigned long quirks = id->driver_data;

        node = dev_to_node(&pdev->dev);

        if (node == NUMA_NO_NODE)

set_dev_node(&pdev->dev, first_memory_node);

        dev = kzalloc_node(sizeof(*dev),

GFP_KERNEL, node);

        if (!dev)       

                return -ENOMEM;          

        dev->queues =

kcalloc_node(num_possible_cpus() + 1,

                        sizeof(struct nvme_queue),

        if (!dev->queues)

                goto free;               

        dev->dev =

get_device(&pdev->dev);

        pci_set_drvdata(pdev, dev);

        result = nvme_dev_map(dev);

        if (result)

                goto put_pci;            

INIT_WORK(&dev->ctrl.reset_work, nvme_reset_work);

INIT_WORK(&dev->remove_work, nvme_remove_dead_ctrl_work);

        mutex_init(&dev->shutdown_lock);

init_completion(&dev->ioq_wait);

        result =

nvme_setup_prp_pools(dev);

        if (result)     

                goto unmap;              

        quirks |=

check_vendor_combination_bug(pdev);

        result = nvme_init_ctrl(&dev->ctrl,

&pdev->dev, &nvme_pci_ctrl_ops,

quirks);                 

                goto

release_pools;      

dev_info(dev->ctrl.device, "pci

function %s\n", dev_name(&pdev->dev));

        nvme_reset_ctrl(&dev->ctrl);

        return 0;

release_pools:

        nvme_release_prp_pools(dev);

 unmap:

        nvme_dev_unmap(dev);

 put_pci:

        put_device(dev->dev);

 free:

        kfree(dev->queues);

        kfree(dev);

        return result;

}

       nvme_probe函数会通过nvme_dev_map函数(层层调用之后)映射设备的bar空间到内核的虚拟地址空间当中, pci协议里规定了pci设备的配置空间里有6个32位的bar寄存器,代表了pci设备上的一段内存空间,可以通过writel, readl这类函数直接读写寄存器。

并分配设备数据结构nvme_dev,队列nvme_queue等,结构体如下。

struct nvme_dev {

        struct nvme_queue *queues;

        struct blk_mq_tag_set tagset;

        struct blk_mq_tag_set admin_tagset;

        u32 __iomem *dbs;

        struct device *dev;

        struct dma_pool *prp_page_pool;

        struct dma_pool *prp_small_pool;

        unsigned online_queues;

        unsigned max_qid;

        int q_depth;

        u32 db_stride;

        void __iomem *bar;

        unsigned long bar_mapped_size;

        struct work_struct remove_work;

        struct mutex shutdown_lock;

        bool subsystem;

        void __iomem *cmb;

        pci_bus_addr_t cmb_bus_addr;

        u64 cmb_size;

        u32 cmbsz;

        u32 cmbloc;

        struct nvme_ctrl ctrl;

        struct completion ioq_wait;

        /* shadow doorbell buffer support: */

        u32 *dbbuf_dbs;

        dma_addr_t

dbbuf_dbs_dma_addr;

        u32 *dbbuf_eis;

dbbuf_eis_dma_addr;

        /* host memory buffer support: */

        u64 host_mem_size;

        u32 nr_host_mem_descs;

host_mem_descs_dma;

        struct nvme_host_mem_buf_desc *host_mem_descs;

        void **host_mem_desc_bufs;

       每个设备至少两个队列,一个是admin管理命令,一个是给I/O命令,这个队列概念和之前介绍块驱动中的磁盘队列一个道理,只是那个驱动比较基础,所以命令和IO并不区分队列,具体结构体如下。

struct nvme_queue {

        struct device *q_dmadev;

        struct nvme_dev *dev;

        spinlock_t q_lock;

        struct nvme_command *sq_cmds;

        struct nvme_command __iomem *sq_cmds_io;

        volatile struct nvme_completion *cqes;

        struct blk_mq_tags **tags;

        dma_addr_t sq_dma_addr;

        dma_addr_t cq_dma_addr;

        u32 __iomem *q_db;

        u16 q_depth;

        s16 cq_vector;

        u16 sq_tail;

        u16 cq_head;

        u16 qid;

        u8 cq_phase;

        u8 cqe_seen;

        u32 *dbbuf_sq_db;

        u32 *dbbuf_cq_db;

        u32 *dbbuf_sq_ei;

        u32 *dbbuf_cq_ei;

继续说nvme_probe函数,nvme_setup_prp_pools,主要是创建dma

pool,后面可以通过dma函数从dma pool中获得memory。主要是为了给4k和128k的不同IO来做优化。

nvme_init_ctrl函数会创建NVMe控制器结构体,这样在后后续probe阶段时候用初始化过的结构,其传入的操作函数集是nvme_pci_ctrl_ops。

static const struct nvme_ctrl_ops nvme_pci_ctrl_ops = {

        .name                   = "pcie",

        .module                 = THIS_MODULE,

        .flags                  = NVME_F_METADATA_SUPPORTED,

        .reg_read32             = nvme_pci_reg_read32,

        .reg_write32            = nvme_pci_reg_write32,

        .reg_read64             = nvme_pci_reg_read64,

        .free_ctrl              = nvme_pci_free_ctrl,

        .submit_async_event     = nvme_pci_submit_async_event,

       另外NVMe磁盘的操作函数集,例如打开,释放等,结构体如下:

static const struct block_device_operations nvme_fops = {

        .owner          = THIS_MODULE,

        .ioctl          = nvme_ioctl,

        .compat_ioctl   = nvme_ioctl,

        .open           = nvme_open,

        .release        = nvme_release,

        .getgeo         = nvme_getgeo,

        .revalidate_disk=

nvme_revalidate_disk,

        .pr_ops         = &nvme_pr_ops,

5.  

NVMe的IO

机械硬盘时代,由于其随机访问性能差,内核开发者主要放在缓存I/O、合并I/O等方面,并没有考虑多队列的设计;而Flash的出现,性能出现了戏剧性的反转,因为单个CPU每秒发出IO请求数量是有限的,所以促进了IO多队列开发。

       驱动中的队列创建,通过函数kcalloc_node如下,可以看到队列数量是和系统中所拥有的cpu数量有关。

dev->queues = kcalloc_node(num_possible_cpus() + 1,

Queue有的概念,那就是队列深度,表示其能够放多少个成员。在NVMe中,这个队列深度是由NVMe

SSD决定的,存储在NVMe设备的BAR空间里。

队列用来存放NVMe Command,NVMe Command是主机与SSD控制器交流的基本单元,应用的I/O请求也要转化成NVMe Command。

不过需要注意的是,就算有很多CPU发送请求,但是块层并不能保证都能处理完,将来可能要绕过IO栈的块层,不然瓶颈就是操作系统本身了。

当前Linux内核提供了blk_queue_make_request函数,调用这个函数注册自定义的队列处理方法,可以绕过io调度和io队列,从而缩短io延时。Block层收到上层发送的IO请求,就会选择该方法处理,如下图:

从图中可 以看出NVMe SSD I/O路径并不经传统的块层。

Linux中nvme驱动详解 1.   NVMe Command 2.   PCI总线 3.   单独编译NVME驱动 4.   注册和初始化 5.   NVMe的IO 6.   DMA 7.   参考

6.  

DMA

PCIe有个寄存器位Bus

Master Enable,这个bit置1后,PCIe设备就可以向Host发送DMA Read Memory和DMA Write Memory请求。

当Host的driver需要跟PCIe设备传输数据的时候,只需要告诉PCIe设备存放数据的地址就可以。

NVMe Command占用64个字节,另外其PCIe

BAR空间被映射到虚拟内存空间(其中包括用来通知NVMe SSD Controller读取Command的Doorbell寄存器)。

NVMe数据传输都是通过NVMe

Command,而NVMe Command则存放在NVMe Queue中,其配置如下图。

Linux中nvme驱动详解 1.   NVMe Command 2.   PCI总线 3.   单独编译NVME驱动 4.   注册和初始化 5.   NVMe的IO 6.   DMA 7.   参考

其中队列中有Submission

Queue,Completion Queue两个。

7.  

参考

NVMe SPEC

http://nvmexpress.org/

Linux

Driver Information

NVM

Express driver

Improvements

in the block layer

Analysis

of NVMe Driver Source Code in linux kernel 4.5

NVMe驱动解析——关键的BAR空间

继续阅读