4. 队列创建:创建SQ和CQ的流程、Admin命令集介绍、Create I/O SQ/CQ命令详解
好,咱们进入正题。队列创建,说白了就是NVMe设备正式干活前的准备工作。你想想看,没有队列,数据怎么传?命令怎么发?所以这一步,是NVMe协议栈初始化的核心环节。
我个人习惯把队列创建分成两个阶段:Admin队列的创建和I/O队列的创建。Admin队列是控制器自己用的,I/O队列才是给上层应用用的。今天咱们重点讲I/O队列的创建流程,以及背后的命令机制。
4.1 队列创建的整体流程
先看一张流程图,帮你建立整体认知。这张图我画了很多遍,每次给团队新人讲NVMe时都会拿出来。
嗯,这张图基本把流程说清楚了。核心就四个步骤:查能力、配Admin、建CQ、建SQ。顺序不能乱,尤其是CQ必须在SQ之前创建。
4.2 Admin命令集概述
在讲具体的创建命令之前,咱们先聊聊Admin命令集。Admin命令集是NVMe协议的"控制面",所有管理操作都通过它来完成。
Admin命令集包含哪些命令?我列个表给你看:
| OPC | 命令名称 | 功能描述 | 我遇到的坑 |
|---|---|---|---|
| 00h | Delete I/O SQ | 删除I/O提交队列 | 删除前要确保队列空闲 |
| 01h | Create I/O SQ | 创建I/O提交队列 | 物理地址必须4K对齐 |
| 04h | Delete I/O CQ | 删除I/O完成队列 | 先删SQ再删CQ |
| 05h | Create I/O CQ | 创建I/O完成队列 | 中断向量别配错 |
| 06h | Identify | 获取控制器/命名空间信息 | 数据结构很大,注意缓冲区 |
| 08h | Abort | 中止正在执行的命令 | 只能中止自己的命令 |
| 09h | Set Features | 设置控制器特性 | 有些特性只读 |
| 0Ah | Get Features | 获取控制器特性 | 返回值要解析对 |
你看,创建队列的命令是01h和05h。这两个命令的格式非常相似,但细节上有区别。我当年第一次看Spec时,差点把这两个命令搞混了。
4.3 Create I/O CQ 命令详解
先讲Create I/O CQ。为什么先讲它?因为CQ是SQ的基础,没有CQ,SQ就没法用。
Create I/O CQ命令的Submission Entry格式如下:
// Create I/O CQ 命令格式 (64字节)
// 字节偏移 | 字段名 | 描述
// --------+----------------+----------------------------------
// DW0 | CDW0 | OPC=05h, FUSE, PSDT, CID
// DW1 | NSID | 命名空间ID (创建队列时设为0)
// DW2 | RSV | 保留
// DW3 | MPTR/Metadata | 元数据指针 (通常为0)
// DW4-DW5 | PRP1 | 物理区域页1 (CQ内存基地址)
// DW6-DW7 | PRP2 | 物理区域页2 (如果跨页)
// DW8 | CDW10 | QID (队列ID) | QSIZE (队列大小)
// DW9 | CDW11 | PC (物理连续) | IEN (中断使能) | IV (中断向量)
// DW10-DW15 | RSV | 保留
// 实际代码中,我习惯这样构造:
struct nvme_create_cq_cmd {
__u8 opcode; // 0x05
__u8 flags; // FUSE相关
__u16 cid; // 命令ID
__u32 nsid; // 0
__u64 prp1; // CQ内存物理地址
__u64 prp2; // 跨页时的第二个地址
__le16 qid; // 队列ID (从1开始)
__le16 qsize; // 队列深度 (实际值-1)
__le16 pc : 1; // 1=物理连续, 0=不连续
__le16 ien : 1; // 1=使能中断
__le16 iv : 14; // 中断向量号
__u32 rsv[5]; // 保留
};
这里有几个关键点,我得重点说说:
- QID:队列ID从1开始,0是Admin队列的。我曾经见过有人把QID设为0,结果控制器直接返回无效参数。
- QSIZE:队列深度,实际值是
qsize + 1。比如你设qsize=127,实际深度就是128。必须是2的幂。 - PC位:物理连续标志。我建议你尽量用物理连续的内存,这样只需要一个PRP1就够了,省事。
- IEN和IV:中断使能和中断向量。如果你用MSI-X,IV就是中断向量号。别配错了,否则中断不来或者乱来。
重要提醒:CQ的内存必须由主机分配,并且保证在队列生命周期内有效。控制器不会帮你管理内存,它只管用。
4.4 Create I/O SQ 命令详解
好,CQ创建完了,接下来创建SQ。SQ的格式和CQ很像,但有一个关键区别:SQ需要指定它关联的CQ。
// Create I/O SQ 命令格式 (64字节)
// DW0-DW7 与Create I/O CQ相同
// DW8 | CDW10 | QID | QSIZE
// DW9 | CDW11 | PC | QPRIO (队列优先级) | CQID (关联的CQ)
struct nvme_create_sq_cmd {
__u8 opcode; // 0x01
__u8 flags;
__u16 cid;
__u32 nsid; // 0
__u64 prp1; // SQ内存物理地址
__u64 prp2;
__le16 qid; // SQ队列ID
__le16 qsize; // 队列深度
__le16 pc : 1; // 物理连续
__le16 qprio : 2; // 队列优先级 (1=URGENT, 2=HIGH, 3=MEDIUM)
__le16 cqid : 13; // 关联的CQ ID
__u32 rsv[5];
};
这里有个容易踩的坑:QPRIO(队列优先级)。NVMe定义了三种优先级:
- 1 (URGENT):紧急队列,控制器会优先处理
- 2 (HIGH):高优先级
- 3 (MEDIUM):中优先级
我当年在一个项目中,把所有SQ都设成了URGENT优先级。结果呢?控制器忙得团团转,反而影响了整体性能。后来我改成按业务类型分配优先级,效果好了很多。
我的建议:管理命令用HIGH优先级,数据读写用MEDIUM。除非你有特殊需求,否则别轻易用URGENT。
4.5 队列创建的完整流程示例
光说不练假把式。我给你一个完整的队列创建流程,从驱动角度看的伪代码:
// 队列创建流程 (伪代码)
int nvme_create_io_queues(struct nvme_ctrl *ctrl, int nr_queues)
{
int i, ret;
// 1. 先查控制器能力
ret = nvme_identify_ctrl(ctrl);
if (ret) return ret;
// 2. 计算需要的队列数量
int nr_cq = min(nr_queues, ctrl->max_qid);
int nr_sq = nr_cq; // 通常1:1映射
// 3. 为每个队列分配内存 (4K对齐, 物理连续)
for (i = 1; i <= nr_cq; i++) {
ctrl->cq[i].mem = dma_alloc_coherent(..., 4K * qdepth);
ctrl->sq[i].mem = dma_alloc_coherent(..., 4K * qdepth);
}
// 4. 先创建所有CQ
for (i = 1; i <= nr_cq; i++) {
ret = nvme_admin_cmd(ctrl, CREATE_CQ,
.qid = i,
.qsize = qdepth - 1,
.prp1 = ctrl->cq[i].dma_addr,
.ien = 1,
.iv = i // 每个CQ用不同的中断向量
);
if (ret) goto err;
}
// 5. 再创建所有SQ
for (i = 1; i <= nr_sq; i++) {
ret = nvme_admin_cmd(ctrl, CREATE_SQ,
.qid = i,
.qsize = qdepth - 1,
.prp1 = ctrl->sq[i].dma_addr,
.cqid = i, // SQ i 关联 CQ i
.qprio = 2 // HIGH优先级
);
if (ret) goto err;
}
return 0;
err:
// 清理:先删SQ,再删CQ
for (; i >= 1; i--) {
nvme_admin_cmd(ctrl, DELETE_SQ, .qid = i);
nvme_admin_cmd(ctrl, DELETE_CQ, .qid = i);
}
return ret;
}
注意:创建失败后的清理顺序很重要。必须先删SQ再删CQ,因为CQ可能还被SQ引用着。我曾经在调试时搞反了顺序,结果控制器直接挂了,只能重启。
4.6 队列创建中的常见问题
最后,我总结几个我在项目中实际遇到过的坑:
- 内存对齐问题:队列内存必须4K对齐。有一次我用了kmalloc分配内存,没注意对齐,结果PRP地址传过去后控制器报错。
- 队列深度设置:深度必须是2的幂,且不能超过控制器支持的最大值。我建议你先读Identify数据,看看
MQES字段。 - 中断向量冲突:多个CQ共用同一个中断向量时,要确保中断处理函数能区分是哪个CQ触发的。
- 队列ID重复:每个队列的ID必须唯一。如果你删除了一个队列,它的ID可以重用,但要确保之前的队列已经完全释放。
嗯,关于队列创建的内容,今天就讲这么多。记住核心思路:先CQ后SQ,顺序不能乱。下节课咱们聊聊队列的删除和重置,那也是容易出问题的地方。
一句话总结:队列创建是NVMe初始化的基石,CQ是SQ的前提,顺序和参数配置决定了系统能否稳定运行。
可可精品资料库