Zephyr Instrumentation 使用说明

概述 链接到标题

Zephyr Instrumentation 子系统利用 GCC -finstrument-functions 自动在每个函数入口/出口插桩,无需手动埋点即可实现函数调用图追踪与性能剖析。提供两种模式:

  • Callgraph 模式用于时序追踪
  • Statistical 模式用于热点分析

可以为 Instrumentation 指定启动和停止函数来限定检测窗口,默认以 main 为边界;配合 retained memory 可实现重启动态指定触发函数。提供 zaru.py 工具可以通过 UART 拿到 trace/profile 数据,trace 数据能够直接放到 Perfetto 可视化。

与 Tracing 子系统不一样,Tracing 关注内核对象操作,通过在内核对象内显式的插桩代码来完成数据收集。Instrumentation 则是通过编译器对所有的函数进行编译级别的插桩,消耗会更高,但数据会更丰富。

维度 Instrumentation Tracing
层级 底层(函数级) RTOS 感知(事件级)
机制 编译器自动插桩 (-finstrument-functions) 手动调用结构化 API
适用场景 代码流程分析、性能瓶颈定位 线程切换、信号量等 RTOS 事件
开销 较高(所有函数) 较低(仅标记点)

使用配置 链接到标题

配置项 默认值 说明 典型取值示例
CONFIG_INSTRUMENTATION n 启用 Instrumentation 子系统 y
CONFIG_INSTRUMENTATION_MODE_CALLGRAPH n 启用调用图追踪模式 y
CONFIG_INSTRUMENTATION_MODE_STATISTICAL n 启用统计剖析模式 y
CONFIG_INSTRUMENTATION_MODE_CALLGRAPH_TRACE_BUFFER_SIZE 4096 调用图环形缓冲区大小(条目数) 8192 / 16384
CONFIG_INSTRUMENTATION_MODE_STATISTICAL_MAX_NUM_FUNC 256 统计模式最大追踪函数数 512 / 1024
CONFIG_INSTRUMENTATION_TRIGGER_FUNCTION main 触发函数(开始记录) get_sem_and_exec_function
CONFIG_INSTRUMENTATION_STOPPER_FUNCTION main 停止函数(结束记录) main
CONFIG_INSTRUMENTATION_DYNAMIC_TRIGGER n 运行时动态配置触发/停止函数 y
CONFIG_INSTRUMENTATION_EXCLUDE_FUNCTION_LIST "" 排除函数列表(逗号分隔) k_busy_wait,memcpy
CONFIG_INSTRUMENTATION_EXCLUDE_FILE_LIST "" 排除文件列表(逗号分隔) log_minimal.c

调用图追踪模式 链接到标题

  • 配置项:CONFIG_INSTRUMENTATION_MODE_CALLGRAPH=y
  • 数据:函数进入/退出事件 + 时间戳 + 上下文信息
  • 缓冲区:环形模式(默认覆盖)或固定模式(满则停)CONFIG_INSTRUMENTATION_MODE_CALLGRAPH_TRACE_BUFFER_SIZE=8192
  • 功能:
    • 可重建完整的函数调用图
    • 观察线程上下文切换
    • 分析执行流程和时间关系

统计剖析模式 链接到标题

  • 配置项:CONFIG_INSTRUMENTATION_MODE_STATISTICAL=y
  • 数据:触发点至停止点间各函数累计执行时间
  • 限制:最大追踪函数个数由 CONFIG_INSTRUMENTATION_MODE_STATISTICAL_MAX_NUM_FUNC 控制
  • 功能:按耗时排序给出函数热点列表

动态 Trigger 设置 链接到标题

  • 启用 CONFIG_INSTRUMENTATION_DYNAMIC_TRIGGER=y
  • 依赖 Retained Memory 设备树节点
  • 跨重启保留触发/停止地址,避免重新烧录

精细化排除 链接到标题

