Linux 内核自测

内核在 tools/testing/selftests/ 目录下包含一组“自测”。这些测试旨在用于执行内核中的各个代码路径的小型测试。测试旨在在构建、安装和启动内核后运行。

主线的 Kselftest 可以在较旧的稳定内核上运行。从主线运行测试可提供最佳覆盖率。有几个测试环在稳定版本上运行主线 kselftest 套件。原因是当添加新的测试来测试现有代码以回归测试错误时,我们应该能够在较旧的内核上运行该测试。因此,保持仍然可以测试较旧内核的代码,并确保它在新版本上正常跳过测试非常重要。

您可以在 Kselftest wiki 上找到有关 Kselftest 框架的更多信息,以及如何使用该框架编写新测试的信息

https://kselftest.wiki.kernel.org/

在某些系统上,热插拔测试可能会永远挂起,等待 CPU 和内存准备好离线。创建了一个特殊的热插拔目标来运行全范围的热插拔测试。在默认模式下,热插拔测试以安全模式运行,范围有限。在有限模式下,cpu-hotplug 测试在单个 CPU 上运行,而不是在所有支持热插拔的 CPU 上运行,并且内存热插拔测试在 2% 的支持热插拔的内存上运行,而不是 10%。

kselftest 作为用户空间进程运行。可以在用户空间中编写/运行的测试可能希望使用测试框架。需要在内核空间中运行的测试可能希望使用测试模块

有关测试的文档

有关 kselftest 本身的文档,请参阅

运行自测 (热插拔测试以有限模式运行)

要构建测试

$ make headers
$ make -C tools/testing/selftests

要运行测试

$ make -C tools/testing/selftests run_tests

要使用单个命令构建和运行测试,请使用

$ make kselftest

请注意,某些测试需要 root 权限。

Kselftest 支持将输出文件保存在单独的目录中,然后运行测试。要在单独的目录中查找输出文件,支持两种语法。在这两种情况下,工作目录都必须是内核 src 的根目录。这适用于下面的“运行自测的子集”部分。

要构建,将输出文件保存在带有 O= 的单独目录中

$ make O=/tmp/kselftest kselftest

要构建,将输出文件保存在带有 KBUILD_OUTPUT 的单独目录中

$ export KBUILD_OUTPUT=/tmp/kselftest; make kselftest

O= 赋值优先于 KBUILD_OUTPUT 环境变量。

默认情况下,以上命令会运行测试并打印完整的通过/失败报告。Kselftest 支持“summary”选项,以便更容易理解测试结果。当指定 summary 选项时,请在 /tmp/testname 文件中查找每个测试的详细单独测试结果。这适用于下面的“运行自测的子集”部分。

要启用 summary 选项运行 kselftest

$ make summary=1 kselftest

运行自测的子集

您可以使用 make 命令行上的“TARGETS”变量来指定要运行的单个测试,或要运行的测试列表。

要仅运行针对单个子系统的测试

$ make -C tools/testing/selftests TARGETS=ptrace run_tests

您可以指定多个要构建和运行的测试

$  make TARGETS="size timers" kselftest

要构建,将输出文件保存在带有 O= 的单独目录中

$ make O=/tmp/kselftest TARGETS="size timers" kselftest

要构建,将输出文件保存在带有 KBUILD_OUTPUT 的单独目录中

$ export KBUILD_OUTPUT=/tmp/kselftest; make TARGETS="size timers" kselftest

此外,您可以使用 make 命令行上的“SKIP_TARGETS”变量来指定要从 TARGETS 列表中排除的一个或多个目标。

要运行所有测试,但排除单个子系统

$ make -C tools/testing/selftests SKIP_TARGETS=ptrace run_tests

您可以指定多个要跳过的测试

$  make SKIP_TARGETS="size timers" kselftest

您还可以指定一个受限制的测试列表与一个专用的跳过列表一起运行

$  make TARGETS="breakpoints size timers" SKIP_TARGETS=size kselftest

有关所有可能的目标的列表,请参阅顶级的 tools/testing/selftests/Makefile。

运行全范围热插拔自测

要构建热插拔测试

$ make -C tools/testing/selftests hotplug

要运行热插拔测试

$ make -C tools/testing/selftests run_hotplug

请注意,某些测试需要 root 权限。

安装自测

您可以使用“make”的“install”目标(调用 kselftest_install.sh 工具)将自测安装在默认位置 (tools/testing/selftests/kselftest_install),或通过 INSTALL_PATH “make”变量安装在用户指定的位置。

