NVMe命令格式:命令通用DWord字段、命令类型与完成状态
NVMe的命令格式,说白了就是主机和控制器之间沟通的"协议书"。你发一个命令过来,控制器得知道你要干嘛、操作哪个命名空间、数据在哪、要不要分片。这些信息全塞在64字节的命令里,分成16个DWord。我刚开始看Spec的时候,也被这些字段绕得头晕。但摸过几个项目后就会发现——其实套路很固定。
这一章我们就把命令格式掰开揉碎,讲清楚三件事:通用DWord字段长什么样、Admin和I/O命令有什么区别、完成状态怎么上报。
命令通用DWord字段
每个NVMe命令都是64字节,从DWord 0到DWord 15。前8个DWord是通用头,后8个是命令特定区域。通用头的字段,所有命令都一样。
| DWord | 位域 | 字段名 | 说明 |
|---|---|---|---|
| DW0 | 31:24 | FUSE | 命令合并标志,0表示独立命令 |
| 23:16 | PSGT | 物理段粒度提示,用于PRP/SGL | |
| 15:0 | CID | 命令标识符,由主机分配 | |
| DW1 | 31:0 | NSID | 命名空间ID,0xFFFFFFFF代表所有 |
| DW2 | 31:0 | RSVD | 保留,必须写0 |
| DW3 | 31:0 | RSVD/META | 保留或元数据指针高位 |
| DW4-DW5 | 63:0 | MPTR/SGL1 | 元数据指针或SGL片段1 |
| DW6-DW7 | 63:0 | PRP1/SGL2 | PRP条目1或SGL片段2 |
| DW8-DW15 | 255:0 | CMD SPEC | 命令特定字段,不同命令含义不同 |
你想想看,DW0里的CID是主机随机分配的,控制器回完成状态时会带上这个CID——这样你就能把完成事件和发出去的命令对上号。NSID就更关键了,填错了控制器根本不认。
命令类型:Admin vs I/O
NVMe命令分两大阵营:Admin命令走Admin Submission Queue,I/O命令走I/O Submission Queue。两者的DWord 0-7格式完全一样,区别在DW8-DW15的命令特定字段。
| 对比项 | Admin命令 | I/O命令 |
|---|---|---|
| 队列类型 | Admin SQ / Admin CQ | I/O SQ / I/O CQ |
| 常见指令 | Identify、Get Log Page、Set Features、Create IO SQ/CQ | Read、Write、Flush、Compare、Write Zeroes |
| 命令数量 | 固定约20种 | 由Controller支持的可选集决定 |
| NSID要求 | 部分命令不需要(如Create IO SQ) | 大部分需要有效NSID |
| 执行优先级 | 通常较高 | 受仲裁权重影响 |
我个人的习惯是,在驱动初始化阶段先用Admin命令把控制器"摸一遍"——Identify拿到命名空间列表,Get Log Page看看有没有异常事件,Set Features配好中断聚合。这些事办妥了,再Create IO SQ/CQ,开始发I/O命令。
命令完成状态
命令执行完了,控制器往Completion Queue里塞一个16字节的完成条目。这个条目里藏着命令的执行结果。
| DWord | 位域 | 字段名 | 说明 |
|---|---|---|---|
| DW0 | 31:0 | CMD SPEC | 命令特定完成信息 |
| DW1 | 31:0 | RSVD | 保留 |
| DW2 | 31:16 | SQHD | SQ Head Pointer,主机下次可用的SQ槽位 |
| 15:0 | SQID | 该命令所属的SQ标识 | |
| DW3 | 31:16 | CID | 命令标识符,与提交时一致 |
| 15:1 | Status Field | 状态码和状态码类型 | |
| 0 | P | Phase Tag,用于判断条目是否新到达 |
这里最容易被忽视的是Phase Tag。主机和控制器轮流翻转这个位——控制器写完一组条目后翻转P,主机处理完一轮后再翻转回来。如果驱动没处理好P Tag的翻转逻辑,就会把旧条目当成新条目处理。我记得有一次排查一个"命令完成丢失"的bug,折腾了两天,最后发现是P Tag比较写反了。
// 完成状态解析示例(伪代码)
void handle_completion(uint32_t dw3) {
uint16_t cid = (dw3 >> 16) & 0xFFFF;
uint16_t status = (dw3 >> 1) & 0x7FFF;
uint8_t ptag = dw3 & 0x1;
uint8_t sct = (status >> 8) & 0x7; // Status Code Type
uint8_t sc = status & 0xFF; // Status Code
if (ptag != expected_phase) return; // 跳过旧条目
if (sct == 0 && sc == 0) {
// 成功,处理数据
} else {
// 错误,根据sct和sc查表
printf("CID %04x failed: Type=%d Code=%d\n", cid, sct, sc);
}
}
一张图串起命令格式、类型与完成状态
下面这张图把命令从构造到完成的全路径串起来了。你从左上角开始看,顺着箭头走到右下角,就能理解整个流程。
避坑总结:命令格式相关的常见错误
- 保留字段未清零:DW2和DW3必须写0,否则控制器可能报"Invalid Field"错误。我刚开始写驱动时在这里栽过跟头。
- NSID填错:如果NSID指向一个不存在的命名空间,控制器返回"Invalid Namespace or Format"。我曾经因为一个离线命名空间的NSID没更新,导致所有读写命令都失败。
- PRP/SGL地址未对齐:PRP地址要求页对齐,SGL虽然灵活但也有对齐要求。不对齐直接报"Invalid PRP Offset"。
- CID重复:同一个SQ中不允许出现重复CID,否则完成条目无法区分。建议用递增计数器分配。
- P Tag处理逻辑反了:如果驱动和控制器对P Tag的翻转理解不一致,轻则丢完成,重则处理到旧数据导致状态混乱。
命令格式是NVMe通信的基石,格式错了后续一切免谈。下一层我们就要把这些命令真正送到队列里,看看Submission Queue和Completion Queue是怎么协作的——但那是另一章的事了。