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。
所以,需要在驱动中指定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.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), 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。主要是为了给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,
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路径并不经传统的块层。
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. 参考
NVMe SPEC http://nvmexpress.org/
Improvements in the block layer
Analysis of NVMe Driver Source Code in linux kernel 4.5