3. bpftrace 基础语法:探针类型、动作块、内置变量与过滤条件
bpftrace 的脚本语法简洁而强大,其核心结构遵循 探针 : 动作块 的模式。理解探针类型、内置变量和过滤条件是编写高效分析脚本的基础。本节将系统性地拆解这些核心概念,并通过实例和图表帮助你快速掌握。
3.1 探针类型
探针是 bpftrace 的“传感器”,决定了脚本挂载到内核或用户空间的哪个位置。选择合适的探针类型是精确测量的第一步。以下是常用的探针类型及其适用场景。
| 探针类型 | 前缀 | 典型用途 | 开销 |
|---|---|---|---|
| tracepoint | tracepoint: |
跟踪系统调用、调度、网络等稳定内核事件 | 低 |
| kprobe/kretprobe | kprobe: / kretprobe: |
动态跟踪内核函数入口和返回 | 中 |
| uprobe/uretprobe | uprobe: / uretprobe: |
动态跟踪用户态函数(如 libc、应用代码) | 中 |
| software | software: |
跟踪软件事件(如 CPU 时钟、上下文切换) | 低 |
| hardware | hardware: |
跟踪硬件事件(如 CPU 周期、缓存未命中) | 低 |
| profile | profile: |
基于时间间隔的采样分析(如每秒 99 次) | 低 |
| interval | interval: |
定时输出报告(如每秒打印一次统计信息) | 极低 |
最佳实践: 对于系统调用延迟分析,
tracepoint:syscalls:sys_enter_* 和 tracepoint:syscalls:sys_exit_* 是首选。它们接口稳定,参数丰富,且开销相对较低。仅在 tracepoint 无法覆盖的场景下(如内核内部函数),才考虑使用 kprobe。
3.2 动作块与内置变量
动作块是脚本的“执行器”,它利用内置变量来捕获上下文信息。bpftrace 提供了丰富的内置变量,用于获取进程信息、时间戳、内核栈等关键数据。
核心内置变量速查表
| 变量名 | 类型 | 描述 | 示例值 |
|---|---|---|---|
pid |
整数 | 当前触发事件的进程 ID | 12345 |
tid |
整数 | 当前触发事件的线程 ID | 12345 |
comm |
字符串 | 当前触发事件的进程名称 | nginx |
uid |
整数 | 当前触发事件的用户 ID | 1000 |
nsecs |
整数 | 系统启动以来的纳秒时间戳 | 1234567890123456 |
kstack |
字符串 | 内核调用栈(用于定位内核代码路径) | sys_read+0x1a |
ustack |
字符串 | 用户态调用栈(用于定位应用代码路径) | read+0x10 |
args |
结构体 | 访问 tracepoint 或 kprobe 的参数(如 args->fd) |
— |
retval |
整数 | kretprobe 或 tracepoint 的返回值 | 0 或 -1 |
延迟测量核心公式: 在系统调用入口探针中记录
@start[tid] = nsecs;,在出口探针中计算 @latency_us = (nsecs - @start[tid]) / 1000;。这是后续所有延迟分析的基础。
3.3 基本过滤条件
过滤条件允许你从海量事件中精准定位目标,避免无关数据干扰分析。过滤条件写在两个斜杠 / / 之间,支持常见的比较和逻辑运算符。
常用过滤模式
- 按进程 ID 过滤:
/pid == 12345/— 只跟踪特定进程。 - 按进程名过滤:
/comm == "nginx"/— 只跟踪名为 "nginx" 的进程。 - 按用户 ID 过滤:
/uid == 0/— 只跟踪 root 用户的操作。 - 按参数过滤:
/args->fd == 3/— 只跟踪文件描述符为 3 的系统调用。 - 组合过滤:
/pid == 12345 && comm == "curl"/— 使用&&(与) 或||(或) 组合条件。
性能警告: 过滤条件虽然强大,但复杂的字符串匹配(如
comm == "long-process-name")会增加开销。对于高频事件(如 sys_enter_read),建议优先使用 pid 或 uid 等整数过滤,它们效率更高。
3.4 综合示例:测量特定进程的 read 系统调用延迟
以下脚本综合运用了 tracepoint 探针、内置变量和过滤条件,用于测量 PID 为 12345 的进程的 read 系统调用延迟。
#!/usr/bin/env bpftrace
BEGIN
{
printf("Tracing read() syscall latency for PID 12345... Hit Ctrl-C to end.\n");
}
tracepoint:syscalls:sys_enter_read
/pid == 12345/
{
@start[tid] = nsecs;
}
tracepoint:syscalls:sys_exit_read
/pid == 12345/
{
$delta_us = (nsecs - @start[tid]) / 1000;
@latency_us = hist($delta_us);
@total_count++;
delete(@start[tid]);
}
END
{
printf("\nRead syscall latency distribution (us):\n");
print(@latency_us);
printf("Total calls: %d\n", @total_count);
clear(@latency_us);
clear(@start);
}
脚本解读:
BEGIN和END是特殊探针,分别在脚本启动和退出时执行一次,用于初始化和清理。@start[tid]是一个关联数组(map),以线程 ID 为键存储开始时间,确保多线程环境下数据隔离。$delta_us是临时变量,使用$前缀声明,用于存储计算出的微秒级延迟。hist()函数自动生成对数直方图,是分析延迟分布的利器。delete(@start[tid])用于清理已处理完的线程记录,防止 map 无限增长。