Linux中nvme驱动详解

匿名 (未验证) 提交于 2019-12-02 21:59:42

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。

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。


所以,需要在驱动中指定class code为010802h,将010802h放入pci_driver nvme_driver的id_table。之后当nvme_driver注册到PCI Bus后,PCI Bus就知道这个驱动是给class code=010802h的设备使用的。nvme_driver中有一个probe函数,nvme_probe(),这个函数才是真正加载设备的处理函数。

0x010802

staticconststruct

…….

0xffffff) },

……

};

3.单独编译NVME驱动

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

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

PWD := $(shell pwd)

default:

clean:

rf *.o *.ko

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

关于Makefile可以参考如下:

KERNELVER ?= $(shell uname -r)

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

nvme:

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,其结构体如下。

staticstruct

"nvme",

};

这样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接口。

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

staticintstructconststruct

{

intENOMEM;

struct

unsignedlong

if

sizeof(*dev), GFP_KERNEL, node);

if

returnENOMEM

1,

sizeof(struct

if

goto

if

goto

if

goto

if

goto

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

return0;

release_pools:

unmap:

put_pci:

free:

return

}

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

structnvme_dev

struct

struct

struct

struct

struct

struct

unsigned

unsigned

int

void

unsignedlong

struct

struct

bool

void

struct

struct

/* shadow doorbell buffer support: */

/* host memory buffer support: */

struct

void

};

structnvme_queue

struct

struct

struct

struct

volatilestruct

struct

};

继续说nvme_probe函数,nvme_setup_prp_pools,主要是创建dma pool,后面可以通过dma函数从dma pool中获得memory。主要是为了给4k128k的不同IO来做优化。

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

staticconststruct

"pcie",

};

staticconststructnvme_fops

};

5.NVMe的IO

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

kcalloc_node如下,可以看到队列数量是和系统中所拥有的cpu数量有关。

1,

sizeof(struct

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


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


其中队列中有Submission Queue,Completion Queue两个。

7.参考

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空间


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!