4. NVMe Admin命令

任务说明

本案例使用 SPDK 的 NVMe 驱动向控制器发送 Identify Controller(CNS=0x01)Admin 命令,读取 4096 字节的 struct spdk_nvme_ctrlr_data,并打印型号、序列号、固件版本、VID/DID 等关键字段。程序直接绑定 PCIe 设备,不依赖 SPDK 应用框架,便于独立验证。

环境与依赖

  • 已编译安装 SPDK(提供 librt、libspdk_env_dpdk、libspdk_nvme 等库)。
  • 以 root 运行,并预留大页内存:echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages。
  • 确认目标 NVMe 设备 PCIe 地址(如 0000:01:00.0),可用 lspci | grep -i nvme 查看。

核心代码

文件 identify_ctrlr.c:初始化 SPDK 环境,探测指定 PCIe 地址的控制器,发送 Identify Controller 命令并打印字段。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "spdk/env.h"
#include "spdk/nvme.h"

static void print_field(const char *name, const char *buf, size_t len)
{
    char tmp[64] = {0};
    size_t n = len < sizeof(tmp) - 1 ? len : sizeof(tmp) - 1;
    memcpy(tmp, buf, n);
    /* NVMe 字符串字段为定长、空格填充,去掉尾部空格 */
    while (n > 0 && (tmp[n - 1] == ' ' || tmp[n - 1] == '\0')) {
        tmp[--n] = '\0';
    }
    printf("%-16s: %s\n", name, tmp);
}

int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "usage: %s <pci_addr e.g. 0000:01:00.0>\n", argv[0]);
        return 1;
    }

    struct spdk_env_opts opts;
    spdk_env_opts_init(&opts);
    opts.name = "identify_ctrlr";
    if (spdk_env_init(&opts) < 0) {
        fprintf(stderr, "spdk_env_init failed\n");
        return 1;
    }

    struct spdk_nvme_transport_id trid = {0};
    trid.trtype = SPDK_NVME_TRANSPORT_PCIE;
    snprintf(trid.traddr, sizeof(trid.traddr), "%s", argv[1]);

    struct spdk_nvme_ctrlr_opts ctrlr_opts;
    spdk_nvme_ctrlr_get_default_ctrlr_opts(&ctrlr_opts, sizeof(ctrlr_opts));

    struct spdk_nvme_ctrlr *ctrlr =
        spdk_nvme_connect(&trid, &ctrlr_opts, sizeof(ctrlr_opts));
    if (!ctrlr) {
        fprintf(stderr, "spdk_nvme_connect failed for %s\n", argv[1]);
        spdk_env_fini();
        return 1;
    }

    const struct spdk_nvme_ctrlr_data *data =
        spdk_nvme_ctrlr_get_data(ctrlr);
    if (!data) {
        fprintf(stderr, "get ctrlr data failed\n");
        spdk_nvme_detach(ctrlr);
        spdk_env_fini();
        return 1;
    }

    printf("=== Identify Controller ===\n");
    printf("%-16s: %04x\n", "VID", data->vid);
    printf("%-16s: %04x\n", "SSVID", data->ssvid);
    print_field("Model", (const char *)data->mn, sizeof(data->mn));
    print_field("Serial", (const char *)data->sn, sizeof(data->sn));
    print_field("Firmware", (const char *)data->fr, sizeof(data->fr));
    printf("%-16s: %u\n", "Max SQ Entries", data->maxcmd + 1);
    printf("%-16s: %u\n", "Max CQ Entries", data->maxcmd + 1);
    printf("%-16s: %u\n", "Namespace Count", data->nn);

    spdk_nvme_detach(ctrlr);
    spdk_env_fini();
    return 0;
}

编译与运行

gcc -o identify_ctrlr identify_ctrlr.c \
    $(pkg-config --cflags --libs spdk_nvme spdk_env_dpdk) \
    -lspdk_nvme -lspdk_env_dpdk -lspdk_util -lspdk_log -lpthread -ldl -lnuma

sudo ./identify_ctrlr 0000:01:00.0

若 pkg-config 未提供 SPDK 描述文件,可改用 SPDK 源码目录下的 build/lib 与 include 路径手动指定 -I 与 -L。

验证方法

  • 程序正常退出码为 0,并打印 === Identify Controller === 及字段列表。
  • Model、Serial、Firmware 应与 nvme id-ctrl /dev/nvme0 输出一致(同一设备)。
  • VID/DID 与 lspci -nn 中该设备的厂商/设备 ID 对应。
  • 若连接失败,检查 PCIe 地址是否正确、是否已绑定到 vfio-pci 或 uio、大页内存是否足够。

关键点

spdk_nvme_ctrlr_get_data 返回的指针在控制器生命周期内有效,无需手动释放;mn/sn/fr 为定长空格填充字符串,打印前需截断尾部空格。Identify Controller 命令由 SPDK 在 spdk_nvme_connect 内部自动发送,本案例直接读取缓存结果,无需手动构造命令。