Ftrace debugfs 接口使用说明(内核文档 ftrace.rst)

Ftrace debugfs 接口使用说明(内核文档 ftrace.rst)

1. 概述

Ftrace是一个内部跟踪程序,用于帮助系统开发者和设计者观察内核内部正在发生的事情。它可以用于调试和分析用户空间之外的延迟和性能问题。

尽管ftrace通常被认为是函数跟踪程序,但他实际上已经是一个由几个分类跟踪实用程序组成的框架。有延时跟踪(latency tracing)去检查在中断禁用与激活之间发生了什么,以及抢占和从任务唤醒到实际任务调度的时间。

Ftrace最常见的用途是Event跟踪。内核有数百个静态事件点,可以通过tracefs文件系统启用这些时间点,以便查看内核的某些部分正在发生什么。详情看"Event trace"章节。

注意:本文主要根据linux-5.12内核文档Document/trace/ftrace.rstDocument/trace/events.rst说明。

2. 实现原理及代码

内核文档的Documentation/trace/ftrace-design.rst作者以注释设计文档已经过时参考价值不是很大,不过设计实现基础思想不变可以看一下,下面这篇博客也是一个对理解设计思想有价值的文档:

参考博客:https://richardweiyang-2.gitbook.io/kernel-exploring/00-index-3/04-ftrace_internal

博客中链接附带的pdf文档很详细的说明了设计者的设计思路。

