通过Ftrace实现高效、精确的内核调试与分析

虽然之前一直听说过ftrace,但从来没将它用在实战中,在一次客户排查问题中,遇到了比较奇怪的现象,一位精通内核的朋友建议使用ftrace来定位一下。虽然那一次并没有使用ftrace,但也让我觉得,后面我们势必要提供ftrace 相关的工具帮助我们在线上定位问题,所以自己也决定重新学习使用下ftrace,当然也决定写一系列的相关出来,这里就先简单介绍下 ftrace。

一、Ftrace简介

1.1Ftrace 是什么

Ftrace 的全称是 Function Tracer,它是一个内部跟踪器,旨在帮助系统的开发人员和设计人员了解内核内部的情况,可用于调试或分析在系统中发生的延迟和性能问题。尽管 Ftrace 通常被认为是函数跟踪器,但实际上它是由几个不同的跟踪实用程序组成的框架。

Ftrace 能帮我们分析内核特定的事件,譬如调度,中断等,也能帮我们去追踪动态的内核函数,以及这些函数的调用栈还有栈的使用这些。它也能帮我们去追踪延迟,譬如中断被屏蔽,抢占被禁止的时间,以及唤醒一个进程之后多久开始执行的时间。

Ftrace 最早在 2008 年由 Steven Rostedt 开发,并在 2.6.27 主线内核中合并进来,它主要源于两个工具,分别是来自 Ingo Molnar 的延迟追踪器和来自 Steven 的 logdev 工具。最初,Ftrace 只是一个简单的函数跟踪器,仅能够记录内核的函数调用流程,比如记录函数的执行流程、测量函数执行时间以发现瓶颈和性能问题、测量实时进程花费的时间并发现延迟问题、测试内核栈使用情况并发现可能的栈溢出以及检查禁用和启用中断之间发生了什么、抢占和从唤醒任务到实际调度任务的时间等。而随着技术的不断发展,如今 Ftrace 已经演变成一个功能丰富的跟踪框架,采用类似插件的方式支持开发人员添加更多种类的跟踪功能,为了解 Linux 内核运行时行为提供了有力的帮助。

1.2Ftrace 的工作原理

Ftrace 的基本工作原理是在内核函数中加入各种探测点,通过这些探测点来跟踪函数的运行信息,随后将跟踪信息打印到内核的一个 Ring Buffer 中,而用户则可以通过 tracefs/debugfs 来访问 Ring Buffer 中的数据。

Ftrace 包含两个核心组成部分:

Framework core(框架核心):它主要起到管理各种 tracer(追踪器)的作用,同时利用 tracefs 文件系统对用户空间提供配置选项以及输出 trace 信息,像是在整个流程中扮演着 “指挥官” 的角色,协调着各部分有序工作,确保数据的准确获取与配置的有效实施。例如,它会负责对内核进行必要的修改、完成 Tracer 的注册以及对 Ring Buffer 的控制等操作。

Tracers(追踪器):Ftrace 实现了多种类型的 tracer,像 Function tracer(函数调用追踪器)、Function graph tracer(函数调用图跟踪器)等都是其中的成员。每一种 tracer 都负责着不同的功能,并且它们统一由 Framework core 管理。具体来说,tracer 负责将 trace 信息保存到 Ring Buffer 中,例如 Function tracer 可以看出哪个函数何时被调用,开发人员还可以通过过滤器指定想要跟踪的函数;Function graph tracer 则能够清晰展现出函数的调用层次关系以及返回情况。

不同的 tracer 可以帮助开发人员从不同角度去分析内核的运行情况,以便更精准地查找问题或者进行性能分析等工作。

1.3Ftrace 的追踪器类型

⑴常见追踪器功能概述

Ftrace 实现了多种类型的追踪器,每一种都有着独特的作用,下面为大家介绍一些常见追踪器的功能:

function 追踪器:这是一个非常实用的追踪器,它的主要作用是可以清晰地看出内核中哪个函数在何时被调用。例如,在分析某个具体功能模块的执行流程时,开发人员若想知道涉及到的各个函数的调用顺序以及调用时间点,就可以启用 function 追踪器,还能通过过滤器指定想要跟踪的函数,精准获取关注的函数调用情况,方便排查函数调用顺序是否符合预期,有没有出现异常调用等问题。

function_graph 追踪器:它在展示函数调用关系方面表现出色,能够清晰展现出函数的调用层次关系以及返回情况,呈现出类似树形结构或者流程图式的调用关系图。比如在研究复杂的内核模块间相互调用逻辑时,借助 function_graph 追踪器,开发人员可以直观地看到函数之间是如何嵌套调用的,哪个函数调用了哪些子函数,以及各个函数执行完毕后是如何返回的,有助于梳理整个代码的执行脉络,定位可能存在的逻辑错误或者性能瓶颈所在的调用链路。

nop 追踪器:比较特殊,它不会跟踪任何内核活动。不过它有一个重要的用途,就是将 nop 写入 current_tracer 文件时,可以删除之前所使用的追踪器,并清空之前收集到的跟踪信息,起到刷新 trace 文件的作用。就好比是对之前跟踪记录做一次 “清零” 操作,便于开启新的跟踪任务或者重新调整跟踪设置。

irqsoff 追踪器:主要用于跟踪关闭中断信息,并能够记录下关闭中断的最大时长。在系统出现响应延迟等问题,怀疑是中断关闭时间过长影响了对外界事件的响应时,启用 irqsoff 追踪器,就能明确知道是哪些函数执行了中断关闭操作以及关闭的最长时间,从而分析是否是不合理的中断关闭导致了系统故障或者性能问题。

preemptoff 追踪器:负责追踪关闭禁止抢占信息,同时记录关闭的最大时长。当遇到内核抢占相关的问题,比如某些任务没有按照预期被及时调度执行,怀疑是抢占被禁止且时间过长造成的,使用 preemptoff 追踪器可以帮助定位是哪些函数进行了抢占禁止操作以及禁止的时长情况,辅助开发人员判断是否是抢占机制出现异常影响了系统正常的任务调度和执行。

⑵不同追踪器适用场景

不同的追踪器适用于不同的分析场景,了解它们各自的适用情况,能帮助我们更高效准确地使用 Ftrace 进行内核相关问题的排查和分析:

分析内核函数调用方面:如果只是简单想知道函数何时被调用,关注函数调用的时间顺序等基本情况,function 追踪器就足够了。例如在排查某个新添加的内核函数是否在正确的时机被调用,通过设置 function 追踪器并指定该函数进行跟踪,查看跟踪结果就能清晰知晓。而要是需要深入了解函数之间复杂的调用层次、嵌套关系以及返回情况,像分析一个大型内核模块内部众多函数之间的交互逻辑,function_graph 追踪器则是更好的选择。它能像绘制流程图一样将函数调用关系直观展示出来,便于开发人员把握整体的代码执行路径和逻辑走向。

进程调度场景下:wakeup 追踪器常用于查看普通进程从被唤醒到真正得到执行之间的延迟情况,比如在排查某些进程出现长时间等待才能执行的问题时,启用 wakeup 追踪器,分析其记录的进程唤醒延迟信息,就能判断是调度器本身的问题,还是其他进程占用资源过多等原因导致的延迟。对于实时进程的调度延迟分析,则可以使用 wakeup_rt 追踪器,它和 wakeup 追踪器类似,但专门针对实时进程,实时进程对调度的及时性要求更高,通过 wakeup_rt 追踪器可以精准获取实时进程的调度延迟数据,保障实时进程能按要求及时执行。

中断处理相关情况分析时:当怀疑系统响应延迟、卡顿等问题是由于中断关闭不合理导致时,irqsoff 追踪器就能派上用场了。它可以记录关闭中断的相关信息以及最大时长,开发人员通过查看追踪结果,能够确定是哪些函数执行了中断关闭操作,进而判断中断关闭时长是否超出合理范围,是否需要对相应的代码进行优化调整,确保中断机制正常运作,系统能及时响应外部事件。

