Skip to content

Latest commit

 

History

History
2643 lines (2033 loc) · 87.5 KB

File metadata and controls

2643 lines (2033 loc) · 87.5 KB

KUnit 内核单元测试框架深度分析

源码基线:Linux mainline(截至 2026-03) 核心文件:lib/kunit/test.clib/kunit/executor.clib/kunit/assert.cinclude/kunit/test.hinclude/kunit/resource.hlib/kunit/try-catch.c


目录

  1. 背景与设计理念
  2. KUnit 与 kselftest 的对比
  3. 整体架构
  4. 核心数据结构
  5. 宏展开分析:KUNIT_CASE 与 kunit_test_suite
  6. 测试执行器(Executor)与结果收集
  7. try-catch 机制:测试隔离的底层原理
  8. 断言宏体系的完整实现
  9. kunit_resource 资源管理(RAII 模式)
  10. TAP / KTAP 输出格式
  11. 在模块中编写 KUnit 测试
  12. 参数化测试
  13. kselftest 框架详解与结构
  14. 运行方式:kunit.py 工具链
  15. debugfs 集成与在线重跑
  16. 实战示例:完整测试模块
  17. 常见问题与最佳实践
  18. Static Stub:函数重定向机制
  19. KUnit 托管设备:kunit_device
  20. Hooks 机制:跨模块边界感知测试
  21. 属性系统与过滤器深度解析
  22. string_stream:日志缓冲实现
  23. UML(User Mode Linux)运行测试
  24. QEMU 虚拟机运行内核测试
  25. gcov 与内核代码覆盖率
  26. 在真实硬件上运行:ftrace 与 debugfs
  27. Kconfig 完整配置参考
  28. KUnit 自测:框架自我验证
  29. 总结

1. 背景与设计理念

1.1 内核测试的历史困境

在 KUnit 出现之前,Linux 内核的测试体系存在明显的"中间层空白":

  • kselftest:运行在用户态(ring 3),通过系统调用与内核交互,无法直接测试 内核内部的 static 函数或数据结构。
  • 散落的 test_*.c 文件:各子系统自行维护,缺乏统一框架约束,输出格式不一, 难以集成到 CI/CD 流水线。

核心矛盾在于:驱动/子系统开发者需要在不暴露内部接口的前提下,快速验证内部逻辑的正确性。

1.2 KUnit 的设计目标

KUnit 由 Google 工程师 Brendan Higgins 于 2019 年编写,随 Linux 5.5 合入主线 (lib/kunit/test.c:1-7):

// SPDX-License-Identifier: GPL-2.0
/*
 * Base unit test (KUnit) API.
 *
 * Copyright (C) 2019, Google LLC.
 * Author: Brendan Higgins <brendanhiggins@google.com>
 */

设计目标体现在以下五个维度:

  1. 内核态直接执行:测试代码与被测代码同处内核地址空间,可调用任意内核 函数,含 static 函数(配合 EXPORT_SYMBOL_IF_KUNIT)。

  2. 零生产开销:当 CONFIG_KUNIT 未开启时,所有测试相关代码被预处理器完整 消除,对生产内核毫无影响。

  3. 快速开发周期:支持在 UML(User Mode Linux)上无需真实硬件运行, 通过 kunit.py 工具链数秒内完成构建和测试。

  4. 标准化输出:输出 KTAP 协议(TAP 的内核超集),可被任意 TAP 解析器处理, 无缝对接 CI 系统。

  5. 资源自动回收:借鉴 devres(device resource management)思路,通过 kunit_resource 实现测试结束后的自动清理,无论测试成功、失败还是超时。


2. KUnit 与 kselftest 的对比

维度 KUnit kselftest
执行空间 内核态(ring 0) 用户态(ring 3)
测试目标 内核内部函数、数据结构 系统调用、内核 ABI
编译方式 随内核构建(built-in 或 module) 独立编译
运行时机 内核启动时 / 模块加载时 独立进程、任意时刻
隔离机制 kthread + try-catch(内核线程) 进程隔离(fork/exec)
无硬件运行 支持(UML) 需要完整用户环境
输出格式 KTAP(TAP 超集) TAP version 13
资源管理 kunit_resource RAII 标准 C 手动管理
适用场景 算法、数据结构、驱动核心逻辑 网络协议、ABI、安全

关键结论:kselftest 测试内核对外暴露的接口,KUnit 测试内核内部实现,两者 互补而非替代。大型子系统通常同时使用两种框架。


3. 整体架构

+------------------------------------------------------------------+
|                      Linux 内核地址空间                           |
|                                                                  |
|  .kunit_test_suites ELF section                                  |
|  +------------------+                                            |
|  | &suite_A         | <-- __kunit_suites_start                   |
|  | &suite_B         |      (executor.c:13-14)                    |
|  | &suite_C         |                                            |
|  | ...              | <-- __kunit_suites_end                     |
|  +------------------+                                            |
|           |                                                      |
|           v  late_initcall / module notifier                     |
|  +-----------------------------+                                 |
|  | kunit_run_all_tests()       |  executor.c:364                 |
|  | kunit_filter_suites()       |  executor.c:165                 |
|  | kunit_exec_run_tests()      |  executor.c:278                 |
|  +-------------+---------------+                                 |
|                |                                                 |
|                v  for each suite                                 |
|  +-----------------------------+                                 |
|  | kunit_run_tests(suite)      |  test.c:789                     |
|  |   add_taint(TAINT_TEST)     |  test.c:796                     |
|  |   suite->suite_init()       |                                 |
|  |   for each test_case:       |                                 |
|  |     kunit_run_one_test()    |  test.c:753                     |
|  +-------------+---------------+                                 |
|                |                                                 |
|                v                                                 |
|  +-----------------------------+                                 |
|  | kunit_run_case_catch_errors |  test.c:568                     |
|  |   kunit_try_catch_run()     |  try-catch.c:37                 |
|  |     kthread_create(...)     |  [子线程执行测试体]              |
|  |     wait_for_completion()   |  [父线程等待/超时]               |
|  +-----------------------------+                                 |
|                |                                                 |
|  [子线程内部]  v                                                 |
|  +-----------------------------+                                 |
|  | suite->init(test)           |                                 |
|  | test_case->run_case(test)   |  [用户编写的测试函数]            |
|  |   KUNIT_EXPECT_EQ(...)      |                                 |
|  |   KUNIT_ASSERT_NOT_NULL(...)| <-- ASSERT 失败时:               |
|  |                             |     kunit_try_catch_throw()     |
|  |                             |     kthread_exit()              |
|  +-----------------------------+                                 |
|                                                                  |
|  结果收集                                                        |
|  string_stream (per-test log) --> debugfs results                |
|  printk (KTAP 格式)           --> dmesg / kunit.py 解析          |
+------------------------------------------------------------------+

4. 核心数据结构

4.1 struct kunit — 测试实例上下文

定义于 include/kunit/test.h:325-357

struct kunit {
    void *priv;                          // 用户自定义数据(init() 中赋值)
    struct kunit *parent;                // 参数化测试父上下文
    struct kunit_params params_array;    // 参数数组元数据

    /* private: 以下字段框架内部使用 */
    const char *name;
    struct string_stream *log;           // per-test 日志(debugfs 使用)
    struct kunit_try_catch try_catch;    // try-catch 执行上下文
    const void *param_value;            // 当前参数值
    int param_index;
    spinlock_t lock;                     // 保护 resources 链表
    enum kunit_status status;            // SUCCESS / FAILURE / SKIPPED
    struct list_head resources;         // RAII 资源链表
    char status_comment[KUNIT_STATUS_COMMENT_SIZE];  // skip/fail 原因
    struct kunit_loc last_seen;          // 最后执行到的源码位置(崩溃调试用)
};

last_seen 在每次断言宏调用时由 _KUNIT_SAVE_LOC() 更新 (include/kunit/test.h:708-711),即使发生 panic 也能定位到最后一行测试代码。

4.2 struct kunit_case — 单个测试用例

定义于 include/kunit/test.h:128-141

struct kunit_case {
    void (*run_case)(struct kunit *test); // 测试函数指针(固定签名)
    const char *name;                     // 由宏字符串化,与函数名一致
    const void* (*generate_params)(       // 参数生成器(参数化测试)
        struct kunit *test, const void *prev, char *desc);
    struct kunit_attributes attr;         // 速度等属性
    int (*param_init)(struct kunit *test);
    void (*param_exit)(struct kunit *test);

    /* private */
    enum kunit_status status;
    char *module_name;
    struct string_stream *log;
};

遍历宏(include/kunit/test.h:467-468)利用哨兵元素(run_case == NULL)终止:

#define kunit_suite_for_each_test_case(suite, test_case)        \
    for (test_case = suite->test_cases; test_case->run_case; test_case++)

4.3 struct kunit_suite — 测试套件

定义于 include/kunit/test.h:273-288,套件的生命周期调用顺序为:

suite_init(suite)                   // 整个套件执行一次
    init(test)                      // 每个用例前执行
    test_cases[0](test)
    exit(test)                      // 每个用例后执行
    init(test)
    test_cases[1](test)
    exit(test)
    ...
suite_exit(suite)                   // 整个套件执行一次

注意:exitsuite_exitinit/suite_init 失败时仍然会被调用, 需要能够处理不完整的初始化状态(include/kunit/test.h:267-269)。


5. 宏展开分析:KUNIT_CASE 与 kunit_test_suite

5.1 KUNIT_CASE 宏

定义于 include/kunit/test.h:164-166

#define KUNIT_CASE(test_name)           \
    { .run_case = test_name, .name = #test_name,    \
      .module_name = KBUILD_MODNAME}

三个关键细节:

  • #test_name:C 预处理器字符串化操作,将函数名直接转为字符串,无需手动维护 名称映射表,函数重命名时名称字段自动同步。
  • .module_name = KBUILD_MODNAME:由构建系统注入,标识测试所属模块,用于 模块级过滤。
  • 结构体初始化列表末尾的 {} 哨兵元素(run_caseNULL)标志数组结束。

变体宏对照(include/kunit/test.h:176-249):

// 带属性(速度等)
#define KUNIT_CASE_ATTR(test_name, attributes) { ... .attr = attributes ... }

// 直接标记为慢速
#define KUNIT_CASE_SLOW(test_name) { ... .attr.speed = KUNIT_SPEED_SLOW ... }

// 参数化测试
#define KUNIT_CASE_PARAM(test_name, gen_params) { ... .generate_params = gen_params ... }

// 参数化 + 自定义 init/exit
#define KUNIT_CASE_PARAM_WITH_INIT(test_name, gen_params, init, exit) { ... }

5.2 kunit_test_suite 宏及 ELF section 注册

完整展开链(include/kunit/test.h:410-433):

// 用户使用:
kunit_test_suite(my_suite);

// 展开为:
kunit_test_suites(&my_suite);

// 继续展开:
__kunit_test_suites(__UNIQUE_ID(array), &my_suite);

// 最终展开:
static struct kunit_suite *__UNIQUE_ID(array)[]
    __aligned(sizeof(struct kunit_suite *))
    __used __section(".kunit_test_suites") = { &my_suite };

四个编译器属性的作用:

属性 作用
__aligned(sizeof(struct kunit_suite *)) 保证指针对齐,跨架构安全
__used 防止 GCC/Clang 因"未使用"而将其优化删除
__section(".kunit_test_suites") 放入专用 ELF section,自动注册
__UNIQUE_ID(array) 生成全局唯一变量名,避免多文件命名冲突

