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。

964b0fcad14ac8916a9161d31ab88465e1efedd6

所以,需要在驱动中指定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                                            

clean:

   $(MAKE) -C $(KERNROOT) M=`pwd`/drivers/block clean

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

       不过,最新的代码位于drivers/nvme/host,主要是core.cpci.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), GFP_KERNEL, node);

        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);                 

        if (result)     

                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;

        dma_addr_t dbbuf_eis_dma_addr;

 

        /* host memory buffer support: */

        u64 host_mem_size;

        u32 nr_host_mem_descs;

        dma_addr_t 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。主要是为了给4k128k的不同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,

                        sizeof(struct nvme_queue), GFP_KERNEL, node);

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路径并不经传统的块层。

86e3f008695a84d35253fae8cc2e86c1601e45f9

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中,其配置如下图。

6ab34bbf499f9c4ad128e7621f591cecbdf326fa

其中队列中有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空间

 

 

 

GitHub 加速计划 / nv / nvm
78.06 K
7.82 K
下载
nvm-sh/nvm: 是一个 Node.js 版本管理器,用于在不同的 Node.js 版本之间进行切换。它可以帮助开发者轻松管理多个 Node.js 版本,方便进行开发和测试。特点包括轻量级、易于使用、支持跨平台等。
最近提交(Master分支:1 个月前 )
da2720a4 - 29 天前
9fb9dec7 Fixes #3417 29 天前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