硬件延迟检测方面:hwlat 追踪器可用于检测硬件是否产生任何延迟。例如在对硬件设备进行性能优化或者排查硬件相关的故障时,使用 hwlat 追踪器,查看硬件操作过程中是否存在延迟异常情况,从而判断是硬件本身的问题,还是软件层面与硬件交互等环节出现了问题,辅助进行针对性的优化和修复。

二、Ftrace如何使用

要使用 ftrace,首先就是需要将系统的 debugfs 或者 tracefs 给挂载到某个地方,幸运的是,几乎所有的 Linux 发行版,都开启了 debugfs/tracefs 的支持,所以我们也没必要去重新编译内核了。

在比较老的内核版本,譬如 CentOS 7 的上面,debugfs 通常被挂载到 /sys/kernel/debug 上面(debug 目录下面有一个 tracing 目录),而比较新的内核,则是将 tracefs 挂载到 /sys/kernel/tracing,无论是什么,我都喜欢将 tracing 目录直接 link 到 /tracing。后面都会假设直接进入了 /tracing 目录,后面,我会使用 Ubuntu 16.04 来举例子,内核版本是 4.13 来举例子。

在使用ftrace之前,需要内核进行支持,也就是内核需要打开编译中的ftrace相关选项,关于怎么激活ftrace选项的问题,可以google之,下面只说明重要的设置步骤:

mkdir /debug;mount -t debugs nodev /debug; /*挂载debugfs到创建的目录中去*/cd /debug; cd tracing; /*如果没有tracing目录,则内核目前还没有支持ftrace,需要配置参数,重新编译*/。echo nop > current_tracer;//清空tracerecho function_graph > current_tracer;//使用图形显示调用关系echo ip_rcv > set_graph_function;//设置过滤函数,可以设置多个echo 1 > tracing_enabled开始追踪

传统 Tracer 的使用

使用传统的 ftrace 需要如下几个步骤:

  • 选择一种 tracer

  • 使能 ftrace

  • 执行需要 trace 的应用程序,比如需要跟踪 ls,就执行 ls

  • 关闭 ftrace

  • 查看 trace 文件

用户通过读写 debugfs 文件系统中的控制文件完成上述步骤。使用 debugfs,首先要挂载它。命令如下:

# mkdir /debug 
 # mount -t debugfs nodev /debug

此时您将在 /debug 目录下看到 tracing 目录。Ftrace 的控制接口就是该目录下的文件。

选择 tracer 的控制文件叫作 current_tracer 。选择 tracer 就是将 tracer 的名字写入这个文件,比如,用户打算使用 function tracer,可输入如下命令:

#echo ftrace > /debug/tracing/current_tracer

文件 tracing_enabled 控制 ftrace 的开始和结束。

#echo 1 >/debug/tracing/tracing_enable

上面的命令使能 ftrace 。同样,将 0 写入 tracing_enable 文件便可以停止 ftrace 。

ftrace 的输出信息主要保存在 3 个文件中。

  • Trace,该文件保存 ftrace 的输出信息,其内容可以直接阅读。

  • latency_trace,保存与 trace 相同的信息,不过组织方式略有不同。主要为了用户能方便地分析系统中有关延迟的信息。

  • trace_pipe 是一个管道文件,主要为了方便应用程序读取 trace 内容。算是扩展接口吧。

我们使用 ls 来看看目录下面到底有什么:

README                      current_tracer            hwlat_detector   per_cpu              set_event_pid       snapshot            trace_marker      tracing_max_latency
available_events            dyn_ftrace_total_info     instances        printk_formats       set_ftrace_filter   stack_max_size      trace_marker_raw  tracing_on
available_filter_functions  enabled_functions         kprobe_events    saved_cmdlines       set_ftrace_notrace  stack_trace         trace_options     tracing_thresh
available_tracers           events                    kprobe_profile   saved_cmdlines_size  set_ftrace_pid      stack_trace_filter  trace_pipe        uprobe_events
buffer_size_kb              free_buffer               max_graph_depth  saved_tgids          set_graph_function  trace               trace_stat        uprobe_profile
buffer_total_size_kb        function_profile_enabled  options          set_event            set_graph_notrace   trace_clock         tracing_cpumask

可以看到,里面有非常多的文件和目录,具体的含义,大家可以去详细的看官方文档的解释,后面只会重点介绍一些文件。

2.1功能

我们可以通过 available_tracers 这个文件知道当前 ftrace 支持哪些插件。

cat available_tracers
hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop

通常用的最多的就是function和function_graph,当然,如果我们不想 trace 了,可以使用nop。我们首先打开function:

echo function > current_tracer
cat current_tracer
function

上面我们将 function 写入到了 current_tracer 来开启 function 的 trace,我通常会在 cat 下 current_tracer 这个文件,主要是防止自己写错了。然后 ftrace 就开始工作了,会将相关的 trace 信息放到 trace 文件里面,我们直接读取这个文件就能获取相关的信息。

cat trace | head -n 15
# tracer: function
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
            bash-29409 [002] .... 16158426.215771: mutex_unlock <-tracing_set_tracer
          -0     [039] d... 16158426.215771: call_cpuidle <-do_idle
          -0     [039] d... 16158426.215772: cpuidle_enter <-call_cpuidle
          -0     [039] d... 16158426.215773: cpuidle_enter_state <-cpuidle_enter
            bash-29409 [002] .... 16158426.215773: __fsnotify_parent <-vfs_write
          -0     [039] d... 16158426.215773: sched_idle_set_state <-cpuidle_enter_state

我们可以设置只跟踪特定的 function

echo schedule > set_ftrace_filter
cat set_ftrace_filter
schedule
cat trace | head -n 15
# tracer: function
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
            bash-29409 [010] .... 16158462.591708: schedule <-schedule_timeout
   kworker/u81:2-29339 [008] .... 16158462.591736: schedule <-worker_thread
            sshd-29395 [012] .... 16158462.591788: schedule <-schedule_hrtimeout_range_clock
       rcu_sched-9     [010] .... 16158462.595475: schedule <-schedule_timeout
            java-23597 [006] .... 16158462.600326: schedule <-futex_wait_queue_me
            java-23624 [020] .... 16158462.600855: schedule <-schedule_hrtimeout_range_clock

当然,如果我们不想 trace schedule 这个函数,也可以这样做:

echo '!schedule' > set_ftrace_filter

或者也可以这样做:

echo schedule > set_ftrace_notrace

Function filter 的设置也支持 *match,match* ,*match* 这样的正则表达式,譬如我们可以 echo '*lock*' < set_ftrace_notrace 来禁止跟踪带 lock 的函数,set_ftrace_notrace 文件里面这时候就会显示:

cat set_ftrace_notrace
xen_pte_unlock
read_hv_clock_msr
read_hv_clock_tsc
update_persistent_clock
read_persistent_clock
set_task_blockstep
user_enable_block_step
...

2.2函数图

相比于 function,function_graph 能让我们更加详细的去知道内核函数的上下文,我们打开 function_graph:

echo function_graph > current_tracer
cat trace | head -15
# tracer: function_graph
#
# CPU  DURATION                  FUNCTION CALLS
# |     |   |                     |   |   |   |
 10)   0.085 us    |  sched_idle_set_state();
 10)               |  cpuidle_reflect() {
 10)   0.035 us    |    menu_reflect();
 10)   0.288 us    |  }
 10)               |  rcu_idle_exit() {
 10)   0.034 us    |    rcu_dynticks_eqs_exit();
 10)   0.296 us    |  }
 10)   0.032 us    |  arch_cpu_idle_exit();
 10)               |  tick_nohz_idle_exit() {
 10)   0.073 us    |    ktime_get();
 10)               |    update_ts_time_stats() {
  
  

我们也可以只跟踪某一个函数的堆栈

echo kfree > set_graph_function
cat trace | head -n 15
# tracer: function_graph
#
# CPU  DURATION                  FUNCTION CALLS
# |     |   |                     |   |   |   |
 16)               |  kfree() {
 16)   0.147 us    |    __slab_free();
 16)   1.437 us    |  }
 10)   0.162 us    |  kfree();
 17) @ 923817.3 us |          } /* intel_idle */
 17)   0.044 us    |          sched_idle_set_state();
 17)   ==========> |
 17)               |          smp_apic_timer_interrupt() {
 17)               |            irq_enter() {
 17)               |              rcu_irq_enter() {
 17)   0.049 us    |                rcu_dynticks_eqs_exit();

