0%

【计算机体系结构】NVMe 架构

NVMe 是一种Host与SSD之间通讯的协议,它在协议栈中隶属高层。

img

NVMe在协议栈中处于应用层或者命令层,它是指挥官,军师,在三国的话,就是诸葛亮的角色。”运筹帷幄之中,决胜千里之外”。军师设计好计谋,就交由手下五虎大将去执行。NVMe的手下大将就是PCIe,它所制定的任何命令,都交由虎将PCIe去完成。虽然NVMe的命令可能可以由别的接口协议完成,但NVMe与PCIe合作形成的战斗力无疑是最强的。

NVMe是为SSD所生的。NVMe出现之前,SSD绝大多数走的是AHCI和SATA的协议,后者其实是为传统HDD服务的。与HDD相比,SSD具有更低的延时和更高的性能,AHCI已经不能跟上SSD性能发展的步伐了,已经成为制约SSD性能的瓶颈。所有SATA接口的SSD,你去看性能参数,会发现都不会超过600MB/s。如果碰到有人跟你说它的SATA SSD读取性能可以超过600MB/s,直接拨打110报警。不是底层Flash带宽不够,是SATA接口速度限制了,因为SATA现在最高带宽就是600MB/s。OK,既然SATA接口速度太慢,我用PCIe好了,不过上层协议还是AHCI。五虎上将有了,由刘备指挥,让人不禁感叹暴殄天物呀。刘备什么水平,诸葛亮出现之前,居无定所,一会跟着曹操混,一会又跟着吕布混,谁肯收留就跟谁混。惨呀!AHCI和刘备一个德行,只有一个命令队列,最多同时只能发32条命令,HDD时代(群雄逐鹿)还能混混,SSD时代(三足鼎立)就只有被灭的份。刘备需要三顾茅庐,需要诸葛亮的辅佐。同样,SSD需要PCIe,更需要NVMe。

在这样的背景下,Intel等巨头携天子以令诸侯,集大家智慧,制定出了NVMe规范,目的就是释放SSD性能潜力,解SSD倒悬之苦。

Capture

上面只列了几个巨头,参与的公司远不止这些。没有上榜的公司不要见怪。

NVMe制定了Host与SSD之间通讯的命令,以及命令如何执行的。

NVMe有两种命令,一种叫Admin Command,用以Host管理和控制SSD;另外一种就是I/O Command,用以Host和SSD之间数据的传输。下面是NVMe1.2支持的命令列表:

NVMe支持的Admin Command:

imgimg

NVMe支持的I/O Command:

img

跟ATA spec中定义的命令相比,NVMe的命令个数少了很多,完全是为SSD量身定制的。大家现在别纠结于具体的命令,了解一下就好。老板交代干活的时候,再找spec一个一个看吧。

命令有了,那么,Host又是怎么把这些命令发送给SSD执行呢?

NVMe有三宝:Submission Queue (SQ),Completion Queue(CQ)和Doorbell Register (DB)。 SQ和CQ位于Host的内存中,DB则位于SSD的控制器内部。上图:

img

这张图信息量比较大,除了让我们知道SQ和CQ在Host的memory中以及DB在SSD端外,而且让我们对一个PCIe系统有一个具体的认识。上图中的NVMe Subsystem一般就是SSD。请看这张图几秒钟,然后闭上眼,脑补SSD所处的位置:SSD作为一个PCIe Endpoint通过PCIe连着Root Complex (RC), 然后RC连接着CPU和内存。RC是什么?我们可以认为RC就是CPU的代言人,助理,或者小蜜。作为系统中最高层,CPU说:我很忙的,你SSD有什么事情先跟我小蜜说!尽管如此,SSD的地位还是较过去提升了一级,过去SSD别说直接接触霸道总裁,就是连小蜜的面都见不到,SSD和小蜜之间还隔着一座南桥呢。滚蛋吧,南桥君!

扯远了,刚才要说什么来着。对了,是三宝。SQ位于Host内存中,Host要发送命令时,先把准备好的命令放在SQ中,然后通知SSD来取;CQ也是位于Host内存中,一个命令执行完成,成功或失败,SSD总会往CQ中写入命令完成状态。DB(大宝?)又是干什么用的呢?Host发送命令时,不是直接往SSD中发送命令的,而是把命令准备好放在自己的内存中,那怎么通知SSD来获取命令执行呢?Host就是通过写SSD端的大宝寄存器来告知SSD的:饭已OK了,下来密西吧!

OK,具体的我们来看看NVMe是如何处理命令的,看图说话:

img

这是NVMe1.2规范中的第207张图。不知道是人家图画得好呢,还是NVMe就是这么简单,抑或是我比较聪明,反正上面的命令处理流程我一看就明白了。好吧,给没我聪明的人再解释一下。

说,把大象放冰箱一共要几步?答:三步。

第一步,打开冰箱门;

第二步,放进大象;

第三步,关上冰箱门。

说,NVMe处理命令需要几步?答:八步:

第一步:Host写命令到SQ;

第二步:Host写DB,通知SSD取指;

第三步:SSD收到通知,于是从SQ中取指;

第四步:SSD执行指令;

第五步:指令执行完成,SSD往CQ中写指令执行结果;

第六步:然后SSD发短信通知Host指令完成;

第七步:收到短信,Host处理CQ,查看指令完成状态;

第八步:Host处理完CQ中的指令执行结果,通过DB回复SSD:指令执行结果已处理,辛苦您了!

曹植七步作诗,NVMe就比曹植差一点,需要八步。