链接脚本(include/asm-generic/vmlinux.lds.h)将该 section 的首尾导出为符号, executor 通过这两个符号无需注册表即可发现所有测试(executor.c:13-16):

extern struct kunit_suite * const __kunit_suites_start[];
extern struct kunit_suite * const __kunit_suites_end[];
extern struct kunit_suite * const __kunit_init_suites_start[];
extern struct kunit_suite * const __kunit_init_suites_end[];

这与内核 initcall 机制(__section(".initcall1.init"))原理完全相同。


6. 测试执行器(Executor)与结果收集

6.1 执行入口:两条路径

内建模式CONFIG_KUNIT=y):

kunit_run_all_tests()executor.c:364 中定义,于内核启动的 late_initcall 阶段被调用(lib/kunit/test.c:1068):

late_initcall(kunit_init);
// kunit_init() 在 test.c:1058 注册模块通知器并调用 kunit_run_all_tests()

模块模式CONFIG_KUNIT=m):

KUnit 在 test.c:1058-1067 注册模块通知器,监听 MODULE_STATE_LIVE 事件:

static int __init kunit_init(void)
{
    kunit_install_hooks();
    kunit_debugfs_init();
    kunit_bus_init();
    return register_module_notifier(&kunit_mod_nb);
}
late_initcall(kunit_init);

当带 KUnit 测试的模块加载时,kunit_module_init()test.c:889)被触发: 合并 init/normal 套件集 -> 应用过滤 -> 调用 kunit_exec_run_tests()

6.2 过滤系统

executor.c 中的过滤支持两个维度,可通过内核命令行参数配置:

Glob 过滤executor.c:87-112):

kunit.filter_glob=example*          # 只运行名称以 example 开头的套件
kunit.filter_glob=example.test_add* # 只运行 example 套件中的特定用例

过滤逻辑将 "suite.test" 格式字符串用 strchr(filter_glob, '.') 分割为 套件 glob 和用例 glob,分别用 glob_match() 进行通配符匹配。

属性过滤executor.c:194-206):

kunit.filter=speed>slow   # 只运行速度属性快于 slow 的测试
kunit.filter_action=skip  # 被过滤的测试标记为 SKIP 而非直接跳过

6.3 核心执行函数 kunit_run_tests()

lib/kunit/test.c:789-821 是整个执行流程的枢纽:

int kunit_run_tests(struct kunit_suite *suite)
{
    struct kunit_case *test_case;
    struct kunit_result_stats suite_stats = { 0 };
    struct kunit_result_stats total_stats = { 0 };

    // 内核污染标记:标识此内核已运行过测试,影响 Oops 报告
    add_taint(TAINT_TEST, LOCKDEP_STILL_OK);   // test.c:796

    if (suite->suite_init) {
        suite->suite_init_err = suite->suite_init(suite);
        if (suite->suite_init_err) {
            kunit_err(suite, "# failed to initialize (%d)",
                      suite->suite_init_err);
            goto suite_end;
        }
    }

    kunit_print_suite_start(suite);    // 打印 KTAP 套件头(test.c:157)

    kunit_suite_for_each_test_case(suite, test_case)
        kunit_run_one_test(suite, test_case, &suite_stats, &total_stats);

    if (suite->suite_exit)
        suite->suite_exit(suite);

    kunit_print_suite_stats(suite, &suite_stats, &total_stats);
suite_end:
    kunit_print_suite_end(suite);
    return 0;
}

6.4 结果统计收集

struct kunit_result_statstest.c:90-95)在套件维度和全局维度分别追踪:

struct kunit_result_stats {
    unsigned long passed;
    unsigned long skipped;
    unsigned long failed;
    unsigned long total;
};

kunit_update_stats()test.c:626-642)在每个用例结束时根据状态更新计数器, 最终通过 kunit_print_suite_stats()test.c:602-624)按配置输出统计行:

# example: pass:10 fail:0 skip:1 total:11

统计输出模式由 kunit_stats_enabled 参数控制(test.c:85-88):

  • 0:禁用
  • 1(默认):仅当子测试数量 > 1 时输出
  • 2:总是输出

7. try-catch 机制:测试隔离的底层原理

7.1 设计动机

内核测试面临一个独特挑战:若测试触发 NULL 指针解引用,整个内核将 panic。KUnit 通过将每个测试用例在独立的内核线程(kthread)中执行,并结合超时机制,在不崩溃 内核的前提下隔离测试失败。

7.2 kunit_try_catch_run() 实现

lib/kunit/try-catch.c:37-91 是 try-catch 的核心实现:

void kunit_try_catch_run(struct kunit_try_catch *try_catch, void *context)
{
    struct kunit *test = try_catch->test;
    struct task_struct *task_struct;
    struct completion *task_done;
    int exit_code, time_remaining;

    try_catch->context = context;
    try_catch->try_result = 0;

    // 创建内核线程执行测试体
    task_struct = kthread_create(kunit_generic_run_threadfn_adapter,
                                 try_catch, "kunit_try_catch_thread");
    if (IS_ERR(task_struct)) {
        try_catch->try_result = PTR_ERR(task_struct);
        try_catch->catch(try_catch->context);
        return;
    }

    get_task_struct(task_struct);
    // 借用 task_struct->vfork_done (-> kthread->exited) 等待线程退出
    task_done = task_struct->vfork_done;
    wake_up_process(task_struct);

    // 阻塞等待,带超时
    time_remaining = wait_for_completion_timeout(
        task_done, try_catch->timeout);
    if (time_remaining == 0) {
        try_catch->try_result = -ETIMEDOUT;
        kthread_stop(task_struct);
    }

    put_task_struct(task_struct);
    exit_code = try_catch->try_result;
    if (!exit_code) return;

    // 超时或内部错误时报告,然后调用 catch 函数
    if (exit_code == -ETIMEDOUT)
        kunit_err(test, "try timed out\n");
    else if (exit_code == -EINTR) {
        if (test->last_seen.file)
            kunit_err(test, "try faulted: last line seen %s:%d\n",
                      test->last_seen.file, test->last_seen.line);
    }
    try_catch->catch(try_catch->context);
}

借用 vfork_done 指针是一个内核编程技巧:kthread 退出时会触发该 completion,父线程通过 wait_for_completion_timeout() 感知到线程结束, 类似于用户空间的 waitpid(),但无需额外分配 completion 对象。

7.3 ASSERT 失败的传播路径

KUNIT_ASSERT_* 失败时,调用链如下(test.c:306-318try-catch.c:18-22):

__kunit_do_failed_assertion()   # 记录失败位置和信息
    kunit_fail()                # 设置 test->status = KUNIT_FAILURE
    __kunit_abort(test)         # 仅 ASSERTION 类型才调用
        kunit_try_catch_throw(&test->try_catch)
            try_catch->try_result = -EFAULT
            kthread_exit(0)     # 子线程退出,触发 task_done completion

父线程检测到 try_result == -EFAULT,将其静默处理(try-catch.c:76-77):

if (exit_code == -EFAULT)
    try_catch->try_result = 0;  // ASSERT abort 是预期行为,不视为错误

然后继续执行清理阶段(第二次 kunit_try_catch_run()),调用 suite->exit()kunit_cleanup()

7.4 完整执行序列

kunit_run_case_catch_errors()           test.c:568
    |
    +-- [第一阶段:执行测试体]
    |   kunit_try_catch_init(try, catch, timeout)    test.c:577-581
    |   kunit_try_catch_run(&ctx)
    |       kthread_create("kunit_try_catch_thread")
    |       wait_for_completion_timeout(task_done, timeout)
    |           [子线程] kunit_try_run_case()        test.c:479
    |               current->kunit_test = test       # 绑定到 task_struct
    |               suite->init(test)
    |               test_case->run_case(test)
    |                   [ASSERT 失败] kthread_exit() # 提前终止
    |
    +-- [第二阶段:清理]
        kunit_try_catch_init(cleanup_try, cleanup_catch, timeout)
        kunit_try_catch_run(&ctx)
            [子线程] kunit_try_run_case_cleanup()    test.c:496
                current->kunit_test = test
                suite->exit(test)
                kunit_cleanup(test)                  # 释放所有资源

超时值由 kunit_test_timeout()test.c:409-423)计算,默认 300 秒:

static unsigned long kunit_test_timeout(struct kunit_suite *suite,
                                        struct kunit_case *test_case)
{
    int mult = 1;
    if (suite->attr.speed != KUNIT_SPEED_UNSET)
        mult = kunit_timeout_mult(suite->attr.speed);
    if (test_case->attr.speed != KUNIT_SPEED_UNSET)
        mult = kunit_timeout_mult(test_case->attr.speed);
    // kunit_timeout_mult(): SLOW=3, VERY_SLOW=12, 其余=1
    return mult * kunit_base_timeout * msecs_to_jiffies(MSEC_PER_SEC);
}

8. 断言宏体系的完整实现

8.1 两类断言的语义差异

KUnit 将断言分为两类,通过 assert_type 参数在同一套宏体系中统一实现:

类别 宏前缀 失败行为 使用场景
Expectation KUNIT_EXPECT_* 记录失败,继续执行 收集多个失败信息
Assertion KUNIT_ASSERT_* 记录失败,立即中止 前提条件检查

选择原则:若某条件失败后继续执行无意义(如 ptr == NULL 后必然崩溃), 用 ASSERT;否则用 EXPECT,尽量在一次运行中收集所有失败。

8.2 宏展开的分层架构

KUNIT_EXPECT_EQ(test, left, right) 为例,完整展开路径 (include/kunit/test.h:1032-1040):

KUNIT_EXPECT_EQ(test, left, right)
    -> KUNIT_EXPECT_EQ_MSG(test, left, right, NULL)
        -> KUNIT_BINARY_INT_ASSERTION(test, KUNIT_EXPECTATION,
                                      left, ==, right, NULL)
            -> KUNIT_BASE_BINARY_ASSERTION(test,
                   kunit_binary_assert,          // assert 结构体类型
                   kunit_binary_assert_format,   // 格式化函数
                   KUNIT_EXPECTATION,
                   left, ==, right, NULL)

KUNIT_BASE_BINARY_ASSERTIONtest.h:829-860)的核心代码:

do {
    const typeof(left) __left = (left);     // 只求值一次,避免副作用
    const typeof(right) __right = (right);
    static const struct kunit_binary_assert_text __text = {
        .operation = #op,          // 操作符字符串化:"=="
        .left_text = #left,        // 左表达式字符串化
        .right_text = #right,      // 右表达式字符串化
    };

    _KUNIT_SAVE_LOC(test);         // 更新 test->last_seen

    if (likely(__left op __right)) // 快速路径:条件满足
        break;

    _KUNIT_FAILED(test, assert_type, kunit_binary_assert,
                  kunit_binary_assert_format,
                  KUNIT_INIT_ASSERT(.text = &__text,
                                    .left_value = __left,
                                    .right_value = __right),
                  fmt, ##__VA_ARGS__);
} while (0)

_KUNIT_FAILEDtest.h:732-744)是触发失败的终点:

