事件追踪

作者:

Theodore Ts’o

更新者:

李泽帆 (Li Zefan) 和 Tom Zanussi

1. 简介

追踪点(参见使用 Linux 内核追踪点)无需创建自定义内核模块即可通过事件追踪基础设施注册探测函数。

并非所有追踪点都可以使用事件追踪系统进行追踪;内核开发者必须提供代码片段,定义追踪信息如何保存到追踪缓冲区,以及追踪信息应如何打印。

2. 使用事件追踪

2.1 通过“set_event”接口

可用于追踪的事件可以在文件 /sys/kernel/tracing/available_events 中找到。

要启用某个特定事件,例如“sched_wakeup”,只需将其回显到 /sys/kernel/tracing/set_event。例如:

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

注意

需要使用“>>”,否则它会首先禁用所有事件。

要禁用某个事件,将事件名称前加上感叹号回显到 set_event 文件:

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

要禁用所有事件,将空行回显到 set_event 文件:

# echo > /sys/kernel/tracing/set_event

要启用所有事件,将 *:**: 回显到 set_event 文件:

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

事件被组织成子系统,例如 ext4、irq、sched 等,完整的事件名称格式为:<subsystem>:<event>。子系统名称是可选的,但它会显示在 available_events 文件中。子系统中的所有事件可以通过 <subsystem>:* 语法指定;例如,要启用所有 irq 事件,可以使用以下命令:

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

set_event 文件也可以用来只启用与特定模块相关的事件:

# echo ':mod:<module>' > /sys/kernel/tracing/set_event

这将启用模块 <module> 中的所有事件。如果模块尚未加载,该字符串将被保存,当匹配 <module> 的模块加载时,事件启用操作就会生效。

:mod: 之前的文本将被解析以指定模块创建的特定事件:

# echo '<match>:mod:<module>' > /sys/kernel/tracing/set_event

上述命令将启用 <match> 匹配的任何系统或事件。如果 <match>"*",它将匹配所有事件。

要只启用系统中某个特定事件:

# echo '<system>:<event>:mod:<module>' > /sys/kernel/tracing/set_event

如果 <event>"*",它将匹配给定模块中系统内的所有事件。

2.2 通过“enable”开关

可用的事件也列在 /sys/kernel/tracing/events/ 目录层次结构中。

要启用事件“sched_wakeup”:

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

要禁用它:

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

要启用 sched 子系统中的所有事件:

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

要启用所有事件:

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

读取这些 enable 文件时,有四种结果:

  • 0 - 此文件影响的所有事件都已禁用

  • 1 - 此文件影响的所有事件都已启用

  • X - 存在事件启用和禁用混杂的情况

  • ? - 此文件不影响任何事件

2.3 启动选项

为了方便早期启动调试,使用启动选项:

trace_event=[event-list]

event-list 是一个逗号分隔的事件列表。事件格式请参阅 2.1 节。

3. 定义一个启用事件的追踪点

请参阅 samples/trace_events 中提供的示例。

4. 事件格式

每个追踪事件都有一个与之关联的“format”文件,其中包含每个已记录事件字段的描述。此信息可用于解析二进制追踪流,也是查找可用于事件过滤器(参见第 5 节)的字段名称的地方。

它还显示了将用于以文本模式打印事件的格式字符串,以及用于性能分析的事件名称和 ID。

每个事件都有一组关联的common字段;这些字段以common_为前缀。其他字段因事件而异,对应于该事件的 TRACE_EVENT 定义中定义的字段。

格式中的每个字段都有以下形式:

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

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

例如,以下是“sched_wakeup”事件显示的信息:

# cat /sys/kernel/tracing/events/sched/sched_wakeup/format

name: sched_wakeup
ID: 60
format:
        field:unsigned short common_type;       offset:0;       size:2;
        field:unsigned char common_flags;       offset:2;       size:1;
        field:unsigned char common_preempt_count;       offset:3;       size:1;
        field:int common_pid;   offset:4;       size:4;
        field:int common_tgid;  offset:8;       size:4;

        field:char comm[TASK_COMM_LEN]; offset:12;      size:16;
        field:pid_t pid;        offset:28;      size:4;
        field:int prio; offset:32;      size:4;
        field:int success;      offset:36;      size:4;
        field:int cpu;  offset:40;      size:4;

