Linux 内核 Open Firmware Devicetree 单元测试指南:动态挂载与卸载测试数据
2026/9/16 23:33:04 网站建设 项目流程

Linux 内核 Open Firmware Devicetree 单元测试指南:动态挂载与卸载测试数据

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

导读:本文围绕 Linux 内核源码树中的 Documentation/devicetree/of_unittest.rst 展开,系统讲解 OF(Open Firmware)Devicetree 单元测试(unittest)的机制与原理——测试数据如何以“与机器架构无关”的方式在启动时动态挂载到 live tree(活动设备树),以及测试结束后如何拆卸清理。读完本文,你将掌握testcases.dtso测试数据从编译到链接、再到unittest_data_add()运行时挂载、最终由selftest_data_remove()清理的完整调用链,并学会用EXPECT机制与scripts/dtc/of_unittest_expect过滤工具解读海量测试日志。

1. 引言:OF unittest 是什么,为什么需要它

设备树(Device Tree)是 Linux 内核描述硬件配置的标准数据结构。驱动开发者通过include/linux/of.h提供的接口,从“非扁平化”(unflattened)的设备树数据结构中获取设备信息——例如查找节点、读取属性、解析中断与 GPIO 等。这套接口被绝大多数设备驱动在各种场景下使用,其正确性至关重要。

OF Selftest(即 OF unittest)正是为验证这套接口而设计的内核级自测框架:它由 drivers/of/unittest.c 实现,在启动阶段一次性执行一组针对设备树基础设施的测试用例,并把结果打印到控制台。其核心设计目标之一,是让测试数据的挂载方式独立于机器的具体架构——不依赖某个特定平台如何构建设备树,而是把测试数据编译进内核镜像,在运行时动态附加到机器现有的设备树(live tree)上。

阅读本文前,建议先了解设备树的基本概念:

  • Documentation/devicetree/usage-model.rst:设备树使用模型;
  • devicetree.org 的 Device_Tree_Usage 文档(设备树用法说明,可作为背景阅读)。

在 drivers/of/Kconfig 中,CONFIG_OF_UNITTEST被描述为 "Device Tree runtime unit tests"(设备树运行时单元测试),其帮助文本明确说明:该选项会在启动时执行一次测试,把结果 dump 到控制台;它只应用于开发内核——测试会以TAINT_TEST污染内核、打印大量 ERROR/WARNING 与栈回溯,甚至可能让设备树处于损坏状态,因此帮助文本明确写道"If unsure, say N here. This option is not safe to enable."(如果不确定,请选 N,该选项不安全)。

2. 详细输出与 EXPECT 机制:区分“预期错误”与“真实故障”

2.1 问题背景