#define _KUNIT_FAILED(test, assert_type, assert_class, assert_format,
                      INITIALIZER, fmt, ...) do {
    static const struct kunit_loc __loc = KUNIT_CURRENT_LOC; // 静态,节省栈
    const struct assert_class __assertion = INITIALIZER;
    __kunit_do_failed_assertion(test, &__loc, assert_type,
                                &__assertion.assert,
                                assert_format, fmt, ##__VA_ARGS__);
    if (assert_type == KUNIT_ASSERTION)
        __kunit_abort(test);   // EXPECT 不中止,ASSERT 中止
} while (0)

8.3 完整断言宏矩阵

分类 EXPECT 宏 ASSERT 宏 底层结构体
布尔 KUNIT_EXPECT_TRUE/FALSE KUNIT_ASSERT_TRUE/FALSE kunit_unary_assert
整数 KUNIT_EXPECT_EQ/NE/LT/LE/GT/GE KUNIT_ASSERT_EQ/NE/... kunit_binary_assert
指针 KUNIT_EXPECT_PTR_EQ/NE KUNIT_ASSERT_PTR_EQ/NE kunit_binary_ptr_assert
指针 KUNIT_EXPECT_NULL/NOT_NULL KUNIT_ASSERT_NULL/NOT_NULL kunit_binary_ptr_assert
指针 KUNIT_EXPECT_NOT_ERR_OR_NULL KUNIT_ASSERT_NOT_ERR_OR_NULL kunit_ptr_not_err_assert
字符串 KUNIT_EXPECT_STREQ/STRNEQ KUNIT_ASSERT_STREQ/STRNEQ kunit_binary_str_assert
内存块 KUNIT_EXPECT_MEMEQ/MEMNEQ KUNIT_ASSERT_MEMEQ/MEMNEQ kunit_mem_assert
无条件 KUNIT_FAIL KUNIT_FAIL_AND_ABORT kunit_fail_assert

每个宏均有 _MSG 后缀变体,附加格式化消息(如 KUNIT_EXPECT_EQ_MSG)。

8.4 失败信息格式化实现

lib/kunit/assert.c:116-142 中的 kunit_binary_assert_format()

void kunit_binary_assert_format(const struct kunit_assert *assert,
                                const struct va_format *message,
                                struct string_stream *stream)
{
    const struct kunit_binary_assert *binary_assert =
        container_of(assert, struct kunit_binary_assert, assert);

    // 输出:Expected left == right, but
    string_stream_add(stream,
        KUNIT_SUBTEST_INDENT "Expected %s %s %s, but\n",
        binary_assert->text->left_text,
        binary_assert->text->operation,
        binary_assert->text->right_text);

    // 若左侧非字面量,展示实际值(十进制 + 十六进制)
    if (!is_literal(binary_assert->text->left_text, binary_assert->left_value))
        string_stream_add(stream,
            KUNIT_SUBSUBTEST_INDENT "%s == %lld (0x%llx)\n",
            binary_assert->text->left_text,
            binary_assert->left_value, binary_assert->left_value);

    if (!is_literal(binary_assert->text->right_text, binary_assert->right_value))
        string_stream_add(stream,
            KUNIT_SUBSUBTEST_INDENT "%s == %lld (0x%llx)",
            binary_assert->text->right_text,
            binary_assert->right_value, binary_assert->right_value);

    kunit_assert_print_msg(message, stream);
}

is_literal()assert.c:94-114)检查表达式文本本身是否就是字面量,避免 重复打印(如 KUNIT_EXPECT_EQ(test, result, 5) 失败时,5 不再重复输出)。

内存块比较失败时(KUNIT_EXPECT_MEMEQ),kunit_assert_hexdump()assert.c:214-233)生成带差异标注的十六进制转储,不同字节用 <> 标记:

        expected ==
            <0f>  ff  00  00
        actual ==
             0f   ff  00  00

8.5 失败处理的完整调用链

KUNIT_EXPECT_EQ 失败
    _KUNIT_FAILED()
        __kunit_do_failed_assertion()           test.c:320-338
            kunit_fail()                        test.c:281-304
                kunit_set_failure(test)         设置 status = KUNIT_FAILURE
                kunit_alloc_string_stream()     分配日志缓冲
                kunit_assert_prologue()         打印 "EXPECTATION FAILED at file:line"
                assert_format()                 打印具体失败描述
                kunit_print_string_stream()     输出到 printk + log 缓冲
        // assert_type == KUNIT_EXPECTATION,不调用 __kunit_abort
        // 函数返回,测试继续执行

9. kunit_resource 资源管理(RAII 模式)

9.1 设计理念

KUnit 资源管理借鉴 C++ RAII 和 Linux devres(devm_* 系列函数)的思想。 所有测试期间分配的资源以链表形式挂载到 test->resources,测试结束时框架按 逆序(后注册先释放,类似栈)自动回收,无论测试如何终止。

9.2 核心结构 struct kunit_resource

定义于 include/kunit/resource.h:83-92

struct kunit_resource {
    void *data;                      // 资源数据指针
    const char *name;                // 可选名称(用于命名查找)
    kunit_resource_free_t free;      // 析构函数指针

    /* private */
    struct kref refcount;            // 引用计数(支持并发安全访问)
    struct list_head node;           // 挂入 test->resources 的链表节点
    bool should_kfree;               // 是否需要 kfree(res) 自身
};

引用计数机制(resource.h:100-138)允许测试代码通过 kunit_find_resource() 获得 资源引用(引用计数+1),用完后通过 kunit_put_resource() 释放(计数-1), 引用计数归零时触发 free 回调。

9.3 资源注册实现

lib/kunit/resource.c:19-45__kunit_add_resource()

int __kunit_add_resource(struct kunit *test,
                         kunit_resource_init_t init,
                         kunit_resource_free_t free,
                         struct kunit_resource *res,
                         void *data)
{
    int ret = 0;
    unsigned long flags;

    res->free = free;
    kref_init(&res->refcount);       // 初始引用计数 = 1

    if (init) {
        ret = init(res, data);       // 用户提供的初始化函数
        if (ret) return ret;
    } else {
        res->data = data;
    }

    spin_lock_irqsave(&test->lock, flags);
    list_add_tail(&res->node, &test->resources);  // 追加到链表尾部
    spin_unlock_irqrestore(&test->lock, flags);

    return ret;
}

9.4 逆序清理实现

lib/kunit/test.c:1023-1056 中的 kunit_cleanup()

void kunit_cleanup(struct kunit *test)
{
    struct kunit_resource *res;
    unsigned long flags;

    // 从链表尾部取出资源(LIFO:后注册先释放)
    while (true) {
        spin_lock_irqsave(&test->lock, flags);
        if (list_empty(&test->resources)) {
            spin_unlock_irqrestore(&test->lock, flags);
            break;
        }
        res = list_last_entry(&test->resources,
                              struct kunit_resource, node);
        // 关键:先解锁再释放,因为 free() 可能递归移除其他资源
        spin_unlock_irqrestore(&test->lock, flags);
        kunit_remove_resource(test, res);
    }
    current->kunit_test = NULL;
}

注释中明确了先解锁的原因(test.c:1047-1051):

Need to unlock here as a resource may remove another resource, and this can't happen if the test->lock is held.

9.5 deferred action 机制

lib/kunit/resource.c:81-113 实现了最轻量的清理注册方式:

struct kunit_action_ctx {
    struct kunit_resource res;   // 内嵌 resource,无需单独分配
    kunit_action_t *func;
    void *ctx;
};

int kunit_add_action(struct kunit *test, void (*action)(void *), void *ctx)
{
    struct kunit_action_ctx *action_ctx = kzalloc_obj(*action_ctx);
    if (!action_ctx) return -ENOMEM;

    action_ctx->func = action;
    action_ctx->ctx = ctx;
    action_ctx->res.should_kfree = true;
    // 以 __kunit_action_free 作为 resource 的 free 函数
    // 当资源被释放时,__kunit_action_free 调用 action_ctx->func(action_ctx->ctx)
    __kunit_add_resource(test, NULL, __kunit_action_free,
                         &action_ctx->res, action_ctx);
    return 0;
}

避免 CFI 问题KUNIT_DEFINE_ACTION_WRAPPER 宏(resource.h:406-411) 包装函数,防止直接 cast 函数指针破坏控制流完整性(CFI)。test.c:977 中的 用例:

KUNIT_DEFINE_ACTION_WRAPPER(kfree_action_wrapper, kfree, const void *)
// 展开为 static void kfree_action_wrapper(void *in) { kfree((const void *)in); }

9.6 测试托管分配函数

include/kunit/test.h:486-536test.c:979-1021 提供标准内存分配的托管版本:

// kunit_kmalloc_array 实现(test.c:979-993):
void *kunit_kmalloc_array(struct kunit *test, size_t n, size_t size, gfp_t gfp)
{
    void *data = kmalloc_array(n, size, gfp);
    if (!data) return NULL;
    // 注册 kfree 动作,测试结束时自动调用 kfree(data)
    if (kunit_add_action_or_reset(test, kfree_action_wrapper, data) != 0)
        return NULL;
    return data;
}

kunit_kfree()test.c:995-1001)可提前释放,底层通过 kunit_release_action() 立即执行并移除对应的 deferred action。


10. TAP / KTAP 输出格式

10.1 KTAP 协议

KUnit 使用 KTAP(Kernel TAP),是 TAP 13 的超集,原生支持多级嵌套子测试。 缩进规则定义于 include/kunit/test.h:50-52

#define KUNIT_INDENT_LEN        4
#define KUNIT_SUBTEST_INDENT    "    "       // 4 空格(套件级)
#define KUNIT_SUBSUBTEST_INDENT "        "   // 8 空格(参数化测试级)

10.2 完整输出示例

KTAP version 1                           <- executor.c:284: pr_info("KTAP version 1\n")
1..2                                     <- 共 2 个套件

    KTAP version 1                       <- test.c:167: 套件头
    # Subtest: example
    1..11
        ok 1 example_simple_test
        ok 2 example_skip_test # SKIP this test should be skipped
        ok 3 example_mark_skipped_test # SKIP this test should be skipped
        ok 4 example_all_expect_macros_test
        ok 5 example_static_stub_test
        ok 6 example_static_stub_using_fn_ptr_test
        ok 7 example_priv_test
        KTAP version 1                   <- 参数化测试子层
        # Subtest: example_params_test
        1..4
            ok 1 example value 3
            ok 2 example value 2
            ok 3 example value 1 # SKIP unsupported param value 1
            ok 4 example value 0 # SKIP unsupported param value 0
        ok 8 example_params_test
        ok 9 example_params_test_with_init
        ok 10 example_params_test_with_init_dynamic_arr
        ok 11 example_slow_test
    # example: pass:9 fail:0 skip:2 total:11
ok 1 example                             <- test.c:199: 套件最终结果

    KTAP version 1
    # Subtest: example_init
    1..1
        ok 1 example_init_test
ok 2 example_init

10.3 输出函数实现

套件开始(test.c:157-173):

static void kunit_print_suite_start(struct kunit_suite *suite)
{
    pr_info(KUNIT_SUBTEST_INDENT "KTAP version 1\n");
    pr_info(KUNIT_SUBTEST_INDENT "# Subtest: %s\n", suite->name);
    kunit_print_attr((void *)suite, false, KUNIT_LEVEL_CASE);
    pr_info(KUNIT_SUBTEST_INDENT "1..%zd\n",
            kunit_suite_num_test_cases(suite));
}

通用 ok/not ok 行(test.c:175-210):