print fmt: "task %s:%d [%d] success=%d [%03d]", REC->comm, REC->pid,
           REC->prio, REC->success, REC->cpu

此事件包含 10 个字段,前 5 个是通用字段,后 5 个是事件特定的。此事件的所有字段都是数值型,除了“comm”是字符串型,这种区别对于事件过滤很重要。

5. 事件过滤

可以通过将布尔“过滤器表达式”与追踪事件关联起来,在内核中对它们进行过滤。事件一旦被记录到追踪缓冲区,其字段就会与该事件类型关联的过滤器表达式进行检查。字段值“匹配”过滤器的事件将出现在追踪输出中,而值不匹配的事件将被丢弃。没有过滤器关联的事件匹配所有内容,并且在未为事件设置过滤器时是默认行为。

5.1 表达式语法

一个过滤器表达式由一个或多个“谓词”组成,这些谓词可以使用逻辑运算符“&&”和“||”进行组合。谓词只是一个子句,它将已记录事件中包含的字段的值与常量值进行比较,并根据字段值是否匹配(1)或不匹配(0)返回 0 或 1。

field-name relational-operator value

括号可用于提供任意的逻辑分组,双引号可用于防止 shell 将运算符解释为 shell 元字符。

可用于过滤器的字段名称可在追踪事件的“format”文件中找到(参见第 4 节)。

关系运算符取决于被测试字段的类型:

数值字段可用的运算符是:

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

字符串字段的运算符是:

==, !=, ~

