- 1. 简介
- 1.1. ftrace
- 1.2. ftrace 与其他 trace 工具的关系和比较
- 1.3. 主要用途
- 2. ftrace内核编译选项
- 2.1. 内核源码编译选项
- 2.2. make menuconfig配置项
- 3. debugfs支持
- 3.1. 内核编译选项
- 3.2. 内核编译
- 4. 通过 debugfs 访问 ftrace
- 5. ftrace 的数据文件
- 6. ftrace 跟踪器
- 7. ftrace 操作流程
- 8. function 跟踪器
- 8.1. 特定进程
- 9. function_graph 跟踪器
- 10. sched_switch 跟踪器
- 11. irqsoff 跟踪器
- 12. preemptoff跟踪器
- 13. preemptirqsoff跟踪器
- 14. 动态跟踪
- 14.1. 指定模块
- 14.2. 跟踪模块初始化
- 15. trace选项(启用或禁用)
- 16. Max Stack Tracer 的使用
- 17. 事件追踪
- 18. trace-cmd and KernelShark
- 19. trace marker
- 20. 相关代码以及使用
- 20.1. trace_printk
- 20.2. 使用 tracing_on/tracing_off 控制跟踪信息的记录
- 21. 参考
ftrace 是 Linux 内核中提供的一种调试工具。使用 ftrace 可以对内核中发生的事情进行跟踪,这在调试 bug 或者分析内核时非常有用。
ftrace(函数跟踪)是内核跟踪的“瑞士军刀”。它是内建在Linux内核中的一种跟踪机制。它能深入内核去发现里面究竟发生了什么,并调试它。
ftrace不只是一个函数跟踪工具,它的跟踪能力之强大,还能调试和分析诸如延迟、意外代码路径、性能问题等一大堆问题。它也是一种很好的学习工具。
ftrace是由Steven Rostedy和Ingo Molnar在内核2.6.27版本中引入的。它有自己存储跟踪数据的环形缓冲区,并使用GCC配置机制。
ftrace官方文档在
Ftrace 最初是在 2.6.27 中出现的,那个时候,systemTap 已经开始崭露头角,其他的 trace 工具包括 LTTng 等也已经发展多年。那为什么人们还要再开发一个 trace 工具呢?
SystemTap项目是 Linux 社区对 SUN Dtrace 的反应,目标是达到甚至超越 Dtrace 。因此 SystemTap 设计比较复杂,Dtrace 作为 SUN 公司的一个项目开发了多年才最终稳定发布,况且得到了 Solaris 内核中每个子系统开发人员的大力支持。 SystemTap 想要赶超 Dtrace,困难不仅是一样,而且更大,因此她始终处在不断完善自身的状态下,在真正的产品环境,人们依然无法放心的使用她。不当的使用和 SystemTap 自身的不完善都有可能导致系统崩溃。
Ftrace的设计目标简单,本质上是一种静态代码插装技术,不需要支持某种编程接口让用户自定义 trace 行为。静态代码插装技术更加可靠,不会因为用户的不当使用而导致内核崩溃。 ftrace 代码量很小,稳定可靠。实际上,即使是 Dtrace,大多数用户也只使用其静态 trace 功能。因此 ftrace 的设计非常务实。
从 2.6.30 开始,ftrace 支持 event tracer,其实现和功能与 LTTng 非常类似,或许将来 ftrace 会同 LTTng 进一步融合,各自取长补短。 ftrace 有定义良好的 ASCII 接口,可以直接阅读,这对于内核开发人员非常具有吸引力,因为只需内核代码加上cat 命令就可以工作了,相当方便; LTTng 则采用 binary 接口,更利于专门工具分析使用。此外他们内部 ring buffer 的实现不相同,ftrace 对所有 tracer 都采用同一个 ring buffer,而 LTTng 则使用各自不同的 ring buffer 。
目前,或许将来 LTTng 都只能是内核主分支之外的工具。她主要受到嵌入式工程师的欢迎,而内核开发人员则更喜欢 ftrace 。
Ftrace 的实现依赖于其他很多内核特性,比如
ftrace 是内建于 Linux 内核的跟踪工具,从 2.6.27 开始加入主流内核。使用 ftrace 可以调试或者分析内核中发生的事情。
ftrace 提供了不同的跟踪器,以用于不同的场合,比如跟踪内核函数调用、对上下文切换进行跟踪、查看中断被关闭的时长、跟踪内核态中的延迟以及性能问题等。
系统开发人员可以使用 ftrace 对内核进行跟踪调试,以找到内核中出现的问题的根源,方便对其进行修复。
另外,对内核感兴趣的读者还可以通过 ftrace 来观察内核中发生的活动,了解内核的工作机制。
使用 ftrace ,首先要将其编译进内核。
内核源码目录下的
清单 1. ftrace 相关的配置选项列表:
1 2 3 4 5 6 7 8 9 10 | CONFIG_FTRACE=y CONFIG_HAVE_FUNCTION_TRACER=y CONFIG_HAVE_FUNCTION_GRAPH_TRACER=y CONFIG_HAVE_DYNAMIC_FTRACE=y CONFIG_FUNCTION_TRACER=y CONFIG_IRQSOFF_TRACER=y CONFIG_SCHED_TRACER=y CONFIG_ENABLE_DEFAULT_TRACERS=y CONFIG_FTRACE_SYSCALLS=y CONFIG_PREEMPT_TRACER=y |
ftrace 相关的配置选项比较多,针对不同的跟踪器有各自对应的配置选项。
不同的选项有不同的依赖关系,内核源码目录下的
读者可以参考
如果你看不到tracing子目录的话,你应该在内核配置上启用相关选项,然后重编译内核。
通常在配置内核时,使用
请在你的内核配置中找到如图所示的选项,启用它们:
Kernel Hacking -> Tracers:
- Kernel Function Tracer (FUNCTION_TRACER)
- Kernel Function Graph Tracer (FUNCTION_GRAPH_TRACER)
- Enable/disable ftrace dynamically (DYNAMIC_FTRACE)
- Trace max stack (STACK_TRACER)
根据你的架构,在选择上面的选项时,一些其他的选项根据依赖关系可能也会自动被启用。上面所列的选项主要是用于跟踪所用。内核编译完成之后,你只需要重启机器,tracing功能就可以用了。
以 2.6.33.1 版本的内核为例,要将 ftrace 编译进内核,可以选中 Kernel hacking (图 1 )下的 Tracers 菜单项(图 2 )。
图 1. Kernel hacking:
图 2. Tracers:
进入 Tracers 菜单下,可以看到内核支持的跟踪器列表。如图 3 所示,这里选中了所有的跟踪器,读者可以根据自己的需要选中特定的跟踪器。
图 3. 内核支持的跟踪器列表:
这里要注意,如果是在 32 位 x86 机器上,编译时不要选中 General setup 菜单项(图 4 )下的 Optimize for size 选项(图 5 ),否则就无法看到图 3 中的
1 | depends on !X86_32 || !CC_OPTIMIZE_FOR_SIZE |
图 4. General setup
图 5. Optimize for size
ftrace 通过 debugfs 向用户态提供了访问接口,所以还需要将 debugfs 编译进内核。
激活对 debugfs 的支持,可以直接编辑内核配置文件
图 6. debugfs 编译选项
配置完成后,编译安装新内核,然后启动到新内核。
注意,激活 ftrace 支持后,编译内核时会使用编译器的
清单 2. 激活编译选项
1 2 3 4 5 6 | ifdef CONFIG_FUNCTION_TRACER ORIG_CFLAGS := $(KBUILD_CFLAGS) KBUILD_CFLAGS = $(subst -pg,,$(ORIG_CFLAGS)) ... endif ... |
使用
ftrace 通过 debugfs 向用户态提供访问接口。配置内核时激活 debugfs 后会创建目录
1 | debugfs /sys/kernel/debug debugfs defaults 0 0 |
或者可以在运行时挂载:
1 | mount -t debugfs debugfs /sys/kernel/debug |
激活内核对 ftrace 的支持后会在 debugfs 下创建一个 tracing 目录 /sys/kernel/debug/tracing 。该目录下包含了 ftrace 的控制和输出文件,如图 7 所示。根据编译内核时针对 ftrace 的设定不同,该目录下实际显示的文件和目录与这里也会不同。
图 7. tracing 目录下的文件
tracing目录(
你可以在内核源代码目录下/Documentation/trace目录中找到这些文件的信息。
tracing_enabled 或tracing_on : 让你可以启用或者禁用当前跟踪功能
在操作这些数据文件时,通常使用 echo 命令来修改其值,也可以在程序中通过文件读写相关的函数来操作这些文件的值。
下面只对部分文件进行描述,读者可以参考内核源码包中
README 文件提供了一个简短的使用说明,展示了 ftrace 的操作命令序列。
可以通过 cat 命令查看该文件以了解概要的操作流程。
-
available_events :列出当前系统支持的event事件。 -
available_tracers 记录了当前编译进内核的跟踪器的列表
可以通过 cat 查看其内容;其包含的跟踪器与图 3 中所激活的选项是对应的。写
-
available_filter_functions 记录了当前可以跟踪的内核函数。对于不在该文件中列出的函数,无法跟踪其活动。 -
buffer_size_kb 用于以KB为单位指定各个CPU追踪缓冲区的大小。
跟踪器会将跟踪到的信息写入缓存,每个 CPU 的跟踪缓存是一样大的, 系统追踪缓冲区的总大小就是这个值乘以CPU的数量。
跟踪缓存实现为环形缓冲区的形式,如果跟踪到的信息太多,则旧的信息会被新的跟踪信息覆盖掉。
注意,要更改该文件的值需要先将
current_tracer 用于设置或显示当前使用的跟踪器;
使用 echo 将跟踪器名字写入该文件可以切换到不同的跟踪器。系统启动后,其缺省值为 nop ,即不做任何跟踪操作。在执行完一段跟踪任务后,可以通过向该文件写入 nop 来重置跟踪器。
-
set_ftrace_pid : 设置跟踪所作用的进程的PID。 -
set_ftrace_filter 和set_ftrace_notrace 在编译内核时配置了动态 ftrace (选中CONFIG_DYNAMIC_FTRACE 选项)后使用。
前者用于显示指定要跟踪的函数,后者则作用相反,用于指定不跟踪的函数。如果一个函数名同时出现在这两个文件中,则这个函数的执行状况不会被跟踪。这些文件还支持简单形式的含有通配符的表达式,这样可以用一个表达式一次指定多个目标函数;具体使用在后续文章中会有描述。注意,要写入这两个文件的函数名必须可以在文件
set_graph_function 设置要清晰显示调用关系的函数,显示的信息结构类似于 C 语言代码,这样在分析内核运作流程时会更加直观一些。
在使用
trace 文件提供了查看获取到的跟踪信息的接口。
可以通过 cat 等命令查看该文件以查看跟踪到的内核活动记录,也可以将其内容保存为记录文件以备后续查看。
-
trace_pipe ,与trace相同,但是运行时像管道一样,可以在每次事件发生时读出追踪信息,但是读出的内容不能再次读出。 -
tracing_cpumask ,以十六进制的位掩码指定要作为追踪对象的处理器,例如,指定0xb时仅在处理器0、1、3上进行追踪。 -
tracing_on 用于控制跟踪的暂停。有时候在观察到某些事件时想暂时关闭跟踪,可以将 0 写入该文件以停止跟踪,这样跟踪缓冲区中比较新的部分是与所关注的事件相关的;写入 1 可以继续跟踪。 -
tracing_max_latency ,
要找到哪些跟踪器可用,你可以对
在编译内核时,也可以看到内核支持的跟踪器对应的选项,如之前图 3 所示。
与输出空间分离的跟踪器有:nop(它不是一个跟踪器,是默认设置的一个值)、函数(函数跟踪器)、函数图(函数图跟踪器),等等,如下所示:
1 2 | # cat /sys/kernel/debug/tracing/available_tracers hwlat blk function_graph wakeup_dl wakeup_rt wakeup function nop |
function ,函数调用追踪器,可以看出哪个函数何时调用。
可以通过文件
function_graph ,函数调用图表追踪器,可以看出哪个函数被哪个函数调用,何时返回。
可以通过文件
-
blk ,block I/O追踪器。 -
mmiotrace ,MMIO( Memory MappedI/O)追踪器,用于Nouveau驱动程序等逆向工程。 -
wakeup ,跟踪进程唤醒信息, 进程调度延迟追踪器。 -
wakeup_rt ,与wakeup相同,但以实时进程为对象。 -
irqsoff ,当中断被禁止时,系统无法响应外部事件,造成系统响应延迟,irqsoff跟踪并记录内核中哪些函数禁止了中断,对于其中禁止中断时间最长的,irqsoff将在log文件的第一行标示出来,从而可以迅速定位造成系统响应延迟的原因。 -
preemptoff ,追踪并记录禁止内核抢占的函数,并清晰显示出禁止内核抢占时间最长的函数。 -
preemptirqsoff ,追踪并记录禁止内核抢占和中断时间最长的函数。即综合了irqoff和preemptoff两个功能。 -
sched_switch ,进行上下文切换的追踪,可以得知从哪个进程切换到了哪个进程。 -
nop ,不会跟踪任何内核活动,将 nop 写入 current_tracer 文件可以删除之前所使用的跟踪器,并清空之前收集到的跟踪信息,即刷新 trace 文件。
ftrace 还支持其它一些跟踪器,比如 initcall、ksym_tracer、mmiotrace、sysprof 等。
ftrace 框架支持扩展添加新的跟踪器。读者可以参考内核源码包中
使用 ftrace 提供的跟踪器来调试或者分析内核时需要如下操作:
- 切换到目录
/sys/kernel/debug/tracing/ 下
1 | cd /sys/kernel/debug/tracing |
- 查看
available_tracers 文件,获取当前内核支持的跟踪器列表
1 | # cat available_tracers |
- 关闭 ftrace 跟踪,即将 0 写入文件
tracing_on
1 | echo 0 > tracing_on |
- 激活 ftrace_enabled,否则 function 跟踪器的行为类似于 nop;另外,激活该选项还可以让一些跟踪器比如 irqsoff 获取更丰富的信息。
建议使用 ftrace 时将其激活。要激活
1 | echo 1 > /proc/sys/kernel/ftrace_enabled |
或者
1 | sysctl kernel.ftrace_enabled=1 |
- 启用追踪器. 将所选择的跟踪器的名字写入文件
current_tracer
ftrace每次只能打开一个追踪器
1 2 3 | # echo function > current_tracer ##选择一个特定的跟踪器。 # cat current_tracer ##检查是否是你所设置的跟踪器。 |
- 将要跟踪的函数写入文件
set_ftrace_filter ,将不希望跟踪的函数写入文件set_ftrace_notrace 。
一旦将函数追踪器启动,ftrace会记录所有函数的运行情况, 如果想要禁用或只查看某一些, 需要通过
通常直接操作文件
1 2 3 | # echo schedule > set_ftrace_filter ## 仅记录schedule # cat set_ftrace_filter schedule |
- 激活 ftrace 跟踪,即将 1 写入文件
tracing_on 。
确保文件
1 | # echo 1 > tracing_on ##初始化跟踪 |
- 停顿一会儿.
如果是对应用程序进行分析的话,启动应用程序的执行,ftrace 会跟踪应用程序运行期间内核的运作情况
- 通过将 0 写入文件
tracing_on 来暂停跟踪信息的记录,此时跟踪器还在跟踪内核的运行,只是不再向文件 trace 中写入跟踪信息
1 | # echo 0 > tracing_on ##禁用跟踪功能 |
- 查看文件 trace 获取跟踪信息,对内核的运行进行分析调试
1 2 | # cat trace > /tmp/trace.txt ##将跟踪文件保存到一个临时文件。 # cat /tmp/trace.txt ##查看trace文件的输出。 |
- 禁用trace功能, 清空trace文件
1 2 | # echo 0 > tracing_on ## 禁用跟踪功能 # echo 0 > trace ## 或者> trace |
function 跟踪器可以跟踪内核函数的调用情况,可用于调试或者分析 bug ,还可用于了解和观察 Linux 内核的执行过程。清单 1 给出了使用 function 跟踪器的示例。
清单 1. function 跟踪器使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | [root@linux tracing]# pwd /sys/kernel/debug/tracing [root@linux tracing]# echo 0 > tracing_on [root@linux tracing]# echo 1 > /proc/sys/kernel/ftrace_enabled [root@linux tracing]# echo function > current_tracer [root@linux tracing]# echo 1 > tracing_on # 让内核运行一段时间,这样 ftrace 可以收集一些跟踪信息,之后再停止跟踪 [root@linux tracing]# echo 0 > tracing_on [root@linux tracing]# cat trace | head -10 # tracer: function # # TASK-PID CPU# TIMESTAMP FUNCTION # | | | | | <idle>-0 [000] 20654.426521: _raw_spin_lock <-scheduler_tick <idle>-0 [000] 20654.426522: task_tick_idle <-scheduler_tick <idle>-0 [000] 20654.426522: cpumask_weight <-scheduler_tick <idle>-0 [000] 20654.426523: cpumask_weight <-scheduler_tick <idle>-0 [000] 20654.426523: run_posix_cpu_timers <-update_process_times <idle>-0 [000] 20654.426524: hrtimer_forward <-tick_sched_timer |
trace 文件给出的信息格式很清晰。
首先,字段
然后是跟踪信息记录的格式,
ftrace允许你对一个特定的进程进行跟踪。
在
手动操作:
1 2 3 4 5 6 7 8 | # cd /sys/kernel/debug/tracing/ # cat set_ftrace_pid no pid # echo 3111 > set_ftrace_pid //跟踪PID为3111的进程 # cat set_ftrace_pid 3111 # echo function > current_tracer # cat trace |
以下traceprocess.sh示例脚本向你展示了如何抓取当前运行的进程的PID,并进行相应跟踪。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #!/bin/bash DPATH="/sys/kernel/debug/tracing" ## shell pid PID=$$ ## Quick basic checks [ `id -u` -ne 0 ] && { echo "needs to be root" ; exit 1; } # check for root permissions [ -z $1 ] && { echo "needs process name as argument" ; exit 1; } # check for args to this function mount | grep -i debugfs &> /dev/null [ $? -ne 0 ] && { echo "debugfs not mounted, mount it first"; exit 1; } #checks for debugfs mount # flush existing trace data echo nop > $DPATH/current_tracer # set function tracer echo function > $DPATH/current_tracer # write current process id to set_ftrace_pid file echo $PID > $DPATH/set_ftrace_pid # start the tracing echo 1 > $DPATH/tracing_on # execute the process # $* all parameter list exec $* # stop the tracing echo 0 > tracing_on |
1 | ./traceprocess.sh ls |
如图中跟踪ls命令所示。
当跟踪完成后,你需要清除
1 | > set_ftrace_pid |
在 function 跟踪器给出的信息中,可以通过 FUNCTION 列中的符号
函数图跟踪器对函数的进入与退出进行跟踪,这对于跟踪它的执行时间很有用。函数执行时间超过10微秒的标记一个
有很多跟踪器,所有的列表在
清单 2 给出了使用
注: 关于模块的参照下面
清单 2.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | [root@linux tracing]# pwd /sys/kernel/debug/tracing [root@linux tracing]# echo 0 > tracing_on [root@linux tracing]# echo 1 > /proc/sys/kernel/ftrace_enabled [root@linux tracing]# echo function_graph > current_tracer [root@linux tracing]# echo __do_fault > set_graph_function [root@linux tracing]# echo 1 > tracing_on # 让内核运行一段时间,这样 ftrace 可以收集一些跟踪信息,之后再停止跟踪 [root@linux tracing]# echo 0 > tracing_on [root@linux tracing]# cat trace | head -20 # tracer: function_graph # # CPU DURATION FUNCTION CALLS # | | | | | | | 1) 9.936 us | } 1) 0.714 us | put_prev_task_fair(); 1) | pick_next_task_fair() { 1) | set_next_entity() { 1) 0.647 us | update_stats_wait_end(); 1) 0.699 us | __dequeue_entity(); 1) 3.322 us | } 1) 0.865 us | hrtick_start_fair(); 1) 6.313 us | } 1) | __switch_to_xtra() { 1) 1.601 us | memcpy(); 1) 3.938 us | } [root@linux tracing]# echo > set_graph_function |
在文件 trace 的输出信息中,首先给出的也是当前跟踪器的名字,这里是
- 对于不调用其它函数的函数,其对应行以
“;” 结尾,而且对应的DURATION 字段给出其运行时长; - 对于调用其它函数的函数,则在其
“}” 对应行给出了运行时长,该时间是一个累加值,包括了其内部调用的函数的执行时长。DURATION 字段给出的时长并不是精确的,它还包含了执行ftrace 自身的代码所耗费的时间,所以示例中将内部函数时长累加得到的结果会与对应的外围调用函数的执行时长并不一致;不过通过该字段还是可以大致了解函数在时间上的运行开销的。
清单 2 中最后通过 echo 命令重置了文件
清单 3. sched_switch 跟踪器使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | [root@linux tracing]# pwd /sys/kernel/debug/tracing [root@linux tracing]# echo 0 > tracing_enabled [root@linux tracing]# echo 1 > /proc/sys/kernel/ftrace_enabled [root@linux tracing]# echo sched_switch > current_tracer [root@linux tracing]# echo 1 > tracing_on [root@linux tracing]# echo 1 > tracing_enabled # 让内核运行一段时间,这样 ftrace 可以收集一些跟踪信息,之后再停止跟踪 [root@linux tracing]# echo 0 > tracing_enabled [root@linux tracing]# cat trace | head -10 # tracer: sched_switch # # TASK-PID CPU# TIMESTAMP FUNCTION # | | | | | bash-1408 [000] 26208.816058: 1408:120:S + [000] 1408:120:S bash bash-1408 [000] 26208.816070: 1408:120:S + [000] 1408:120:S bash bash-1408 [000] 26208.816921: 1408:120:R + [000] 9:120:R events/0 bash-1408 [000] 26208.816939: 1408:120:R ==> [000] 9:120:R events/0 events/0-9 [000] 26208.817081: 9:120:R + [000] 1377:120:R gnome-terminal events/0-9 [000] 26208.817088: 9:120:S ==> [000] 1377:120:R gnome-terminal |
在
- 唤醒操作记录给出了当前进程唤醒运行的进程,
- 进程调度切换记录中显示了接替当前进程运行的后续进程。
描述进程状态的格式为“
以示例跟踪信息中的第一条跟踪记录为例,可以看到
在 Linux 内核中,进程的状态在内核头文件
当关闭中断时,CPU就不能响应其他的事件,如果这时有一个鼠标中断,要在下一次开中断时才能响应这个鼠标中断,这段延迟称为中断延迟, 有时候这样做会对系统性能造成比较大的影响。
irqsoff 跟踪器可以对中断被关闭的状况进行跟踪,有助于发现导致较大延迟的代码;当出现最大延迟时,跟踪器会记录导致延迟的跟踪信息,文件
清单 4. irqsoff 跟踪器使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | # pwd /sys/kernel/debug/tracing # echo 0 > tracing_on # echo 0 > options/function-trace //关闭function-trace可以减少一些延迟 # echo 1 > /proc/sys/kernel/ftrace_enabled # echo irqsoff > current_tracer # echo 1 > tracing_on # echo 1 > tracing_enabled # 让内核运行一段时间,这样 ftrace 可以收集一些跟踪信息,之后再停止跟踪 [root@linux tracing]# echo 0 > tracing_on [root@linux tracing]# cat trace | head -35 # tracer: irqsoff # # irqsoff latency trace v1.1.5 on 2.6.33.1 # -------------------------------------------------------------------- # latency: 34380 us, #6/6, CPU#1 | (M:desktop VP:0, KP:0, SP:0 HP:0 #P:2) # ----------------- # | task: -0 (uid:0 nice:0 policy:0 rt_prio:0) # ----------------- # => started at: reschedule_interrupt # => ended at: restore_all_notrace # # # _------=> CPU# # / _-----=> irqs-off # | / _----=> need-resched # || / _---=> hardirq/softirq # ||| / _--=> preempt-depth # |||| /_--=> lock-depth # |||||/ delay # cmd pid |||||| time | caller # \ / |||||| \ | / <idle>-0 1dN... 4285us!: trace_hardirqs_off_thunk <-reschedule_interrupt <idle>-0 1dN... 34373us+: smp_reschedule_interrupt <-reschedule_interrupt <idle>-0 1dN... 34375us+: native_apic_mem_write <-smp_reschedule_interrupt <idle>-0 1dN... 34380us+: trace_hardirqs_on_thunk <-restore_all_notrace <idle>-0 1dN... 34384us : trace_hardirqs_on_caller <-restore_all_notrace <idle>-0 1dN... 34384us : <stack trace> => trace_hardirqs_on_thunk [root@linux tracing]# cat tracing_max_latency 34380 |
从清单 4 中的输出信息中,可以看到当前 irqsoff 延迟跟踪器的版本信息。显示当前跟踪器的版本信息为
接下来是最大延迟时间,以 us 为单位,本例中为 34380 us ,查看文件
从“task:”字段可以知道延迟发生时正在运行的进程为 idle(其 pid 为 0 )。中断的关闭操作是在函数 reschedule_interrupt 中进行的,由“=> started at:”标识,函数 restore_all_ontrace 重新激活了中断,由“=> ended at:”标识;中断关闭的最大延迟发生在函数 trace_hardirqs_on_thunk 中,这个可以从最大延迟时间所在的记录项看到,也可以从延迟记录信息中最后的“=>”标识所对应的记录行知道这一点。
在输出信息中,irqs-off、need_resched 等字段对应于进程结构 struct task_struct 的字段或者状态标志,可以从头文件
在文件的头部,irqsoff tracer 记录了中断禁止时间最长的函数。在本例中,函数 trace_hardirqs_on 将中断禁止了 12us 。
文件中的每一行代表一次函数调用。 Cmd 代表进程名,pid 是进程 ID 。中间有 5 个字符,分别代表了 CPU#,irqs-off 等信息,具体含义如下:
CPU# 表示 CPU ID ;
irqs-off 这个字符的含义如下:’ d ’表示中断被 disabled 。’ . ’表示中断没有关闭;
need-resched 字符的含义:’ N ’表示 need_resched 被设置,’ . ’表示 need-reched 没有被设置,中断返回不会进行进程切换;
hardirq/softirq 字符的含义 : 'H' 在 softirq 中发生了硬件中断, 'h' – 硬件中断,’ s ’表示 softirq,’ . ’不在中断上下文中,普通状态。
preempt-depth: 当抢占中断使能后,该域代表 preempt_disabled 的级别。
在每一行的中间,还有两个域:time 和 delay 。 time: 表示从 trace 开始到当前的相对时间。 Delay 突出显示那些有高延迟的地方以便引起用户注意。当其显示为 ! 时,表示需要引起注意。
另外,还有用于跟踪禁止进程抢占的跟踪器 preemptoff 和跟踪中断 / 抢占禁止的跟踪器 preemptirqsoff,它们的使用方式与输出信息格式与 irqsoff 跟踪器类似,这里不再赘述。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | # tracer: irqsoff # # irqsoff latency trace v1.1.5 on 4.0.0 # -------------------------------------------------------------------- # latency: 259 us, #4/4, CPU#2 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:4) # ----------------- # | task: ps-6143 (uid:0 nice:0 policy:0 rt_prio:0) # ----------------- # => started at: __lock_task_sighand # => ended at: _raw_spin_unlock_irqrestore # # # _------=> CPU# # / _-----=> irqs-off # | / _----=> need-resched # || / _---=> hardirq/softirq # ||| / _--=> preempt-depth # |||| / delay # cmd pid ||||| time | caller # \ / ||||| \ | / ps-6143 2d... 0us!: trace_hardirqs_off < -__lock_task_sighand ps-6143 2d..1 259us+: trace_hardirqs_on < -_raw_spin_unlock_irqrestore ps-6143 2d..1 263us+: time_hardirqs_on < -_raw_spin_unlock_irqrestore ps-6143 2d..1 306us : < stack trace> => trace_hardirqs_on_caller => trace_hardirqs_on => _raw_spin_unlock_irqrestore => do_task_stat => proc_tgid_stat => proc_single_show => seq_read => vfs_read => sys_read => system_call_fastpath |
文件的开头显示了当前跟踪器为irqsoff,并且显示当前跟踪器的版本信息为v1.1.5,运行的内核版本为4.0。显示当前最大的中断延迟是259微秒,跟踪条目和总共跟踪条目为4条(
started at和ended at显示发生中断的开始函数和结束函数分别为__lock_task_sighand和_raw_spin_unlock_irqrestore。接下来ftrace信息表示的内容分别如下。
- cmd:进程名字为“ps”。
- pid:进程的PID号。
- CPU#:该进程运行在哪个CPU上。
- irqs-off:“d”表示中断已经关闭。
- need_resched:“N”表示进程设置了
TIF_NEED_RESCHED 和PREEMPT_NEED_RESCHED 标志位;“n”表示进程仅设置了TIF_NEED_RESCHED 标志位;“p”表示进程仅设置了PREEMPT_NEED_RESCHED 标志位。 - hardirq/softirq:“H”表示在一次软中断中发生了一个硬件中断;“h”表示硬件中断发生;“s”表示软中断;“.”表示没有中断发生。
- preempt-depth:表示抢占关闭的嵌套层级。
- time:表示时间戳。如果打开了latency-format选项,表示时间从开始跟踪算起,这是一个相对时间,方便开发者观察,否则使用系统绝对时间。
- delay:用一些特殊符号来延迟的时间,方便开发者观察。“$”表示大于1秒,“#”表示大于1000微秒,“!”表示大于100微秒,“+”表示大于10微秒。
最后要说明的是,文件最开始显示中断延迟是259微秒,但是在
当抢占关闭时,虽然可以响应中断,但是高优先级进程在中断处理完成之后不能抢占低优先级进程直至打开抢占,这样也会导致抢占延迟。和irqsoff跟踪器一样,preemptoff跟踪器用于跟踪和记录关闭抢占的最大延迟。
1 2 3 4 5 6 7 | # cd /sys/kernel/debug/tracing/ # echo 0 > options/function-trace # echo preemptoff > current_tracer # echo 1 > tracing_on [...] # echo 0 > tracing_on # cat trace |
下面是一个preemptoff的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | # tracer: preemptoff # # preemptoff latency trace v1.1.5 on 3.8.0-test+ # -------------------------------------------------------------------- # latency: 46 us, #4/4, CPU#1 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:4) # ----------------- # | task: sshd-1991 (uid:0 nice:0 policy:0 rt_prio:0) # ----------------- # => started at: do_IRQ # => ended at: do_IRQ # # # _------=> CPU# # / _-----=> irqs-off # | / _----=> need-resched # || / _---=> hardirq/softirq # ||| / _--=> preempt-depth # |||| / delay # cmd pid ||||| time | caller # \ / ||||| \ | / sshd-1991 1d.h. 0us+: irq_enter < -do_IRQ sshd-1991 1d..1 46us : irq_exit < -do_IRQ sshd-1991 1d..1 47us+: trace_preempt_on < -do_IRQ sshd-1991 1d..1 52us : < stack trace> => sub_preempt_count => irq_exit => do_IRQ => ret_from_intr |
在优化系统延迟时,如果能快速定位何处关中断或者关抢占,对开发者来说会很有帮助,思考如下代码片段。
1 2 3 4 5 6 7 | local_irq_disable(); call_function_with_irqs_off(); //函数A preempt_disable(); call_function_with_irqs_and_preemption_off(); //函数B local_irq_enable(); call_function_with_preemption_off(); //函数C preempt_enable(); |
如果使用irqsoff跟踪器,那么只能记录函数A和函数B的时间。如果使用preemptoff跟踪器,那么只能记录函数B和函数C的时间。可是函数A+B+C中都不能被调度,因此preemptirqsoff用于记录这段时间的最大延迟。
1 2 3 4 5 6 7 | # cd /sys/kernel/debug/tracing/ # echo 0 > options/function-trace # echo preemptirqsoff > current_tracer # echo 1 > tracing_on [...] # echo 0 > tracing_on # cat trace |
preemptirqsoff跟踪器抓取的信息如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | # tracer: preemptoff # # preemptoff latency trace v1.1.5 on 3.8.0-test+ # -------------------------------------------------------------------- # latency: 46 us, #4/4, CPU#1 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:4) # ----------------- # | task: sshd-1991 (uid:0 nice:0 policy:0 rt_prio:0) # ----------------- # => started at: do_IRQ # => ended at: do_IRQ # # # _------=> CPU# # / _-----=> irqs-off # | / _----=> need-resched # || / _---=> hardirq/softirq # ||| / _--=> preempt-depth # |||| / delay # cmd pid ||||| time | caller # \ / ||||| \ | / sshd-1991 1d.h. 0us+: irq_enter < -do_IRQ sshd-1991 1d..1 46us : irq_exit < -do_IRQ sshd-1991 1d..1 47us+: trace_preempt_on < -do_IRQ sshd-1991 1d..1 52us : < stack trace> => sub_preempt_count => irq_exit => do_IRQ => ret_from_intr |
在配置内核时打开了
在实际调试过程中,我们通常会被ftrace提供的大量信息淹没,因此动态过滤的方法非常有用。
1 2 3 4 5 6 7 | # cd /sys/kernel/debug/tracing/ # echo sys_nanosleep hrtimer_interrupt > set_ftrace_filter # echo function > current_tracer # echo 1 > tracing_on # usleep 1 # echo 0 > tracing_on # cat trace |
抓取的数据如下.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | # tracer: function # # entries-in-buffer/entries-written: 5/5 #P:4 # # _-----=> irqs-off # / _----=> need-resched # | / _---=> hardirq/softirq # || / _--=> preempt-depth # ||| / delay # TASK-PID CPU# |||| TIMESTAMP FUNCTION # | | | |||| | | usleep-2665 [001] .... 4186.475355: sys_nanosleep < -system_call_fastpath < idle>-0 [001] d.h1 4186.475409: hrtimer_interrupt < -smp_apic_timer_interrupt usleep-2665 [001] d.h1 4186.475426: hrtimer_interrupt < -smp_apic_timer_interrupt < idle>-0 [003] d.h1 4186.475426: hrtimer_interrupt < -smp_apic_timer_interrupt < idle>-0 [002] d.h1 4186.475427: hrtimer_interrupt < -smp_apic_timer_interrupt |
此外,过滤器还支持如下通配符。
:匹配所有match开头的函数。* * :匹配所有match结尾的函数。* :匹配所有包含match的函数。*
需要注意的是,这三种形式不能组合使用,比如
如果要跟踪所有“
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | # echo sys_nanosleep > set_ftrace_filter //往过滤器里写入sys_nanosleep # cat set_ftrace_filter //查看过滤器里的内容 sys_nanosleep # echo 'hrtimer_*' >> set_ftrace_filter //再向过滤器中增加”hrtimer_”开头的函数 # cat set_ftrace_filter hrtimer_run_queues hrtimer_run_pending hrtimer_init hrtimer_cancel hrtimer_try_to_cancel hrtimer_forward hrtimer_start hrtimer_reprogram hrtimer_force_reprogram hrtimer_get_next_event hrtimer_interrupt sys_nanosleep hrtimer_nanosleep hrtimer_wakeup hrtimer_get_remaining hrtimer_get_res hrtimer_init_sleeper # echo '*preempt*' '*lock*' > set_ftrace_notrace //表示不跟踪包含”preempt”和”lock”的函数 # echo > set_ftrace_filter //向过滤器中输入空字符表示清空过滤器 # cat set_ftrace_filter |
图4就是一个动态跟踪的例子。
另外,在参数前面加上
1 | root@thinker:/sys/kernel/debug/tracing# echo 'write*:mod:ext3' > set_ftrace_filter |
仅追踪ext3模块中包含的以write开头的函数。
通过该文件还可以指定属于特定模块的函数,这要用到 mod 指令。指定模块的格式为:
1 | echo ':mod:[module_name]' > set_ftrace_filter |
下面给出了一个指定跟踪模块 ipv6 中的函数的例子。可以看到,指定跟踪模块 ipv6 中的函数会将文件
清单 5. 指定跟踪 ipv6 模块中的函数
1 2 3 4 5 6 7 8 9 | [root@linux tracing]# pwd /sys/kernel/debug/tracing [root@linux tracing]# echo ':mod:ipv6' > set_ftrace_filter [root@linux tracing]# cat set_ftrace_filter | head -5 ipv6_opt_accepted inet6_net_exit ipv6_gro_complete inet6_create ipv6_addr_copy |
1 2 3 4 | # echo ':mod:snd_seq' > set_ftrace_filter # echo function > current_tracer # modprobe snd_seq # cat trace |
让我们从tracer的选项开始。
tracing的输入可以由一个叫
可以通过更新
要禁用一个跟踪选项,只需要在相应行首加一个“no”即可。
比如,
这个 tracer 记录内核函数的堆栈使用情况,用户可以使用如下命令打开该 tracer:
1 | echo 1 > /proc/sys/kernel/stack_tracer_enabled |
从此,ftrace 便留心记录内核函数的堆栈使用。 Max Stack Tracer 的输出在 stack_trace 文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | # cat /debug/tracing/stack_trace Depth Size Location (44 entries) ----- ---- -------- 0) 3088 64 update_curr+0x64/0x136 1) 3024 64 enqueue_task_fair+0x59/0x2a1 2) 2960 32 enqueue_task+0x60/0x6b 3) 2928 32 activate_task+0x27/0x30 4) 2896 80 try_to_wake_up+0x186/0x27f … 42) 80 80 sysenter_do_call+0x12/0x32 |
从上例中可以看到内核堆栈最满的情况如下,有 43 层函数调用,堆栈使用大小为 3088 字节。此外还可以在 Location 这列中看到整个的 calling stack 情况。这在某些情况下,可以提供额外的 debug 信息,帮助开发人员定位问题。
ftrace里的跟踪机制主要有两种,分别是函数和tracepoint。前者属于“傻瓜式”操作,后者tracepoint可以理解为一个Linux内核中的占位符函数,内核子系统的开发者通常喜欢利用它来调试。
tracepoint可以输出开发者想要的参数、局部变量等信息。tracepoint的位置比较固定,一般都是内核开发者添加上去的,可以把它理解为传统C语言程序中
也可以在系统特定事件触发的时候打开跟踪。可以在
1 2 3 4 5 6 7 8 9 10 11 | # cat available_events | head -10 devlink:devlink_hwmsg sunrpc:rpc_call_status sunrpc:rpc_bind_status sunrpc:rpc_connect_status sunrpc:rpc_task_begin sunrpc:rpc_task_run_action sunrpc:rpc_task_complete sunrpc:rpc_task_sleep sunrpc:rpc_task_wakeup sunrpc:rpc_socket_state_change |
比如,为了启用某个事件,你需要:
图5是一个事件跟踪场景示例。同样,可用的事件是列在事件目录里面的。
有关事件跟踪的更多细节,请阅读内核目录下
trace-cmd是由Steven Rostedt在2009年发在LKML上的,它可以让操作跟踪器更简单。以下几步是获取最新的版本并装在你的系统上,包括它的GUI工具KernelShark。
1 2 3 4 5 6 7 8 9 10 11 12 13 | wget http://ftp.be.debian.org/pub/linux/analysis/trace-cmd/trace-cmd-1.0.5.tar.gz tar -zxvf trace-cmd-1.0.5.tar.gz cd trace-cmd* make make gui # compiles GUI tools (KernelShark)[3] make install make install_gui # installs GUI tools |
有了trace-cmd,跟踪将变得小菜一碟(见图6的示例用法):
1 2 3 4 5 6 7 | trace-cmd list ##to see available events trace-cmd record -e syscalls ls ##Initiate tracing on the syscall 'ls' ##(A file called trace.dat gets created in the current directory.) trace-cmd report ## displays the report from trace.dat |
通过上面的make install_gui命令安装的KernelShark可以用于分析trace.dat文件中的跟踪数据,如图7所示。
有时需要跟踪用户程序和内核空间的运行情况,trace marker可以很方便地跟踪用户程序。trace_marker是一个文件节点,允许用户程序写入字符串,ftrace会记录该写入动作时的时间戳。
内核头文件
ftrace 提供了一个用于向 ftrace 跟踪缓冲区输出跟踪信息的工具函数,叫做
可以通过
从头文件
1 2 | #define trace_printk(fmt, args...) \ ... |
下面通过一个示例模块
注意,编译模块时要加入
清单 1. 示例模块 ftrace_demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | /* * ftrace_demo.c */ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> MODULE_LICENSE("GPL"); static int ftrace_demo_init(void) { trace_printk("Can not see this in trace unless loaded for the second time\n"); return 0; } static void ftrace_demo_exit(void) { trace_printk("Module unloading\n"); } module_init(ftrace_demo_init); module_exit(ftrace_demo_exit); |
示例模块非常简单,仅仅是在模块初始化函数和退出函数中输出信息。接下来要对模块的运行进行跟踪,如清单 2 所示。
清单 2. 对模块 ftrace_demo 进行跟踪
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | [root@linux tracing]# pwd /sys/kernel/debug/tracing [root@linux tracing]# echo 0 > tracing_on [root@linux tracing]# echo 1 > /proc/sys/kernel/ftrace_enabled [root@linux tracing]# echo function_sgraph > current_tracer # 事先加载模块 ftrace_demo [root@linux tracing]# echo ':mod:ftrace_demo' > set_ftrace_filter [root@linux tracing]# cat set_ftrace_filter ftrace_demo_init ftrace_demo_exit # 将模块 ftrace_demo 卸载 [root@linux tracing]# echo 1 > tracing_on # 重新进行模块 ftrace_demo 的加载与卸载操作 [root@linux tracing]# cat trace # tracer: function_graph # # CPU DURATION FUNCTION CALLS # | | | | | | | 1) | /* Can not see this in trace unless loaded for the second time */ 0) | /* Module unloading */ |
在这个例子中,使用 mod 指令显式指定跟踪模块
这里仅仅是为了以简单的模块进行演示,故只定义了模块的 init/exit 函数,重复加载模块也只是为了获取初始化函数输出的跟踪信息。实践中,可以在模块的功能函数中加入对 trace_printk 的调用,这样可以记录模块的运作情况,然后对其特定功能进行调试优化。还可以将对
在跟踪过程中,有时候在检测到某些事件发生时,想要停止跟踪信息的记录,这样,跟踪缓冲区中较新的数据是与该事件有关的。在用户态,可以通过向文件
现在对清单 1 中的代码进行修改,使用 tracing_off() 来控制跟踪信息记录的暂停。
清单 3. 使用 tracing_off 的模块 ftrace_demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /* * ftrace_demo.c * modified to demostrate the usage of tracing_off */ #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> MODULE_LICENSE("GPL"); static int ftrace_demo_init(void) { trace_printk("ftrace_demo_init called\n"); tracing_off(); return 0; } static void ftrace_demo_exit(void) { trace_printk("ftrace_demo_exit called\n"); tracing_off(); } module_init(ftrace_demo_init); module_exit(ftrace_demo_exit); |