一、故事前传
上一篇文章中我们对NVMe作了概况性的介绍,详细请见历史消息。
1. NVMe技术概述;
二、队列管理
在上一篇文章(NVMe系列专题之一:NVMe技术概述)中,我们提到了NVMe有一个很大的优势就是队列深度达到了64K,并且支持队列个数最大可达64K。所以呢,这里我们就先聊聊NVMe中队列相关的一些知识点。
队列,在NVMe协议中,是专门为NVMe命令服务的。介绍队列之前,我们还是先看看NVMe定义的命令种类:
NVMe定义的命令很简单,只有两种: Admin Command和IO Command。加起来总共只要求13个(10 Admin Commands + 3 IO Commands),相比于ATA Commands,真是一下子清爽了很多~
- 当Host要下发Admin command时,需要一个放置Admin command的队列,这个队列就叫做Admin Submission Queue, 简称Admin SQ.
- Device执行完成Admin command时,会生成一个对应的Completion回应,此时也需要一个放置Completion的队列,这个队列就叫做Admin Completion Queue,简称Admin CQ.
同样,执行IO Command时,也会有对应的两个队列,分别是IO SQ和IO CQ。
NVMe Spec对Admin SQ/CQ和IO SQ/CQ有不同的约定:
- 系统中只有一对Admin SQ/CQ,则可以有最多64K对 IO SQ/CQ;
- Admin SQ/CQ的队列深度是2~4K;而IO SQ/CQ的队列深度是2~64K;
注: Admin/IO command大小为64B,对应的Completion大小为16B。
- Admin SQ和CQ是一对一的,而IO SQ和CQ可以一对一,也可以多对一。多个SQ可以支持多线程工作,不同SQ之间可以赋予不同的优先级;
- Admin和IO的SQ/CQ均放在Host端Memory中;
- SQ由Host来更新,CQ则由NVMe Controller更新。
SQ/CQ是队列,那就应该有队列的头(Head)和尾(Tail)。在NVMe Spec中定义SQ/CQ均是循环队列,可以理解为一个圆形(如下图左),但是内存中实际的长条状的(如下图右),其实,队列可以是连续的物理空间,也可以不连续。
Tail: 指向队列中的下一个空位;
Head: 指向下一个将要被执行的命令所在的位置。
- 当队列为空时,Head==Tail;
- 当队列为满时,Head==Tail+1;
有了SQ/CQ是不是就可以执行Command了呢?
我们先看一下NVMe Spec中Command执行的整个流程再回过来审视一下这个问题。
在NVMe Spec中Command执行的流程有八步,Host与Controller之间用PCIe TLP传递信息。
- Host提交新的Command。Host下发一个新Command时,将其放入Host内存中SQ;
- Host通知Controller提取Command。Host把Command写入SQ之后,此时Device并不知道这件事。所以,Host此时需要给Controller发信息,通知NVMe Controller:"我提交了新的命令请求,麻烦尽快帮忙处理!"。这个过程通过更新在Controller内部的寄存器SQ Tail Doorbell来完成。
- NVMe Controller从SQ提取Command。取走Command之后,需要在Controller内部的SQ Head Pointer寄存器中更新Head所在的位置。NVMe没有规定Command存入队列的执行顺序,Controller可以一次取出多个Command进行批量处理。
- NVMe Controller执行从SQ提取的Commands。一个队列中的Command执行顺序是不固定的(可能导致先提交的请求后处理),这涉及到NVMe Spec定义的命令仲裁机制,在后续文章中介绍。执行Read/Wirte Command时,这个过程也会与Host Memory进行数据传递。
- NVMe Controller将Commands的完成状态写入CQ。此时,Controller需要更新CQ Tail Pointer寄存器。
- NVMe Controller通知Host检查Commands的完成状态。Controller通过发送一个中断信息告知Host:"您提交的Commands,我已经执行完毕了,请您检查结果!"。
- Host检查CQ中的Completion信息。
- Host告知Controller已处理完成Completion信息。此时,Host更新Controller内部的CQ Head Doorbell。告知Controller:"您发送回来的Command执行结果,我已处理完毕,非常感谢!"。
看完上面NVMe Command执行流程之后,我们再回过头来看一下刚才我们的问题:"有了SQ/CQ是不是就可以放心的执行Command了呢?".
答案是否定的。从上述Command执行流程中,我们发现除了SQ/CQ之外,还有两个关键的"人物": PCIe TLP和寄存器.
在之前的PCIe专题(PCIe系列专题之二:2.2 TLP事务处理方式解析)中,我们有介绍过PCIe TLP的类型有很多,如下图,不过,NVMe只挑选了Memory Read/Wirte传递信息。
注:Non-Posted: 需要completion返回响应包;Posted: 不需要completion返回响应包
NVMe Command执行过程中所需的寄存器有两种:Doorbell Register和Pointer Register。Doorbell,可以简称DB,是NVMe Spec定义的寄存器;Pointer register 是主控厂商自定义的寄存器,归纳一下:
寄存器 | 用途 | 位置 | Update By | Defined By |
SQ Tail Doorbell | 记录SQ Tail的位置 | Controller Memory | Host | NVMe Spec |
SQ Head Pointer | 记录SQ Head的位置 | Controller Memory | Controller | Vendor Specific |
CQ Tail Pointer | 记录CQ Tail的位置 | Controller Memory | Controller | Vendor Specific |
CQ Head Doorbell | 记录CQ Head的位置 | Controller Memory | Host | NVMe Spec |
从上表的信息中,不知道聪慧的你,有没有发现:
- 这个四个寄存器全部放在Controller内存中。也就是说Controller知道这SQ Tail/Head和CQ Tail/Head的全部信息。
- 而Host仅仅知道自己更新的两个信息SQ Tail和CQ Head。
很显然,Host与Controller之间的信息是不对等的。不过,还好Controller是个乐于分享的人。Controller会与Host共享自己所知的信息。那Controller怎么把SQ Head和CQ Tail的信息告知Host呢?
不怕,聪明如你!Controller把SQ Head和CQ Tail的信息写入了Completion报文中,如下图:
- SQ Head Pointer就是SQ Head的信息。
- P代表着Phase bit。CQ队列中所有的位置会被初始为0, 当有新的Completion信息写入时,Phase bit被置为1. Host通过读取Host 内存中CQ的Phase bit信息就能判断中CQ Tail的位置。
前面说了理论,我们还是通过真实的NVMe/PCIe Trace再回顾一下CMD执行流程,以Admin Command: Set Feature和IO Command: Read 为例:
1. Set Feature
首先,看全局,如下图,
结合前面介绍的Command执行流程的八步分解Trace:
(1)第一步是Host向Host内存中的SQ写入新的Command。因为这部分是在Host内部执行的,所以在PCIe Trace中没有体现。
(2)第二步是Host更新Controller内存中的SQ Tail DB,告知Controller过来提取Command。
PCIe通过Memory Write TLP(Posted)完成Host对SQ Tail DB的更新。从下图的Trace中,我们可以看到SQ Tail=0x1E(先记住这个值哈,后面有用). SQ Tail DB在Controller内存中的位置=0xFB301000.
(3)第三步是Controller去Host内存中的SQ中取回Command。Controller通过发送Memory Read TLP(Non-Posted)从Host内存提取Command。SQ在Host内存中的位置=0x10040C740. 读出数据的长度=0x40(64),这个也是NVMe规定Command的长度64B。
因为Memory Write是Non-Posted TLP,所以Host会发回一个Memory Write对应的Completion TLP。(需要注意的是在Xgig PCIe设备中,此时MRd的CpID被叫做了ASubmQ,实际就是CpID)
从CpID中包含取回Command的一些信息,比如这个Set feature命令是为了改变NVMe设备的Power state.
(4)第四步是在Controller内部开始执行上一步取回的Set feature命令。此时在PCIe链路中没有交互,所以在PCIe Trace中看不到执行过程的Trace。
(5)第五步是Controller更新CQ,反馈Set feature命令执行结果。
Controller发送Memory Write TLP将set feature命令执行结果写入CQ。此时CQ在Host内存中的位置=0x10041090. 同时在告知Host SQ Head的位置=0x1E。还记得第二步中SQ Tail=0x1E这个信息吗? Head==Tail就说明了SQ现在空了。此外,还有一个信息就是Phase Tag=1, 代表Host CQ中有新增Completion信息。
(6)第六步是Controller通知Host检查Set feature命令执行结果。这个过程中,Controller通过发送MSI-X中断告知Host去检查CQ中的返回结果。
(7)第七步是Host检查CQ中的Set feature命令执行结果。这个过程时在Host内部实现的,在PCIe Trace中也没有体现。
(8)第八步是Host更新Controller内存中的CQ Head DB,告知Controller:"您的完成报告我已经处理完了,非常感谢您!" 从Trace中可以看到,CQ Head的位置=0x1A. CQ Head DB在Controller内部的位置=0xFB301004.
2. Read
IO command是与Admin command处理的流程基本一致,有一点不同的是:IO Command处理过程中会涉及到数据的传输。在这里就不展开解析咯~