static void kunit_print_ok_not_ok(struct kunit *test,
                                  unsigned int test_level,
                                  enum kunit_status status,
                                  size_t test_number,
                                  const char *description,
                                  const char *directive)
{
    const char *directive_header = (status == KUNIT_SKIPPED) ? " # SKIP " : "";
    // test_level 控制缩进深度,LEVEL_SUITE=0,LEVEL_CASE=1,LEVEL_CASE_PARAM=2
    if (!test)
        pr_info("%s %zd %s%s%s\n",
                kunit_status_to_ok_not_ok(status),
                test_number, description, ...);
    else
        kunit_log(KERN_INFO, test,
                  "%*s%s %zd %s%s%s",
                  KUNIT_INDENT_LEN * test_level, "",  // 动态计算缩进
                  kunit_status_to_ok_not_ok(status),
                  test_number, description, ...);
}

kunit_log() 宏(test.h:659-664)同时写入 printktest->log 缓冲:

#define kunit_log(lvl, test_or_suite, fmt, ...)             \
    do {                                                    \
        printk(lvl fmt, ##__VA_ARGS__);                     \
        kunit_log_append((test_or_suite)->log, fmt,         \
                         ##__VA_ARGS__);                    \
    } while (0)

11. 在模块中编写 KUnit 测试

11.1 测试文件的标准结构

lib/kunit/kunit-example-test.c 为参考模板:

#include <kunit/test.h>
#include <kunit/static_stub.h>  // 若需要静态桩

/* ===== 被测函数(或引用被测模块头文件)===== */

/* ===== 测试函数:固定签名 void fn(struct kunit *) ===== */
static void example_simple_test(struct kunit *test)
{
    KUNIT_EXPECT_EQ(test, 1 + 1, 2);          // 期望:失败后继续
    KUNIT_ASSERT_NOT_NULL(test, some_ptr);     // 断言:失败后中止
}

/* ===== init/exit:每个用例前后调用 ===== */
static int example_test_init(struct kunit *test)
{
    // kunit_kzalloc 分配的内存自动释放
    test->priv = kunit_kzalloc(test, sizeof(struct my_ctx), GFP_KERNEL);
    return test->priv ? 0 : -ENOMEM;
}

static void example_test_exit(struct kunit *test)
{
    // kunit_kzalloc 的内存已由框架自动释放,这里做其他清理
}

/* ===== suite_init/suite_exit:整个套件前后各调用一次 ===== */
static int example_test_init_suite(struct kunit_suite *suite)
{
    kunit_info(suite, "initializing suite\n");
    return 0;
}

static void example_test_exit_suite(struct kunit_suite *suite)
{
    kunit_info(suite, "exiting suite\n");
}

/* ===== 用例数组(以 {} 空项结尾)===== */
static struct kunit_case example_test_cases[] = {
    KUNIT_CASE(example_simple_test),
    KUNIT_CASE_SLOW(example_slow_test),
    KUNIT_CASE_PARAM(example_params_test, example_gen_params),
    {}
};

/* ===== 套件定义 ===== */
static struct kunit_suite example_test_suite = {
    .name        = "example",
    .suite_init  = example_test_init_suite,
    .suite_exit  = example_test_exit_suite,
    .init        = example_test_init,
    .exit        = example_test_exit,
    .test_cases  = example_test_cases,
};

/* ===== 注册到 .kunit_test_suites section ===== */
kunit_test_suites(&example_test_suite);   // kunit-example-test.c:555

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("Example KUnit test suite");

11.2 Kconfig 与 Makefile 集成模式

Kconfig

config MY_SUBSYSTEM_KUNIT_TEST
    tristate "KUnit tests for my_subsystem" if !KUNIT_ALL_TESTS
    depends on MY_SUBSYSTEM && KUNIT
    default KUNIT_ALL_TESTS
    help
      KUnit unit tests for my_subsystem internal functions.
      Say M to build as a loadable module.

Makefile

obj-$(CONFIG_MY_SUBSYSTEM_KUNIT_TEST) += my_subsystem_test.o

11.3 访问模块内部静态函数

若要测试 static 函数,在被测模块中用条件宏导出:

// 被测模块 my_module.c
static int my_internal_helper(int x)
{
    return x * 2;
}
// 仅在 CONFIG_KUNIT 时导出
EXPORT_SYMBOL_IF_KUNIT(my_internal_helper);

或使用 VISIBLE_IF_KUNIT 修改符号可见性(include/kunit/visibility.h):

VISIBLE_IF_KUNIT int my_internal_func(int x) { ... }
EXPORT_SYMBOL_IF_KUNIT(my_internal_func);

这两个宏在非 KUnit 构建下展开为空,不影响生产代码的符号导出范围。

11.4 init section 测试

对含 __init 函数的测试,使用 kunit_test_init_section_suites()test.h:460-462):

static void __init my_init_func_test(struct kunit *test)
{
    KUNIT_EXPECT_EQ(test, init_add(1, 1), 2);  // kunit-example-test.c:566-568
}

static struct kunit_case my_init_cases[] = {
    KUNIT_CASE(my_init_func_test),
    {}
};

static struct kunit_suite my_init_suite = {
    .name = "my_init_suite",
    .test_cases = my_init_cases,
};

kunit_test_init_section_suites(&my_init_suite);

这类测试只在内核初始化阶段运行一次,不支持 debugfs 重新触发(test.h:454-455):

Note: these init tests are not able to be run after boot so there is no "run" debugfs file generated for these tests.


12. 参数化测试

12.1 设计思路

参数化测试(parameterized test)让同一测试函数以多组输入运行,每组参数产生独立的 KTAP 结果行,兼顾代码复用与清晰的失败定位。

12.2 KUNIT_ARRAY_PARAM 宏

include/kunit/test.h:1729-1743 展开为一个参数生成器函数:

#define KUNIT_ARRAY_PARAM(name, array, get_desc)
    static const void *name##_gen_params(struct kunit *test,
                                          const void *prev, char *desc)
    {
        typeof((array)[0]) *__next =
            prev ? ((typeof(__next)) prev) + 1 : (array);
        if (!prev)
            kunit_register_params_array(test, array,
                                        ARRAY_SIZE(array), NULL);
        if (__next - (array) < ARRAY_SIZE((array))) {
            void (*__get_desc)(typeof(__next), char *) = get_desc;
            if (__get_desc) __get_desc(__next, desc);
            return __next;
        }
        return NULL;   // 返回 NULL 表示参数已用尽
    }

kunit-example-test.c:224-238 中的完整使用示例:

static const struct example_param {
    int value;
} example_params_array[] = {
    { .value = 3 }, { .value = 2 }, { .value = 1 }, { .value = 0 },
};

static void example_param_get_desc(const struct example_param *p, char *desc)
{
    snprintf(desc, KUNIT_PARAM_DESC_SIZE, "example value %d", p->value);
}

KUNIT_ARRAY_PARAM(example, example_params_array, example_param_get_desc);

static void example_params_test(struct kunit *test)
{
    const struct example_param *param = test->param_value;
    KUNIT_ASSERT_NOT_NULL(test, param);
    if (!is_power_of_2(param->value))
        kunit_skip(test, "unsupported param value %d", param->value);
    KUNIT_EXPECT_EQ(test, param->value % param->value, 0);
}

12.3 参数化执行流程

test.c:684-751 中的 kunit_run_param_test() 为每个参数值创建独立的 struct kunit 实例:

while (curr_param) {
    struct kunit param_test = {
        .param_value = curr_param,
        .param_index = ++test->param_index,
        .parent = test,          // parent 用于访问参数化测试级别的共享资源
    };
    kunit_init_test(&param_test, test_case->name, NULL);
    param_test.log = test_case->log;
    kunit_run_case_catch_errors(suite, test_case, &param_test);
    // 打印该参数的 ok/not ok 行...
    curr_param = test_case->generate_params(test, curr_param, param_desc);
}

12.4 带 init/exit 的参数化测试

KUNIT_CASE_PARAM_WITH_INITtest.h:245-249)支持为整个参数化测试 (而非每次参数运行)定义初始化和清理函数,通过 test->parent 在参数间共享资源, 参见 kunit-example-test.c:332-388 的完整示例。


13. kselftest 框架详解与结构

13.1 框架概述

kselftest 位于 tools/testing/selftests/,测试程序以普通用户态进程运行, 通过系统调用、ioctl、sysfs 等接口验证内核行为,覆盖 200 多个子系统。

13.2 kselftest.h 核心 API

tools/testing/selftests/kselftest.h 提供最基础的 TAP 报告接口:

// 初始化(kselftest.h:139-152)
ksft_print_header();    // 打印 "TAP version 13"(受 KSFT_TAP_LEVEL 环境变量影响)
ksft_set_plan(N);       // 打印 "1..N"

// 结果报告
ksft_test_result_pass(fmt, ...);  // "ok N msg"
ksft_test_result_fail(fmt, ...);  // "not ok N msg"
ksft_test_result_skip(fmt, ...);  // "ok N # SKIP msg"
ksft_test_result_xfail(fmt, ...); // "ok N # XFAIL msg"(预期失败)
ksft_test_result_xpass(fmt, ...); // "ok N # XPASS msg"(意外通过)
ksft_test_result_error(fmt, ...); // "not ok N # error msg"

// 条件报告宏(kselftest.h:238-243)
ksft_test_result(condition, fmt, ...);  // 真则 pass,假则 fail

// 退出(打印统计后调用 exit())
ksft_exit_pass();           // exit(KSFT_PASS=0)
ksft_exit_fail();           // exit(KSFT_FAIL=1)
ksft_exit_fail_msg(fmt, ...); // 打印 "Bail out!" 后 exit(1)
ksft_finished();            // 所有计划测试通过则 pass,否则 fail

返回码(kselftest.h:85-89):

#define KSFT_PASS  0
#define KSFT_FAIL  1
#define KSFT_XFAIL 2
#define KSFT_XPASS 3
#define KSFT_SKIP  4

13.3 kselftest_harness.h 测试夹具

tools/testing/selftests/kselftest_harness.h 提供 Google Test 风格的 fixture(测试夹具)机制:

// 定义夹具数据结构
FIXTURE(my_fixture) {
    int fd;
    char *buf;
};

// 每个测试用例前执行
FIXTURE_SETUP(my_fixture) {
    self->fd = open("/dev/null", O_RDONLY);
    ASSERT_NE(-1, self->fd) {
        TH_LOG("open failed: %s", strerror(errno));
    }
    self->buf = malloc(1024);
    ASSERT_NE(NULL, self->buf);
}

// 每个测试用例后执行
FIXTURE_TEARDOWN(my_fixture) {
    close(self->fd);
    free(self->buf);
}

// 使用夹具的测试(可访问 self->)
TEST_F(my_fixture, read_returns_zero) {
    char buf[16];
    ssize_t n = read(self->fd, buf, sizeof(buf));
    EXPECT_EQ(0, n);
}

// 独立测试(无夹具)
TEST(standalone) {
    EXPECT_EQ(1 + 1, 2);
}

TEST_HARNESS_MAIN  // 生成 main() 函数,包含运行所有测试的逻辑

13.4 目录结构

