NVMe 协议学习笔记

NVMe 协议学习笔记

Created
Jan 26, 2024 06:00 AM
Tags
NVMe
Linux
Virtualization
Category
Coding
Last Edited
Last updated January 26, 2024
Abstract
本文记录了学习 NVMe 协议以及在 Linux 系统中实现的 NVMe 驱动的笔记。
Related to Reading List (Column)

一、Linux NVMe 驱动中的限流:

notion image
为了防止 SQ 和 CQ 队列溢出,驱动中基于 tag 实现了一套 IO 限流策略:
  1. NVMe 驱动在初始化时会调用 blk_mq_init_queue 创建用于容纳 request 的 queue,对于每个 queue Linux 驱动中可以为其指定一种用于调度 request 的 elevator 算法(通常包括 kyber、mq-deadline 以及 bfq),算法是通过 blk_mq_init_sched 初始化的,其中指定了 queue 中的 nr_requests 用于限制可容纳的最大请求。
    1. notion image
  1. 此后其会根据 nr_requests 值去创建一系列 blk_mq_tags 以及 request用于后续发送消息。
  1. 在 Linux Block 层创建新的 request 时会通过 blk_mq_get_tag 来判断软件队列 tag 是否有剩余,如果有剩余则下发 IO 到软件缓冲队列中,如果无剩余则循环 sleep 等待。
    1. notion image
      另外初始化完成后,调度算法会定期的去处理软件缓冲队列,将其中的 request 派发到对应的硬件队列中,具体是通过 blk_mq_dispatch_rq_list 进行派发的,此处通过 __blk_mq_get_driver_tag 判断硬件队列 tag 是否有剩余来进行限流。
      notion image
  1. 如果通过了限流,其就会调用 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 的位置了。
notion image

三、虚拟机重启时 NVMe 的逻辑:

流程:

  1. Linux 驱动下令删除所有的 IO Queue。
  1. Linux 驱动更新 cc.shn 通知驱动 shutdown,SPDK 关闭所有剩余的 io queue,并调用 disable_ctrlr
  1. Qemu 通过 vfio_user 下发 VFIO_USER_DEVICE_RESET 指令,SPDK 调用vnvmf_ctrlr_reset ,清理 SDBL注意根据代码注释这里可能缺少一些逻辑。
  1. 分配一系列用于 dma 的内存。
  1. Qemu 再次下发VFIO_USER_DEVICE_RESET 指令。
  1. Qemu 清理 cc 寄存器,获取vs、cap寄存器,设置 aqa acq asq 等,并触发 SPDK 调用 enable_ctrlr
  1. Qemu 创建 Admin Queue 和一个 IO Queue,之后销毁。
  1. Linux 进入启动阶段,驱动开始读取寄存器,获取 vs、cap。驱动清空 cc 寄存器,设置 aqa acq asq 等寄存器。
  1. 更新 cc 寄存器,SPDK 调用 enable_ctrlr

四、PRP & SGL

notion image
notion image

五、PersistentReservation

参考