事件追踪¶
- 作者:
Theodore Ts’o
- 更新:
李泽帆和 Tom Zanussi
1. 简介¶
可以使用跟踪点(请参阅 使用 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
注意
必须使用 '>>',否则它将首先禁用所有事件。
要禁用事件,请将事件名称 echo 到以感叹号为前缀的 set_event 文件
# echo '!sched_wakeup' >> /sys/kernel/tracing/set_event
要禁用所有事件,请将空行 echo 到 set_event 文件
# echo > /sys/kernel/tracing/set_event
要启用所有事件,请将 *:*
或 *:
echo 到 set_event 文件
# echo *:* > /sys/kernel/tracing/set_event
事件被组织成子系统,例如 ext4、irq、sched 等,完整的事件名称如下所示:<子系统>:<事件>。子系统名称是可选的,但它会显示在 available_events 文件中。可以使用 <子系统>:*
语法指定子系统中的所有事件;例如,要启用所有 irq 事件,您可以使用以下命令
# echo 'irq:*' > /sys/kernel/tracing/set_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 个是 common 字段,其余 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"
因为内核必须知道如何从用户空间检索指针所在的内存。
您可以将任何 long 类型转换为函数地址并按函数名称搜索
call_site.function == security_prepare_creds
当字段 “call_site” 位于 “security_prepare_creds” 中的地址上时,以上内容将进行过滤。也就是说,它将比较 “call_site” 的值,如果该值大于或等于函数 “security_prepare_creds” 的开头,并且小于该函数的结尾,则过滤器将返回 true。
“.function” 后缀只能附加到大小为 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 过滤器限制¶
如果将过滤器放置在不指向环形缓冲区上的字符串,而是指向内核或用户空间内存的字符串指针 (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
这些命令可以在每次命中触发事件时启用或禁用另一个跟踪事件。注册这些命令时,另一个跟踪事件将被激活,但在“软”模式下被禁用。也就是说,将调用跟踪点,但不会被跟踪。只要存在可以触发它的触发器,事件跟踪点就会保持在这种模式下。
例如,以下触发器会导致在输入读取系统调用时跟踪 kmalloc 事件,并且末尾的 :1 指定此启用仅发生一次
# echo 'enable_event:kmem:kmalloc:1' > \ /sys/kernel/tracing/events/syscalls/sys_enter_read/trigger
以下触发器会导致在读取系统调用退出时停止跟踪 kmalloc 事件。此禁用在每次读取系统调用退出时发生
# 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
以下触发器在前 5 次发生大小 >= 64K 的 kmalloc 请求时转储堆栈跟踪
# 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
请注意,每个触发事件只能有一个堆栈跟踪触发器。
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],则该字段被视为数组。
也可以使用 synth_field_desc 数组和 add_synth_fields() 一次添加一组字段。例如,这将只添加前四个 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 args,每个合成事件字段对应一个,以及传递的值的数量。
因此,要跟踪与上述合成事件定义对应的事件,可以使用如下代码
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 */
所有值都应强制转换为 u64,字符串值只是指向字符串的指针,强制转换为 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 数组,其数量必须与合成事件中的字段数匹配,并且必须与合成事件字段的顺序相同。
所有值都应强制转换为 u64,字符串值只是指向字符串的指针,强制转换为 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,该 ID 旨在用于检查进一步的 API 调用是否用于正确的命令类型,以及指向特定于事件的 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);
arg 对象首先使用 dynevent_arg_init() 初始化,在这种情况下,参数为 NULL 或 0,这意味着没有可选的健全性检查函数或追加到 arg 末尾的分隔符。
这是另一个更复杂的示例,使用了“arg 对”,用于创建由几个组件组合在一起作为单元的参数,例如,“type field_name;” arg 或简单的表达式 arg,例如“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 函数定义本身。