ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从U-Boot到Linux内核:NVMe驱动初始化与PCIe枚举全链路解析

从U-Boot到Linux内核:NVMe驱动初始化与PCIe枚举全链路解析 NVMe 这几年在存储圈的热度一直没降过但真正敢说自己吃透NVMe 驱动的人并不多。大部分人停留在会用nvme命令行工具、会看/dev/nvme0n1这种设备节点再往下走一层——比如队列是怎么建的、命令是怎么提交的、PCIe 枚举和 NVMe 控制器初始化之间是什么关系——就开始含糊了。这篇内容就是写给这批人的你已经会装系统、会分区、会跑 fio现在想搞清楚从 U-Boot 到 Linux 内核这一整条链路上NVMe 到底是怎么被点亮的。我会把 NVMe 驱动的核心机制、PCIe 枚举的来龙去脉、U-Boot 阶段的初始化、内核驱动的分层结构以及实际调试中那些文档里不会写的坑全部摊开讲一遍。适合做嵌入式存储、服务器固件、内核驱动方向的从业者也适合想从应用层往底层走的工程师。1. 先把 NVMe 和 PCIe 的关系理清楚1.1 NVMe 不是一种接口而是一套跑在 PCIe 上的协议很多人第一次接触 NVMe 会把它和 SATA、SAS 并列觉得是另一种硬盘接口。这个理解不算错但容易误导。准确地说NVMe 是一套主机软件与固态存储设备之间的通信协议它规定了命令集、队列模型、寄存器接口而它跑的物理通道是 PCIe。也就是说NVMe 设备本质上是一个 PCIe 设备它挂在 PCIe 总线上通过 PCIe 的配置空间被枚举、被分配 BAR 空间然后主机通过 MMIO 访问它的寄存器来提交命令。这个认知非常关键因为它决定了你调试 NVMe 时的排查顺序。如果一块 NVMe 盘在系统里完全看不到你第一步不该去查 NVMe 协议而应该先确认 PCIe 层面有没有枚举到它。lspci能不能看到设备、BAR 有没有分配成功、链路有没有训练到预期速率这些是 NVMe 能不能工作的前提。我见过太多人一上来就怀疑盘坏了或者驱动有问题结果发现是 PCIe 链路根本没起来。从协议栈角度看NVMe 驱动在 Linux 里位于drivers/nvme/host/目录下它依赖 PCIe 子系统完成设备发现和资源分配然后自己负责控制器的初始化、队列的创建、命令的提交与完成处理。理解这个分层后面看代码才不会迷路。1.2 队列模型NVMe 性能的根源NVMe 相比 AHCI 最大的结构性优势在于队列模型。AHCI 只有一个命令队列深度 32所有命令串行排队NVMe 支持最多 65535 个队列每个队列深度也是 65535。这个设计直接决定了 NVMe 在高并发场景下的性能天花板。具体来说NVMe 有两类队列Admin 队列和I/O 队列。Admin 队列只有一个负责控制器管理类命令比如识别控制器、创建/删除 I/O 队列、获取日志等。I/O 队列可以有多个每个队列绑定一个提交队列SQ和一个完成队列CQ主机往 SQ 里写命令设备处理完后往 CQ 里写完成项。这里有个细节值得注意SQ 和 CQ 是成对出现的但它们的对应关系可以是一对一也可以是多对一。实际驱动里通常用一对一因为这样中断处理更清晰。队列的内存是主机分配的物理地址通过 Admin 命令告诉设备设备通过 DMA 直接读写这些内存。这就是为什么 NVMe 的延迟能做到微秒级——命令提交和完成都是内存操作加门铃寄存器写入没有传统存储协议那套复杂的帧封装。1.3 门铃寄存器与命令提交的完整路径命令提交的路径是这样的主机在 SQ 的尾部写入一个命令条目然后写 SQ 的 Tail Doorbell 寄存器告诉设备有新命令了。设备从 SQ 头部开始取命令执行执行完把完成项写到 CQ 的尾部然后根据中断聚合策略决定什么时候发中断。主机收到中断后处理 CQ处理完写 CQ 的 Head Doorbell告诉设备这些完成项我处理完了。这个机制里门铃寄存器是 MMIO 地址位于设备的 BAR0 空间里。驱动初始化时会把这些寄存器的基地址映射到内核虚拟地址空间之后所有门铃操作都是内存写。理解这一点你在看驱动代码时看到writel往某个偏移写值就知道那是在敲哪个门铃。提示调试 NVMe 性能问题时门铃写入的频率和中断聚合策略是两个高频嫌疑点。中断聚合太激进会导致延迟升高太保守会导致 CPU 被中断打满。2. PCIe 枚举NVMe 设备被看见之前发生了什么2.1 枚举的本质是配置空间扫描PCIe 枚举这个词听起来很玄其实做的事情很朴素从总线 0 开始逐个扫描每个总线号、设备号、功能号组合读它们的配置空间看看有没有设备存在。如果读回来的 Vendor ID 是 0xFFFF说明这个位置没有设备如果是有效值就说明发现了一个设备。配置空间是 PCIe 设备的一组标准寄存器前 64 字节是 PCI 兼容的头部包含 Vendor ID、Device ID、Class Code、BAR 等关键信息。NVMe 设备的 Class Code 通常是 0x010802其中 0x01 是大类Mass Storage Controller0x08 是子类Non-Volatile Memory Controller0x02 是编程接口NVM Express。枚举过程中主机需要给设备分配 BAR 空间。BAR 是设备向主机申请的内存或 I/O 地址窗口NVMe 设备通常需要一个大 BAR 来映射它的寄存器。主机读 BAR 寄存器写入全 1再读回来就能知道设备需要多大的地址空间然后从可用地址池里分配一块给它。2.2 RC 和 EP 的启动顺序问题PCIe 拓扑里有两个角色RCRoot Complex和 EPEndpoint。RC 是主机侧的根复合体负责发起枚举EP 是设备侧比如 NVMe 盘。热词里有人问pcie ep先启动还是rc先启动这个问题在实际系统里很关键。标准流程是 RC 先启动完成枚举和资源分配后EP 才能被正常访问。但在一些嵌入式场景里EP 可能先上电这时候 RC 还没准备好EP 的配置空间读出来可能是无效的。解决办法通常是 RC 侧做延迟枚举或者在 EP 侧做好复位和链路训练的时序控制。我遇到过一块 FPGA 做的 NVMe EP上电后如果 RC 枚举太早链路还没训练完读配置空间全是 0xFFFF。后来在 RC 侧加了一个延时等链路稳定后再枚举问题就解决了。这个坑在标准服务器上很少遇到因为服务器 BIOS 的枚举时序已经做得很成熟了但在自研板卡上非常常见。2.3 链路训练与速率协商PCIe 链路训练是枚举之前的一步发生在物理层。两端设备通过交换 TS1/TS2 有序集来协商链路宽度和速率。比如一个 Gen3 x4 的 NVMe 盘插在 Gen3 x4 的槽位上链路训练后会以 Gen3 x4 工作如果插在 Gen2 槽位上会降速到 Gen2。调试时可以用lspci -vv查看链路状态关注LnkCap和LnkSta两个字段。LnkCap是链路能力LnkSta是当前状态。如果LnkSta显示的速率低于LnkCap说明链路没有训练到最高速率可能是信号完整性问题、连接器问题或者两端能力不匹配。注意PCIe 信号完整性对走线和连接器很敏感。M.2 接口的 NVMe 盘如果走线太长或者阻抗不匹配很容易出现链路降速甚至训练失败。设计阶段一定要按规范做阻抗控制和长度匹配。3. U-Boot 阶段的 NVMe 初始化3.1 为什么 U-Boot 也要支持 NVMe在嵌入式系统里U-Boot 的主要职责是加载内核。如果内核镜像和根文件系统放在 NVMe 盘上U-Boot 就必须能读 NVMe。这就是 U-Boot NVMe 驱动的存在意义。U-Boot 的 NVMe 驱动比 Linux 内核的简单很多它只需要支持基本的读操作不需要处理复杂的中断、多队列、电源管理。它的初始化流程通常是枚举 PCIe 总线找到 NVMe 设备初始化 Admin 队列发送 Identify 命令获取命名空间信息然后就可以通过 I/O 命令读数据了。U-Boot 里 PCIe 枚举的代码在drivers/pci/下NVMe 驱动在drivers/nvme/下。不同 SoC 的 PCIe 控制器驱动差异很大比如基于 LS1028A 的板子它的 PCIe 控制器有自己的初始化序列需要配置 PHY、时钟、复位等。3.2 U-Boot 中 NVMe 初始化的关键步骤以常见的 ARM 平台为例U-Boot 启动 NVMe 的流程大致如下PCIe 控制器初始化配置控制器寄存器使能链路训练等待链路 up。总线枚举从总线 0 开始扫描找到 NVMe 设备分配 BAR。NVMe 控制器初始化映射 BAR0读 CAP 寄存器获取队列深度等能力配置 Admin 队列。Identify 命令获取控制器信息和命名空间信息确定盘容量和扇区大小。注册块设备把 NVMe 命名空间注册成 U-Boot 的块设备供load命令使用。这个过程里最容易出问题的是第 1 步和第 3 步。PCIe 控制器初始化依赖 SoC 的具体实现时钟、复位、PHY 配置任何一个不对链路就起不来。NVMe 控制器初始化则要注意队列内存的对齐要求——Admin 队列的基地址必须按页对齐ASQ 和 ACQ 寄存器写的是物理地址。3.3 调试 U-Boot NVMe 的实用手段U-Boot 阶段没有 Linux 那么丰富的调试工具但基本手段还是有的。pci命令可以枚举和查看 PCIe 设备nvme命令可以扫描和读取 NVMe 盘。如果pci能看到设备但nvme扫描不到问题多半在 NVMe 控制器初始化如果pci都看不到问题在 PCIe 链路或枚举。我调试过一块板子U-Boot 里pci能看到 NVMe 设备但nvme scan一直失败。后来抓寄存器发现 CAP 寄存器读出来是 0说明 BAR0 映射有问题。检查设备树发现 PCIe 节点的ranges属性配错了导致 BAR 映射到了错误的地址。改过来之后一切正常。这个案例说明U-Boot 阶段的问题往往出在配置而不是代码逻辑。4. Linux 内核 NVMe 驱动的分层结构4.1 三层架构PCIe 层、NVMe 核心层、块层Linux 内核的 NVMe 驱动是分层设计的从上到下大致是块层block layer、NVMe 核心层nvme-core、NVMe PCIe 传输层nvme-pci、PCIe 子系统。块层负责通用的块设备抽象NVMe 核心层负责协议相关的逻辑nvme-pci 负责 PCIe 相关的硬件操作。这种分层的目的是支持多种传输方式。NVMe 协议不仅可以跑在 PCIe 上还可以跑在 RDMA、TCP 等传输层上对应的驱动分别是 nvme-rdma、nvme-tcp。核心层把协议逻辑抽出来传输层只负责具体的命令提交和完成处理这样新增一种传输方式只需要实现传输层接口。看代码时drivers/nvme/host/core.c是核心层drivers/nvme/host/pci.c是 PCIe 传输层。核心层里能看到nvme_setup_cmd这样的函数负责把块层的请求转换成 NVMe 命令传输层里能看到nvme_queue_rq负责把命令写到 SQ 并敲门铃。4.2 控制器初始化流程Linux 内核初始化一个 NVMe 控制器的大致流程是PCIe 设备探测PCIe 子系统发现设备匹配到 nvme 驱动的 ID 表调用 probe 函数。使能设备调用pci_enable_device分配 BAR 空间使能 DMA。映射 BAR0把 NVMe 寄存器空间映射到内核虚拟地址。控制器复位与使能写 CC 寄存器配置队列深度、页大小等然后使能控制器。创建 Admin 队列分配 Admin SQ 和 CQ 内存写 AQA、ASQ、ACQ 寄存器。Identify 控制器发送 Identify 命令获取控制器能力、命名空间列表等。创建 I/O 队列根据 CPU 数量和性能需求创建多个 I/O 队列。扫描命名空间对每个命名空间发送 Identify 命令注册块设备。这个过程里第 4 步的控制器复位和使能是最关键的。CC 寄存器里的 EN 位从 0 写到 1 后控制器开始初始化驱动需要轮询 CSTS 寄存器的 RDY 位等待控制器就绪。如果 RDY 一直不置位说明初始化失败可能是队列内存有问题或者设备状态异常。4.3 中断处理与轮询模式NVMe 驱动支持中断和轮询两种完成处理模式。中断模式下设备完成命令后发 MSI-X 中断驱动在中断处理函数里处理 CQ。轮询模式下驱动主动轮询 CQ不依赖中断。中断模式适合一般场景CPU 占用低轮询模式适合极致低延迟场景比如高频交易但会占满一个 CPU 核。驱动里可以通过io_poll参数控制轮询行为。实际调优时如果发现中断开销太大可以尝试开启轮询如果发现 CPU 占用过高就关掉轮询。提示轮询模式不是万能的。它在低队列深度、高 IOPS 场景下收益明显但在大块顺序读写场景下反而浪费 CPU。选之前先想清楚你的负载特征。5. 从 /dev/nvme0n1p5 看命名空间的命名规则5.1 设备节点命名的完整逻辑热词里有人问/dev/nvme0n1p5是不是表示第 1 个 NVMe 硬盘的第 5 个分区。这个理解基本正确但可以讲得更细。nvme0表示第 0 个 NVMe 控制器n1表示这个控制器下的第 1 个命名空间p5表示这个命名空间上的第 5 个分区。所以完整读法是第 0 个 NVMe 控制器的第 1 个命名空间的第 5 个分区。这里有个容易混淆的点控制器编号和命名空间编号是独立的。一个 NVMe 控制器可以有多个命名空间编号从 1 开始。如果系统里有两块 NVMe 盘它们分别是 nvme0 和 nvme1每块盘上的命名空间都从 n1 开始编号。分区编号则是在命名空间内部编号从 1 开始。5.2 命名空间与分区的区别命名空间是 NVMe 协议层面的概念它是控制器管理的一组逻辑块地址空间。一个命名空间可以理解成一块独立的逻辑盘有自己的容量和 LBA 格式。分区则是操作系统层面的概念是在命名空间之上划分的逻辑区域。这个区别在实际使用中很重要。比如做双系统时你可以把两个系统装在同一个命名空间的不同分区里也可以装在同一个控制器的不同命名空间里。前者是分区级别的隔离后者是命名空间级别的隔离。命名空间级别的隔离更彻底因为不同命名空间可以有独立的 LBA 格式和容量甚至可以被不同的主机访问在多路径场景下。5.3 实际排查中的命名空间问题我遇到过一种情况一块 NVMe 盘在系统里只显示 nvme0n1但盘的实际容量应该是 2TB显示的却只有 1TB。查了半天发现是命名空间配置问题——这块盘支持多个命名空间出厂时只配置了一个 1TB 的命名空间剩下的容量没有分配。用nvme id-ns和nvme list-ns可以查看命名空间配置必要时可以用nvme create-ns创建新的命名空间。还有一种情况是命名空间被格式化成了非 512 字节的 LBA 格式比如 4K。这时候如果操作系统或者文件系统不支持 4K 扇区就会出现识别问题。用nvme id-ns看flbas字段可以确认当前 LBA 格式。6. 驱动调试中的典型问题与排查链路6.1 设备完全不可见从 PCIe 开始查如果lspci看不到 NVMe 设备排查顺序应该是物理连接确认盘插好了M.2 螺丝拧紧了金手指没有氧化。供电确认槽位供电正常有些 M.2 槽位需要额外的电源配置。链路训练查 PCIe 控制器的链路状态寄存器看链路有没有 up。时钟与复位确认 PCIe 参考时钟正常复位信号时序符合要求。枚举配置确认设备树的 PCIe 节点配置正确ranges属性没有错误。这个顺序是从硬件到软件从底层到上层。很多人喜欢反过来查先怀疑驱动结果绕了一大圈才发现是硬件问题。6.2 设备可见但无法读写查控制器状态如果lspci能看到设备但/dev/nvme0不存在或者读写报错排查方向就转到 NVMe 控制器层面查dmesg里有没有 NVMe 相关的错误日志比如控制器初始化失败、队列创建失败。查 CSTS 寄存器的 RDY 位有没有置位确认控制器是否就绪。查 Admin 队列的 ASQ/ACQ 寄存器是否写入了正确的物理地址。查中断是否正常触发cat /proc/interrupts看 NVMe 中断计数有没有增长。我遇到过一次控制器初始化超时的问题dmesg 里报Controller not ready。最后发现是队列内存分配时用了非连续内存而设备要求队列内存必须物理连续。改成用dma_alloc_coherent分配连续内存后问题解决。6.3 性能不达预期从队列和中断入手如果盘能正常工作但性能远低于预期排查方向包括排查项检查方法常见问题链路速率lspci -vv看 LnkSta链路降速到 Gen1/Gen2队列数量nvme get-feature -f 0x07队列数太少并发不足中断亲和性cat /proc/interrupts中断集中在单个 CPU轮询配置cat /sys/block/nvme0n1/queue/io_poll轮询未开启或配置不当调度器cat /sys/block/nvme0n1/queue/scheduler用了不适合的 I/O 调度器NVMe 盘建议用none调度器因为 NVMe 本身支持多队列调度器反而增加开销。中断亲和性可以用irqbalance或者手动绑核来优化让每个队列的中断落到不同的 CPU 上。7. 内核虚拟化场景下的 NVMe 直通7.1 VFIO 直通的基本原理在虚拟化场景下把 NVMe 盘直通给虚拟机可以绕过宿主机的存储栈获得接近原生的性能。Linux 上常用的方案是 VFIO它把 PCIe 设备从宿主机解绑交给用户态程序比如 QEMU管理虚拟机通过 IOMMU 直接访问设备。VFIO 直通的关键是 IOMMU 分组。每个 PCIe 设备属于一个 IOMMU 组直通时整个组都要交给虚拟机。如果 NVMe 盘和别的设备在同一个组里要么一起直通要么都别直通。实际部署时可以通过 PCIe 拓扑设计让 NVMe 盘独占一个 IOMMU 组。7.2 直通后的性能表现与注意事项直通后的 NVMe 性能通常能到原生的 90% 以上延迟增加很小。但有几个坑要注意中断直通虚拟机的 MSI-X 中断需要正确配置否则中断会落到宿主机上性能大打折扣。BAR 空间映射虚拟机的 BAR 空间要正确映射否则驱动初始化会失败。电源管理直通设备的电源状态由虚拟机控制宿主机不能随意改否则会出现设备状态不一致。我在一个项目里做过 NVMe 直通虚拟机里跑 fio 能到 200 万 IOPS延迟在 100 微秒以内。但一开始中断没配对性能只有一半。后来在 QEMU 命令行里加了-device vfio-pci,host01:00.0并确认 MSI-X 使能性能才上来。8. 一些实际项目中的经验碎片做 NVMe 驱动开发这些年有几个经验是文档里不会写但很实用的。第一个是关于复位时序NVMe 控制器的复位信号需要保持足够长的时间太短了设备可能没完全复位导致后续初始化失败。具体多长要看设备手册一般建议至少 100ms。第二个是关于队列内存的对齐Admin 队列的基地址必须按页对齐I/O 队列的基地址也有对齐要求。用dma_alloc_coherent分配时返回的地址通常是对齐的但如果你自己管理内存池一定要检查对齐。第三个是关于热插拔PCIe 热插拔在服务器上很常见但 NVMe 盘的热插拔需要驱动和硬件都支持。热插拔时驱动要先停止 I/O 队列等所有未完成的命令处理完再移除设备。如果直接拔盘可能导致数据丢失或者系统 hang 住。第四个是关于固件升级NVMe 盘支持通过 Admin 命令做固件升级但升级过程中不能断电否则盘可能变砖。生产环境做固件升级一定要接 UPS并且升级前备份数据。最后说一个调试技巧如果怀疑 NVMe 命令有问题可以用nvme工具的--dry-run选项先看看命令长什么样或者用trace-cmd抓内核里的 NVMe 事件。内核的 NVMe 驱动里有 tracepoint能看到命令提交和完成的详细过程对定位问题很有帮助。
返回列表