要在默认位置安装自测

$ make -C tools/testing/selftests install

要在用户指定的位置安装自测

$ make -C tools/testing/selftests install INSTALL_PATH=/some/other/path

运行已安装的自测

在安装目录中以及 Kselftest tarball 中,有一个名为 run_kselftest.sh 的脚本用于运行测试。

您可以简单地执行以下操作来运行已安装的 Kselftest。请注意,某些测试需要 root 权限

$ cd kselftest_install
$ ./run_kselftest.sh

要查看可用测试的列表,可以使用 -l 选项

$ ./run_kselftest.sh -l

可以使用 -c 选项运行来自测试集合的所有测试,或使用 -t 选项运行特定的单个测试。两者都可以多次使用

$ ./run_kselftest.sh -c size -c seccomp -t timers:posix_timers -t timer:nanosleep

有关其他功能,请参阅脚本使用输出,使用 -h 选项查看。

自测超时

自测旨在快速运行,因此每个测试的默认超时时间为 45 秒。测试可以通过在其目录中添加设置文件并在那里将超时变量设置为测试配置的期望上限超时时间来覆盖默认超时时间。只有少数测试将超时时间覆盖为高于 45 秒的值,自测努力保持这种状态。自测中的超时不被视为致命错误,因为运行测试的系统可能会发生变化,这也可能会修改运行测试所需的预期时间。如果您可以控制将运行测试的系统,则可以在这些系统上配置测试运行程序,以在命令行中使用 -o--override-timeout 参数来使用更大或更低的超时时间。例如,要使用 165 秒而不是 1 秒,可以使用

$ ./run_kselftest.sh --override-timeout 165

您可以查看 TAP 输出以查看您是否遇到了超时。知道测试必须在特定时间内运行的测试运行程序可以选择将这些超时视为致命错误。

打包自测

在某些情况下,需要打包,例如当测试需要在不同的系统上运行时。要打包自测,请运行

$ make -C tools/testing/selftests gen_tar

这会在 INSTALL_PATH/kselftest-packages 目录中生成一个 tarball。默认情况下,使用 .gz 格式。可以通过指定 FORMAT make 变量来覆盖 tar 压缩格式。支持 tar 的 auto-compress 选项识别的任何值,例如

$ make -C tools/testing/selftests gen_tar FORMAT=.xz

make gen_tar 调用 make install,因此您可以使用它通过使用运行自测的子集部分中指定的变量来打包测试的子集

$ make -C tools/testing/selftests gen_tar TARGETS="size" FORMAT=.xz

贡献新的测试

通常,自测的规则是

  • 如果您不是 root 用户,请尽可能多地执行操作;

  • 不要花费太长时间;

  • 不要破坏任何架构上的构建,并且

  • 如果您的功能未配置,不要导致顶级的“make run_tests”失败。

  • 测试的输出必须符合 TAP 标准,以确保高质量的测试并捕获具有特定详细信息的失败/错误。kselftest.h 和 kselftest_harness.h 标头为输出测试结果提供了包装器。这些包装器应该用于通过、失败、退出和跳过消息。CI 系统可以轻松解析 TAP 输出消息以检测测试结果。