关于NVMe,到现在相信大家有了一些基本认识。关于更多技术细节,今天我不打算讲了。我要吸取之前的教训,比如在一篇文章里就把SSD基本原理介绍了,而不是分别介绍。这样很不讨巧,一口气写完,对自己写文章是压力,对读者读文章也是压力,对网站的浏览量也不好。阿呆的做法值得学习,一个话题,采用连载的方式推出,有朋友也这么向我建议,于是我决定采取类似方式来谈NVMe,毕竟NVMe是个大话题。于是,我把标题从”蛋蛋读NVMe”改成”蛋蛋读NVMe之一”,后面还有之二,之三。。。接下来《蛋蛋读NVMe之二》我会详细解读NVMe的三宝 (SQ,CQ,DB),敬请期待。


上回书说道,NVMe有三宝:SQ,CQ和DB。接下来我们就详细的看看这吉祥三宝。

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

640?wx_fmt=png

有两种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)。行政人员少,干活的人多,很多公司都是这样的吧,所以Admin SQ/CQ少,I/O SQ/CQ多就不难理解了。Host端每个Core可以有一个或者多个SQ,但只有一个CQ。给每个Core分配一对SQ/CQ好理解,为什么一个Core中还要多个SQ呢?一是性能需求,一个Core中有多线程,可以做到一个线程独享一个SQ;二是QoS需求,什么是QoS?Quality of Service,服务质量。脑补一个场景,蛋蛋一边看小电影,同时迅雷在后台下载小电影,由于电脑配置差,看个小电影都卡。蛋蛋最讨厌看小电影的时候卡顿了,因为你刚刚燃起的激情会被那个缓冲浇灭。所以,蛋蛋不要卡顿!怎么办?NVMe建议,你设置两个SQ,一个赋予高优先级,一个低优先级,把看小电影所需的命令放到高优先级的SQ,迅雷下载所需的命令放到低优先级的SQ,这样,你那破电脑就能把有限的资源优先满足你看小电影了。至于迅雷卡不卡,下载慢不慢,这个时候已经不重要了。能让蛋蛋舒舒服服的看完一个小电影,就是好的QoS。

实际系统中用多少个SQ,取决于系统配置和性能需求,可灵活设置I/O SQ个数。关于系统中I/O SQ的个数,NVMe白皮书给出如下建议:

640?wx_fmt=png

作为队列,每个SQ和CQ都有一定的深度:对Admin SQ/CQ来说,其深度可以是2-4096(4K);对I/O SQ/CQ,深度可以是2-65536(64K)。队列深度也是可以配置的。

SQ/CQ的个数可以配置,每个SQ/CQ的深度又可以配置,因此NVMe的性能是可以通过配置队列个数和队列深度来灵活调节的。NVMe太牛了吧,想胖就胖,想瘦就瘦;想高就高,想矮就矮,整一孙悟空呀!我们已经知道,AHCI只有一个命令队列,且队列深度是固定的32,就凡人一个,和NVMe相比,无论是在命令队列广度还是深度上,都是无法望其项背的;NVMe命令队列的百般变化,更是AHCI无法做到的。说到百般变化,我突然又想到一件残忍的事情:PCIe也是可以的。一个PCIe接口,可以有1,2,4,8,12,16,32条lane!SATA都要哭了,单挑都挑不过你,你还来群殴我。总之AHCI/SATA和NVMe/PCIe 这么一比较,画面太美,蛋蛋不敢看。

蛋蛋在这里总是贬低AHCI/SATA,有人要说蛋蛋忘恩负义,过河拆桥。怎么说?想当年,你SSD刚出来的时候,要不是AHCI/SATA收留了你,辛苦把你养大,都不知道你现在在哪里流浪。现在好了,你SSD翅膀硬了,不说一句感谢的话,倒反过来嫌弃我。各位看官,误会了,前面都是演戏,不说你AHCI/SATA不好,怎么能突出我NVMe/PCIe的好,毕竟后者才是男女一号,这么做完全是剧情需要。戏外,SSD不会忘记你AHCI/SATA的好。忘恩负义?蛋蛋不是那种人。

虽然是在戏里,但总说AHCI/SATA的不好,这样真的好吗?蛋蛋是个怀旧的人,突然就有种蛋蛋的忧伤。好吧,以后就谈NVME,不说AHCI了。孰好孰坏,留与读者评说。

戏还得继续演。

每个SQ放入的是命令条目,无论是Admin还是I/O命令,每个命令条目大小都是64字节;每个CQ放入的是命令完成状态信息条目,每个条目大小是16字节。

在继续谈大宝(DB)之前,先对SQ和CQ做个小结:

  1. SQ用以Host发命令,CQ用以SSD回命令完成状态
  2. SQ/CQ可以在Host的内存中,也可以在SSD中,但一般在Host 内存中(所有系列文章都是基于SQ/CQ在Host内存中讲的);
  3. 两种类型的SQ/CQ:Admin和I/O,前者发送Admin命令,后者发送I/O命令;
  4. 系统中只能有一对Admin SQ/CQ,但可以有很多对I/O SQ/CQ;
  5. I/O SQ与CQ可以是一对一的关系,也可以是一对多的关系;
  6. I/O SQ是可以赋予不同优先级的;
  7. I/O SQ/CQ深度可达64K,Admin SQ/CQ深达4K;
  8. I/O SQ/CQ的广度和深度都可以灵活配置;
  9. 每条命令大小是64字节,每条命令完成状态是16字节;
  10. 不要过河拆桥。

SQ/CQ中的”Q”,是Queue,队列的意思,无论SQ还是CQ,都是队列,并且是环形队列。队列有几要素,除了队列深度,队列内容,还有两个重要的,就是队列的头(Head)和尾巴(Tail)。大家都排过队,你加入队伍的时候,都是站到队伍的最后,如果你插队,蛋蛋就会鄙视你。队伍最前头的那个,正在被服务或者等待被服务,一旦完成,就离开队伍。队列的头尾很重要,头决定谁会被马上服务,尾巴决定了新来的人站的位置。DB,就是用来记录了一个SQ或者CQ的Head和Tail。每个SQ或者CQ,都有两个对应的DB: Head DB和Tail DB。DB是在SSD端的寄存器,记录SQ和CQ的头和尾巴的位置。

