Linux 内核 Open Firmware Devicetree 单元测试指南:动态挂载与卸载测试数据
发布时间:2026/9/16 23:33:05来源:尧图网络
Linux 内核 Open Firmware Devicetree 单元测试指南动态挂载与卸载测试数据【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux导读本文围绕 Linux 内核源码树中的 Documentation/devicetree/of_unittest.rst 展开系统讲解 OFOpen FirmwareDevicetree 单元测试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 \ : textbegin开始标记在触发预期 warning/error之前打印EXPECT / : textend结束标记在触发预期 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.cEXPECT_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_expectEXPECT 消息会让控制台输出极度嘈杂、难以阅读。为此内核提供了 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 \ : textbegin与EXPECT / : textend成对匹配如果期望的消息没有出现会报告** 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.dtsiphandle、tests-match.dtsi匹配、tests-platform.dtsi平台设备、tests-lifecycle.dtsi生命周期、tests-overlay.dtsioverlay。此外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)用 DTCDevice 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 的场景提供了额外的构建期测试覆盖而故意含错的 overlayoverlay_bad_*被注释排除在静态测试之外drivers/of/unittest-data/Makefile。4. 测试数据挂载向 live tree 动态附加节点4.1 前置知识非扁平化设备树结构内核运行时的设备树live tree由相互连接的device_node以树形结构组成。文档给出的核心结构成员如下定义见 include/linux/of.hstruct device_node { ... struct device_node *parent; // 指向父节点 struct device_node *child; // 指向第一个子节点 struct device_node *sibling; // 指向下一个兄弟节点 ... };仅考虑 child 与 sibling 指针时一台机器的非扁平化设备树呈现如下通用结构Figure 1parent指针用于反向遍历——同一层的 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_rootlive 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 2root (/) | 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新节点取代当前 childtest-child0成为 childtest-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/Makefiletestcases.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.dtsotests-*.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),仅供参考
网站建设高端定制企业官网