Non-Volatile Memory Express | My Note

Non-Volatile Memory Express

Updated: Sep 21st, 2023


NVMe是一种主机与SSD之间通信的协议,其运行在PCIe接口之上。在NVMe出现之前,我们一般使用AHCI协议和SATA接口与存储设备进行通信,随着存储设备的性能不断提升,存储系统的瓶颈转移到了上层的接口和软件中。在这样的背景下,如今的高速存储设备转而使用PCIe接口和NVMe协议了。PCIe接口能够达到更低的延迟和更高的带宽,同时NVMe协议开销更小,更高的扩展性,更能够充分利用如今高速SSD的性能。

NVMe协议命令集

作为一个PCIe之上的应用层协议,NVMe协议制定了主机和SSD之间通信的命令,如今NVMe协议已经发展至2.0版本,较新的NVMe协议中包含了更多的命令,能够实现一些比较有趣的功能,然而这些需要NVMe SSD的支持,我们可以使用nvme-cli来查看自己的设备支持了NVMe协议中的哪些特性。

no title picture (图源:《深入浅出SSD》P220)

NVMe协议最基本的命令集包含Admin命令集和I/O命令集,在阅读NVMe协议的文档后可以了解到,除去这些基本命令集之外,NVMe协议还能够支持一些Extended Capacity,如Namespace Management,Stream操作等。

下图是NVMe命令集中的Admin命令集,用于主机控制和管理SSD。

no title picture (图源:《深入浅出SSD》P220)

下图是NVMe命令集中的I/O命令集,用于对设备进行I/O操作。

no title picture (图源:《深入浅出SSD》P221)

I/O命令集中,还有一类称为Zoned Namespace Command Set,是一种新型的命令集。

NVMe协议的执行

前面说了NVMe协议是什么,这里主要写一下主机如何通过NVMe协议与存储设备交互,进行I/O操作。

主机发送命令给NVMe SSD,SSD执行指令,完成后通知主机指令已完成,这个过程涉及到这几个东西:Submission Queue(简称SQ)、Completion Queue(简称CQ)、Doorbell、MSI-X Interrupt。

  1. Submission Queue中存放了主机发送给NVMe SSD的命令,当命令完成后完成信息将会由NVMe SSD的控制器写到Completion Queue里。
  2. Doorbell,顾名思义,是一个门铃,主机对其修改意味着按下门铃,通知NVMe SSD控制器现在有新的请求了!
  3. MSI-X Interrupt:是一种硬件中断,NVMe SSD使用该中断通知主机已完成请求。

这几个概念应该大致有一个了解了,那么现在我们要弄清楚,这相关的数据结构存放在哪里。从下图中,我们可以看到SQ和CQ都存放在主机内存中,Doorbell在NVMe SSD控制器里。

no title picture (图源:《深入浅出SSD》P222)

了解了上面的这些信息后,我们应该能够猜到一个I/O命令的执行流程了。如下图所示,当有新的I/O请求后,将会有这8步操作,其中每个操作的执行者我加粗表示了。

  1. 首先主机将请求写到位于主机内存的SQ中。
  2. 写完后,主机写Doorbell,通知NVMe SSD有新命令。
  3. SSD控制器读取SQ,取得指令。
  4. SSD控制器执行指令。
  5. SSD控制器完成指令后,写入完成信息到CQ中。
  6. SSD控制器触发MSI-X中断,控制主机请求已完成。
  7. 主机读取CQ中的完成信息。
  8. 主机写入Doorbell,通知NVMe SSD主机已知道请求完成。

no title picture (图源:《深入浅出SSD》P222)

在了解了整个流程后,解释一些问题:

SQ/CQ有多少个?

一般来说,每个核都有一个SQ和CQ。每个核都有自己的Queue能够避免多核之间对queue的竞争,提升扩展性,同时多个queue同时向硬件提交请求也能够充分发挥SSD内部的并行性。

另外,在单个核上还可能有多个SQ,这一般会发生在对QoS有约束的环境下,此时就可以设立一个低优先级的SQ和一个高优先级的SQ,避免互相干扰,形成Head of Line阻塞。