640?wx_fmt=png

上面是一个队列的生产/消费模型。生产者往队列的Tail写入东西,消费者往队列的Head取出东西。对一个SQ来说,它的生产者是Host,因为它往SQ的Tail位置写入命令,消费者是SSD,因为它往SQ的Head取出指令执行;对一个CQ来说,刚好相反,生产者是SSD,因为它往CQ的Tail写入命令完成信息,消费者则是Host,它从CQ的Head取出命令完成信息。

举个例子,看图说话.

\1. 开始假设SQ1和CQ1是空的,Head = Tail = 0.

640?wx_fmt=png

\2. 这个时候,Host往SQ1中写入了三个命令,SQ1的Tail则变成3。Host在往SQ1写入三个命令后,同时漂洋过海去更新SSD Controller端的SQ1 Tail DB寄存器,值为3。Host更新这个寄存器的同时,也是在告诉SSD Controller:有新命令了,需要你去取。

640?wx_fmt=png

\3. SSD Controller收到通知后,于是派人去SQ1把3个命令都取回来执行。SSD把SQ1的三个命令都消费了,SQ1的Head从而也调整为3,SSD Controller会把这个Head值写入到本地的SQ1 Head DB寄存器。

640?wx_fmt=png

\4. SSD执行完了两个命令,于是往CQ1中写入两个命令完成信息,同时更新CQ1对应的Tail DB 寄存器,值为2。SSD并且发消息给Host:有命令完成,请注意查看。

640?wx_fmt=png

\5. Host收到SSD的短信通知,于是从CQ1中取出那两条完成信息处理。处理完毕,Host又漂洋过海的往CQ1 Head DB寄存器中写入CQ1的head,值为2。

640?wx_fmt=png

看完这个例子,又重温了一下命令处理流程。之前我们也许只记住了命令处理需要8步(距离曹植一步之遥),看完上面的例子,我们应该对命令处理流程有个更深入具体的认识。

那么,DB在命令处理流程中起了什么作用呢?

首先,如前所示,它记住了SQ和CQ的头和尾。对SQ来说,SSD是消费者,它直接和队列的头打交道,很清楚SQ的头在哪里,所以SQ head DB由SSD自己维护;但它不知道队伍有多长,尾巴在哪,后面还有多少命令等待执行,相反,Host知道,所以SQ Tail DB由Host来更新。SSD结合SQ的头和尾,就知道还有多少命令在SQ中等待执行了。对CQ来说,SSD是生产者,它很清楚CQ的尾巴在哪里,所以CQ Tail DB由自己更新,但是SSD不知道Host处理了多少条命令完成信息,需要Host告知,因此CQ Head DB由Host更新。SSD根据CQ的头和尾,就知道CQ能不能以及能接受多少命令完成信息。

DB的另外一个作用,就是通知作用:Host更新SQ Tail DB的同时,也是在告知SSD有新的命令需要处理;Host更新CQ Head DB的同时,也是在告知SSD,你返回的命令完成状态信息我已经处理,同时表示谢意。

这里有一个对Host不公平的地方,Host对DB只能写,还仅限于写SQ Tail DB和CQ Head DB,不能读取DB。蛋蛋突然想唱首歌:

我俩太不公平爱和恨全由你操纵可今天我已离不开你不管你爱不爱我

Host就是这样痴情。在这个限制下,我们看看Host是怎样维护SQ和CQ的。SQ的尾巴没有问题,Host是生产者,对新命令来说,它清楚自己应该站在队伍哪里。但是Head呢?SSD在取指的时候,是偷偷进行的,Host对此毫不知情。Host发了取指通知后,它并不清楚SSD什么时候去取命令,取了多少命令。怎么破?机智如你,如果是你,你会怎么做?山人自有妙计。给个提示:

640?wx_fmt=png

这是什么鬼东西?这是SSD往CQ中写入的命令完成状态信息(16字节)。

是的,SSD往CQ中写入命令状态信息的同时,还把SQ Head DB的信息告知了Host!!这样,Host对SQ中Head和Tail的信息都有了,轻松玩转SQ。

CQ呢?Host知道Head,不知道Tail。那怎么能知道Tail呢?思路很简单,既然你SSD知道,那你告诉我呗!SSD怎么告诉Host呢?还是通过SSD返回命令状态信息中。哈哈,看到上图中的“P”吗?干什么用,做标记用。

640?wx_fmt=png

具体是这样的:一开始CQ中每条命令完成条目中的”P” bit初始化为0,SSD在往CQ中写入命令完成条目时,会把”P”写成1。记住一点,CQ是在Host端的内存中,Host可以检查CQ中的所有内容,当然包括”P”了。Host记住上次的Tail,然后往下一个一个检查”P”,就能得出新的Tail了。就是这样。

最后,给大宝做个小结:

  1. DB在SSD Controller端,是寄存器
  2. DB记录着SQ和CQ的Head和Tail
  3. 每个SQ或者CQ有两个DB: Head DB 和Tail DB
  4. Host只能写DB,不能读DB
  5. Host通过SSD往CQ中写入的命令完成状态获取Head或者Tail

有个人一直在思考三个问题:我是谁?我从哪里来?我要去哪里?

你猜这个人最后怎么着?

成了哲学家?

疯了?

疯了的哲学家?

我觉得无外乎这三种结果了。

相比人的世界,这三个问题在NVMe的世界就很容易得到答案了,至少不会把人逼疯。

我是数据,我从Host来,要到SSD去,或者,我从SSD来,要去到Host。

img