主要代码路径:kernel/trace/,更多详细信息可以在源代码中获取,kernel/trace/*.c

3. Function 跟踪

3.1 Trace文件系统

Ftrace使用tracefs文件系统去保存控制文件和显示输出的文件。

当tracefs被配置进内核时(选择任何ftrace选项将完成此操作),目录/sys/kernel/tracing将会被创建。为了挂在这个目录,你可以在/etc/fstab文件中添加以下信息::

tracefs       /sys/kernel/tracing       tracefs defaults        0       0

或者在运行时挂在::

mount -t tracefs nodev /sys/kernel/tracing

为了快速访问这个目录可以创建一个软链接::

ln -s /sys/kernel/tracing /tracing

. .注意::

​ 在4.1版本之前,所有的ftrace跟踪控制文件都在debugfs文件系统中,该文件系统通常位于/sys/kernel/debug/tracing。为了向后兼容,当挂载debugfs文 件系统时,tracefs文件系统将自动挂载在/sys/kernel/debug/tracing

​ 位于tracefs文件系统中的所有文件也将位于debugfs文件系统目录中。

. .注意::

​ 任何选定的ftrace选项也将创建tracefs文件系统。文档中的操作都假定在ftrace的目录中(/sys/kernel/tracing或者/sys/kernel/debug/tracing)。

在mount tracefs后,即可访问ftrace的控制和输出文件。以下是一些关键文件的列表:

注意:所有的时间值都以微妙(us)为单位。

  • current_tracer

    这个用于设置和显示已配置的当前tracer。更改当前tracer将会清除ring buffer内容和"snapshot"缓冲区(后面有章节专门介绍"snapshot"用法)。

  • available_tracers

    这个文件保存了已编译到内核中的不同tracer。这里列出的tracer可以通过将其名称echo到current_tracer中来配置。

  • tracing_on

    设置或者回显是否启用写入trace ring buffer。将0 echo到该文件以禁用tracer,或者将1 echo到该文件中以启用tracer。注意,这只是禁止写入到ring buffer,跟踪程序的开销仍然可能会发生。

    可以在内核中使用内核函数tracing_off()来禁用写入ring buffer,这将把该文件设置为"0"。用户空间可以通过在文件echo 1来重新启用tracing。

    注意:函数和事件触发器"traceoff"也会将该文件设置为0并停止tracing。也可以由用户空间使用此文件重新启用tracing。

  • trace

    这个文件以用户可读的格式保存了trace输出(如下所述)。使用O_TRUNC标志打开这个文件进行写入将清除ring buffer内容。注意,这个文件不是一个消费者。如果跟踪处于关闭状态(没有tracing运行,或者tracing_on为0),那么每次读取它时会产生相同的输出。当traceing处于开启状态,尝试读取整个缓冲区时而不清除它时,可能会产生不一致的结果。

  • trace_pipe

    输出与“trace”文件相同,但该文件是用于实时跟踪流的。从这个文件读取trace将会阻塞直到有新数据到来。不像"trace"文件,这个文件是一个消费者。这意味着从这个文件读取将导致顺序读取去显示更多当前数据。一旦从这个文件中读取数据,它就会被消费掉,并且不会再被顺序读取出来。"trace"文件是静态的,如果tracing程序没有产生添加更多数据,那么每次读取数据时都会显示相同的信息。

  • trace_options

    该文件允许用户控制在上述(“trace"或者"trace_pipe”)输出文件之一中显示的数据量,还有一些选项可以修改trace程序或事件的工作方式(堆栈跟踪,时间戳等,也可以直接使用options目录设置)。

  • options/

    这个目录为每个可用的trace options(也在trace_options)提供一个文件。也可以通过向带有选项名称的相应文件写"1"或者"0"来设置或清除选项。

  • tracing_max_latency

    有些tracer会记录最大的延迟。

    例如,禁用中断(irqsoff tracer)最大的时间。该文件保存最大的时间。最大跟踪也将被存储,并通过"trace"显示。只有当延迟大于此文件中的值(以微妙为单位)时,该文件才会刷新记录新的max time。

  • tracing_thresh

    当延迟大于此文件中的值时,一些延迟tracer才会记录跟踪。

    只有在文件包含大于0的数字时激活(以微妙为单位)。

  • buffer_size_kb

    这个文件设置或者显示每个cpu缓冲区保存的千字节数。默认情况下,每个cpu的trace buffer大小相同。显示的数字是单个cpu缓冲区的大小,而不是所有缓冲区的总和。trace buffer以页的形式分配(内核用于分配的内存块,通常的大小是4KB btyes)。可能会分配一些额外的页面来容纳缓冲区管理元数据。如果分配的最后一页的空间大于请求的字节,那么该页面余下部分将有预留,从而使实际分配的大小大于请求的大小和显示的大小(注意,由于缓冲区的元数据管理,大小可能不是页面的倍数)。

    每个cpu的缓冲区大小可能会有所不同(参考下面的"per_cpu/cpu0/buffer_size_kb"),如果有这样去为单独某个cpu设置,这个文件将会显示"X"。

  • buffer_total_size_kb

    这个文件显示了所有trace buffer大小的总和。

  • free_buffer

    如果一个进程正在执行tracing,并且在进程完成时ring buffer应该被收缩"freed",即使它将被一个信号杀死,这个文件用于这样的目的。在关闭该文件时,ring buffer将被调整为其最小大小。如果有一个tracing的进程也打开了这个文件,当该进程退出该文件的文件描述符时,该文件的描述符将被关闭,在此过程,ring bufer也将被"freed"。

    如果options/disable_on_free选项被设置将会停止tracing。

  • tracing_cpumask

    这个cpumask允许用户只跟踪指定的cpu。格式是cpu的十六进制字符串。

  • set_ftrace_filter

    当在内核配置中启用了"dynamic ftrace",将动态修改代码(code text重写)以禁用函数探测器(mcount)的调用。这使得tracing被配置成实际上几乎没有性能开销。这样做也有一个副作用,即启用或者禁用要tracing的特定函数,将函数名echo到该文件将只会tracing这个文件里的函数。

    这将影响tracer “function"和"function_graph”,从而也影响函数分析(参考"function_profile_enabled")。

    在"available_filter_functions"文件中列出了所有可以写入该文件的trace函数。

    该接口支持commands的使用,更过详细信息,参阅Ftrace的Filter commands章节。

    作为一种提速手段,因为处理字符串可能代价昂贵,并且需要检查注册到trace的所有函数,所以可以将索引写入该文件。一个数字(以"1"开头)将选择与"available_filter_functions"文件相同的行位置。

  • set_ftrace_notrace

    这与"set_ftrace_filter"的效果相反。添加到这里的任何函数都不会被跟踪。如果一个函数同时存在于"set_ftrace_filter"和"set_ftrace_notrace"中,则该函数将不会被跟踪,即"set_ftrace_notrace"优先级高于"set_ftrace_filter"。

  • set_ftrace_pid

    让function tracer只跟踪PID在该文件中列出的线程。

    如果options/设置了"function-fork"选项,那么当PID列在该文件中的任务被fork时,子任务的PID会自动添加到该文件中,并且该子任务也会被function tracer跟踪。此选项还将导致退出的任务的pid从该文件中删除。

  • set_ftrace_notrace_pid

    让function tracer忽略该文件中列出的PID的线程。

    如果options/设置了"function-fork"选项,那么当PID列在该文件中的任务fork时,子任务的PID将自动添加到该文件中,并且该子任务也不会被function tracer跟踪。此选项还将导致退出的任务的PID从该文件中删除。

    如果PID同时存在与"set_ftrace_pid"和"set_ftrace_notrace_pid"中,那么这个文件优先级更高,线程将不会被tracing。

  • set_event_pid

    让事件只跟踪该文件列出的具有PID的任务。注意,事件"sched_switch"和"sched_wake_up"也将跟踪该文件中列出的事件。

    要在fork上添加该文件中的PID的子任务的PID,请启用options “event-fork”。该选项还将导致在任务退出时从该文件中删除任务的pid。

  • set_event_notrace_pid

    让事件不跟踪具有此文件中列出的PID的任务。注意,事件"sched_switch"和"sched_wakeup"将跟踪该文件中未列出的线程,即使线程的PID在文件中,而事件"sched_switch"和"sched_wakeup"也跟踪应该tracing的线程。

    要在fork上添加该文件中的PID的子任务的PID,请启用options “event-fork”。该选项还将导致在任务退出时从该文件中删除任务的pid。

  • set_graph_function

    此文件列出的函数将导致function_graph tracer只跟踪这些函数及其调用的函数(参考"dynamic ftrace"章节获取更多信息)。注意,"set_ftrace_filter"和"set_ftrace_notrace"仍然影响正在tracing的函数。

  • set_graph_notrace

    类似于"set_graph_function",但是当函数被命中时将禁用function_graph tracer,直到它退出函数。这使得忽略由特定函数调用的tracing函数成为可能。

  • available_filter_functions

    它列出了ftrace处理过的和可以跟踪的函数。这些是可以传递给"set_ftrace_filter",“set_ftrace_notrace”,“set_graph_function”,或"set_graph_notrace"的函数名(参考"dynamic ftrace"章节获取更多信息)。

  • dyn_ftrace_total_info

    此文件用于调试目的。已转换为nops并可被跟踪的函数的数量。

  • enabled_functions

    这个文件更多的是用于调试ftrace,但也可以用于查看是否有任何函数附加了回调。不仅ftrace框架使用ftrace函数tracing,其他子系统也可能使用。该文件显示所有附加回调的函数,以及附加回调的数量。注意,一个回调也可以调用多个函数,这些函数不会在这个计数中列出。

    如果回调被一个带有"save regs"属性的函数注册tracing(这样开销更大),一个’R’将显示在与返回寄存器的函数的同一行上。

    如果回调被一个带有"ip modify"属性的函数注册tracing(这样regs->ip就可以被修改),'I’将显示在可以被覆盖的函数的同一行上。

    如果体系架构支持,它还将显示函数直接调用的回调。如果计数大于1,则很可能是ftrace_ops_list_func()。

    如果函数的回调跳转到特定于回调而不是标准的"跳转点"的跳转点,它的地址将和跳转点调用的函数一起打印。

  • function_profile_enabled

    当我们设置时所有function都被激活,要么是function tracer,要么是配置后的function graph tracer。它将保存被调用函数数量的直方图,如果配置了function graph tracer,它还将跟踪在这些函数中花费的时间。直方图的内容在文件中显示:

    trace_stat/function<cpu>(function0,function1,function2,etc)。

  • trace_stat/

    保存不同tracing统计信息的目录。

  • kprobe_events

    激活 dynamic trace opoints。看内核文档Documentation/trace/kprobetrace.rst

  • kprobe_profile

    Dynamic trace points 统计信息。 看内核文档Documentation/trace/kprobetrace.rst

  • max_graph_depth

    被用于function_graph tracer。这是tracing一个函数的最大深度。将其设置为1将只显示从用户空间调用的第一个内核函数。

  • printk_formats

    这是用于读取原始格式文件的工具。如果缓冲区中的事件引用了字符串,则只记录指向该字符串的指针到缓冲区中,而不记录字符串本身。这将阻止工具知道该字符串是什么。该文件显示字符串和字符串地址,允许工具将指针映射到字符串的内容。

  • saved_cmdlines

    只有任务的pid被记录在跟踪事件中,除非该事件还特别保存了任务comm。Ftrace缓存了comms的pid映射,以尝试显示事件的comms。如果未列出comm的pid则在输出中显示"<…>"。

    如果record-cmd选项为0,则记录过程中不保存任务的comm。缺省情况下,启用该功能。

  • saved_cmdlines_size

    默认可以保存128个comms(见上面saved_cmdlines)。要增加或减少缓存的通信数量,echo数量到这个文件。

  • saved_tgids

    如果设置了"record-tgid"选项,在每次调度上下文切换时,任务的Task Group ID会保存在一个表中,将线程的PID映射到其TGID。默认情况下,”record-tgid“选项是禁用的。

  • snapshot

    这个文件显示"snapshot”缓冲区,并允许用户获取当前运行tracing的"snapshot"(快照)。

    更多细节看下面"snapshot"部分。

  • stack_max_size

    当stack tracer被激活时,这将显示它遇到的最大堆栈大小。参考下面的"Stack Trace"部分。

  • stack_trace

    这个文件显示激活stack tracer时遇到的最大堆栈的堆栈反向跟踪。参考下面的"Stack Trace"部分。

  • stack_trace_filter

    这个类似于“set_ftrace_filter”,但它限制了堆栈跟踪程序将检查的函数。

  • trace_clock

    每当一个事件被记录到ring buffer时,就会添加一个"时间戳"。这个戳来自一个特定的时钟。默认情况下,ftrace使用"local"时钟。这个时钟非常快,并且严格按照cpu计算,但在一些系统上,它可能与其他cpu相比并不单调。也就是说,"local"时钟与其他cpu上的"local"时钟不同步。

    用于跟踪的常用时钟::

    # cat trace_clock
    [local] global counter uptime perf mono mono_raw boot
    

    有方括号的时钟是有效时钟。

    • local

      默认时钟,但可能不会在cpu之间同步

    • global

      这个时钟与所有cpu同步,但可能比本地时钟慢一点。

    • counter

      不是时钟,是一个原子counter计数器。逐个计数,但与所有cpu同步。当需要确切地知道事件在不同cpu上发生地顺序时,很有用。

    • uptime

      使用jiffies计数器,时间戳是相对于启动后的时间。

    • perf

      使得ftrace和perf使用相同的时钟。最终perf将能够读取ftrace缓冲区,有助于交织数据。

    • x86-tsc

      架构自定义时钟,例如,x86在这里使用它自己的TSC循环时钟。

    • ppc-tb

      架构自定义,powerpc是基寄存器值。这是在cpu之间同步的,如果tb_offset是已知的,还可以用于跨hypervisor/guest关联事件。

    • mono

      它使用快速单调时钟(CLOCK_MONOTONIC),该时钟是单调的,并受NTP速率调整的影响。

    • mono_raw

      这是原始单调时钟(clock_monoic_raw),它是单调的,但不受任何速率调整和节拍的影响,其速率与硬件时钟源相同。

    • boot

      这是引导时钟(CLOCK_BOOTTIME),它基于快速单调时钟,但也考虑了挂起所花费的时间。由于时钟访问被设计用于在挂起路径中跟踪,如果在快速单点时钟更新之前计算挂起时间之后访问时钟,可能会产生一些副作用。在这种情况下,时钟更新似乎比正常情况下发生的稍早。同样在32位系统上,64位的引导偏移量可能会出现部分更新。这些效果是罕见的,后处理应该能够处理它们。有关更多信息,请参阅ktime_get_boot_fast_ns()函数中的注释。

    要设置这些时钟echo时钟名字到这个文件::

    # echo global > trace_clock
    

    设置一个时钟清除ring buffer和"snapshot"缓冲区内容。

  • trace_marker

    这是一个实用文件,用于将用户空间与内核空间中发生的事件同步。将字符串写入该文件将被写入ftrace缓冲区。

    在应用程序中,应用程序开始打开这个文件并引用文件描述符::

    void trace_write(const char *fmt, ...)
    {
    	va_list ap;
    	char buf[256];
    	int n;
    
    	if (trace_fd < 0)
    		return;
    
    	va_start(ap, fmt);
    	n = vsnprintf(buf, 256, fmt, ap);
    	va_end(ap);
    
    	write(trace_fd, buf, n);
    }
    

    开始::

    trace_fd = open("trace_marker", WR_ONLY);
    

    注意:写入trace_marker文件也可以触发写入/sys/kernel/tracing/events/ftrace/print/trigger的触发器。详细看内核文档Documentation/trace/histogram.rst (Section 3.)

  • trace_marker_raw

    这类似于上面的trace_marker,但这意味着要向它写入二进制数据,其中可以使用一个工具来解析来自trace_pipe_raw的数据。

  • uprobe_events

    在应用程序中添加dynamic tracepoints。详细看内核文档Documentation/trace/uprobetracer.rst

  • uprobe_profile

    uprobe的统计数据。详细看内核文档Documentation/trace/uprobetrace.txt

  • instances/

    这是创建多个trace buffer的方法,可以在不同的缓冲区中记录不同的事件。参见下面的"Instances"一节。

  • events/

    这是events trace目录。他保存已编译到内核中的事件跟踪点(也是静态跟踪点)。它显示了存在哪些事件跟踪点,以及如何按子系统分组。不同级别的"enable"文件可以向跟踪点写入"1"时启用跟踪点。详细看Event trace章节。

  • set_event

    可以通过将事件echo到该文件从而启用该事件。详细可看Event trace章节。

  • available_events

    可在tracing中启用的事件列表。详细可看Event trace章节。

  • timestamp_mode

    某些tracer可能会更改在将事件记录到事件缓冲区时所使用的事件戳模式。具有不同模式的事件可以在缓冲区内共存,但记录事件时有效的模式决定了该事件使用哪种模式的时间戳模式。默认的时间戳模式是"delta"。

    用于跟踪的常用时间戳模式:

    # cat timestamp_mode
    [delta] absolute
    

    带方括号的时间戳模式是有效的。

    • delta

      默认的时间戳模式 - timestamp是针对每个缓冲区的时间戳的增量。

    • absolute

      时间戳是一个完整的时间戳,而不是针对其他值的增加量。因此,它占用更多空间和更低的效率。

  • hwlat_detector/

    硬件延迟检测器的目录。请看下面"Hardware Latency Detector"章节。

  • per_cpu/

    这个目录包含了per_cpu的信息。

  • per_cpu/cpu0/buffer_size_kb

    ftrace缓冲区被定义为per_cpu。也就是每个CPU都有一个独立的缓冲区,以允许自动地完成写操作,并且不受缓冲反弹的影响。这些缓冲区可能有不同大小。该文件类似于"buffer_size_kb"文件,但它只显示或设置特定cpu地缓冲区大小(这里是cpu0)。

  • per_cpu/cpu0/trace

    类似于"trace"文件,但它只显示特定于CPU的数据。如果写它,它只清除特定的CPU缓冲区。

  • per_cpu/cpu0/trace_pipe

    这类似于“trace_pipe”文件,并且是一个消耗型的读取,它只显示(并消耗)特定的CPU的数据。

  • per_cpu/cpu0/trace_pipe_raw

    对于能够解析ftrace ring buffer二进制格式的工具,可以使用"trace_pipe_raw"文件直接从ring buffer提取数据。通过使用splice()系统调用,可以将缓冲区数据快速传输到一个文件或一个正在收集数据服务的网络。

    与"trace_pipe"一样,这是一个消耗大量数据的读取器,多次读取总是会产生不同的数据。

  • per_cpu/cpu0/snapshot

    这类似于主“快照”文件,但只会快照当前的CPU(如果支持)。它只显示给定CPU的快照内容,如果写入,只清除该CPU缓冲区。

  • per_cpu/cpu0/snapshot_raw

    类似于"trace_pipe_raw",但将从给定CPU的快照缓冲区读取二进制格式。

  • per_cpu/cpu0/stats

    这显示了ring buffer的一些统计信息:

    • entries

      仍在缓冲区中的事件数。

    • overrun

      缓冲区满时由于重写而丢失的事件数。

    • commit overrun

      应该总是0。如果一个嵌套事件中发生了如此多的事件(ring buffer是可重入的),那么它将填充缓冲区并开始删除事件。

    • bytes

      实际读取的字节(没有覆盖)。

    • oldest event ts

      缓冲区中最旧的时间戳。

    • now ts

      当前时间戳。

    • dropped events

      由于overwrite选项关闭而丢失的事件数量。

    • read events

      读取的事件数。

3.2 Tracers

下面是可能配置的当前tracers的列表:

  • function

    函数调用跟踪程序来跟踪所有内核函数。

  • function_graph

    与函数跟踪程序相似,不同之处在于函数跟踪程序在函数的入口探测函数,而函数图跟踪程序在函数的入口和出口都进行跟踪。然后,它提供了绘制函数调用图的能力,类似于C源代码。

  • blk

    the block tracer。blktrace应用程序使用的跟踪程序。

  • hwlat

    硬件延迟跟踪器用于检测硬件是否产生任何延迟。请参阅下面的“Hardware Latency Detector”一节。

  • irqsoff

    跟踪禁用中断的区域,并以最长的最大延迟保存跟踪。

    看"tracing_max_latency"文件。当记录新的max时,它将替换旧的跟踪值。最好在启用"latency-format"选项的情况下查看此跟踪,这在选择跟踪程序时自动设置。

  • preemptoff

    类似于irqsoff,但是跟踪和记录抢占被禁用的时间量。

  • preemptirqsoff

    类似于irqsoff和preemptoff,但是跟踪和记录irqs和/或抢占被禁用的最大时间。

  • wakeup

    跟踪和记录在最高优先级任务被唤醒后实际调度它所需要的最大延迟。按照一般开发人员的预期跟踪所有任务。

  • wakeup_rt

    跟踪和记录RT任务所需要的最大延迟。这对于那些对RT任务的唤醒时间感兴趣的人很有用。

  • wakeup_dl

    跟踪和记录唤醒SCHED_DEADLINE任务所需的最大延迟(与“wakeup”和“wakeup_rt”一样)。

  • mmiotrace

    一种用于跟踪二进制模块的特殊跟踪程序。它将跟踪一个模块对硬件的所有调用。它也从I/O中写入和读取所有内容。

  • branch

    这个跟踪程序可以在跟踪内核中可likely/unlikley的调用时配置。它将跟踪何时命中可能和不可能的分支,以及它的预测是否正确。

  • nop

    这是“无跟踪”跟踪器。要从跟踪中删除所有的跟踪程序,只需将“nop” echo到"current_tracer"中。

3.3 Error conditions

对于大多数ftrace命令,故障模式是显而易见的,并且使用标准返回码进行通信。

对于其他更复杂的命令,扩展的错误信息可以通过tracing/error_log文件获得。对于支持它的命令,在错误之后读取tracing/error_log文件将显示关于出错的更详细的信息(如果信息可用的话)。trace /error_log文件是一个循环的错误日志,它为最后(8)个失败的命令显示少量的ftrace错误(目前为8)。

扩展的错误信息和用法采用如下示例所示的形式::

# echo xxx > /sys/kernel/debug/tracing/events/sched/sched_wakeup/trigger
echo: write error: Invalid argument

# cat /sys/kernel/debug/tracing/error_log
[ 5348.887237] location: error: Couldn't yyy: zzz
Command: xxx
^
[ 7517.023364] location: error: Bad rrr: sss
Command: ppp qqq

要清除 error log::

# echo > /sys/kernel/debug/tracing/error_log

注意,在新的内核上面大多数错误信息可以直接显示出来,而不需要查看error_log文件。

3.4 tracer使用的示例

下面是使用tracer的典型示例,它们只使用"tracers"接口来控制跟踪程序(不使用任何用户使用的实用程序)。

3.4.1 Output format

下面是文件“trace”的输出格式示例::

# tracer: function
#
# entries-in-buffer/entries-written: 165223/272378   #P:8
#
#                                _-----=> irqs-off
#                               / _----=> need-resched
#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| /     delay
#           TASK-PID     CPU#  ||||   TIMESTAMP  FUNCTION
#              | |         |   ||||      |         |
              sh-147     [002] d..2 17598.798559: gic_handle_irq <-el1_irq
              sh-147     [002] d..2 17598.798883: __handle_domain_irq <-gic_handle_irq
              sh-147     [002] d..2 17598.798888: irq_enter <-__handle_domain_irq
              sh-147     [002] d..2 17598.798891: irq_enter_rcu <-irq_enter
              sh-147     [002] d..2 17598.798892: preempt_count_add <-irq_enter_rcu
              sh-147     [002] d.h2 17598.798895: irqtime_account_irq <-irq_enter_rcu
              sh-147     [002] d.h2 17598.798898: irq_find_mapping <-__handle_domain_irq
              sh-147     [002] d.h2 17598.798900: generic_handle_irq <-__handle_domain_irq
              sh-147     [002] d.h2 17598.798904: handle_percpu_devid_irq <-generic_handle_irq
              sh-147     [002] d.h2 17598.798906: arch_timer_handler_virt <-handle_percpu_devid_irq
              sh-147     [002] d.h2 17598.798911: hrtimer_interrupt <-arch_timer_handler_virt
              sh-147     [002] d.h2 17598.798913: _raw_spin_lock_irqsave <-hrtimer_interrupt
              sh-147     [002] d.h2 17598.798914: preempt_count_add <-_raw_spin_lock_irqsave
              sh-147     [002] d.h3 17598.798917: ktime_get_update_offsets_now <-hrtimer_interrupt
              sh-147     [002] d.h3 17598.798920: arch_counter_read <-ktime_get_update_offsets_now
              sh-147     [002] d.h3 17598.798923: __hrtimer_run_queues <-hrtimer_interrupt
              sh-147     [002] d.h3 17598.798925: __remove_hrtimer <-__hrtimer_run_queues
              sh-147     [002] d.h3 17598.798929: _raw_spin_unlock_irqrestore <-__hrtimer_run_queues
              ....

头部是使用的tracer名字打印。在这种情况下,tracer是"function"。然后显示缓冲区中事件的数量以及写入的条目的总数。差异是由于缓冲区填充而丢失的条目数量(272378 - 165223 = 107155事件丢失)。

头部解释事件内容。任务名"sh",任务pid"147",运行在CPU 002上,延迟格式(latency-format下文解释),时间戳格式为<secs>.<usecs>,函数名字是gic_handle_irq和其父函数el1_irq。时间戳是进入函数的时间。

3.4.2 Latency trace format

当我们使用"latency-format"选项时,或者设置了一个延迟tracer,"trace"文件提供了更多信息,以便了解延迟发生的原因。下面是一个典型的trace:

# tracer: irqsoff
#
# irqsoff latency trace v1.1.5 on 5.12.0
# --------------------------------------------------------------------
# latency: 259 us, #4/4, CPU#2 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:8)
#    -----------------
#    | 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" tracer展示了跟踪中断被禁用的时间。首先给出了trace版本(永不改变)和跟踪的内核版本(5.12)。然后显示最大延迟259us,单位为微秒。显示的跟踪条目的数量和总数(都是4个:#4/4)。P、KP、SP和HP始终为零,保留为以后使用。#P是在线cpu的数量(#P:8)。

该任务是发生延迟时正在运行的进程。(ps pid: 6143)。

导致延迟的启动和停止(分别禁用和启用中断的函数):

​ – __lock_task_sighand是中断被禁用的地方。

​ --_raw_spin_unlock_irqrestore是它们再次被启用的位置。

头文件之后的下几行是跟踪本身。标题解释了哪个是哪个。

  • cmd

    跟踪中的进程名。

  • pid

    跟踪中的进程pid。

  • CPU#

    程序运行的CPU。

  • irqs-off

    ‘d’表示中断被禁用,相反是’.‘,如果架构不支持则是’X’。

  • need-resched

    - ‘N’ TIF_NEED_RESCHED和PREEMPT_NEED_RESCHED都被设置了,

    - ‘n’ 仅设置了TIF_NEED_RESCHED,

    - ‘p’ 仅设置了PREEMPT_NEED_RESCHED,

    - ‘.’ 都不是。

  • hardirq/softirq

    - ‘Z’ - NMI正在硬件内部发生

    - ‘z’ - NMI正在运行

    - ‘H’ - 硬中断请求发生在软中断请求内

    - ‘h’ - 硬irq正在运行

    - ‘s’ - 软irq正在运行

    - ‘.’ - 普通上下文

  • preempt-depth

    preempt_disable调用级别。(preempt_disable调用一次加一,preempt_enable调用一次减一)

下面这些信息对内核开发人员有意义:

  • time

    当启用"latency-format"选项时,"trace"文件输出包含一个相对于跟踪开始的时间戳。这与禁用"latency-format"选项输出不同,后者是一个绝对时间戳。

  • delay

    这只是为了更好地吸引你的眼球。并且需要被固定,只相对于相同的CPU。标记由当前跟踪和下一个跟踪之间的差异决定。

    - ‘$’ - 大于1秒

    - ‘@’ - 大于100毫秒

    - ‘*’ - 大于10毫秒

    - ‘#’ - 大于1000微秒

    - ’!‘ - 超过100微秒

    - ‘+’ - 大于10微秒

    - ’ ’ - 小于或等于10微秒。

其余部分与“trace”文件相同。

注意,延迟tracer通常会以反向跟踪结束,以便轻松找到延迟发生的位置。

3.4.3 trace options

trace_options文件(或options目录)用于控制在跟踪输出中打印的内容,或操作跟踪程序。要查看当前选项是如何配置的,只需cat文件::

# cat trace_options
	print-parent
	nosym-offset
	nosym-addr
	noverbose
	noraw
	nohex
	nobin
	noblock
	trace_printk
	annotate
	nouserstacktrace
	nosym-userobj
	noprintk-msg-only
	context-info
	nolatency-format
	record-cmd
	norecord-tgid
	overwrite
	nodisable_on_free
	irq-info
	markers
	noevent-fork
	function-trace
	nofunction-fork
	nodisplay-graph
	nostacktrace
	nobranch

若要禁用其中一个选项,请回写前面的选项并跟上“no”字符串:

# echo noprint-parent > trace_options

激活选项则不要"no":

echo sym-offset > trace_options

这是一些可用的options:

  • print-parent

    在function tracing中,显示调用(父)函数以及正在跟踪的函数::

    print-parent:
    	bash-4000  [01]  1477.606694: simple_strtoul <-kstrtoul
    
    noprint-parent:
    	bash-4000  [01]  1477.606694: simple_strtoul
    
  • sym-offset

    不仅显示函数名,还显示函数中的偏移量。例如,将看到“ktime_get+0xb/0x20”,而不是仅仅看到“ktime_get”::

    sym-offset:
    	bash-4000  [01]  1477.606694: simple_strtoul+0x6/0xa0
    
  • sym-addr

    这将显示函数地址和函数名::

    sym-addr:
    	bash-4000  [01]  1477.606694: simple_strtoul <ffff8000100d5b8c>
    
  • verbose

    当"latency-format"选项也激活时,它按照下面原始信息显示"trace"文件::

    bash  4000 1 0 00000000 00010a95 [58127d26] 1720.415ms \
    	(+0.000ms): simple_strtoul (kstrtoul)
    
  • raw

    这将显示原始数字。这个选项最适合于那些比在内核中更好地转换原始数字的用户应用程序::

    0 4 87666084422608 ffff80001014b5bc ffff80001014ba44
    59 5 87666084424800 type: 254
    0 4 87666084425504 ffff80001014b6c4 ffff80001014ba58
    
  • hex

    类似于raw选项,但数字将以十六进制格式输出。

  • bin

    这将以原始二进制格式输出。

  • block

    当设置时,在轮询时读取trace_pipe不会被阻塞。

  • trace_printk

    可以禁用trace_printk()写入缓冲区。(内核中使用trace_printk()向function_graph tracer的"trace"文件函数内部添加注释信息,后面介绍)

  • annotate

    当CPU缓冲区满了,并且一个CPU缓冲区最近又有很多事件产生,因此在一个较短的时间帧里面,另一个CPU可能只有几个很旧的事件,有时会令人混淆的。当报告跟踪时,它首先显示最旧的事件,看起来可能只有一个CPU在运行(具有最旧事件的那个)。设置了annotate选项后,当一个新的CPU缓冲区启动时,它将显示一个下面这样的注释::

    	<idle>-0     [001] dNs4 21169.031481: wake_up_idle_cpu <-add_timer_on
    	<idle>-0     [001] dNs4 21169.031482: _raw_spin_unlock_irqrestore <-add_timer_on
    	<idle>-0     [001] .Ns4 21169.031484: sub_preempt_count <-_raw_spin_unlock_irqrestore
    ##### CPU 2 buffer started ####
    	<idle>-0     [002] .N.1 21169.031484: rcu_idle_exit <-cpu_idle
    	<idle>-0     [001] .Ns3 21169.031484: _raw_spin_unlock <-clocksource_watchdog
    	<idle>-0     [001] .Ns3 21169.031485: sub_preempt_count <-_raw_spin_unlock
    
  • userstacktrace

    此选项将更改跟踪。它在每个跟踪事件之后记录当前用户空间线程的堆栈跟踪。

  • sym-userobj

    当启用user stacktrace事件时,查找地址属于哪个对象,并打印一个相对地址。这在ASLR开启时特别有用,否则在应用程序不再运行后,你没有机会解析到对象/文件/行的地址。

    在读取"trace","trace_pipe"时执行查找。例如::

    a.out-1623  [000] 40874.465068: /root/a.out[+0x480] <-/root/a.out[+0
    	x494] <- /root/a.out[+0x4a8] <- /lib/libc-2.7.so[+0x1e1a6]
    
  • printk-msg-only

    设置后,trace_printk()将只显示格式,而不显示它们的参数(如果trace_bprintk()或trace_bputs()用于保存trace_printk())。

  • context-info

    只显示事件数据。隐藏comm、PID、时间戳、CPU和其他有用的数据。

  • latency-format

    此选项将更改跟踪输出。当它被启用时,跟踪将显示关于延迟的附加信息,如“Latency trace format”中所述。

  • pause-on-trace

    设置后,打开"trace"文件进行读操作,将暂停对循环缓冲区的写操作(就像tracing_on设置为0一样)。这模拟了跟踪文件的原始行为。当文件关闭时,跟踪将再次启用。

  • hash-ptr

    设置后,事件printk格式中的"%p"将显示散列后的指针值,而不是实际地址。如果您想要找出哪个散列值对应于跟踪日志中的实际值,这将非常有用。

  • record-cmd

    当启用任何事件或跟踪程序时,在sched_switch跟踪点中启用一个钩子,以使用映射的pid和comm填充comm缓存。但是这可能会造成一些开销,而且如果您只关心pid而不关心任务的名称,禁用此选项可以降低跟踪的影响。看到“saved_cmdlines”。

  • record-tgid

    当启用任何事件或跟踪程序时,在sched_switch跟踪点中启用一个钩子,以填充映射到pid的线程组id (TGID)映射的缓存。看到“saved_tgids”。

  • overwrite

    这将控制跟踪缓冲区满时发生的情况。如果“1”(默认值),则丢弃和覆盖最老的事件。如果“0”,则丢弃最新的事件。(请参阅per_cpu/cpu0/stats了解溢出和丢弃)

  • disable_on_free

    当free_buffer关闭时,跟踪将停止(tracing_on设置为0)。

  • irq-info

    显示中断,抢占计数,需要重新排序的数据。禁用时,跟踪看起来像::

    # tracer: function
    #
    # entries-in-buffer/entries-written: 144405/9452052   #P:4
    #
    #           TASK-PID   CPU#      TIMESTAMP  FUNCTION
    #              | |       |          |         |
    		  <idle>-0     [002]  23636.756054: ttwu_do_activate.constprop.89 <-try_to_wake_up
    		  <idle>-0     [002]  23636.756054: activate_task <-ttwu_do_activate.constprop.89
    		  <idle>-0     [002]  23636.756055: enqueue_task <-activate_task
    
  • markers

    当设置时,trace_marker是可写的(仅由根用户)。

    当禁用时,trace_marker将在写入时出错,并带有EINVAL。

  • event-fork

    当设置时,在"set_event_pid"中列出的任务将在这些任务fork时将其子任务的pid添加到"set_event_pid"中。此外,当具有"set_event_pid"中的pid的任务退出时,它们的pid将从文件中删除。

    这也会影响"set_event_notrace_pid"中列出的pid。

  • function-trace

    如果启用此选项(默认是),延迟类tracer将启用函数跟踪。当它被禁用时,延迟跟踪器不会跟踪函数。这将降低执行延迟测试时tracer的开销。

  • function-fork

    当设置时,在"set_ftrace_pid"中列出的pid的任务将在这些任务分叉时将其子任务的pid添加到"set_ftrace_pid"中。同样,当具有"set_ftrace_pid"中的pid的任务退出时,它们的pid将从文件中删除。

    这也会影响"set_ftrace_notrace_pid"中的pid。

  • display-graph

    当设置后,延迟类tracer(irqsoff、wakeup等)将使用函数图跟踪而不是函数跟踪。

  • stacktrace

    当设置时,堆栈跟踪将在记录任何跟踪事件之后记录。

  • branch

    tracer启用分支tracing。这将支持branch tracer以及当前设置的tracer。使用“nop”tracer启用此功能与启用branch tracer相同。

    . .提示:有些tracer有自己的选择。它们只在tracer激活时自动地在"trace_options"文件中启用这个选项。

下面是一些per tracer选项:

  1. function tracer 选项

    • func_stack_trace

      当设置时,堆栈跟踪将在记录的每个函数之后记录。注意,使用“set_ftrace_filter”限制在启用前记录的函数,否则系统性能将严重降低。记得在清除函数过滤器之前禁用此选项。

  2. function_graph tracer 选项

    由于function_graph tracer的输出略有不同,所以它有自己的选项来控制显示的内容。

    • funcgraph-overrun

      当设置时,在tracing每个函数之后,图形堆栈的“overrun”将显示出来。溢出是指调用的堆栈深度大于为每个任务保留的堆栈深度。每个任务在调用图中都有一个固定的函数数组来跟踪。如果调用的深度超过该值,则不会跟踪函数。溢出是由于超过这个数组而错过的函数数。

    • funcgraph-cpu

      设置后,会显示跟踪发生的CPU编号。

    • funcgraph-overhead

      当设置时,如果函数花费的时间超过一定的数量,则显示一个延迟标记。参见上面标题描述下的“delay”。

    • funcgraph-proc

      与其他tracer不同,进程的命令行在默认情况下不显示,而是只在上下文切换期间跟踪任务时显示。启用此选项将在每行显示每个进程的命令。

    • funcgraph-duration

      在每个函数(返回)结束时,函数中时间量的持续时间以微秒为单位显示。

    • funcgraph-abstime

      设置后,时间戳将显示在每一行。

    • funcgraph-irqs

      当禁用时,在中断中发生的函数将不会被跟踪。

    • funcgraph-tail

      设置后,返回事件将包含它所代表的函数。默认情况下这是关闭的,并且在返回函数时只显示一个右花括号"}"。

    • sleep-time

      在运行函数图跟踪程序时,将任务计划的时间包含在其函数中。当启用时,它将计算任务作为函数调用的一部分被调度出去的时间。

    • graph-time

      当用函数图跟踪器运行函数分析器时,要包括调用嵌套函数的时间。如果没有设置此参数,则报告的函数时间将只包括函数本身执行的时间,而不包括调用函数的时间。

  3. blk tracer 选项

    • blk_classic

      显示出更简约的输出。

3.4.4 irqsoff

当中断被禁用时,CPU不能响应任何其他外部事件(除了NMIs和SMIs)。这可以防止计时器中断触发或鼠标中断让内核知道新的鼠标事件。结果是响应时间的延迟。

irqsoff tracer跟踪中断被禁用的时间。当遇到新的最大延迟时,tracer将保存指向该延迟点的跟踪,以便每次达到新的最大延迟时,将丢弃旧的保存的跟踪,并保存新的跟踪。

要重置最大值,echo 0到tracing_max_latency。下面是一个例子:

  # echo 0 > options/function-trace
  # echo irqsoff > current_tracer
  # echo 1 > tracing_on
  # echo 0 > tracing_max_latency
  # ls -ltr
  [...]
  # echo 0 > tracing_on
  # cat trace
  # tracer: irqsoff
  #
  # irqsoff latency trace v1.1.5 on 5.12.0
  # --------------------------------------------------------------------
  # latency: 16 us, #4/4, CPU#0 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:4)
  #    -----------------
  #    | task: swapper/0-0 (uid:0 nice:0 policy:0 rt_prio:0)
  #    -----------------
  #  => started at: run_timer_softirq
  #  => ended at:   run_timer_softirq
  #
  #
  #                  _------=> CPU#            
  #                 / _-----=> irqs-off        
  #                | / _----=> need-resched    
  #                || / _---=> hardirq/softirq 
  #                ||| / _--=> preempt-depth   
  #                |||| /     delay             
  #  cmd     pid   ||||| time  |   caller      
  #     \   /      |||||  \    |   /           
    <idle>-0       0d.s2    0us+: _raw_spin_lock_irq <-run_timer_softirq
    <idle>-0       0dNs3   17us : _raw_spin_unlock_irq <-run_timer_softirq
    <idle>-0       0dNs3   17us+: trace_hardirqs_on <-run_timer_softirq
    <idle>-0       0dNs3   25us : <stack trace>
   => _raw_spin_unlock_irq
   => run_timer_softirq
   => __do_softirq
   => call_softirq
   => do_softirq
   => irq_exit
   => smp_apic_timer_interrupt
   => apic_timer_interrupt
   => rcu_idle_exit
   => cpu_idle
   => rest_init
   => start_kernel
   => x86_64_start_reservations
   => x86_64_start_kernel

这里我们看到我们有16微秒的延迟(这是非常好的)。run_timer_softirq中的_raw_spin_lock_irq禁用中断。16和显示的时间戳25us之间的差异是因为时钟在记录最大延迟和记录有延迟的函数之间的时间增加了。

注意,上面的示例没有设置函数跟踪。如果我们设置function-trace,我们会得到一个更大的输出::

 with echo 1 > options/function-trace

  # tracer: irqsoff
  #
  # irqsoff latency trace v1.1.5 on 3.8.0-test+
  # --------------------------------------------------------------------
  # latency: 71 us, #168/168, CPU#3 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:4)
  #    -----------------
  #    | task: bash-2042 (uid:0 nice:0 policy:0 rt_prio:0)
  #    -----------------
  #  => started at: ata_scsi_queuecmd
  #  => ended at:   ata_scsi_queuecmd
  #
  #
  #                  _------=> CPU#            
  #                 / _-----=> irqs-off        
  #                | / _----=> need-resched    
  #                || / _---=> hardirq/softirq 
  #                ||| / _--=> preempt-depth   
  #                |||| /     delay             
  #  cmd     pid   ||||| time  |   caller      
  #     \   /      |||||  \    |   /           
      bash-2042    3d...    0us : _raw_spin_lock_irqsave <-ata_scsi_queuecmd
      bash-2042    3d...    0us : add_preempt_count <-_raw_spin_lock_irqsave
      bash-2042    3d..1    1us : ata_scsi_find_dev <-ata_scsi_queuecmd
      bash-2042    3d..1    1us : __ata_scsi_find_dev <-ata_scsi_find_dev
      bash-2042    3d..1    2us : ata_find_dev.part.14 <-__ata_scsi_find_dev
      bash-2042    3d..1    2us : ata_qc_new_init <-__ata_scsi_queuecmd
      bash-2042    3d..1    3us : ata_sg_init <-__ata_scsi_queuecmd
      bash-2042    3d..1    4us : ata_scsi_rw_xlat <-__ata_scsi_queuecmd
      bash-2042    3d..1    4us : ata_build_rw_tf <-ata_scsi_rw_xlat
  [...]
      bash-2042    3d..1   67us : delay_tsc <-__delay
      bash-2042    3d..1   67us : add_preempt_count <-delay_tsc
      bash-2042    3d..2   67us : sub_preempt_count <-delay_tsc
      bash-2042    3d..1   67us : add_preempt_count <-delay_tsc
      bash-2042    3d..2   68us : sub_preempt_count <-delay_tsc
      bash-2042    3d..1   68us+: ata_bmdma_start <-ata_bmdma_qc_issue
      bash-2042    3d..1   71us : _raw_spin_unlock_irqrestore <-ata_scsi_queuecmd
      bash-2042    3d..1   71us : _raw_spin_unlock_irqrestore <-ata_scsi_queuecmd
      bash-2042    3d..1   72us+: trace_hardirqs_on <-ata_scsi_queuecmd
      bash-2042    3d..1  120us : <stack trace>
   => _raw_spin_unlock_irqrestore
   => ata_scsi_queuecmd
   => scsi_dispatch_cmd
   => scsi_request_fn
   => __blk_run_queue_uncond
   => __blk_run_queue
   => blk_queue_bio
   => submit_bio_noacct
   => submit_bio
   => submit_bh
   => __ext3_get_inode_loc
   => ext3_iget
   => ext3_lookup
   => lookup_real
   => __lookup_hash
   => walk_component
   => lookup_last
   => path_lookupat
   => filename_lookup
   => user_path_at_empty
   => user_path_at
   => vfs_fstatat
   => vfs_stat
   => sys_newstat
   => system_call_fastpath

这里我们追踪了71微秒的延迟。但我们也看到了那段时间调用的所有函数。注意,通过启用函数跟踪,我们引入了额外的开销。这种开销可能会延长延迟时间。但是,这个跟踪提供了一些非常有用的调试信息。

如果我们更喜欢函数图形输出而不是函数,我们可以设置显示图形选项::

with echo 1 > options/display-graph

  # tracer: irqsoff
  #
  # irqsoff latency trace v1.1.5 on 4.20.0-rc6+
  # --------------------------------------------------------------------
  # latency: 3751 us, #274/274, CPU#0 | (M:desktop VP:0, KP:0, SP:0 HP:0 #P:4)
  #    -----------------
  #    | task: bash-1507 (uid:0 nice:0 policy:0 rt_prio:0)
  #    -----------------
  #  => started at: free_debug_processing
  #  => ended at:   return_to_handler
  #
  #
  #                                       _-----=> irqs-off
  #                                      / _----=> need-resched
  #                                     | / _---=> hardirq/softirq
  #                                     || / _--=> preempt-depth
  #                                     ||| /
  #   REL TIME      CPU  TASK/PID       ||||     DURATION                  FUNCTION CALLS
  #      |          |     |    |        ||||      |   |                     |   |   |   |
          0 us |   0)   bash-1507    |  d... |   0.000 us    |  _raw_spin_lock_irqsave();
          0 us |   0)   bash-1507    |  d..1 |   0.378 us    |    do_raw_spin_trylock();
          1 us |   0)   bash-1507    |  d..2 |               |    set_track() {
          2 us |   0)   bash-1507    |  d..2 |               |      save_stack_trace() {
          2 us |   0)   bash-1507    |  d..2 |               |        __save_stack_trace() {
          3 us |   0)   bash-1507    |  d..2 |               |          __unwind_start() {
          3 us |   0)   bash-1507    |  d..2 |               |            get_stack_info() {
          3 us |   0)   bash-1507    |  d..2 |   0.351 us    |              in_task_stack();
          4 us |   0)   bash-1507    |  d..2 |   1.107 us    |            }
  [...]
       3750 us |   0)   bash-1507    |  d..1 |   0.516 us    |      do_raw_spin_unlock();
       3750 us |   0)   bash-1507    |  d..1 |   0.000 us    |  _raw_spin_unlock_irqrestore();
       3764 us |   0)   bash-1507    |  d..1 |   0.000 us    |  tracer_hardirqs_on();
      bash-1507    0d..1 3792us : <stack trace>
   => free_debug_processing
   => __slab_free
   => kmem_cache_free
   => vm_area_free
   => remove_vma
   => exit_mmap
   => mmput
   => begin_new_exec
   => load_elf_binary
   => search_binary_handler
   => __do_execve_file.isra.32
   => __x64_sys_execve
   => do_syscall_64
   => entry_SYSCALL_64_after_hwframe
3.4.5 preemptoff

当抢占被禁用时,我们可以接收到中断,但是任务不能被抢占,一个高优先级的任务必须等待抢占再次被启用,然后它才能抢占一个低优先级的任务。

preemptoff tracer跟踪禁用抢占的地方。与irqsoff tracer一样,它记录禁用抢占时的最大延迟::

  # echo 0 > options/function-trace
  # echo preemptoff > current_tracer
  # echo 1 > tracing_on
  # echo 0 > tracing_max_latency
  # ls -ltr
  [...]
  # echo 0 > tracing_on
  # cat trace
  # 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

这里有更多的变化。抢占在中断出现时被禁用(注意’h’),在退出时被启用。但我们也看到,当进入抢占off部分并离开它(‘d’)时,中断已经被禁用。我们不知道中断是在此期间启用的,还是在此结束后不久启用的::

  # tracer: preemptoff
  #
  # preemptoff latency trace v1.1.5 on 3.8.0-test+
  # --------------------------------------------------------------------
  # latency: 83 us, #241/241, CPU#1 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:4)
  #    -----------------
  #    | task: bash-1994 (uid:0 nice:0 policy:0 rt_prio:0)
  #    -----------------
  #  => started at: wake_up_new_task
  #  => ended at:   task_rq_unlock
  #
  #
  #                  _------=> CPU#            
  #                 / _-----=> irqs-off        
  #                | / _----=> need-resched    
  #                || / _---=> hardirq/softirq 
  #                ||| / _--=> preempt-depth   
  #                |||| /     delay             
  #  cmd     pid   ||||| time  |   caller      
  #     \   /      |||||  \    |   /           
      bash-1994    1d..1    0us : _raw_spin_lock_irqsave <-wake_up_new_task
      bash-1994    1d..1    0us : select_task_rq_fair <-select_task_rq
      bash-1994    1d..1    1us : __rcu_read_lock <-select_task_rq_fair
      bash-1994    1d..1    1us : source_load <-select_task_rq_fair
      bash-1994    1d..1    1us : source_load <-select_task_rq_fair
  [...]
      bash-1994    1d..1   12us : irq_enter <-smp_apic_timer_interrupt
      bash-1994    1d..1   12us : rcu_irq_enter <-irq_enter
      bash-1994    1d..1   13us : add_preempt_count <-irq_enter
      bash-1994    1d.h1   13us : exit_idle <-smp_apic_timer_interrupt
      bash-1994    1d.h1   13us : hrtimer_interrupt <-smp_apic_timer_interrupt
      bash-1994    1d.h1   13us : _raw_spin_lock <-hrtimer_interrupt
      bash-1994    1d.h1   14us : add_preempt_count <-_raw_spin_lock
      bash-1994    1d.h2   14us : ktime_get_update_offsets <-hrtimer_interrupt
  [...]
      bash-1994    1d.h1   35us : lapic_next_event <-clockevents_program_event
      bash-1994    1d.h1   35us : irq_exit <-smp_apic_timer_interrupt
      bash-1994    1d.h1   36us : sub_preempt_count <-irq_exit
      bash-1994    1d..2   36us : do_softirq <-irq_exit
      bash-1994    1d..2   36us : __do_softirq <-call_softirq
      bash-1994    1d..2   36us : __local_bh_disable <-__do_softirq
      bash-1994    1d.s2   37us : add_preempt_count <-_raw_spin_lock_irq
      bash-1994    1d.s3   38us : _raw_spin_unlock <-run_timer_softirq
      bash-1994    1d.s3   39us : sub_preempt_count <-_raw_spin_unlock
      bash-1994    1d.s2   39us : call_timer_fn <-run_timer_softirq
  [...]
      bash-1994    1dNs2   81us : cpu_needs_another_gp <-rcu_process_callbacks
      bash-1994    1dNs2   82us : __local_bh_enable <-__do_softirq
      bash-1994    1dNs2   82us : sub_preempt_count <-__local_bh_enable
      bash-1994    1dN.2   82us : idle_cpu <-irq_exit
      bash-1994    1dN.2   83us : rcu_irq_exit <-irq_exit
      bash-1994    1dN.2   83us : sub_preempt_count <-irq_exit
      bash-1994    1.N.1   84us : _raw_spin_unlock_irqrestore <-task_rq_unlock
      bash-1994    1.N.1   84us+: trace_preempt_on <-task_rq_unlock
      bash-1994    1.N.1  104us : <stack trace>
   => sub_preempt_count
   => _raw_spin_unlock_irqrestore
   => task_rq_unlock
   => wake_up_new_task
   => do_fork
   => sys_clone
   => stub_clone

上面是一个设置了函数跟踪的抢占跟踪示例。这里我们看到中断在整个时间内都没有被禁用。irq_enter代码让我们知道我们进入了一个中断’h’。在此之前,被跟踪的函数仍然显示它不是在中断中,但我们可以从函数本身看到情况并非如此。

3.4.6 preemptirqsoff

了解禁用中断或禁用抢占时间最长的位置是有必要的。但有时我们想知道抢占和/或中断在什么时候被禁用。

思考以下代码::

    local_irq_disable();
    call_function_with_irqs_off();
    preempt_disable();
    call_function_with_irqs_and_preemption_off();
    local_irq_enable();
    call_function_with_preemption_off();
    preempt_enable();

irqsoff tracer将记录call_function_with_irqs_off()和call_function_with_irqs_and_preemption_off()的总长度。

preemptoff tracer将记录call_function_with_irqs_and_preemption_off()和call_function_with_preemption_off()的总长度。

但两者都无法跟踪中断和/或禁用抢占的时间。这总时间是我们无法安排的时间。要记录这一次,使用preemptirqsoff tracer。

同样,使用这个跟踪很像irqsoff tracer和preemptoff tracer::

  # echo 0 > options/function-trace
  # echo preemptirqsoff > current_tracer
  # echo 1 > tracing_on
  # echo 0 > tracing_max_latency
  # ls -ltr
  [...]
  # echo 0 > tracing_on
  # cat trace
  # tracer: preemptirqsoff
  #
  # preemptirqsoff latency trace v1.1.5 on 3.8.0-test+
  # --------------------------------------------------------------------
  # latency: 100 us, #4/4, CPU#3 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:4)
  #    -----------------
  #    | task: ls-2230 (uid:0 nice:0 policy:0 rt_prio:0)
  #    -----------------
  #  => started at: ata_scsi_queuecmd
  #  => ended at:   ata_scsi_queuecmd
  #
  #
  #                  _------=> CPU#            
  #                 / _-----=> irqs-off        
  #                | / _----=> need-resched    
  #                || / _---=> hardirq/softirq 
  #                ||| / _--=> preempt-depth   
  #                |||| /     delay             
  #  cmd     pid   ||||| time  |   caller      
  #     \   /      |||||  \    |   /           
        ls-2230    3d...    0us+: _raw_spin_lock_irqsave <-ata_scsi_queuecmd
        ls-2230    3...1  100us : _raw_spin_unlock_irqrestore <-ata_scsi_queuecmd
        ls-2230    3...1  101us+: trace_preempt_on <-ata_scsi_queuecmd
        ls-2230    3...1  111us : <stack trace>
   => sub_preempt_count
   => _raw_spin_unlock_irqrestore
   => ata_scsi_queuecmd
   => scsi_dispatch_cmd
   => scsi_request_fn
   => __blk_run_queue_uncond
   => __blk_run_queue
   => blk_queue_bio
   => submit_bio_noacct
   => submit_bio
   => submit_bh
   => ext3_bread
   => ext3_dir_bread
   => htree_dirblock_to_tree
   => ext3_htree_fill_tree
   => ext3_readdir
   => vfs_readdir
   => sys_getdents
   => system_call_fastpath

当在汇编代码中禁用中断时,在x86上的汇编中调用trace_hardirqs_off_thunk。如果没有函数跟踪,我们就不知道在抢占点中是否启用了中断。我们确实看到它开始时启用了抢占。

下面是函数跟踪集的跟踪::

  # tracer: preemptirqsoff
  #
  # preemptirqsoff latency trace v1.1.5 on 3.8.0-test+
  # --------------------------------------------------------------------
  # latency: 161 us, #339/339, CPU#3 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:4)
  #    -----------------
  #    | task: ls-2269 (uid:0 nice:0 policy:0 rt_prio:0)
  #    -----------------
  #  => started at: schedule
  #  => ended at:   mutex_unlock
  #
  #
  #                  _------=> CPU#            
  #                 / _-----=> irqs-off        
  #                | / _----=> need-resched    
  #                || / _---=> hardirq/softirq 
  #                ||| / _--=> preempt-depth   
  #                |||| /     delay             
  #  cmd     pid   ||||| time  |   caller      
  #     \   /      |||||  \    |   /           
  kworker/-59      3...1    0us : __schedule <-schedule
  kworker/-59      3d..1    0us : rcu_preempt_qs <-rcu_note_context_switch
  kworker/-59      3d..1    1us : add_preempt_count <-_raw_spin_lock_irq
  kworker/-59      3d..2    1us : deactivate_task <-__schedule
  kworker/-59      3d..2    1us : dequeue_task <-deactivate_task
  kworker/-59      3d..2    2us : update_rq_clock <-dequeue_task
  kworker/-59      3d..2    2us : dequeue_task_fair <-dequeue_task
  kworker/-59      3d..2    2us : update_curr <-dequeue_task_fair
  kworker/-59      3d..2    2us : update_min_vruntime <-update_curr
  kworker/-59      3d..2    3us : cpuacct_charge <-update_curr
  kworker/-59      3d..2    3us : __rcu_read_lock <-cpuacct_charge
  kworker/-59      3d..2    3us : __rcu_read_unlock <-cpuacct_charge
  kworker/-59      3d..2    3us : update_cfs_rq_blocked_load <-dequeue_task_fair
  kworker/-59      3d..2    4us : clear_buddies <-dequeue_task_fair
  kworker/-59      3d..2    4us : account_entity_dequeue <-dequeue_task_fair
  kworker/-59      3d..2    4us : update_min_vruntime <-dequeue_task_fair
  kworker/-59      3d..2    4us : update_cfs_shares <-dequeue_task_fair
  kworker/-59      3d..2    5us : hrtick_update <-dequeue_task_fair
  kworker/-59      3d..2    5us : wq_worker_sleeping <-__schedule
  kworker/-59      3d..2    5us : kthread_data <-wq_worker_sleeping
  kworker/-59      3d..2    5us : put_prev_task_fair <-__schedule
  kworker/-59      3d..2    6us : pick_next_task_fair <-pick_next_task
  kworker/-59      3d..2    6us : clear_buddies <-pick_next_task_fair
  kworker/-59      3d..2    6us : set_next_entity <-pick_next_task_fair
  kworker/-59      3d..2    6us : update_stats_wait_end <-set_next_entity
        ls-2269    3d..2    7us : finish_task_switch <-__schedule
        ls-2269    3d..2    7us : _raw_spin_unlock_irq <-finish_task_switch
        ls-2269    3d..2    8us : do_IRQ <-ret_from_intr
        ls-2269    3d..2    8us : irq_enter <-do_IRQ
        ls-2269    3d..2    8us : rcu_irq_enter <-irq_enter
        ls-2269    3d..2    9us : add_preempt_count <-irq_enter
        ls-2269    3d.h2    9us : exit_idle <-do_IRQ
  [...]
        ls-2269    3d.h3   20us : sub_preempt_count <-_raw_spin_unlock
        ls-2269    3d.h2   20us : irq_exit <-do_IRQ
        ls-2269    3d.h2   21us : sub_preempt_count <-irq_exit
        ls-2269    3d..3   21us : do_softirq <-irq_exit
        ls-2269    3d..3   21us : __do_softirq <-call_softirq
        ls-2269    3d..3   21us+: __local_bh_disable <-__do_softirq
        ls-2269    3d.s4   29us : sub_preempt_count <-_local_bh_enable_ip
        ls-2269    3d.s5   29us : sub_preempt_count <-_local_bh_enable_ip
        ls-2269    3d.s5   31us : do_IRQ <-ret_from_intr
        ls-2269    3d.s5   31us : irq_enter <-do_IRQ
        ls-2269    3d.s5   31us : rcu_irq_enter <-irq_enter
  [...]
        ls-2269    3d.s5   31us : rcu_irq_enter <-irq_enter
        ls-2269    3d.s5   32us : add_preempt_count <-irq_enter
        ls-2269    3d.H5   32us : exit_idle <-do_IRQ
        ls-2269    3d.H5   32us : handle_irq <-do_IRQ
        ls-2269    3d.H5   32us : irq_to_desc <-handle_irq
        ls-2269    3d.H5   33us : handle_fasteoi_irq <-handle_irq
  [...]
        ls-2269    3d.s5  158us : _raw_spin_unlock_irqrestore <-rtl8139_poll
        ls-2269    3d.s3  158us : net_rps_action_and_irq_enable.isra.65 <-net_rx_action
        ls-2269    3d.s3  159us : __local_bh_enable <-__do_softirq
        ls-2269    3d.s3  159us : sub_preempt_count <-__local_bh_enable
        ls-2269    3d..3  159us : idle_cpu <-irq_exit
        ls-2269    3d..3  159us : rcu_irq_exit <-irq_exit
        ls-2269    3d..3  160us : sub_preempt_count <-irq_exit
        ls-2269    3d...  161us : __mutex_unlock_slowpath <-mutex_unlock
        ls-2269    3d...  162us+: trace_hardirqs_on <-mutex_unlock
        ls-2269    3d...  186us : <stack trace>
   => __mutex_unlock_slowpath
   => mutex_unlock
   => process_output
   => n_tty_write
   => tty_write
   => vfs_write
   => sys_write
   => system_call_fastpath

这是一个有趣的轨迹。它开始于kworker运行和调度,然后ls接管。但是一旦ls释放了rq锁并启用了中断(但不是抢占),就会触发中断。当中断结束时,它开始运行软中断。但是当软中断运行时,另一个中断被触发。当一个中断在软中断中运行时,注释是’H’。

3.4.7 wakeup

人们感兴趣的一个常见情况是跟踪一个被唤醒的任务实际醒来所花费的时间。对于非Real-Time任务,这可以是任意的。但追踪它仍然是有趣的。

没有function跟踪::

  # echo 0 > options/function-trace
  # echo wakeup > current_tracer
  # echo 1 > tracing_on
  # echo 0 > tracing_max_latency
  # chrt -f 5 sleep 1
  # echo 0 > tracing_on
  # cat trace
  # tracer: wakeup
  #
  # wakeup latency trace v1.1.5 on 3.8.0-test+
  # --------------------------------------------------------------------
  # latency: 15 us, #4/4, CPU#3 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:4)
  #    -----------------
  #    | task: kworker/3:1H-312 (uid:0 nice:-20 policy:0 rt_prio:0)
  #    -----------------
  #
  #                  _------=> CPU#            
  #                 / _-----=> irqs-off        
  #                | / _----=> need-resched    
  #                || / _---=> hardirq/softirq 
  #                ||| / _--=> preempt-depth   
  #                |||| /     delay             
  #  cmd     pid   ||||| time  |   caller      
  #     \   /      |||||  \    |   /           
    <idle>-0       3dNs7    0us :      0:120:R   + [003]   312:100:R kworker/3:1H
    <idle>-0       3dNs7    1us+: ttwu_do_activate.constprop.87 <-try_to_wake_up
    <idle>-0       3d..3   15us : __schedule <-schedule
    <idle>-0       3d..3   15us :      0:120:R ==> [003]   312:100:R kworker/3:1H

tracer只跟踪系统中优先级最高的任务,避免跟踪正常情况。这里我们看到kworker的优先级是-20(不是很好),从它醒来到它运行的时间只花了15微秒。

非实时任务不是那么有趣。更有趣的跟踪是只关注实时任务。

3.4.8 wakeup_rt

在实时环境中,了解被唤醒的最高优先级任务到它执行的时间所花费的唤醒时间非常重要。这也被称为“调度延迟”。我强调一点,这是关于RT任务的。了解非rt任务的调度延迟也很重要,但是平均调度延迟对于非rt任务更好。像LatencyTop这样的工具更适合于这种测量。

实时环境对最坏情况下的延迟感兴趣。这是某事发生所需要的最长延迟,而不是平均值。我们可以有一个非常快的调度程序,它可能只在一段时间内有一个很大的延迟,但这并不适合实时任务。wakeup_rt跟踪程序被设计用来记录RT任务的最坏情况唤醒。不记录非RT任务,因为跟踪程序只记录一个最坏的情况,而跟踪不可预测的非RT任务将覆盖RT任务的最坏情况延迟(只需运行一段时间的正常唤醒跟踪程序,看看效果)。

由于此跟踪程序只处理RT任务,因此我们将以与之前的跟踪程序略有不同的方式运行此跟踪程序。我们不再执行’ls’,而是在’chrt’下运行’sleep 1’,这会改变任务的优先级::

  # echo 0 > options/function-trace
  # echo wakeup_rt > current_tracer
  # echo 1 > tracing_on
  # echo 0 > tracing_max_latency
  # chrt -f 5 sleep 1
  # echo 0 > tracing_on
  # cat trace
  # tracer: wakeup
  #
  # tracer: wakeup_rt
  #
  # wakeup_rt latency trace v1.1.5 on 3.8.0-test+
  # --------------------------------------------------------------------
  # latency: 5 us, #4/4, CPU#3 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:4)
  #    -----------------
  #    | task: sleep-2389 (uid:0 nice:0 policy:1 rt_prio:5)
  #    -----------------
  #
  #                  _------=> CPU#            
  #                 / _-----=> irqs-off        
  #                | / _----=> need-resched    
  #                || / _---=> hardirq/softirq 
  #                ||| / _--=> preempt-depth   
  #                |||| /     delay             
  #  cmd     pid   ||||| time  |   caller      
  #     \   /      |||||  \    |   /           
    <idle>-0       3d.h4    0us :      0:120:R   + [003]  2389: 94:R sleep
    <idle>-0       3d.h4    1us+: ttwu_do_activate.constprop.87 <-try_to_wake_up
    <idle>-0       3d..3    5us : __schedule <-schedule
    <idle>-0       3d..3    5us :      0:120:R ==> [003]  2389: 94:R sleep

在空闲系统上运行它,我们看到执行任务切换只花了5微秒。注意,由于调度中的跟踪点在实际的“切换”之前,所以当记录的任务即将调度进来时,我们停止跟踪。如果我们在调度程序的末尾添加一个新的标记,这可能会改变。

注意,记录的任务是“sleep”,PID为2389,rt_prio为5。这个优先级是用户空间优先级,而不是内部内核优先级。SCHED_FIFO策略为1,SCHED_RR策略为2。

注意,跟踪数据显示了内部优先级(99 - rtprio)::

<idle>-0       3d..3    5us :      0:120:R ==> [003]  2389: 94:R sleep

0:120:R表示空闲正在以良好的优先级0(120 - 120)运行,并处于运行状态’R’。睡眠任务被安排在2389:94:R。优先级是内核rtprio(99 - 5 = 94),它也处于运行状态。

对chrt -r 5和函数跟踪集执行相同的操作::

  echo 1 > options/function-trace

  # tracer: wakeup_rt
  #
  # wakeup_rt latency trace v1.1.5 on 3.8.0-test+
  # --------------------------------------------------------------------
  # latency: 29 us, #85/85, CPU#3 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:4)
  #    -----------------
  #    | task: sleep-2448 (uid:0 nice:0 policy:1 rt_prio:5)
  #    -----------------
  #
  #                  _------=> CPU#            
  #                 / _-----=> irqs-off        
  #                | / _----=> need-resched    
  #                || / _---=> hardirq/softirq 
  #                ||| / _--=> preempt-depth   
  #                |||| /     delay             
  #  cmd     pid   ||||| time  |   caller      
  #     \   /      |||||  \    |   /           
    <idle>-0       3d.h4    1us+:      0:120:R   + [003]  2448: 94:R sleep
    <idle>-0       3d.h4    2us : ttwu_do_activate.constprop.87 <-try_to_wake_up
    <idle>-0       3d.h3    3us : check_preempt_curr <-ttwu_do_wakeup
    <idle>-0       3d.h3    3us : resched_curr <-check_preempt_curr
    <idle>-0       3dNh3    4us : task_woken_rt <-ttwu_do_wakeup
    <idle>-0       3dNh3    4us : _raw_spin_unlock <-try_to_wake_up
    <idle>-0       3dNh3    4us : sub_preempt_count <-_raw_spin_unlock
    <idle>-0       3dNh2    5us : ttwu_stat <-try_to_wake_up
    <idle>-0       3dNh2    5us : _raw_spin_unlock_irqrestore <-try_to_wake_up
    <idle>-0       3dNh2    6us : sub_preempt_count <-_raw_spin_unlock_irqrestore
    <idle>-0       3dNh1    6us : _raw_spin_lock <-__run_hrtimer
    <idle>-0       3dNh1    6us : add_preempt_count <-_raw_spin_lock
    <idle>-0       3dNh2    7us : _raw_spin_unlock <-hrtimer_interrupt
    <idle>-0       3dNh2    7us : sub_preempt_count <-_raw_spin_unlock
    <idle>-0       3dNh1    7us : tick_program_event <-hrtimer_interrupt
    <idle>-0       3dNh1    7us : clockevents_program_event <-tick_program_event
    <idle>-0       3dNh1    8us : ktime_get <-clockevents_program_event
    <idle>-0       3dNh1    8us : lapic_next_event <-clockevents_program_event
    <idle>-0       3dNh1    8us : irq_exit <-smp_apic_timer_interrupt
    <idle>-0       3dNh1    9us : sub_preempt_count <-irq_exit
    <idle>-0       3dN.2    9us : idle_cpu <-irq_exit
    <idle>-0       3dN.2    9us : rcu_irq_exit <-irq_exit
    <idle>-0       3dN.2   10us : rcu_eqs_enter_common.isra.45 <-rcu_irq_exit
    <idle>-0       3dN.2   10us : sub_preempt_count <-irq_exit
    <idle>-0       3.N.1   11us : rcu_idle_exit <-cpu_idle
    <idle>-0       3dN.1   11us : rcu_eqs_exit_common.isra.43 <-rcu_idle_exit
    <idle>-0       3.N.1   11us : tick_nohz_idle_exit <-cpu_idle
    <idle>-0       3dN.1   12us : menu_hrtimer_cancel <-tick_nohz_idle_exit
    <idle>-0       3dN.1   12us : ktime_get <-tick_nohz_idle_exit
    <idle>-0       3dN.1   12us : tick_do_update_jiffies64 <-tick_nohz_idle_exit
    <idle>-0       3dN.1   13us : cpu_load_update_nohz <-tick_nohz_idle_exit
    <idle>-0       3dN.1   13us : _raw_spin_lock <-cpu_load_update_nohz
    <idle>-0       3dN.1   13us : add_preempt_count <-_raw_spin_lock
    <idle>-0       3dN.2   13us : __cpu_load_update <-cpu_load_update_nohz
    <idle>-0       3dN.2   14us : sched_avg_update <-__cpu_load_update
    <idle>-0       3dN.2   14us : _raw_spin_unlock <-cpu_load_update_nohz
    <idle>-0       3dN.2   14us : sub_preempt_count <-_raw_spin_unlock
    <idle>-0       3dN.1   15us : calc_load_nohz_stop <-tick_nohz_idle_exit
    <idle>-0       3dN.1   15us : touch_softlockup_watchdog <-tick_nohz_idle_exit
    <idle>-0       3dN.1   15us : hrtimer_cancel <-tick_nohz_idle_exit
    <idle>-0       3dN.1   15us : hrtimer_try_to_cancel <-hrtimer_cancel
    <idle>-0       3dN.1   16us : lock_hrtimer_base.isra.18 <-hrtimer_try_to_cancel
    <idle>-0       3dN.1   16us : _raw_spin_lock_irqsave <-lock_hrtimer_base.isra.18
    <idle>-0       3dN.1   16us : add_preempt_count <-_raw_spin_lock_irqsave
    <idle>-0       3dN.2   17us : __remove_hrtimer <-remove_hrtimer.part.16
    <idle>-0       3dN.2   17us : hrtimer_force_reprogram <-__remove_hrtimer
    <idle>-0       3dN.2   17us : tick_program_event <-hrtimer_force_reprogram
    <idle>-0       3dN.2   18us : clockevents_program_event <-tick_program_event
    <idle>-0       3dN.2   18us : ktime_get <-clockevents_program_event
    <idle>-0       3dN.2   18us : lapic_next_event <-clockevents_program_event
    <idle>-0       3dN.2   19us : _raw_spin_unlock_irqrestore <-hrtimer_try_to_cancel
    <idle>-0       3dN.2   19us : sub_preempt_count <-_raw_spin_unlock_irqrestore
    <idle>-0       3dN.1   19us : hrtimer_forward <-tick_nohz_idle_exit
    <idle>-0       3dN.1   20us : ktime_add_safe <-hrtimer_forward
    <idle>-0       3dN.1   20us : ktime_add_safe <-hrtimer_forward
    <idle>-0       3dN.1   20us : hrtimer_start_range_ns <-hrtimer_start_expires.constprop.11
    <idle>-0       3dN.1   20us : __hrtimer_start_range_ns <-hrtimer_start_range_ns
    <idle>-0       3dN.1   21us : lock_hrtimer_base.isra.18 <-__hrtimer_start_range_ns
    <idle>-0       3dN.1   21us : _raw_spin_lock_irqsave <-lock_hrtimer_base.isra.18
    <idle>-0       3dN.1   21us : add_preempt_count <-_raw_spin_lock_irqsave
    <idle>-0       3dN.2   22us : ktime_add_safe <-__hrtimer_start_range_ns
    <idle>-0       3dN.2   22us : enqueue_hrtimer <-__hrtimer_start_range_ns
    <idle>-0       3dN.2   22us : tick_program_event <-__hrtimer_start_range_ns
    <idle>-0       3dN.2   23us : clockevents_program_event <-tick_program_event
    <idle>-0       3dN.2   23us : ktime_get <-clockevents_program_event
    <idle>-0       3dN.2   23us : lapic_next_event <-clockevents_program_event
    <idle>-0       3dN.2   24us : _raw_spin_unlock_irqrestore <-__hrtimer_start_range_ns
    <idle>-0       3dN.2   24us : sub_preempt_count <-_raw_spin_unlock_irqrestore
    <idle>-0       3dN.1   24us : account_idle_ticks <-tick_nohz_idle_exit
    <idle>-0       3dN.1   24us : account_idle_time <-account_idle_ticks
    <idle>-0       3.N.1   25us : sub_preempt_count <-cpu_idle
    <idle>-0       3.N..   25us : schedule <-cpu_idle
    <idle>-0       3.N..   25us : __schedule <-preempt_schedule
    <idle>-0       3.N..   26us : add_preempt_count <-__schedule
    <idle>-0       3.N.1   26us : rcu_note_context_switch <-__schedule
    <idle>-0       3.N.1   26us : rcu_sched_qs <-rcu_note_context_switch
    <idle>-0       3dN.1   27us : rcu_preempt_qs <-rcu_note_context_switch
    <idle>-0       3.N.1   27us : _raw_spin_lock_irq <-__schedule
    <idle>-0       3dN.1   27us : add_preempt_count <-_raw_spin_lock_irq
    <idle>-0       3dN.2   28us : put_prev_task_idle <-__schedule
    <idle>-0       3dN.2   28us : pick_next_task_stop <-pick_next_task
    <idle>-0       3dN.2   28us : pick_next_task_rt <-pick_next_task
    <idle>-0       3dN.2   29us : dequeue_pushable_task <-pick_next_task_rt
    <idle>-0       3d..3   29us : __schedule <-preempt_schedule
    <idle>-0       3d..3   30us :      0:120:R ==> [003]  2448: 94:R sleep

这并不是一个很大的跟踪,即使启用了函数跟踪,所以这里包含了整个跟踪。

当系统处于空闲状态时中断停止。在调用task_woken_rt()之前,设置了NEED_RESCHED标志,这由第一次出现’N’标志表示。

3.4.9 Latency tracing and events

由于function tracing可能导致更大的延迟,但是如果不看到延迟中发生了什么,就很难知道是什么导致了延迟。有一个中间地带,那就是启用事件::

  # echo 0 > options/function-trace
  # echo wakeup_rt > current_tracer
  # echo 1 > events/enable
  # echo 1 > tracing_on
  # echo 0 > tracing_max_latency
  # chrt -f 5 sleep 1
  # echo 0 > tracing_on
  # cat trace
  # tracer: wakeup_rt
  #
  # wakeup_rt latency trace v1.1.5 on 3.8.0-test+
  # --------------------------------------------------------------------
  # latency: 6 us, #12/12, CPU#2 | (M:preempt VP:0, KP:0, SP:0 HP:0 #P:4)
  #    -----------------
  #    | task: sleep-5882 (uid:0 nice:0 policy:1 rt_prio:5)
  #    -----------------
  #
  #                  _------=> CPU#            
  #                 / _-----=> irqs-off        
  #                | / _----=> need-resched    
  #                || / _---=> hardirq/softirq 
  #                ||| / _--=> preempt-depth   
  #                |||| /     delay             
  #  cmd     pid   ||||| time  |   caller      
  #     \   /      |||||  \    |   /           
    <idle>-0       2d.h4    0us :      0:120:R   + [002]  5882: 94:R sleep
    <idle>-0       2d.h4    0us : ttwu_do_activate.constprop.87 <-try_to_wake_up
    <idle>-0       2d.h4    1us : sched_wakeup: comm=sleep pid=5882 prio=94 success=1 target_cpu=002
    <idle>-0       2dNh2    1us : hrtimer_expire_exit: hrtimer=ffff88007796feb8
    <idle>-0       2.N.2    2us : power_end: cpu_id=2
    <idle>-0       2.N.2    3us : cpu_idle: state=4294967295 cpu_id=2
    <idle>-0       2dN.3    4us : hrtimer_cancel: hrtimer=ffff88007d50d5e0
    <idle>-0       2dN.3    4us : hrtimer_start: hrtimer=ffff88007d50d5e0 function=tick_sched_timer expires=34311211000000 softexpires=34311211000000
    <idle>-0       2.N.2    5us : rcu_utilization: Start context switch
    <idle>-0       2.N.2    5us : rcu_utilization: End context switch
    <idle>-0       2d..3    6us : __schedule <-schedule
    <idle>-0       2d..3    6us :      0:120:R ==> [002]  5882: 94:R sleep
3.4.10 Hardware Latency Detector

通过启用hwlat tracer来执行硬件延迟检测器。

注意,这个tracer会影响系统的性能,因为它会周期性地使CPU在关闭中断的情况下持续忙碌::

  # echo hwlat > current_tracer
  # sleep 100
  # cat trace
  # tracer: hwlat
  #
  # entries-in-buffer/entries-written: 13/13   #P:8
  #
  #                              _-----=> irqs-off
  #                             / _----=> need-resched
  #                            | / _---=> hardirq/softirq
  #                            || / _--=> preempt-depth
  #                            ||| /     delay
  #           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
  #              | |       |   ||||       |         |
             <...>-1729  [001] d...   678.473449: #1     inner/outer(us):   11/12    ts:1581527483.343962693 count:6
             <...>-1729  [004] d...   689.556542: #2     inner/outer(us):   16/9     ts:1581527494.889008092 count:1
             <...>-1729  [005] d...   714.756290: #3     inner/outer(us):   16/16    ts:1581527519.678961629 count:5
             <...>-1729  [001] d...   718.788247: #4     inner/outer(us):    9/17    ts:1581527523.889012713 count:1
             <...>-1729  [002] d...   719.796341: #5     inner/outer(us):   13/9     ts:1581527524.912872606 count:1
             <...>-1729  [006] d...   844.787091: #6     inner/outer(us):    9/12    ts:1581527649.889048502 count:2
             <...>-1729  [003] d...   849.827033: #7     inner/outer(us):   18/9     ts:1581527654.889013793 count:1
             <...>-1729  [007] d...   853.859002: #8     inner/outer(us):    9/12    ts:1581527658.889065736 count:1
             <...>-1729  [001] d...   855.874978: #9     inner/outer(us):    9/11    ts:1581527660.861991877 count:1
             <...>-1729  [001] d...   863.938932: #10    inner/outer(us):    9/11    ts:1581527668.970010500 count:1 nmi-total:7 nmi-count:1
             <...>-1729  [007] d...   878.050780: #11    inner/outer(us):    9/12    ts:1581527683.385002600 count:1 nmi-total:5 nmi-count:1
             <...>-1729  [007] d...   886.114702: #12    inner/outer(us):    9/12    ts:1581527691.385001600 count:1

上面的输出在头文件中有些相同。所有事件都将被禁用中断’d’。在函数标题下有:

  • #1
    这是大于"tracing_thresh" 文件保存的最小延迟的事件记录计数(参见下面)。

  • inner/outer(us): 11/11

    这显示了两个数字:“inner延迟”和“outer延迟”。测试在一个循环中运行,检查两次时间戳。在两个时间戳中检测到的延迟是“内部延迟”,在循环中前一个时间戳和下一个时间戳之后检测到的延迟是“外部延迟”。

  • ts:1581527483.343962693

    在窗口中记录第一次延迟的绝对时间戳。

  • count:6

    在窗口期间检测到延迟的次数。

  • nmi-total:7 nmi-count:1

    在支持它的架构上,如果在测试期间出现一个NMI,花费在NMI上的时间以“nmi-total”(以微秒为单位)报告。

    如果在测试期间出现NMI,那么所有具有NMI的架构都将显示“NMI计数”。

3.4.10.1 hwlat files
  • tracing_threshold

    这将被自动设置为“10”来表示10微秒。这是在记录跟踪之前需要检测的延迟阈值。

    注意,当helat tracer完成时(另一个tracer写入"current_tracer"),tracing_thresh的原始值将会被放回这个文件中。

  • hwlat_detector/width

    禁用中断时测试运行的时间长度。(循环中关中断的时间 off)

  • hwlat_detector/window

    测试运行的窗口时间的长度,意思是将以每个窗口微妙的宽度运行(总的时间,off + on,其中on包括sleep的时间)。

  • tracing_cpumask

    当测试开始时。创建一个内核线程来运行测试。这个线程将在每个周期(一个“window”)之间在tracing_cpumask中列出的cpu之间交替。要将测试限制为特定的cpu,请将此文件中的掩码设置为只运行测试的cpu。

3.4.11 function

这个tracer是函数跟踪器。可以在debugfs中启用function tracer。确保设置了ftrace_enabled;否则这个tracer是一个nop tracer。看下面"ftrace_enable"一节::

  # sysctl kernel.ftrace_enabled=1
  # echo function > current_tracer
  # echo 1 > tracing_on
  # usleep 1
  # echo 0 > tracing_on
  # cat trace
  # tracer: function
  #
  # entries-in-buffer/entries-written: 24799/24799   #P:4
  #
  #                              _-----=> irqs-off
  #                             / _----=> need-resched
  #                            | / _---=> hardirq/softirq
  #                            || / _--=> preempt-depth
  #                            ||| /     delay
  #           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
  #              | |       |   ||||       |         |
              bash-1994  [002] ....  3082.063030: mutex_unlock <-rb_simple_write
              bash-1994  [002] ....  3082.063031: __mutex_unlock_slowpath <-mutex_unlock
              bash-1994  [002] ....  3082.063031: __fsnotify_parent <-fsnotify_modify
              bash-1994  [002] ....  3082.063032: fsnotify <-fsnotify_modify
              bash-1994  [002] ....  3082.063032: __srcu_read_lock <-fsnotify
              bash-1994  [002] ....  3082.063032: add_preempt_count <-__srcu_read_lock
              bash-1994  [002] ...1  3082.063032: sub_preempt_count <-__srcu_read_lock
              bash-1994  [002] ....  3082.063033: __srcu_read_unlock <-fsnotify
  [...]

注意:function tracer使用ring buffer来存储上述条目。最新的数据可能会覆盖旧数据。有时候使用echo停止跟踪是不够的,因为tracing可能已经覆盖了要记录的数据。出于这个原因,有时最好从程序中直接禁用tracing。这允许在到达感兴趣部分停止跟踪。要直接从C程序禁用跟踪,可以使用如下代码片段::

	int trace_fd;
	[...]
	int main(int argc, char *argv[]) {
		[...]
		trace_fd = open(tracing_file("tracing_on"), O_WRONLY);
		[...]
		if (condition_hit()) {
			write(trace_fd, "0", 1);
		}
		[...]
	}
3.4.12 Single thread tracing

通过写入set_ftrace_pid,可以跟踪单个线程。例如::

  # cat set_ftrace_pid
  no pid
  # echo 3111 > set_ftrace_pid
  # cat set_ftrace_pid
  3111
  # echo function > current_tracer
  # cat trace | head
  # tracer: function
  #
  #           TASK-PID    CPU#    TIMESTAMP  FUNCTION
  #              | |       |          |         |
      yum-updatesd-3111  [003]  1637.254676: finish_task_switch <-thread_return
      yum-updatesd-3111  [003]  1637.254681: hrtimer_cancel <-schedule_hrtimeout_range
      yum-updatesd-3111  [003]  1637.254682: hrtimer_try_to_cancel <-hrtimer_cancel
      yum-updatesd-3111  [003]  1637.254683: lock_hrtimer_base <-hrtimer_try_to_cancel
      yum-updatesd-3111  [003]  1637.254685: fget_light <-do_sys_poll
      yum-updatesd-3111  [003]  1637.254686: pipe_poll <-do_sys_poll
  # echo > set_ftrace_pid
  # cat trace |head
  # tracer: function
  #
  #           TASK-PID    CPU#    TIMESTAMP  FUNCTION
  #              | |       |          |         |
  ##### CPU 3 buffer started ####
      yum-updatesd-3111  [003]  1701.957688: free_poll_entry <-poll_freewait
      yum-updatesd-3111  [003]  1701.957689: remove_wait_queue <-free_poll_entry
      yum-updatesd-3111  [003]  1701.957691: fput <-free_poll_entry
      yum-updatesd-3111  [003]  1701.957692: audit_syscall_exit <-sysret_audit
      yum-updatesd-3111  [003]  1701.957693: path_put <-audit_syscall_exit

如果想在执行时跟踪一个函数,您可以使用类似于这个简单程序的东西::

	#include <stdio.h>
	#include <stdlib.h>
	#include <sys/types.h>
	#include <sys/stat.h>
	#include <fcntl.h>
	#include <unistd.h>
	#include <string.h>

	#define _STR(x) #x
	#define STR(x) _STR(x)
	#define MAX_PATH 256

	const char *find_tracefs(void)
	{
	       static char tracefs[MAX_PATH+1];
	       static int tracefs_found;
	       char type[100];
	       FILE *fp;

	       if (tracefs_found)
		       return tracefs;

	       if ((fp = fopen("/proc/mounts","r")) == NULL) {
		       perror("/proc/mounts");
		       return NULL;
	       }

	       while (fscanf(fp, "%*s %"
		             STR(MAX_PATH)
		             "s %99s %*s %*d %*d\n",
		             tracefs, type) == 2) {
		       if (strcmp(type, "tracefs") == 0)
		               break;
	       }
	       fclose(fp);

	       if (strcmp(type, "tracefs") != 0) {
		       fprintf(stderr, "tracefs not mounted");
		       return NULL;
	       }

	       strcat(tracefs, "/tracing/");
	       tracefs_found = 1;

	       return tracefs;
	}

	const char *tracing_file(const char *file_name)
	{
	       static char trace_file[MAX_PATH+1];
	       snprintf(trace_file, MAX_PATH, "%s/%s", find_tracefs(), file_name);
	       return trace_file;
	}

	int main (int argc, char **argv)
	{
		if (argc < 1)
		        exit(-1);

		if (fork() > 0) {
		        int fd, ffd;
		        char line[64];
		        int s;

		        ffd = open(tracing_file("current_tracer"), O_WRONLY);
		        if (ffd < 0)
		                exit(-1);
		        write(ffd, "nop", 3);

		        fd = open(tracing_file("set_ftrace_pid"), O_WRONLY);
		        s = sprintf(line, "%d\n", getpid());
		        write(fd, line, s);

		        write(ffd, "function", 8);

		        close(fd);
		        close(ffd);

		        execvp(argv[1], argv+1);
		}

		return 0;
	}

或者是这样简单的脚本::

  #!/bin/bash

  tracefs=`sed -ne 's/^tracefs \(.*\) tracefs.*/\1/p' /proc/mounts`
  echo nop > $tracefs/tracing/current_tracer
  echo 0 > $tracefs/tracing/tracing_on
  echo $$ > $tracefs/tracing/set_ftrace_pid
  echo function > $tracefs/tracing/current_tracer
  echo 1 > $tracefs/tracing/tracing_on
  exec "$@"