glob (~) 接受通配符 (*,?) 和字符类 ([)。例如:

prev_comm ~ "*sh"
prev_comm ~ "sh*"
prev_comm ~ "*sh*"
prev_comm ~ "ba*sh"

如果字段是指向用户空间的指针(例如 sys_enter_openat 中的“filename”),则必须在字段名称后附加“.ustring”:

filename.ustring ~ "password"

因为内核需要知道如何从用户空间中检索指针指向的内存。

您可以将任何长类型转换为函数地址并按函数名称搜索:

call_site.function == security_prepare_creds

当字段“call_site”落入“security_prepare_creds”函数地址范围内时,上述命令将进行过滤。也就是说,它将比较“call_site”的值,如果它大于或等于“security_prepare_creds”函数的起始地址且小于该函数的结束地址,则过滤器将返回 true。

“.function”后缀只能附加到 long 类型的值,并且只能与“==”或“!=”进行比较。

Cpumask 字段或编码 CPU 编号的标量字段可以使用用户提供的 cpulist 格式的 cpumask 进行过滤。格式如下:

CPUS{$cpulist}

cpumask 过滤可用的运算符是:

& (交集), ==, !=

例如,这将过滤其 .target_cpu 字段存在于给定 cpumask 中的事件:

target_cpu & CPUS{17-42}

5.2 设置过滤器

通过将过滤器表达式写入给定事件的“filter”文件来设置单个事件的过滤器。

例如:

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

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

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

如果表达式中有错误,设置时会得到“Invalid argument”错误,并且通过查看过滤器(例如)可以看到错误的字符串和错误消息:

# cd /sys/kernel/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

目前,错误指示符(‘^’)总是出现在过滤器字符串的开头;但即使没有更准确的位置信息,错误消息也应该有用。

5.2.1 过滤器限制

如果对指向环形缓冲区之外的内核或用户空间内存的字符串指针 (char *) 设置过滤器,出于安全原因,最多会将 1024 字节的内容复制到临时缓冲区进行比较。如果内存复制失败(指针指向不应访问的内存),则字符串比较将被视为不匹配。

5.3 清除过滤器

要清除事件的过滤器,将“0”写入事件的过滤器文件。

要清除子系统中所有事件的过滤器,将“0”写入子系统的过滤器文件。

5.4 子系统过滤器

为了方便,可以通过将过滤器表达式写入子系统根目录下的过滤器文件,将子系统中每个事件的过滤器作为一个组进行设置或清除。但是请注意,如果子系统中任何事件的过滤器缺少子系统过滤器中指定的字段,或者由于任何其他原因无法应用过滤器,则该事件的过滤器将保留其先前的设置。这可能导致意外的过滤器混合,从而导致令人困惑(对于可能认为不同过滤器生效的用户)的追踪输出。只有仅引用通用字段的过滤器才能保证成功传播到所有事件。

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

清除 sched 子系统中所有事件的过滤器:

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

为 sched 子系统中的所有事件设置一个只使用通用字段的过滤器(所有事件最终都使用相同的过滤器):

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

尝试为 sched 子系统中的所有事件设置一个使用非通用字段的过滤器(除了那些具有 prev_pid 字段的事件外,所有事件都保留其旧过滤器):

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

5.5 PID 过滤

与顶级 events 目录位于同一目录下的 set_event_pid 文件存在,它将过滤所有不包含在 set_event_pid 文件中列出的 PID 的任务的追踪事件。

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

将只追踪当前任务的事件。

要添加更多 PID 而不丢失已包含的 PID,请使用“>>”。

# echo 123 244 1 >> set_event_pid

6. 事件触发器

追踪事件可以被设置为有条件地调用触发器“命令”,这些命令可以采用各种形式,详述如下;例如,当追踪事件被触发时,启用或禁用其他追踪事件,或者调用堆栈追踪。每当一个带有附加触发器的追踪事件被调用时,与该事件关联的触发器命令集就会被调用。任何给定的触发器还可以额外关联一个与第 5 节(事件过滤)中描述的相同形式的事件过滤器——只有当被调用的事件通过关联的过滤器时,命令才会被调用。如果没有过滤器与触发器关联,它总是通过。

通过将触发器表达式写入给定事件的“trigger”文件来向特定事件添加和移除触发器。

给定事件可以关联任意数量的触发器,但受限于各个命令在这方面的任何限制。

事件触发器是在“软”模式之上实现的,这意味着无论何时一个追踪事件关联了一个或多个触发器,即使它实际上没有启用,也会以“软”模式激活该事件。也就是说,追踪点将被调用,但不会被追踪,当然除非它实际被启用。这种机制允许即使对于未启用的事件,触发器也能被调用,并且还允许使用当前的事件过滤器实现有条件地调用触发器。

事件触发器的语法大致基于 set_ftrace_filter “ftrace 过滤器命令”的语法(参见ftrace - 函数追踪器的“过滤器命令”部分),但存在主要差异,并且实现目前与它没有任何关联,因此请注意不要在这两者之间进行泛化。

注意

写入 trace_marker (参见ftrace - 函数追踪器) 也可以启用写入 /sys/kernel/tracing/events/ftrace/print/trigger 的触发器。

6.1 表达式语法

通过将命令回显到“trigger”文件来添加触发器:

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

通过将相同的命令(但以“!”开头)回显到“trigger”文件来移除触发器:

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

[if filter] 部分在移除时不会用于匹配命令,因此在“!”命令中省略它会达到与包含它相同的效果。

过滤器语法与上面“事件过滤”一节中描述的相同。

为了方便使用,目前使用“>”写入触发文件仅添加或移除单个触发器,并且没有明确的“>>”支持(“>”实际上行为类似于“>>”)或截断支持以移除所有触发器(您必须为每个添加的触发器使用“!”)。

6.2 支持的触发命令

支持以下命令:

  • enable_event/disable_event

    当触发事件发生时,这些命令可以启用或禁用另一个追踪事件。当这些命令注册时,另一个追踪事件被激活,但处于“软”禁用模式。也就是说,追踪点将被调用,但不会被追踪。只要有触发器可以触发它,事件追踪点就保持此模式。

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

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

    以下触发器导致在 read 系统调用退出时停止追踪 kmalloc 事件。此禁用操作在每次 read 系统调用退出时发生:

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

    格式是:

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

    要删除上述命令:

    # echo '!enable_event:kmem:kmalloc:1' > \
        /sys/kernel/tracing/events/syscalls/sys_enter_read/trigger
    
    # echo '!disable_event:kmem:kmalloc' > \
        /sys/kernel/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 上的单个过滤器)。

  • stacktrace

    每当触发事件发生时,此命令会在追踪缓冲区中转储堆栈跟踪。

    例如,以下触发器每次命中 kmalloc 追踪点时都会转储堆栈跟踪:

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

    以下触发器会在 kmalloc 请求发生前 5 次(大小 >= 64K)时转储堆栈跟踪:

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

    格式是:

    stacktrace[:count]
    

    要删除上述命令:

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

    后者也可以更简单地通过以下方式删除(不带过滤器):

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

    请注意,每个触发事件只能有一个 stacktrace 触发器。

  • snapshot

    此命令使快照在触发事件发生时被触发。

    以下命令在块请求队列深度大于 1 被拔出时创建快照。如果您当时正在追踪一组事件或函数,则快照追踪缓冲区将在触发事件发生时捕获这些事件:

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

    只快照一次:

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

    要删除上述命令:

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

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

  • traceon/traceoff

    当指定的事件发生时,这些命令会打开和关闭追踪。参数决定了追踪系统打开和关闭的次数。如果未指定,则没有限制。

    以下命令会在块请求队列深度大于 1 首次拔出时关闭追踪。如果您当时正在追踪一组事件或函数,您可以检查追踪缓冲区以查看导致触发事件的事件序列:

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

    当 nr_rq > 1 时始终禁用追踪:

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

    要删除上述命令:

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

    请注意,每个触发事件只能有一个 traceon 或 traceoff 触发器。

  • hist

    此命令将事件命中聚合到一个哈希表中,该哈希表以一个或多个追踪事件格式字段(或堆栈跟踪)为键,并以一个或多个追踪事件格式字段和/或事件计数(hitcount)派生的一组运行总数为值。

    有关详细信息和示例,请参阅事件直方图

