3. NVMe驱动核心数据结构:struct nvme_dev、struct nvme_queue、struct nvme_ns、struct nvme_cmd
说实话,NVMe驱动里最绕不开的就是这四个结构体。我刚开始啃NVMe源码时,光是把它们之间的关系理清楚就花了两天。你想想看,一个PCIe设备、一堆队列、一堆命名空间、还有无数命令在飞,没有这几个核心结构体撑着,整个驱动根本跑不起来。
今天我就带你把这四个“骨架”拆开看看。嗯,咱们一个一个来。
3.1 struct nvme_dev:整个驱动的“大脑”
这个结构体代表一个NVMe控制器实例。说白了,就是你的SSD插到PCIe槽上后,内核为它创建的那个“总管家”。
我个人习惯把 nvme_dev 想象成一座工厂的中央控制室。它里面存着控制器的寄存器映射地址、中断向量、各个队列的指针、还有一堆配置信息。
核心字段一览:
pci_dev *pdev—— 指向PCIe设备的指针,所有硬件交互的起点dma_addr_t bar—— 控制器的寄存器基地址,MMIO操作全靠它struct nvme_queue **queues—— 队列数组指针,管理所有提交/完成队列struct nvme_ns *ns—— 命名空间数组,每个namespace对应一个逻辑盘int q_depth—— 队列深度,我遇到过因为设太小导致IOPS上不去的情况u32 db_stride—— doorbell步长,不同厂商的SSD这个值可能不一样
struct nvme_dev {
struct pci_dev *pdev;
void __iomem *bar;
struct nvme_queue **queues;
struct nvme_ns *ns;
int q_depth;
u32 db_stride;
// ... 还有不少字段,但上面这几个是核心
};
我记得有一次调试一个国产SSD,死活读不到命名空间信息。查了两天,最后发现是 bar 地址映射错了。嗯,这种低级错误最坑人。
3.2 struct nvme_queue:命令的“流水线”
队列是NVMe的灵魂。没有队列,命令就没法提交,也没法完成。nvme_queue 就是管理这条流水线的结构体。
每个队列包含两部分:提交队列(SQ)和完成队列(CQ)。驱动往SQ里塞命令,硬件处理完后往CQ里写完成状态。
我的经验: 队列深度不是越大越好。我曾经把q_depth设成4096,结果内存占用暴涨,性能反而下降了。一般128或256就够用。
struct nvme_queue {
struct nvme_dev *dev; // 所属的控制器
struct nvme_command *sq_cmds; // 提交队列的命令缓冲区
struct nvme_completion *cqes; // 完成队列的完成项缓冲区
dma_addr_t sq_dma_addr; // 提交队列的DMA地址
dma_addr_t cq_dma_addr; // 完成队列的DMA地址
u16 sq_tail; // 提交队列的尾指针
u16 cq_head; // 完成队列的头指针
u16 qid; // 队列ID,0号队列是管理队列
// ...
};
这里有个关键点:sq_tail 和 cq_head 是驱动和硬件通信的“信使”。驱动往SQ里写命令后,更新 sq_tail 并写doorbell通知硬件。硬件处理完,更新 cq_head,驱动轮询或中断来消费完成项。
我曾经犯过一个错:更新了 sq_tail 但忘了写doorbell。结果命令一直卡在队列里,系统直接hang住。这种问题排查起来特别痛苦,因为没有任何错误日志。
3.3 struct nvme_ns:你看到的“盘”
用户看到的 /dev/nvme0n1,背后就是 nvme_ns。每个命名空间对应一个逻辑块设备。
这个结构体相对简单,但很重要。它把NVMe协议层的命名空间概念,映射到了Linux块设备层。
struct nvme_ns {
struct nvme_dev *dev; // 所属控制器
struct gendisk *disk; // 通用块设备,用户看到的就是它
u32 ns_id; // 命名空间ID,从1开始
u64 nsze; // 总逻辑块数
u32 ncap; // 容量
u16 nlbaf; // LBA格式数量
u8 flbas; // 当前选中的LBA格式
// ...
};
注意: 命名空间ID不是连续的。有些SSD会跳过某些ID,或者动态创建/删除命名空间。千万别假设ID从1开始递增。
我记得有一次,用户报告说系统里只看到一个nvme设备,但BIOS里明明有两个。查了半天,发现第二个命名空间的 ns_id 是5,驱动在遍历时只检查了1到4,直接跳过了。嗯,这种边界情况最容易出bug。
3.4 struct nvme_cmd:最底层的“指令”
所有IO操作,最终都要变成一条NVMe命令。这个结构体就是命令的“模板”。
NVMe命令分为管理命令和IO命令两大类。管理命令用来操作控制器本身(比如创建队列、获取日志),IO命令用来读写数据。
struct nvme_command {
union {
struct nvme_common_command common;
struct nvme_rw_command rw;
struct nvme_admin_command admin;
// 各种命令类型
};
};
以读命令为例,核心字段包括:
| 字段 | 说明 |
|---|---|
| opcode | 操作码,0x02表示读,0x01表示写 |
| nsid | 目标命名空间ID |
| slba | 起始逻辑块地址 |
| nlb | 要传输的逻辑块数(注意是0-based,nlb=0表示1个块) |
| prp1/prp2 | 物理区域页指针,指向数据缓冲区 |
避坑指南: 我曾经把 nlb 直接设成块数,结果读出来的数据全是错的。后来翻spec才发现,nlb=0表示1个块,nlb=1表示2个块。这种“0-based”的坑,NVMe协议里还有不少。
3.5 四个结构体的关系
这四个结构体不是孤立的。它们的关系可以用一张图说清楚:
从图上可以看得很清楚:
nvme_dev是顶层容器,持有所有队列和命名空间的指针nvme_queue是命令的载体,每个队列里有一串nvme_cmdnvme_ns是命令的目标,每条命令都要指定操作哪个命名空间nvme_cmd是最小执行单元,驱动最终就是往队列里塞命令
嗯,这四个结构体搞明白了,NVMe驱动的一半就算拿下了。剩下的就是怎么初始化它们、怎么让它们协同工作。那些内容,咱们后面慢慢聊。
一句话总结: dev是大脑,queue是血管,ns是器官,cmd是血液。没有哪个都活不了。
公众号:蓝海资料掘金营,微信deep3321