3.4.13 function graph tracer

function_graph tracer与function tracer类似,不同之处在于它探测函数的入口和出口。这是通过在每个task_struct中使用动态分配的返回地址堆栈实现的。在函数项上,tracer覆盖跟踪的每个函数的返回地址以设置一个自定义探针。因此原始返回地址存储在task_struct中的返回地址堆栈中。

在函数的两端进行探测会导致一些特殊的特性,例如:

  • 测量函数的执行时间
  • 有一个可靠的调用堆栈来绘制函数调用图

这个tracer在以下几种情况是很有用的:

  • 想要找到一个奇怪的内核行为的原因,并需要看到在任何区域(或特定区域)发生了什么细节
  • 你正好遇到了奇怪的延迟,但很难找到它的起源
  • 想要快速找到特定函数的路径
  • 只想看看一个正在运行的内核里面发生了什么
  # tracer: function_graph
  #
  # CPU  DURATION                  FUNCTION CALLS
  # |     |   |                     |   |   |   |

   0)               |  sys_open() {
   0)               |    do_sys_open() {
   0)               |      getname() {
   0)               |        kmem_cache_alloc() {
   0)   1.382 us    |          __might_sleep();
   0)   2.478 us    |        }
   0)               |        strncpy_from_user() {
   0)               |          might_fault() {
   0)   1.389 us    |            __might_sleep();
   0)   2.553 us    |          }
   0)   3.807 us    |        }
   0)   7.876 us    |      }
   0)               |      alloc_fd() {
   0)   0.668 us    |        _spin_lock();
   0)   0.570 us    |        expand_files();
   0)   0.586 us    |        _spin_unlock();