贡献新的测试 (详情)

  • 在您的 Makefile 中,通过包含 lib.mk 来使用其中的工具,而不是重复发明轮子。在包含 lib.mk 之前,根据需要指定标志和二进制生成标志。

    CFLAGS = $(KHDR_INCLUDES)
    TEST_GEN_PROGS := close_range_test
    include ../lib.mk
    
  • 如果编译期间生成此类二进制文件或文件,则使用 TEST_GEN_XXX。

    TEST_PROGS、TEST_GEN_PROGS 表示它是默认情况下测试的可执行文件。

    需要构建模块才能开始测试的测试应该使用 TEST_GEN_MODS_DIR。该变量将包含包含模块的目录的名称。

    需要自定义构建规则并防止使用通用构建规则的测试应该使用 TEST_CUSTOM_PROGS。

    TEST_PROGS 用于测试 shell 脚本。请确保 shell 脚本已设置其执行位。否则,lib.mk run_tests 将生成警告。

    TEST_CUSTOM_PROGS 和 TEST_PROGS 将由通用 run_tests 运行。

    TEST_PROGS_EXTENDED、TEST_GEN_PROGS_EXTENDED 表示它是默认情况下未测试的可执行文件。

    TEST_FILES、TEST_GEN_FILES 表示它是测试使用的文件。

    TEST_INCLUDES 类似于 TEST_FILES,它列出了在导出或安装测试时应包含的文件,但有以下区别

    • 保留指向其他目录中文件的符号链接

    • 当将文件复制到输出目录时,保留 tools/testing/selftests/ 下的路径部分

    TEST_INCLUDES 旨在列出自测层次结构其他目录中的依赖项。

  • 首先使用内核源代码和/或 git 存储库中的标头,然后使用系统标头。内核发行版的标头(而不是系统上发行版安装的标头)应作为主要关注点,以便能够查找回归。在 Makefile 中使用 KHDR_INCLUDES 来包含来自内核源代码的标头。

  • 如果测试需要启用特定的内核配置选项,请在测试目录中添加一个配置文件来启用它们。

    例如:tools/testing/selftests/android/config

  • 在测试目录中创建一个 .gitignore 文件,并将所有生成的对象添加到其中。

  • 在 selftests/Makefile 中的 TARGETS 中添加新的测试名称

    TARGETS += android
    
  • 所有更改都应通过

    kselftest-{all,install,clean,gen_tar}
    kselftest-{all,install,clean,gen_tar} O=abo_path
    kselftest-{all,install,clean,gen_tar} O=rel_path
    make -C tools/testing/selftests {all,install,clean,gen_tar}
    make -C tools/testing/selftests {all,install,clean,gen_tar} O=abs_path
    make -C tools/testing/selftests {all,install,clean,gen_tar} O=rel_path
    

测试模块

Kselftest 从用户空间测试内核。有时需要从内核内部进行测试,一种方法是创建一个测试模块。我们可以通过使用 shell 脚本测试运行器将模块绑定到 kselftest 框架。kselftest/module.sh 旨在方便此过程。还提供了一个头文件,以帮助编写用于 kselftest 的内核模块

  • tools/testing/selftests/kselftest_module.h

  • tools/testing/selftests/kselftest/module.sh

请注意,测试模块应使用 TAINT_TEST 污染内核。对于 tools/testing/ 目录中的模块,或者对于使用上面的 kselftest_module.h 头文件的模块,这将自动发生。否则,您需要将 MODULE_INFO(test, "Y") 添加到模块源代码中。通常,不加载模块的自测不应污染内核,但在加载非测试模块的情况下,可以通过写入 /proc/sys/kernel/tainted 从用户空间应用 TEST_TAINT。

如何使用

这里我们展示了创建测试模块并将其绑定到 kselftest 的典型步骤。我们以 lib/ 的 kselftest 为例。

  1. 创建测试模块

  2. 创建将运行(加载/卸载)模块的测试脚本,例如 tools/testing/selftests/lib/printf.sh

  3. 将行添加到配置文件,例如 tools/testing/selftests/lib/config

  4. 将测试脚本添加到 makefile,例如 tools/testing/selftests/lib/Makefile

  5. 验证它是否有效

# Assumes you have booted a fresh build of this kernel tree
cd /path/to/linux/tree
make kselftest-merge
make modules
sudo make modules_install
make TARGETS=lib kselftest

示例模块

一个最基本的测试模块可能如下所示

// SPDX-License-Identifier: GPL-2.0+

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include "../tools/testing/selftests/kselftest_module.h"

KSTM_MODULE_GLOBALS();

/*
 * Kernel module for testing the foobinator
 */

static int __init test_function()
{
        ...
}

static void __init selftest(void)
{
        KSTM_CHECK_ZERO(do_test_case("", 0));
}

KSTM_MODULE_LOADERS(test_foo);
MODULE_AUTHOR("John Developer <[email protected]>");
MODULE_LICENSE("GPL");
MODULE_INFO(test, "Y");

示例测试脚本

#!/bin/bash
# SPDX-License-Identifier: GPL-2.0+
$(dirname $0)/../kselftest/module.sh "foo" test_foo

测试框架

kselftest_harness.h 文件包含构建测试的有用助手。测试框架用于用户空间测试,有关内核空间测试,请参见上面的测试模块

可以使用 tools/testing/selftests/seccomp/seccomp_bpf.c 中的测试作为示例。

示例

#include "../kselftest_harness.h"

TEST(standalone_test) {
  do_some_stuff;
  EXPECT_GT(10, stuff) {
     stuff_state_t state;
     enumerate_stuff_state(&state);
     TH_LOG("expectation failed with state: %s", state.msg);
  }
  more_stuff;
  ASSERT_NE(some_stuff, NULL) TH_LOG("how did it happen?!");
  last_stuff;
  EXPECT_EQ(0, last_stuff);
}