Host如果想往SSD上写入用户数据,需要告诉SSD写入什么数据,写入多少数据,以及数据源在内存中的什么位置,这些信息包含在Host向SSD发送的Write命令中。每笔用户数据对应着一个叫做LBA(Logical Block Address)的东西,Write命令通过指定LBA来告诉SSD写入的是什么数据。对NVMe/PCIe来说,SSD收到Write命令后,通过PCIe去Host的内存数据所在位置读取数据,然后把这些数据写入到闪存中,同时得到LBA与闪存位置的映射关系。

Host如果想读取SSD上的用户数据,同样需要告诉SSD需要什么数据,需要多少数据,以及数据最后需要放到Host内存的哪个位置上去,这些信息包含在Host向SSD发送的Read命令中。SSD根据LBA,查找映射表,找到对应闪存物理位置,然后读取闪存获得数据。数据从闪存读上来以后,对NVMe/PCIe来说,SSD会通过PCIe把数据写入到Host指定的内存中。这样就完成了Host对SSD的读访问。

在上面的描述中,大家有没有注意到一个问题,那就是Host在与SSD的数据传输过程中,Host是被动的一方,SSD是主动的一方。你Host需要数据,是我SSD主动把数据写入到你的内存中;你Host写数据,同样是我SSD主动去你Host的内存中取数据,然后写入到闪存。SSD跟快递小哥一样辛劳,不仅送货上门,还上门取件。之前蛋蛋还为Host不能读取DB打抱不平,现在看来,Host不值得同情,太懒了。

无论送货上门,还是上门取件,你都需要告诉快递小哥你的地址,不然茫茫人海,快递小哥怎么就能找到你呢?同样的,Host你不亲自传输数据,那总该告诉我SSD去你内存中什么地方取用户数据,或者要把数据写入到你内存中的什么位置。你在告诉快递小哥送货地址或者取件地址时,会说XX路XX号XX弄XX楼XX室,也可能会说XX小区XX楼XX室,anyway,快递小哥能找到就行。Host也有两种方式来告诉SSD数据所在内存位置,一是PRP (Physical Region Page, 不是P2P!),二是SGL (Scatter/Gather List)。不过,后者感觉不怎么友善,因为怎么听起来都像”死过来”(SGL)。当然了,也可能是我误会了,人家只是在说”送过来”。

先说PRP。

NVMe把Host的内存划分为一个一个页(Page),页的大小可以是4KB,8KB,16KB… 128MB。

PRP是什么,长什么样呢?

img

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

img

PRP Entry描述的是一段连续的物理内存的起始地址。如果需要描述若干个不连续的物理内存呢?那就需要若干个PRP Entry。把若干个PRP Entry链接起来,就成了PRP List。

img

是的,正如你所见,PRP List中的每个PRP Entry的偏移量都必须是0,PRP List中的每个PRP Entry都是描述一个物理页。它们不允许有相同的物理页,不然SSD往同一个物理页写入几次的数据,导致先写入的数据被覆盖。

每个NVMe命令中有两个域:PRP1和PRP2,Host就是通过这两个域告诉SSD数据在内存中的位置或者数据需要写入的地址。

img

PRP1和PRP2有可能指向数据所在位置,也可能指向PRP List。类似C语言中的指针概念,PRP1和PRP2可能是指针,也可能是指针的指针,还有可能是指针的指针的指针。别管你包的有多严实,根据不同的命令,SSD总能一层一层的剥下包装,找到数据在内存的真正物理地址。SSD善解人衣。

下面是一个PRP1指向PRP List的示例:

img

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

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

img

Host在命令中会告诉SSD采用何种方式。具体来说,如果命令当中DW0[15:14]是0,就是PRP的方式,否则就是SGL的方式。

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

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

img

有4种SGL Descriptor,一种是Data Block,这个好理解,就是描述的这段空间是用户数据空间;一种是Segment描述符。SGL不是由SGL Segment组成的链表吗?既然是链表,前面一个Segment就需要有个指针指向下一个Segment,这个指针就是SGL Segment描述符,它描述的是它下个Segment所在的空间。特别地,对链表当中倒数第二个Segment,它的SGL Segment描述符我们把它叫做SGL Last Segment描述符。它本质还是SGL Segment描述符,描述的还是SGL Segment所在的空间。为什么需要把倒数第二个SGL Segment描述符单独的定义成一种类型呢?我认为是让SSD在解析SGL的时候,碰到SGL Last Segment描述符,就知道链表快到头了,后面只有一个Segement了。那么,SGL Bit Bucket是什么鬼?它只对Host读有用,用以告诉SSD,你往这个内存写入的东西我是不要的。好吧,你既然不要,我也就不传了。

说了这么多,可能有点晕,结合下张图,可能会更明白点。

img

如果还是晕,看个例子吧。

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

img

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

大家也许跟我有个同样的疑问(自作多情?),那就是,既然有PRP,为什么还需要SGL?事实上,NVMe1.0的时候的确只有PRP,SGL是NVMe1.1之后引入的。SGL和PRP本质的区别在哪?下图道出了真相:一段数据空间,对PRP来说,它只能映射到一个个物理页,而对SGL来说,它可以映射到任意大小的连续物理空间。

啊

这章就到这吧。下面《蛋蛋读NVMe之四》,蛋蛋会带大家走基层,看看一个NVMe读写命令在PCIe层是怎样实现的。精彩继续,不要错过。


img

今天我又把这张图搬出来了。没错,它是《蛋蛋读NVMe之一》里面的第一张图。任何一种计算机协议,它都是采用这种分层结构的。下层总是为上层服务的。有些协议,上图所有的层次都有定义和实现,而有些协议,只定义了其中的几层。然而,要让一种协议能工作,它需要一个完整的协议栈,PCIe定义了下三层,NVMe定义了最上层,两者一拍即合,构成一个完整的Host与SSD通讯的协议。