有几列可以被动态地启用和禁用。可以根据自己的需要,使用任何想要的选项组合:

  • 执行该功能的cpu编号默认为启用状态。有时候只跟踪一个cpu更好(参见"tracing_cpu_mask"文件),或者有时在cpu跟踪开关时可能会看到无序的函数调用。

    • 隐藏:echo nofuncgraph-cpu > trace_options
    • 显示:echo funcgraph-cpu > trace_options
  • 持续时间(函数的执行时间)显示在函数的右括号行上,如果是第一个叶节点,则显示在与当前函数相同的行上。默认是启用的。

    • 隐藏:echo nofuncgraph-duration > trace_options
    • 显示:echo funcgraph-duration > trace_options
  • 当达到持续时间阈值时,开销字段在持续时间字段之前。

    • 隐藏:echo nofuncgraph-overhead > trace_options
    • 显示:echo funcgraph-overhead > trace_options
    • 依赖于选项:funcgraph-duration

    ie::

        3) # 1837.709 us |          } /* __switch_to */
        3)               |          finish_task_switch() {
        3)   0.313 us    |            _raw_spin_unlock_irq();
        3)   3.177 us    |          }
        3) # 1889.063 us |        } /* __schedule */
        3) ! 140.417 us  |      } /* __schedule */
        3) # 2034.948 us |    } /* schedule */
        3) * 33998.59 us |  } /* schedule_preempt_disabled */
    
        [...]
    
        1)   0.260 us    |              msecs_to_jiffies();
        1)   0.313 us    |              __rcu_read_unlock();
        1) + 61.770 us   |            }
        1) + 64.479 us   |          }
        1)   0.313 us    |          rcu_bh_qs();
        1)   0.313 us    |          __local_bh_enable();
        1) ! 217.240 us  |        }
        1)   0.365 us    |        idle_cpu();
        1)               |        rcu_irq_exit() {
        1)   0.417 us    |          rcu_eqs_enter_common.isra.47();
        1)   3.125 us    |        }
        1) ! 227.812 us  |      }
        1) ! 457.395 us  |    }
        1) @ 119760.2 us |  }
    
        [...]
    
        2)               |    handle_IPI() {
        1)   6.979 us    |                  }
        2)   0.417 us    |      scheduler_ipi();
        1)   9.791 us    |                }
        1) + 12.917 us   |              }
        2)   3.490 us    |    }
        1) + 15.729 us   |            }
        1) + 18.542 us   |          }
        2) $ 3594274 us  |  }
    
    • flags::

      ‘+’ 表示函数超过10个usec。

      ‘!’ 表示函数超过100个usec。

      ‘#’ 表示函数超过1000个usec。

      ‘*’ 表示函数超过10毫秒。

      ‘@’ 表示函数超过100毫秒。

      ‘$’ 表示函数超过1秒。

  • task/pid字段显示执行该函数的线程cmdline和pid。默认是禁用的。

    • 隐藏:echo nofuncgraph-proc > trace_options
    • 显示:echo funcgraph-proc > trace_options

    ie::

        # tracer: function_graph
        #
        # CPU  TASK/PID        DURATION                  FUNCTION CALLS
        # |    |    |           |   |                     |   |   |   |
        0)    sh-4802     |               |                  d_free() {
        0)    sh-4802     |               |                    call_rcu() {
        0)    sh-4802     |               |                      __call_rcu() {
        0)    sh-4802     |   0.616 us    |                        rcu_process_gp_end();
        0)    sh-4802     |   0.586 us    |                        check_for_new_grace_period();
        0)    sh-4802     |   2.899 us    |                      }
        0)    sh-4802     |   4.040 us    |                    }
        0)    sh-4802     |   5.151 us    |                  }
        0)    sh-4802     | + 49.370 us   |                }
    
  • 绝对时间字段是系统时钟自启动以来给出的绝对时间戳。在函数的每次进入/退出时都会给出这个时间的快照。

    • 隐藏:echo nofuncgraph-abstime > trace_options
    • 显示:echo funcgraph-abstime > trace_options

    ie::

        #
        #      TIME       CPU  DURATION                  FUNCTION CALLS
        #       |         |     |   |                     |   |   |   |
        360.774522 |   1)   0.541 us    |                                          }
        360.774522 |   1)   4.663 us    |                                        }
        360.774523 |   1)   0.541 us    |                                        __wake_up_bit();
        360.774524 |   1)   6.796 us    |                                      }
        360.774524 |   1)   7.952 us    |                                    }
        360.774525 |   1)   9.063 us    |                                  }
        360.774525 |   1)   0.615 us    |                                  journal_mark_dirty();
        360.774527 |   1)   0.578 us    |                                  __brelse();
        360.774528 |   1)               |                                  reiserfs_prepare_for_journal() {
        360.774528 |   1)               |                                    unlock_buffer() {
        360.774529 |   1)               |                                      wake_up_bit() {
        360.774529 |   1)               |                                        bit_waitqueue() {
        360.774530 |   1)   0.594 us    |                                          __phys_addr();
    

