NVME概述

NVMe即Non-Volatile Memory Express,即非易失性内存主机控制器接口规范。是跑在PCIe接口上的协议标准

NVME的诞生始于2009年下半年,其规范由包含90多家公司在内的工作小组所定制,Intel是主要领头人,小组成员包括美光、戴尔、三星、Marvell、NetAPP、EMC、IDT等公司,目的就是为SSD建立新的存储规范标准,让它在老旧的SATA与AHCI中解放出来。2011年,NVMe标准正式出炉,该标准是根据闪存存储的特点量身定制的,新的标准解除了旧标准施放在SSD上的各种限制。直至目前,已发展到NVME1.4。

对比老旧的AHCI协议,它主要有着三个特点,即低时延,高性能,低功耗。

1、低时延

  • 在存储介质方面,闪存(Flash)比机械硬盘(HDD)的磁盘快得多。
  • 因为采用了PCIe总线协议,PCIe主控直接与CPU相连,不像SATA方式通过南桥控制器中转连接CPU,因此基于PCIe的SSD时延更低。
  • NVME缩短了CPU到SSD的指令路径,减少了对寄存器的访问次数,而AHCI每条命令需要调用四次寄存器;NVME使用了MSI-X终端管理;并行线程优化等技术。

2、高性能

理论上,IOPS(每秒读写次数)=队列深度/ IO延迟,故IOPS的性能,与队列深度有较大的关系(但IOPS并不与队列深度成正比,因为实际应用中,随着队列深度的增大,IO延迟也会提高)。

市面上性能不错的SATA接口SSD,在队列深度上都可以达到32,然而这也是AHCI所能做到的极限。但目前高端的企业级PCIe SSD,其队列深度可能要达到128,甚至是256才能够发挥出最高的IOPS性能。而NVMe标准下,最大的队列深度可达64000。此外,NVMe的队列数量也从AHCI的1,提高了64000。

3、低功耗

NVMe加入了自动功耗状态切换和动态能耗管理功能。

设备从Power State 0闲置50ms后可以切换到Power State 1,继续闲置的话,在500ms后又会进入功耗更低的Power State 2,切换时会有短暂延迟。SSD在闲置时可以非常快速的控制在极低的水平,在功耗管理上NVMe标准的SSD会比现在主流的AHCI SSD拥有较大优势,这一点对移动设备来说尤其重要,可以显著增加笔记本和平板电脑的续航能力。

指令集

此节内容参考自NVMe技术分析之数据结构_tiantianuser的博客-CSDN博客

NVMe协议对指令集做了精简。NVMe将所支持的指令分为Admin和NVM指令集(即I/O控制)两种类型。其中,Admin指令主要对SSD控制器进行管理操作,而NVM指令则主要是针对数据实现相关控制,比如主机和SSD之间的数据传输。同时,为了简化指令集,NVMe协议规定除了少数如创建(删除)I/O队列、读、写、刷新等必须包括的指令之外,其余均为可选指令。

下表给出了主要的几个指令集。

Admin命令 NVM命令
Create I/O Submission Queue Read
Delete I/O Submission Queue Write
Create I/O Completion Queue Flush
Delete I/O Completion Queue Write Uncorrectable(可选)
Identify Dataset Management(可选)
Get Log Page Compare(可选)
Abort Vendor Specific Commands(可选)
Command Set Specific Commands(可选)

(1)创建I/O提交/完成队列指令
由于I/O提交/完成队列均位于主机端内部,所以需通过发送创建I/O提交/完成队列指令来将队列的相关信息告知SSD控制器,包括队列深度、队列ID号、队列基地址以及队列地址空间是否连续等信息。
(2)识别指令
用于获取NVM子系统、控制器或对应命名空间的相关信息。当SSD获取到主机发送的识别指令后,将返回4KB的数据结构,其中包括SSD的总存储空间、剩余可用空间、逻辑块大小等信息。
(3)读取指令/写入指令
用于写入或读取数据。由于在NVM系统中数据按照逻辑块(Logic Block)为基本单位的方式进行存储,因此执行读写指令时,操控的数据量至少为一个逻辑块,且一条指令仅作用于一个LBA(Logical Block Address,逻辑块地址)范围。
(4)刷新指令
在读写数据的过程中,为了提高效率,部分数据可能缓存在缓冲区中,而不是处于非易失性状态。因此,若出现突然掉电的情况,将导致缓冲区中数据丢失。针对这种问题,可使用刷新指令主动将存储在缓冲区中的数据立即写入非易失性存储介质中。

SQ、CQ、DB

处理命令流程

NVMe有三个特殊字段来处理命令执行,即三宝:Submission Queue(SQ)**、Completion Queue(CQ)Doorbell Register(DB)**。

SQ和CQ位于主机内存中,是Controller Registers中的字段,DB则位于SSD控制器的内部。host通过更新SQ映射的内存上的记录,就可以达到告知SSD有新的命令需要处理,同样,在命令执行尾声,host也是通过更新CQ映射的内存上的信息而实现告知SSD命令已经执行完毕。

通过三宝,NVMe处理一条命令的流程如下:

NVMe命令处理流程

  1. 主机写命令到SQ;
  2. 主机写SQ的DB,通知SSD取指;
  3. SSD收到通知后,到SQ中取指;
  4. SSD执行指令;
  5. 指令执行完成,SSD往CQ中写指令执行结果;
  6. SSD发中断通知主机指令完成;
  7. 收到中断,主机处理CQ,查看指令完成状态;
  8. 主机处理完CQ中的指令执行结果,通过DB回复SSD处理完成。

三宝的作用

先看看SQ和CQ的作用。

Host往SQ中写入命令,SSD往CQ中写入命令完成结果。SQ与CQ的关系,可以是一对一的关系,也可以是多对一的关系,但不管怎样,他们是成对的:有因就有果,有SQ就必然有CQ。

有两种SQ和CQ,一种是Admin,另外一种是I/O,前者放Admin命令,用以Host管理控制SSD,后者放置I/O命令,用以Host与SSD之间传输数据。Admin SQ/CQ 和I/O SQ/CQ各司其职,你不能把Admin命令放到I/O SQ中,同样,你也不能把I/O命令放到Admin SQ里面。

正如下图所示,系统中只有一对Admin SQ/CQ,它们是一一对应的关系;I/O SQ/CQ却可以很多,多达65535(64K减去一个SQ/CQ)。Host端每个Core可以有一个或者多个SQ,但只有一个CQ

SQ和CQ

至于为什么SQ/CQ很多?这是为了提升QoS(服务质量),通常设置一个高优先级SQ和一个低优先级SQ,高优先级SQ用来存放用户当前想做的指令,低优先级SQ则用来做其他不急需的指令,处理命令时优先处理高优先级SQ,也就是提升了QoS。实际系统用多少个SQ,取决于系统配置和性能需求。

总结一下SQ和CQ:

  1. 主机用SQ发命令,SSD用CQ回复状态;
  2. SQ/CQ一般在主机内存中,但也可以在SSD中;
  3. 有两种类型SQ和CQ:Admin和IO,前者发送Admin命令,后者发送IO命令;
  4. 系统中只有一对Admin SQ/CQ,但也可以有很多对IO SQ/CQ;
  5. IO SQ与CQ可以是一对一的关系,也可以是多对一的关系;
  6. IO SQ是可以赋予不同优先级的;
  7. IO SQ/CQ深度可达64K,Admin SQ.CQ深度可达4K;
  8. IO SQ/CQ广度和深度可以灵活配置;
  9. 每条命令大小是64B,每条命令完成状态是16B。

接下来看看DB的作用。

因为SQ和CQ是一种环形队列,DB则记录了一个SQ和CQ的头和尾。每个SQ和CQ都有两个对应的DB:Head DB和Tail DB,DB是在SSD的寄存器,记录SQ和CQ的头和尾巴的位置。其结构原理如下:

队列工作原理

对于SQ来说,生产者为主机,消费者则为SSD控制器。主机将所产生的提交指令将依次放至队列的尾部,而SSD则将从队列的头部开始读取指令;对于CQ来说,生产者为SSD,消费者则为主机。SSD将新的完成信息返回至队列尾部,而主机则通过消耗队列头部来表示完成信息检查完成。

同时,在完成上述过程时,也起到了对SQ和CQ的通知作用。

另外,主机对DB只能写(只能写SQ Tail DB和CQ Head DB),不能读DB。

对DB的总结如下:

  1. DB在SSD控制器端,是寄存器;
  2. DB记录着SQ和CQ队列的头部和尾部;
  3. 每个SQ和CQ都有两个DB—-Head DB和Tail DB;
  4. 主机只能写DB,不能读DB;
  5. 主机通过SSD往CQ中写入的命令完成状态获取其队列头部或者尾部。

寻址:PRP和SGL

主机如何告诉SSD数据所在的内存位置?有两种方法,一种是PRP(Physical Region Page,物理区域页),另一种是SGL(Scatter/Gather List,分散/聚集列表)。

PRP

一个PRP条目(Entry)本质是一个64位内存物理地址。它分为两部分:页起始地址页内偏移。最后两bit是0。这说明PRP表示的物理地址只能四字节对齐访问,页内偏移可以是0,也可以是非零值。

PRP Entry

一个PRP Entry描述一个物理页空间,要描述多个物理页,就需要多个PRP Entry。将这多个物理页连起来就成了PRP链表,其上每个PRP Entry偏移量都必须是0。

每个NVMe命令中有两个域:PRP1PRP2,主机通过这两个域告诉SSD数据在内存中的位置或数据需要写入的位置。PRP1和PRP2有可能指向数据所在位置,也可能指向PRP链表(类似于C语言中的指针)。PRP1和PRP2可能是指针,也可能是指针的指针,还有可能是指针的指针的指针。但不管多复杂,SSD都能找到真正的地址。

PRP1指向PRP List的示例

PRP1指向一个PRP List,PRP List位于Page 100,页内偏移20的位置。SSD确定PRP1是个指向PRP List的指针后,就会去Host内存中(Page 100,Offset 20)把PRP List取过来。获得PRP List后,就获得数据的真正物理地址,SSD然后就会往这些物理地址读入或者写入数据。