2.3事件

上面提到了 function 的 trace,在 ftrace 里面,另外用的多的就是 event 的 trace,我们可以在 events 目录下面看支持哪些事件:

ls events/
alarmtimer  cma         ext4      fs_dax        i2c          kvm      mmc     nmi             printk        regulator  smbus       task                     vmscan     xdp
block       compaction  fib       ftrace        iommu        kvmmmu   module  oom             random        rpm        sock        thermal                  vsyscall   xen
bpf         cpuhp       fib6      gpio          irq          libata   mpx     page_isolation  ras           sched      spi         thermal_power_allocator  wbt        xhci-hcd
btrfs       dma_fence   filelock  header_event  irq_vectors  mce      msr     pagemap         raw_syscalls  scsi       swiotlb     timer                    workqueue
cgroup      enable      filemap   header_page   jbd2         mdio     napi    percpu          rcu           signal     sync_trace  tlb                      writeback
clk         exceptions  fs        huge_memory   kmem         migrate  net     power           regmap        skb        syscalls    udp                      x86_fpu

上面列出来的都是分组的,我们可以继续深入下去,譬如下面是查看sched相关的事件:

ls events/sched/
enable                  sched_migrate_task  sched_process_exit  sched_process_wait  sched_stat_sleep  sched_switch                 sched_wakeup_new
filter                  sched_move_numa     sched_process_fork  sched_stat_blocked  sched_stat_wait   sched_wait_task              sched_waking
sched_kthread_stop      sched_pi_setprio    sched_process_free  sched_stat_iowait   sched_stick_numa  sched_wake_idle_without_ipi
sched_kthread_stop_ret  sched_process_exec  sched_process_hang  sched_stat_runtime  sched_swap_numa   sched_wakeup

对于某一个具体的事件,我们也可以查看:

ls events/sched/sched_wakeup
enable  filter  format  hist  id  trigger

不知道大家注意到了没有,上述目录里面,都有一个enable的文件,我们只需要往里面写入 1,就可以开始 trace 这个事件。譬如下面就开始 tracesched_wakeup这个事件:

echo 1 > events/sched/sched_wakeup/enable
cat trace | head -15
# tracer: nop
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
            bash-29409 [012] d... 16158657.832294: sched_wakeup: comm=kworker/u81:1 pid=29359 prio=120 target_cpu=008
   kworker/u81:1-29359 [008] d... 16158657.832321: sched_wakeup: comm=sshd pid=29395 prio=120 target_cpu=010
          -0     [012] dNs. 16158657.835922: sched_wakeup: comm=rcu_sched pid=9 prio=120 target_cpu=012
          -0     [024] dNh. 16158657.836908: sched_wakeup: comm=java pid=23632 prio=120 target_cpu=024
          -0     [022] dNh. 16158657.839921: sched_wakeup: comm=java pid=23624 prio=120 target_cpu=022
          -0     [016] dNh. 16158657.841866: sched_wakeup: comm=java pid=23629 prio=120 target_cpu=016

我们也可以 tracesched里面的所有事件:

echo 1 > events/sched/enable
cat trace | head -15
# tracer: nop
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
            bash-29409 [010] d... 16158704.468377: sched_waking: comm=kworker/u81:2 pid=29339 prio=120 target_cpu=008
            bash-29409 [010] d... 16158704.468378: sched_stat_sleep: comm=kworker/u81:2 pid=29339 delay=164314267 [ns]
            bash-29409 [010] d... 16158704.468379: sched_wake_idle_without_ipi: cpu=8
            bash-29409 [010] d... 16158704.468379: sched_wakeup: comm=kworker/u81:2 pid=29339 prio=120 target_cpu=008
            bash-29409 [010] d... 16158704.468382: sched_stat_runtime: comm=bash pid=29409 runtime=360343 [ns] vruntime=131529875864926 [ns]
            bash-29409 [010] d... 16158704.468383: sched_switch: prev_comm=bash prev_pid=29409 prev_prio=120 prev_state=S ==> next_comm=swapper/10 next_pid=0 next_prio=120

当然也可以 trace 所有的事件:

echo 1 > events/enable
cat trace | head -15
# tracer: nop
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
            bash-29409 [010] .... 16158761.584188: writeback_mark_inode_dirty: bdi (unknown): ino=3089 state=I_DIRTY_SYNC|I_DIRTY_DATASYNC|I_DIRTY_PAGES flags=I_DIRTY_SYNC|I_DIRTY_DATASYNC|I_DIRTY_PAGES
            bash-29409 [010] .... 16158761.584189: writeback_dirty_inode_start: bdi (unknown): ino=3089 state=I_DIRTY_SYNC|I_DIRTY_DATASYNC|I_DIRTY_PAGES flags=I_DIRTY_SYNC|I_DIRTY_DATASYNC|I_DIRTY_PAGES
            bash-29409 [010] .... 16158761.584190: writeback_dirty_inode: bdi (unknown): ino=3089 state=I_DIRTY_SYNC|I_DIRTY_DATASYNC|I_DIRTY_PAGES flags=I_DIRTY_SYNC|I_DIRTY_DATASYNC|I_DIRTY_PAGES
            bash-29409 [010] .... 16158761.584193: do_sys_open: "trace" 8241 666
            bash-29409 [010] .... 16158761.584193: kmem_cache_free: call_site=ffffffff8e862614 ptr=ffff91d241fa4000
            bash-29409 [010] .... 16158761.584194: sys_exit: NR 2 = 3

2.4trace-cmd

从上面的例子可以看到,其实使用 ftrace 并不是那么方便,我们需要手动的去控制多个文件,但幸运的是,我们有 trace-cmd,作为 ftrace 的前端,trace-cmd 能够非常方便的让我们进行 ftrace 的操作,譬如我们可以使用 record 命令来 trace sched 事件:

trace-cmd record -e sched

然后使用report命令来查看 trace 的数据:

trace-cmd report | head -10
version = 6
CPU 27 is empty
cpus=40
       trace-cmd-29557 [003] 16159201.985281: sched_waking:         comm=kworker/u82:3 pid=28507 prio=120 target_cpu=037
       trace-cmd-29557 [003] 16159201.985283: sched_migrate_task:   comm=kworker/u82:3 pid=28507 prio=120 orig_cpu=37 dest_cpu=5
       trace-cmd-29557 [003] 16159201.985285: sched_stat_sleep:     comm=kworker/u82:3 pid=28507 delay=137014529 [ns]
       trace-cmd-29585 [023] 16159201.985286: sched_stat_runtime:   comm=trace-cmd pid=29585 runtime=217630 [ns] vruntime=107586626253137 [ns]
       trace-cmd-29557 [003] 16159201.985286: sched_wake_idle_without_ipi: cpu=5
       trace-cmd-29595 [037] 16159201.985286: sched_stat_runtime:   comm=trace-cmd pid=29595 runtime=213227 [ns] vruntime=105364596011783 [ns]
       trace-cmd-29557 [003] 16159201.985287: sched_wakeup:         kworker/u82:3:28507 [120] success=1 CPU:005

当然,在report的时候也可以加入自己的 filter 来过滤数据,譬如下面,我们就过滤出sched_wakeup事件并且是success为 1 的。