如果函数的开头不在trace buffer中,则函数名总是显示在函数的右括号之后。

对于开头在跟踪缓冲区中的函数,可以启用在结束括号后显示函数名,以便使用grep更容易地搜索函数持续时间。默认是禁用的。

  • 隐藏:echo nofuncgraph-tail > trace_options

  • 显示:echo funcgraph-tail > trace_options

    使用nofuncgraph-tail(默认)的示例::

        0)               |      putname() {
        0)               |        kmem_cache_free() {
        0)   0.518 us    |          __phys_addr();
        0)   1.757 us    |        }
        0)   2.861 us    |      }
    

    示例funcgraph-tail::

        0)               |      putname() {
        0)               |        kmem_cache_free() {
        0)   0.518 us    |          __phys_addr();
        0)   1.757 us    |        } /* kmem_cache_free() */
        0)   2.861 us    |      } /* putname() */
    

例如,如果你想在__might_sleep()函数中添加注释,你只需要包含<linux/ftrace. h>;并在__might_sleep()::中调用trace_printk()

trace_printk("I'm a comment!\n")

将会产生::

   1)               |             __might_sleep() {
   1)               |                /* I'm a comment! */
   1)   1.449 us    |             }

在下面的“dynamic ftrace”一节中会发现此跟踪程序的其他有用特性,比如只跟踪特定的函数或任务。

