本文总结 NVMe 的 Linux 驱动是如何实现的。
Update: 2022 / 11 / 2
系列文章
驱动 | Linux | NVMe - 1. 内核驱动
总览
NVMe (Non-VolatileMemory express),是一种建立在 M.2 接口上的类似 AHCI 的一种协议,是专门为闪存类存储设计的协议。
NVMe 具体优势包括:
- 性能有数倍的提升;
- 可降低延迟超过50%;
NVMe PCIe SSD可提供的IOPs十倍于高端企业级SATA SSD;- 自动功耗状态切换和动态能耗管理功能大大降低功耗;
- 支持未来十年技术发展的可扩展能力。
码农该怎么理解?——
- 问:它是一个存储协议,既然是存储协议是不是需要快速的读写?
答:对。 -
PCIe才是最快的协议啊,为啥不用PCIe呢?
答:PCIe很复杂的。 - 问:那我们给
PCIe穿个马甲,就可以?
答:NVMe就是给PCIe穿个马甲。 - 问:
NVMe是怎么做到的?
答:PCIe是作文题,NVMe是选词填空,最后的结果却一样。 - 问:怎么填?填什么?
答:按照这个表格填写,发什么就填什么,总共64字节,不需要的填0就行了。
IO Command |
|||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| appmask | apptag | reftag | dsmgmt | slba | addr | metadata | rsvd | nblocks | control | Flags | Opcode |
Admin Command |
|||||||||||
| rsvd11 | numd | offset | lid | prp2 | prp1 | rsvd1 | command_id | flags | Opcode |
NVMe 是一种 Host 与 SSD 之间通讯的协议,制定了 Host 与 SSD 之间通讯的命令,以及命令如何执行的,它在协议栈中隶属高层,

NVMe 离不开 PCIe,NVMe SSD 是 PCIe 的 endpoint。PCIe 是 x86 平台上一种流行的 bus 总线,由于其 Plug and Play 的特性,目前很多外设都通过 PCI Bus 与 Host 通信,甚至不少 CPU 的集成外设都通过 PCI Bus 连接,如 APIC等。
NVMe SSD 在 PCIe 接口上使用新的标准协议 NVMe ,由大厂 Intel 推出并交由 nvmexpress 组织推广,现在被全球大部分存储企业采纳 1。
NVMe SSD 本身是一个块设备,因此 NVMe 的驱动也是遵循块设备的驱动架构。
本文基于 Linux 4.1.12 版本的内核( 其它版本的内核代码可能略有不同,但不影响理解)通过两部分介绍 NVMe 的驱动程序 2:
- 操作系统如何创建
NVMe块设备 NVMe的主要流程,包括读写流程和管理流程等
NVMe 命令
NVMe Host 和 NVMe Controller 通过 NVMe Command 进行信息交互。
NVMe Command 是 Host 与 SSD Controller 交流的基本单元,应用的 I/O 请求也要转化成 NVMe Command。
NVMe Spec 中定义了 NVMe Command 的格式,占用 64 字节。
NVMe Command 分为 Admin Command 和 IO Command 两大类,前者主要是 Host 用于管理和控制 SSD,后者用于 Host 和 SSD 之间的数据传输。
发送的太快我来不及执行咋办?——
搞两个缓冲区吧:
- 发送缓冲区
SubmissionQueue(SQ) - 完成缓冲区
CompletionQueue(CQ)
处理完了,我该怎么告诉你呢?——
- 写
Doorbell Register(DB)
这个系统结构可以下图表示,

这个 namespace 是什么?——
每个 flash 块就是一个 namaspce,它有个 id ,叫 namaspce ID。
NVMe 到 SDD 是怎么玩的?——
举例 Host 需要从 flash 地址 0x02000000 上读取 nblock = 2 的数据,PRP1 给出内存地址是0x10000000,该怎么操作?
首先我们得组包 nvme_cmd,这个包为读命令,它包含我们读地址( 0x02000000 )、长度( nblock = 2 )、和读到什么地方( PRP ),然后把这个包扔给 SQ,写 doorbell 通知控制器来取命令,控制器取出命令来转换为 TLP 包通过 PCIe Memory 方式把 0x02000000 的数据写入到0x10000000 中,然后在 CQ 的尾部写入完成标志,再写 doorbell 告诉控制器我的事干完了。
-
- 这个命令放在
SQ里;
- 这个命令放在
-
Host通过写SQ的Tail DB,通知SSD来取命令;
-
SSD收到通知,去Host端的SQ中取指。PCIe是通过发一个Memory Read TLP到Host的SQ中取命令的;
-
SSD执行读命令,把数据从闪存中读到缓存中,然后把数据传给Host;
-
SSD往Host的CQ中返回状态;
-
SSD采用中断的方式告诉Host去处理CQ;
-
Host处理相应的CQ
PCI 总线
参考这里 1
在系统启动时,BIOS 会枚举整个 PCI 的总线,之后将扫描到的设备通过 ACPI tables 传给操作系统。当操作系统加载时,PCI Bus 驱动则会根据此信息读取各个 PCI 设备的 Header Config 空间,从 class code 寄存器获得一个特征值。
class code 是 PCI bus 用来选择哪个驱动加载设备的唯一根据。NVMe Spec 定义的 class code 是 010802h。NVMe SSD 内部的 Controller PCIe Header 中 class code 都会设置成 010802h。

所以,需要在驱动中指定 class code 为 010802h,将 010802h 放入 pci_driver nvme_driver 的 id_table。之后当nvme_driver 注册到 PCI Bus 后,PCI Bus 就知道这个驱动是给 class code=010802h 的设备使用的。nvme_driver 中有一个 probe 函数,nvme_probe(),这个函数才是真正加载设备的处理函数。
#define PCI_CLASS_STORAGE_EXPRESS 0x010802
static const struct pci_device_id nvme_id_table[] = {
……
{
PCI_DEVICE_CLASS(PCI_CLASS_STORAGE_EXPRESS, 0xffffff) },
……
};
注册和初始化驱动
参考这里 1
我们知道首先是驱动需要注册到PCI总线。那么nvme_driver是如何注册的呢?
当驱动被加载时就会调用 nvme_init ( drivers/nvme/host/pci.c 4 ) 函数,如下所示,
static int __init nvme_init(void)
{
BUILD_BUG_ON(sizeof(struct nvme_create_cq) != 64);
BUILD_BUG_ON(sizeof(struct nvme_create_sq) != 64);
BUILD_BUG_ON(sizeof(struct nvme_delete_queue) != 64);
BUILD_BUG_

本文深入探讨了NVMe在Linux环境下的驱动实现原理,涵盖了从硬件接口到软件架构的全过程,重点介绍了NVMe命令格式、队列管理、DMA机制及IO处理流程。

1005

被折叠的 条评论
为什么被折叠?