CONFIG_INSTRUMENTATION_EXCLUDE_FUNCTION_LIST:按函数名排除

CONFIG_INSTRUMENTATION_EXCLUDE_FILE_LIST:按源文件排除

主机交互工具 链接到标题

scripts/instrumentation/zaru.py 是宿主端控制工具,通过 UART 与目标板通信,依赖 ELF 文件做符号解析,支持 Instrumentation trace/profile 数据采集与可视化。

安装依赖 链接到标题

zaru.py 依赖 python3-bt2 不在 pip 包内,需要使用下面命令安装:

sudo apt-get install python3-bt2

安装完后需要将系统包设置给 Zephyr 编译虚拟环境,并重新激活:

deactivate
python3 -m venv --system-site-packages .venv
source .venv/bin/activate

执行命令时需要 ZEPHYR_BASE 环境变量,执行下面命令设置环境变量:

source zephyr/zephyr-env.sh

使用说明 链接到标题

全局参数 链接到标题

全局参数需放置在子命令之前,用于配置串口连接与编译输出目录。

参数 缩写 默认值 说明
--serial /dev/ttyACM0 指定连接目标的串口设备(支持本地 /dev/ttyUSBx)。
--build-dir 自动推导 指定编译输出目录(需包含 zephyr.elfctf_metadata)。

子命令 链接到标题

子命令及详细参数

reboot(重启目标设备):用于重启下位机目标设备并检测其连通状态。

status(查看系统状态):获取目标设备的 Instrumentation 状态(查看 Trace、Profile 及动态 Trigger 支持情况)。

trace(系统追溯分析):获取、配置或导出系统的函数调用 Trace 序列。

参数 缩写 参数值 说明
--trigger -t FUNC_NAME 设置触发函数。进入该函数时开启 Trace(设为 0 可禁用)。
--stopper -s FUNC_NAME 设置停止函数。退出该函数时关闭 Trace(设为 0 可禁用)。
--couple -c FUNC_NAME 同时将 Trigger 和 Stopper 设为同一个函数(进入该函数开启,退出该函数停止)。
--list -l 打印当前目标设备上设置的 Trigger 和 Stopper 函数名称。
--reboot -r 在拉取 Trace 数据前自动重启目标设备。
--export-tef -e 导出 Trace 为 Trace Event Format (JSON),可直接导入 Perfetto 查看。
--output -o 文件名 指定导出 JSON 的文件名(默认为 ./trace_event.json)。
--demangle -d 解析/还原 C++ 符号名(调用 c++filt)。
--annotation -a 禁用函数返回(退出)处的名称注释(默认开启注释)。
--verbose -v 开启详细调试输出。

profile(性能分析)测量并统计各个函数的执行耗时占比。

参数 缩写 参数值 说明
-n 整数(默认 100) 显示耗时最高的前 N 个函数。
--reboot -r 在拉取 Profile 数据前自动重启目标设备。
--verbose -v 开启详细输出。

使用示例 链接到标题

  • 查看硬件支持状态
python3 zaru.py --serial /dev/ttyUSB0 status
  • 查询当前配置的 Trigger 和 Stopper
python3 zaru.py --serial /dev/ttyUSB0 trace -l
  • 设置触发/停止函数并重启拉取 Trace
# 进入 main 时开启,退出 process_data 时停止,拉取前重启板子
python3 zaru.py --serial /dev/ttyUSB0 trace -t main -s process_data -r
  • 追踪单个函数并导出为 Perfetto JSON 图表
python3 zaru.py --serial /dev/ttyUSB0 trace -c my_function -r -e -o my_trace.json
  • 获取耗时前 10 的函数(Profile)
python3 zaru.py --serial /dev/ttyUSB0 profile -n 10 -r
  • 手动重启板子
python3 zaru.py --serial /dev/ttyUSB0 reboot

ESP32S3 下示例 链接到标题

我使用的 ESP32S3,对 Instrumentation 的示例进行编译