tools/testing/selftests/
├── kselftest.h              # 基础 TAP API
├── kselftest_harness.h      # fixture 框架
├── kselftest_module.h       # 内核模块 kselftest 支持
├── run_kselftest.sh         # 批量运行脚本
├── Makefile                 # 顶层构建(TARGETS 变量列出子系统)
│
├── mm/                      # 内存管理(mmap、mremap、hugepage、userfaultfd)
├── net/                     # 网络(socket、netfilter、mptcp、tcp_ao)
├── bpf/                     # BPF(最大子集,含数百测试)
│   ├── prog_tests/          # 按功能分类的 BPF 测试
│   └── test_progs.c         # BPF 测试框架主文件
├── cgroup/                  # cgroup v1/v2
├── seccomp/                 # 安全计算过滤
├── futex/                   # futex 进程间同步
├── kvm/                     # KVM 虚拟化
├── arm64/                   # ARM64 架构(BTI、MTE、GCS、PAC)
│   ├── bti/
│   ├── mte/
│   ├── gcs/
│   └── signal/
├── ftrace/                  # ftrace 功能
├── lkdtm/                   # 内核故障注入
└── ...(200+ 子系统)

13.5 运行 kselftest

# 编译所有 kselftest
make -C tools/testing/selftests

# 只编译特定子系统
make -C tools/testing/selftests TARGETS=net

# 运行特定子系统
make -C tools/testing/selftests TARGETS="net mm" run_tests

# 安装后批量运行
make -C tools/testing/selftests install INSTALL_PATH=/tmp/ksft
/tmp/ksft/run_kselftest.sh

# 只运行特定测试(-t collection:test)
./tools/testing/selftests/run_kselftest.sh -t net:socket

14. 运行方式:kunit.py 工具链

14.1 工具链架构

tools/testing/kunit/kunit.py 封装了 KUnit 的完整运行流程,支持 UML 和 QEMU 后端。 主要 Python 模块(tools/testing/kunit/):

kunit.py          # 入口,命令行解析,协调各模块
kunit_kernel.py   # 内核构建和 UML/QEMU 执行
kunit_parser.py   # KTAP 输出解析,生成结构化结果
kunit_printer.py  # 彩色终端报告输出
kunit_json.py     # JSON 格式输出
kunit_config.py   # .kunitconfig 处理

kunit.py:28-68 中的数据类定义了请求参数结构:

@dataclass
class KunitExecRequest(KunitParseRequest):
    build_dir: str
    timeout: int
    filter_glob: str       # 套件/用例过滤 glob
    filter: str            # 属性过滤
    filter_action: Optional[str]
    kernel_args: Optional[List[str]]
    run_isolated: Optional[str]
    list_tests: bool
    list_tests_attr: bool

14.2 常用命令

# 运行所有 KUnit 测试(UML 模式)
./tools/testing/kunit/kunit.py run

# 按套件名过滤
./tools/testing/kunit/kunit.py run 'kunit*'

# 按套件.用例名过滤
./tools/testing/kunit/kunit.py run 'example.example_simple_test'

# 并行编译
./tools/testing/kunit/kunit.py run --jobs=8

# 查看原始 KTAP 输出
./tools/testing/kunit/kunit.py run --raw_output

# 仅显示失败测试
./tools/testing/kunit/kunit.py run --failed

# 输出 JSON 结果
./tools/testing/kunit/kunit.py run --json=/tmp/results.json

# 按属性过滤(只运行非慢速测试)
./tools/testing/kunit/kunit.py run --filter 'speed>=normal'

# 列出所有可用测试(不运行)
./tools/testing/kunit/kunit.py run --list_tests

# 在 ARM64 QEMU 上运行
./tools/testing/kunit/kunit.py run --arch=arm64 \
    --cross_compile=aarch64-linux-gnu-

# 逐个套件隔离运行(避免测试间状态污染)
./tools/testing/kunit/kunit.py run --run_isolated=suite

14.3 .kunitconfig 配置文件

.kunitconfig 是最小化的 Kconfig 片段,kunit.py 将其合并到 UML defconfig:

# 典型的 .kunitconfig
CONFIG_KUNIT=y
CONFIG_KUNIT_EXAMPLE_TEST=y

# 要测试的子系统
CONFIG_SLAB=y
CONFIG_SLAB_KUNIT_TEST=y

14.4 内核命令行参数

KUnit 行为可通过内核启动参数精细控制(executor.c:18-79 中的 module_param 定义):

kunit.enable=1             # 启用 KUnit(test.c:64-69)
kunit.timeout=300          # 基础超时(秒,可运行时修改)
kunit.stats_enabled=1      # 统计输出模式(0/1/2)
kunit.filter_glob=*.*      # glob 过滤
kunit.filter=speed>slow    # 属性过滤
kunit.filter_action=skip   # 过滤行为
kunit.action=list          # list/list_attr(只列出,不运行)
kunit_shutdown=poweroff    # 测试完成后关机(自动化场景)

15. debugfs 集成与在线重跑

CONFIG_KUNIT_DEBUGFS=y 时,lib/kunit/debugfs.c 在 debugfs 下创建:

/sys/kernel/debug/kunit/
├── <suite_name>/
│   ├── results    # 只读:上次运行的 KTAP 结果(seq_file 接口)
│   └── run        # 写入任意内容触发重新运行
└── ...

results 文件通过 seq_file 接口回放存储在 suite->log 和各 test_case->log 中的 string_stream 内容,与启动时的 printk 输出一致。

run 文件的写操作直接调用 __kunit_test_suites_init()debugfs.c),无需 重新加载模块。常用方式:

# 重新运行特定套件
echo 1 > /sys/kernel/debug/kunit/example/run

# 查看结果
cat /sys/kernel/debug/kunit/example/results

注意:通过 kunit_test_init_section_suites() 注册的 init section 测试不会生成 run 文件,因为 __init 代码已在启动后被释放(test.h:454-456 的注释说明)。


16. 实战示例:完整测试模块

以下是一个针对假想链表实现的完整 KUnit 测试模块,综合演示了资源管理、参数化 测试、断言使用和跳过机制:

// my_list_test.c
#include <kunit/test.h>
#include "my_list.h"

/* ===== 测试上下文(通过 test->priv 传递)===== */
struct list_test_ctx {
    struct my_list *list;
};

/* ===== 初始化:分配链表,注册析构 ===== */
KUNIT_DEFINE_ACTION_WRAPPER(destroy_list_wrapper,
                             my_list_destroy, struct my_list *);

static int list_test_init(struct kunit *test)
{
    struct list_test_ctx *ctx;

    ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
    if (!ctx)
        return -ENOMEM;

    ctx->list = my_list_create();
    if (!ctx->list)
        return -ENOMEM;

    /* 注册销毁动作:测试结束时自动调用 */
    if (kunit_add_action(test, destroy_list_wrapper, ctx->list))
        return -ENOMEM;

    test->priv = ctx;
    return 0;
}

/* ===== 基础测试 ===== */
static void test_list_empty_after_create(struct kunit *test)
{
    struct list_test_ctx *ctx = test->priv;

    /* ASSERT:ptr 为 NULL 则无意义继续 */
    KUNIT_ASSERT_NOT_NULL(test, ctx->list);
    KUNIT_EXPECT_EQ(test, my_list_size(ctx->list), 0);
    KUNIT_EXPECT_TRUE(test, my_list_empty(ctx->list));
}

static void test_list_push_then_pop(struct kunit *test)
{
    struct list_test_ctx *ctx = test->priv;
    int val = 42;
    int *result;

    my_list_push(ctx->list, &val);
    KUNIT_EXPECT_EQ(test, my_list_size(ctx->list), 1);

    result = my_list_pop(ctx->list);
    KUNIT_ASSERT_NOT_NULL(test, result);  /* 后续解引用的前提条件 */
    KUNIT_EXPECT_EQ(test, *result, 42);
    KUNIT_EXPECT_EQ(test, my_list_size(ctx->list), 0);
}

/* ===== 参数化测试:验证多种元素数量 ===== */
struct push_count_param {
    int count;
    const char *desc;
};

static const struct push_count_param push_count_params[] = {
    { .count = 1,   .desc = "single element"   },
    { .count = 10,  .desc = "ten elements"     },
    { .count = 100, .desc = "hundred elements" },
};

KUNIT_ARRAY_PARAM_DESC(push_count, push_count_params, desc);

static void test_list_size_matches_push_count(struct kunit *test)
{
    struct list_test_ctx *ctx = test->priv;
    const struct push_count_param *param = test->param_value;
    int i, dummy = 0;

    /* 跳过可能触发 OOM 的大规模测试(CI 内存受限时)*/
    if (param->count > 50 && !IS_ENABLED(CONFIG_KUNIT_LARGE_TESTS))
        kunit_skip(test, "large test skipped (count=%d)", param->count);

    for (i = 0; i < param->count; i++)
        my_list_push(ctx->list, &dummy);

    KUNIT_EXPECT_EQ(test, my_list_size(ctx->list), param->count);
}

/* ===== 用例数组 ===== */
static struct kunit_case list_test_cases[] = {
    KUNIT_CASE(test_list_empty_after_create),
    KUNIT_CASE(test_list_push_then_pop),
    KUNIT_CASE_PARAM(test_list_size_matches_push_count,
                     push_count_gen_params),
    {}
};

/* ===== 套件注册 ===== */
static struct kunit_suite list_test_suite = {
    .name       = "my_list",
    .init       = list_test_init,
    .test_cases = list_test_cases,
};

kunit_test_suite(list_test_suite);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("KUnit tests for my_list");

17. 常见问题与最佳实践

17.1 EXPECT 与 ASSERT 的选择

错误用法:对前提条件使用 EXPECT,导致 NULL 解引用:

// 错误:EXPECT 失败后继续执行,ptr 可能为 NULL
KUNIT_EXPECT_NOT_NULL(test, ptr);
KUNIT_EXPECT_EQ(test, ptr->value, 42);  // 若 ptr==NULL,此处崩溃

正确用法:前提条件用 ASSERT,后续验证用 EXPECT:

KUNIT_ASSERT_NOT_NULL(test, ptr);       // 前提失败则立即中止
KUNIT_EXPECT_EQ(test, ptr->value, 42);  // 安全访问
KUNIT_EXPECT_EQ(test, ptr->count, 1);  // 收集更多信息

17.2 init() 中不要使用断言宏

suite->init() 的失败通过返回错误码处理:

// 错误:ASSERT 在 init() 中行为未定义(try_catch 尚未设置)
static int bad_init(struct kunit *test)
{
    test->priv = kmalloc(...);
    KUNIT_ASSERT_NOT_NULL(test, test->priv);  // 不要这样做
    return 0;
}

// 正确:检查返回值,使用 kunit_kzalloc 自动释放
static int good_init(struct kunit *test)
{
    test->priv = kunit_kzalloc(test, sizeof(struct ctx), GFP_KERNEL);
    return test->priv ? 0 : -ENOMEM;
}

17.3 速度属性标注

超过 2 秒(2 × KUNIT_SPEED_SLOW_THRESHOLD_S)的测试会触发内核警告 (test.c:373-390):

kunit: test_xxx should be marked slow (runtime: 3.200000000s)

应在注册时标注速度属性:

KUNIT_CASE_SLOW(my_slow_test),              // 1s-3s
KUNIT_CASE_ATTR(my_very_slow_test,          // >3s
    (struct kunit_attributes){ .speed = KUNIT_SPEED_VERY_SLOW }),

17.4 全局状态的处理

KUnit 测试共享内核地址空间,修改全局状态(sysfs 属性、全局变量)可能导致 测试间相互影响。解决方案:

  • 使用 kunit_add_action() 注册恢复函数,确保 exit() 前全局状态复原。
  • 尽量通过 test->priv 传递状态,避免使用全局变量。