unittest 检测到问题时会在控制台打印 warning 或 error 消息。但关键难点在于:unittest 本身会故意构造“坏数据”(例如过短的 interrupt 描述、缺失的#phandle-cells属性、非法的 phandle 引用等),从而触发内核其他模块(如OF:前缀的解析代码)产生 warning/error 消息。这就带来一个困惑:控制台上刷出的一堆报错,究竟是测试预期会触发的正常现象,还是与 unittest 无关的真实内核故障

2.2 EXPECT 成对标记

为解决上述歧义,unittest 引入了 EXPECT 成对标记:

  • EXPECT \ : text(begin,开始标记):在触发预期 warning/error之前打印;
  • EXPECT / : text(end,结束标记):在触发预期 warning/error之后打印。

在 drivers/of/unittest.c 中可以找到宏定义:

#define EXPECT_BEGIN(level, fmt, ...) \ printk(level pr_fmt("EXPECT \\ : ") fmt, ##__VA_ARGS__) #define EXPECT_END(level, fmt, ...) \ printk(level pr_fmt("EXPECT / : ") fmt, ##__VA_ARGS__)

实际用例的典型形态如下(摘自 phandle 测试,drivers/of/unittest.c):

EXPECT_BEGIN(KERN_INFO, "OF: /testcase-data/phandle-tests/consumer-a: could not get #phandle-cells-missing for /testcase-data/phandle-tests/provider1"); rc = of_parse_phandle_with_args(np, "phandle-list", "#phandle-cells-missing", 0, &args); EXPECT_END(KERN_INFO, "OF: /testcase-data/phandle-tests/consumer-a: could not get #phandle-cells-missing for /testcase-data/phandle-tests/provider1"); unittest(rc == -EINVAL, "expected:%i got:%i\n", -EINVAL, rc);

这里故意传入一个不存在的属性名#phandle-cells-missing,预期of_parse_phandle_with_args()返回-EINVAL并打印一条OF:错误消息——该消息被EXPECT_BEGIN/EXPECT_END包裹,明确声明“这条错误是预期的”。

2.3 过滤工具:scripts/dtc/of_unittest_expect

EXPECT 消息会让控制台输出极度嘈杂、难以阅读。为此内核提供了 Perl 脚本 scripts/dtc/of_unittest_expect 来过滤噪音并高亮“预期消息与实际触发消息”之间的不匹配。

该脚本的用法(可通过scripts/dtc/of_unittest_expect --help查看完整帮助):

scripts/dtc/of_unittest_expect CONSOLE_LOG

支持的选项包括:

选项含义
-h/--help打印用法
--hide-expect抑制(不输出)EXPECT 行的内容
--line-num显示 CONSOLE_LOG 中的行号
--no-expect-stats不输出 EXPECT 统计信息
--no-strip-ts不剥离行首的内核时间戳
--verbose不抑制 EXPECT begin/end 行
--version打印脚本版本

处理流程要点(与脚本源码 scripts/dtc/of_unittest_expect 一一对应):

  • 脚本按### dt-test ###前缀识别 unittest 输出,其中EXPECT \ : text(begin)与EXPECT / : text(end)成对匹配;
  • 如果期望的消息没有出现,会报告** WARNING - not found;如果 begin 与 end 文本不匹配、或 end 缺少对应 begin、或嵌套顺序错误,都会显式报错;
  • 每条输出行带前缀:ok(匹配上 EXPECT 对)、**(警告/错误)、->(测试开始/结束)、>>(unittest FAIL)、(普通行);
  • 默认剥离行首时间戳([ 0.123456]格式),可用--no-strip-ts关闭;
  • EXPECT 文本中支持三类特殊通配模式:
    • <<int>>:匹配[+-]*[0-9]+(带符号整数);
    • <<hex>>:匹配(0x)*[0-9a-f]+(十六进制数);
    • <<all>>:匹配到行尾的任意内容;
  • 最后输出 EXPECT 统计:期望出现但未找到的(EXPECT not found)、出现但不应出现的(EXPECT_NOT found)、缺失的 begin/end、unittest FAIL 数等。

CONFIG_OF_UNITTEST的帮助文本也建议了标准操作流程:捕获控制台输出或使用dmesg把输出保存到文件,再交给scripts/dtc/of_unittest_expect处理,以降低噪音、检验预期输出是否存在并汇总结果。

3. 测试数据:从 DT 源文件到内核镜像的构建流水线

3.1 测试数据源文件

测试数据的源头是设备树源文件(.dtso):

  • 主文件 drivers/of/unittest-data/testcases.dtso:包含执行 drivers/of/unittest.c 中自动化单元测试所需的测试数据;
  • drivers/of/unittest-data/tests-*.dtsi:一组被testcases.dtso通过#include引入的 DTS include 文件,按主题划分,例如tests-address.dtsi(地址解析)、tests-interrupts.dtsi(中断)、tests-phandle.dtsi(phandle)、tests-match.dtsi(匹配)、tests-platform.dtsi(平台设备)、tests-lifecycle.dtsi(生命周期)、tests-overlay.dtsi(overlay)。

此外,testcases.dtso还在/根节点下定义了一个带注释的“故意出错”的测试节点(见 drivers/of/unittest-data/testcases.dtso),例如testcase-device2interrupts = <1>;被注释标注为"invalid specifier - too short"(非法描述符——太短)。文件注释说明:这类故意产生错误的测试数据放在testcases.dtso而非testcases_common.dtsi中,是为了让静态 overlay 应用测试不包含这些错误。

3.2 编译链接三步走

当内核以CONFIG_OF_UNITTEST构建时,drivers/of/unittest-data/Makefile 中的obj-y += testcases.dtbo.o会触发如下三步流水线:

第一步:dtso → dtbo(扁平化二进制)

$(obj)/%.dtbo: $(src)/%.dtso $(DTC) FORCE $(call if_changed_dep,dtc)

用 DTC(Device Tree Compiler)把testcases.dtso编译为二进制 blobtestcases.dtbo,即扁平化设备树(flattened DT / FDT)。

第二步:dtbo → dtbo.S(包装为汇编)

$(obj)/%.dtbo.S: $(obj)/%.dtbo FORCE $(call if_changed,wrap_S_dtb)

把二进制 blob 包装成汇编文件testcases.dtbo.S

第三步:汇编 → 目标文件 → 链接进内核镜像

汇编文件被编译为对象文件testcases.dtbo.o,并链接进内核镜像。链接后,测试数据 blob 的边界由两个内核符号标识:

__dtb_testcases_begin - 测试数据 blob 起始地址 __dtb_testcases_end - 测试数据 blob 结束地址

从源码看,这两个符号在 drivers/of/unittest.c 中被引用,注释明确指出它们由scripts/Makefile.dtbs中的cmd_wrap_S_dtbo规则“神奇地”创建:

extern uint8_t __dtbo_testcases_begin[]; extern uint8_t __dtbo_testcases_end[]; const int size = __dtbo_testcases_end - __dtbo_testcases_begin;

3.3 Makefile 中的相关细节

drivers/of/unittest-data/Makefile 还展示了几个值得注意的点:

  • testcases.dtbo外,所有 overlay 测试数据(overlay_*.dtbo)都在CONFIG_OF_OVERLAY下编译;
  • 通过DTC_FLAGS_testcases += -@等为 overlay/testcases 开启__symbols__节点生成(-@标志),这是 overlay 与 phandle 解析所必需的;
  • DTC_FLAGS_testcases += -Wno-interrupts_property ...用于抑制 DTC 对故意错误数据的告警;
  • Makefile 还实现了“静态 overlay 应用测试”:用fdtoverlay在构建期把apply_static_overlay_1/2中的 overlay 应用到static_base_1.dtb/static_base_2.dtb上生成static_test_1.dtb/static_test_2.dtb——如果fdtoverlay检测到错误,内核构建直接失败。这为不执行 unittest 的场景提供了额外的构建期测试覆盖;而故意含错的 overlay(overlay_bad_*)被注释排除在静态测试之外(drivers/of/unittest-data/Makefile)。

4. 测试数据挂载:向 live tree 动态附加节点

4.1 前置知识:非扁平化设备树结构

内核运行时的设备树(live tree)由相互连接的device_node以树形结构组成。文档给出的核心结构成员如下(定义见 include/linux/of.h):

struct device_node { ... struct device_node *parent; // 指向父节点 struct device_node *child; // 指向第一个子节点 struct device_node *sibling; // 指向下一个兄弟节点 ... };

仅考虑 child 与 sibling 指针时,一台机器的非扁平化设备树呈现如下通用结构(Figure 1,parent指针用于反向遍历——同一层的 child 及所有 sibling 的 parent 都指向共同父节点):

root ('/') | child1 -> sibling2 -> sibling3 -> sibling4 -> null | | | | | | | null | | child31 -> sibling32 -> null | | | | | | null null | | | child21 -> sibling22 -> sibling23 -> null | | | | | null null null | child11 -> sibling12 -> sibling13 -> sibling14 -> null | | | | | | | null | | | null null child131 -> null | null

Figure 1:非扁平化设备树的通用结构

4.2 unittest_data_add():挂载流程

执行 OF unittest 之前,需要把测试数据附加到机器现有的设备树(如果存在)。文档中描述的函数名为selftest_data_add(),而当前源码中对应实现为unittest_data_add()(见 drivers/of/unittest.c),流程完全吻合:

第一步:定位并复制扁平化数据。通过内核符号__dtbo_testcases_begin[]__dtbo_testcases_end[]计算测试数据 blob 大小,用kmalloc分配内存(含FDT_ALIGN_SIZE对齐余量),PTR_ALIGN对齐后memcpy复制一份。若 blob 为空(size == 0)则打印警告并返回-ENODATA

第二步:非扁平化。调用of_fdt_unflatten_tree(unittest_data_align, NULL, &unittest_data_node)把扁平化 blob 还原为device_node树。失败或结果为空时返回-ENODATA

第三步:解析 phandle。of_overlay_mutex_lock()保护下调用of_resolve_phandles()解析测试数据内部的 phandle 引用(该锁通常包住 phandle 解析过程)。

第四步:挂载到 live tree。of_root(live tree 根)不存在则返回-ENODEV。随后遍历unittest_data_node的子节点,把每个节点的parent设为of_root,再调用attach_node_and_children()递归挂载整棵子树。

值得注意的是,unittest_data_add()在挂载前后用EXPECT_BEGIN/EXPECT_END包裹了一个预期消息:

EXPECT_BEGIN(KERN_INFO, "Duplicate name in testcase-data, renamed to \"duplicate-name#1\""); ... EXPECT_END(KERN_INFO, "Duplicate name in testcase-data, renamed to \"duplicate-name#1\"");

这说明测试数据中故意包含重名节点,内核会把重名节点改名为duplicate-name#1并打印信息——这同样是一个“预期消息”,可通过第 2 节的过滤脚本校验其是否如约出现。

4.3 attach_node_and_children() 与 of_attach_node():插入语义

attach_node_and_children()的实现(drivers/of/unittest.c)逻辑如下:

  1. kasprintf(GFP_KERNEL, "%pOF", np)生成节点的完整路径full_name
  2. 调用of_find_node_by_path(full_name)查找 live tree 中是否已存在同名节点;
  3. 若已存在(dup):不挂载节点本身,而是调用update_node_properties(np, dup)把新节点的属性合并更新到 live tree 已有节点上,然后返回(注意:由于np->child此时还未遍历,children 不会被挂载——源码注释明确提示这是个"misleading function name",对当前测试树之所以可行,是因为这类重复节点没有子节点);
  4. 若不存在:先暂存child = np->child并把np->child置空,然后在of_mutex保护下调用__of_attach_node_sysfs(np)挂载节点(attach_node_and_children内部直接调用的是带__前缀的内部函数),最后递归地对每个 child 调用attach_node_and_children()

底层__of_attach_node()(drivers/of/dynamic.c)在devtree_lock保护下完成关键插入操作:

np->child = NULL; np->sibling = np->parent->child; // 新节点的 sibling 指向父节点当前第一个 child np->parent->child = np; // 新节点成为父节点的新 child(把旧 child 挤成 sibling) of_node_clear_flag(np, OF_DETACHED);

而对外接口of_attach_node()(drivers/of/dynamic.c)在其外面再加一层of_mutexof_reconfig_notify(OF_RECONFIG_ATTACH_NODE, &rd)重配置通知。新节点总是被插到父节点 child 链表的头部——这正是下面 Figure 2→Figure 3 顺序颠倒的原因。

4.4 挂载示例:从 Figure 2 到 Figure 3

文档用具体例子演示插入语义。待挂载的测试数据树(Figure 2):

root ('/') | testcase-data | test-child0 -> test-sibling1 -> test-sibling2 -> test-sibling3 -> null | | | | test-child01 null null null

Figure 2:待挂载到 live tree 的示例测试数据树

由于 live tree 已存在,根节点/无需挂载,其余节点逐个调用of_attach_node()

  1. 挂载testcase-data:成为 root 的 child;
  2. 挂载test-child0:成为testcase-data的 child;
  3. 挂载test-sibling1新节点取代当前 child(test-child0)成为 child,test-child0 被挤成 sibling

以此类推,最终挂载完test-sibling3后,live tree 变为(Figure 3,文档同时给出了 child/sibling 两个视角):

root ('/') | testcase-data -> child1 -> sibling2 -> sibling3 -> sibling4 -> null | | | | | (...) | | | null | | child31 -> sibling32 -> null | | | | | | null null | | | child21 -> sibling22 -> sibling23 -> null | | | | | null null null | child11 -> sibling12 -> sibling13 -> sibling14 -> null | | | | null null | null | child131 -> null | null ------------------------------------------------------------------------- root ('/') | testcase-data -> child1 -> sibling2 -> sibling3 -> sibling4 -> null | | | | | | (...) (...) (...) null | test-sibling3 -> test-sibling2 -> test-sibling1 -> test-child0 -> null | | | | null null null test-child01

Figure 3:挂载 testcase-data 后的 live tree 结构

细心的读者会发现:test-child0从最初的第一个 child 变成了最后一个 sibling——因为每次挂载新节点都会把它插到链表头部,把原有 child 依次往后挤成 sibling,这正是__of_attach_node()np->sibling = np->parent->child; np->parent->child = np;两行代码的直接后果。

4.5 重复节点的处理:update_node_properties()

如果发现重复节点(即 live tree 中已存在full_name相同的节点),则不挂载该节点,而是调用update_node_properties()把其属性更新到 live tree 已有节点上(drivers/of/unittest.c)。从函数注释可知其职责:

  • np的属性(逐个遍历np->properties)添加/更新到重复节点dup上;
  • 同时把np的子节点的 parent 更新为dup

挂载完成后,unittest_data_add()会调用retain_and_null_ptr()保留已复制到 live tree 的数据副本,避免释放。

5. 测试数据拆卸:从 live tree 摘除节点

测试用例执行完毕后,文档描述的selftest_data_remove()负责移除最初附加的设备节点——先摘除叶子节点,再沿树向上逐级摘除父节点,最终移除整棵树。它调用detach_node_and_children(),后者使用of_detach_node()把节点从 live tree 中分离。

底层__of_detach_node()(drivers/of/dynamic.c)在devtree_lock保护下完成链表的摘除逻辑,正是文档所说的“二选一”:

parent = np->parent; if (parent->child == np) parent->child = np->sibling; // 情况一:np 是父节点的第一个 child else { // 情况二:遍历找到 np 的前一个兄弟 prevsib for (prevsib = np->parent->child; prevsib->sibling != np; prevsib = prevsib->sibling) ; prevsib->sibling = np->sibling; // 前一个兄弟跳过 np,直连 np 的 sibling } of_node_set_flag(np, OF_DETACHED); __of_phandle_cache_inv_entry(np->phandle); // 使 phandle 缓存项失效

也就是说:

  • 若被摘除节点是其父节点的第一个 child,则把父节点的child指针直接更新为它的 sibling;
  • 否则,把它的前一个 sibling 的sibling指针指向它的 sibling,从而把它从兄弟链表中“跳过”。

摘除后节点被标记为OF_DETACHED,其 phandle 缓存项也会失效(防止of_find_node_by_phandle()竞态)。对外接口of_detach_node()(drivers/of/dynamic.c)同样在of_mutex保护下执行,并触发OF_RECONFIG_DETACH_NODE重配置通知。

补充:unittest 中另一类基于 changeset 的动态增删测试(如 drivers/of/unittest.c 中的of_changeset_attach_node()/of_changeset_detach_node()/of_changeset_revert()),同样以__of_attach_node()/__of_detach_node()为底层原语,说明这套挂载/摘除机制是内核动态设备树操作(overlay、changeset、unittest)共享的基础设施。

6. 如何运行与解读测试结果

6.1 开启与运行

  1. 在开发内核的配置中开启CONFIG_OF_UNITTEST(依赖OF_EARLY_FLATTREE,并自动select IRQ_DOMAINOF_RESOLVEOF_DYNAMIC等,见 drivers/of/Kconfig);
  2. 重新编译内核。构建时unittest-data/子目录会在CONFIG_OF_UNITTEST下被编译(见 drivers/of/Makefile 与 drivers/of/Makefile),testcases.dtbo.o及一组overlay_*.dtbo.o被链接进内核;
  3. 启动内核,测试在启动阶段自动执行一次,结果打印到控制台,形如### dt-test ### start of unittest ...### dt-test ### end of unittest - N passed, M failed

注意(务必遵守)CONFIG_OF_UNITTEST只应用于开发内核。测试会以TAINT_TEST污染内核、打印大量 ERROR/WARNING 与栈回溯,并可能让设备树处于损坏状态——切勿在生产/引导关键环境中开启。

6.2 解读输出

标准流程是:把控制台输出或dmesg结果保存为文件,然后运行:

scripts/dtc/of_unittest_expect CONSOLE_LOG

脚本会剥离时间戳、把命中 EXPECT 对的真实消息标记为ok、把测试失败标记为>>、把预期消息缺失/不匹配等情况标记为**,最后给出统计汇总。这样,控制台上原本“满屏报错”的日志,就能被快速区分为“预期触发的消息”与“真正的测试失败/内核问题”。

7. 总结

OF unittest 通过一条精心设计的链路实现了“架构无关”的设备树接口测试:

阶段关键动作代码/文件位置
数据准备testcases.dtso+tests-*.dtsi描述测试树drivers/of/unittest-data/testcases.dtso
构建dtso→dtbo→dtbo.S→dtbo.o,链接进内核drivers/of/unittest-data/Makefile
定位数据__dtbo_testcases_begin/end符号drivers/of/unittest.c
运行时挂载unittest_data_add()of_fdt_unflatten_tree()attach_node_and_children()drivers/of/unittest.c
节点插入__of_attach_node():插到 child 链表头部drivers/of/dynamic.c
重复处理update_node_properties()合并属性drivers/of/unittest.c
运行时拆卸detach_node_and_children()of_detach_node()drivers/of/dynamic.c
结果解读EXPECT 标记 + 过滤脚本scripts/dtc/of_unittest_expect

这套机制不仅是 unittest 自身的基础,也折射出内核动态设备树子系统的通用原语:of_attach_node()/of_detach_node()同时服务于 overlay 与 changeset。理解测试数据的挂载/拆卸语义(插链表头导致顺序反转、重复节点属性合并、先叶子后根的拆卸顺序),也就理解了内核如何在不重启、不依赖具体平台的情况下,安全地在运行时重组设备树。

【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询