3.5 dynamic ftrace

如果设置了CONFIG_DYNAMIC_FTRACE,当function tracing被禁用时,系统几乎没有开销的运行。它的工作方式是调用mcount函数(放在每个内核函数的开始处,由gcc中-pg开关产生)开始指向一个简单的返回指令。(启用FTRACE将在编译内核时包含-pg开关。)

在编译时,每个C文件对象都通过recordmcount程序(位于scripts目录中)运行。这个程序将解析C对象中的ELF头,以找到.text段中调用mcount的所有位置。从gcc 4.6版本开始,“-mentry"已经被添加到x86中,它调用”_fentry_“而不是"mcount”。在创建堆栈框架之前调用。

创建了一个名为“__mcount_loc”的段,它保存了对.text段中所有mcount/fentry调用站点的引用。recordmcount程序将此部分重新链接到原始对象中。内核的最后一个链接阶段将把所有这些引用添加到一个表中。

在启动时,在初始化SMP之前,动态ftrace代码扫描该表并将所有位置更新为nops。它还记录位置,这些位置被添加到available_filter_functions列表中。模块在加载时和执行之前被处理。当一个模块被卸载时,它也会从ftrace函数列表中删除它的函数。这在模块卸载代码中是自动的,模块作者不需要担心它。

当启用tracing时,修改功能跟踪点的过程取决于体系结构。旧的方法是使用kstop_machine来防止与执行被修改代码的CPU之间的竞争(这会导致CPU做一些不希望做的事情,特别是当修改的代码跨越缓存(或页面)边界时),并将nops修补回调用。但这一次,它们不调用mcount(它只是一个函数存根)。他们现在调用ftrace基础设施。

修改tracepoints函数的新方法是在要修改的位置放置一个断点,同步所有的cpu,修改断点未覆盖的指令的其余部分。再次同步所有的cpu,然后在完成后移除断点并保存到ftrace调用点。

有些架构甚至不需要在同步上瞎捣鼓,只要在旧代码之上添加新代码,其他cpu同时执行它也不会产生任何问题。

记录正在跟踪的函数的一个特殊副作用是,现在我们可以有选择地选择希望跟踪的函数,以及希望保留mcount调用为nops的函数。

使用了两个文件,一个用于启用,一个用于禁用指定函数的跟踪。它们是:

set_ftrace_filterset_ftrace_notrace

可以添加到这些文件的可用函数列表在available_filter_functions::

  # cat available_filter_functions
  put_prev_task_idle
  kmem_cache_create
  pick_next_task_rt
  get_online_cpus
  pick_next_task_fair
  mutex_lock
  [...]

如果我们仅对sys_nanosleep和hrtimer_interrupt感兴趣::

  # echo sys_nanosleep hrtimer_interrupt > set_ftrace_filter
  # echo function > current_tracer
  # echo 1 > tracing_on
  # usleep 1
  # echo 0 > tracing_on
  # cat trace
  # 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

要查看哪些函数正在被跟踪,可以cat这个文件::

# cat set_ftrace_filter
hrtimer_interrupt
sys_nanosleep

也许这还不够,filter还允许glob(7)匹配。

``<match>``

将从<match>的开始开始匹配函数。

``*<match>``

将从<match>的尾部开始匹配函数。

``*<match>*``

将从<match>的中间往前后开始匹配函数。

``<match1>*<match2>``

将从前后往中间开始匹配函数。

. .注意::

​ 最好使用引号将通配符括起来,否则shell可能会将参数展开为本地目录中的文件名。

  # echo 'hrtimer_*' > set_ftrace_filter
Produces::
  # tracer: function
  #
  # entries-in-buffer/entries-written: 897/897   #P:4
  #
  #                              _-----=> irqs-off
  #                             / _----=> need-resched
  #                            | / _---=> hardirq/softirq
  #                            || / _--=> preempt-depth
  #                            ||| /     delay
  #           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
  #              | |       |   ||||       |         |
            <idle>-0     [003] dN.1  4228.547803: hrtimer_cancel <-tick_nohz_idle_exit
            <idle>-0     [003] dN.1  4228.547804: hrtimer_try_to_cancel <-hrtimer_cancel
            <idle>-0     [003] dN.2  4228.547805: hrtimer_force_reprogram <-__remove_hrtimer
            <idle>-0     [003] dN.1  4228.547805: hrtimer_forward <-tick_nohz_idle_exit
            <idle>-0     [003] dN.1  4228.547805: hrtimer_start_range_ns <-hrtimer_start_expires.constprop.11
            <idle>-0     [003] d..1  4228.547858: hrtimer_get_next_event <-get_next_timer_interrupt
            <idle>-0     [003] d..1  4228.547859: hrtimer_start <-__tick_nohz_idle_enter
            <idle>-0     [003] d..2  4228.547860: hrtimer_force_reprogram <-__rem

注意,我们丢失了sys_nanosleep::

  # 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
  hrtimer_nanosleep
  hrtimer_wakeup
  hrtimer_get_remaining
  hrtimer_get_res
  hrtimer_init_sleeper

‘>‘和’>>‘是适用于ftrace的,要重写filter使用’>’,附加到filter使用’>>'。

清除一个filter,以便所有功能将被再次记录::

 # echo > set_ftrace_filter
 # cat set_ftrace_filter
 #

再一次,现在试试附加::

  # echo sys_nanosleep > set_ftrace_filter
  # cat set_ftrace_filter
  sys_nanosleep
  # echo 'hrtimer_*' >> set_ftrace_filter
  # 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

"set_ftrace_notrace"文件阻止这些函数被跟踪::

# echo '*preempt*' '*lock*' > set_ftrace_notrace
Produces::
  # tracer: function
  #
  # entries-in-buffer/entries-written: 39608/39608   #P:4
  #
  #                              _-----=> irqs-off
  #                             / _----=> need-resched
  #                            | / _---=> hardirq/softirq
  #                            || / _--=> preempt-depth
  #                            ||| /     delay
  #           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
  #              | |       |   ||||       |         |
              bash-1994  [000] ....  4342.324896: file_ra_state_init <-do_dentry_open
              bash-1994  [000] ....  4342.324897: open_check_o_direct <-do_last
              bash-1994  [000] ....  4342.324897: ima_file_check <-do_last
              bash-1994  [000] ....  4342.324898: process_measurement <-ima_file_check
              bash-1994  [000] ....  4342.324898: ima_get_action <-process_measurement
              bash-1994  [000] ....  4342.324898: ima_match_policy <-ima_get_action
              bash-1994  [000] ....  4342.324899: do_truncate <-do_last
              bash-1994  [000] ....  4342.324899: should_remove_suid <-do_truncate
              bash-1994  [000] ....  4342.324899: notify_change <-do_truncate
              bash-1994  [000] ....  4342.324900: current_fs_time <-notify_change
              bash-1994  [000] ....  4342.324900: current_kernel_time <-current_fs_time
              bash-1994  [000] ....  4342.324900: timespec_trunc <-current_fs_time

可以看到没有更多的锁或抢占跟踪。

3.5.1 Selecting function filters via index

因为处理字符串的开销很大(在与传入的字符串进行比较之前,需要查找函数的地址),所以也可以使用索引来启用函数。在一次设置数千个特定函数的情况下,这很有用。通过传入一个数字列表,不会发生任何字符串处理。相反,将选择内部数组中特定位置的函数(对应于“available_filter_functions”文件中的函数)。

# echo 1 > set_ftrace_filter

将选择“available_filter_functions”中列出的第一个函数::

  # head -1 available_filter_functions
  trace_initcall_finish_cb

  # cat set_ftrace_filter
  trace_initcall_finish_cb

  # head -50 available_filter_functions | tail -1
  x86_pmu_commit_txn

  # echo 1 50 > set_ftrace_filter
  # cat set_ftrace_filter
  trace_initcall_finish_cb
  x86_pmu_commit_txn
3.5.2 Dynamic ftrace with the function graph tracer

虽然上面已经解释了function tracer和function_graph tracer,但是有一些特殊的特性只在function_graph tracer中可用。

如果你只想跟踪一个函数及其所有子函数,你只需要将它的名字回传给set_graph_function::

# echo __do_fault > set_graph_function

将产生以下__do_fault()函数的“扩展”跟踪::

   0)               |  __do_fault() {
   0)               |    filemap_fault() {
   0)               |      find_lock_page() {
   0)   0.804 us    |        find_get_page();
   0)               |        __might_sleep() {
   0)   1.329 us    |        }
   0)   3.904 us    |      }
   0)   4.979 us    |    }
   0)   0.653 us    |    _spin_lock();
   0)   0.578 us    |    page_add_file_rmap();
   0)   0.525 us    |    native_set_pte_at();
   0)   0.585 us    |    _spin_unlock();
   0)               |    unlock_page() {
   0)   0.541 us    |      page_waitqueue();
   0)   0.639 us    |      __wake_up_bit();
   0)   2.786 us    |    }
   0) + 14.237 us   |  }
   0)               |  __do_fault() {
   0)               |    filemap_fault() {
   0)               |      find_lock_page() {
   0)   0.698 us    |        find_get_page();
   0)               |        __might_sleep() {
   0)   1.412 us    |        }
   0)   3.950 us    |      }
   0)   5.098 us    |    }
   0)   0.631 us    |    _spin_lock();
   0)   0.571 us    |    page_add_file_rmap();
   0)   0.526 us    |    native_set_pte_at();
   0)   0.586 us    |    _spin_unlock();
   0)               |    unlock_page() {
   0)   0.533 us    |      page_waitqueue();
   0)   0.638 us    |      __wake_up_bit();
   0)   2.793 us    |    }
   0) + 14.012 us   |  }

您还可以同时扩展多个函数::