7. 内核追踪事件 API

在大多数情况下,追踪事件的命令行界面已经足够。然而,有时应用程序可能需要比通过简单的系列链接命令行表达式所能表达的更复杂的关系,或者组合命令集可能过于繁琐。一个例子是应用程序需要“监听”追踪流以维护内核状态机,例如,检测调度程序中何时发生非法内核状态。

追踪事件子系统提供了一个内核 API,允许模块或其他内核代码随意生成用户定义的“合成”事件,这些事件可用于增强现有追踪流和/或发出特定重要状态已发生的信号。

类似的内核 API 也可用于创建 kprobe 和 kretprobe 事件。

合成事件和 k/ret/probe 事件 API 都建立在更底层的“dynevent_cmd”事件命令 API 之上,该 API 也可用于更专业的应用程序,或作为其他更高级别追踪事件 API 的基础。

为这些目的提供的 API 描述如下,并允许以下功能:

  • 动态创建合成事件定义

  • 动态创建 kprobe 和 kretprobe 事件定义

  • 从内核代码追踪合成事件

  • 底层的“dynevent_cmd”API

7.1 动态创建合成事件定义

有几种方法可以从内核模块或其他内核代码创建新的合成事件。

第一种方法是一步到位地创建事件,使用 synth_event_create()。在此方法中,要创建的事件名称和定义字段的数组会提供给 synth_event_create()。如果成功,调用后将存在具有该名称和字段的合成事件。例如,要创建一个新的“schedtest”合成事件:

ret = synth_event_create("schedtest", sched_fields,
                         ARRAY_SIZE(sched_fields), THIS_MODULE);

此示例中的 sched_fields 参数指向一个 struct synth_field_desc 数组,其中每个元素都按类型和名称描述一个事件字段:

static struct synth_field_desc sched_fields[] = {
      { .type = "pid_t",              .name = "next_pid_field" },
      { .type = "char[16]",           .name = "next_comm_field" },
      { .type = "u64",                .name = "ts_ns" },
      { .type = "u64",                .name = "ts_ms" },
      { .type = "unsigned int",       .name = "cpu" },
      { .type = "char[64]",           .name = "my_string_field" },
      { .type = "int",                .name = "my_int_field" },
};

有关可用类型,请参阅 synth_field_size()

如果 field_name 包含 [n],则该字段被视为静态数组。

如果 field_names 包含 [] (无下标),则该字段被视为动态数组,它在事件中只会占用容纳数组所需的空间。

因为事件的空间是在分配字段值给事件之前预留的,所以使用动态数组意味着下面描述的分段式内核 API 不能与动态数组一起使用。但是,其他非分段式内核 API 可以与动态数组一起使用。