17.5 测试隔离与 kunit_test 指针

每个测试的 kthread 在 kunit_try_run_case()test.c:486)中设置:

current->kunit_test = test;

这使得框架内部(如 __kunit_fail_current_test_impl()test.c:33)可以通过 current->kunit_test 在任意内核代码中访问当前测试上下文,实现从被测代码外部 触发测试失败的机制(include/kunit/test-bug.h)。


18. Static Stub:函数重定向机制

18.1 设计目标

静态桩(static stub)是 KUnit 提供的轻量级函数替换机制,无需动态修改指令(如 ftrace 或 livepatch),在编译期在被替换函数头部插入跳转检查点,测试时在运行时 重定向到替换函数。定义于 include/kunit/static_stub.hlib/kunit/static_stub.c

18.2 KUNIT_STATIC_STUB_REDIRECT 宏

被替换函数在其函数体开头调用此宏(include/kunit/static_stub.h:59-72):

#define KUNIT_STATIC_STUB_REDIRECT(real_fn_name, args...)           \
do {                                                                \
    typeof(&real_fn_name) replacement;                              \
    struct kunit *current_test = kunit_get_current_test();          \
                                                                    \
    if (likely(!current_test))                                      \
        break;                         /* 非测试上下文:零开销直通 */ \
                                                                    \
    replacement = kunit_hooks.get_static_stub_address(current_test, \
                                                      &real_fn_name);\
                                                                    \
    if (unlikely(replacement))                                      \
        return replacement(args);      /* 跳转到替换函数 */          \
} while (0)

快速路径设计:

  • 通过 static_branch_unlikely(&kunit_running) 静态分支(test-bug.h:43-46), 非测试上下文下该检查被硬编码为 NOP,运行时指令开销几乎为零。
  • kunit_get_current_test() 在非 KUnit 上下文返回 NULLlikely(!current_test) 预测条件为真,直接 break 进入正常执行路径。

18.3 激活与注销

测试中使用 kunit_activate_static_stub()static_stub.h:95-98):

// 宏进行类型检查,确保 real_fn_addr 和 replacement_addr 类型一致
#define kunit_activate_static_stub(test, real_fn_addr, replacement_addr) do { \
    typecheck_fn(typeof(&replacement_addr), real_fn_addr);                    \
    __kunit_activate_static_stub(test, real_fn_addr, replacement_addr);       \
} while (0)

底层实现(lib/kunit/static_stub.c:87-122):

void __kunit_activate_static_stub(struct kunit *test,
                                   void *real_fn_addr,
                                   void *replacement_addr)
{
    struct kunit_static_stub_ctx *ctx;
    struct kunit_resource *res;

    // 查找已有的 stub(支持在测试中途替换 replacement)
    res = kunit_find_resource(test,
                              __kunit_static_stub_resource_match,
                              real_fn_addr);
    if (res) {
        ctx = res->data;
        ctx->replacement_addr = replacement_addr;
        kunit_put_resource(res);
    } else {
        ctx = kmalloc_obj(*ctx);
        ctx->real_fn_addr = real_fn_addr;
        ctx->replacement_addr = replacement_addr;
        // 注册为 kunit_resource,测试结束时自动清理
        kunit_alloc_resource(test, NULL,
                             &__kunit_static_stub_resource_free,
                             GFP_KERNEL, ctx);
    }
}

资源自动管理:stub 作为 kunit_resource 注册,测试结束时框架自动释放, 无需手动调用 kunit_deactivate_static_stub()(尽管可以提前注销)。

18.4 完整使用示例

参见 lib/kunit/kunit-example-test.c:154-222

// 被替换函数(必须包含 KUNIT_STATIC_STUB_REDIRECT)
static int add_one(int i)
{
    KUNIT_STATIC_STUB_REDIRECT(add_one, i);  // kunit-example-test.c:158
    return i + 1;
}

// 替换函数
static int subtract_one(int i)
{
    return i - 1;
}

static void example_static_stub_test(struct kunit *test)
{
    KUNIT_EXPECT_EQ(test, add_one(1), 2);             // 未激活,正常执行

    kunit_activate_static_stub(test, add_one, subtract_one);
    KUNIT_EXPECT_EQ(test, add_one(1), 0);             // 已激活,返回 1-1=0

    kunit_deactivate_static_stub(test, add_one);
    KUNIT_EXPECT_EQ(test, add_one(1), 2);             // 注销后,恢复正常
}

18.5 通过函数指针替换静态函数

对于无法直接引用地址的 static 函数,可导出函数指针 (kunit-example-test.c:179):

// 模块内部导出函数指针(模拟外部访问 static 函数的场景)
static int (* const add_one_fn_ptr)(int i) = add_one;

static void example_static_stub_using_fn_ptr_test(struct kunit *test)
{
    kunit_activate_static_stub(test, add_one_fn_ptr, subtract_one);
    KUNIT_EXPECT_EQ(test, add_one(1), 0);   // 通过函数指针激活也会生效
    kunit_deactivate_static_stub(test, add_one_fn_ptr);
}

18.6 与 ftrace/livepatch 的对比

特性 Static Stub ftrace hook livepatch
替换粒度 单个函数入口 单个函数入口 整个函数体
运行时指令修改 否(编译期插桩) 是(运行期 NOP->JMP)
测试上下文感知 是(per-test) 否(全局) 否(全局)
生产代码侵入 极小(一行宏)
适用范围 仅 KUnit 测试 内核调试/跟踪 热补丁
架构依赖 依赖 arch ftrace 依赖 arch livepatch

19. KUnit 托管设备:kunit_device

19.1 背景

测试驱动程序时,通常需要一个有效的 struct device 对象(如用于 DMA 映射、 devm 分配等)。KUnit 提供了托管设备接口,在专用的 kunit_bus 上注册虚拟设备, 测试结束后自动注销。

19.2 kunit_bus 注册流程

lib/kunit/device.ckunit_init() 阶段(lib/kunit/test.c 中的 kunit_bus_init() 调用)注册专用总线和根设备:

kunit_bus_init()                        device.c:44
    root_device_register("kunit")       -> /sys/devices/kunit
    bus_register(&kunit_bus_type)       -> /sys/bus/kunit
/sys/
├── devices/
│   └── kunit/                          # 根设备
│       ├── my_test.my_device/          # kunit_device (test.device_name)
│       └── ...
└── bus/
    └── kunit/                          # kunit 总线
        ├── devices/
        └── drivers/

19.3 API 使用

#include <kunit/device.h>

static void example_device_test(struct kunit *test)
{
    struct device *dev;

    // 创建托管设备(含自动创建的 driver)
    dev = kunit_device_register(test, "my_device");
    KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev);

    // 使用设备(如 DMA 映射)
    void *buf = dma_alloc_coherent(dev, 4096, &dma_addr, GFP_KERNEL);
    KUNIT_ASSERT_NOT_NULL(test, buf);

    // dev 和 driver 在测试结束时由框架自动注销,无需手动调用
}

static void example_device_with_driver_test(struct kunit *test)
{
    const struct device_driver *drv;
    struct device *dev;

    // 先创建 driver(可共享)
    drv = kunit_driver_create(test, "my_driver");
    KUNIT_ASSERT_NOT_ERR_OR_NULL(test, drv);

    // 使用已有 driver 创建设备
    dev = kunit_device_register_with_driver(test, "device0", drv);
    KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev);
}

19.4 内部实现分析

kunit_device_register() 的完整调用链(device.c:165-186):

kunit_device_register(test, "my_device")
    kunit_driver_create(test, "my_device")       # 创建 driver
        kunit_kzalloc(test, sizeof(*driver), ...) # 托管分配
        driver_register(driver)
        kunit_add_action(test, driver_unregister_wrapper, driver)  # 注册清理
    kunit_device_register_internal(test, "my_device")
        kzalloc_obj(*kunit_dev)
        dev_set_name(&dev, "%s.%s", test->name, name)  # 设备名 = "test.device"
        device_register(&kunit_dev->dev)
        kunit_add_action(test, device_unregister_wrapper, dev)     # 注册清理

设备命名格式(device.c:120):

err = dev_set_name(&kunit_dev->dev, "%s.%s", test->name, name);
// 例如:test->name = "example", name = "my_device"
// 结果:/sys/devices/kunit/example.my_device

20. Hooks 机制:跨模块边界感知测试

20.1 问题背景

CONFIG_KUNIT=m(KUnit 编译为模块)时,非 KUnit 的内核代码(built-in 或其他 模块)无法直接调用 KUnit API,但仍可能需要与测试框架交互(如调用 kunit_fail_current_test() 报告失败)。

KUnit 的解决方案是hooks 机制:将关键函数指针导出到 built-in 的全局表中, KUnit 模块加载时填充该表,非 KUnit 代码通过表间接调用。

20.2 Hooks 表定义

lib/kunit/hooks.c:15-20 中的核心定义:

// 静态分支:任意内核代码可以零开销检查 KUnit 是否运行
DEFINE_STATIC_KEY_FALSE(kunit_running);
EXPORT_SYMBOL(kunit_running);

// 函数指针表:KUnit 模块加载后填充
struct kunit_hooks_table kunit_hooks;
EXPORT_SYMBOL(kunit_hooks);

函数指针表结构(include/kunit/test-bug.h:23-26):

extern struct kunit_hooks_table {
    __printf(3, 4) void (*fail_current_test)(const char*, int,
                                              const char*, ...);
    void *(*get_static_stub_address)(struct kunit *test, void *real_fn_addr);
} kunit_hooks;

20.3 非 KUnit 代码触发失败

include/kunit/test-bug.h:55-61 中的 kunit_fail_current_test() 宏:

#define kunit_fail_current_test(fmt, ...) do {                          \
    if (static_branch_unlikely(&kunit_running)) {                       \
        /* Guaranteed to be non-NULL when kunit_running true */         \
        kunit_hooks.fail_current_test(__FILE__, __LINE__,               \
                                      fmt, ##__VA_ARGS__);              \
    }                                                                   \
} while (0)

使用场景:在被测的普通内核函数中,当检测到不应发生的错误时标记测试失败:

// 在被测代码 my_subsystem.c 中(非测试代码)
void my_critical_path(struct request *req)
{
    if (unlikely(req->flags & INVALID_FLAG)) {
        // 生产代码应有的处理...
        pr_err("invalid request flags\n");

        // 同时通知 KUnit 当前测试失败(如果正在测试)
        kunit_fail_current_test("invalid flags in request: 0x%x",
                                req->flags);
    }
}

20.4 kunit_running 静态分支

kunit_runningDEFINE_STATIC_KEY_FALSE,默认 disabled(所有代码路径均 预测为 false)。KUnit 在执行第一个测试前通过 static_branch_enable() 打开, 所有测试结束后关闭。

这使得 kunit_fail_current_test() 在生产内核中完全没有运行时开销(分支预测 命中率接近 100%,CPU 分支预测器消除了跳转开销)。


21. 属性系统与过滤器深度解析

21.1 属性定义架构

lib/kunit/attributes.c 维护所有可过滤属性的注册表。每个属性由 struct kunit_attr 描述:

struct kunit_attr {
    const char *name;                              // 属性名("speed"、"module"、"is_init")
    void *(*get_attr)(void *test_or_suite, bool is_test);  // 取值函数
    const char *(*to_string)(void *attr, bool *to_free);   // 转字符串
    int (*filter)(void *attr, const char *input, int *err); // 过滤匹配
    void *attr_default;                            // 默认值(无设置时使用)
    enum print_ops print;                          // 打印时机
};