# echo sys_open > set_graph_function
# echo sys_close >> set_graph_function

现在,如果你想返回跟踪所有函数,只需要清除这个特殊的过滤器::

# echo > set_graph_function

3.6 ftrace_enabled

注意,proc sysctl ftrace_enable是函数跟踪程序的一个很大的开/关开关。默认情况下,它是启用的(当函数跟踪在内核中启用时)。如果禁用,则禁用所有功能跟踪。这不仅包括ftrace的函数跟踪器,还包括用于任何其他用途的函数跟踪器(perf、kprobes、堆栈跟踪、profiling,etc)。如果有一个注册了FTRACE_OPS_FL_PERMANENT设置的回调,则不能禁用它。

请小心禁用此功能。

可以这样禁用(启用)::

# sysctl kernel.ftrace_enabled=0
# sysctl kernel.ftrace_enabled=1

或者::

# echo 0 > /proc/sys/kernel/ftrace_enabled
# echo 1 > /proc/sys/kernel/ftrace_enabled

3.7 Filter commands

"set_ftrace_filter"接口支持一些命令。

跟踪命令的格式:: <function>:<command>:<parameter>

下面的commands被支持:

  • mod

    该命令开启每个模块的功能过滤。参数定义模块。例如,如果只需要ext3模块中的write*函数,则运行:

    # echo 'write*:mod:ext3' > set_ftrace_filter
    

    这个命令与过滤器交互的方式与基于函数名的过滤相同。因此,通过向过滤器文件追加(>>),可以在不同模块中添加更多的函数。通过在前面添加"!"来移除特定模块::

    # echo '!writeback*:mod:ext3' >> set_ftrace_filter
    

    模块命令支持模块通配符。禁用跟踪所有功能,除了特定模块::

    # echo '!*:mod:!ext3' >> set_ftrace_filter
    

    禁用所有模块的跟踪,但仍然跟踪内核::

    # echo '!*:mod:*' >> set_ftrace_filter
    

    只在内核中启用过滤器::

    # echo '*write*:mod:!*' >> set_ftrace_filter
    

    为模块通配符启用过滤器::

    # echo '*write*:mod:*snd*' >> set_ftrace_filter
    
  • traceon/traceoff

    这些命令在命中指定的函数时打开或关闭跟踪。该参数确定跟踪系统被打开和关闭的次数。如果没有具体说明,则没有限制。例如,要禁用跟踪时,调度错误击中前5次,运行::

    # echo '__schedule_bug:traceoff:5' > set_ftrace_filter
    

    在命中__schedule_bug时总是关闭tracing::

    # echo '__schedule_bug:traceoff' > set_ftrace_filter
    

    无论是否将这些命令追加到set_ftrace_filter,这些命令都是累积的。要删除一个命令,在前面加上’!’然后删除参数::

    echo '!__schedule_bug:traceoff:0' > set_ftrace_filter
    

    上面的代码删除了具有计数器的__schedule_bug的traceoff命令。删除不带计数器的命令::

    # echo '!__schedule_bug:traceoff' > set_ftrace_filter
    
  • snapshot

    当函数命中时,将导致一个snapshot被触发::

    # echo 'native_flush_tlb_others:snapshot' > set_ftrace_filter
    

    仅触发一次:

    # echo 'native_flush_tlb_others:snapshot:1' > set_ftrace_filter
    

    为了移除上面的commands::

    # echo '!native_flush_tlb_others:snapshot' > set_ftrace_filter
    # echo '!native_flush_tlb_others:snapshot:0' > set_ftrace_filter
    
  • enable_event/disable_event

    这些命令可以启用/禁用跟踪事件。注意,因为function tracing是敏感的,当这些命令被注册,跟踪点被激活,但是在"soft"模式下是禁用的。意思是,跟踪点会被调用,但是不会被跟踪。只要有命令触发事件跟踪点,该事件跟踪点就会保持这种模式::

    # echo 'try_to_wake_up:enable_event:sched:sched_switch:2' > \
       	 set_ftrace_filter
    

    The format is::

    <function>:enable_event:<system>:<event>[:count]

    <function>:disable_event:<system>:<event>[:count]

    为了移除事件命令::

    # echo '!try_to_wake_up:enable_event:sched:sched_switch:0' > \
       	 set_ftrace_filter
    # echo '!schedule:disable_event:sched:sched_switch' > \
       	 set_ftrace_filter
    
  • dump

    当函数被命中时,它将把ftrace ring buffer的内容转储到控制台。如果您需要调试某些内容,并希望在遇到某个函数时转储跟踪,这将非常有用。也许它是一个在三重错误发生之前调用的函数,并且不允许获得常规转储。

  • cpudump

    当函数被命中时,它将把当前CPU的ftrace环缓冲区的内容转储到控制台。与“dump”命令不同,它只打印出执行触发转储的函数的CPU的环形缓冲区的内容。

  • stacktrace

    当函数被命中时,将记录一个堆栈跟踪。

3.8 trace_pipe

"trace_pipe"输出与"trace"文件相同的内容,但是对跟踪的影响不同。每次从"trace_pipe"读取都会被消耗。这意味着后续的读取将有所不同。跟踪是动态的::

  # echo function > current_tracer
  # cat trace_pipe > /tmp/trace.out &
  [1] 4153
  # echo 1 > tracing_on
  # usleep 1
  # echo 0 > tracing_on
  # cat trace
  # tracer: function
  #
  # entries-in-buffer/entries-written: 0/0   #P:4
  #
  #                              _-----=> irqs-off
  #                             / _----=> need-resched
  #                            | / _---=> hardirq/softirq
  #                            || / _--=> preempt-depth
  #                            ||| /     delay
  #           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
  #              | |       |   ||||       |         |

  #
  # cat /tmp/trace.out
             bash-1994  [000] ....  5281.568961: mutex_unlock <-rb_simple_write
             bash-1994  [000] ....  5281.568963: __mutex_unlock_slowpath <-mutex_unlock
             bash-1994  [000] ....  5281.568963: __fsnotify_parent <-fsnotify_modify
             bash-1994  [000] ....  5281.568964: fsnotify <-fsnotify_modify
             bash-1994  [000] ....  5281.568964: __srcu_read_lock <-fsnotify
             bash-1994  [000] ....  5281.568964: add_preempt_count <-__srcu_read_lock
             bash-1994  [000] ...1  5281.568965: sub_preempt_count <-__srcu_read_lock
             bash-1994  [000] ....  5281.568965: __srcu_read_unlock <-fsnotify
             bash-1994  [000] ....  5281.568967: sys_dup2 <-system_call_fastpath

注意,读取"trace_pipe"文件将会阻塞,直到添加更多输入。这与"trace"文件相反。如果任何进程打开"trace"文件进行读取,它实际上将禁用tracing并阻止添加新条目。"trace_pipe"文件没有这个限制。

3.9 trace entries

在诊断内核中的问题时,数据太多或不足都会带来麻烦。文件buffer_size_kb用于修改内部跟踪缓冲区的大小。列出的数字是每个CPU可以记录的条目数。要知道完整的大小,可以将可能的cpu数量与条目数量相乘::

  # cat buffer_size_kb
  1408 (units kilobytes)

或者简单的读取"buffer_total_size_kb"::

  # cat buffer_total_size_kb 
  5632

为了修改buffer大小,echo数值到文件里(以1024字节段为单位)::

  # echo 10000 > buffer_size_kb
  # cat buffer_size_kb
  10000 (units kilobytes)

它会尽可能多地分配。如果分配太多,可能会导致内存不足::

  # echo 1000000000000 > buffer_size_kb
  -bash: echo: write error: Cannot allocate memory
  # cat buffer_size_kb
  85

per_cpu缓冲区也可以单独更改:

  # echo 10000 > per_cpu/cpu0/buffer_size_kb
  # echo 100 > per_cpu/cpu1/buffer_size_kb

当per_cpu缓冲区不相同时,顶层的buffer_size_kb将只显示一个X::

  # cat buffer_size_kb
  X

这就是buffer_total_size_kb有用的地方::

  # cat buffer_total_size_kb 
  12916

写入顶层buffer_size_kb将重置所有缓冲区,使其再次保持不变。

3.10 Snapshot

CONFIG_TRACER_SNAPSHOT为所有非延迟tracer提供了一个通用快照特性。(记录最大延迟的延迟tracer,如“irqsoff”或“wakeup”,不能使用这个特性,因为它们已经在内部使用快照机制。)

快照在特定时间点保留当前跟踪缓冲区,而不停止跟踪。Ftrace将当前缓冲区与一个备用缓冲区交换,并在新的当前(=以前的备用)缓冲区中继续跟踪。

“tracing”中的以下跟踪文件与此特性相关:

  • snapshot

    这用于获取快照并读取快照的输出。Echo 1到该文件中以分配一个空闲缓冲区并获取一个快照(交换),然后以与“trace”相同的格式从该文件中读取快照(在“文件系统”一节中描述)。快照读取和跟踪都是可并行执行的。当分配空闲缓冲区时,echo 0释放它,而echo else(正)值则清除快照内容。更多的细节在下表中显示。

    ±-------------±-----------±-----------±-----------+
    |status\input | 0 | 1 | else |
    ++++==+
    |not allocated |(do nothing)| alloc+swap |(do nothing)|
    ±-------------±-----------±-----------±-----------+
    |allocated | free | swap | clear |
    ±-------------±-----------±-----------±-----------+

    下面是一个使用快照特性的示例::

      # echo 1 > events/sched/enable
      # echo 1 > snapshot
      # cat snapshot
      # tracer: nop
      #
      # entries-in-buffer/entries-written: 71/71   #P:8
      #
      #                              _-----=> irqs-off
      #                             / _----=> need-resched
      #                            | / _---=> hardirq/softirq
      #                            || / _--=> preempt-depth
      #                            ||| /     delay
      #           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
      #              | |       |   ||||       |         |
                <idle>-0     [005] d...  2440.603828: sched_switch: prev_comm=swapper/5 prev_pid=0 prev_prio=120   prev_state=R ==> next_comm=snapshot-test-2 next_pid=2242 next_prio=120
                 sleep-2242  [005] d...  2440.603846: sched_switch: prev_comm=snapshot-test-2 prev_pid=2242 prev_prio=120   prev_state=R ==> next_comm=kworker/5:1 next_pid=60 next_prio=120
      [...]
              <idle>-0     [002] d...  2440.707230: sched_switch: prev_comm=swapper/2 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=snapshot-test-2 next_pid=2229 next_prio=120  
    
      # cat trace  
      # tracer: nop
      #
      # entries-in-buffer/entries-written: 77/77   #P:8
      #
      #                              _-----=> irqs-off
      #                             / _----=> need-resched
      #                            | / _---=> hardirq/softirq
      #                            || / _--=> preempt-depth
      #                            ||| /     delay
      #           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
      #              | |       |   ||||       |         |
                <idle>-0     [007] d...  2440.707395: sched_switch: prev_comm=swapper/7 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=snapshot-test-2 next_pid=2243 next_prio=120
       snapshot-test-2-2229  [002] d...  2440.707438: sched_switch: prev_comm=snapshot-test-2 prev_pid=2229 prev_prio=120 prev_state=S ==> next_comm=swapper/2 next_pid=0 next_prio=120
      [...]
    

    如果当前tracer是延迟tracer之一时尝试使用此快照特性,将得到以下结果::

      # echo wakeup > current_tracer
      # echo 1 > snapshot
      bash: echo: write error: Device or resource busy
      # cat snapshot
      cat: snapshot: Device or resource busy
    

3.11 Instances

在tracefs跟踪目录中有一个名为“instances”的目录。

使用mkdir可以在这个目录中创建新的目录,使用rmdir可以删除目录。在这个目录中使用mkdir创建的目录在创建之后已经包含了文件和其他目录::

  # mkdir instances/foo
  # ls instances/foo
  buffer_size_kb  buffer_total_size_kb  events  free_buffer  per_cpu
  set_event  snapshot  trace  trace_clock  trace_marker  trace_options
  trace_pipe  tracing_on

如你所见,新目录看起来类似于tracing/目录本身。事实上,它非常相似,除了缓冲区和事件对于主目录或其他创建的实例是不可知的。

新目录中的文件与跟踪目录中同名的文件的工作方式相同,只是所使用的缓冲区是一个单独的新缓冲区。这些文件会影响该缓冲区,但不会影响主缓冲区(trace_options除外)。目前,trace_options对所有实例和顶层缓冲区的影响是一样的,但在未来的版本中可能会有所改变。也就是说,选项可能特定于它们所在的实例。

注意,这里没有function tracer文件,current_tracer和available_tracers也没有。这是因为缓冲区目前只能为其启用事件(注意,在目前测试的5.12新内核已经可以使用function tracer等等)::

  # mkdir instances/foo
  # mkdir instances/bar
  # mkdir instances/zoot
  # echo 100000 > buffer_size_kb
  # echo 1000 > instances/foo/buffer_size_kb
  # echo 5000 > instances/bar/per_cpu/cpu1/buffer_size_kb
  # echo function > current_trace
  # echo 1 > instances/foo/events/sched/sched_wakeup/enable
  # echo 1 > instances/foo/events/sched/sched_wakeup_new/enable
  # echo 1 > instances/foo/events/sched/sched_switch/enable
  # echo 1 > instances/bar/events/irq/enable
  # echo 1 > instances/zoot/events/syscalls/enable
  # cat trace_pipe
  CPU:2 [LOST 11745 EVENTS]
              bash-2044  [002] .... 10594.481032: _raw_spin_lock_irqsave <-get_page_from_freelist
              bash-2044  [002] d... 10594.481032: add_preempt_count <-_raw_spin_lock_irqsave
              bash-2044  [002] d..1 10594.481032: __rmqueue <-get_page_from_freelist
              bash-2044  [002] d..1 10594.481033: _raw_spin_unlock <-get_page_from_freelist
              bash-2044  [002] d..1 10594.481033: sub_preempt_count <-_raw_spin_unlock
              bash-2044  [002] d... 10594.481033: get_pageblock_flags_group <-get_pageblock_migratetype
              bash-2044  [002] d... 10594.481034: __mod_zone_page_state <-get_page_from_freelist
              bash-2044  [002] d... 10594.481034: zone_statistics <-get_page_from_freelist
              bash-2044  [002] d... 10594.481034: __inc_zone_state <-zone_statistics
              bash-2044  [002] d... 10594.481034: __inc_zone_state <-zone_statistics
              bash-2044  [002] .... 10594.481035: arch_dup_task_struct <-copy_process
  [...]

  # cat instances/foo/trace_pipe
              bash-1998  [000] d..4   136.676759: sched_wakeup: comm=kworker/0:1 pid=59 prio=120 success=1 target_cpu=000
              bash-1998  [000] dN.4   136.676760: sched_wakeup: comm=bash pid=1998 prio=120 success=1 target_cpu=000
            <idle>-0     [003] d.h3   136.676906: sched_wakeup: comm=rcu_preempt pid=9 prio=120 success=1 target_cpu=003
            <idle>-0     [003] d..3   136.676909: sched_switch: prev_comm=swapper/3 prev_pid=0 prev_prio=120 prev_state=R ==> next_comm=rcu_preempt next_pid=9 next_prio=120
       rcu_preempt-9     [003] d..3   136.676916: sched_switch: prev_comm=rcu_preempt prev_pid=9 prev_prio=120 prev_state=S ==> next_comm=swapper/3 next_pid=0 next_prio=120
              bash-1998  [000] d..4   136.677014: sched_wakeup: comm=kworker/0:1 pid=59 prio=120 success=1 target_cpu=000
              bash-1998  [000] dN.4   136.677016: sched_wakeup: comm=bash pid=1998 prio=120 success=1 target_cpu=000
              bash-1998  [000] d..3   136.677018: sched_switch: prev_comm=bash prev_pid=1998 prev_prio=120 prev_state=R+ ==> next_comm=kworker/0:1 next_pid=59 next_prio=120
       kworker/0:1-59    [000] d..4   136.677022: sched_wakeup: comm=sshd pid=1995 prio=120 success=1 target_cpu=001
       kworker/0:1-59    [000] d..3   136.677025: sched_switch: prev_comm=kworker/0:1 prev_pid=59 prev_prio=120 prev_state=S ==> next_comm=bash next_pid=1998 next_prio=120
  [...]

  # cat instances/bar/trace_pipe
       migration/1-14    [001] d.h3   138.732674: softirq_raise: vec=3 [action=NET_RX]
            <idle>-0     [001] dNh3   138.732725: softirq_raise: vec=3 [action=NET_RX]
              bash-1998  [000] d.h1   138.733101: softirq_raise: vec=1 [action=TIMER]
              bash-1998  [000] d.h1   138.733102: softirq_raise: vec=9 [action=RCU]
              bash-1998  [000] ..s2   138.733105: softirq_entry: vec=1 [action=TIMER]
              bash-1998  [000] ..s2   138.733106: softirq_exit: vec=1 [action=TIMER]
              bash-1998  [000] ..s2   138.733106: softirq_entry: vec=9 [action=RCU]
              bash-1998  [000] ..s2   138.733109: softirq_exit: vec=9 [action=RCU]
              sshd-1995  [001] d.h1   138.733278: irq_handler_entry: irq=21 name=uhci_hcd:usb4
              sshd-1995  [001] d.h1   138.733280: irq_handler_exit: irq=21 ret=unhandled
              sshd-1995  [001] d.h1   138.733281: irq_handler_entry: irq=21 name=eth0
              sshd-1995  [001] d.h1   138.733283: irq_handler_exit: irq=21 ret=handled
  [...]

  # cat instances/zoot/trace
  # tracer: nop
  #
  # entries-in-buffer/entries-written: 18996/18996   #P:4
  #
  #                              _-----=> irqs-off
  #                             / _----=> need-resched
  #                            | / _---=> hardirq/softirq
  #                            || / _--=> preempt-depth
  #                            ||| /     delay
  #           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
  #              | |       |   ||||       |         |
              bash-1998  [000] d...   140.733501: sys_write -> 0x2
              bash-1998  [000] d...   140.733504: sys_dup2(oldfd: a, newfd: 1)
              bash-1998  [000] d...   140.733506: sys_dup2 -> 0x1
              bash-1998  [000] d...   140.733508: sys_fcntl(fd: a, cmd: 1, arg: 0)
              bash-1998  [000] d...   140.733509: sys_fcntl -> 0x1
              bash-1998  [000] d...   140.733510: sys_close(fd: a)
              bash-1998  [000] d...   140.733510: sys_close -> 0x0
              bash-1998  [000] d...   140.733514: sys_rt_sigprocmask(how: 0, nset: 0, oset: 6e2768, sigsetsize: 8)
              bash-1998  [000] d...   140.733515: sys_rt_sigprocmask -> 0x0
              bash-1998  [000] d...   140.733516: sys_rt_sigaction(sig: 2, act: 7fff718846f0, oact: 7fff71884650, sigsetsize: 8)
              bash-1998  [000] d...   140.733516: sys_rt_sigaction -> 0x0

