bpftrace简介

概述

bpftrace是一个基于eBPF的高级跟踪工具, 用于动态追踪Linux系统的行为和性能.

用于可以用简单的一行命令或者简洁的脚本,实现监控分析内核和用户空间程序的运行,而无需修改或者重新编译

安装

sudo apt-get install bpftrace

基本语法

bpftrace 脚本由 probe 和 action 组成:

probe filter { action }
  • probe:定义事件触发点(例如内核函数、系统调用、定时器等)。
  • action:当事件触发时执行的代码块(例如打印信息、统计计数等)。
  • filter: 定义事件的过滤器(例如map中key存在,变量值比较)

打印信息

使用 printf 打印信息:

用法与c语言的printf一致

tracepoint:syscalls:sys_enter_open { printf("File opened by %s (PID: %d)\n", comm, pid);}

统计计数

使用 @ 定义映射(map)来统计事件:

tracepoint:syscalls:sys_enter_open { @[comm] = count(); }

过滤条件

使用 /条件/ 过滤事件:

tracepoint:syscalls:sys_enter_open /pid == 1234/ { printf("File opened by PID 1234\n"); }

调用栈

打印内核或用户空间调用栈:

kprobe:do_sys_open { printf("Kernel stack: %s\n", kstack); }
uprobe:/bin/bash:readline { printf("User stack: %s\n", ustack); }

# sudo bpftrace -e 'kprobe:kernel_clone { printf("Kernel stack: %s\n", kstack); }’
Attaching 1 probe...
Kernel stack:
kernel_clone+1
__do_sys_clone+96
do_syscall_64+51
entry_SYSCALL_64_after_hwframe+68

如果调用栈深度不足,可以尝试增加调用栈深度。bpftrace 默认的调用栈深度可能不足以捕获完整的调用栈。

条件语句

bpftrace 支持条件语句(if/else):

if (条件) {
// 条件为真时执行的代码
} else {
// 条件为假时执行的代码
}

示例

tracepoint:syscalls:sys_enter_open {
if (pid == 1234) {
printf("File opened by PID 1234: %s\n", str(args->filename));
} else {
printf("File opened by another process: %s\n", str(args->filename));
}
}

映射(Map)

bpftrace 使用 @ 定义映射(map),用于存储和统计数据。

  • 统计事件次数
tracepoint:syscalls:sys_enter_open { @[comm] = count(); }
  • 统计唯一值
tracepoint:syscalls:sys_enter_open { @files[str(args->filename)] = 1; }
  • 打印映射

在脚本结束时打印映射:

END { print(@); }

传递参数

$1 $2 $3之类的都是内置变量,可以直接引用,用法如以下示例

sudo bpftrace -e 'tracepoint:tlb:tlb_flush { printf("TLB flushed by %s (pid: %d)\n", comm, $1); }' 1

这条命令是过滤所有的pid=1的TLB刷新事件

内置变量和函数

进程相关的变量

  • pid: 当前进程的进程 ID(PID)。
  • tid: 当前线程的线程 ID(TID)。
  • uid: 当前进程的用户 ID(UID)。
  • gid: 当前进程的组 ID(GID)。
  • comm: 当前进程的命令名称(进程名,最多 16 个字符)。
  • nsecs: 当前时间戳(纳秒级别,从系统启动开始计算)。
  • cgroup: 当前进程的 cgroup ID(需要内核支持)。
  • args: 用于访问 tracepoint 或 kprobe 的参数(具体参数取决于 tracepoint 或 probe)。

CPU 相关的变量

  • cpu: 当前 CPU 的编号。
  • curtask: 当前 CPU 上正在运行的任务的 task_struct 指针。
  • kstack: 当前的内核调用栈。
  • ustack: 当前的用户空间调用栈。

时间和计时相关的变量

  • nsecs: 当前时间戳(纳秒级别,从系统启动开始计算)。
  • elapsed: 从 bpftrace 脚本启动开始经过的时间(纳秒级别)。

上下文相关的变量

  • retval: 函数的返回值(通常用于 kretprobe)。
  • func: 当前函数的名称(用于 kprobe 或 uprobe)。
  • probe: 当前 probe 的名称。
  • arg0arg1arg2, …: 函数或 tracepoint 的参数(具体含义取决于 probe 类型)。

其他常用变量

  • bpftrace: 当前 bpftrace 脚本的 PID。
  • $1$2$3, …: 脚本的命令行参数(例如 $1 表示第一个参数)。
  • @: 用于定义和访问映射(map)的变量。

