队列机制详解

队列机制是NVMe的灵魂。说实话,当年我刚开始接触NVMe时,最大的感触就是——这玩意儿把"并行"和"异步"玩到了极致。AHCI那种老掉牙的寄存器轮询,在NVMe的队列设计面前,完全不是一个时代的产物。你想想看,一条PCIe通道上能同时跑几十个队列,每个队列又各自独立,这吞吐量能不高吗?

4.1 提交队列与完成队列

提交队列(Submission Queue,简称SQ)和完成队列(Completion Queue,简称CQ),说白了就是主机和控制器之间两条方向相反的"管道"。

  • SQ:主机往里写命令,控制器从中取命令。
  • CQ:控制器往里写完成状态,主机从中读结果。

这两条队列都设计成了环形缓冲区。每个队列都有两个指针:Head(头指针)和Tail(尾指针)。主机往SQ的Tail位置写命令,然后写门铃寄存器告诉控制器"我有新命令"。控制器从SQ的Head位置取命令,处理完后把完成状态写到CQ的Tail位置,再通过中断或门铃通知主机"活干完了"。

核心要点:SQ和CQ是配对的。一个SQ必须绑定一个CQ,这种配对关系叫做队列对(Queue Pair,QP)。没有配对的SQ,控制器根本不会搭理它。

我在项目中遇到过有人把SQ和CQ的中断向量配错了,结果控制器死活不产生中断,主机只能靠轮询硬扛,性能直接腰斩。嗯,这种坑踩过一次就不会忘了。

4.2 队列对(QP)的创建与删除

QP的创建和删除,都是通过Admin命令集来完成的。系统初始化时,Admin队列本身就在那里了,那是第0号QP。我们要创建的是I/O队列。

创建顺序:先CQ,后SQ。

  1. 发送"创建I/O完成队列"Admin命令,指定CQ ID、队列大小、中断向量等参数。
  2. 发送"创建I/O提交队列"Admin命令,指定SQ ID、队列大小,以及它要绑定的CQ ID。

删除顺序:先SQ,后CQ。次序不能乱,否则控制器会报错。

我曾经见过一个坑:有人在热拔插场景下先删了CQ,再删SQ,结果控制器还在等待那个已经不存在的CQ写完成状态,直接卡死。从此我写代码时,删除队列的顺序都写在最显眼的注释里。

下面是一个创建I/O队列的伪代码示例:

// 1. 创建 I/O 完成队列 (CQ)
struct nvme_admin_cmd cmd;
cmd.opcode  = 0x05;                // 创建 I/O CQ
cmd.cdw10   = (cq_id << 16) | queue_size;
cmd.cdw11   = (irq_vector << 16) | 0x01;  // 使能中断
submit_admin_command(&cmd);
// 2. 创建 I/O 提交队列 (SQ)
cmd.opcode  = 0x