nvme-cli 测试与 CI 实战指南:容器化构建复现、Valgrind 内存检测与 ASan/UBSan 清理

原创2026-10-04 22:41:211,987 阅读
文章标签:CLI存储

nvme-cli 测试与 CI 实战指南:容器化构建复现、Valgrind 内存检测与 ASan/UBSan 清理

nvme-cli 是 NVMe 管理命令行工具,其质量保障体系覆盖从单元测试到真机端到端(e2e)测试的多层测试套件,并配套了与 GitHub Actions 一致的 CI 容器、内存检测与 Sanitizer 流程。本文以 Documentation/TESTING.md 为核心骨架,结合仓库内 scripts/build.sh、valgrind.supp、meson.build 与 tests/ 目录的实际实现,完整讲解如何本地复现 CI 构建、完成跨架构交叉编译,并系统排查 C 代码中的内存泄漏与未定义行为问题。读完本文,你将掌握一套可复制、可落地的 nvme-cli 开发自测工作流。

一、测试套件全景:先弄清你在测什么

在进入具体工具之前,先了解 nvme-cli 的测试是如何组织的。根据 tests/README 的说明,所有测试按运行条件分为三层:

套件 目录 运行前提 覆盖内容
单元测试 tests/unit 无需构建产物、无需硬件 快速 C 与 Python 单测,如 test-argconfig-parse.c、test-plugin.c、命令元数据 schema 校验等
CLI 测试 tests/cli 需要已构建的 nvme 二进制,无需硬件 registry、keys、config-convert/create、nbft 等不触碰设备的功能
端到端测试 tests/e2e 需要真实(或模拟的)控制器/命名空间 真实下发 nvme 命令,例如 nvme_id_ctrl_test.py、nvme_format_test.py、nvme_read_write_test.py 等

单元与 CLI 测试默认构建(由 meson tests 选项控制,默认开启);e2e 测试因为会读写真实设备、创建/删除命名空间,属于破坏性测试,必须显式用 -De2e-tests=true 才启用,且目标设备从不猜测,必须显式传入 -De2e-controller=/dev/nvme0 与 -De2e-ns1=/dev/nvme0n1,否则 meson setup 直接报错(见 tests/e2e/meson.build 与 meson_options.txt)。e2e 目录下还提供了 nvme_simple_template_test.py 模板,供编写新用例时复制参考。

二、快速体验:预构建静态二进制

对于只想快速验证 CLI 行为、不想从头编译的场景,项目在常规 CI 之外额外提供一份基于当前 HEAD 构建的 x86_64 静态二进制(详见 Documentation/TESTING.md 的 "Pre-built binary" 一节)。静态构建意味着不依赖目标系统上的动态库版本,拿到即可运行,适合在无编译环境的机器上做快速冒烟测试。其构建方式与仓库内 scripts/build.sh 的 static 配置一致(--default-library=static、-Dc_link_args="-static" 等,见 scripts/build.sh)。

三、容器化调试与 CI 构建复现

nvme-cli 官方 CI 工作流运行在预构建的容器镜像中,因此你可以在本地用 Docker 或 Podman 拉取与 CI 完全一致的环境,复现任何构建或调试问题,无需自己手工搭建依赖环境。

3.1 拉取 CI 容器

所有 CI 容器都以 OCI/Docker 镜像形式发布在 ghcr.io/linux-nvme/ 命名空间下。以 Debian 最新版 CI 镜像为例:

docker pull ghcr.io/linux-nvme/debian:latest

使用 Podman 的等价命令:

podman pull ghcr.io/linux-nvme/debian:latest

3.2 启动容器并登录

启动一个交互式 shell:

docker run --rm -it \
  --name nvme-cli-debug \
  ghcr.io/linux-nvme/debian:latest \
  bash

Podman 用户:

podman run --rm -it \
  --name nvme-cli-debug \
  ghcr.io/linux-nvme/debian:latest \
  bash

进入后,你得到的就是 CI 使用的同一套构建环境(同样的编译器、依赖库版本、构建工具链)。--rm 保证退出即清理,-it 提供交互式终端。

