1. NVMe协议基础与命令队列模拟
任务说明
本案例实现一个用户态 NVMe 命令队列模拟器:用环形数组模拟提交队列(SQ)与完成队列(CQ),支持提交读/写命令,并在命令被“处理”后生成完成队列条目(CQE),最后解析 CQE 得到状态与结果。核心是理解 SQ/CQ 的入队、出队、门铃(doorbell)与相位(phase)翻转机制。
关键机制
- SQ 条目(SQE):命令操作码(读/写)、起始 LBA、数据长度、命令 ID。
- CQ 条目(CQE):命令 ID、状态码、结果(如读回的数据长度)。
- 队列为环形,用
head/tail与容量取模推进;CQ 用phase位判断条目是否有效。 - 提交命令 = 写 SQE 到 SQ 并推进 SQ tail(模拟写门铃);处理命令 = 从 SQ 取 SQE,生成 CQE 写入 CQ。
完整代码(C++17,单文件)
#include <cstdint>
#include <cstdio>
#include <vector>
#include <optional>
enum class Op : uint8_t { Read = 0x02, Write = 0x01 };
struct SQE { // 提交队列条目
Op op;
uint64_t lba; // 起始逻辑块地址
uint32_t nlb; // 传输块数
uint16_t cid; // 命令 ID
};
struct CQE { // 完成队列条目
uint16_t cid;
uint16_t status; // 0 = 成功
uint32_t result; // 传输字节数
};
// 环形队列:容量固定,head/tail 取模推进
template <typename T, size_t N>
struct Ring {
T buf[N];
size_t head = 0; // 消费者位置
size_t tail = 0; // 生产者位置
bool full = false;
bool push(const T& v) {
if (full) return false;
buf[tail] = v;
tail = (tail + 1) % N;
full = (tail == head);
return true;
}
std::optional<T> pop() {
if (!full && head == tail) return std::nullopt;
T v = buf[head];
head = (head + 1) % N;
full = false;
return v;
}
bool empty() const { return !full && head == tail; }
};
struct NvmeSim {
static constexpr size_t QD = 4; // 队列深度
Ring<SQE, QD> sq;
Ring<CQE, QD> cq;
bool cqPhase = true; // CQ 相位位,初始为 1
uint16_t nextCid = 1;
// 提交一条命令,返回分配的命令 ID
uint16_t submit(Op op, uint64_t lba, uint32_t nlb) {
SQE e{op, lba, nlb, nextCid++};
if (!sq.push(e)) { printf("SQ full, submit failed\n"); return 0; }
printf("submit cid=%u op=%s lba=%llu nlb=%u\n",
e.cid, op == Op::Read ? "READ" : "WRITE",
(unsigned long long)e.lba, e.nlb);
return e.cid;
}
// 处理一条 SQ 命令,生成 CQE(模拟控制器执行)
bool processOne() {
auto e = sq.pop();
if (!e) return false;
uint32_t bytes = e->nlb * 512; // 每块 512 字节
CQE c{e->cid, 0, bytes};
if (!cq.push(c)) { printf("CQ full\n"); return false; }
printf("complete cid=%u status=%u result=%u bytes\n",
c.cid, c.status, c.result);
return true;
}
// 解析一条 CQE:检查相位位后取出
std::optional<CQE> poll() {
if (cq.empty()) return std::nullopt;
CQE c = *cq.pop();
// 相位位翻转:每绕一圈翻转一次(此处简化,仅演示语义)
if (cq.head == 0) cqPhase = !cqPhase;
printf("poll cid=%u status=%u result=%u phase=%d\n",
c.cid, c.status, c.result, (int)cqPhase);
return c;
}
};
int main() {
NvmeSim dev;
dev.submit(Op::Write, 0, 2); // 写 2 块
dev.submit(Op::Read, 8, 1); // 读 1 块
dev.submit(Op::Read, 16, 4); // 读 4 块
while (dev.processOne()) {} // 控制器处理全部命令
while (auto c = dev.poll()) { // 主机回收完成
if (c->status != 0) printf("command %u failed\n", c->cid);
}
return 0;
}
编译与运行
g++ -std=c++17 -O2 nvme_sim.cpp -o nvme_sim
./nvme_sim
验证方法与通过条件
- 运行后应看到 3 条
submit、3 条complete、3 条poll日志,且 cid 一一对应。 - 每条 CQE 的
status为 0,result等于nlb * 512(如 nlb=2 时 result=1024)。 - 若把
QD改为 2 并提交 3 条命令,应出现SQ full, submit failed,说明环形队列容量约束生效。