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-device2的interrupts = <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 | nullFigure 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)逻辑如下:
- 用
kasprintf(GFP_KERNEL, "%pOF", np)生成节点的完整路径full_name; - 调用
of_find_node_by_path(full_name)查找 live tree 中是否已存在同名节点; - 若已存在(dup):不挂载节点本身,而是调用
update_node_properties(np, dup)把新节点的属性合并更新到 live tree 已有节点上,然后返回(注意:由于np->child此时还未遍历,children 不会被挂载——源码注释明确提示这是个"misleading function name",对当前测试树之所以可行,是因为这类重复节点没有子节点); - 若不存在:先暂存
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_mutex与of_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 nullFigure 2:待挂载到 live tree 的示例测试数据树
由于 live tree 已存在,根节点/无需挂载,其余节点逐个调用of_attach_node():
- 挂载
testcase-data:成为 root 的 child; - 挂载
test-child0:成为testcase-data的 child; - 挂载
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-child01Figure 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 开启与运行
- 在开发内核的配置中开启
CONFIG_OF_UNITTEST(依赖OF_EARLY_FLATTREE,并自动select IRQ_DOMAIN、OF_RESOLVE、OF_DYNAMIC等,见 drivers/of/Kconfig); - 重新编译内核。构建时
unittest-data/子目录会在CONFIG_OF_UNITTEST下被编译(见 drivers/of/Makefile 与 drivers/of/Makefile),testcases.dtbo.o及一组overlay_*.dtbo.o被链接进内核; - 启动内核,测试在启动阶段自动执行一次,结果打印到控制台,形如
### 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),仅供参考