trace-cmd report -F 'sched/sched_wakeup: success == 1'  | head -10
version = 6
CPU 27 is empty
cpus=40
       trace-cmd-29557 [003] 16159201.985287: sched_wakeup:         kworker/u82:3:28507 [120] success=1 CPU:005
       trace-cmd-29557 [003] 16159201.985292: sched_wakeup:         trace-cmd:29561 [120] success=1 CPU:007
          -0     [032] 16159201.985294: sched_wakeup:         qps_json_driver:24669 [120] success=1 CPU:032
          -0     [032] 16159201.985298: sched_wakeup:         trace-cmd:29590 [120] success=1 CPU:026
          -0     [010] 16159201.985300: sched_wakeup:         trace-cmd:29563 [120] success=1 CPU:010
       trace-cmd-29597 [037] 16159201.985302: sched_wakeup:         trace-cmd:29595 [120] success=1 CPU:039
          -0     [010] 16159201.985302: sched_wakeup:         sshd:29395 [120] success=1 CPU:010

大家可以注意下success == 1,这其实是一个对事件里面 field 进行的表达式运算了,对于不同的事件,我们可以通过查看其 format 来知道它的实际 fields 是怎样的,譬如:

cat events/sched/sched_wakeup/format
name: sched_wakeup
ID: 294
format:
    field:unsigned short common_type;   offset:0;   size:2; signed:0;
    field:unsigned char common_flags;   offset:2;   size:1; signed:0;
    field:unsigned char common_preempt_count;   offset:3;   size:1; signed:0;
    field:int common_pid;   offset:4;   size:4; signed:1;

    field:char comm[16];    offset:8;   size:16;    signed:1;
    field:pid_t pid;    offset:24;  size:4; signed:1;
    field:int prio; offset:28;  size:4; signed:1;
    field:int success;  offset:32;  size:4; signed:1;
    field:int target_cpu;   offset:36;  size:4; signed:1;

print fmt: "comm=%s pid=%d prio=%d target_cpu=%03d", REC->comm, REC->pid, REC->p

三、Ftrace的探测技术

3.1静态探针机制

静态探测是在内核代码中调用 Ftrace 提供的相应接口来实现的,之所以称之为静态,是因为这种探测方式是在内核代码里写死的,会静态编译到内核代码中。一旦内核编译完成,就没办法再进行动态修改了。

这些在内核代码里插入的用于探测的 tracepoint(跟踪点),主要是为了支持 Event tracing(事件跟踪)。并且,这些 tracepoint 有着对应的开关,而这个开关是由内核配置选项来进行控制的。值得一提的是,内核开发人员已经提前在一些关键的地方设置好了静态探测点,当使用者有相应需求,开启相关功能后,就可以查看这些地方的探测信息了,方便对内核特定关键环节进行分析和调试。

3.2动态探针机制

动态探测则利用了 GCC 编译的 profile 特性来发挥作用。在 Linux 内核编译之时,会在每个函数入口处预留数个字节。等到实际使用 Ftrace 时,就可以将之前预留的这些字节替换为所需要的指令,例如替换为能够跳转到需要执行探测操作代码处的指令,从而实现探测功能。

像我们熟知的 function tracer(函数调用追踪器)、function graph tracer(函数调用图跟踪器)等追踪器,都是基于这种动态探测机制实现的。通过这样的动态探测机制,开发人员可以更加灵活地根据具体需求去对内核函数进行跟踪,获取相应的运行信息,为查找性能问题、分析代码执行逻辑等工作提供有力支持。

四、Ftrace的使用方法

4.1ftrace 的整体构架

通过Ftrace实现高效、精确的内核调试与分析_第1张图片

Ftrace 有两大组成部分,一是 framework,另外就是一系列的 tracer 。每个 tracer 完成不同的功能,它们统一由 framework 管理。ftrace 的 trace 信息保存在 ring buffer 中,由 framework 负责管理。Framework 利用 debugfs 系统在 /debugfs 下建立 tracing 目录,并提供了一系列的控制文件。

一句话总结:各类tracer往ftrace主框架注册,不同的trace则在不同的probe点把信息通过probe函数给送到ring buffer中,再由暴露在用户态debufs实现相关控制。对不同tracer来说:

  • 1)需要实现probe点(需要跟踪的代码侦测点),有的probe点需要静态代码实现,有的probe点借助编译器在运行时动态替换,event tracing属于前者;

  • 2)还要实现具体的probe函数,把需要记录的信息送到ring buffer中;

  • 3)增加debugfs 相关的文件,实现信息的解析和控制。

而ring buffer 和debugfs的通用部分的管理由框架负责。

4.2安装步骤

⑴配置前准备

在使用 Ftrace 之前,需要进行一些内核配置操作,以确保能正常访问 Ftrace 的控制文件,以下是具体步骤:

首先,要激活 debugfs,这可以通过内核配置选项 CONFIG_DEBUG_FS=y 来实现。激活后,系统在启动时会创建 /sys/kernel/debug 目录,debugfs 文件系统将会挂载到该目录下。手动挂载的命令示例如下:

mount -t debugfs nodev /sys/kernel/debug

或者也可以选择将下面这行内容添加到 /etc/fstab 文件中,实现系统启动自动挂载:

debugfs  /sys/kernel/debug  debugfs  defaults  0  0

另外,还需要挂载 tracefs 文件系统,tracefs 是 Ftrace 操作所在的虚拟文件系统,其挂载点通常在 /sys/kernel/debug/tracing 下。若没有自动挂载,可以使用如下命令手动挂载:

mount -t tracefs /sys/kernel/debug/tracing

完成上述挂载操作后,就可以进入 /sys/kernel/debug/tracing 目录去访问和操作 Ftrace 相关的各种控制文件与配置选项了,比如 current_tracer(用于设置或显示当前追踪器)、available_tracers(查看内核支持的可用追踪器列表)等文件,后续的 Ftrace 使用都是基于这些文件来进行相应的配置和操作的。

⑵基本操作流程

Ftrace 的基本使用步骤如下:

  1. 选择追踪器:进入 /sys/kernel/debug/tracing 目录后,通过查看 available_tracers 文件,可以了解当前内核中支持哪些追踪器。例如,使用 cat available_tracers 命令,可能会列出诸如 function_graph、function、nop、irqsoff 等追踪器名称(不同平台和内核配置下支持的追踪器会有不同)。然后根据自己的分析需求,选择合适的追踪器,将其名称写入 current_tracer 文件来设置当前使用的追踪器。比如,若想使用 function 追踪器查看函数调用情况,就可以执行 echo function > current_tracer 命令。

  2. 设置追踪器参数(可选):部分追踪器支持设置一些参数来进一步限定跟踪范围等。例如,对于 function 追踪器,如果只想跟踪特定的函数,可以通过 set_ftrace_filter 文件来指定。假设要跟踪名为 my_function 的函数,就可以使用 echo my_function > set_ftrace_filter 命令。而且,这里还支持一些简单的通配符表达式,像 echo '*lock*' > set_ftrace_notrace 就可以指定不跟踪带 lock 的函数(前提是这些函数在内核配置允许跟踪的函数列表中,可查看 available_filter_functions 文件了解)。

  3. 使能追踪器:通过操作 tracing_on 文件来控制追踪的开启和关闭,向该文件写入 1 即开启追踪,写入 0 则关闭追踪。例如,执行 echo 1 > tracing_on 命令后,Ftrace 就开始记录相应的跟踪信息了。

  4. 关闭追踪器:当完成跟踪任务后,为避免不必要的系统开销,可以关闭追踪器。同样是向 tracing_on 文件写入 0,像 echo 0 > tracing_on 这样的命令操作即可关闭追踪功能。另外,如果想彻底清除之前的跟踪记录并重置追踪器相关设置,可以将 nop 写入 current_tracer 文件,命令示例为 echo nop > current_tracer,它能起到刷新 trace 文件的作用,便于下次重新进行跟踪任务的配置和使用。

⑶跟踪结果查看与分析

当追踪器运行一段时间并记录了相应的跟踪信息后,就可以查看和分析这些跟踪结果了。

查看跟踪记录主要通过 trace 和 trace_pipe 这两个文件:

trace 文件:它以人类可读的格式保存跟踪的输出信息,可以直接使用 cat 命令查看,如 cat trace,不过要注意在读取(打开)此文件时会暂时禁用跟踪功能。

