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,说明环形队列容量约束生效。

SVG:SQ/CQ 交互流程

提交队列 SQ SQE: op/lba/nlb/cid tail++ (写门铃) 环形取模 满则拒绝提交 控制器处理 从 SQ 取 SQE 执行读/写 生成 CQE 写入 CQ 完成队列 CQ CQE: cid/status/result head++ (回收) phase 位判断有效 主机 poll 解析 取命令 写完成