PCIe与NVMe最直接接触的是传输层。在NVMe层,我们能看到的是64字节的命令,16字节的命令返回状态,以及跟命令相关的数据。而在PCIe的传输层,我们能看到的是TLP (Transaction Layer Packet)。还是跟快递做类比,你要寄东西,可能是手机,可能是电脑,不管是什么,你交给快递小哥,他总是把你要寄的东西打包,快递员看到的就是包裹,他根本不关心你里面的内容。PCIe传输层作为NVMe最直接的服务者,不管你NVMe发给我的是命令,还是命令状态,还是用户数据,我统统帮你放进包裹,打包后交给下一层,让数据链路层继续处理。

img

今天不打算深入讲解PCIe,这又是一个大的话题。SSD FANS可能后续会推出类似NVMe系列文章,来个PCIe系列的,大家可以期待一下。对PCIe,我们今天只关注传输层,因为它跟NVMe接触是最直接最亲密的。PCIe传输层传输的是TLP,它就是个包裹,一般由包头和数据组成,当然也有可能只有包头没有数据。NVMe传下来的数据都是放在TLP的数据部分的(Payload)。为实现不同的目的,TLP可分为以下几种类型:

  1. Configuration Read/Write
  2. I/O Read/Write
  3. Memory Read/write
  4. Message
  5. Completion

注意,这个Completion跟NVMe层的Completion不是同一个东西,他们处在不同层。在NVMe命令处理过程中,PCIe传输层基本只用Memory read/write TLP来为NVMe服务,其他TLP我们不用管。

Host发送一个Read命令,PCIe是怎么服务的?今天主要目的,就是结合NVMe命令处理流程,蛋蛋带着大家把下面这张图看懂,看看NVMe和PCIe的传输层发生了什么。

img

我靠,密密麻麻的,什么鬼东西?别急,蛋蛋带你一步一步把它看懂。

首先,Host准备了一个Read命令给SSD:

img

也许你对NVMe Read命令格式不是很清楚,说实话,我也不清楚,但从上图,我们还是能得到下面的信息: Host需要从起始LBA 0x20E0448(SLBA)上读取128个DWORD (512字节)的数据,读到哪里去呢?PRP1给出内存地址是0x14ACCB000。这个命令放在编号为3的SQ里 (SQID = 3),CQ编号也是3 (CQID = 3)。我觉得知道这些就够了。相信看了蛋蛋读NVMe系列的,刚才说的这些都应该能懂。

当Host把一个命令准备好放到SQ后,接下来步骤是什么呢?回想一下NVMe命令处理的八个步骤。

第二步就是:Host通过写SQ的Tail DB,通知SSD来取命令

img

上图中,上层是NVMe层,下层是PCIe的传输层,这一层我们看到的是TLP。Host想往SQ Tail DB中写入的值是5。PCIe是通过一个Memory Write TLP来实现Host写CQ的Tail DB的。

一个Host,下面可能连接着若干个Endpoint,该SSD只是其中的一个Endpoint而已,那有个问题,Host怎么能准确更新该SSD Controller中的Tail DB寄存器呢?怎么寻址?其实,在上电的过程中,每个Endpoint的内部空间都会通过内存映射(memory map)的方式映射到Host的内存中,SSD Controller当中的寄存器会被映射到Host的内存,当然也包括 Tail DB寄存器。Host在用Memory Write写的时候,Address只需设置该寄存器在Host内存中映射的地址,就能准确写入到该寄存器。以上图为例,该Tail DB寄存器应该映射在Host内存地址F7C11018,所以Host写DB,只需指定这个物理地址,就能准确无误的写入到对应的寄存器中去。应该注意的是:Host并不是往自己内存的那个物理地址写入东西,而是用那个物理地址作为寻址用,往SSD方向写。否则就太神奇了,往自己内存写东西就能改变SSD中的寄存器值,那不是量子效应吗?我们的东西还没有那么玄乎。

NVMe处理命令的第三步:SSD收到通知,去Host端的SQ中取指

img

PCIe是通过发一个Memory Read TLP到Host的SQ中取指的。可以看到,PCIe需要往Host内存中读取16个DWORD的数据。为什么是16 DWORD数据,因为每个NVMe命令的大小是64个字节。从上图中,我们可以推断SQ 3当前的Head指向的内存地址是0x101A41100?怎么推断来的?因为SSD总是从Host 的SQ的Head取指的,而上图中,Address就是0x101A41100,所以我们有此推断。

在上图中,SSD往Host发送了一个Memory Read的请求,Host通过Completion的方式把命令数据返回给SSD。和前面的Memory Write不同,Memory Read中是不含数据,只是个请求,数据的传输需要对方发个Completion。像这种需要对方返回状态的TLP请求,我们叫它Non-Posted请求。怎么理解呢?Post,有”邮政”的意思,就像你寄信一样,你往邮箱中一扔,对方能不能收到,就看快递员的素养了,反正你是把信发出去了。像Memory Write这种,就是Posted请求,数据传给对方,至于对方有没有处理,我们不在乎;而像Memory Read这种请求,它就必须是Non-Posted了,因为如果对方不响应(不返回数据)给我,Memory Read就是失败的。所以,每个Memory read请求都有相应的Completion。

NVMe处理命令的第四步:SSD执行读命令,把数据从闪存中读到缓存中,然后把数据传给Host。数据从闪存中读到缓存中,这个是SSD内部的操作,跟PCIe和NVMe没有任何关系,因此,我们捕捉不到SSD的这个行为。我们在PCIe接口上,我们只能捕捉到SSD把数据传给Host的过程。

img

