4、NVMe命令集:Admin命令、I/O命令、命名空间管理
NVMe的命令集,说实话,比SATA时代的AHCI简洁太多了。我刚开始接触NVMe协议时,看到那薄薄的Spec就觉得舒服——命令种类少,但功能一点都不弱。今天咱们就把Admin命令、I/O命令和命名空间管理这三块拆开揉碎了讲。
4.1 命令提交与完成机制
先别急着看具体命令。你得先知道命令是怎么跑起来的。NVMe用了Submission Queue (SQ)和Completion Queue (CQ)这对经典搭档。主机往SQ里扔命令,控制器干完活往CQ里塞完成状态。没有中断风暴,全靠门铃寄存器触发。
4.2 Admin命令——控制面的基石
Admin命令走的是专门的Admin Submission Queue。每个控制器都得至少有一个Admin SQ/CQ对,否则你连门都进不去。我习惯把Admin命令分成三大类:队列管理、信息查询和控制配置。
| OPC (命令码) | 命令名称 | 干什么吃的 |
|---|---|---|
| 00h | I/O SQ Create | 创建一条I/O提交队列 |
| 01h | I/O SQ Delete | 删除I/O SQ,注意删除前要确保队列空 |
| 02h | I/O CQ Create | 创建完成队列,与SQ配对 |
| 03h | I/O CQ Delete | 对应删除 |
| 05h | Get Log Page | 拿各种日志,比如错误日志、健康信息 |
| 09h | Set Features | 调参数,比如中断合并、电源状态 |
| 0Ah | Get Features | 读当前参数 |
| 10h | Namespace Management | 创建/删除/重新分配命名空间 |
重点: Create I/O SQ这个命令有个坑——它的SQID必须和对应的CQID相同,或者你得用Get Features去查控制器支持什么样的映射关系。我踩过一回,配错了直接报无效队列标识符。
Admin命令的提交方式跟I/O命令完全一样,只不过队列ID固定为0(Admin SQ)和0(Admin CQ)。驱动初始化时,必须先配好Admin队列才能做其他事。说白了,这是整个NVMe协议的冷启动流程。
4.3 I/O命令——数据面的主角
I/O命令才是真正搬砖的。最常见的几个:Read、Write、Compare、Dataset Management。命令格式跟Admin一样是64字节,但字段含义不同。
我简单列个核心字段对比:
| 字段 | Admin命令 | I/O命令 |
|---|---|---|
| OPC | 0~127 | 128~255(最高位为1) |
| PRP/SGL | 可能用于传缓冲 | 几乎必用,指定数据地址 |
| Namespace ID | 有时使用 | 必须指定,对应哪个命名空间 |
| SLBA (起始LBA) | 无 | 读写从哪个逻辑块开始 |
这里提一嘴Dataset Management命令。它用来告诉控制器哪些逻辑块以后不需要了(类似TRIM)。它的命令码是09h,走的是I/O Admin?嗯,实际上它属于I/O命令集里的NVM命令集(Command Set Identifier = 0)。这个命令做得粗一点没关系,但做得太精细反而会降低性能。我在项目中就因为这个踩了性能坑——每笔IO都发一个单块的DM命令,结果SSD的GC效率反而下降了。
4.4 命名空间管理——多命名空间的幕后操作
命名空间(Namespace)是NVMe里非常重要的抽象概念。你可以把它想象成一个逻辑块地址的容器。一个SSD可以有一个命名空间(默认),也可以有多个。每个命名空间有自己的ID、大小、保护信息设置等属性。
Admin命令里有专门的Namespace Management和Namespace Attachment操作。前者用来创建/删除命名空间,后者用来把命名空间挂载到某个控制器上。
创建命名空间的典型流程:
- 发送Namespace Management命令(Create),指定LBADataSize和NSZE。
- 控制器返回新创建的NSID。
- 发送Namespace Attachment命令,将NSID attach到当前控制器。
- 然后就能用这个NSID来发I/O命令了。
删除命名空间时,记得先确保没有正在进行的I/O。我曾经犯过傻——删了一个正在被读写的命名空间,结果控制器返回了一个不太友好的错误码(Namespace Not Ready)。
另外,Identify命令(Admin OPC = 06h)也能拿到命名空间的详细信息,比如LBA数据大小、元数据大小、是否支持逻辑块扩展等。你写驱动时,估计得反复调这个命令来确认盘的拓扑。
4.5 我在项目中踩过的三个坑
- 坑一:Admin命令超时不处理。 盘忙的时候,Admin命令也可能挂起。我建议设个超时保护,比如10秒没回就发Abort命令。
- 坑二:队列深度没对齐。 很多控制器只支持2的幂次队列深度(如32、64)。我试过设成63,直接不干活。
- 坑三:命名空间ID从1开始。 NSID 0是保留的,不能用于I/O操作。测试时容易写成0,然后死活不读写。
好了,命令集这一章就这些。理解清楚Admin和I/O的分工,以及命名空间是怎么管理的,后面讲热插拔的事件通知和队列管理才能顺下来。下个阶段咱们直接看中断和门铃那边怎么配合。行,今天就到这儿。