可以看到最顶部trace buffer的跟踪只显示了函数跟踪。foo实例显示唤醒和任务切换。

移除这些实例::

  # rmdir instances/foo
  # rmdir instances/bar
  # rmdir instances/zoot

注意,如果一个进程在一个实例目录中打开了一个跟踪文件,那么rmdir将返回-EBUSY失败。

3.12 Stack trace

因为内核有固定大小的堆栈,所以重要的是不要把它浪费在函数中。内核开发人员必须意识到他们在堆栈上分配了什么。如果它们添加太多,系统可能会面临堆栈溢出的危险,并将发生损坏,通常会导致系统panic。

有一些工具可以进行检查,通常使用中断定期检查使用情况。但如果你能在每个函数调用中执行检查,那将变得非常有用。由于ftrace提供了一个function tracer,因此它可以方便地在每次函数调用时检查堆栈大小。这是通过stack tracer启用的。

CONFIG_STACK_TRACER启用ftrace堆栈跟踪功能。

要启用它,在/proc/sys/kernel/stack_tracer_enabled中写入一个’1’::

# echo 1 > /proc/sys/kernel/stack_tracer_enabled

还可以通过在内核命令行参数中添加“stacktrace”来启用它,以便在启动期间跟踪内核的堆栈大小。

运行几分钟后,输出如下所示::

# cat stack_max_size
  2928

  # cat stack_trace
          Depth    Size   Location    (18 entries)
          -----    ----   --------
    0)     2928     224   update_sd_lb_stats+0xbc/0x4ac
    1)     2704     160   find_busiest_group+0x31/0x1f1
    2)     2544     256   load_balance+0xd9/0x662
    3)     2288      80   idle_balance+0xbb/0x130
    4)     2208     128   __schedule+0x26e/0x5b9
    5)     2080      16   schedule+0x64/0x66
    6)     2064     128   schedule_timeout+0x34/0xe0
    7)     1936     112   wait_for_common+0x97/0xf1
    8)     1824      16   wait_for_completion+0x1d/0x1f
    9)     1808     128   flush_work+0xfe/0x119
   10)     1680      16   tty_flush_to_ldisc+0x1e/0x20
   11)     1664      48   input_available_p+0x1d/0x5c
   12)     1616      48   n_tty_poll+0x6d/0x134
   13)     1568      64   tty_poll+0x64/0x7f
   14)     1504     880   do_select+0x31e/0x511
   15)      624     400   core_sys_select+0x177/0x216
   16)      224      96   sys_select+0x91/0xb9
   17)      128     128   system_call_fastpath+0x16/0x1b

注意,如果gcc使用-mentry,函数在建立堆栈frame之前就会被跟踪。这意味着在使用-mentry时,stack tracer不会测试到顶端叶的函数。

目前,-mfentry只在x86上的gcc 4.6.0及以上版本中使用。

4. Event 跟踪

4.1 set_event设置事件接口

可在/sys/kernel/debug/tracing/available_events查看可设置事件。

为了激活sched_wakeup事件,For example:

# echo sched_wakeup >> /sys/kernel/debug/tracing/set_event

. . Note:: '>>'是有必要的,否则它首先禁用所有事件。

为了禁用事件可在事件前加!:

# echo '!sched_wakeup' >> /sys/kernel/debug/tracing/set_event

为了禁用所有事件,向set_event写空:

# echo > /sys/kernel/debug/tracing/set_event

为了激活所有事件,echo *:*或者 、*: 到set_event文件:

# echo *:* > /sys/kernel/debug/tracing/set_event

事件被有序的组织到subsystems里,比如,ext4,irq,sched,etc.等,所有的事件名都像这种结构:<subsystem>:<event>。

子系统的名字是可选的,但是在available_events中是被列出来了的。所有子系统事件可用subsystem:*这样的语法,比如这条命令:

# echo 'irq:*' > /sys/kernel/debug/tracing/set_event

4.2 events的enable开关

所有可用事件也可以通过/sys/kernel/debug/tracing/events/这个文件夹查看

为了enable事件’sched_wakeup’::

# echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable

为了disable这个事件::

# echo 0 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable

为了在sched子系统中enable所有事件::

# echo 1 > /sys/kernel/debug/tracing/events/sched/enable

为了enable所有事件::

# echo 1 > /sys/kernel/debug/tracing/events/enable

当我们读这些enable文件时,可能有下面4种值:

  • 0 - 所有事件都被禁用
  • 1 - 所有事件都被激活
  • X - 这是一个混合集,有激活也有禁用
  • ? - 这个文件不影响任何事件

4.3 Boot选项

为了能便捷的尽早boot debug,使用boot选项:

trace_event=[event_list]

event_list以逗号分隔。

4.4 定义一个启动事件的跟踪点

TODO

可以查看内核samples/trace_events

4.5 事件格式formats

每个跟踪事件都有一个关联的formats文件,该文件包含记录了事件中每个字段的描述。这些信息可以用解析二进制跟踪流,并且也是查找可以在事件filters中使用的字段名的地方。也在文本模式下展示被用于打印事件的格式字符串,以及用于分析事件名和ID。

每个事件都有一组与之相关的’common’字段,字段前缀为common_,其他字段每个事件各不相同,他们对应于该事件的TRACE_EVENT。

每个字段格式如下::

field:field-type field-name; offset:N; size:N;

其中offset是跟踪记录字段的偏移量,size是数据项的大小(以字节为单位)

如下展示sched_wakeup事件的formats::

# cat /sys/kernel/debug/tracing/events/sched/sched_wakeup/formats
name: sched_wakeup
ID: 256
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:0;
        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->prio, REC->target_cpu

事件包含十个字段,前五个为common字段,后五个为事件特有字段。

4.6 事件过滤器 filters

事件可以在kernel中被过滤,通过boolean类型的’filter expressions’,一旦事件被记录到了trace buffer中,那就会根据该事件关联的filter筛选表达式来检查字段。

4.6.1 filters表达式语法

一个筛选表达式由一个或多个`判断`组成,可以用使用逻辑运算符号"&&“和”||"的组合。

它将记录事件中包含的字段值和常量进行比较,根据字段值是否匹配返回,不匹配(0),匹配(1)。

filed-name relational-operator value

圆括号可用于提供逻辑分组,双引号可用于防止shell将操作符解释为shell元字符。

filed-name可在formats中找到。

The relational-operators取决于被测试字段的类型。

这些 relational-operators可以用与数值类型字段:

==, !=, <, <=, >, >=, &

这些relational-operators可以用于字符串类型字段:

==, !=, ~

通配符~可以接受通配符字符(\*,?)和字符类([):

prev_comm ~ "*sh"
prev_comm ~ "sh*"
prev_comm ~ "*sh*"
prev_comm ~ "ba*sh"
4.6.2 设置filters

单个事件的筛选器是通过筛选器表达式写入给定事件的filters文件来设置的。

比如:

# cd /sys/kernel/debug/tracing/events/sched/sched_wakeup
# echo "common_preempt_count > 4" > filter

一个稍微复杂一点的例子:

# cd /sys/kernel/debug/tracing/events/signal/signal_generate
# echo "((sig >= 10 && sig < 15) || sig == 17) && comm != bash" > filter

如果表达式有错误,你会得到’Invalid argument’的错误,错误的字符串和错误消息可以通过查看filter看到,比如:

# cd /sys/kernel/debug/tracing/events/signal/signal_generate
# echo "((sig >= 10 && sig < 15) || dsig == 17) && comm != bash" > filter
-bash: echo: write error: Invalid argument
# cat filter
((sig >= 10 && sig < 15) || dsig == 17) && comm != bash
                                 ^
parse_error: Field not found
4.6.3 清除filters

为了清除一个事件的filters,向事件的filter文件写0。

为了清除一个子系统的所有filters,向subsystem的filter文件写0

4.6.4 Subsystem filters

为了方便,可以将一个过滤器表达式写入子系统的filter文件,从而将子系统中每个事件的filter作为一组设置或者清除。但是要注意,如果子系统中用于任何事件的过滤器缺少子系统过滤器中指定的字段,或者如果由于任何其他原因不能应用该过滤器,该事件的过滤器将保留原先已有的设置。这有可能导致意外的过滤器混用输出 ‘‘混乱’’ 跟踪信息(用户可能认为每个事件的过滤器的设置都是有效的)。只有仅引用公共字段"common_"的筛选器才能保证成功传播到所有事件。

下面是一些子系统过滤器例子,也说明了上述几点:

在sched subsystem中清除所有事件的filters::

# cd /sys/kernel/debug/tracing/events/sched
# echo 0 > filter
# cat sched_switch/filter
none
# cat sched_wakeup/filter
none

在sched subsystem中为所有事件设置过滤common字段::

# cd /sys/kernel/debug/tracing/events/sched
# echo common_pid == 0 > filter
# cat sched_switch/filter
common_pid == 0
# cat sched_wakeup/filter
common_pid == 0

尝试为sched subsystem中的所有事件设置一个非common的过滤器字段(除了那些带有prev_pid字段的事件外的所有事件保留他们原来的过滤器表达式)::

# cd /sys/kernel/debug/tracing/events/sched
# echo prev_pid == 0 > filter
# cat sched_switch/filter
prev_pid == 0
# cat sched_wakeup/filter
common_pid == 0
4.6.5 PID filtering

在顶层目录存在一个set_event_pid文件,用于过滤任务事件(这将仅跟踪当前任务事件)::

# cd /sys/kernel/debug/tracing
# echo $$ > set_event_pid
# echo 1 > events/enable

为了在不丢失已存在的PID的情况下添加更多PIDs,使用">>"。

# echo 123 244 1 >> set_event_pid

4.7 事件触发 triggers

跟踪事件可以有条件的调用触发器“命令”,这些命令可以采取各种形式,下面将详细描述;例如,在跟踪事件发生时启用或者禁用其他跟踪事件,或调用堆栈跟踪。每当调用带有附加触发器的跟踪事件时,就会调用与该事件关联的触发器命令集。任何给定的触发器还可以与事件筛选器关联,即事件只有通过了关联的筛选器才会调用该命令。如果没有与之关联的筛选器,他总是被调用的。

通过将触发器表达式写入给定事件的"trigger"文件,可以将触发器添加到特定事件中,也可以从特定事件中删除触发器。

一个给定的事件可以有任意数量的触发器与之关联,这取决于每条命令在这方面的限制。

事件触发器是在“soft”模式上实现的,这意味着每当跟踪事件有一个或者多个与之关联的触发器时,该事件即使没有实际启用,也会被激活,但会在“soft”模式下禁用,也就是说,跟踪点将被调用,但是不会被跟踪。

事件触发的语法大致是基于set_ftrace_filter,但有些是不同的,需要注意。

. .Note::

​ 写入trace_marker也可以启用写入/sys/kernel/tracing/events/ftrace/print/trigger的触发器。

4.7.1 触发器表达式语法

触发器是通过echo命令到"trigger"文件来添加的::

# echo 'command[:count] [if filter]' > trigger

通过在命令前加!来移除触发命令::

# echo '!command[:count] [if filter]' > trigger

当移除时[if filter]不会在匹配命令时使用,所以可以在!中省略它。

[if filter]的语法和过滤器表达式语法是相同的。

为了方便使用,使用>写入trigger文件目前只是添加或删除单个触发器,并没有显示的支持>>>的实际行为类似于>>)或者截断支持来删除所有触发器(对于每添加一个你必须使用!

4.7.2 被支持的触发器commands

下面这些commands被支持:

  • enable_event/disable_event

    这些命令可以在触发事件发生时启用或者禁用另一个跟踪事件。当这些命令被注册时,其他跟踪事件被激活,但是在“soft”模式下是被禁用的。也就是说跟踪点被调用,但是不会被跟踪。只要有一个有效的触发器可以触发事件跟踪点,该事件跟踪带你就会保持在此模式中。

    例如,下面的触发器在进入read系统调用时跟踪kmalloc事件,末尾的:1指定此启用只发生一次::

    echo 'enable_event:kmalloc:1' > \
    	/sys/kernel/debug/tracing/events/syscalls/sys_enter_read/trigger
    

    当一个read系统调用退出时,下面的触发器将导致kmalloc事件停止跟踪。这种禁用发生在每次read系统调用退出时::

    # echo 'disable_event:kmem:kmalloc' > \
    	/sys/kernel/debug/tracing/events/syscalls/sys_exit_read/trigger
    

    The format is::

    enable_event:<system>:<event>[:count]
    disable_event:<system>:<event>[:count]
    

    为了去移除上面的commands::

    # echo '!enable_event:kmem:kmalloc:1' > \
    	/sys/kernel/debug/tracing/events/syscalls/sys_enter_read/trigger
    
    # echo '!disable_event:kmem:kmalloc' > \
    	/sys/kernel/debug/tracing/events/syscalls/sys_exit_read/trigger
    

    注意,每个触发事件可以有任意数量的enable/disable_event触发器,但每个触发事件只能有一个触发器。例如,sys_enter_read可以有触发器同时启用kmem:kmalloc和sched:sched_switch,但不能有两个kmem:kmalloc版本,例如kmem:kmalloc和kmem:kmalloc:1或’kmem:kmalloc if bytes_req == 256’和’kmem:kmalloc if bytes_alloc == 256’(不过它们可以合并为kmem:kmalloc if bytes_alloc == 256’)。

  • stacktrace

    每当触发事件发生时,此命令将堆栈跟踪转储到trace buffer中。

    例如,下面的触发器在每次命中kmalloc跟踪点时转储堆栈跟踪::

    # echo 'stacktrace' > \
    	/sys/kernel/debug/tracing/events/kmem/kmalloc/trigger
    

    下面的触发器在大小为 size >= SZ_64K的kmalloc请求发生前5次时转储堆栈跟踪::

    # echo 'stacktrace:5 if bytes_req >= 65536' > \
    	/sys/kernel/debug/tracing/events/kmem/kmalloc/trigger
    

    The format is::

    stacktrace[:count]
    

    为了移除上面的commands::

    # echo '!stacktrace' > \
    	/sys/kernel/debug/tracing/events/kmem/kmalloc/trigger
    
    # echo '!stacktrace:5 if bytes_req >= 65536' > \
    	/sys/kernel/debug/tracing/events/kmem/kmalloc/trigger
    

    后者也可以更简单的移除::

    # echo '!stacktrace:5' > \
    	/sys/kernel/debug/tracing/events/kmem/kmalloc/trigger
    

    注意:每个触发事件只能有一个堆栈跟踪触发器。

  • snapshot

    当触发事件发生时,该命令将触发快照。

    下面的命令在每次depth > 1时拨出一个块请求队列时创建一个快照。如果当时正在跟踪一组事件或函数,快照跟踪缓冲区将在触发事件发生时捕获这些事件::

    # echo 'snapshot if nr_rq > 1' > \
    	/sys/kernel/debug/tracing/events/block/block_unplug/trigger
    

    仅触发一次快照::

    # echo 'snapshot:1 if nr_rq > 1' > \
    	/sys/kernel/debug/tracing/events/block/block_unplug/trigger
    

    移除上面的commands::

    # echo '!snapshot if nr_rq > 1' > \
    	/sys/kernel/debug/tracing/events/block/block_unplug/trigger
    
    # echo '!snapshot:1 if nr_rq > 1' > \
    	/sys/kernel/debug/tracing/events/block/block_unplug/trigger
    

    注意:每个触发事件只能有一个快照触发器。

  • traceon/traceoff

    当特定事件触发时这些命令将会开启或者关闭tracing。参数决定了tracing系统的开启或关闭次数,如果没有指定,则无限制。

    下面的命令在depth > 1首次拨出块请求时关闭tracing。如果当时正在跟踪一组事件或者函数,那么可以检查trace buffer,以查看导致触发事件的事件序列::

    # echo 'traceoff:1 if nr_rq > 1' > \
    	/sys/kernel/debug/tracing/events/block/block_unplug/trigger
    

    当nr_rq > 1时总是关闭tracing::

    # echo 'traceoff if nr_rq > 1' > \
    	/sys/kernel/debug/tracing/events/block/block_unplug/trigger
    

    移除上面的commands::

    # echo '!traceoff:1 if nr_rq > 1' > \
    	/sys/kernel/debug/tracing/events/block/block_unplug/trigger
    
    # echo '!traceoff if nr_rq > 1' > \
    	/sys/kernel/debug/tracing/events/block/block_unplug/trigger
    

    注意:每个触发事件只能有一个taceon/off。

  • hist

    TODO

    可以查看内核文档Documentation/trace/histogram.rst

4.8 在内核中使用trace event API

TODO

5. 参考文档

博客:https://richardweiyang-2.gitbook.io/kernel-exploring/00-index-3/04-ftrace_internal

文档:linux-5.12 Document/trace/ftrace.rstDocument/trace/events.rst

PDF:ftrace-kernel-hooks-2014.pdf

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值