3.3 克隆仓库并检出目标分支

容器内克隆 nvme-cli 源码:

git clone https://github.com/linux-nvme/nvme-cli.git
cd nvme-cli

(可选)检出指定分支或提交,用于复现某个 PR 或某个历史版本的构建问题:

git checkout <branch-or-commit>

3.4 运行 CI 构建脚本

GitHub Actions 工作流实际调用的是仓库根目录下的 scripts/build.sh。在容器内直接执行默认配置即可复现 CI 的常规构建:

./scripts/build.sh

默认配置会做三件事:meson setup(开启 --werror,把警告视为错误)、meson compile 编译、meson test 运行测试。构建产物默认留在容器内部,除非挂载宿主卷,否则退出容器即丢失——需要保留产物时,可在 docker run 时用 -v 挂载宿主目录。

四、交叉编译示例:为 s390x 构建

CI 支持使用专用交叉编译容器进行跨架构构建。以 s390x(IBM Z)为例:

4.1 拉取交叉编译容器

docker pull ghcr.io/linux-nvme/ubuntu-cross-s390x:latest

Podman 用户:

podman pull ghcr.io/linux-nvme/ubuntu-cross-s390x:latest

4.2 启动交叉编译容器

docker run --rm -it \
  --name nvme-cli-cross \
  ghcr.io/linux-nvme/ubuntu-cross-s390x:latest \
  bash

Podman 用户:

podman run --rm -it \
  --name nvme-cli-cross \
  ghcr.io/linux-nvme/ubuntu-cross-s390x:latest \
  bash

4.3 克隆仓库

git clone https://github.com/linux-nvme/nvme-cli.git
cd nvme-cli

4.4 执行交叉构建

./scripts/build.sh -b release -c gcc -t s390x cross

参数含义:-b release 指定 release 构建类型,-c gcc 指定编译器,-t s390x 指定交叉编译目标,最后一个位置参数 cross 选择交叉编译配置。从 scripts/build.sh 的实现可以看到,cross 配置会通过 --cross-file=.github/cross/ubuntu-cross-${CROSS_TARGET}.txt 加载对应架构的交叉工具链文件(仓库中已有 armhf、i386、ppc64le、s390x 四个:.github/cross),并针对 i386 特殊处理 pkg-config 的 multiarch 查找路径。具体支持哪些目标取决于容器内安装的工具链,例如 -t arm、-t ppc64le、-t i386 都可尝试。

五、scripts/build.sh 深度解析:CI 构建矩阵

scripts/build.sh 是整个 CI 构建矩阵的入口,支持大量配置与开关,理解它等于理解官方 CI 在测什么。其完整用法可执行 ./scripts/build.sh -h 查看,核心参数如下:

位置参数(config,选择 meson 构建配置):

配置 作用
default 默认配置:--werror + ioctl 测试
libdbus 启用 libdbus 构建(-Dlibdbus=enabled)
liburing 启用 liburing 构建(-Dliburing=enabled)
fallback 下载所有依赖并作为共享库构建(--wrap-mode=forcefallback)
cross 使用交叉工具链构建(配合 -t 指定目标)
distro 分别构建 libnvme 与 nvme-cli,模拟发行版打包流程
docs / man_docs / html_docs / rst_docs 构建全部/手册/HTML/rst 文档
static 构建静态二进制
minimal_static 构建不含 fabrics 支持的最小静态二进制(使用 musl-gcc)
nofabrics 关闭 fabrics 支持并运行单元测试
libnvme 只构建 libnvme
libnvme_abi 只构建 libnvme3.so,不构建测试与示例(用于 ABI 对比)
tests 面向夜间(nightly)构建的测试配置,配合 e2e 参数使用

命令行开关:

开关 作用
-a 用 clang 静态分析器(scan-build)做静态分析
-b [release|debug] 构建类型,默认 release
-c [gcc|clang] 编译器,默认 gcc
-m [meson|muon] 构建工具,默认 meson;muon 是 meson 的轻量替代
-p 启用覆盖率报告(仅 meson 支持,产出 coverage.xml)
-s 使用 ASan+UBSan 测试(对应 asanubsan setup)
-t [arm|ppc64le|s390x|i386] 交叉编译目标
-x 使用 Valgrind 测试(对应 valgrind setup)