如果事件是从模块内部创建的,则必须将指向该模块的指针传递给 synth_event_create()。这将确保在模块移除时,追踪缓冲区不会包含不可读的事件。

至此,事件对象已准备好用于生成新事件。

在第二种方法中,事件分几个步骤创建。这允许动态创建事件,而无需事先创建和填充字段数组。

要使用此方法,应首先使用 synth_event_gen_cmd_start()synth_event_gen_cmd_array_start() 创建一个空或部分空的合成事件。对于 synth_event_gen_cmd_start(),应提供事件名称以及一个或多个参数对,每对参数代表一个“type field_name;”字段规范。对于 synth_event_gen_cmd_array_start(),应提供事件名称以及一个 struct synth_field_desc 数组。在调用 synth_event_gen_cmd_start()synth_event_gen_cmd_array_start() 之前,用户应使用 synth_event_cmd_init() 创建并初始化一个 dynevent_cmd 对象。

例如,创建一个包含两个字段的新“schedtest”合成事件:

struct dynevent_cmd cmd;
char *buf;

/* Create a buffer to hold the generated command */
buf = kzalloc(MAX_DYNEVENT_CMD_LEN, GFP_KERNEL);

/* Before generating the command, initialize the cmd object */
synth_event_cmd_init(&cmd, buf, MAX_DYNEVENT_CMD_LEN);

ret = synth_event_gen_cmd_start(&cmd, "schedtest", THIS_MODULE,
                                "pid_t", "next_pid_field",
                                "u64", "ts_ns");

或者,使用包含相同信息的 struct synth_field_desc 字段数组:

ret = synth_event_gen_cmd_array_start(&cmd, "schedtest", THIS_MODULE,
                                      fields, n_fields);

合成事件对象创建后,可以填充更多字段。字段通过 synth_event_add_field() 一个接一个地添加,提供 dynevent_cmd 对象、字段类型和字段名称。例如,要添加一个名为“intfield”的新 int 字段,应进行以下调用:

ret = synth_event_add_field(&cmd, "int", "intfield");

有关可用类型,请参阅 synth_field_size()。如果 field_name 包含 [n],则该字段被视为数组。

也可以使用 add_synth_fields() 和一个 synth_field_desc 数组一次性添加一组字段。例如,这将只添加前四个 sched_fields

ret = synth_event_add_fields(&cmd, sched_fields, 4);

如果您已经有一个“type field_name”形式的字符串,可以使用 synth_event_add_field_str() 直接添加它;它还会自动在字符串末尾添加一个“;”。

添加完所有字段后,应通过调用 synth_event_gen_cmd_end() 函数来最终确定并注册事件:

ret = synth_event_gen_cmd_end(&cmd);

此时,事件对象已准备好用于追踪新事件。

7.2 从内核代码追踪合成事件

要追踪合成事件,有几种选项。第一种选项是在一次调用中追踪事件,使用带有可变数量值的 synth_event_trace(),或使用带有值数组的 synth_event_trace_array()。第二种选项可以避免预先形成值数组或参数列表的需要,通过 synth_event_trace_start()synth_event_trace_end() 以及 synth_event_add_next_val()synth_event_add_val() 来分段添加值。

7.2.1 一次性追踪合成事件

要一次性追踪合成事件,可以使用 synth_event_trace()synth_event_trace_array() 函数。

synth_event_trace() 函数传入代表合成事件的 trace_event_file(可以使用 trace_get_event_file() 通过合成事件名称、“synthetic”作为系统名称和追踪实例名称(如果使用全局追踪数组则为 NULL)来检索),以及可变数量的 u64 参数,每个合成事件字段一个,以及传入值的数量。

因此,要追踪与上述合成事件定义对应的事件,可以使用以下代码:

ret = synth_event_trace(create_synth_test, 7, /* number of values */
                        444,             /* next_pid_field */
                        (u64)"clackers", /* next_comm_field */
                        1000000,         /* ts_ns */
                        1000,            /* ts_ms */
                        smp_processor_id(),/* cpu */
                        (u64)"Thneed",   /* my_string_field */
                        999);            /* my_int_field */