对Admin命令来说,它只用PRP告诉SSD内存物理地址;对I/O 命令来说,除了用PRP,Host还可以用SGL的方式来告诉SSD数据在内存中写入或者读取的物理地址。

至于PRP的具体算法,留待以后有机会再研究。

SGL

SGL是一个数据结构,用以描述一段数据空间,这个空间可以是数据源所在的空间,也可以是数据目标空间。SGL(Scatter Gather List)首先是个List,是个链表,由一个或者多个SGL Segment组成,而每个SGL Segment又由一个或者多个SGL Descriptor组成。SGL Descriptor是SGL最基本的单元,它描述了一段连续的物理内存空间:起始地址+空间大小。

每个SGL Descriptor(描述符)大小是16字节。一块内存空间,可以用来放用户数据,也可以用来放SGL Segment,根据这段空间的不同用途,SGL Descriptor也分几种类型。

编码 描述符
0h SGL数据块描述符(Data Block Descripter)
1h SGL位桶描述符(Bit Bucket Descripter)
2h SGL段描述符(Segment Descripter)
3h SGL末端描述符(Last Segment Descripter)
4h-Eh 保留
Fh 商家指定

由表可知有四种类型:

  • 一种是Data Block,就是描述的这段空间是用户数据空间;
  • 一种是Segment描述符(指向链表下一段内容。Last Segment Descripter用来告诉SSD碰到它时,链表就将要到头了)。
  • SGL Bit Bucket它只对Host读有用,用以告诉SSD,你往这个内存写入的东西我是不要的。

举个栗子:

假设Host需要往SSD中读取13KB的数据,其中真正只需要11KB数据,这11KB的数据需要放到3个大小不同的内存中,分别是:3KB,4KB和4KB。详情见下图。

例子

无论是PRP还是SGL,本质都是描述内存中的一段数据空间,这段数据空间在物理上可能连续的,也可能是不连续的。Host在命令中设置好PRP或者SGL,告诉SSD数据源在内存的什么位置,或者从闪存上读取的数据应该放到内存的什么位置。

RPR和SGL区别

为什么?既然有PRP,为什么还需要SGL?

事实上,NVMe1.0的时候的确只有PRP,SGL是NVMe1.1之后引入的。它们的不同是:

  1. SGL不仅提供了一个内存基地址,还提供了该段内存的大小信息(length),PRP只提供了一个基地址,内存大小需要SSD根据命令上下文去猜;
  2. SGL可描述任意的内存空间,相对PRP来说更灵活,后者基本按页访问内存;
  3. 另外SGL提供了一个Bit Bucket的东西,一段连续的LBA空间,其中的一些数据我可以不需要传输,PRP好像做不到这点。

NVMe和PCIe交互Trace

以Host发出read命令为例。完整Trace如下:

例子

  1. Host准备了一个Read命令给SSD:

例子

分析该包,Host需要从起始LBA 0x20E0448(SLBA)上读取128个DWORD (512字节)的数据,读到哪里去呢?PRP1给出内存地址是0x14ACCB000。这个命令放在编号为3的SQ里 (SQID = 3),CQ编号也是3 (CQID = 3)

  1. Host通过写SQ的Tail DB,通知Controller来取命令。

例子

上图中,上层是NVMe层,下层是PCIe传输层的TLP。Host想往SQ Tail DB中写入的值是5。PCIe是通过一个Memory Write TLP来实现Host写CQ的Tail DB的。该Tail DB寄存器映射在Host的内存地址为F7C11018,由于NVMe 的寄存器映射到了Host内存中,所以可以根据这个地址写入寄存器值。

  1. SSD收到通知,去Host端的SQ中取指。

例子

PCIe是通过发一个Memory Read TLP到Host的SQ中取指的。可以看到,PCIe需要往Host内存中读取16个DWORD的数据(一个NVMe指令大小)。

  1. SSD执行读命令,把数据从闪存中读到缓存中,然后把数据传给Host。

例子

SSD是通过Memory write TLP 把Host命令所需的128个DWORD数据写入到Host命令所要求的内存中去。SSD每次写入32个DWORD,一共写了4次。

  1. SSD往Host的CQ中返回状态。

例子

SSD是通过Memory write TLP 把16个字节的命令完成状态信息写入到Host的CQ中。

  1. SSD采用中断的方式告诉Host去处理CQ:

例子

上图使用的是MSI-X中断方式。这种方式将中断信息和正常的数据信息一样,PCIe打包把中断信息告知Host。SSD还是通过Memory Write TLP把中断信息告知Host,这个中断信息长度是1DWORD。

  1. Host处理相应的CQ

Host处理完相应的CQ后,需要更新SSD端的CQ Head DB告知SSD处理。完成:

例子

Host还是通过Memory Write TLP更新SSD端的CQ Head DB。

结束。

参考文章

《深入浅出SSD:固态存储核心技术、原理与实战》–SSDFans

NVMe技术分析之数据结构_tiantianuser的博客-CSDN博客

可乐学习NVMe之三:解读PRP/SGL_Angel20200620的博客-CSDN博客_prp和sgl