从上图中可以看出,SSD是通过Memory write TLP 把Host命令所需的128个DWORD数据写入到Host命令所要求的内存中去。SSD每次写入32个DWORD,一共写了4次。正如之前所说,我们没有看到Completion,合理。

SSD一旦把数据返回给Host,SSD认为命令以及处理完毕,第五步就是:SSD往Host的CQ中返回状态。

img

从上图中可以看出,SSD是通过Memory write TLP 把16个字节的命令完成状态信息写入到Host的CQ中。

SSD往Host的CQ中写入后,第六步就是:SSD采用中断的方式告诉Host去处理CQ。

img

SSD中断Host,NVMe/PCIe有四种方式:Pin-based interrupt, single message MSI,multiple message MSI,和MSI-X。关于中断,具体的可以参看spec 第171页,有详细介绍,有兴趣的可以去看看。从上图中,这个例子中使用的是MSI-X中断方式。跟传统的中断不一样,它不是通过硬件引脚的方式,而是把中断信息和正常的数据信息一样,PCIe打包把中断信息告知Host。上图告诉我们,SSD还是通过Memory Write TLP把中断信息告知Host,这个中断信息长度是1DWORD。

Host收到中断后,第七步就是:Host处理相应的CQ。这步是在Host端内部发生的事情,在PCIe线上我们捕捉不到这个处理过程。

最后一步,Host处理完相应的CQ后,需要更新SSD端的CQ Head DB,告知SSD CQ处理完毕。

img

跟前面一样,Host还是通过Memory Write TLP更新SSD端的CQ Head DB。

从我们抓的PCIe trace(感谢山哥提供!!除此之外,在整个NVMe系列的写作过程中,经常向山哥取经,在这深表感谢,感谢无私的帮助与解惑!)上,我们从PCIe的传输层看到了一个NVMe Read命令是怎么处理的,看到传输层基本都是通过Memory Write和Memory Read TLP传输NVMe命令、数据和状态等信息;我们确实也看到了NVMe命令处理的八个步骤,蛋蛋没有欺骗大家。

上面举的是NVMe读命令处理,其他命令处理过程其实差不多,就不凑篇幅了。

最后,我再贴出完整Trace,相信,也希望大家不会再有一团乱麻的感觉。

img


我们需要保护的是数据。Host与SSD之间,数据传输的最小单元是逻辑块(Logical Block,LB),每个逻辑块大小可以是512/520/1024/2048/4096字节等,Host在格式化SSD的时候,逻辑块大小就确定了,以后两者就按这个逻辑块大小进行数据交互。

数据从Host到NVM(Non-Volatile Memory,目前一般是闪存,后面我就用闪存来代表NVM),首先要经过PCIe传输到SSD的Controller,然后Controller把数据写入到闪存;反过来,Host想从闪存上读取数据,首先SSD Controller从闪存上获得数据,然后经过PCIe把数据传送给Host。

img

Host与SSD之间,数据在PCIe上传输的时候,由于信道噪声的存在(说白了就是存在干扰),可能导致数据出错;另外,在SSD内部,Controller与闪存之间,数据也可能发生错误。路途凶险。为确保Host与闪存之间数据的完整性,即Host写入到闪存的数据与最初Host写的数据一致,以及Host读到的数据与最初从闪存上读上来的数据一致,NVMe提供了一个端到端数据保护功能。

除了逻辑块数据本身,NVMe还允许每个逻辑块带个助理,叫做元数据(Meta Data)。这个助理的职责,NVMe虽然没有明确的要求,但如果数据需要保护,NVMe要求这个助理必须能充当保镖的角色。

元数据有两种存在方式,一种是作为逻辑块数据的扩展,和逻辑块数据放一起存放,这是贴身保镖:

img

另外一种方式就是逻辑块数据放在一起,元数据单独放在别处。虽不是贴身保护,但保镖在附近时刻注意着主人的安全,属非贴身保镖:

img

贴身保护与否,我们不关心形式,我们只关心元数据是如何保护逻辑块数据的。NVMe要求每个逻辑块数据的保镖配备下面这把武器:

img

其中的”Guard”是16比特的CRC (Cyclic Redundancy Check),它是逻辑块数据算出来的;”Application Tag”和”Reference Tag”包含该数据块的逻辑地址(LBA)等信息。CRC校验能够检测出数据是否有错,后者则是保证数据不会出现张冠李戴的问题,比如我LBA X使用了LBA Y的数据,这种情况往往是SSD固件Bug导致的。Anyway,NVMe能帮你发现这个问题。

佩了保镖的数据看起来就是下面这个样子(以512字节的数据块为例):

img

在Host与SSD数据传输过程中,NVMe可以让每个逻辑块数据都带上保镖,也可以让他们不带保镖,也可以在某个治安差的地方把保镖带上,然后在治安环境好的地方不用保镖。

Host往SSD写入数据,不带保镖

img

什么情况下可以不带保镖?

如果你普通人一个,完全没有必要配保镖,原因有:1. 你请不起保镖;2. 谁有空来伤害你呢?3. 太平盛世。

如果是无关紧要的数据(如小电影),完全没有必要进行端到端的保护,毕竟数据保护需要传输额外的数据 (每个逻辑数据块需要至少额外8字节的数据保护信息,有效带宽减少),还需要SSD做额外的数据完整性校验(耗时,性能变差),最关键的是PCIe通道上,其数据天然就能受到保护。怎么说?

img

对每个TLP来说,其中有个Digest域,就是对HDR和Data进行数据保护的,本质就是CRC。这个Digest是可选的。如果使能了Digest,数据在PCIe上传输是毫无风险的,因为有便衣警察保护,在NVMe层完全没有必要进行额外的数据保护。

当然,它不能发现数据张冠李戴的问题。

Host往SSD写入数据,全程带上保镖的情况

img