所有 vals 都应转换为 u64,字符串 vals 只是指向字符串的指针,转换为 u64。字符串将使用这些指针复制到事件中为字符串预留的空间。

或者,可以使用 synth_event_trace_array() 函数完成相同的操作。它传入代表合成事件的 trace_event_file(可以使用 trace_get_event_file() 通过合成事件名称、“synthetic”作为系统名称和追踪实例名称(如果使用全局追踪数组则为 NULL)来检索),以及一个 u64 数组,每个合成事件字段一个。

要追踪与上述合成事件定义对应的事件,可以使用以下代码:

u64 vals[7];

vals[0] = 777;                  /* next_pid_field */
vals[1] = (u64)"tiddlywinks";   /* next_comm_field */
vals[2] = 1000000;              /* ts_ns */
vals[3] = 1000;                 /* ts_ms */
vals[4] = smp_processor_id();   /* cpu */
vals[5] = (u64)"thneed";        /* my_string_field */
vals[6] = 398;                  /* my_int_field */

“vals”数组只是一个 u64 数组,其数量必须与合成事件中的字段数量匹配,并且必须与合成事件字段的顺序相同。

所有 vals 都应转换为 u64,字符串 vals 只是指向字符串的指针,转换为 u64。字符串将使用这些指针复制到事件中为字符串预留的空间。

为了追踪一个合成事件,需要一个指向追踪事件文件的指针。trace_get_event_file() 函数可以用来获取它——它会在给定的追踪实例中找到该文件(在这种情况下为 NULL,因为使用的是顶层追踪数组),同时防止包含它的实例被销毁:

schedtest_event_file = trace_get_event_file(NULL, "synthetic",
                                            "schedtest");

在追踪事件之前,应该以某种方式启用它,否则合成事件实际上不会出现在追踪缓冲区中。

要从内核启用合成事件,可以使用 trace_array_set_clr_event()(它并非特定于合成事件,因此确实需要明确指定“synthetic”系统名称)。

要启用事件,向其传递“true”:

trace_array_set_clr_event(schedtest_event_file->tr,
                          "synthetic", "schedtest", true);

要禁用它,传递 false:

trace_array_set_clr_event(schedtest_event_file->tr,
                          "synthetic", "schedtest", false);

最后,可以使用 synth_event_trace_array() 实际追踪事件,追踪后事件应在追踪缓冲区中可见:

ret = synth_event_trace_array(schedtest_event_file, vals,
                              ARRAY_SIZE(vals));

要移除合成事件,应禁用事件,并使用 trace_put_event_file() 将追踪实例“放回”:

trace_array_set_clr_event(schedtest_event_file->tr,
                          "synthetic", "schedtest", false);
trace_put_event_file(schedtest_event_file);

如果这些都成功了,就可以调用 synth_event_delete() 来删除事件:

ret = synth_event_delete("schedtest");

7.2.2 分段追踪合成事件

要使用上述分段方法追踪合成事件,使用 synth_event_trace_start() 函数“打开”合成事件追踪:

struct synth_event_trace_state trace_state;

ret = synth_event_trace_start(schedtest_event_file, &trace_state);

它被传入代表合成事件的 trace_event_file(使用上述相同的方法),以及一个指向 struct synth_event_trace_state 对象的指针,该对象在使用前将归零,并用于维护此调用与后续调用之间的状态。

一旦事件被打开,这意味着在追踪缓冲区中为其保留了空间,就可以设置单个字段。有两种方法可以做到这一点:一种是逐个设置事件中的每个字段,无需查找;另一种是按名称设置,需要查找。两者之间的权衡是赋值的灵活性与每个字段查找的成本。

要一次性按顺序赋值而无需查找,应使用 synth_event_add_next_val()。每次调用都会传入 synth_event_trace_start() 中使用的相同 synth_event_trace_state 对象,以及要设置事件中下一个字段的值。设置完每个字段后,“游标”指向下一个字段,该字段将由后续调用设置,如此继续直到所有字段按顺序设置完毕。使用此方法时,与上述示例相同的调用序列将是(不含错误处理代码):

/* next_pid_field */
ret = synth_event_add_next_val(777, &trace_state);

/* next_comm_field */
ret = synth_event_add_next_val((u64)"slinky", &trace_state);