特殊变量

  • kstack: 内核调用栈。
  • ustack: 用户空间调用栈。
  • args: 用于访问 tracepoint 的参数(例如 args->filename)。

map函数

函数 功能 适用场景
hist() 生成 2 的幂次方直方图 分析数据分布(非线性区间)
lhist() 生成线性直方图 分析数据分布(自定义区间)
count() 统计事件次数 统计事件发生的总次数
sum() 累加数值 计算总和
avg() 计算平均值 分析数据的平均水平
min() 返回最小值 查找数据的最小值
max() 返回最大值 查找数据的最大值
  • hist()

    • 功能: 生成一个直方图,用于统计数据的分布情况。
    • 参数: 一个数值(通常是整数或指针)。
    • 输出: 将数据分布划分为 2 的幂次方区间(如 0-1, 2-3, 4-7, 8-15, …),并统计每个区间的计数。
  • lhist()

    • 功能: 生成一个线性直方图,用于统计数据的分布情况。
    • 参数:
      • 第一个参数是数值。
      • 第二个参数是区间的最小值。
      • 第三个参数是区间的最大值。
      • 第四个参数是区间的步长。
    • 输出: 将数据分布划分为线性区间,并统计每个区间的计数。
    # 统计 pid 在 0 到 1000 之间的分布,步长为 100。
    bpftrace -e 'profile:hz:99 { @ = lhist(pid, 0, 1000, 100); }'
  • count()

    • 功能: 统计事件发生的次数。

    • 参数: 无(直接调用)。

    • 输出: 统计事件的总次数。

    • 示例:

      bpftrace -e 'tracepoint:syscalls:sys_enter_open { @ = count(); }'

      这会统计 open 系统调用的总次数。

  • sum()

    • 功能: 对某个数值进行累加。

    • 参数: 一个数值。

    • 输出: 返回所有事件的累加值。

    • 示例:

      bpftrace -e 'tracepoint:syscalls:sys_enter_read { @ = sum(args->count); }'

      这会统计所有 read 系统调用中 count 参数的总和。

  • avg()

    • 功能: 计算某个数值的平均值。

    • 参数: 一个数值。

    • 输出: 返回所有事件的平均值。

    • 示例:

      bpftrace -e 'tracepoint:syscalls:sys_enter_read { @ = avg(args->count); }'

      这会计算所有 read 系统调用中 count 参数的平均值。

  • min()

    • 功能: 返回某个数值的最小值。

    • 参数: 一个数值。

    • 输出: 返回所有事件中的最小值。

    • 示例:

      bpftrace -e 'tracepoint:syscalls:sys_enter_read { @ = min(args->count); }'

      这会返回所有 read 系统调用中 count 参数的最小值。

  • max()

    • 功能: 返回某个数值的最大值。

    • 参数: 一个数值。

    • 输出: 返回所有事件中的最大值。

    • 示例:

      bpftrace -e 'tracepoint:syscalls:sys_enter_read { @ = max(args->count); }'

      这会返回所有 read 系统调用中 count 参数的最大值。

probe类型

bpftrace 支持多种 probe 类型,用于捕获不同的事件。常见的 probe 类型包括:

内核动态跟踪

  • **kprobe**:跟踪内核函数的入口。

    kprobe:do_sys_open { printf("File opened by %s\n", comm); }
  • **kretprobe**:跟踪内核函数的返回值。

    kretprobe:do_sys_open { printf("File opened, return value: %d\n", retval); }

静态跟踪点

  • **tracepoint**:跟踪内核静态跟踪点。

    tracepoint:syscalls:sys_enter_open { printf("File: %s\n", str(args->filename)); }

用户空间动态跟踪

  • **uprobe**:跟踪用户空间函数的入口。

    uprobe:/bin/bash:readline { printf("Bash readline: %s\n", str(arg0)); }
  • **uretprobe**:跟踪用户空间函数的返回值。

    uretprobe:/bin/bash:readline { printf("Bash readline returned: %s\n", str(retval)); }

定时器

  • **interval**:按固定时间间隔触发。

    interval:s:1 { printf("1 second passed\n"); }

其他

  • **BEGIN**:脚本开始时触发。

    BEGIN { printf("bpftrace started\n"); }
  • **END**:脚本结束时触发。

    END { printf("bpftrace ended\n"); }

编写脚本

shell脚本中嵌入bt脚本

方便动态传递探针名字之类的复杂场景

#!/bin/bash

# 检查是否提供了路径
if [ -z "$1" ]; then
echo "Usage: $0 <binary-path>"
exit 1
fi