FIXTURE(my_fixture) {
  mytype_t *data;
  int awesomeness_level;
};
FIXTURE_SETUP(my_fixture) {
  self->data = mytype_new();
  ASSERT_NE(NULL, self->data);
}
FIXTURE_TEARDOWN(my_fixture) {
  mytype_free(self->data);
}
TEST_F(my_fixture, data_is_good) {
  EXPECT_EQ(1, is_my_data_good(self->data));
}

TEST_HARNESS_MAIN

助手

TH_LOG

TH_LOG (fmt, ...)

参数

fmt

格式化字符串

...

可选参数

描述

TH_LOG(format, ...)

可用于测试的可选调试日志记录函数。可以通过定义 TH_LOG_ENABLED 来启用或禁用日志记录。例如,#define TH_LOG_ENABLED 1

如果未提供任何定义,则默认启用日志记录。

TEST

TEST (test_name)

定义测试函数并创建注册存根

参数

test_name

测试名称

描述

TEST(name) { implementation }

按名称定义测试。名称必须唯一,并且测试不得并行运行。包含块的实现是一个函数,其作用域应如此对待。可以使用简单的“return;”语句提前返回。

EXPECT_* 和 ASSERT_* 在 TEST() { } 上下文中有效。

TEST_SIGNAL

TEST_SIGNAL (test_name, signal)

参数

test_name

测试名称

signal

信号编号

描述

TEST_SIGNAL(name, signal) { implementation }

按名称和预期终止信号定义测试。名称必须唯一,并且测试不得并行运行。包含块的实现是一个函数,其作用域应如此对待。可以使用简单的“return;”语句提前返回。

EXPECT_* 和 ASSERT_* 在 TEST() { } 上下文中有效。

FIXTURE_DATA

FIXTURE_DATA (datatype_name)

包装结构体名称,以便我们少传递一个参数

参数

datatype_name

数据类型名称

描述

FIXTURE_DATA(datatype_name)

几乎总是,您只需要 FIXTURE() (请参见下文)。当需要 fixture 数据的类型时,可以使用此调用。通常,除非直接将 *self* 传递给助手,否则不需要这样做。

FIXTURE

FIXTURE (fixture_name)

每个 fixture 调用一次以设置数据并注册

参数

fixture_name

fixture 名称

描述

FIXTURE(fixture_name) {
  type property1;
  ...
};

将提供给 TEST_F() 定义的测试的数据定义为 *self*。应使用 FIXTURE_SETUP()FIXTURE_TEARDOWN() 对其进行填充和清理。

FIXTURE_SETUP

FIXTURE_SETUP (fixture_name)

为 fixture 准备 setup 函数。包含 *_metadata*,以便 EXPECT_*、ASSERT_* 等可以正常工作。

参数

fixture_name

fixture 名称

描述

FIXTURE_SETUP(fixture_name) { implementation }

为 fixture 填充所需的“setup”函数。使用 FIXTURE_DATA() 定义的数据类型的实例将作为 *self* 公开给实现。

ASSERT_* 在此上下文中有效,并且将抢占任何依赖 fixture 测试的执行。

可以使用简单的 “return;” 语句提前返回。

FIXTURE_TEARDOWN

FIXTURE_TEARDOWN (fixture_name)

参数

fixture_name

fixture 名称

描述

包含 *_metadata*,以便 EXPECT_*、ASSERT_* 等可以正常工作。

FIXTURE_TEARDOWN(fixture_name) { implementation }

为 fixture 填充所需的 “teardown” 函数。使用 FIXTURE_DATA() 定义的数据类型的实例将作为 *self* 公开给实现以进行清理。

可以使用简单的 “return;” 语句提前返回。

FIXTURE_VARIANT

FIXTURE_VARIANT (fixture_name)

(可选)每个 fixture 调用一次以声明 fixture 变体

参数

fixture_name

fixture 名称

描述

FIXTURE_VARIANT(fixture_name) {
  type property1;
  ...
};

定义提供给 FIXTURE_SETUP()TEST_F() 和 FIXTURE_TEARDOWN 的常量参数类型为 *variant*。变体允许使用不同的参数运行相同的测试。

FIXTURE_VARIANT_ADD

FIXTURE_VARIANT_ADD (fixture_name, variant_name)

每个 fixture 变体调用一次,以设置和注册数据

参数

fixture_name

fixture 名称

variant_name

参数集的名称

描述

FIXTURE_VARIANT_ADD(fixture_name, variant_name) {
  .property1 = val1,
  ...
};

