第3章 Submission Queue 与 Completion Queue
这一章我们聊聊 NVMe 的通信核心——Submission Queue 和 Completion Queue。说白了,SQ 就是主机放命令的地方,CQ 就是控制器写结果的地方。两者配合,构成了 NVMe 高效 I/O 的基础。
我最早接触 NVMe 时,最困惑的就是这对队列到底怎么协同。后来做了几个项目,踩了些坑,才算真正搞明白。今天我把这些经验梳理出来,希望对你有帮助。
3.1 一对 SQ/CQ:NVMe 的通信管道
每一对 SQ 和 CQ 组成一个独立的 I/O 通道。你可以想象成食堂取餐:SQ 是点餐窗口,你把菜单递进去;CQ 是取餐窗口,厨师做完后把餐盘放在这里,你来取。
几个关键点:
- SQ 在主机内存里,由主机写入命令,控制器读取。
- CQ 也在主机内存里,由控制器写入完成项,主机读取。
- 一个 SQ 必须关联一个 CQ,但多个 SQ 可以共享同一个 CQ。
- SQ 和 CQ 都是环形缓冲区(ring buffer),靠头尾指针管理。
我在项目中见过有人把 SQ 和 CQ 搞混,结果中断处理逻辑写反了,系统直接 hang 住。嗯,这种错一次就记住了。
3.2 队列深度:并发能力的关键
队列深度(Queue Depth)指的是队列里最多能容纳的条目数。NVMe 允许每个队列深度从 1 到 65536 不等,具体取决于硬件实现和驱动配置。
深度越大,能同时“在空中”的命令就越多,吞吐量自然更高。但也不是越大越好——深度太大,延迟会变长,还可能把缓存打满。
| 队列深度 | 吞吐量 | 平均延迟 | 适用场景 |
|---|---|---|---|
| 1–4 | 低 | 低(稳定) | 延迟敏感型、控制类命令 |
| 16–64 | 中 | 中 | 通用业务、数据库事务 |
| 128–1024 | 高 | 较高 | 大块读写、视频流、AI 训练 |
我个人习惯:在驱动初始化时先查一下设备支持的 MQES(Maximum Queue Entries Supported),再根据业务场景做配置。别上来就拉满,容易出问题。
3.3 门铃机制:通知的艺术
门铃(Doorbell)是 NVMe 里最巧妙的设计之一。主机把命令写入 SQ 后,控制器并不知道有新命令。必须由主机写一次门铃寄存器,控制器才会去 SQ 里取命令。
为什么叫“门铃”?你想想看——你去拜访朋友,推开门进去,但朋友在里屋不知道你来了。你得按一下门铃,他才知道。门铃寄存器就是这个“按铃”的动作。
门铃写入流程:
- 主机把命令写入 SQ 的当前生产指针位置。
- 更新 SQ 的生产指针(tail pointer)。
- 写门铃寄存器(将新的 tail 值写入 BAR 空间中的对应偏移)。
- 控制器检测到门铃更新,开始从 SQ 取命令执行。
3.4 SQ/CQ 交互流程
整个交互流程可以用一张图讲清楚。下面这张图展示了从主机提交命令到控制器完成返回的完整路径:
我习惯把这个流程拆成 6 个步骤来理解:
- 提交命令:Host App 把命令写入 SQ 的当前生产指针位置。
- 写门铃:主机更新 SQ 的 Tail 门铃寄存器,通知控制器有新命令。
- 取命令:控制器从 SQ 中取出命令,开始解析和执行。
- 执行 I/O:控制器访问 NAND Flash,完成读写操作。
- 写完成项:控制器把完成状态写入 CQ,并更新 CQ 的生产指针。
- 中断通知:控制器通过 MSI-X 中断告诉主机“活干完了”。
nvme intel set-latency 或 perf 观察门铃写入频率和 CQ 完成率。如果门铃写入远高于完成