BINARY_PATH=$1

# 生成 bpftrace 脚本
cat <<EOF > /tmp/trace.bpf

uprobe:$BINARY_PATH:"server/gamesvr/handler.Handler"
{
printf("Handler called\n");
}

uretprobe:$BINARY_PATH:"server/gamesvr/handler.Handler"
{
printf("gHandler returned\n");
}
EOF

# 运行 bpftrace 脚本
sudo bpftrace /tmp/trace.bpf
sudo bash demo.sh /xxx/xxx/bin/xxx

编写bt文件执行

写成文件有时候可读性要比单行命令更好一些

#!/usr/bin/env bpftrace
uprobe:/xxx/xxx/bin/xxx:"server/gamesvr/handler.Handler"
{
printf("Handler called\n");
}

uretprobe:/xxx/xxx/bin/xxx:"server/gamesvr/handler.Handler"
{
printf("gHandler returned\n");
}
chmod +x ./demo.bt
./demo.bt

注意:后边的符号尽量加引号,防止解析失败的问题; 这个常用于go的符号Cannot find go symbols · Issue #1098 · bpftrace/bpftrace (github.com)

用例

Counts

利用map对象, 输出按指定key的统计计数

bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
Attaching 1 probe...
^C

@[bpftrace]: 6
@[systemd]: 24
@[snmp-pass]: 96
@[sshd]: 125

Distribution

输出指定字段的值的分布

# bpftrace -e 'tracepoint:syscalls:sys_exit_read /pid == 18644/ { @bytes = hist(args.ret); }'
Attaching 1 probe...
^C

@bytes:
[0, 1] 12 |@@@@@@@@@@@@@@@@@@@@ |
[2, 4) 18 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[4, 8) 0 | |
[8, 16) 0 | |
[16, 32) 0 | |
[32, 64) 30 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[64, 128) 19 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[128, 256) 1 |@
  • @: 这个和map是相似的, 但是没有 ([]) , 他的作用是设置输出图标的字段为 “bytes”
  • hist(): 这是一个map函数,用于总结数据为一个2的幂次的直方图
  • 其他类似的map函数有 lhist() (linear hist), count(), sum(), avg(), min(), and max().

在内核栈上执行Profile On-CPU

# bpftrace -e 'profile:hz:99 { @[kstack] = count(); }'
Attaching 1 probe...
^C

[...]
@[
filemap_map_pages+181
__handle_mm_fault+2905
handle_mm_fault+250
__do_page_fault+599
async_page_fault+69
]: 12
[...]
@[
cpuidle_enter_state+164
do_idle+390
cpu_startup_entry+111
start_secondary+423
secondary_startup_64+165
]: 22122
  • 99是采样频率,这个数字既不会干扰性能,又不会因为是整数而跟定时任务同步了

记录Off-CPU 时间分布

#!/usr/bin/env bpftrace

BEGIN
{
printf("Tracing off-CPU time... Hit Ctrl+C to end.\n");
}

tracepoint:sched:sched_switch
{
@start[tid] = nsecs;
}

tracepoint:sched:sched_switch
/ @start[args->prev_pid] /
{
$duration = nsecs - @start[args->prev_pid];
@offcpu[args->prev_comm] = sum($duration);
delete(@start[args->prev_pid]);
}

END
{
printf("\nOff-CPU time by process:\n");
print(@offcpu);
}

使用kprobe追踪

# cat path.bt
#ifndef BPFTRACE_HAVE_BTF
#include <linux/path.h>
#include <linux/dcache.h>
#endif

kprobe:vfs_open
{
printf("open path: %s\n", str(((struct path *)arg0)->dentry->d_name.name));
}

# bpftrace path.bt
Attaching 1 probe...
open path: dev
open path: if_inet6
open path: retrans_time_ms
[...]

用户空间动态追踪

用户空间程序可以使用uretprobe和uprobe进行追踪, 支持多种编程语言

注意事项

  • 对go程序使用uretprobe可能会导致崩溃,因为栈可能发生移动(参考文档种如此解释:It is important to note that for uretprobe s to work the kernel runs a special helper on user-space function entry which overrides the return address on the stack. This can cause issues with languages that have their own runtime like Golang:

参考资料

bpftrace/man/adoc/bpftrace.adoc at master · bpftrace/bpftrace (github.com)

The bpftrace One-Liner Tutorial | bpftrace

作者

deepwzh

发布于

2025-01-15

更新于

2025-01-16

许可协议

评论