红色PI,Protection Information,就是传说中的保镖。NVMe居然给数据配这么一个大红显眼的保镖,我也算是服了。

Host数据通过PCIe传输到SSD Controller之间,按理来说数据已经受到PCIe的保护,但PCIe保镖也有可能不在情况,那就是TLP中Digest域可能不存在,这是PCIe允许的。这个时候,如果要保证在PCIe上数据传输的可靠性,就需要NVMe自带保镖。数据到达SSD Controller时,SSD Controller会重新计算逻辑块数据的CRC,与保镖的CRC比较,如果两者匹配,说明数据传输是没有问题的;否则,数据就是有问题的,这个时候,SSD Controller就会给Host报错。

除了CRC校验,还要检测有没有张冠李戴的问题,通过检测Reference Tag 和Application Tag,看看这个没有CRC问题的数据是不是该笔Host写命令对应的数据,如果不匹配,同样需要向Host报错。

如果数据检测没有问题,SSD Controller会把逻辑块数据和PI一同写入闪存中。这个PI一同写入到闪存中有什么意义呢?在读的时候有意义。

img

SSD Controller读闪存的时候,会对读上来的数据进行CRC校验,如果写入的时候带有PI,这个时候就能检测出读上来的数据是否正确,从而决定这个数据要不要传给Host。有人要说,对闪存来说,数据不是受ECC保护吗?为什么还要额外进行数据校验?没错,写入到闪存中的数据是受ECC保护,这个没有问题,但在SSD内部,数据从Controller到闪存之间,一般都要经过DRAM或者SRAM,在之前SSD Controller写入到闪存,或者这个时候从闪存读数据到SSD Controller,可能就会发生比特翻转之类的小概率事件,从而导致数据不正确。如果在NVMe层再做个CRC保护,这类数据错误就能被发现了。

除了数据在SSD内发生反转,由于固件问题,或者别的原因,还是会出现数据张冠李戴的问题:数据虽然没有CRC错误,但是它不是我们想要的数据。因此,还需要做Reference Tag和Application Tag检测。

SSD Controller通过PCIe把数据传给Host,Host端也会对数据进行校验,看SSD返回过来的数据是否有错。

Host往SSD写入数据,半程带保镖的情况

img

这种情况,Host与Controller端之间是没有数据保护,因为PCIe已经能提供数据完整性保证了(TLP中的Digest使能)。但在SSD内部,Controller到闪存之间,由于乱七八糟的原因(数据反转,LBA数据不匹配),存在数据错误的可能,NVMe要求SSD Controller在把数据写入到闪存前,计算好数据的PI,然后把数据和PI一同写入到闪存。

SSD Controller读闪存的时候,会对读上来的数据进行PI校验,如果没有问题,剥除PI,然后把逻辑块数据返回给Host;如果校验失败,说明数据存在问题,SSD需要向Host报错。如下图所示:

img

数据端到端保护是NVMe的一个特色,其本质就是在数据块当中加入CRC和数据块对应的LBA等冗余信息,SSD Controller或者Host端利用这个这些信息进行数据校验,然后根据校验结果执行相应的操作。加入这些检错信息的好处是能让Host与SSD Controller及时发现数据错误,副作用就是:

  1. 每个数据块需要额外的至少8字节的数据保护信息,有效带宽减少:数据块大小越小,带宽影响越大。
  2. SSD Controller需要做数据校验,影响性能。

但是,我觉得这二个副作用的影响是微乎其微的,跟数据安全性相比,这又算得了什么呢?


那么什么是Namespace (以下简称NS,不打算翻译成中文)?

img

上图中红圈圈起来的是一个NVMe子系统,通常来说就是SSD。一个NVMe SSD主要由SSD Controller,闪存空间和PCIe接口组成。如果把闪存空间划分成若干个独立的逻辑空间,每个空间逻辑块地址(LBA)范围是0到N-1 (N是逻辑空间大小),这样划分出来的每一个逻辑空间我们就叫做NS。对SATA SSD来说,一个闪存空间只对应着一个逻辑空间,与之不同的是,NVMe SSD可以是一个闪存空间对应多个逻辑空间。

每个NS都有一个名称与ID,如同每个人都有名字和身份证号码,ID是独一无二的,系统就是通过 NS的ID来区分不同的NS。

img

如上图例子,整个闪存空间划分成2个NS,名字分别是NS A和NS B,对应的NS ID分别是1和2。如果NS A大小是M (以逻辑块大小为单位),NS B大小是N,则他们的逻辑地址空间分别是0到M-1和0到N-1。Host读写SSD,都是要在命令中指定读写的是哪个NS中的逻辑块。原因很简单,如果不指定NS,对同一个LBA来说,假设就是LBA 0,SSD根本就不知道去读或者写哪里,因为有两个逻辑空间,每个逻辑空间都有LBA 0。如同我只说德州,如果不告诉你是哪个国家的,你怎知道我说的是美国德州还是山东德州。

一个NVMe命令一共64字节,其中第4到第7个Byte指定了要访问的NS。

img

对每个NS来说,都有一个4KB大小的数据结构来描述它。

img

该数据结构描述了该NS的大小,整个空间已经写了多少,每个LBA的大小,以及端到端数据保护相关设置,该NS是否属于某个Controller还是几个Controller可以共享,等等。

NS由Host创建和管理,每个创建好的NS,从Host操作系统角度看来,就是一个独立的磁盘,用户可在每个NS做分区等操作。

img

上例中,整个闪存空间划分成两个NS,NS A和NS B,操作系统看到两个完全独立的磁盘。我的天呀,太神奇了,我买一个SSD,居然得到两个磁盘,赚大发了。