当前内置属性列表(attributes.c:251-276):

属性名 类型 来源 打印时机 用途
speed 枚举(unset/very_slow/slow/normal) kunit_case.attr.speed 总是 过滤慢速测试
module 字符串 kunit_case.module_name 仅套件 按模块过滤
is_init 布尔 kunit_suite.is_init 仅套件 标识 init 阶段测试

21.2 过滤表达式语法

属性过滤器支持六种比较运算符(attributes.c:84):

op_list = "<>!="   # 单字符或双字符运算符

过滤表达式格式:属性名 运算符 值,例如:

speed>slow          # 速度比 slow 快(即 normal)
speed>=slow         # 速度不低于 slow
speed!=very_slow    # 非极慢测试
module=my_module    # 属于特定模块
is_init=false       # 非 init 阶段测试

多个过滤器用逗号分隔(attributes.c:320-333):

speed>slow,module=net_tests

21.3 过滤器的优先级逻辑

kunit_filter_attr_tests()attributes.c:397-473)的匹配优先级:

对每个 test_case:
    如果 test_case 设置了该属性 -> 用 test_case 值匹配
    否则,如果 suite 设置了该属性 -> 用 suite 值匹配
    否则 -> 用属性默认值匹配

对于 speed 属性,默认值为 KUNIT_SPEED_NORMAL,因此 speed>=slow 会过滤掉 所有未明确标注为 slowvery_slow 的测试(默认值 normal >= slow 为真)。

21.4 list_attr 输出格式

./tools/testing/kunit/kunit.py run --list_tests_attr

输出格式(executor.c:291-311):

KTAP version 1
example                             # 套件名
# speed: normal                     # 套件属性
# module: kunit_example             # 模块属性
# is_init: false
example.example_simple_test         # 用例名
# example_simple_test.speed: normal # 用例属性
example.example_slow_test
# example_slow_test.speed: slow
...

22. string_stream:日志缓冲实现

22.1 设计原因

KUnit 的日志系统需要在中断上下文安全地构建多段字符串,且不能一次性分配大缓冲 (内核栈有限,也可能在 atomic 上下文下无法 kmalloc 大块内存)。string_stream 使用碎片链表解决这一问题。

22.2 核心数据结构

定义于 lib/kunit/string-stream.h

struct string_stream {
    size_t length;              // 已写入的总字符数
    struct list_head fragments; // 碎片链表
    gfp_t gfp;                  // 分配标志(GFP_KERNEL 或 GFP_ATOMIC)
    bool append_newlines;       // 自动追加换行符
    spinlock_t lock;            // 保护 fragments 链表(中断安全)
};

struct string_stream_fragment {
    struct list_head node;
    char *fragment;             // 动态分配的字符串片段
};

22.3 写入实现

string_stream_vadd()string-stream.c:41-88)的两步写入策略:

1. 用 vsnprintf(NULL, 0, fmt, args) 预测需要的字节数
2. kmalloc(buf_len) 分配精确大小的碎片
3. vsnprintf(frag->fragment, buf_len, fmt, args) 格式化写入
4. 加锁,追加碎片到链表尾部

预测然后精确分配(而非先分配大缓冲再 realloc)避免了内存浪费,也保证每次 string_stream_add() 都是原子操作(单个碎片要么完整追加,要么失败)。

22.4 debugfs 回放

lib/kunit/debugfs.c:44-59 中的 debugfs_print_result() 在回放时直接遍历碎片 链表,无需分配临时大缓冲:

static void debugfs_print_result(struct seq_file *seq,
                                  struct string_stream *log)
{
    struct string_stream_fragment *frag_container;

    spin_lock(&log->lock);
    list_for_each_entry(frag_container, &log->fragments, node)
        seq_printf(seq, "%s", frag_container->fragment);  // 逐片输出
    spin_unlock(&log->lock);
}

seq_file 的分页机制与碎片链表天然配合:seq_printf() 会在内部缓冲满时自动 暂停,用户态 read() 系统调用分页读取,整个过程零额外分配。


23. UML(User Mode Linux)运行测试

23.1 UML 简介

User Mode Linux(UML)是 Linux 内核的一种特殊编译配置(ARCH=um),将整个内核 编译为一个 ELF 可执行文件,运行在宿主 Linux 的用户态进程中。KUnit 官方推荐的 快速开发模式正是基于 UML。

UML 的优势:

  • 无需真实硬件或 VM:直接运行在开发机上,启动时间 < 5 秒
  • 支持 gdb 调试:可以像调试普通程序一样设置断点、检查内存
  • 隔离性好:内核 panic 不影响宿主系统
  • 覆盖率支持:与 gcov 配合,收集代码覆盖率数据

23.2 kunit.py 的 UML 工作流

kunit.py run 的内部步骤(tools/testing/kunit/kunit_kernel.py):

1. 读取 .kunitconfig,合并到 UML defconfig
   (arch/um/configs/kunit_defconfig)

2. make ARCH=um defconfig O=.kunit
   make ARCH=um -j$(nproc) O=.kunit

3. 执行:.kunit/linux \
       mem=1G \
       kunit.filter_glob=<filter> \
       kunit_shutdown=halt   <- 测试完成后关机

4. 捕获标准输出,传给 kunit_parser.py 解析 KTAP

5. 通过 kunit_printer.py 输出彩色结果

23.3 UML 运行的限制

