驱动 | Linux | NVMe - 1. 内核驱动

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

本文总结 NVMeLinux 驱动是如何实现的。

Update: 2022 / 11 / 2


系列文章



总览

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 是一种 HostSSD 之间通讯的协议,制定了 HostSSD 之间通讯的命令,以及命令如何执行的,它在协议栈中隶属高层,

在这里插入图片描述

NVMe 离不开 PCIeNVMe SSDPCIeendpointPCIex86 平台上一种流行的 bus 总线,由于其 Plug and Play 的特性,目前很多外设都通过 PCI BusHost 通信,甚至不少 CPU 的集成外设都通过 PCI Bus 连接,如 APIC等。
NVMe SSDPCIe 接口上使用新的标准协议 NVMe ,由大厂 Intel 推出并交由 nvmexpress 组织推广,现在被全球大部分存储企业采纳 1
  
NVMe SSD 本身是一个块设备,因此 NVMe 的驱动也是遵循块设备的驱动架构。
本文基于 Linux 4.1.12 版本的内核( 其它版本的内核代码可能略有不同,但不影响理解)通过两部分介绍 NVMe 的驱动程序 2

  • 操作系统如何创建 NVMe 块设备
  • NVMe 的主要流程,包括读写流程和管理流程等

NVMe 命令

参考这里 13

NVMe HostNVMe Controller 通过 NVMe Command 进行信息交互。
NVMe CommandHostSSD Controller 交流的基本单元,应用的 I/O 请求也要转化成 NVMe Command

NVMe Spec 中定义了 NVMe Command 的格式,占用 64 字节。
NVMe Command 分为 Admin CommandIO Command 两大类,前者主要是 Host 用于管理和控制 SSD,后者用于 HostSSD 之间的数据传输。

发送的太快我来不及执行咋办?——
搞两个缓冲区吧:

  • 发送缓冲区 SubmissionQueueSQ
  • 完成缓冲区 CompletionQueueCQ

处理完了,我该怎么告诉你呢?——

  • Doorbell RegisterDB

这个系统结构可以下图表示,

在这里插入图片描述
这个 namespace 是什么?——
每个 flash 块就是一个 namaspce,它有个 id ,叫 namaspce ID

NVMeSDD 是怎么玩的?——
举例 Host 需要从 flash 地址 0x02000000 上读取 nblock = 2 的数据,PRP1 给出内存地址是0x10000000,该怎么操作?
首先我们得组包 nvme_cmd,这个包为读命令,它包含我们读地址( 0x02000000 )、长度( nblock = 2 )、和读到什么地方( PRP ),然后把这个包扔给 SQ,写 doorbell 通知控制器来取命令,控制器取出命令来转换为 TLP 包通过 PCIe Memory 方式把 0x02000000 的数据写入到0x10000000 中,然后在 CQ 的尾部写入完成标志,再写 doorbell 告诉控制器我的事干完了。

    1. 这个命令放在 SQ 里;
    1. Host 通过写 SQTail DB,通知 SSD 来取命令;
    1. SSD 收到通知,去 Host 端的 SQ 中取指。 PCIe 是通过发一个 Memory Read TLPHostSQ 中取命令的;
    1. SSD 执行读命令,把数据从闪存中读到缓存中,然后把数据传给 Host
    1. SSDHostCQ 中返回状态;
    1. SSD 采用中断的方式告诉 Host 去处理 CQ
    1. Host 处理相应的 CQ

PCI 总线

参考这里 1

在系统启动时,BIOS 会枚举整个 PCI 的总线,之后将扫描到的设备通过 ACPI tables 传给操作系统。当操作系统加载时,PCI Bus 驱动则会根据此信息读取各个 PCI 设备的 Header Config 空间,从 class code 寄存器获得一个特征值。

class codePCI bus 用来选择哪个驱动加载设备的唯一根据。NVMe Spec 定义的 class code010802hNVMe SSD 内部的 Controller PCIe Headerclass code 都会设置成 010802h
在这里插入图片描述
所以,需要在驱动中指定 class code010802h,将 010802h 放入 pci_driver nvme_driverid_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_
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值