每个NS是独立的,逻辑块大小可以不同,端到端数据保护配置也可以不同:你可以让一个NS使用保镖,另一个NS不使用保镖,再一个NS半程使用保镖(见《蛋蛋读NVMe之五》)。这样我就在想,是不是可以把我的SSD划分成两个NS: 一个NS使用数据端到端保护,上面存放操作系统、软件和其他重要数据,另外一个NS不使用端到端数据保护,上面只存放小电影之类的数据。

其实,NS更多的是应用在企业级,可以根据客户不同需求创建不同特征的NS,也就是在一个SSD上创建出若干个不同功能特征的磁盘(NS)供不同客户使用。

NS的另外一个重要使用场合是:SR-IOV。

什么是SR-IOV? 英文全称为 Single Root- I/O Virtualization,SR-IOV技术允许在虚拟机之间高效共享PCIe设备,并且它是在硬件中实现的,可以获得能够与本机性能媲美的I/O 性能。单个I/O 资源(单个SSD)可由许多虚拟机共享。共享的设备将提供专用的资源,并且还使用共享的通用资源。这样,每个虚拟机都可访问唯一的资源。

img

如上图所示,该SSD作为PCIe的一个Endpoint,实现了一个物理功能 (Physical Function ,PF),有4个虚拟功能(Virtual Function,VF)关联该PF。每个VF,都有自己独享的NS,还有公共的NS (NS E)。此功能使得虚拟功能可以共享物理设备,并在没有 CPU 和虚拟机管理程序软件开销的情况下执行 I/O。关于SR-IOV更多知识,请自行百度或者谷歌。这里我们只需知道NVMe中的NS有用武之地就可以。

对一个NVMe子系统来说,除了包含若干个NS,还可以由若干个 SSD Controller。注意,这里不是说一个SSD Controller有多个CPU,而是说一个SSD有几个实现了NVMe功能的Controller。

img

如上图例子,一个NVMe子系统包含了两个Controller,分别实现不同功能(也可以是相同功能)。整个闪存空间分成3个NS,其中NS A由Controller 0(左边)独享,NS C由Controller 1(右边)独享,而NS B是两者共享。独享的意思是说只有与之关联的Controller才能访问该NS,别的Controller是不能对之访问的,上图中Controller 0是不能对NS C进行读写操作的,同样,Controller 1也不能访问 NS A;共享的意思是说,该NS(这里是NS B)是可以被两个Controller共同 访问的。对共享NS,由于几个Controller都可以对它进行访问,所以要求每个Controller对该NS的访问都是原子操作,从而避免同步问题。

事实上,一个NVMe子系统,除了可以有若干个NS,除了可以有若干个Controller,还可以有若干个PCIe接口。

img

与前面的架构不一样,上图的架构是每一个Controller有自己的PCIe接口,而不是两者共享一个。Dual Port,哈哈,在SATA SSD上没有见过吧。这两个接口,往上有可能连着同一个主机,也可能连着不同的主机。现在能提供 Dual PCIe Port的SSD 接口只有SFF-8639 (关于这个接口,可参看站内文章《SFF-8639接口来袭》),也叫U.2,它支持标准的NVMe协议和Dual-Port,号称SSD接口明日之星。

img

下图是两个PCIe接口连着一个主机的情况:

img

为什么要这么玩?

我认为,一方面,Host访问SSD,可以双管齐下,性能可能更好点。不过对访问NS B来说,同一时刻只能被一个Controller访问,双管齐下又如何。考虑到还可以同时操作NS A 和 NS C,性能或多或少的有所提升。

我觉得,更重要的是,这种双接口冗余设计,可以提升系统可靠性。假设 PCIe A接口出现问题,这个时候Host可以通过 PCIe B无缝衔接,继续对NS B进行访问。当然了,NS A是无法访问了。

如果Host突然死机怎么办?据小道消息,阿法狗输给李世石那盘,就是阿法狗死机了,然后重启再战,结果超时认输。哈哈,开个玩笑。在一些很苛刻的场景下,是不允许Host宕机的。但是,是电脑总有死机的时候,怎么办?最直接有效的办法还是采用冗余容错策略:SSD有两个Controller,有两个PCIe接口,那么我主机也弄个双主机:一个主机挂了,另一个主机接管任务,继续执行,你就慢慢重启吧。

img

我们来看一个Dual Port的真实产品。

2015年,OCZ发布了业界第一个具有Dual Port的PCIe NVMe的SSD: Z-Drive 6000系列。

img

物理上,这些SSD都有两个PCIe Port,但可以通过不同的固件,实现Single Port 和Dual Port功能。

如果只用一个Port,那么它就是一个4通道的PCIe接口,向上连接一个主机:

img

如果使能Dual Port,那么可以配置成2个2通道的PCIe接口,即每个Port有两个通道。

img

具体来看,整个系统就是这个样子:

img

每个Port可以连接两个独立的Host,Host有两个独立的数据通道(Data Path)对闪存空间进行访问,如果其中一个数据通道发生故障,OCZ的Host热交换(Hot-swap )技术能让另外一个Host无缝低延时的接管任务。有些应用,比如银行金融系统、在线交易处理(OnLine Transaction Processing,OLTP)、在线分析处理(OnLine Analytical Processing,OLAP)、高性能计算(High Performance Computing ,HPC),大数据等,对系统可靠性和实时性要求非常高,这个时候,带有Dual Port的SSD就能派上用场了。

img

带有Dual Port的这种SSD,主要是面向企业用户,特别是上面提到的那些应用行业。对我们普通用户来说,我感觉使用Dual Port就没有这个必要了。

多NS,多Controller,多PCIe接口,给NVMe SSD开发者,以及存储架构师很大的发挥空间。给不同的NS配置不同的数据保护机制,或者虚拟化,或者使用冗余容错提高系统可靠性,抑或别的设计,NVMe提供了这些基础设施,怎么玩就看你的想象力了。

参考资料