/* ts_ns */
ret = synth_event_add_next_val(1000000, &trace_state);

/* ts_ms */
ret = synth_event_add_next_val(1000, &trace_state);

/* cpu */
ret = synth_event_add_next_val(smp_processor_id(), &trace_state);

/* my_string_field */
ret = synth_event_add_next_val((u64)"thneed_2.01", &trace_state);

/* my_int_field */
ret = synth_event_add_next_val(395, &trace_state);

要以任意顺序赋值,应使用 synth_event_add_val()。每次调用都会传入 synth_event_trace_start() 中使用的相同 synth_event_trace_state 对象,以及要设置的字段的字段名称和要设置的值。使用此方法时,与上述示例相同的调用序列将是(不含错误处理代码):

ret = synth_event_add_val("next_pid_field", 777, &trace_state);
ret = synth_event_add_val("next_comm_field", (u64)"silly putty",
                          &trace_state);
ret = synth_event_add_val("ts_ns", 1000000, &trace_state);
ret = synth_event_add_val("ts_ms", 1000, &trace_state);
ret = synth_event_add_val("cpu", smp_processor_id(), &trace_state);
ret = synth_event_add_val("my_string_field", (u64)"thneed_9",
                          &trace_state);
ret = synth_event_add_val("my_int_field", 3999, &trace_state);

请注意,如果 synth_event_add_next_val()synth_event_add_val() 在同一个事件追踪中使用,则它们是不兼容的——两者只能使用其中之一,不能同时使用。

最后,事件在“关闭”之前不会实际被追踪,这通过使用 synth_event_trace_end() 完成,该函数只接受之前调用中使用的 struct synth_event_trace_state 对象:

ret = synth_event_trace_end(&trace_state);

请注意,无论任何添加调用是否失败(例如由于传入了错误的字段名称),synth_event_trace_end() 都必须在最后调用。

7.3 动态创建 kprobe 和 kretprobe 事件定义

要从内核代码创建 kprobe 或 kretprobe 追踪事件,可以使用 kprobe_event_gen_cmd_start()kretprobe_event_gen_cmd_start() 函数。

要创建 kprobe 事件,应首先使用 kprobe_event_gen_cmd_start() 创建一个空或部分空的 kprobe 事件。事件名称和探测位置应与一个或多个表示探测字段的参数一起提供给此函数。在调用 kprobe_event_gen_cmd_start() 之前,用户应使用 kprobe_event_cmd_init() 创建并初始化一个 dynevent_cmd 对象。

例如,创建一个包含两个字段的新“schedtest”kprobe 事件:

struct dynevent_cmd cmd;
char *buf;

/* Create a buffer to hold the generated command */
buf = kzalloc(MAX_DYNEVENT_CMD_LEN, GFP_KERNEL);

/* Before generating the command, initialize the cmd object */
kprobe_event_cmd_init(&cmd, buf, MAX_DYNEVENT_CMD_LEN);

/*
 * Define the gen_kprobe_test event with the first 2 kprobe
 * fields.
 */
ret = kprobe_event_gen_cmd_start(&cmd, "gen_kprobe_test", "do_sys_open",
                                 "dfd=%ax", "filename=%dx");

kprobe 事件对象创建后,可以填充更多字段。字段可以使用 kprobe_event_add_fields() 添加,提供 dynevent_cmd 对象以及可变参数列表的探测字段。例如,要添加几个附加字段,可以进行以下调用:

ret = kprobe_event_add_fields(&cmd, "flags=%cx", "mode=+4($stack)");

添加完所有字段后,应通过调用 kprobe_event_gen_cmd_end()kretprobe_event_gen_cmd_end() 函数来最终确定和注册事件,具体取决于启动的是 kprobe 还是 kretprobe 命令:

ret = kprobe_event_gen_cmd_end(&cmd);

或者

ret = kretprobe_event_gen_cmd_end(&cmd);

此时,事件对象已准备好用于追踪新事件。

类似地,可以使用 kretprobe_event_gen_cmd_start() 创建一个 kretprobe 事件,其中包含探测名称、位置以及诸如 $retval 等附加参数:

ret = kretprobe_event_gen_cmd_start(&cmd, "gen_kretprobe_test",
                                    "do_sys_open", "$retval");