例如,从该文件中我们可以看到类似如下格式的内容:

# tracer: function
#
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
my_process-1234    0      1d...  1234567890  my_function

这里能看到进程名称(my_process)、进程 PID(1234)、所在 CPU 编号(0)、时间戳(1234567890)以及被跟踪到的函数名称(my_function)等信息,通过分析这些内容可以了解函数的调用顺序、在哪个 CPU 上运行等情况,帮助排查如函数调用异常、性能瓶颈相关的问题。

trace_pipe 文件:它输出的内容与 trace 文件相同,但此文件适用于实时跟踪进行流式处理,也就是会随着新数据的产生实时输出,并且一旦从该文件读取数据,相应的数据就会被消耗(不会像 trace 文件那样固定显示之前的内容)。使用方式可以是通过类似 cat trace_pipe 的命令查看实时输出信息,或者将其输出重定向到其他文件保存下来以便后续详细分析,比如 cat trace_pipe > my_trace_result.txt。

从输出信息中提取有用内容辅助分析方面,常见的有用信息包含进程信息(如进程名、PID 等)、函数调用关系(哪个函数调用了哪些函数等)、时间戳(判断函数调用的先后顺序以及时间间隔,进而分析性能问题)等。例如,在排查性能问题时,通过查看时间戳可以知道某个函数执行耗费的时长是否过长;分析函数调用关系能够梳理出代码的执行脉络,判断是否符合预期逻辑,有没有出现不合理的嵌套调用或者遗漏调用等情况,从而定位可能存在的故障点或者性能瓶颈所在的具体环节,为后续的优化和问题修复提供依据。

4.3Function tracer 的实现

Ftrace 采用 GCC 的 profile 特性在所有内核函数的开始部分加入一段 stub 代码,ftrace 重载这段代码来实现 trace 功能。gcc 的 -pg 选项将在每个函数入口处加入对 mcount 的调用代码。比如下面的 C 代码。

清单 1. test.c

//test.c 
 void foo(void) 
 { 
   printf( “ foo ” ); 
 }

用 gcc 编译:

gcc – S test.c

反汇编如下:

清单 2. test.c 不加入 pg 选项的汇编代码

_foo: 
        pushl   %ebp 
        movl    %esp, %ebp 
        subl    $8, %esp 
        movl    $LC0, (%esp) 
        call    _printf 
        leave 
        ret

再加入 -gp 选项编译:

gcc –pg –S test.c

得到的汇编如下:

清单 3. test.c 加入 pg 选项后的汇编代码

_foo: 
        pushl   %ebp 
        movl    %esp, %ebp 
        subl    $8, %esp 
 LP3: 
        movl    $LP3,%edx 
        call    _mcount 
        movl    $LC0, (%esp) 
        call    _printf 
        leave 
        ret

增加 pg 选项后,gcc 在函数 foo 的入口处加入了对 mcount 的调用:call _mcount 。原本 mcount 由 libc 实现,但您知道内核不会连接 libc 库,因此 ftrace 编写了自己的 mcount stub 函数,并借此实现 trace 功能。

在每个内核函数入口加入 trace 代码,必然会影响内核的性能,为了减小对内核性能的影响,ftrace 支持动态 trace 功能。

当 CONFIG_DYNAMIC_FTRACE 被选中后,内核编译时会调用一个 perl 脚本:recordmcount.pl 将每个函数的地址写入一个特殊的段:__mcount_loc

在内核初始化的初期,ftrace 查询 __mcount_loc 段,得到每个函数的入口地址,并将 mcount 替换为 nop 指令。这样在默认情况下,ftrace 不会对内核性能产生影响。

当用户打开 ftrace 功能时,ftrace 将这些 nop 指令动态替换为 ftrace_caller,该函数将调用用户注册的 trace 函数。其具体的实现在相应 arch 的汇编代码中,以 x86 为例,在 entry_32.s 中:

清单 4. entry_32.s

ENTRY(ftrace_caller) 
       cmpl $0, function_trace_stop 
       jne  ftrace_stub 
       pushl %eax 
       pushl %ecx 
       pushl %edx 
       movl 0xc(%esp), %eax 
       movl 0x4(%ebp), %edx 
       subl $MCOUNT_INSN_SIZE, %eax 
 .globl ftrace_call 
 ftrace_call: 
       call ftrace_stubline 10popl %edx 
       popl %ecx 
       popl %eax 

 .globl ftrace_stub 
 ftrace_stub: 
       ret 
 END(ftrace_caller)

Function tracer 将 line10 这行代码替换为 function_trace_call() 。这样每个内核函数都将调用 function_trace_call() 。

在 function_trace_call() 函数内,ftrace 记录函数调用堆栈信息,并将结果写入 ring buffer,稍后,用户可以通过 debugfs 的 trace 文件读取该 ring buffer 中的内容。

4.4Irqsoff tracer 的实现

Irqsoff tracer 的实现依赖于 IRQ-Flags 。IRQ-Flags 是 Ingo Molnar 维护的一个内核特性。使得用户能够在中断关闭和打开时得到通知,ftrace 重载了其通知函数,从而能够记录中断禁止时间。即,中断被关闭时,记录下当时的时间戳。此后,中断被打开时,再计算时间差,由此便可得到中断禁止时间。

IRQ-Flags 封装开关中断的宏定义:

清单 5. IRQ-Flags 中断代码

#define local_irq_enable() \ 
    do { trace_hardirqs_on (); raw_local_irq_enable(); } while (0)

ftrace 在文件 ftrace_irqsoff.c 中重载了 trace_hardirqs_on 。具体代码不再罗列,主要是使用了 sched_clock()函数来获得时间戳。

4.5hw-branch 的实现

Hw-branch 只在 IA 处理器上实现,依赖于 x86 的 BTS 功能。BTS 将 CPU 实际执行到的分支指令的相关信息保存下来,即每个分支指令的源地址和目标地址。

软件可以指定一块 buffer,处理器将每个分支指令的执行情况写入这块 buffer,之后,软件便可以分析这块 buffer 中的功能。

Linux 内核的 DS 模块封装了 x86 的 BTS 功能。Debug Support 模块封装了和底层硬件的接口,主要支持两种功能:Branch trace store(BTS) 和 precise-event based sampling (PEBS) 。ftrace 主要使用了 BTS 功能。

4.6branch tracer 的实现

内核代码中常使用 likely 和 unlikely 提高编译器生成的代码质量。Gcc 可以通过合理安排汇编代码最大限度的利用处理器的流水线。合理的预测是 likely 能够提高性能的关键,ftrace 为此定义了 branch tracer,跟踪程序中 likely 预测的正确率。

为了实现 branch tracer,重新定义了 likely 和 unlikely 。具体的代码在 compiler.h 中。

清单 6. likely/unlikely 的 trace 实现

# ifndef likely 
 #  define likely(x) (__builtin_constant_p(x) ? !!(x) : __branch_check__(x, 1)) 
 # endif 
 # ifndef unlikely 
 #  define unlikely(x) (__builtin_constant_p(x) ? !!(x) : __branch_check__(x, 0)) 
 # endif

其中 __branch_check 的实现如下:

清单 7. _branch_check_ 的实现

#define __branch_check__(x, expect) ({\ 
    int ______r;    \ 
    static struct ftrace_branch_data \ 
    __attribute__((__aligned__(4)))  \ 
    __attribute__((section("_ftrace_annotated_branch"))) \ 
                         ______f = { \ 
                           .func = __func__, \ 
                           .file = __FILE__, \ 
                           .line = __LINE__, \ 
                    }; \ 
              ______r = likely_notrace(x);\ 
              ftrace_likely_update(&______f, ______r, expect); \ 
              ______r; \ 
  })

ftrace_likely_update() 将记录 likely 判断的正确性,并将结果保存在 ring buffer 中,之后用户可以通过 ftrace 的 debugfs 接口读取分支预测的相关信息。从而调整程序代码,优化性能。

