北京网站搭建哪家好自己怎么设计公司的logo
我们上一节对NVME协议进行了一个简单的了解,现在我们具体分析NVME内部使用的命令。
NVME的两种命令
NVMe 制定了主机与 SSD 之间通信的命令,以及命令的执行方式。
NVMe 有两种命令,一种叫 Admin 命令,用于帮主机管理和控制 SSD;另外一种就是 I/O 命令,用于在主机和 SSD 之间传输数据,NVMe 支持的 Admin 命令如下所示。
跟 ATA 规范中定义的命令相比,NVMe 的命令个数少了很多,它完全是为 SSD 量身定制的。在 SATA 时代,即使只有 HDD 才需要的命令(SSD 上其实完全没有必要),但为了符合协议标准,SSD 还是需要实现它(完全只是为了兼容性)。NVMe 让 SSD 摆脱了这种困境。
NVME通信使用的三大队列
相较于AHCI,NVME对于命令的发送和接收过程做了很多大的改进。NVMe 使用三个基本的队列:Submission Queue(SQ,提交队列)、Completion Queue(CQ,完成队列)和 DoorBell register(DB,门铃队列)。SQ 和 CQ 位于主机的内存中,DB 则位于 SSD 的控制器内部,如下图所示。
如上图所示,SSD 作为一个 PCIe Endpoint(PCIe 终端,简称 EP)通过 PCIe 连着 RC,然后 RC 连接着 CPU 和内存。尽管 CPU 不直接与 SSD 交互,但SSD 的地位还是较过去提升了一级,过去 SSD 别说直接接触 CPU,就是连 RC 的面都见不到,SSD 和 RC 之间还隔着一座南桥。
三大队列的基本功能:
SQ 位于主机内存中,主机要发送命令时,先把准备好的命令放在 SQ 中,然后通知 SSD 来取;CQ 也是位于主机内存中,一个命令执行完成,无论成功还是失败,SSD 总会往 CQ 中写入命令回复主机完成状态。
主机发送命令时,不是直接往 SSD 中发送命令,而是把命令准备好放在自己的内存中,通过写 SSD 端的 DB 来告知 SSD 的。
我们来看看 NVMe 是如何处理命令的,
第一步,主机写命令到 SQ;
第二步,主机写 SQ 的 DB,通知 SSD 取指;
第三步,SSD 收到通知后,到 SQ 中取指;
第四步,SSD 执行指令;
第五步,指令执行完成,SSD 往 CQ 中写指令执行结果;
第六步,SSD 发中断通知主机指令完成;
第七步,收到中断,主机处理 CQ,查看指令完成状态;
第八步,主机处理完 CQ 中的指令执行结果,通过 DB 回复 SSD。
主机往 SQ 中写入命令,SSD 往 CQ 中写入命令完成结果。SQ 与 CQ 的关系可以是一对一的,也可以是多对一的,但不管怎样,它们是成对的:有因就有果,有 SQ 就必然有 CQ。
对于 Admin 命令 和 I/O 命令,它们各有自己的 SQ 和 CQ 队列。Admin SQ/CQ 和 I/O SQ/CQ 各司其职,不能把 Admin 命令放到 I/O SQ 中,同样也不能把 I/O 命令放到 Admin SQ 里面。I/O SQ/CQ 不是一生下来就有的,它们是通过 Admin 命令创建的。
系统中只能有 1 对 Admin SQ/CQ,它们是一一对应的关系;I/O SQ/CQ 却可以多达 65 535 对(64K 减去 1 对 Admin SQ/CQ)。
主机端每个 CPU 核(Core)可以有一个或者多个 SQ,但只有一个 CQ。因为性能有需求,一个 CPU 核中有多线程,可以做到一个线程独享一个 SQ;二是 QoS 有需求。这取决于系统配置和性能需求,可灵活设置 I/O SQ 个数。
作为队列,每个 SQ 和 CQ 都有一定的深度:对 Admin SQ/CQ 来说,其深度可以是 2 ~ 4096(4K);对 I/O SQ/CQ 来说,深度可以是 2 ~ 65 536(64K)。在主机中,SQ/CQ 的个数和深度是可以灵活配置的。
每个 SQ 放入的是命令条目,无论是 Admin 还是 I/O 命令,每个命令的条目大小都是 64B;每个 CQ 放入的是命令完成状态信息条目,每个条目大小是 16B。
总结:
SQ 用于主机发送命令,CQ 用于 SSD 回复命令完成状态;
SQ/CQ 可以在主机的内存中,也可以在 SSD 中,但一般在主机内存中(本书中除非特殊说明,不然都是基于 SQ/CQ 在主机内存中进行介绍);
Admin 和 I/O 两种类型的 SQ/CQ,前者发送 Admin 命令,后者发送 I/O 命令;
系统中只能有一对 Admin SQ/CQ,但可以有很多对 I/O SQ/CQ;
I/O SQ 与 CQ 可以是一对一的关系,也可以是多对一的关系;
可以赋予 I/O SQ 不同优先级的;
I/O SQ/CQ 深度可达 64K,Admin SQ/CQ 深度可达 4K;
I/O SQ/CQ 的广度和深度都可以灵活配置;
每条命令是 64B,每条命令完成状态是 16B。
SQ/IQ 队列结构
队伍头部的部分表示正在被服务或者等待被服务,一旦其服务完成,就会离开队伍。尾部决定了新来的人站的位置。DB 就是用来记录一个 SQ 或者 CQ 的头和尾。每个 SQ 或者 CQ 都有两个对应的 DB——Head DB(头部 DB)和 Tail DB(尾部 DB)。DB 是 SSD 端的寄存器,记录 SQ 和 CQ 的头和尾巴的位置。
如下图所示是一个队列生产者 / 消费者(Producer/Consumer)模型。生产者往队列的尾部写入东西,消费者从队列的头部取出东西。对一个 SQ 来说,它的生产者是主机,因为它向 SQ 的尾部写入命令,消费者是 SSD,因为它从 SQ 的头部取出指令并执行;对一个 CQ 来说,刚好相反,生产者是 SSD,因为它向 CQ 的尾部写入命令完成信息,消费者则是主机,它从 CQ 的头部取出命令完成信息。
发送演示
主机向SSD发送消息
1)开始假设 SQ1 和 CQ1 是空的,Head = Tail = 0。
2)这个时候,主机往 SQ1 中写入了 3 条命令,SQ1 的 Tail 变成 3。主机往 SQ1 写入 3 条命令后,然后更新 SSD 控制器端的 SQ1 Tail DB 寄存器的值为 3。主机更新这个寄存器的同时,也是在告诉 SSD 控制器:有新命令了,去我那里取一下。
3)SSD 控制器收到通知后派人去 SQ1 把 3 条命令都取回来执行。SSD 把 SQ1 的 3 条命令都消耗了,SQ1 的 Head 也调整为 3,SSD 控制器会把这个 Head 值写入本地的 SQ1 Head DB 寄存器。
4)SSD 执行完 2 条命令,于是往 CQ1 中写入 2 条命令完成信息,将 CQ1 对应的 Tail DB 寄存器的值更新为 2。同时发消息给主机:有命令完成,请注意查看。
5)主机收到 SSD 的短信通知(中断信息),于是从 CQ1 中取出那 2 条完成信息。处理完毕,主机又将 CQ1 Head DB 寄存器中 CQ1 的 Head 值更新为 2。
DB 工作流程
在这其中,我们要特别关注一下DB的工作流程
DB 用于记录 SQ 和 CQ 的头和尾。对 SQ 来说,SSD 是消费者,它直接和队列的头打交道,很清楚 SQ 的头在哪里,所以 SQ Head DB 由 SSD 自己维护;它不知道队伍有多长,尾巴在哪,后面还有多少命令等待执行,相反,主机知道,所以 SQ Tail DB 由主机来更新。SSD 结合 SQ 的头和尾,就知道还有多少命令在 SQ 中等待执行了。对 CQ 来说,SSD 是生产者,它很清楚 CQ 的尾巴在哪里,所以 CQ Tail DB 由自己更新,但是 SSD 不知道主机处理了多少条命令完成信息,这需要主机告知,因此 CQ Head DB 由主机更新。SSD 根据 CQ 的头和尾,就知道 CQ 还能不能,以及能接受多少命令完成信息。
DB 还起到了通知作用:主机更新 SQ Tail DB 的同时,也是在告知 SSD 有新的命令需要处理;主机更新 CQ Head DB 的同时,也是在告知 SSD,你返回的命令完成状态信息我已经处理。
返回状态信息
这里有一个对主机不公平的地方:主机对 DB 只能写(还仅限于写 SQ Tail DB 和 CQ Head DB),不能读。在这个限制下,我们看看主机是怎样维护 SQ 和 CQ 的。SQ 的尾部没有问题,主机是生产者,对新命令来说,它清楚自己应该站在队伍的哪个位置。但是头部呢?SSD 在取指的时候,主机对此毫不知情。主机发了取指通知后,它并不清楚 SSD 什么时候去取命令、取了多少命令。
对于这个问题的解决方法是, SSD 往 CQ 中写入命令完成状态信息(16 字节)。
SSD 往 CQ 中写入命令状态信息的同时,还把 SQ Head DB 的信息告知了主机!这样,主机中就有了 SQ 队列的头部和尾部的信息。
对于 CQ ,主机知道它队列的头部,不知道尾部。那怎么能知道尾部呢?思路很简单,还是通过 SSD 返回命令状态信息获取。
具体是这样的:一开始 CQ 中每条命令完成将条目中的 P 位初始化为 0 的工作,SSD 在往 CQ 中写入命令完成条目时,会把 P 写成 1(如果之前该位置为 1,控制器写 CQ 的时候翻转该位,即写 0)。记住一点,CQ 是在主机端的内存中,主机可以检查 CQ 中的所有内容。主机记住上次队列的尾部,然后往下一个一个检查 P,就能得出新的队列尾部了。
最后,对 DB 做个小结:
DB 在 SSD 控制器端是寄存器;
DB 记录着 SQ 和 CQ 队列的头部和尾部;
每个 SQ 或者 CQ 有两个 DB——Head DB 和 Tail DB;
主机只能写 DB,不能读 DB;
主机通过 SSD 往 CQ 中写入的命令完成状态获取队列头部或者尾部。