主机和SSD控制器分别是如何对SQ和CQ进行操作的?

这里主要用到生产者消费者模型来描述,如下图所示。

no title picture (图源:《深入浅出SSD》P226)

对于SQ而言,主机是生产者,SSD控制器是消费者,因此主机加入请求并修改队列的Tail,SSD控制器消费请求并修改队列的Head。在这种模式下,两个操作可以并行发生,无需加锁。而对于CQ则相反。

SQ和CQ的Tail、Head存在哪里?

存在Doorbell寄存器中。实际上主机对Doorbell的写入就是(在提交请求时)更新SQ的Tail指针),以及(在完成请求时)更新CQ的Head指针。

主机在写入SQ和消费CQ的时候,如何得知队列已满or队列空了?

对于SQ来说,主机是生产者,会不断的加入新请求,因此主机会修改队列的Tail。但主机并不知道Head指针的位置。为了帮助判断,在消费CQ的时候主机能够从完成信息中得知最新的SQ Head指针的位置,因此可以根据这个判断SQ是否已满。(CQ完成一个那么SQ就可以少一个嘛)

对于CQ来说,主机是消费者,会消费请求,修改队列的Head。这里主机判断队列是否为空的策略是:有效的完成信息中包含一个标志位P,主机消费完后会将标志位清零。因此在消费队列的时候检查标志位即可知道当前完成信息是否有效,又因为这是个队列,如果遇到无效的那队列肯定是空了。

NVMe协议上的I/O

上面解释了主机和NVMe SSD之间命令的交互,但是我们对其中的第四步没有详细分析。具体执行指令时,对于read指令,NVMe SSD需要读取一些数据发送给主机,而对于write指令,NVMe SSD需要写一些数据到盘中。这些具体命令的执行需要注意以下细节。

这里最需要关注的细节是:无论是数据的读取还是写入,其主导作用的都是SSD控制器。对于读操作,主机先指定需要读取的内容的位置(使用LBA寻址),以及指定一块内存区域,然后将其包含在Read命令中发送给SSD控制器(通过上述的SQ来发送),SSD控制器收到Read命令后,将对应的内容读取并放到指定的内存区域中,然后再通知主机来读。而写操作也是类似,主机指定了要写入的数据所在的内存区域,然后发送给SSD控制器,SSD控制器自己从对应的内存区域中读取数据,写入到SSD中,并生成LBA返回。

no title picture (图源:《深入浅出SSD》P230)

这读写的过程,可以从上图中理解为数据从内存到SSD之间的流动。这里的困难在于:我如何告诉SSD我要发送或我要用来读取的一大块内存区域在哪里?这其中涉及到两种可选的内存描述方式,一种是PRP(Physical Region Page),另一种是SGL(Scatter/Gather List),他们的差别可以见下图。

no title picture (图源:《深入浅出SSD》P230)

PRP是较旧的内存描述方式,现在新的NVMe标准都已经支持SGL了。需要注意的是,NVMe on Fabrics只支持SGL而不支持PRP。

NVMe 新功能

  1. CMB
  2. redictable latency mode (PLM) interface
    1. [[ ATC22-Vigil-KV ]] 用到了这个

相关资料

  1. wikipedia_ch,wikipedia_en

  2. 《深入浅出SSD》第六章:NVMe介绍

  3. NVMe 用户空间驱动

    1. https://zhuanlan.zhihu.com/p/40320158
    2. https://zhuanlan.zhihu.com/p/49944748
    3. https://zhuanlan.zhihu.com/p/49945102

一些研究

  1. 关于使用中断通知主机完成请求的做法,可以进一步优化:
    1. [1]
  2. SOSP’22 ccNVMe
[1]
A. Tai, I. Smolyar, M. Wei, and D. Tsafrir, “Optimizing Storage Performance with Calibrated Interrupts,” 2021, pp. 129–145 [Online]. Available: https://www.usenix.org/conference/osdi21/presentation/tai. [Accessed: 04-Aug-2021]

Instead of authenticating the giscus application, you can also comment directly on GitHub.


Notes mentioning this note