五、Ftrace的应用场景

5.1性能优化方面

在系统性能优化工作中,Ftrace 是一个得力的助手。例如,当开发人员想要优化某个软件系统的性能时,可利用 Ftrace 来找出系统中的性能瓶颈所在。

假设开发一款大型的网络服务应用,在高并发场景下响应时间较长,怀疑是某些核心模块的函数执行效率问题。这时就可以使用 Ftrace 的 function 追踪器,通过在 /sys/kernel/debug/tracing 目录下操作,将 function 写入 current_tracer 文件启用该追踪器。如果只想关注特定模块(比如网络通信模块)里涉及的函数,还能通过 set_ftrace_filter 文件来指定,像 echo 'module:network_communication*' > set_ftrace_filter 这样的命令(假设网络通信模块相关函数命名有特定前缀),就可以筛选出对应函数进行跟踪。

然后开启追踪功能,让系统在高并发场景下运行一段时间后,查看 trace 文件或者实时通过 trace_pipe 文件来获取跟踪信息。从这些信息中,可以清晰看到每个被跟踪函数的调用时间戳、所在 CPU 等情况,进而对比分析出哪些函数的执行耗时较长。比如发现 handle_network_request 函数每次执行时间都远超预期,那么开发人员就可以针对这个函数进行代码优化,比如检查算法复杂度是否过高、是否存在频繁的资源申请释放等情况,通过优化这个函数来提升整个网络服务应用在高并发场景下的性能表现。

5.2故障排查方面

在系统出现故障,尤其是内核相关故障时,Ftrace 能发挥重要作用。比如系统突然崩溃,开发人员需要弄清楚崩溃前的函数调用情况来定位问题根源。

Ftrace 可以记录崩溃前的函数调用栈,为开发人员提供崩溃时的上下文信息。例如,当系统出现内核崩溃,怀疑是某个驱动模块在执行过程中出现了异常导致的。通过之前配置好的 Ftrace(确保已经挂载好相关文件系统并且设置好了合适的追踪器等,如使用 function_graph 追踪器来详细查看函数调用关系和执行流程),在系统下次复现崩溃问题前开启追踪功能,等崩溃发生后,查看记录下来的跟踪数据。

从 trace 文件中,能够看到在崩溃前各个函数是如何依次被调用的,哪个函数可能是最后执行的,或者有没有出现反复调用某个函数却无法返回等异常情况。像是看到 driver_init_function 函数调用后,紧接着就出现了一系列系统报错并最终崩溃,那开发人员就可以重点排查这个驱动初始化函数内部的代码逻辑,检查是否存在内存越界访问、空指针引用等常见的导致内核崩溃的问题,大大提高定位内核故障问题的效率,帮助更快地修复系统故障。

5.3内核开发方面

在内核开发过程中,Ftrace 的应用十分广泛且重要。

比如开发一个新的内核模块,需要验证函数调用的正确性。可以启用 Ftrace 的 function 追踪器,在模块加载和运行过程中,跟踪相关函数的调用情况。查看 trace 文件里记录的函数被调用的顺序、时间以及对应的参数情况(如果有相关记录的话),来确认是否和预期的函数调用逻辑一致。若开发的是一个和系统调用相关的内核功能,想要了解其执行过程中涉及的系统调用细节,利用 Ftrace 的系统调用跟踪功能,就能清晰看到具体触发了哪些系统调用,每个系统调用的传入参数以及返回结果等情况,方便判断是否符合设计预期,有没有出现不该出现的系统调用或者参数传递错误等问题。

再举例来说,在对内核中某个复杂的任务调度模块进行开发优化时,通过 Ftrace 的相关调度追踪器(像 sched_switch 追踪器等),可以观察到不同进程在调度过程中的切换情况、各个进程等待调度的时间等信息,基于这些信息来优化任务调度算法,确保内核能高效合理地进行任务调度,使得系统整体运行更加流畅稳定,由此可见 Ftrace 对于内核开发工作的重要支撑作用。

5.4Ftrace 使用的注意事项

⑴性能影响

在使用 Ftrace 时,需要留意它可能对系统性能产生的影响。由于 Ftrace 的工作机制是对内核函数进行跟踪记录相关信息,尤其是当开启它去跟踪大量函数时,这种数据收集和记录的操作会占用一定的系统资源,比如会消耗 CPU 的运算能力以及占用内存空间来存储跟踪数据等。

例如,在一个高负载运行且本身对性能要求极为苛刻的服务器环境中,如果不加选择地启用 Ftrace 去跟踪众多函数,可能会导致系统响应速度变慢,影响正常业务的开展。所以,建议大家仅在确实有必要进行内核相关分析、调试或者性能排查等情况下才启用 Ftrace,避免因不必要的跟踪给系统性能带来不良影响。

⑵安全问题

安全方面至关重要,对于 Ftrace 而言,要确保只有获得授权的用户能够访问 Ftrace 相关文件。因为 Ftrace 涉及到内核层面的信息跟踪,如果权限把控不当,恶意用户有可能通过获取这些跟踪文件中的数据,分析出系统内核的运行逻辑、函数调用关系等关键信息,进而找到系统潜在的漏洞或者实施其他攻击行为。

比如,在一个企业内部的多用户服务器环境中,若没有对 Ftrace 文件设置合理的访问权限,可能会出现普通用户甚至外部非法用户获取到敏感的内核跟踪数据,这无疑会给整个系统带来严重的安全隐患。所以,在使用过程中,一定要严格配置好权限,保障使用的安全性。

⑶数据存储管理

Ftrace 在运行过程中,跟踪数据会快速增加。这是因为它会持续记录内核函数调用等相关信息,随着时间的推移以及跟踪函数数量的增多,所占用的存储空间会越来越大。

倘若不及时进行清理,当存储空间被大量的跟踪数据占满后,不仅会影响后续 Ftrace 继续正常记录数据,

六、Ftrace与其他工具对比

6.1Ftrace 与 perf 对比

Ftrace 和 perf 都是在 Linux 系统分析中常用的工具,但它们在功能侧重、适用场景以及跟踪方式等方面存在着一定差异。

首先在功能侧重方面,Ftrace 主要用于跟踪函数调用和内核事件,它可以清晰展现函数的调用情况、执行顺序以及内核中各类事件的发生情况等,像是能记录函数何时被调用、中断关闭的相关信息等,帮助开发人员深入了解内核运行时的细节。而 perf 则专注于性能分析,它提供了丰富的性能计数器,可用于剖析系统在不同层面的性能表现,比如能统计出 CPU 在各个函数或者进程上花费的时间占比,进而帮助定位性能瓶颈所在,更侧重于从宏观角度去衡量系统性能情况。

适用场景上,Ftrace 在内核开发、调试以及对内核行为的深度分析场景中表现出色,例如开发人员开发新的内核模块时,可利用 Ftrace 来验证函数调用的正确性;在排查内核相关故障时,借助 Ftrace 记录的函数调用栈等信息定位问题根源。perf 更多地适用于对系统整体性能进行评估和优化的场景,像分析一个大型软件在高并发场景下出现性能问题时,perf 可以通过采样等方式快速定位到是哪些模块或者函数消耗了较多的 CPU 资源,是性能优化工作中常用的利器。

跟踪方式来看,Ftrace 有着静态探针和动态探针机制,静态探针在内核代码中写死并静态编译进去,通过提前设置好的探测点来收集信息;动态探针则利用 GCC 编译的特性在函数入口预留字节,按需替换指令实现探测。perf 基于事件采样原理,每隔固定时间在 CPU 上产生中断,查看对应时刻的 pid、函数等信息并进行统计分析,通过这种抽样的方式来跟踪系统情况,成本相较于 Ftrace 的全面跟踪会低一些,不过准确性上相对弱一点。

6.2Ftrace 与 SystemTap 对比

Ftrace 和 SystemTap 同样都是用于分析内核相关情况的工具,但它们在灵活性、学习成本、脚本编写以及适用人群等方面有着不同之处。

