事件追踪¶
- 作者:
Theodore Ts’o
- 更新于:
Li Zefan and Tom Zanussi
1. 简介¶
通过事件追踪基础设施,无需创建自定义内核模块即可使用跟踪点(Tracepoints)(参见 使用 Linux 内核跟踪点)来注册探测函数。
并非所有的跟踪点都可以使用事件追踪系统进行追踪;内核开发者必须提供代码片段,以定义如何将追踪信息保存到追踪缓冲区中,以及应如何打印追踪信息。
2. 使用事件追踪¶
2.1 通过 ‘set_event’ 接口¶
可用于追踪的事件可以在文件 /sys/kernel/tracing/available_events 中找到。
要启用特定事件(例如 ‘sched_wakeup’),只需将其回显(echo)到 /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 节)。
它还显示将用于在文本模式下打印事件的格式字符串,以及用于性能分析(profiling)的事件名称和 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 表达式语法¶
过滤表达式由一个或多个可以使用逻辑运算符 ‘&&’ 和 ‘||’ 组合的‘谓词(predicates)’组成。谓词简单来说就是一个子句,它将记录事件中包含的字段的值与常量进行比较,并根据字段值是否匹配(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"
因为内核必须知道如何从用户空间检索指针所在的内存。
您可以将任何长整型(long)类型转换为函数地址并按函数名搜索
call_site.function == security_prepare_creds
当字段 “call_site” 落在 “security_prepare_creds” 内的地址时,上述内容将进行过滤。也就是说,它将比较 “call_site” 的值,如果该值大于或等于函数 “security_prepare_creds” 的起始地址且小于该函数的结束地址,则过滤器将返回 true。
“.function” 后缀只能附加到 size 为 long 的值上,并且只能与 “==” 或 “!=” 进行比较。
可以(使用 cpulist 格式的用户提供的 cpumask)过滤编码了 CPU 编号的 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 过滤器限制¶
如果将过滤器置于不指向环形缓冲区(ring buffer)上的字符串、而是指向内核或用户空间内存的字符串指针 (char *) 上,那么出于安全原因,最多会将其内容的 1024 字节复制到临时缓冲区中来进行比较。如果内存复制发生故障(指针指向了不应访问的内存),则该字符串比较将被视为不匹配。
5.3 清除过滤器¶
要清除事件的过滤器,请向该事件的 filter 文件写入 ‘0’。
要清除子系统中所有事件的过滤器,请向该子系统的 filter 文件写入 ‘0’。
5.4 子系统过滤器¶
为方便起见,可以通过将过滤表达式写入子系统根目录下的 filter 文件中,来成组地设置或清除子系统中每个事件的过滤器。但请注意,如果子系统内的任何事件的过滤器缺少子系统过滤器中指定的字段,或者由于任何其他原因无法应用过滤器,则该事件的过滤器将保留其先前的设置。这可能会导致未预期的过滤器混合,从而导致令人困惑的追踪输出(对于可能认为不同过滤器正在生效的用户而言)。只有仅引用公共字段的过滤器才能保证成功传播到所有事件。
以下是一些同样说明上述要点的子系统过滤器示例
清除 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’ 文件中,可以将触发器添加到特定事件或从中移除。
给定事件可以关联任意数量的触发器,但须遵守各个命令在这方面可能具有的任何限制。
事件触发器是在“软”(soft)模式之上实现的,这意味着每当一个追踪事件关联了一个或多个触发器时,即使该事件未被实际启用,它也会被激活,但在“软”模式下被禁用。也就是说,将调用跟踪点,但只是不会被追踪,除非它确实被启用了。这种方案允许为未启用的事件调用触发器,也允许将当前的事件过滤器实现用于有条件地调用触发器。
事件触发器的语法大致基于 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] 部分不用于匹配命令,因此在 ‘!’ 命令中省略它与包含它将达到相同的效果。
过滤器的语法与上面‘事件过滤’一节中描述的相同。
为了易于使用,目前使用 ‘>’ 写入 trigger 文件只是添加或移除单个触发器,并且没有显式的 ‘>>’ 支持(‘>’ 实际上表现得像 ‘>>’)或用于移除所有触发器的截断支持(必须为添加的每一个触发器使用 ‘!’)。
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
每当发生触发事件时,此命令会在追踪缓冲区中转储堆栈追踪(stacktrace)。
例如,以下触发器每次命中 kmalloc 跟踪点时都会转储堆栈追踪
# echo 'stacktrace' > \ /sys/kernel/tracing/events/kmem/kmalloc/trigger以下触发器在大小 >= 64K 的 kmalloc 请求发生的前 5 次转储堆栈追踪
# 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
此命令导致在发生触发事件时触发快照(snapshot)。
以下命令在每次深度 > 1 的块请求队列被接通(unplug)时创建一个快照。如果您当时正在追踪一组事件或函数,那么当触发事件发生时,快照追踪缓冲区将捕获这些事件
# 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
此命令将事件命中聚合成一个哈希表,该哈希表以一个或多个追踪事件格式字段(或 stacktrace)为键,并包含一组派生自一个或多个追踪事件格式字段和/或事件计数(hitcount)的运行总计。
详细信息和示例请参见 事件直方图。
7. 内核内追踪事件 API¶
在大多数情况下,用于追踪事件的命令行接口已经足够了。然而,有时应用程序可能会发现需要比通过一系列简单的链式命令行表达式所能表达的更复杂的关系,或者将多组命令拼凑在一起可能过于繁琐。一个例子可能是需要“监听”追踪流以维护内核内状态机的应用程序,例如用于检测调度程序中何时发生非法的内核状态。
追踪事件子系统提供了一个内核内 API,允许模块或其他内核代码随意生成用户定义的“合成”(synthetic)事件,这些事件既可用于扩充现有的追踪流,也可用于发出某个重要状态已发生的信号。
还有一个类似内核内 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() “释放”(put)回追踪实例
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 对象,以及用于设置事件中下一个字段的值。在设置完每个字段后,“光标”(cursor)指向下一个字段,该字段将由后续调用设置,持续到按顺序设置完所有字段。使用此方法的调用序列与上述示例中相同(不含错误处理代码)
/* 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);
请注意,无论任何 add 调用是否失败(例如由于传入了错误的字段名),都必须在最后调用 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);
此时,事件对象已准备好用于追踪新事件。
类似地,可以使用带有探测名称和位置以及诸如 $retval 之类附加参数的 kretprobe_event_gen_cmd_start() 来创建 kretprobe 事件
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 时,它通常太大而无法舒适地放在栈上,因此通常是动态分配的)、用于检查后续 API 调用是否针对正确命令类型的 dynevent 类型 ID,以及指向事件特定 run_command() 回调函数的指针,该回调函数将被调用以实际执行事件特定的命令函数。
一旦完成,就可以通过连续调用添加参数的函数来构建命令字符串。
要添加单个参数,请定义并初始化一个 struct dynevent_arg 或 struct dynevent_arg_pair 对象。下面是最简单的参数添加示例,即只需将给定字符串作为以空白分隔的参数追加到命令中
struct dynevent_arg arg;
dynevent_arg_init(&arg, NULL, 0);
arg.str = name;
ret = dynevent_arg_add(cmd, &arg);
首先使用 dynevent_arg_init() 初始化 arg 对象,在这种情况下参数为 NULL 或 0,这意味着没有可选的健全性检查函数(sanity-checking function)或追加到参数末尾的分隔符。
这是另一个更复杂的例子,使用了‘参数对’(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),连同用于在该对之间添加运算符的字符(此处无)以及要追加到参数对末尾的分隔符(此处为 ‘;’)。
还有一个 dynevent_str_add() 函数,可用于简单地原样添加字符串,不带空格、分隔符或参数检查。
可以进行任意数量的 dynevent_*_add() 调用来构建字符串(直到其长度超过 cmd->maxlen)。当添加完所有参数并且命令字符串完成时,唯一要做的就是运行命令,这通过简单地调用 dynevent_create() 来实现
ret = dynevent_create(&cmd);
此时,如果返回值为 0,则动态事件已创建并准备好使用。
有关 API 的详细信息,请参见 dynevent_cmd 函数定义本身。