由于 UML 在用户态模拟内核,部分测试无法运行:

  • 需要真实硬件寄存器的驱动测试(如 PCI BAR 映射,除非 CONFIG_KUNIT_UML_PCI=y
  • 依赖特定 CPU 指令集扩展的测试(如 ARM64 的 MTE/SVE)
  • 测试多 CPU 并发行为(UML 默认单核,可配置 SMP 但有限制)

lib/kunit/Kconfig:133-142 中的 UML PCI 支持:

config KUNIT_UML_PCI
    bool "KUnit UML PCI Support"
    depends on UML
    select UML_PCI
    help
      Enables the PCI subsystem on UML for use by KUnit tests.

23.4 手动使用 UML

# 编译 UML 内核
make ARCH=um defconfig
make ARCH=um CONFIG_KUNIT=y CONFIG_KUNIT_EXAMPLE_TEST=y -j8

# 运行(mem= 指定内存,rootfstype=hostfs,/ 挂载宿主根目录)
./linux mem=512M \
        kunit.enable=1 \
        kunit.filter_glob='example*' \
        kunit_shutdown=halt 2>&1 | grep -E 'ok|not ok|#'

# gdb 调试
gdb --args ./linux mem=512M kunit.filter_glob='example.example_simple_test'
(gdb) b kunit_run_tests
(gdb) run

24. QEMU 虚拟机运行内核测试

24.1 QEMU 后端支持

kunit.py--arch 参数指定目标架构,自动选择合适的 QEMU 命令和配置。 工具链定义于 tools/testing/kunit/qemu_configs/ 目录,每个架构一个配置文件。

支持的架构示例:

架构 QEMU 命令 交叉编译器
x86_64 qemu-system-x86_64 (无需)
arm qemu-system-arm arm-linux-gnueabihf-
arm64 qemu-system-aarch64 aarch64-linux-gnu-
riscv qemu-system-riscv64 riscv64-linux-gnu-
s390 qemu-system-s390x s390x-linux-gnu-
powerpc qemu-system-ppc64 powerpc64le-linux-gnu-

24.2 典型 QEMU 运行命令

以 ARM64 为例:

./tools/testing/kunit/kunit.py run \
    --arch=arm64 \
    --cross_compile=aarch64-linux-gnu- \
    --kunitconfig=lib/kunit/.kunitconfig \
    --jobs=8

工具链内部调用类似于:

# 编译
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- \
     -C . O=.kunit_arm64 \
     CONFIG_KUNIT=y ...

# 运行
qemu-system-aarch64 \
    -machine virt \
    -cpu cortex-a57 \
    -m 1G \
    -nographic \
    -kernel .kunit_arm64/arch/arm64/boot/Image \
    -append "console=ttyAMA0 kunit.enable=1 kunit_shutdown=halt" \
    -serial stdio

24.3 QEMU 与 UML 的选择建议

场景 建议
快速迭代、纯逻辑测试 UML(最快)
跨架构兼容性验证 QEMU(模拟目标架构)
架构特定功能测试(如 ARM64 MTE) 真实硬件或 QEMU
CI 集成(x86_64 主机) UML(无额外依赖)
CI 集成(多架构) QEMU

24.4 在真实内核启动时运行

将测试编译进内核(CONFIG_KUNIT=y):

# 内核启动参数(grub、U-Boot 等)
kunit.enable=1
kunit.filter_glob=my_driver*
kunit_shutdown=poweroff

# 通过串口或 dmesg 查看结果
dmesg | grep -E 'KTAP|ok |not ok |#'

25. gcov 与内核代码覆盖率

25.1 内核 gcov 原理

内核 gcov 支持(kernel/gcov/)将 gcc 的 -fprofile-arcs -ftest-coverage 特性移植到内核空间。每个被插桩的源文件在内核中维护一份 struct gcov_info 对象, 记录各基本块的执行次数。

启用方式:

CONFIG_GCOV_KERNEL=y
CONFIG_GCOV_PROFILE_ALL=y   # 对所有内核代码插桩
# 或
CONFIG_GCOV_PROFILE_KUNIT=y # 仅对带测试的代码插桩(若存在)

构建时额外标志:

# 单个文件开启覆盖
GCOV_PROFILE_my_file.o := y
# 整个目录开启
GCOV_PROFILE := y

25.2 覆盖率数据访问

CONFIG_GCOV_KERNEL=y 时,debugfs 下暴露原始覆盖率数据:

/sys/kernel/debug/gcov/
├── <src_path>/
│   ├── *.gcda     # 覆盖率计数数据(二进制)
│   └── *.gcno     # 插桩图(构建时生成,不在此处)
└── reset          # 写入任意内容清零所有计数器

25.3 与 KUnit 集成的工作流

# 1. 编译带 gcov 的 UML 内核
cat >> .kunitconfig << 'EOF'
CONFIG_GCOV_KERNEL=y
CONFIG_GCOV_PROFILE_ALL=y
EOF

./tools/testing/kunit/kunit.py run --build_dir=.kunit_cov

# 2. 执行完测试后,UML 进程退出
#    gcov 数据自动写入 .kunit_cov/gcov/ 目录

# 3. 用 lcov 生成 HTML 报告
lcov --capture \
     --directory .kunit_cov \
     --output-file coverage.info

genhtml coverage.info \
     --output-directory cov_html/

# 4. 打开浏览器查看
open cov_html/index.html

25.4 覆盖率报告示例

File              Lines     Hit   Coverage
lib/kunit/test.c   1150     892     77.6%
lib/kunit/assert.c  280     215     76.8%
my_module.c         400     312     78.0%
    -- critical function:
       my_critical_path:  12/15  (80.0%)
       my_edge_handler:    3/20  (15.0%)  <-- 低覆盖,需补充测试

25.5 覆盖率的局限性

  • 行覆盖 vs 分支覆盖:行被执行不代表所有分支都被测试到,应同时关注 --branch-coverage 报告。
  • 中断上下文路径:KUnit 在内核线程中运行,无法覆盖中断处理器中的代码路径。
  • 并发路径:单线程测试无法触发竞态窗口,需结合 KCSAN(内核数据竞争检测器)。

26. 在真实硬件上运行:ftrace 与 debugfs

26.1 真实硬件上的调试工具集成

在真实硬件(非 UML/QEMU)上运行 KUnit 测试时,可以结合 ftrace 和其他内核调试 工具对测试过程进行深度分析。

26.2 用 ftrace 追踪测试执行

# 挂载 tracefs(若未挂载)
mount -t tracefs nodev /sys/kernel/tracing

# 追踪 KUnit 相关函数
echo 'kunit_*' > /sys/kernel/tracing/set_ftrace_filter
echo function > /sys/kernel/tracing/current_tracer
echo 1 > /sys/kernel/tracing/tracing_on

# 触发指定套件重跑
echo 1 > /sys/kernel/debug/kunit/example/run

# 关闭追踪并查看
echo 0 > /sys/kernel/tracing/tracing_on
cat /sys/kernel/tracing/trace | head -60

典型输出(显示测试框架调用链):

kunit-1        kunit_run_tests
kunit-1          kunit_print_suite_start
kunit-1          kunit_run_one_test
kunit-1            kunit_run_case_catch_errors
kunit-1              kunit_try_catch_run
kunit_try_catc         kunit_try_run_case
kunit_try_catc           example_simple_test
kunit_try_catc           kunit_cleanup

26.3 用 function_graph 追踪测试体内部

# 追踪测试函数及其调用的子函数
echo function_graph > /sys/kernel/tracing/current_tracer
echo 'example_static_stub_test' > /sys/kernel/tracing/set_graph_function
echo 1 > /sys/kernel/tracing/tracing_on

echo 1 > /sys/kernel/debug/kunit/example/run

echo 0 > /sys/kernel/tracing/tracing_on
cat /sys/kernel/tracing/trace

26.4 结合 kmemleak 检测测试内存泄漏

# 测试前清零 kmemleak 记录
echo clear > /sys/kernel/debug/kmemleak

# 运行测试
echo 1 > /sys/kernel/debug/kunit/my_suite/run

# 触发扫描并查看泄漏
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak

若 KUnit 资源管理正确(所有分配通过 kunit_kmalloc 等),测试结束后应无泄漏。 如有泄漏,说明存在未通过 kunit_resource 注册的分配。

26.5 通过 dmesg 过滤 KUnit 输出

在真实内核上,KUnit 输出混入其他 dmesg 消息。过滤方法:

# 实时监控 KUnit 输出
dmesg --follow | grep -E 'kunit|KTAP|ok |not ok'

# 使用时间戳过滤(KUnit 输出集中在短时间内)
dmesg -T | awk '/KTAP version/,/^[^[:space:]]/'

# 直接读取 debugfs 结果(最清晰,避免日志混淆)
cat /sys/kernel/debug/kunit/example/results

26.6 用 perf 分析测试性能热点

# 记录测试执行期间的性能数据
perf record -g -- sh -c \
    'echo 1 > /sys/kernel/debug/kunit/my_suite/run && sleep 1'

# 分析调用图
perf report --stdio --call-graph=dwarf | head -40

27. Kconfig 完整配置参考

lib/kunit/Kconfig 提供了完整的 KUnit 配置选项,以下是各选项的详细说明:

27.1 主配置选项

CONFIG_KUNIT                   # 核心框架(tristate:y/m/n)
CONFIG_KUNIT_DEBUGFS           # debugfs 接口(bool,默认跟随 KUNIT_ALL_TESTS)
CONFIG_KUNIT_FAULT_TEST        # 故障处理测试(UML 上不可用,可能触发 BUG 栈跟踪)
CONFIG_KUNIT_TEST              # KUnit 自测(框架测试自己)
CONFIG_KUNIT_EXAMPLE_TEST      # 官方示例测试
CONFIG_KUNIT_ALL_TESTS         # 启用所有满足依赖的 KUnit 测试

27.2 运行时控制选项

CONFIG_KUNIT_DEFAULT_ENABLED   # kunit.enable 默认值(默认 y)
CONFIG_KUNIT_AUTORUN_ENABLED   # kunit.autorun 默认值(默认 y)
                               # 设为 n 时测试只能通过 debugfs 手动触发
CONFIG_KUNIT_DEFAULT_FILTER_GLOB  # 默认 glob 过滤(空字符串 = 不过滤)
CONFIG_KUNIT_DEFAULT_FILTER       # 默认属性过滤
CONFIG_KUNIT_DEFAULT_FILTER_ACTION # 默认过滤动作(空/skip)
CONFIG_KUNIT_DEFAULT_TIMEOUT   # 默认超时(秒,默认 300)
CONFIG_KUNIT_UML_PCI           # UML 上的 PCI 支持

27.3 典型配置组合

开发机快速测试(UML)

CONFIG_KUNIT=y
CONFIG_KUNIT_DEBUGFS=y
CONFIG_KUNIT_ALL_TESTS=y
CONFIG_KUNIT_DEFAULT_FILTER_GLOB=""
CONFIG_KUNIT_AUTORUN_ENABLED=y

CI/CD 生产内核(嵌入式)

CONFIG_KUNIT=y
CONFIG_KUNIT_DEBUGFS=n
CONFIG_KUNIT_ALL_TESTS=n
CONFIG_MY_DRIVER_KUNIT_TEST=y
CONFIG_KUNIT_DEFAULT_ENABLED=y
CONFIG_KUNIT_AUTORUN_ENABLED=n    # 不自动运行,通过调试接口按需触发

产品内核(禁用所有测试)

CONFIG_KUNIT=n
# 以下选项自动消失,生产代码零开销

28. KUnit 自测:框架自我验证

28.1 设计哲学

KUnit 框架本身也使用 KUnit 进行测试——这是一个递归的自验证机制。主要自测文件:

  • lib/kunit/kunit-test.c:核心框架自测(try-catch、资源管理、断言宏)
  • lib/kunit/assert_test.c:断言格式化输出自测
  • lib/kunit/executor_test.c:过滤和执行逻辑自测(内联进 executor.c
  • lib/kunit/string-stream-test.c:字符串流自测
  • lib/kunit/platform-test.c:平台设备托管自测

28.2 自测的特殊性

测试 KUnit 的"失败处理"路径时,需要制造预期的失败,同时不让框架认为测试真的失败 了。kunit-test.c 通过嵌套测试套件(struct kunit 嵌套)实现:

// 制造一个"假的"失败,验证失败信息格式正确
static void kunit_test_all_expect_macros_test(struct kunit *test)
{
    // 创建一个内嵌 kunit 实例,让失败发生在内嵌实例上
    struct kunit fake;
    kunit_init_test(&fake, "fake_test", NULL);

    KUNIT_EXPECT_EQ(&fake, 1, 2);    // 故意失败
    // 验证 fake 的 status 变为 KUNIT_FAILURE
    KUNIT_EXPECT_EQ(test, fake.status, KUNIT_FAILURE);

    kunit_cleanup(&fake);
}

28.3 executor_test.c 的过滤测试

lib/kunit/executor_test.c 使用 IS_BUILTIN(CONFIG_KUNIT_TEST) 守护, 仅在内建模式下编译进 executor.cexecutor.c:425-427):

#if IS_BUILTIN(CONFIG_KUNIT_TEST)
#include "executor_test.c"
#endif

测试覆盖的过滤场景包括:

  • 无 glob 时所有套件都通过
  • glob 匹配套件名
  • suite.test 格式 glob 同时过滤套件和用例
  • 属性过滤(speed/module/is_init)
  • 多过滤器组合
  • filter_action=skip 时被过滤的用例状态

29. 总结

KUnit 是 Linux 内核测试体系中的重要环节,其设计在内核环境的严格约束下实现了 现代单元测试框架的核心功能:

架构层面

  • ELF section 自动注册test.h:410-413):测试套件通过 __section(".kunit_test_suites") 放入专用 ELF section,执行器通过链接脚本生成的符号扫描,无需全局注册表,实现零侵入式发现。

  • kthread 隔离try-catch.c:37-91):每个测试用例在独立内核线程中运行,ASSERT 失败通过 kthread_exit() 模拟异常抛出,父线程感知后继续清理,防止单个失败影响整个测试集。

  • RAII 资源管理resource.ctest.c:1023-1056):以 LIFO 顺序自动清理资源链表,无论测试如何终止,消除资源泄漏,是内核环境下 devres 思想的直接应用。

用户体验层面

  • 分层宏体系test.h:829-979):EXPECT/ASSERT 两类断言通过统一的 assert_type 参数区分,字符串化操作确保失败信息直接引用源码表达式,无需手动填写名称。

  • KTAP 标准化输出test.c:157-240):嵌套子测试支持、ok/not ok 格式,与 CI 工具链无缝集成。

  • kunit.py 工具链tools/testing/kunit/):从 UML 构建到 KTAP 解析的完整自动化,数秒内在开发机完成测试,大幅降低内核开发的迭代成本。

扩展机制层面

  • Static Stubinclude/kunit/static_stub.hlib/kunit/static_stub.c):无需修改指令即可替换函数,per-test 作用域,测试结束自动恢复,零生产开销。

  • Hooks 机制lib/kunit/hooks.cinclude/kunit/test-bug.h):通过函数指针表和静态分支,允许非 KUnit 代码与测试框架通信,支持 KUnit 编译为模块的场景。

  • 托管设备lib/kunit/device.c):为驱动测试提供虚拟 struct device,集成于 kunit_bus,通过资源管理机制自动清理。

  • 属性系统lib/kunit/attributes.c):speed/module/is_init 三维属性,支持六种比较运算符的过滤表达式,可在 CI 中精细控制测试集。

与 kselftest 的协作:KUnit 覆盖内核内部逻辑的单元测试,kselftest 覆盖系统调用级的集成测试,两者共同构成 Linux 内核质量保障的双层防线。

主要源码路径:

文件 内容
lib/kunit/test.c 执行引擎、资源清理、KTAP 输出
lib/kunit/executor.c 测试发现、过滤、批量执行
lib/kunit/assert.c 断言格式化、hexdump
lib/kunit/resource.c 资源/action 注册与释放
lib/kunit/try-catch.c kthread 隔离机制
lib/kunit/static_stub.c 函数重定向实现
lib/kunit/device.c 托管虚拟设备
lib/kunit/hooks.c 跨模块边界感知
lib/kunit/attributes.c 属性注册与过滤
lib/kunit/string-stream.c 碎片链表日志缓冲
lib/kunit/debugfs.c debugfs 接口与在线重跑
lib/kunit/kunit-example-test.c 官方示例(全面演示各特性)
lib/kunit/Kconfig 完整配置选项
include/kunit/test.h 公开 API、断言宏(~1800 行)
include/kunit/resource.h 资源管理 API
include/kunit/assert.h 断言数据结构
include/kunit/static_stub.h 静态桩 API
include/kunit/visibility.h VISIBLE_IF_KUNITEXPORT_SYMBOL_IF_KUNIT
include/kunit/device.h 托管设备 API
include/kunit/test-bug.h kunit_fail_current_testkunit_get_current_test
tools/testing/selftests/kselftest.h kselftest 基础 API
tools/testing/selftests/kselftest_harness.h kselftest fixture 框架
tools/testing/kunit/kunit.py 运行工具链入口

由 Claude Code 分析生成