灵活性方面,SystemTap 提供了相对灵活的脚本语言,使用者可以根据自己的需求编写复杂的脚本,能够在函数中的任意位置实现探测,可定制化程度较高,能满足各种特定的、精细化的内核行为分析需求。例如在一些复杂的内核逻辑分析场景中,若需要按照特定条件、特定顺序去探测多个不同位置的信息,SystemTap 就能凭借其脚本编写功能较好地完成任务。而 Ftrace 虽然功能也很强大,但它主要是基于已有的追踪器以及固定的探测机制来工作,更多是在内核既定的框架内进行信息收集,灵活性上相对弱一些,不过它配置简单,能较为便捷地实现一些常规的内核跟踪任务。

学习成本上,SystemTap 由于有着自己独特的脚本语言,需要使用者去学习掌握相关的语法、函数等知识,并且要对内核结构和运行机制有比较深入的理解,才能编写出有效的脚本进行准确的探测,整体学习曲线比较陡,对于初学者来说可能不太容易上手。Ftrace 相对而言学习成本就低很多,它通过简单的文件操作,比如在 /sys/kernel/debug/tracing 目录下对 current_tracer、set_ftrace_filter 等文件进行配置,就能实现基本的跟踪功能,上手难度较小。

脚本编写层面,SystemTap 的脚本编写是其一大特色,使用者可以按照自己设定的规则和逻辑来组织代码实现想要的探测流程,这使得它在应对复杂多变的内核分析场景时更具优势,但同时也要求编写者具备一定的编程能力和经验。Ftrace 基本不需要使用者去编写复杂脚本,更多是依靠选择不同的追踪器以及设置相应追踪器的参数来达到跟踪目的,使用起来更加直观简便。

适用人群角度来看,SystemTap 更适合那些对内核有深入了解、具备一定编程基础且需要对内核行为进行深度定制化分析的专业开发人员或者内核研究人员,他们可以利用 SystemTap 强大的脚本功能去挖掘内核深层次的运行逻辑和问题。Ftrace 则更适用于普通的系统开发人员、运维人员等,在日常的内核调试、简单的性能分析以及常规的故障排查等场景中,能够快速便捷地获取到所需的内核相关信息,帮助他们定位和解决问题。

七、实战案例——隐藏的电灯开关

7.1iosnoop

首先,Gregg 使用 iosnoop 工具进行检查,iosnoop 用来跟踪 I/O 的详细信息,当然也包括 latency,结果如下:

# ./iosnoop -ts
STARTs          ENDs            COMM         PID    TYPE DEV      BLOCK        BYTES     LATms
13370264.614265 13370264.614844 java         8248   R    202,32   1431244248   45056      0.58
13370264.614269 13370264.614852 java         8248   R    202,32   1431244336   45056      0.58
13370264.614271 13370264.614857 java         8248   R    202,32   1431244424   45056      0.59
13370264.614273 13370264.614868 java         8248   R    202,32   1431244512   45056      0.59
[...]
# ./iosnoop -Qts
STARTs          ENDs            COMM         PID    TYPE DEV      BLOCK        BYTES     LATms
13370410.927331 13370410.931182 java         8248   R    202,32   1596381840   45056      3.85
13370410.927332 13370410.931200 java         8248   R    202,32   1596381928   45056      3.87
13370410.927332 13370410.931215 java         8248   R    202,32   1596382016   45056      3.88
13370410.927332 13370410.931226 java         8248   R    202,32   1596382104   45056      3.89
[...]

上面看不出来啥,一个繁忙的 I/O,势必会带来高的 latency。我们来说说 iosnoop 是如何做的。
iosnoop 主要是处理的 block 相关的 event,主要是:

  • block:block_rq_issue - I/O 发起的时候的事件

  • block:block_rq_complete - I/O 完成事件

  • block:block_rq_insert - I/O 加入队列的时间

如果使用了 -Q 参数,我们对于 I/O 开始事件就用 block:block_rq_insert,否则就用的 block:block_rq_issue 。下面是我用 FIO 测试 trace 的输出:

             fio-30749 [036] 5651360.257707: block_rq_issue:       8,0 WS 4096 () 1367650688 + 8 [fio]
          -0     [036] 5651360.257768: block_rq_complete:    8,0 WS () 1367650688 + 8 [0]

我们根据1367650688 + 8能找到对应的 I/O block sector,然后根据 issue 和 complete 的时间就能知道 latency 了。

7.2t点

为了更好的定位 I/O 问题,Gregg 使用 tpoint 来追踪 block_rq_insert,如下:

# ./tpoint -H block:block_rq_insert
Tracing block:block_rq_insert. Ctrl-C to end.
# tracer: nop
#
#       TASK-PID    CPU#    TIMESTAMP  FUNCTION
#          | |       |          |         |
        java-16035 [000] 13371565.253582: block_rq_insert: 202,16 WS 0 () 550505336 + 88 [java]
        java-16035 [000] 13371565.253582: block_rq_insert: 202,16 WS 0 () 550505424 + 56 [java]
        java-8248  [007] 13371565.278372: block_rq_insert: 202,32 R 0 () 660621368 + 88 [java]
        java-8248  [007] 13371565.278373: block_rq_insert: 202,32 R 0 () 660621456 + 88 [java]
        java-8248  [007] 13371565.278374: block_rq_insert: 202,32 R 0 () 660621544 + 24 [java]
        java-8249  [007] 13371565.311507: block_rq_insert: 202,32 R 0 () 660666416 + 88 [java]
[...]

然后也跟踪了实际的堆栈:

# ./tpoint -s block:block_rq_insert 'rwbs ~ "*R*"' | head -1000
Tracing block:block_rq_insert. Ctrl-C to end.
        java-8248  [005] 13370789.973826: block_rq_insert: 202,16 R 0 () 1431480000 + 8 [java]
        java-8248  [005] 13370789.973831: 
 => blk_flush_plug_list
 => blk_queue_bio
 => generic_make_request.part.50
 => generic_make_request
 => submit_bio
 => do_mpage_readpage
 => mpage_readpages
 => xfs_vm_readpages
 => read_pages
 => __do_page_cache_readahead
 => ra_submit
 => do_sync_mmap_readahead.isra.24
 => filemap_fault
 => __do_fault
 => handle_pte_fault
 => handle_mm_fault
 => do_page_fault
 => page_fault
        java-8248  [005] 13370789.973831: block_rq_insert: 202,16 R 0 () 1431480024 + 32 [java]
        java-8248  [005] 13370789.973836: 
 => blk_flush_plug_list
 => blk_queue_bio
 => generic_make_request.part.50
[...]

tpoint 的实现比较简单,譬如上面的 block:block_rq_insert,它直接会找events/block/block_rq_insert 是否存在,如果存在,就是找到了对应的 event。然后给这个 event 的 enable 文件写入 1,如果我们要开启堆栈,就往 options/stacktrace 里面也写入 1。

从上面的堆栈可以看到,有 readahead 以及 page fault 了。在 Netflix 新升级的 Ubuntu 系统里面,默认的 direct map page size 是 2 MB,而之前的 系统是 4 KB,另外就是默认的 readahead 是 2048 KB,老的系统是 128 KB。看起来慢慢找到问题了。

7.3函数计数

为了更好的看函数调用的次数,Gregg 使用了 funccount 函数,譬如检查 submit_bio :

# ./funccount -i 1 submit_bio
Tracing "submit_bio"... Ctrl-C to end.

FUNC                              COUNT
submit_bio                        27881

FUNC                              COUNT
submit_bio                        28478

# ./funccount -i 1 filemap_fault
Tracing "filemap_fault"... Ctrl-C to end.

FUNC                              COUNT
filemap_fault                      2203

FUNC                              COUNT
filemap_fault                      3227
[...]

上面可以看到,有 10 倍的膨胀。对于 funccount 脚本,主要是需要开启 function profile 功能,也就是给 function_profile_enabled 文件写入 1,当打开之后,就会在 trace_stat 目录下面对相关的函数进行统计,可以看到 function0,function1 这样的文件,0 和 1 就是对应的 CPU。cat 一个文件:

cat function0
  Function                               Hit    Time            Avg             s^2
  --------                               ---    ----            ---             ---
  schedule                                56    12603274 us     225058.4 us     4156108562 us
  do_idle                                 51    4750521 us      93147.47 us     5947176878 us
  call_cpuidle                            51    4748981 us      93117.27 us     5566277250 us

就能知道各个函数的 count 了。

7.4功能变慢

为了更加确定系统的延迟是先前堆栈上面看到的函数引起的,Gregg 使用了 funcslower 来看执行慢的函数:

# ./funcslower -P filemap_fault 1000
Tracing "filemap_fault" slower than 1000 us... Ctrl-C to end.
 0)   java-8210    | ! 5133.499 us |  } /* filemap_fault */
 0)   java-8258    | ! 1120.600 us |  } /* filemap_fault */
 0)   java-8235    | ! 6526.470 us |  } /* filemap_fault */
 2)   java-8245    | ! 1458.30 us  |  } /* filemap_fault */
[...]

可以看到,filemap_fault 这个函数很慢。对于 funcslower,我们主要是用 tracing_thresh 来进行控制,给这个文件写入 threshold,如果函数的执行时间超过了 threshold,就会记录。

7.5funccount(再次)

Gregg 根据堆栈的情况,再次对 readpage 和 readpages 进行统计:

# ./funccount -i 1 '*mpage_readpage*'
Tracing "*mpage_readpage*"... Ctrl-C to end.

FUNC                              COUNT
mpage_readpages                     364
do_mpage_readpage                122930

FUNC                              COUNT
mpage_readpages                     318
do_mpage_readpage                110344
[...]

仍然定位到是 readahead 的写放大引起,但他们已经调整了 readahead 的值,但并没有起到作用。

7.6k探头

因为 readahead 并没有起到作用,所以 Gregg 准备更进一步,使用 dynamic tracing。他注意到上面堆栈的函数 __do_page_cache_readahead() 有一个 nr_to_read 的参数,这个参数表明的是每次 read 需要读取的 pages 的个数,使用 kprobe:

# ./kprobe -H 'p:do __do_page_cache_readahead nr_to_read=%cx'
Tracing kprobe m. Ctrl-C to end.
# tracer: nop
#
#   TASK-PID    CPU#    TIMESTAMP  FUNCTION
#      | |       |          |         |
    java-8714  [000] 13445354.703793: do: (__do_page_cache_readahead+0x0/0x180) nr_to_read=200
    java-8716  [002] 13445354.819645: do: (__do_page_cache_readahead+0x0/0x180) nr_to_read=200
    java-8734  [001] 13445354.820965: do: (__do_page_cache_readahead+0x0/0x180) nr_to_read=200
    java-8709  [000] 13445354.825280: do: (__do_page_cache_readahead+0x0/0x180) nr_to_read=200
[...]

可以看到,每次 nr_to_read 读取了 512 (200 的 16 进制)个 pages。在上面的例子,他并不知道 nr_to_read 实际的符号是多少,于是用 %cx 来猜测的,也真能蒙对,太猛了。

关于 kprobe 的使用,具体可以参考 kprobetrace 文档,kprobe 解析需要 trace 的 event 之后,会将其写入到 kprobe_events 里面,然后在 events/kprobes// 进行对应的 enable 和 filter 操作。

7.7函数图

为了更加确认,Gregg 使用 funcgraph 来看 filemap_fault 的实际堆栈,来看 nr_to_read 到底是从哪里传进来的。

# ./funcgraph -P filemap_fault | head -1000
 2)   java-8248    |               |  filemap_fault() {
 2)   java-8248    |   0.568 us    |    find_get_page();
 2)   java-8248    |               |    do_sync_mmap_readahead.isra.24() {
 2)   java-8248    |   0.160 us    |      max_sane_readahead();
 2)   java-8248    |               |      ra_submit() {
 2)   java-8248    |               |        __do_page_cache_readahead() {
 2)   java-8248    |               |          __page_cache_alloc() {
 2)   java-8248    |               |            alloc_pages_current() {
 2)   java-8248    |   0.228 us    |              interleave_nodes();
 2)   java-8248    |               |              alloc_page_interleave() {
 2)   java-8248    |               |                __alloc_pages_nodemask() {
 2)   java-8248    |   0.105 us    |                  next_zones_zonelist();
 2)   java-8248    |               |                  get_page_from_freelist() {
 2)   java-8248    |   0.093 us    |                    next_zones_zonelist();
 2)   java-8248    |   0.101 us    |                    zone_watermark_ok();
 2)   java-8248    |               |                    zone_statistics() {
 2)   java-8248    |   0.073 us    |                      __inc_zone_state();
 2)   java-8248    |   0.074 us    |                      __inc_zone_state();
 2)   java-8248    |   1.209 us    |                    }
 2)   java-8248    |   0.142 us    |                    prep_new_page();
 2)   java-8248    |   3.582 us    |                  }
 2)   java-8248    |   4.810 us    |                }
 2)   java-8248    |   0.094 us    |                inc_zone_page_state();

找到了一个比较明显的函数 max_sane_readahead。对于 funcgraph,主要就是将需要关注的函数放到 set_graph_function 里面,然后在 current_tracer 里面开启 function_graph。

7.8k探针(再次)

然后,Gregg 继续使用 kprobe 来 trace max_sane_readahead 函数,这次不用猜测寄存器了,直接用 $retval 来看返回值:

# ./kprobe 'r:m max_sane_readahead $retval'
Tracing kprobe m. Ctrl-C to end.
    java-8700  [000] 13445377.393895: m: (do_sync_mmap_readahead.isra.24+0x62/0x9c <- \
      max_sane_readahead) arg1=200
    java-8723  [003] 13445377.396362: m: (do_sync_mmap_readahead.isra.24+0x62/0x9c <- \
      max_sane_readahead) arg1=200
    java-8701  [001] 13445377.398216: m: (do_sync_mmap_readahead.isra.24+0x62/0x9c <- \
      max_sane_readahead) arg1=200
    java-8738  [000] 13445377.399793: m: (do_sync_mmap_readahead.isra.24+0x62/0x9c <- \
      max_sane_readahead) arg1=200
    java-8728  [000] 13445377.408529: m: (do_sync_mmap_readahead.isra.24+0x62/0x9c <- \
      max_sane_readahead) arg1=200
[...]

发现仍然是 0x200 个 pages,然后他又发现,readahead 的属性其实是在 file_ra_state_init 这个函数就设置好了,然后这个函数是在文件打开的时候调用的。但他在进行 readahead tune 的时候,一直是让 Cassandra 运行着,也就是无论怎么改 readahead,都不会起到作用,于是他把 Cassandra 重启,问题解决了。

# ./kprobe 'r:m max_sane_readahead $retval'
Tracing kprobe m. Ctrl-C to end.
    java-11918 [007] 13445663.126999: m: (ondemand_readahead+0x3b/0x230 <- \
      max_sane_readahead) arg1=80
    java-11918 [007] 13445663.128329: m: (ondemand_readahead+0x3b/0x230 <- \
      max_sane_readahead) arg1=80
    java-11918 [007] 13445663.129795: m: (ondemand_readahead+0x3b/0x230 <- \
      max_sane_readahead) arg1=80
    java-11918 [007] 13445663.131164: m: (ondemand_readahead+0x3b/0x230 <- \
      max_sane_readahead) arg1=80
[...]

这次只会读取 0x80 个 pages 了。

上面就是一个完完整整使用 ftrace 来定位问题的例子,可以看到,虽然 Linux 系统在很多时候对我们是一个黑盒,但是有了 ftrace,如果在黑暗中开启了一盏灯,能让我们朝着光亮前行。我们内部也在基于 ftrace 做很多有意思的事情。

你可能感兴趣的:(性能优化,linux,LInux内核,性能分析,调试工具)