west build -b esp32s3_touch_lcd_2/esp32s3/procpu zephyr/samples/subsys/instrumentation/ -- -DCONFIG_DEBUG_THREAD_INFO=y -DOPENOCD=/home/frank/workspace/tools/openocd-esp32/bin/openocd -DOPENOCD_DEFAULT_PATH=/home/frank/workspace/tools/openocd-esp32/share/openocd/scripts

非动态触发 链接到标题

编译时就确认好触发 INSTRUMENTATION 的函数,配置如下

# 基础启用
CONFIG_INSTRUMENTATION=y

# 双模式同时开启:调用图 + 统计剖析
CONFIG_INSTRUMENTATION_MODE_CALLGRAPH=y
CONFIG_INSTRUMENTATION_MODE_STATISTICAL=y

# 缓冲区调大,避免高频调用丢帧
CONFIG_INSTRUMENTATION_MODE_CALLGRAPH_TRACE_BUFFER_SIZE=112000


# 排除上下文切换函数
CONFIG_INSTRUMENTATION_EXCLUDE_FUNCTION_LIST="z_thread_mark_switched_in,z_thread_mark_switched_out,z_swap,do_swap"

非动态触发下默认在进入 main 函数时开启 Trace 开始收集数据,退出 main 函数时退出 Trace 收集数据,也可以通过 CONFIG_INSTRUMENTATION_TRIGGER_FUNCTIONCONFIG_INSTRUMENTATION_STOPPER_FUNCTION 来修改。

性能分析示例:

./zephyr/scripts/instrumentation/zaru.py --build-dir build/ --serial /dev/ttyUSB0 profile -n 10 -r 获取耗时前 10 的函数(Profile)

Using '/dev/ttyUSB0' to connect.
Connected to target.
Rebooting target... done!
Using ELF file: build/zephyr/zephyr.elf
Temporary dir: /tmp/tmpaaei0z39
Using ctf_metadata file: build/ctf_metadata
15.53% 1077396408 k_cpu_idle
15.52% 1077389376 arch_cpu_idle
 6.16% 1107302792 thread_A
 4.82% 1107302416 k_sem_take
 4.81% 1077404436 z_impl_k_sem_take
 3.59% 1107302552 k_msleep
 3.58% 1107302484 k_sleep
 3.57% 1077428856 z_impl_k_sleep
 3.56% 1077428344 z_tick_sleep
 2.70% 1077428288 arch_switch

Trace 抓取示例: ./zephyr/scripts/instrumentation/zaru.py --build-dir build/ --serial /dev/ttyUSB0 trace -r 获取 trace 信息

