NVMe 子系统的结构

image.png

SPDK 查找 NVMe 设备

下面是 spdk_nvme_probe 函数的调用过程

image.png

SPDK 对 NVMe 设备的读写过程

HOST 就是NVMe卡所插入的系统,如图3所示,HOST和Controller之间的交互通过Qpair进行。Qpair分为IO Qpair和Admin Qpair,顾名思义,Admin Qpair用于控制命令的传输,而IO Qpair用于IO命令的传输。

Qpair对由提交队列(Submission Queue, SQ)和完成队列(Completion Queue, CQ)组成的固定元素数量的环形队列[2]。提交队列是由固定元素数量的64字节的命令组成的数组,加上2个整数(头和尾索引)。完成队列由固定元素数量的16字节命令加上2个整数(头和尾索引)所组成的环形队列。另外还有两个32位寄存器(Doorbell),Head Doorbell和Tail Doorbell。

image.png

HOST需要向NVMe写入数据时,需要指明数据在内存中的地址,以及写入到NVMe中的位置,HOST从NVMe读数据也是一样的,需要指明NVMe地址和内存地址,这样HOST和NVMe才知道去哪里取数据,取完后数据放到哪里。这里就不得不提到两种数据地址表示的方式了,一种是PRP(Physical Region Page Entry),还有一种是SGL。PRP指向一个物理内存页。

image.png

如图所示,PRP和正常的寻址方式相似,基地址加上偏移地址。PRP指向一个物理地址页,command中有两个PRP entry,如果还有更多的PRP entry可以用PRP list的形式存放。

通过构造一个64字节的命令,将I/O提交到NVMe设备,将其放入提交队列尾部索引当前位置的提交队列中,然后将提交队列尾部的新索引写入提交队列Tail Doorbell。也可以写多条命令到SQ,然后只写一次Doorbell就可以提交所有命令。

命令本身描述了操作,还描述了主机内存中包含与命令关联的主机内存数据的位置,也就是我们要写入数据的位置,或将读取的数据放置到内存中的位置。通过DMA的方式将数据传输到该地址或从该地址传输数据

完成队列的工作方式类似,设备将命令的响应消息写入到CQ中。CQ中的每个元素包含一个相位Phase Tag,在整个环的每个循环上在0和1之间切换。设备通过中断通知HOST CQ的更新,但是SPDK不启用中断,而是轮询相位位以检测CQ的更新。中断是非常繁重的操作,因此轮询这个相位通常效率要高得多。下图详细展示了Host和Controller交互的过程。

image.png

提交 SQ

image.png

轮询 CQ

用户轮询spdk_nvme_qpair_process_completions来告诉SPDK检查完成队列。具体来说,它读取CQ的元素的Phase Tag,当它翻转时,查看命令的CID值并以CID值作为索引找到指向Request对象的Tacker。Request对象包含用户最初提供的函数指针,然后调用该指针来完成命令。若用户未指定最多处理多少个完成IO (max_completions),spdk_nvme_qpair_process_completions函数将执行完当前CQ中所有已经完成的IO,然后统一更新CQ  Head Doorbell,告诉设备有可用的CQ元素。

在spdk_nvme_ns_cmd_write函数的回调函数write_complete中,当写入的IO完成后,将会释放写IO相关联的缓冲区,然后重新分配一个缓冲区用于将写入NVMe的数据读取回来,spdk_nvme_ns_cmd_read函数完成数据读取的功能,以此进行验证,完成完整的一次写入和读取的过程。读取过程和写入过程类似,同样地,读取完成后调用read_complete回调函数,在read_complete回调函数中会打印出读取过来的内容,并且释放缓冲区。read_complete还会将sequence.is_completed 置1作为结束轮询的标志。

image.png