提要
本系列文章,旨在带你开发一个NVMe SSD控制器的前端协议逻辑,只不过是在QEMU虚拟机环境中。万事开头难,前面我们花了8篇文章来写NVMe在QEMU中的初始化,一方面说明初始化的重要和琐碎,另一方面也暴露了阿呆的水平不够,要是真完全掌握的人必然是高屋建瓴,言简意赅。阿呆学艺不精,只能一个函数一个函数来带你逛NVMe的集市了。
本篇我们来看看NVMe设备在QEMU的Doorbell如何实现,探探蛋蛋说过的大宝,阿呆改名成贴心暖男——大白。
NVMe寄存器配置流程
前面我们讲到,NVMe初始化的时候注册了MMIO,如下,两个函数nvme_mmio_read, nvme_mmio_write就是来做BAR空间的读写,对BAR空间的读写最终都会调用这兄弟俩。
1 /* NVMe is Little Endian. */ 2 n->mmio_index = cpu_register_io_memory(nvme_mmio_read, nvme_mmio_write, 3 n, DEVICE_LITTLE_ENDIAN);
再来看看NVMe的BAR空间地址分配,如下,看得出来,前面0x1000之前都是寄存器空间。自然,每当Host来读写这些地址的时候,其实就是在读写对应的寄存器。0x1000之后是每个SQ,CQ队列的大白(门铃,doorbell)的地盘。
1 /* NVMe Controller Registers */ 2 enum { 3 NVME_CAP = 0x0000, /* Controller Capabilities, 64bit */ 4 // 各种寄存器 5 NVME_CMD_SS = 0x0F00, /* Command Set Specific*/ 6 NVME_SQ0TDBL = 0x1000, /* SQ 0 Tail Doorbell, 32bit (Admin) */ 7 NVME_CQ0HDBL = 0x1004, /* CQ 0 Head Doorbell, 32bit (Admin)*/ 8 NVME_SQ1TDBL = 0x1008, /* SQ 1 Tail Doorbell, 32bit */ 9 NVME_CQ1HDBL = 0x100c, /* CQ 1 Head Doorbell, 32bit */ 10 11 NVME_SQMAXTDBL = (NVME_SQ0TDBL + 8 * NVME_MAX_QID), 12 NVME_CQMAXHDBL = (NVME_CQ0HDBL + 8 * NVME_MAX_QID) 13 };
对于每个寄存器,QEMU都有对应变量,所以调用前面的都写函数读写寄存器都会反映到这些内部变量,例如:
1 case NVME_ASQ: 2 nvme_cntrl_write_config(nvme_dev, NVME_ASQ, val, DWORD); 3 *((uint32_t *) &nvme_dev->sq[ASQ_ID].dma_addr) = val; 4 break;
大白是干啥的?
如果调用nvme_mmio_write写SQ的大白地址段,大白就开始工作了。大白工作的办公室名字叫process_doorbell,参数如下。每次门铃一响,大白就通过地址addr推算出按的是哪个队列的门铃,其中偶数号的是SQ,Host给device的命令队列,奇数号的是CQ,device发给host的响应队列。
1 static void process_doorbell(NVMEState *nvme_dev, target_phys_addr_t addr, 2 uint32_t val)
先转一下蛋蛋总结的SQ和CQ的作用:
- SQ用以Host发命令,CQ用以SSD回命令完成状态
- SQ/CQ在Host 内存中;
- 两种类型的SQ/CQ:Admin和I/O,前者发送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的广度和深度都可以灵活配置;
- 每条命令大小是64字节,每条命令完成状态是16字节;
- 不要过河拆桥。
再来看蛋蛋对大白的定义:“doorbell,DB,就是用来记录了一个SQ或者CQ的Head和Tail。每个SQ或者CQ,都有两个对应的DB: Head DB和Tail DB。DB是在SSD端的寄存器,记录SQ和CQ的头和尾巴的位置。”
一句话太简单,阿呆继续友情转载一大段:
“那么,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,你返回的命令完成状态信息我已经处理,同时表示谢意。”
现实中的大白
看得出来,SQ的大白有消息,就意味着新的命令来了,赶快记下来,找人开工。CQ的大白有消息,就意味着上次汇报之后,老板已经知道SSD最近干了什么活,所以需要把这些记录清理掉,以后不用汇报了。
我们来到基层实际看看上面的理论是怎么操作的。首先看CQ队列,只看核心的代码:
1 uint16_t new_head = val & 0xffff; 2 queue_id = (addr - NVME_CQ0HDBL) / QUEUE_BASE_ADDRESS_WIDTH; 3 nvme_dev->cq[queue_id].head = new_head; 4 5 if (nvme_dev->cq[queue_id].tail != nvme_dev->cq[queue_id].head) { 6 /* more completion entries, submit interrupt */ 7 isr_notify(nvme_dev, &nvme_dev->cq[queue_id]); 8 }
可以看出来,通过大白的地址算出来队列编号queue_id,接着更新CQ的head到上级领导写下来的位置,意味着下次从这个新的位置开始汇报工作。最后,再检查一下,是不是还有没处理完的CQ,如果有,就触发中断,申请上级领导在百忙之中抽出时间听取最新工作汇报。
接下来,看看SQ队列的大白要干些啥子事情。计算队列编号和拿寄存器数据和CQ一样,只不过寄存器内容变成了队列的尾巴。因为写从tail往后填,读从head开始拿,所以领导往SQ尾巴填命令,SSD就得知道尾巴填到哪里了。大白发现上级领导有新的工作任务发下来了,就设置定时器,5微秒之后让负责的人执行。可见分工很严密啊,大白忠心耿耿,所以只负责传达命令。
1 uint16_t new_tail = val & 0xffff; 2 queue_id = (addr - NVME_SQ0TDBL) / QUEUE_BASE_ADDRESS_WIDTH; 3 nvme_dev->sq[queue_id].tail = new_tail; 4 5 deadline = qemu_get_clock_ns(vm_clock) + 5000; 6 7 if (nvme_dev->sq_processing_timer_target == 0) { 8 qemu_mod_timer(nvme_dev->sq_processing_timer, deadline); 9 nvme_dev->sq_processing_timer_target = deadline; 10 }
大白的活是干完了,但是设置了定时器,到时候活得有人干啊,下文我们就来看看到底是谁在处理SQ。