定义测试 fixture 的变体,作为 *variant* 提供给 FIXTURE_SETUP()TEST_F()。每个 fixture 的测试将为每个变体运行一次。

TEST_F

TEST_F (fixture_name, test_name)

为基于 fixture 的测试用例发出测试注册和助手

参数

fixture_name

fixture 名称

test_name

测试名称

描述

TEST_F(fixture, name) { implementation }

定义一个依赖于 fixture 的测试(例如,属于测试用例的一部分)。与 TEST() 非常相似,不同之处在于 *self* 是 fixture 数据类型的设置实例,用于实现。

_metadata 对象与所有潜在的 fork 进程共享(MAP_SHARED),这使它们可以使用 EXCEPT_*() 和 ASSERT_*()。

仅当使用 FIXTURE_TEARDOWN_PARENT() 而不是 FIXTURE_TEARDOWN() 时,*self* 对象才与潜在的 fork 进程共享。

TEST_HARNESS_MAIN

TEST_HARNESS_MAIN

运行测试框架的简单包装器

描述

TEST_HARNESS_MAIN

使用一次以将 main() 附加到测试文件。

运算符

TEST()TEST_F() 中使用的运算符。ASSERT_* 调用将立即停止测试执行。EXPECT_* 调用将发出失败警告、记录它并继续。

ASSERT_EQ

ASSERT_EQ (expected, seen)

参数

expected

预期值

seen

测量值

描述

ASSERT_EQ(预期值, 测量值): 预期值 == 测量值

ASSERT_NE

ASSERT_NE (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

ASSERT_NE(预期值, 测量值): 预期值 != 测量值

ASSERT_LT

ASSERT_LT (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

ASSERT_LT(预期值, 测量值): 预期值 < 测量值

ASSERT_LE

ASSERT_LE (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

ASSERT_LE(预期值, 测量值): 预期值 <= 测量值

ASSERT_GT

ASSERT_GT (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

ASSERT_GT(预期值, 测量值): 预期值 > 测量值

ASSERT_GE

ASSERT_GE (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

ASSERT_GE(预期值, 测量值): 预期值 >= 测量值

ASSERT_NULL

ASSERT_NULL (实际值)

参数

seen

测量值

描述

ASSERT_NULL(测量值): NULL == 测量值

ASSERT_TRUE

ASSERT_TRUE (实际值)

参数

seen

测量值

描述

ASSERT_TRUE(测量值): 测量值 != 0

ASSERT_FALSE

ASSERT_FALSE (实际值)

参数

seen

测量值

描述

ASSERT_FALSE(测量值): 测量值 == 0

ASSERT_STREQ

ASSERT_STREQ (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

ASSERT_STREQ(预期值, 测量值): !strcmp(预期值, 测量值)

ASSERT_STRNE

ASSERT_STRNE (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

ASSERT_STRNE(预期值, 测量值): strcmp(预期值, 测量值)

EXPECT_EQ

EXPECT_EQ (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

EXPECT_EQ(预期值, 测量值): 预期值 == 测量值

EXPECT_NE

EXPECT_NE (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

EXPECT_NE(预期值, 测量值): 预期值 != 测量值

EXPECT_LT

EXPECT_LT (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

EXPECT_LT(预期值, 测量值): 预期值 < 测量值

EXPECT_LE

EXPECT_LE (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

EXPECT_LE(预期值, 测量值): 预期值 <= 测量值

EXPECT_GT

EXPECT_GT (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

EXPECT_GT(预期值, 测量值): 预期值 > 测量值

EXPECT_GE

EXPECT_GE (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

EXPECT_GE(预期值, 测量值): 预期值 >= 测量值

EXPECT_NULL

EXPECT_NULL (实际值)

参数

seen

测量值

描述

EXPECT_NULL(测量值): NULL == 测量值

EXPECT_TRUE

EXPECT_TRUE (实际值)

参数

seen

测量值

描述

EXPECT_TRUE(测量值): 0 != 测量值

EXPECT_FALSE

EXPECT_FALSE (实际值)

参数

seen

测量值

描述

EXPECT_FALSE(测量值): 0 == 测量值

EXPECT_STREQ

EXPECT_STREQ (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

EXPECT_STREQ(预期值, 测量值): !strcmp(预期值, 测量值)

EXPECT_STRNE

EXPECT_STRNE (预期值, 实际值)

参数

expected

预期值

seen

测量值

描述

EXPECT_STRNE(预期值, 测量值): strcmp(预期值, 测量值)