tests 配置专属的 e2e 参数(与 meson 的 e2e-* 选项一一对应,见 meson_options.txt):

--e2e-controller /dev/nvme0   # 控制器设备(必填)
--e2e-ns1 /dev/nvme0n1        # 命名空间设备(必填)
--e2e-log-dir nvmetests       # e2e 日志目录
--e2e-log-level DEBUG         # 日志级别:DEBUG/INFO/WARNING/ERROR/CRITICAL
--e2e-nvme-bin /path/to/nvme  # 被测 nvme 二进制路径
--plugin-tests micron,ocp     # 针对真实硬件运行的厂商插件测试套件

值得注意的两个实现细节(从 scripts/build.sh 与 scripts/build.sh 可以确认):

  • 除 tests 配置外,任何配置传入 --e2e-* 或 --plugin-tests 参数都会直接报错退出,避免误用;
  • e2e 测试破坏性极强,脚本明确"拒绝猜测目标设备",所以 --e2e-controller 与 --e2e-ns1 必须显式给出。

在 test_meson()(scripts/build.sh)中可以看到,-x 与 -s 最终分别映射为 meson test --setup valgrind 与 meson test --setup asanubsan,与下文的内存检测流程一一对应。

六、Valgrind 内存测试:查泄漏与越界

Valgrind 是检测内存泄漏、非法读写的第一道防线。nvme-cli 在 meson.build 中注册了一个命名测试 setup:meson test --setup valgrind 会通过 exe_wrapper 把所有单元测试都跑在 valgrind --leak-check=full 之下,并将任何泄漏视为测试失败(--error-exitcode=1)。

6.1 前置条件与构建

Valgrind 必须预先安装(Debian/Ubuntu 系:apt install valgrind,其他发行版同理)。然后按常规 meson 流程构建:

meson setup .build
meson compile -C .build
meson test -C .build --setup valgrind

测试输出会单独写到 .build/meson-logs/testlog-valgrind.txt,便于事后分析。

6.2 泄漏判定策略:definite/possible 才失败

从 meson.build 可见,Valgrind 命令行启用了 --show-leak-kinds=all,即日志中会列出每一种泄漏类型并附完整调用栈,包括进程退出时仍被全局变量可达(still reachable)的块。但只有 "definite"(确定)与 "possible"(可能)泄漏才会导致测试失败,这是 Valgrind 的默认行为:still-reachable 的块通常是进程持有到退出为止的第三方全局状态,因此只报告供人工检视,而不作为 CI 门禁。若检视后发现某个 still-reachable 块其实是自己的代码且无界增长,应当修复或加入抑制文件。

6.3 内置抑制文件 valgrind.supp

仓库根目录自带的 valgrind.supp 会被 meson.build 自动加载,针对三类已知误报做了抑制:

  1. Shell 脚本测试:Valgrind 包装的是 shell 解释器而非被测 C 二进制,bash 自身的内部分配被抑制(obj:*/bash);
  2. CPython 内部机制:Python 测试脚本通过 SWIG 导入 libnvme 的 Python 绑定,解释器的导入机制(import machinery)、marshal 与 tokenizer 代码会产生非本项目的报告;由于不同发行版中 Python 可能是静态二进制或动态链接 libpython3,抑制文件为两者各准备了一条 obj 模式;
  3. SWIG 模块析构:SWIG 在模块初始化时分配一个 varlink 对象,CPython 在解释器关闭时从不释放它——这是 SWIG 的已知限制,而非 libnvme 的泄漏,抑制规则精确匹配 SWIG_Python_newvarlink 调用栈(valgrind.supp)。

此外,如果系统提供了 /usr/lib/valgrind/python3.supp,配置时也会被自动加载(见 meson.build)。抑制之后仍然出现的失败,就是 C 代码中的真实问题,需要认真对待。

