
1. 为什么说 NVMe 是存储驱动入门的最优解1.1 一个被低估的学习切入点很多做嵌入式或者内核方向的朋友一提到写驱动三个字就头大。字符设备、平台设备、块设备、中断子系统、DMA 映射、电源管理……光是这些名词就够劝退一批人了。但如果你问我2024 年想找一个既能学到完整驱动开发链路、又不会一上来就被复杂硬件协议劝退的方向我会毫不犹豫地说从 NVMe 入手。原因很简单。NVMe 是块设备驱动里协议最规整、文档最完整、硬件最容易获取的一类。你不需要去啃某家厂商几百页的私有寄存器手册也不需要面对各种历史遗留的兼容性坑。NVMe 的规范是公开的队列模型是统一的命令集是标准化的。一块普通的 M.2 固态插到开发板上你就能从 PCIe 枚举一路跟到块设备注册整条链路清清楚楚。而且从实际需求来看现在做国产化平台适配、做嵌入式存储方案、做内核裁剪定制的团队越来越多NVMe 驱动的调试能力几乎是刚需。你去看那些招聘 JD只要涉及存储、涉及内核、涉及 PCIeNVMe 基本都会出现在技能要求里。1.2 这篇文章适合谁看我写这篇东西预设的读者是这几类人刚接触 Linux 内核驱动开发想找一个完整项目练手的工程师。你可能看过《Linux 设备驱动开发详解》也跑过几个 hello world 模块但一直不知道怎么把这些零散知识串成一个真实可用的驱动。做嵌入式开发需要在新平台上适配 NVMe 固态的开发者。比如你拿到一块新的 ARM 开发板PCIe 控制器是新的需要确认 NVMe 盘能不能正常识别、性能是否达标。做系统集成或国产化适配需要理解存储栈底层原理的技术人员。你不一定要自己写驱动但你需要知道/dev/nvme0n1p5到底是怎么来的出问题了该从哪一层查。如果你属于上面任何一类这篇内容应该能给你省下不少翻文档和踩坑的时间。1.3 先厘清一个高频疑问/dev/nvme0n1p5 到底怎么读这个问题在搜索热词里出现频率极高我先把结论说清楚后面再展开。/dev/nvme0n1p5这个设备节点拆开来看是这样的字段含义nvme0第 0 个 NVMe 控制器也就是第一块 NVMe 盘n1该控制器下的第 1 个命名空间namespacep5该命名空间上的第 5 个分区所以你的理解基本正确但有一个细节要纠正它表示的是第 1 个 NVMe 控制器的第 1 个命名空间的第 5 个分区而不是简单说第 1 个硬盘的第 5 个分区。因为一块 NVMe 盘可以划分多个 namespace每个 namespace 在系统里看起来就像一块独立的盘。这个区别在多 namespace 的企业级盘上非常关键消费级盘通常只有一个 namespace所以平时感觉不到差异。理解了设备命名规则其实你就已经摸到了 NVMe 驱动的一个核心概念控制器、命名空间、分区是三个不同层级的东西。驱动要做的就是把物理设备抽象成这三层再交给块层去管理。2. NVMe 驱动的整体架构与设计思路拆解2.1 从 PCIe 枚举到块设备注册的完整链路要理解 NVMe 驱动你得先有一个全局视角。一块 NVMe 固态从插入插槽到你能mount它中间经历了这么几个阶段第一阶段是 PCIe 枚举。系统上电后RCRoot Complex会扫描总线给每个设备分配总线号、设备号、功能号读取配置空间的 Vendor ID 和 Device ID。NVMe 设备的 Class Code 是 0x010802驱动就是靠这个来匹配的。第二阶段是驱动 probe。内核的 PCIe 子系统发现设备后会遍历已注册的驱动找到匹配的pci_driver结构调用它的probe函数。NVMe 驱动的 probe 就在这里启动。第三阶段是控制器初始化。probe 里要做的事情很多映射 BAR 空间、使能设备、配置 Admin 队列、发送 Identify 命令获取控制器和命名空间信息、配置 I/O 队列。这一步是 NVMe 驱动最核心的部分。第四阶段是块设备注册。控制器初始化完成后驱动会为每个 namespace 创建一个gendisk注册到块层。这时候/dev/nvme0n1就出现了。第五阶段是分区扫描。块层会读取设备上的分区表GPT 或 MBR解析出各个分区创建/dev/nvme0n1p1、/dev/nvme0n1p2等节点。整条链路走下来你会接触到 PCIe 子系统、中断子系统、DMA 子系统、块设备子系统、电源管理子系统。这就是为什么我说 NVMe 是入门首选——它一个驱动把内核里最常用的几个子系统全串起来了。2.2 为什么 NVMe 的队列模型是理解重点NVMe 和传统 SATA/AHCI 最大的区别就在队列模型上。AHCI 只有一个命令队列深度是 32。所有 I/O 请求都挤在这一个队列里多核 CPU 的并行能力根本发挥不出来。NVMe 则支持最多 65535 个 I/O 队列每个队列深度也是 65535。更关键的是每个 CPU 核可以有自己的队列互不干扰。这个设计带来的直接好处是在多核系统上每个核提交 I/O 时操作的是自己的队列不需要和其他核抢锁。这就是 NVMe 能轻松跑出几百万 IOPS 的根本原因。在驱动层面这意味着你要理解几个关键数据结构SQSubmission Queue主机提交命令的地方驱动往里写命令CQCompletion Queue设备返回完成状态的地方驱动从这里读结果Doorbell 寄存器驱动写完 SQ 后写 Doorbell 通知设备设备写完 CQ 后写 Doorbell 通知主机SQ 和 CQ 都是环形缓冲区通过尾指针tail和头指针head来管理。驱动维护 SQ 的 tail 和 CQ 的 head设备维护 SQ 的 head 和 CQ 的 tail。这种各管一半的设计避免了双方同时修改同一个指针减少了同步开销。我建议你在读驱动代码时把nvme_queue这个结构体吃透它把 SQ、CQ、Doorbell、队列深度、CPU 绑定关系全封装在一起了。理解了它整个驱动的数据流就清楚了。2.3 方案选型为什么用 U-Boot 做前期验证在实际项目里我通常建议分两步走先用 U-Boot 做底层验证再进 Linux 内核做完整驱动。U-Boot 的好处是轻量、启动快、调试方便。它本身就有 PCIe 枚举和 NVMe 读取的能力nvme scan、nvme read这些命令。你可以先在 U-Boot 里确认PCIe 链路能不能正常建立pcie enum看设备有没有被扫到NVMe 控制器能不能正常初始化能不能读到盘上的数据如果 U-Boot 阶段就有问题那基本可以确定是硬件或 PCIe 控制器配置的问题不用往内核里查。这一步能帮你排除掉一大半的干扰因素。等 U-Boot 验证通过再进内核调驱动你面对的变量就少多了。这个分层验证的思路是我踩过很多坑之后总结出来的强烈建议你养成习惯。3. 核心细节解析与实操要点3.1 PCIe 配置空间与 BAR 映射NVMe 驱动 probe 的第一步就是读取 PCIe 配置空间。这里有几个关键寄存器你必须认识Vendor ID 和 Device ID用来匹配驱动。NVMe 设备的 Vendor ID 通常是厂商自己的但 Class Code 一定是 0x010802。BAR0 和 BAR1NVMe 规范规定控制器的寄存器映射在 BAR064 位地址里。BAR1 通常不用。BAR0 里包含了控制器寄存器Controller Registers比如 CAP、VS、CC、CSTS、AQA、ASQ、ACQ 等。Command 寄存器用来使能 Memory Space、Bus Master 等。驱动 probe 时必须先设置这些位否则后面 DMA 根本没法工作。映射 BAR 的代码大概长这样bar pci_resource_start(pdev, 0); len pci_resource_len(pdev, 0); dev-bar ioremap(bar, len);ioremap之后你就能通过readl/writel访问控制器寄存器了。这里有个坑NVMe 寄存器是 64 位的读写要用readq/writeq或者分两次 32 位读写。我见过有人用readl读 64 位寄存器结果只拿到低 32 位调了半天才发现问题。注意ioremap之后一定要检查返回值映射失败还继续访问会直接 panic。另外映射的地址在驱动卸载时要iounmap释放否则会内存泄漏。3.2 控制器初始化CC 和 CSTS 的握手控制器初始化是 NVMe 驱动里最讲究时序的部分。核心是操作 CCController Configuration和 CSTSController Status这两个寄存器。流程是这样的读 CAP 寄存器确认控制器支持的特性比如 MQES 最大队列数、TO 超时时间、DSTRD Doorbell 步长等待 CSTS.RDY 为 0确认控制器处于禁用状态配置 AQAAdmin Queue Attributes设置 Admin 队列深度配置 ASQ 和 ACQ写入 Admin SQ 和 CQ 的物理地址配置 CC设置 IOCQES、IOSQES队列条目大小、CSS命令集、MPS内存页大小、AMS仲裁机制设置 CC.EN 为 1使能控制器轮询 CSTS.RDY等待它变为 1第 7 步的等待是有超时限制的规范建议用 CAP.TO 字段计算超时时间。如果超时还没 ready说明初始化失败要回滚。/* 等待控制器 ready 的典型写法 */ timeout jiffies NVME_CTRL_RESET_TIMEOUT; while (readl(dev-bar NVME_REG_CSTS) NVME_CSTS_RDY) { if (time_after(jiffies, timeout)) { dev_err(dev-dev, Controller reset timeout\n); return -ENODEV; } msleep(1); }这里的NVME_CTRL_RESET_TIMEOUT一般设 5 秒左右。我实测下来正常盘几百毫秒就 ready 了如果超过 1 秒还没好大概率是硬件问题。实操心得调试控制器初始化时把每一步的寄存器值都打印出来。尤其是 CC 和 CSTS出问题时对比规范手册一眼就能看出是哪个位配错了。我习惯在 probe 里加一堆dev_dbg虽然啰嗦但省时间。3.3 Admin 队列与 Identify 命令控制器 ready 之后驱动要通过 Admin 队列发送 Identify 命令获取控制器和命名空间的详细信息。Admin 队列只有一对 SQ/CQ深度通常是 32 或 64。驱动需要先分配 DMA 内存给 SQ 和 CQ把物理地址写进 ASQ 和 ACQ 寄存器。Identify 命令有两个重要的 CNSController or Namespace Structure值CNS0x01Identify Controller返回控制器的能力信息比如型号、序列号、支持的队列数、支持的命名空间数CNS0x02Identify Namespace返回指定命名空间的信息比如容量、LBA 格式、支持的读写命令发送 Identify 命令的过程其实就是构造一个 NVMe 命令结构体写进 SQ敲 Doorbell然后等 CQ 里的完成条目。/* 构造 Identify 命令的简化示意 */ struct nvme_command cmd {0}; cmd.identify.opcode nvme_admin_identify; cmd.identify.cns NVME_ID_CNS_CTRL; cmd.identify.prp1 cpu_to_le64(dma_addr); /* 提交到 Admin SQ */ nvme_submit_cmd(dev-admin_q, cmd); /* 等待完成 */ nvme_wait_completion(dev-admin_q);这里的关键是PRPPhysical Region Page。NVMe 用 PRP 来描述数据缓冲区而不是像传统驱动那样用 scatter-gather list。PRP1 指向第一个页如果数据超过一页PRP2 指向第二个页或者一个 PRP List。理解 PRP 是理解 NVMe 数据传输的基础。3.4 I/O 队列的创建与 CPU 绑定Admin 队列搞定后驱动要根据系统 CPU 数量创建 I/O 队列。NVMe 规范建议每个 CPU 核至少一个队列这样每个核提交 I/O 时不用抢锁。创建 I/O 队列用的是Create I/O Completion Queue和Create I/O Submission Queue这两个 Admin 命令。驱动要指定队列 ID、队列深度、中断向量、CPU 亲和性等参数。/* 创建 I/O CQ 的关键参数 */ struct nvme_command c {0}; c.create_cq.opcode nvme_admin_create_cq; c.create_cq.qid cpu 1; /* 0 是 Admin 队列 */ c.create_cq.qsize queue_depth - 1; c.create_cq.irq_vector cpu 1; c.create_cq.pc 1; /* 物理连续 */注意qsize字段填的是深度减一因为队列深度是从 0 开始计数的。这个细节很容易搞错填错了设备会返回 Invalid Field 错误。中断方面NVMe 支持 MSI-X。每个 I/O 队列可以绑定一个独立的中断向量这样不同队列的完成中断可以分发到不同 CPU 上处理进一步提升并行度。驱动要调用pci_alloc_irq_vectors申请 MSI-X 向量然后用pci_irq_vector拿到具体的中断号。踩坑记录有些平台的 PCIe 控制器 MSI-X 支持不完整只能申请到少量向量。这时候驱动要能降级到 MSI 甚至 INTx 模式。我在某国产 ARM 平台上就遇到过MSI-X 只能申请 4 个向量但系统有 8 个核最后只能多个队列共享中断。这种兼容性处理是实际项目里必须考虑的。4. 实操过程与核心环节实现4.1 环境准备与硬件确认动手之前先把环境理清楚。我用的典型配置是这样的项目配置开发板带 PCIe 3.0 x4 接口的 ARM64 平台NVMe 盘普通 M.2 2280 消费级固态256GB内核版本Linux 5.10 LTS交叉编译工具链aarch64-linux-gnu-gcc调试工具串口、逻辑分析仪可选第一步是确认硬件连接。M.2 接口的 PCIe 信号包括 TX、RX、REFCLK、PERST 等。如果你用的是转接板要确认 PERST 信号有没有正确连接。我遇到过转接板没接 PERST导致设备一直不 ready 的情况。上电后先在 U-Boot 里确认 PCIe 链路# U-Boot 命令行 pci enum pci 0如果能看到 NVMe 设备的 Vendor ID 和 Device ID说明 PCIe 物理链路和枚举都没问题。如果看不到先查硬件别急着进内核。4.2 U-Boot 阶段的 NVMe 验证U-Boot 的 NVMe 命令很好用能帮你快速确认盘能不能读nvme scan nvme info nvme read 0x80000000 0 1nvme scan会扫描所有 NVMe 设备nvme info打印设备信息nvme read从指定 LBA 读取数据到内存。如果这三步都成功说明 NVMe 控制器初始化、Admin 队列、I/O 队列、数据读取整条链路都是通的。这一步的价值在于它把问题范围缩小到了内核驱动层面。如果 U-Boot 能读内核读不了那问题一定在内核的 PCIe 控制器驱动、NVMe 驱动或者设备树配置上跟硬件无关。4.3 内核配置与设备树调整进内核之前先确认配置项CONFIG_PCIy CONFIG_PCI_MSIy CONFIG_PCIEPORTBUSy CONFIG_BLK_DEV_NVMEy CONFIG_NVME_COREy设备树里要确认 PCIe 控制器的节点配置正确包括reg控制器寄存器地址ranges地址映射范围interrupts中断号clocks时钟配置physPHY 配置如果有pciefe260000 { compatible rockchip,rk3399-pcie; reg 0x0 0xfe260000 0x0 0x10000; ranges 0x83000000 0x0 0xfa000000 0x0 0xfa000000 0x0 0x600000; interrupts GIC_SPI 49 IRQ_TYPE_LEVEL_HIGH; clocks cru ACLK_PCIE, cru HCLK_PCIE; status okay; };设备树配错是新手最常见的坑。我建议你对照 SoC 手册和已有的参考设备树逐项核对。尤其是ranges的地址映射配错了会导致 BAR 映射失败设备根本 probe 不起来。4.4 驱动加载与设备识别验证内核启动后用dmesg看 NVMe 驱动的 probe 日志dmesg | grep -i nvme正常的话你会看到类似这样的输出nvme nvme0: pci function 0000:01:00.0 nvme nvme0: 8/0/0 default/read/poll queues nvme nvme0: mapped 8 queues nvme0n1: p1 p2 p3 p4 p5最后一行p1 p2 p3 p4 p5就是分区扫描的结果。这时候/dev/nvme0n1p5就出现了。如果卡在pci function之后没有后续说明控制器初始化失败。如果出现了nvme0n1但没有分区说明分区表读取有问题。4.5 性能验证与队列调优设备识别之后用fio做个简单的性能测试fio --namerandread --ioenginelibaio --iodepth32 \ --rwrandread --bs4k --direct1 --size1G \ --numjobs4 --runtime60 --group_reporting \ --filename/dev/nvme0n1重点看 IOPS 和延迟。如果 IOPS 明显低于盘的标称值可能是队列数不够或者中断绑定不合理。调优的方向有几个增加队列深度iodepth调大让设备有更多命令可以并行处理确认队列数dmesg里的mapped X queues应该接近 CPU 核数中断亲和性用irqbalance或者手动设置/proc/irq/*/smp_affinity让中断分散到不同核检查 PCIe 链路速率lspci -vv看 LnkSta确认跑在预期的速率和宽度上# 查看 PCIe 链路状态 lspci -vv -s 01:00.0 | grep -i lnksta如果显示Speed 2.5GT/s Width x1而你的盘和插槽都支持 8GT/s x4那说明链路协商有问题可能是信号完整性或者配置问题。5. 常见问题与排查技巧实录5.1 设备识别不到从 PCIe 枚举开始查设备完全识别不到是最常见也最让人头疼的问题。我的排查顺序是这样的第一步确认物理连接。M.2 盘有没有插紧转接板供电是否正常PERST 信号有没有。这些用万用表就能量。第二步看 U-Boot 能不能枚举到。如果 U-Boot 都扫不到内核肯定也扫不到。这时候要查 PCIe 控制器的初始化包括时钟、PHY、复位。第三步看内核 PCIe 枚举日志。dmesg | grep -i pcie会打印枚举过程。如果看到link down或者LTSSM timeout说明链路训练失败。第四步检查设备树配置。ranges、interrupts、clocks这些有没有配错。下面这个表是我整理的常见现象和对应原因现象可能原因排查方向U-Boot 扫不到设备物理连接、PERST、时钟硬件测量U-Boot 能扫到内核扫不到设备树配置、控制器驱动对比 U-Boot 配置内核扫到但 probe 失败BAR 映射、控制器初始化看 probe 日志probe 成功但无分区分区表、命名空间读 LBA0 验证能识别但性能差队列数、链路速率、中断fio lspci5.2 控制器初始化超时CSTS.RDY 一直不为 1这个问题的表现是dmesg里出现Controller reset timeout或者Device not ready。原因通常有几个CC 寄存器配置错误比如 MPS 设错了或者 CSS 设了不支持的值Admin 队列地址错误ASQ/ACQ 写入了错误的物理地址设备固件问题有些盘的固件在特定条件下会卡住电源问题供电不足导致设备无法完成初始化排查方法把 CC、CSTS、CAP 的值都打印出来对照规范手册逐位检查。我遇到过一次是 MPS 配成了 0表示 4KB但设备只支持 4KB 以上的页大小改成 0 对应的正确值就好了。独家技巧如果怀疑是设备固件问题可以试试在 CC.EN 置 1 之前先做一次 Controller ResetCC.EN 置 0等 CSTS.RDY 变 0再重新初始化。有些盘的固件需要这个软复位才能正常启动。5.3 性能不达标队列和中断的调优性能问题往往不是能不能用而是够不够快。我遇到过好几次盘识别正常但 IOPS 只有标称值的一半。排查思路先看队列数。dmesg里的mapped X queues如果 X 远小于 CPU 核数说明队列创建失败或者被限制了。检查pci_alloc_irq_vectors的返回值看 MSI-X 向量申请到了几个。再看中断分布。cat /proc/interrupts | grep nvme看各个队列的中断是不是均匀分布在不同 CPU 上。如果全挤在一个核上那并行度肯定上不去。然后看链路速率。lspci -vv确认 LnkSta 的 Speed 和 Width。如果链路降速了带宽就是瓶颈。最后看 I/O 调度器。NVMe 设备建议用none调度器避免额外的排序开销echo none /sys/block/nvme0n1/queue/scheduler5.4 分区识别异常/dev/nvme0n1p5 不出现分区不出现通常是这几个原因分区表损坏用fdisk -l /dev/nvme0n1看能不能读出分区表命名空间问题有些盘有多个 namespace分区在别的 namespace 上驱动没扫描分区检查CONFIG_EFI_PARTITION和CONFIG_MSDOS_PARTITION有没有开如果fdisk -l能看到分区但/dev下没有节点可能是 udev 的问题。手动partprobe /dev/nvme0n1触发重新扫描。注意/dev/nvme0n1p5里的p5是分区号不是第 5 个分区的绝对含义。如果分区表里只有 3 个分区但编号是 1、2、5那p5依然存在只是中间的分区号被跳过了。这是 GPT 分区表的特性别被编号迷惑。5.5 常见问题速查表问题快速排查命令可能原因设备不识别lspci、dmesg | grep pcie链路、枚举、设备树probe 失败dmesg | grep nvmeBAR、控制器初始化无分区fdisk -l、partprobe分区表、命名空间性能差fio、lspci -vv、/proc/interrupts队列、中断、链路读写错误dmesg、smartctl盘故障、PRP 错误热插拔异常dmesg、lspci电源管理、移除流程6. 进阶方向从能用到好用6.1 电源管理与热插拔NVMe 规范定义了多种电源状态PS0 到 PS4驱动要支持这些状态的切换。Linux 内核里通过nvme_set_power_state之类的接口来操作。热插拔是另一个重点。PCIe 热插拔涉及 Presence Detect、Power Indicator、Attention Indicator 等信号。NVMe 层面还要处理命名空间的动态增删。这部分调试起来比较麻烦建议先用echo 1 /sys/bus/pci/rescan手动触发扫描确认基本流程通了再上真实热插拔。6.2 多命名空间与多路径企业级 NVMe 盘支持多个 namespace每个 namespace 可以独立格式化、独立挂载。驱动要正确处理Identify Namespace List命令把所有 namespace 都枚举出来。多路径Multipath是另一个进阶话题。当系统有多个 PCIe 路径通向同一块盘时驱动要能识别并做路径冗余。Linux 内核的nvme-multipath模块就是干这个的。6.3 调试工具与内核追踪除了dmesg和lspci还有几个工具值得掌握nvme-cli用户态工具能发各种 NVMe 命令nvme id-ctrl、nvme id-ns、nvme smart-log都很常用ftrace追踪内核函数调用echo function /sys/kernel/debug/tracing/current_tracer然后看trace文件perf性能分析perf top看热点函数bpftrace动态追踪能挂到 NVMe 驱动的关键函数上# 用 nvme-cli 查看控制器信息 nvme id-ctrl /dev/nvme0 # 查看命名空间信息 nvme id-ns /dev/nvme0n1 # 查看 SMART 信息 nvme smart-log /dev/nvme0这些工具能帮你在不重新编译内核的情况下快速定位问题。6.4 从消费级到企业级的差异消费级盘和企业级盘在驱动层面有一些差异需要注意队列数企业级盘支持更多队列消费级盘可能只支持少量命名空间企业级盘常有多 namespace消费级通常只有一个电源管理企业级盘的电源状态更复杂错误处理企业级盘有更完善的错误上报和恢复机制固件升级企业级盘支持在线固件升级驱动要处理如果你做的是国产化适配大概率会遇到企业级盘。建议提前了解这些差异免得到时候手忙脚乱。6.5 一个真实的调试案例最后分享一个我印象比较深的调试经历。某次在一块国产 ARM 平台上适配 NVMe 盘U-Boot 能正常读写但进内核后 probe 一直失败报Controller reset timeout。查了 CC、CSTS、CAP 都没问题Admin 队列地址也对。后来用逻辑分析仪抓 PCIe 信号发现设备在初始化过程中发了一个 Completion但主机没收到。进一步查发现是 MSI-X 中断没使能。原来这块平台的 PCIe 控制器在设备树里配了msi-parent但内核的 MSI 控制器驱动没加载导致 MSI-X 中断无法分发。解决办法是在设备树里补上 MSI 控制器的节点并确保CONFIG_PCI_MSI和对应的 MSI 控制器驱动都编译进内核。这个问题卡了我两天最后发现是配置问题不是代码问题。这个案例的教训是PCIe 相关的问题一定要把中断链路查清楚。MSI-X 不只是驱动的事还涉及 MSI 控制器、设备树、内核配置多个环节。7. 写在最后的一些个人体会NVMe 驱动开发这件事入门门槛其实没有想象中那么高但要把每一个细节都吃透确实需要花时间。我的建议是先跑通再深挖。先找一个成熟的开发板用现成的内核和设备树把 NVMe 盘跑起来看到/dev/nvme0n1出现能正常读写。这一步能给你很大的信心。然后再回过头去读驱动代码从nvme_probe开始一行一行跟。遇到不懂的寄存器翻规范手册遇到不懂的函数查内核文档。慢慢地你会发现那些曾经陌生的名词都变成了具体的代码和数据流。最后尝试改点东西。比如把队列深度调大看看性能有没有变化比如加一些调试打印看看初始化流程的每一步。这种动手改的过程才是真正把知识变成自己的过程。存储驱动这个方向看起来枯燥但底层的很多东西是相通的。你把 NVMe 搞明白了再去看 SATA、USB 存储、甚至网络驱动会发现很多设计思想是类似的。这就是为什么我说NVMe 是一个很好的起点。如果你在调试过程中遇到什么奇怪的问题欢迎一起交流。踩过的坑多了也就成了路。