一、Linux NVMe 驱动中的限流:
为了防止 SQ 和 CQ 队列溢出,驱动中基于 tag 实现了一套 IO 限流策略:
- NVMe 驱动在初始化时会调用
blk_mq_init_queue
创建用于容纳 request 的 queue,对于每个 queue Linux 驱动中可以为其指定一种用于调度 request 的 elevator 算法(通常包括 kyber、mq-deadline 以及 bfq),算法是通过blk_mq_init_sched
初始化的,其中指定了 queue 中的nr_requests
用于限制可容纳的最大请求。
- 此后其会根据
nr_requests
值去创建一系列blk_mq_tags
以及request
用于后续发送消息。
- 在 Linux Block 层创建新的 request 时会通过 blk_mq_get_tag 来判断软件队列 tag 是否有剩余,如果有剩余则下发 IO 到软件缓冲队列中,如果无剩余则循环 sleep 等待。
另外初始化完成后,调度算法会定期的去处理软件缓冲队列,将其中的 request 派发到对应的硬件队列中,具体是通过
blk_mq_dispatch_rq_list
进行派发的,此处通过 __blk_mq_get_driver_tag
判断硬件队列 tag 是否有剩余来进行限流。- 如果通过了限流,其就会调用
queue_rq
将其放入到 nvme 的队列中,并写 sq tail doorbell 通知 NVMe 驱动。
可以参考:
二、NVMe 中的 SQ 和 CQ:
他们队列是初始化在主机内存中的,因此控制器是只写的,而他们的寄存器则是初始化在控制器中的,因此主机是只读的。于是需要特殊的通信逻辑,对于 SQ 来讲,主机通过写 SQTD 寄存器来告知控制器有新的 SQE,但它不能读取 SQHD,因此 Head 是通过控制器写的每一个 CQE 中的 SQHP 字段来上报的。而对于 CQ 来讲,主机可以通过写 CQHD 来告知控制器消费到了哪里,而 Tail 则是由控制器发送 CQE 时的 Phase 字段来确定的,CQ 第一轮会被写 1,第二轮就被写 0,循环往复,主机通过检查 CQE 的 Phase 字段是否被翻转就可以知道 Tail 的位置了。
三、虚拟机重启时 NVMe 的逻辑:
流程:
- Linux 驱动下令删除所有的 IO Queue。
- Linux 驱动更新
cc.shn
通知驱动 shutdown,SPDK 关闭所有剩余的 io queue,并调用disable_ctrlr
。
- Qemu 通过 vfio_user 下发
VFIO_USER_DEVICE_RESET
指令,SPDK 调用vnvmf_ctrlr_reset
,清理SDBL
,注意根据代码注释这里可能缺少一些逻辑。
- 分配一系列用于 dma 的内存。
- Qemu 再次下发
VFIO_USER_DEVICE_RESET
指令。
- Qemu 清理
cc
寄存器,获取vs、cap
寄存器,设置aqa acq asq
等,并触发 SPDK 调用enable_ctrlr
。
- Qemu 创建 Admin Queue 和一个 IO Queue,之后销毁。
- Linux 进入启动阶段,驱动开始读取寄存器,获取
vs、cap
。驱动清空cc
寄存器,设置aqa acq asq
等寄存器。
- 更新
cc
寄存器,SPDK 调用enable_ctrlr
。