Using '/dev/ttyUSB0' to connect.
Connected to target.
Rebooting target... done!
Using ELF file: build/zephyr/zephyr.elf
Temporary dir: /tmp/tmpsme0zx80
Using ctf_metadata file: build/ctf_metadata
         Thread Name      Thread ID  CPU  Mode     Timestamp          Function(s)
    ------------------------------------------------------------------------------------------------
                main    0x3fcc3ea0  13)    0 |    261620458 ns | main() {
                main    0x3fcc3ea0  13)    0 |    261648383 ns |   k_thread_create() {
                main    0x3fcc3ea0  13)    0 |    261670529 ns |     z_impl_k_thread_create() {
                main    0x3fcc3ea0  13)    0 |    261690908 ns |       z_setup_new_thread() {
                main    0x3fcc3ea0  13)    0 |    261711383 ns |         z_waitq_init() {
                main    0x3fcc3ea0  13)    0 |    261731783 ns |           sys_dlist_init();
                main    0x3fcc3ea0  13)    0 |    261772545 ns |         };   /* z_waitq_init */
                main    0x3fcc3ea0  13)    0 |    261793275 ns |         z_init_thread_base() {
                main    0x3fcc3ea0  13)    0 |    261813775 ns |           z_init_thread_timeout() {
                main    0x3fcc3ea0  13)    0 |    261834437 ns |             z_init_timeout() {
                main    0x3fcc3ea0  13)    0 |    261854987 ns |               sys_dnode_init();
                main    0x3fcc3ea0  13)    0 |    261896104 ns |             };   /* z_init_timeout */
                main    0x3fcc3ea0  13)    0 |    261916700 ns |           };   /* z_init_thread_timeout */
                main    0x3fcc3ea0  13)    0 |    261937150 ns |         };   /* z_init_thread_base */
                main    0x3fcc3ea0  13)    0 |    261958000 ns |         setup_thread_stack() {
                main    0x3fcc3ea0  13)    0 |    261978637 ns |           K_KERNEL_STACK_BUFFER();
                main    0x3fcc3ea0  13)    0 |    262019904 ns |         };   /* setup_thread_stack */
                main    0x3fcc3ea0  13)    0 |    262040862 ns |         arch_new_thread() {
                main    0x3fcc3ea0  13)    0 |    262061583 ns |           init_stack();
                main    0x3fcc3ea0  13)    0 |    262103370 ns |         };   /* arch_new_thread */
                main    0x3fcc3ea0  13)    0 |    262124333 ns |         k_spin_lock() {
                main    0x3fcc3ea0  13)    0 |    262145054 ns |           arch_irq_lock();
                main    0x3fcc3ea0  13)    0 |    262186654 ns |           z_spinlock_validate_pre();
                main    0x3fcc3ea0  13)    0 |    262228387 ns |           z_spinlock_validate_post();

./zephyr/scripts/instrumentation/zaru.py --build-dir build/ --serial /dev/ttyUSB0 trace -r -e -o test.json 将 trace 信息保存为 Perfetto 格式

Using '/dev/ttyUSB0' to connect.
Connected to target.
Rebooting target... done!
Using ELF file: build/zephyr/zephyr.elf
Temporary dir: /tmp/tmp7qzw55qc
Using ctf_metadata file: build/ctf_metadata
Found thread 'main'.
Found thread 'thread_A'.
Found thread 'input'.
Found thread 'idle'.
Found 864 event(s), 4 thread(s), 0 context switch(es).
76525 byte(s) written to 'test.json'.

在 ui.perfetto.dev 中打开 test.json 可以看到各个线程 trace 分析结果

alt text

动态触发 链接到标题

动态触发需要增加下面配置:

# 动态触发(需 retained_mem 驱动 + DT overlay)
CONFIG_INSTRUMENTATION_DYNAMIC_TRIGGER=y

# 排除引起递归的文件
CONFIG_INSTRUMENTATION_EXCLUDE_FILE_LIST="retention.c,retention.h,retained_mem.h,retained_mem_zephyr_ram.c"

由于配置了 CONFIG_INSTRUMENTATION_DYNAMIC_TRIGGER 需要使用 retained_mem,因此需要在 devicetree 中添加 instrumentation_triggers 节点,对于 esp32s3 将其放在 rtc 的 retained_mem 中,地址为 0x0,大小为 0x20 字节,prefix = [be ef] 用于启动时校验 retained 区是否有效

memory@50001f00 {
		compatible = "zephyr,memory-region", "mmio-sram";
		reg = <0x50001f00 0x100>;
		zephyr,memory-region = "RTC_SLOW_RAM_RETAINED_MEM";
		status = "okay";

		retainedmem0: retainedmem {
			compatible = "zephyr,retained-ram";
			status = "okay";
			#address-cells = <1>;
			#size-cells = <1>;

			instrumentation_triggers: retention@0 {
				compatible = "zephyr,retention";
				status = "okay";

				reg = <0x0 0x20>;

				prefix = [be ef];
			};
		};
	};

配置触发函数为 z_init_static_threads,停止函数为 main./zephyr/scripts/instrumentation/zaru.py --build-dir build/ --serial /dev/ttyUSB0 trace -t z_init_static_threads -s main