七、ASan / UBSan:编译期注入的守卫

ASan(AddressSanitizer)与 UBSan(UndefinedBehaviourSanitizer)直接编译进二进制,因此各自需要独立的构建目录。二者检测的问题类别不同:

  • ASan(address):堆溢出、use-after-free(释放后使用)、内存泄漏;
  • UBSan(undefined):有符号整数溢出、指针未对齐访问、空指针解引用、越界索引,以及其他 C 未定义行为。

7.1 构建与运行

meson setup .build-asanubsan -Db_sanitize=address,undefined
meson compile -C .build-asanubsan
meson test -C .build-asanubsan --setup asanubsan

关键点:不需要任何包装二进制——Sanitizer 在检测到违规时直接把报告输出到 stderr(meson test 的默认输出流)。asanubsan setup 在 meson.build 中只设置了 timeout_multiplier: 2(Sanitizer 运行较慢),而没有额外包装命令。

7.2 已知限制与排查建议

与 Valgrind 相同的限制依然适用:Python 测试与 Shell 脚本测试可能产生误报(原因见上文 6.3 节)。排查时应把重点放在 nvme-cli - * 命名的单元测试上——它们直接针对 C 代码,出现报告即基本可判定为真实缺陷。另外注意:Sanitizer 检测到的内存泄漏默认只在退出时报告,且 ASan 的泄漏检测与 Valgrind 的判定口径不同,两者可以互为补充,不必互相替代。

八、覆盖率与 CI 工作流:从本地到流水线

理解 CI 工作流能帮助你判断本地复现的价值。仓库的 .github/workflows 目录中与本主题直接相关的工作流有:

  • coverage.yml:手动触发的覆盖率工作流,在 ghcr.io/linux-nvme/debian:latest 容器内执行 scripts/build.sh -p,产出 coverage.xml 后上报;
  • run-nightly-tests.yml:每日定时(cron 0 01 * * *)的夜间测试,在真实 NVMe 硬件虚拟机上运行;其中 nvme-cli 测试步骤完整演示了 tests 配置的实战用法:
scripts/build.sh -b release -c gcc -p \
    --e2e-controller "${CONTROLLER}" \
    --e2e-ns1 "${BDEV0}" \
    --e2e-log-dir "/nvme-cli/tests/nvmetests" \
    --e2e-log-level "DEBUG" \
    --e2e-nvme-bin "/nvme-cli/.build-ci/nvme" \
    --plugin-tests ocp,micron \
    tests

这也印证了 Documentation/TESTING.md 的核心主张:在容器/虚拟机中跑 scripts/build.sh 就是在本地复现 CI。夜间流水线还把 e2e 与 blktests 的 coverage-*.xml 合并上报,形成多维度覆盖率数据。

九、一套完整的本地自测流程

综合以上内容,推荐在提交代码前按如下顺序执行自测(无需真实硬件):

# 1. 常规构建 + 单元/CLI 测试(对应 CI 默认配置)
./scripts/build.sh

# 2. 内存泄漏检查(需先 apt install valgrind)
./scripts/build.sh -x

# 3. 地址/未定义行为清理(Sanitizer 编译进二进制)
./scripts/build.sh -s

# 4. 静态分析(可选,需 clang 与 scan-build)
./scripts/build.sh -a

若手头有真实 NVMe 设备且确认了解其风险(e2e 会读写真实数据),可进一步在 CI 容器中运行:

./scripts/build.sh -b release -c gcc \
    --e2e-controller /dev/nvme0 \
    --e2e-ns1 /dev/nvme0n1 \
    --e2e-log-level DEBUG \
    tests

三个实战要点务必牢记:Valgrind 与 Sanitizer 的误报高发区是 Python/Shell 测试,以 C 单元测试结果为准;e2e 测试必须显式指定设备且默认拒绝猜测;构建产物默认只存在于容器内,需要持久化就挂载宿主卷。 掌握这套流程后,任何 CI 报错都可以在本地快速还原、定位并验证修复,这正是 nvme-cli 测试体系设计的核心价值所在。

登录后查看全文
nvme-cli