阿呆实战NVMe之十

原创内容,转载请注明:  [http://www.ssdfans.com]  谢谢!

提要

 

本系列文章,旨在带你开发一个NVMe SSD控制器的前端协议逻辑,只不过是在QEMU虚拟机环境中。上回说到了大白接受领导写到SQ队列的命令,设定计时器,让下面的小弟到时间后执行任务。本篇就来看SQ的命令怎么执行。你知道大部分SSD公司的工程师是工作在哪个SQ队列吗?阿呆将告诉你惊人的真相。

 

 

SQ的定时回调函数

 

其实我们在NVMe初始化的时候,就注册了SQ计时器对应的回调函数,sq_processing_timer_cb,所以计时器时间一到,这个函数就被调用了。这个函数的任务很简单,就是把所有的SQ队列扫描一遍,只要那个队列Head和Tail不相等,就意味着还有活没干完,就调用干活的函数process_sq去料理一下。如果料理完,还有没干完的活,说明在干活过程中勤快的大白又分派领导下发的命令了,所以再设定一个定时器,稍后继续干。唉,大白啊大白,怪不得领导那么信任你,谁叫你那么勤快呢?就是苦了俺们这帮干活的函数,每次听大白门铃一响,就不敢偷懒了:赶快干完活回家带孩子。

 

看得出来,process_sq是执行任务的核心,SQ有Admin队列和普通IO队列,这些都要在这个函数去区分执行。核心流程很清晰,如下,就是如果Admin队列,就调用nvme_admin_command执行Admin命令,IO队列,就调用nvme_command_set执行。执行完成后,填响应的内容到CQ条目中。post_cq_entry函数会把cqe的内容复制到CQ队列的尾巴上,并触发中断,向上级领导汇报工作成果。

 1 if (sq_id == ASQ_ID) {
 2     nvme_admin_command(n, &sqe, &cqe);
 3 } else {
 4    nvme_command_set(n, &sqe, &cqe);
 5 }
 6 
 7 /* Filling up the CQ entry */
 8 cqe.sq_id = sq_id;
 9 cqe.sq_head = n->sq[sq_id].head;
10 cqe.command_id = sqe.cid;
11 post_cq_entry(n, &n->cq[cq_id], &cqe);

 

Admin队列的执行

 

上面说是所有的SQ都扫一遍,那第一个就是Admin队列了。nvme_admin_command的任务很简单,就是看Admin命令是什么,就调用对应的处理函数,如下。adm_cmds_funcs是个静态数组,里面是各种Admin命令的处理函数指针。阿呆哥还是决定把所有的函数都贴出来,满足你的好奇心。

1 f = adm_cmds_funcs[sqe->opcode];
2 ret = f(n, sqe, cqe);

 

 1 static adm_command_func * const adm_cmds_funcs[] = {
 2     [NVME_ADM_CMD_DELETE_SQ] = adm_cmd_del_sq,
 3     [NVME_ADM_CMD_CREATE_SQ] = adm_cmd_alloc_sq,
 4     [NVME_ADM_CMD_GET_LOG_PAGE] = adm_cmd_get_log_page,
 5     [NVME_ADM_CMD_DELETE_CQ] = adm_cmd_del_cq,
 6     [NVME_ADM_CMD_CREATE_CQ] = adm_cmd_alloc_cq,
 7     [NVME_ADM_CMD_IDENTIFY] = adm_cmd_identify,
 8     [NVME_ADM_CMD_ABORT] = adm_cmd_abort,
 9     [NVME_ADM_CMD_SET_FEATURES] = adm_cmd_set_features,
10     [NVME_ADM_CMD_GET_FEATURES] = adm_cmd_get_features,
11     [NVME_ADM_CMD_ASYNC_EV_REQ] = adm_cmd_async_ev_req,
12     [NVME_ADM_CMD_ACTIVATE_FW] = adm_cmd_act_fw,
13     [NVME_ADM_CMD_DOWNLOAD_FW] = adm_cmd_dl_fw,
14     [NVME_ADM_CMD_FORMAT_NVM] = adm_cmd_format_nvm,
15     [NVME_ADM_CMD_LAST] = NULL,
16 };

 

也许你觉得这些命令都很陌生,但是一旦你踏入NVMe SSD Firmware或者QA职业,那么很有可能,你每天的大部分时间就是在跟上面这些函数打交道,因为相比SSD控制器的FTL和后端Flash命令处理,前端的任务其实更琐碎,更多,而且一旦一个产品开发完成之后,FTL和后端的活很少,大部分人都要到前端处理NVMe的命令了。所以,也许对你来说,Admin队列才是养家糊口的饭碗,不要小看这些命令,每一个命令都有很多细节的协议,相当复杂,不夸张的说,如果公司大一点,这里面每个命令都可以养一个工程师。我们的社会也是如此,国家刚开始的时候,需要的是工程师领导人,建设祖国,等到经济发达了,大家都有钱了,需要的就是律师型领导人,这些人精通各种法律条文,擅长的不是建设,而是沟通,分配人群的利益,解决纠纷。本文题图就是喜剧明星金凯利演的律师,法律越复杂,律师的饭碗就越稳,协议越复杂,FW和QA的饭碗就越稳,总是有干不完的活啊!为什么阿呆这么清楚,因为阿呆当年刚入行的时候也是每天靠ATA协议吃饭,搞各种Smart Log,GPL Log之类,忙得不亦乐乎。

 

对用户来说,这些函数都挺重要的,因为它们是用户观察SSD内部的窗口。比如,我们要了解SSD内部情况,经常要查看Smart信息,处理Smart的函数流程是adm_cmd_get_log_page->adm_cmd_smart_info,里面是各种SSD的统计数据,比如Host读写命令的个数等。

 

PRP的原理

 

先来补补课,回顾一下《蛋蛋读NVMe之三》里面讲的PRP,这个是Host内存和SSD数据交互的内存页管理结构。

 

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

PRP是什么,长什么样呢?

蛋蛋读NVMe之三

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

蛋蛋读NVMe之三

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

蛋蛋读NVMe之三

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

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

蛋蛋读NVMe之三

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

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

蛋蛋读NVMe之三

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

 

读写IO命令执行

 

nvme_command_set函数内容如下,看得出来,包括读写IO命令和Trim命令:

1 if (sqe->opcode == NVME_CMD_READ ,, (sqe->opcode == NVME_CMD_WRITE)){ // 读写IO命令
2     return nvme_io_command(n, sqe, cqe);
3 } else if (sqe->opcode == NVME_CMD_DSM) { // Data Set Management,其实这就是Trim命令
4     return nvme_dsm_command(n, sqe, cqe);
5 } else if (sqe->opcode == NVME_CMD_FLUSH) {
6     return NVME_SC_SUCCESS;

 

一说到读写,你或许会觉得很复杂,不过阿呆要善意的提醒一句:这里是QEMU虚拟机,读写就是个读写文件,操作起来相当的easy。首先是通过LBA地址,计算出文件内将要读写的偏移地址和LBA个数。后面就是内存操作了,读就是往内存写数据,写就是从内存读数据。我们前文说过,QEMU通过mmap,把文件的操作等同于内存读写,读写文件的某个位置,就是操作指针和偏移地址。

 

比较绕的反而是PRP的操作。核心代码如下,首先对prp1进行读写,如果数据还没完,就看数据量是不是在一个page内,在的话,只需要读写prp2内存地址就可以了,数据量大于1个page,就需要读出prp list。

 1 /* Writing/Reading PRP1 */
 2 res = do_rw_prp(n, e->prp1, &data_size, &file_offset, mapping_addr,
 3     e->opcode);
 4 
 5 if (data_size > 0) {
 6     if (data_size <= PAGE_SIZE) {
 7         res = do_rw_prp(n, e->prp2, &data_size, &file_offset, mapping_addr,
 8             e->opcode);
 9     } else {
10         res = do_rw_prp_list(n, sqe, &data_size, &file_offset,
11             mapping_addr);
12     }
13 }

 

do_rw_prp_list函数的内容和上面的理论一致,首先通过数据大小算出有多少个prp条目,然后从prp2内存地址读出prp list,接着遍历每个prp条目,读写内存地址,与SSD对应的文件指针进行数据搬移。

 

虚拟机的Trim命令

 

其实很简单,就是每个namespace维护了一个bitmap,有Trim命令来,就在bitmap做个标记而已。

 

NVMe的metadata机制

 

这里需要额外提一下,NVMe支持一种metadata机制,就是每个LBA有一段metadata,内容是什么完全看上级领导的心情,可以是校验位,也可以是其他的。metadata有两种传输方法,一种如下图,紧跟在LBA之后连续传输。

image

 

另一种是单独传输。请你往上翻翻有张图是NVMe命令的解析,紧跟在PRP1,PRP2之后的就是metadata pointer,指向的是metadata内存地址的PRP信息,所以虚拟机里面也有metadata文件来提供metadata的读写。代码很简单,就是DMA读写,所以阿呆就不复制粘贴了。

image

 

大结局

 

至此,阿呆实战NVMe算是写完了,真是虎头蛇尾啊,花了8篇写初始化,NVMe的具体实现却只用了2篇。主要还是因为我们是虚拟机,不管SSD的具体实现,只负责NVMe协议实现。对协议来说,万事开头难,开头初始化流程理清楚了,基本就知道NVMe和PCIe的虚拟化技术,后面的真正实现反而很简单,水到渠成。

 

引用

https://github.com/nvmeqemu

分类目录 SSD, 技术文章.
扫一扫二维码或者微信搜索公众号ssdfans关注(添加朋友->点最下面的公众号->搜索ssdfans),可以经常看到SSD技术和产业的文章(SSD Fans只推送干货)。
ssdfans微信群介绍
技术讨论群 覆盖2000多位中国和世界华人圈SSD以及存储技术精英
固件、软件、测试群 固件、软件和测试技术讨论
异构计算群 讨论人工智能和GPU、FPGA、CPU异构计算
ASIC-FPGA群 芯片和FPGA硬件技术讨论群
闪存器件群 NAND、3D XPoint等固态存储介质技术讨论
企业级 企业级SSD、企业级存储
销售群 全国SSD供应商都在这里,砍砍价,会比某东便宜20%
工作求职群 存储行业换工作,发招聘,要关注各大公司招聘信息,赶快来
高管群 各大SSD相关存储公司高管和创始人、投资人

想加入这些群,请微信扫描下面二维码,或搜索nanoarchplus,加阿呆为微信好友,介绍你的昵称-单位-职务,注明群名,拉你进群。SSD业界需要什么帮助,也可以找阿呆聊。