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.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,其结构体如下。
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。主要是为了给4k和128k的不同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.参考
Improvements in the block layer
Analysis of NVMe Driver Source Code in linux kernel 4.5