Using '/dev/ttyUSB0' to connect.
Connected to target.
Using ELF file: build/zephyr/zephyr.elf
trigger set at entry of 'z_init_static_threads' function, at address 0x4037c0d0.
New trigger set at entry of 'z_init_static_threads' function, at address 0x4037c0d0.
stopper set at entry of 'main' function, at address 0x42001a74.
New stopper set at exit of 'main' function, at address 0x1107303028.

启动重新抓取 trace 信息,可以看到是从 z_init_static_threads 开始抓取

Using '/dev/ttyUSB0' to connect.
Connected to target.
Rebooting target... done!
Using ELF file: build/zephyr/zephyr.elf
Temporary dir: /tmp/tmp2eyhg79i
Using ctf_metadata file: build/ctf_metadata
         Thread Name      Thread ID  CPU  Mode     Timestamp          Function(s)
    ------------------------------------------------------------------------------------------------
                main    0x3fcc3ea0  13)    0 |    261510200 ns | z_init_static_threads() {
                main    0x3fcc3ea0  13)    0 |    261532629 ns |   z_setup_new_thread() {
                main    0x3fcc3ea0  13)    0 |    261553029 ns |     z_waitq_init() {
                main    0x3fcc3ea0  13)    0 |    261573375 ns |       sys_dlist_init();
                main    0x3fcc3ea0  13)    0 |    261613916 ns |     };   /* z_waitq_init */
                main    0x3fcc3ea0  13)    0 |    261634570 ns |     z_init_thread_base() {
                main    0x3fcc3ea0  13)    0 |    261655012 ns |       z_init_thread_timeout() {
                main    0x3fcc3ea0  13)    0 |    261675579 ns |         z_init_timeout() {

instrumentation 子系统的一些坑 链接到标题

以 Instrumentation 子系统目前的状态并不能开箱即用,并且还有设计缺陷,因此不太建议现阶段用于生产问题分析

Instrumetion本身的坑 链接到标题

由于设计上和实现上的问题有如下坑

  1. 递归调用:动态触发器与 retention 代码同时编译时会导致无限递归,需加入排除列表。
  2. 过早访问:系统初始化过早调用插桩时,retention 设备尚未就绪,需延迟加载机制
  3. 数据丢失:多线程下使用禁用/启用插桩来管理临界区会导致函数日志被绕过,造成追踪数据丢失
  4. 工具问题:无法正常设置stoper

已提 Issue 进行讨论 https://github.com/zephyrproject-rtos/zephyr/issues/116834

1,2 两点可以本地修改解决,3是设计缺陷不好修改,4已经提PR

https://github.com/zephyrproject-rtos/zephyr/pull/116836

ESP32S3 下的填坑 链接到标题

使用 Zephyr 下的子系统另一个大的问题就是不同的平台、不同的配置、不同的结果。instrumentation 示例代码是在 mps2_an385 和 b_u585i_iot02a 上跑,搬到 esp32s3 上需要填一些坑才能使用。

  1. 开机一直重启
    • 问题原因:instrumentation 会对所有 Zephyr 的函数进行插桩,插桩代码被放到 flash 内,在 flash mmu 建立前的代码会调用 instrumentation 的代码访问 flash mmu 的总线会导致 exception 重启。
    • 解决方法:通过修改 ld 文件将 instrumentation 及其关联代码放到 iram 内
  2. 开启 CONFIG_INSTRUMENTATION_DYNAMIC_TRIGGER 后一直重启
    • 问题原因:retained text/rodata 都放到 flash 中,在 flash mmu 建立前的代码会调用 retained device 的代码访问 flash mmu 的总线会导致 exception 重启。
    • 解决方法:延迟 retained 访问,在 flash mmu 建立并且 retention 设备驱动初始化完成后再进行访问。

具体的填坑和调试过程将在下一篇实现分析的博客中说明。

参考 链接到标题