源码基线:Linux mainline(截至 2026-03) 核心文件:
lib/kunit/test.c、lib/kunit/executor.c、lib/kunit/assert.c、include/kunit/test.h、include/kunit/resource.h、lib/kunit/try-catch.c
- 背景与设计理念
- KUnit 与 kselftest 的对比
- 整体架构
- 核心数据结构
- 宏展开分析:KUNIT_CASE 与 kunit_test_suite
- 测试执行器(Executor)与结果收集
- try-catch 机制:测试隔离的底层原理
- 断言宏体系的完整实现
- kunit_resource 资源管理(RAII 模式)
- TAP / KTAP 输出格式
- 在模块中编写 KUnit 测试
- 参数化测试
- kselftest 框架详解与结构
- 运行方式:kunit.py 工具链
- debugfs 集成与在线重跑
- 实战示例:完整测试模块
- 常见问题与最佳实践
- Static Stub:函数重定向机制
- KUnit 托管设备:kunit_device
- Hooks 机制:跨模块边界感知测试
- 属性系统与过滤器深度解析
- string_stream:日志缓冲实现
- UML(User Mode Linux)运行测试
- QEMU 虚拟机运行内核测试
- gcov 与内核代码覆盖率
- 在真实硬件上运行:ftrace 与 debugfs
- Kconfig 完整配置参考
- KUnit 自测:框架自我验证
- 总结
在 KUnit 出现之前,Linux 内核的测试体系存在明显的"中间层空白":
- kselftest:运行在用户态(ring 3),通过系统调用与内核交互,无法直接测试
内核内部的
static函数或数据结构。 - 散落的
test_*.c文件:各子系统自行维护,缺乏统一框架约束,输出格式不一, 难以集成到 CI/CD 流水线。
核心矛盾在于:驱动/子系统开发者需要在不暴露内部接口的前提下,快速验证内部逻辑的正确性。
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>
*/设计目标体现在以下五个维度:
-
内核态直接执行:测试代码与被测代码同处内核地址空间,可调用任意内核 函数,含
static函数(配合EXPORT_SYMBOL_IF_KUNIT)。 -
零生产开销:当
CONFIG_KUNIT未开启时,所有测试相关代码被预处理器完整 消除,对生产内核毫无影响。 -
快速开发周期:支持在 UML(User Mode Linux)上无需真实硬件运行, 通过
kunit.py工具链数秒内完成构建和测试。 -
标准化输出:输出 KTAP 协议(TAP 的内核超集),可被任意 TAP 解析器处理, 无缝对接 CI 系统。
-
资源自动回收:借鉴 devres(device resource management)思路,通过
kunit_resource实现测试结束后的自动清理,无论测试成功、失败还是超时。
| 维度 | 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 测试内核内部实现,两者 互补而非替代。大型子系统通常同时使用两种框架。
+------------------------------------------------------------------+
| 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 解析 |
+------------------------------------------------------------------+
定义于 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 也能定位到最后一行测试代码。
定义于 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++)定义于 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) // 整个套件执行一次
注意:exit 和 suite_exit 在 init/suite_init 失败时仍然会被调用,
需要能够处理不完整的初始化状态(include/kunit/test.h:267-269)。
定义于 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_case为NULL)标志数组结束。
变体宏对照(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) { ... }完整展开链(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"))原理完全相同。
内建模式(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()。
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 而非直接跳过
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;
}struct kunit_result_stats(test.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:总是输出
内核测试面临一个独特挑战:若测试触发 NULL 指针解引用,整个内核将 panic。KUnit 通过将每个测试用例在独立的内核线程(kthread)中执行,并结合超时机制,在不崩溃 内核的前提下隔离测试失败。
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 对象。
当 KUNIT_ASSERT_* 失败时,调用链如下(test.c:306-318 和 try-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()。
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);
}KUnit 将断言分为两类,通过 assert_type 参数在同一套宏体系中统一实现:
| 类别 | 宏前缀 | 失败行为 | 使用场景 |
|---|---|---|---|
| Expectation | KUNIT_EXPECT_* |
记录失败,继续执行 | 收集多个失败信息 |
| Assertion | KUNIT_ASSERT_* |
记录失败,立即中止 | 前提条件检查 |
选择原则:若某条件失败后继续执行无意义(如 ptr == NULL 后必然崩溃),
用 ASSERT;否则用 EXPECT,尽量在一次运行中收集所有失败。
以 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_ASSERTION(test.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_FAILED(test.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)| 分类 | 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)。
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
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
// 函数返回,测试继续执行
KUnit 资源管理借鉴 C++ RAII 和 Linux devres(devm_* 系列函数)的思想。
所有测试期间分配的资源以链表形式挂载到 test->resources,测试结束时框架按
逆序(后注册先释放,类似栈)自动回收,无论测试如何终止。
定义于 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 回调。
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;
}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.
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); }include/kunit/test.h:486-536 和 test.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。
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 空格(参数化测试级)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
套件开始(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)同时写入 printk 和 test->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)以 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");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若要测试 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 构建下展开为空,不影响生产代码的符号导出范围。
对含 __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.
参数化测试(parameterized test)让同一测试函数以多组输入运行,每组参数产生独立的 KTAP 结果行,兼顾代码复用与清晰的失败定位。
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);
}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(¶m_test, test_case->name, NULL);
param_test.log = test_case->log;
kunit_run_case_catch_errors(suite, test_case, ¶m_test);
// 打印该参数的 ok/not ok 行...
curr_param = test_case->generate_params(test, curr_param, param_desc);
}KUNIT_CASE_PARAM_WITH_INIT(test.h:245-249)支持为整个参数化测试
(而非每次参数运行)定义初始化和清理函数,通过 test->parent 在参数间共享资源,
参见 kunit-example-test.c:332-388 的完整示例。
kselftest 位于 tools/testing/selftests/,测试程序以普通用户态进程运行,
通过系统调用、ioctl、sysfs 等接口验证内核行为,覆盖 200 多个子系统。
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 4tools/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() 函数,包含运行所有测试的逻辑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+ 子系统)
# 编译所有 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:sockettools/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# 运行所有 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.kunitconfig 是最小化的 Kconfig 片段,kunit.py 将其合并到 UML defconfig:
# 典型的 .kunitconfig
CONFIG_KUNIT=y
CONFIG_KUNIT_EXAMPLE_TEST=y
# 要测试的子系统
CONFIG_SLAB=y
CONFIG_SLAB_KUNIT_TEST=y
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 # 测试完成后关机(自动化场景)
当 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 的注释说明)。
以下是一个针对假想链表实现的完整 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");错误用法:对前提条件使用 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); // 收集更多信息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;
}超过 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 }),KUnit 测试共享内核地址空间,修改全局状态(sysfs 属性、全局变量)可能导致 测试间相互影响。解决方案:
- 使用
kunit_add_action()注册恢复函数,确保exit()前全局状态复原。 - 尽量通过
test->priv传递状态,避免使用全局变量。
每个测试的 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)。
静态桩(static stub)是 KUnit 提供的轻量级函数替换机制,无需动态修改指令(如
ftrace 或 livepatch),在编译期在被替换函数头部插入跳转检查点,测试时在运行时
重定向到替换函数。定义于 include/kunit/static_stub.h 和 lib/kunit/static_stub.c。
被替换函数在其函数体开头调用此宏(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 上下文返回NULL,likely(!current_test)预测条件为真,直接 break 进入正常执行路径。
测试中使用 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()(尽管可以提前注销)。
参见 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); // 注销后,恢复正常
}对于无法直接引用地址的 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);
}| 特性 | Static Stub | ftrace hook | livepatch |
|---|---|---|---|
| 替换粒度 | 单个函数入口 | 单个函数入口 | 整个函数体 |
| 运行时指令修改 | 否(编译期插桩) | 是(运行期 NOP->JMP) | 是 |
| 测试上下文感知 | 是(per-test) | 否(全局) | 否(全局) |
| 生产代码侵入 | 极小(一行宏) | 无 | 无 |
| 适用范围 | 仅 KUnit 测试 | 内核调试/跟踪 | 热补丁 |
| 架构依赖 | 无 | 依赖 arch ftrace | 依赖 arch livepatch |
测试驱动程序时,通常需要一个有效的 struct device 对象(如用于 DMA 映射、
devm 分配等)。KUnit 提供了托管设备接口,在专用的 kunit_bus 上注册虚拟设备,
测试结束后自动注销。
lib/kunit/device.c 在 kunit_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/
#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);
}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当 CONFIG_KUNIT=m(KUnit 编译为模块)时,非 KUnit 的内核代码(built-in 或其他
模块)无法直接调用 KUnit API,但仍可能需要与测试框架交互(如调用
kunit_fail_current_test() 报告失败)。
KUnit 的解决方案是hooks 机制:将关键函数指针导出到 built-in 的全局表中, KUnit 模块加载时填充该表,非 KUnit 代码通过表间接调用。
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;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);
}
}kunit_running 是 DEFINE_STATIC_KEY_FALSE,默认 disabled(所有代码路径均
预测为 false)。KUnit 在执行第一个测试前通过 static_branch_enable() 打开,
所有测试结束后关闭。
这使得 kunit_fail_current_test() 在生产内核中完全没有运行时开销(分支预测
命中率接近 100%,CPU 分支预测器消除了跳转开销)。
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 阶段测试 |
属性过滤器支持六种比较运算符(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
kunit_filter_attr_tests()(attributes.c:397-473)的匹配优先级:
对每个 test_case:
如果 test_case 设置了该属性 -> 用 test_case 值匹配
否则,如果 suite 设置了该属性 -> 用 suite 值匹配
否则 -> 用属性默认值匹配
对于 speed 属性,默认值为 KUNIT_SPEED_NORMAL,因此 speed>=slow 会过滤掉
所有未明确标注为 slow 或 very_slow 的测试(默认值 normal >= slow 为真)。
./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
...
KUnit 的日志系统需要在中断上下文安全地构建多段字符串,且不能一次性分配大缓冲
(内核栈有限,也可能在 atomic 上下文下无法 kmalloc 大块内存)。string_stream
使用碎片链表解决这一问题。
定义于 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; // 动态分配的字符串片段
};
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() 都是原子操作(单个碎片要么完整追加,要么失败)。
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() 系统调用分页读取,整个过程零额外分配。
User Mode Linux(UML)是 Linux 内核的一种特殊编译配置(ARCH=um),将整个内核
编译为一个 ELF 可执行文件,运行在宿主 Linux 的用户态进程中。KUnit 官方推荐的
快速开发模式正是基于 UML。
UML 的优势:
- 无需真实硬件或 VM:直接运行在开发机上,启动时间 < 5 秒
- 支持 gdb 调试:可以像调试普通程序一样设置断点、检查内存
- 隔离性好:内核 panic 不影响宿主系统
- 覆盖率支持:与 gcov 配合,收集代码覆盖率数据
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 输出彩色结果
由于 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.
# 编译 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) runkunit.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- |
以 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| 场景 | 建议 |
|---|---|
| 快速迭代、纯逻辑测试 | UML(最快) |
| 跨架构兼容性验证 | QEMU(模拟目标架构) |
| 架构特定功能测试(如 ARM64 MTE) | 真实硬件或 QEMU |
| CI 集成(x86_64 主机) | UML(无额外依赖) |
| CI 集成(多架构) | QEMU |
将测试编译进内核(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 |#'内核 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 := yCONFIG_GCOV_KERNEL=y 时,debugfs 下暴露原始覆盖率数据:
/sys/kernel/debug/gcov/
├── <src_path>/
│ ├── *.gcda # 覆盖率计数数据(二进制)
│ └── *.gcno # 插桩图(构建时生成,不在此处)
└── reset # 写入任意内容清零所有计数器
# 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.htmlFile 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%) <-- 低覆盖,需补充测试
- 行覆盖 vs 分支覆盖:行被执行不代表所有分支都被测试到,应同时关注
--branch-coverage报告。 - 中断上下文路径:KUnit 在内核线程中运行,无法覆盖中断处理器中的代码路径。
- 并发路径:单线程测试无法触发竞态窗口,需结合 KCSAN(内核数据竞争检测器)。
在真实硬件(非 UML/QEMU)上运行 KUnit 测试时,可以结合 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
# 追踪测试函数及其调用的子函数
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# 测试前清零 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 注册的分配。
在真实内核上,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# 记录测试执行期间的性能数据
perf record -g -- sh -c \
'echo 1 > /sys/kernel/debug/kunit/my_suite/run && sleep 1'
# 分析调用图
perf report --stdio --call-graph=dwarf | head -40lib/kunit/Kconfig 提供了完整的 KUnit 配置选项,以下是各选项的详细说明:
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 测试
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 支持
开发机快速测试(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
# 以下选项自动消失,生产代码零开销
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:平台设备托管自测
测试 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);
}lib/kunit/executor_test.c 使用 IS_BUILTIN(CONFIG_KUNIT_TEST) 守护,
仅在内建模式下编译进 executor.c(executor.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时被过滤的用例状态
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.c和test.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 Stub(
include/kunit/static_stub.h、lib/kunit/static_stub.c):无需修改指令即可替换函数,per-test 作用域,测试结束自动恢复,零生产开销。 -
Hooks 机制(
lib/kunit/hooks.c、include/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_KUNIT、EXPORT_SYMBOL_IF_KUNIT |
include/kunit/device.h |
托管设备 API |
include/kunit/test-bug.h |
kunit_fail_current_test、kunit_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 分析生成