类似于合成事件的情况,可以使用以下代码启用新创建的 kprobe 事件:

gen_kprobe_test = trace_get_event_file(NULL, "kprobes", "gen_kprobe_test");

ret = trace_array_set_clr_event(gen_kprobe_test->tr,
                                "kprobes", "gen_kprobe_test", true);

最后,同样类似于合成事件,可以使用以下代码释放 kprobe 事件文件并删除事件:

trace_put_event_file(gen_kprobe_test);

ret = kprobe_event_delete("gen_kprobe_test");

7.4 “dynevent_cmd”底层 API

内核内部的合成事件和 kprobe 接口都建立在更底层的“dynevent_cmd”接口之上。此接口旨在为合成事件和 kprobe 接口等更高级别接口提供基础,这些接口可以作为示例使用。

基本思想很简单,就是提供一个通用的层,可用于生成追踪事件命令。然后可以将生成的命令字符串传递给追踪事件子系统中已存在的命令解析和事件创建代码,以创建相应的追踪事件。

简而言之,其工作方式是:高级接口代码创建一个 struct dynevent_cmd 对象,然后使用 dynevent_arg_add()dynevent_arg_pair_add() 两个函数构建命令字符串,最后使用 dynevent_create() 函数执行该命令。接口的详细信息如下所述。

构建新命令字符串的第一步是创建并初始化一个 dynevent_cmd 实例。例如,这里我们在栈上创建一个 dynevent_cmd 并初始化它:

struct dynevent_cmd cmd;
char *buf;
int ret;

buf = kzalloc(MAX_DYNEVENT_CMD_LEN, GFP_KERNEL);

dynevent_cmd_init(cmd, buf, maxlen, DYNEVENT_TYPE_FOO,
                  foo_event_run_command);

dynevent_cmd 初始化需要提供用户指定的缓冲区和缓冲区长度(可以使用 MAX_DYNEVENT_CMD_LEN,但由于它通常过大(2k)而无法舒适地放在栈上,因此动态分配),一个 dynevent 类型 ID,旨在用于检查后续 API 调用是否针对正确的命令类型,以及一个指向事件特定的 run_command() 回调的指针,该回调将被调用以实际执行事件特定的命令函数。

完成之后,可以通过连续调用添加参数的函数来构建命令字符串。

要添加单个参数,请定义并初始化一个 struct dynevent_argstruct dynevent_arg_pair 对象。下面是可能最简单的参数添加示例,它只是将给定字符串作为以空格分隔的参数附加到命令中:

struct dynevent_arg arg;

dynevent_arg_init(&arg, NULL, 0);

arg.str = name;

ret = dynevent_arg_add(cmd, &arg);

arg 对象首先使用 dynevent_arg_init() 初始化,在这种情况下,参数为 NULL 或 0,这意味着没有可选的健全性检查函数或附加到参数末尾的分隔符。

这是一个更复杂的示例,使用“arg pair”,它用于创建一个由两个组件组成一个单元的参数,例如“type field_name;”参数或简单的表达式参数,例如“flags=%cx”:

struct dynevent_arg_pair arg_pair;

dynevent_arg_pair_init(&arg_pair, dynevent_foo_check_arg_fn, 0, ';');

arg_pair.lhs = type;
arg_pair.rhs = name;

ret = dynevent_arg_pair_add(cmd, &arg_pair);

同样,arg_pair 首先被初始化,在这种情况下,使用了一个回调函数来检查参数的健全性(例如,对的任何一部分都不是 NULL),以及一个用于在对之间添加运算符的字符(这里没有)和一个附加到 arg_pair 末尾的分隔符(这里是“;”)。

还有一个 dynevent_str_add() 函数,可以用来直接添加字符串,不带空格、分隔符或参数检查。

可以进行任意数量的 dynevent_*_add() 调用来构建字符串(直到其长度超过 cmd->maxlen)。当所有参数都已添加并且命令字符串完整时,唯一剩下要做的事情就是运行命令,这只需调用 dynevent_create() 即可实现:

ret = dynevent_create(&cmd);

此时,如果返回值为 0,则动态事件已创建并可以使用。

有关 API 的详细信息,请参阅 dynevent_cmd 函数定义本身。