bpftrace 指南

文章目录

  • 0. bpftrace
    • 0.1 bpftrace组件
    • 0.2 bpftrace 帮助信息
    • 0.3 bpftrace 工具速览表
    • 0.4 bpftrace 探针
      • 0.4.1 tracepoint
      • 0.4.2 usdt
      • 0.4.3 kprobe和kretprobe
      • 0.4.4 uprobe和uretprobe
      • 0.4.5 software和hardware
      • 0.4.6 profile和interval
  • 1. 安装bpftrace
  • 2. bpftrace 工具
    • 2.1 bpftrace 编程
      • 2.1.1 bpftrace 用法
      • 2.1.2 程序结构
      • 2.1.3 探针格式
      • 2.1.4 变量
      • 2.1.5 控制流
        • 2.1.5.1 filter
        • 2.1.5.2 if 语句
        • 2.1.5.3 unroll 循环语句
      • 2.1.5 函数
    • 2.2 bpftrace 示例
      • 2.2.1 单行程序示例

0. bpftrace

Alastair 与2016.12创建了bpftrace。bpftrace 是一款基于BPF和BCC的开源跟踪器。其自带了许多多性能工具和支持文档,同时提供了一个高级编程语言环境,可以用来创建强大的单行程序和小工具。

0.1 bpftrace组件

在安装bpftrace的时候,其会存在以下的目录树。其中提供了工具的文档、man帮助文档、示例文件等等。

├── docs		# 参考手册 单行小程序指引
├── libbpf		# 库文件
├── src			# 前端
│   └── ast		#中间代码生成
├── man
│   ├── adoc
│   └── man8 	# 帮助文档
├── README 
├── INSTALL
└── tools		# 工具和实例

0.2 bpftrace 帮助信息

使用-h参数以获取帮助信息

[root@bogon bpftrace]# bpftrace -h
USAGE:
    bpftrace [options] filename
    bpftrace [options] - <stdin input>
    bpftrace [options] -e 'program'

OPTIONS:
    -B MODE        output buffering mode ('full', 'none')
    -f FORMAT      output format ('text', 'json')
    -o file        redirect bpftrace output to file
    -d             debug info dry run
    -dd            verbose debug info dry run
    -e 'program'   execute this program
    -h, --help     show this help message
    -I DIR         add the directory to the include search path
    --include FILE add an #include file before preprocessing
    -l [search]    list probes
    -p PID         enable USDT probes on PID
    -c 'CMD'       run CMD and enable USDT probes on resulting process
    --usdt-file-activation
                   activate usdt semaphores based on file path
    --unsafe       allow unsafe builtin functions
    -q             keep messages quiet
    -v             verbose messages
    --info         Print information about kernel BPF support
    -k             emit a warning when a bpf helper returns an error (except read functions)
    -kk            check all bpf helper functions
    -V, --version  bpftrace version
    --no-warnings  disable all warning messages

ENVIRONMENT:
    BPFTRACE_STRLEN             [default: 64] bytes on BPF stack per str()
    BPFTRACE_NO_CPP_DEMANGLE    [default: 0] disable C++ symbol demangling
    BPFTRACE_MAP_KEYS_MAX       [default: 4096] max keys in a map
    BPFTRACE_CAT_BYTES_MAX      [default: 10k] maximum bytes read by cat builtin
    BPFTRACE_MAX_PROBES         [default: 512] max number of probes
    BPFTRACE_LOG_SIZE           [default: 1000000] log size in bytes
    BPFTRACE_PERF_RB_PAGES      [default: 64] pages per CPU to allocate for ring buffer
    BPFTRACE_NO_USER_SYMBOLS    [default: 0] disable user symbol resolution
    BPFTRACE_CACHE_USER_SYMBOLS [default: auto] enable user symbol cache
    BPFTRACE_VMLINUX            [default: none] vmlinux path used for kernel symbol resolution
    BPFTRACE_BTF                [default: none] BTF file

EXAMPLES:
bpftrace -l '*sleep*'
    list probes containing "sleep"
bpftrace -e 'kprobe:do_nanosleep { printf("PID %d sleeping...\n", pid); }'
    trace processes calling sleep
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
    count syscalls by process name

0.3 bpftrace 工具速览表

应用场景 工具名称 使用场景及用法
CPU execsnoop.bt

0.4 bpftrace 探针

类型 缩写 描述
tracepoint t 内核静态插桩点
usdt U 用户静态插桩点
kprobe k 内核动态函数插桩点
kretprobe kr 内核动态函数返回值插桩点
uprobe u 用户动态函数插桩点
uretprobe ur 用户动态函数返回值插桩点
software s 内核软件事件
hardware h 硬件基于计数器的插桩
profile p 对全部cpu进行采样
interval i 一个cpu上周期性报告
BEGIN 启动
END 退出

0.4.1 tracepoint

tracepoint 探针类型会对内核跟踪点进行插桩,格式如下:

# tracepoint_name 是跟踪点全名,包括用来将跟踪点所在列和事件名字分割开的冒号
tracepoint:tracepoint_name

跟踪点通常是带有参数的:bpftrace 可以通过内置变量args来访问这些参数信息。假如说我们访问的跟踪点有一个代表数据包长度的参数,名称为len,我们可以用agrs->len来访问这个变量。而一个跟踪点具体具有哪些参数则是可以bpftrace的-lv参数来进行查看:

[root@bogon ik]# bpftrace -lv tracepoint:syscalls:sys_enter_read
tracepoint:syscalls:sys_enter_read
    int __syscall_nr
    unsigned int fd
    char * buf
    size_t count

我们可以看到这里跟man参数文档描述的其实是不一致的,这里会多一个系统调用号__syscall_nr

ssize_t read(int fd,void *buf,size_t count)

0.4.2 usdt

udst探针对用户态静态探针点进行插桩。格式如下:

usdt:binary_path:probe_name
usdt:libary_path:probe_name
usdt:binary_path:namespace:probe_name
usdt:libary_path:namespace:probe_name

比如说我们定义了如下的函数:

// 会编译成可执行文件test
namespace bpf_method{
void bpf_test();
}

那么对这个函数来说,其探针可以表述usdt:./test:bpf_method:bpf_test。另外,如果是在不确定的话,我们可以使用-l参数列出当前二进制文件之中所有可用的usdt探针。

# 这里存疑,笔者并没有实验成功
bpftrace -l 'usdt:/usr/local/cpython/python'

除此之外,我们还可以使用 -p pid的方式列出正在运行的程序其支持usdt类型探针:

bpftrace -lp 2266 | grep usdt

usdt:/proc/2266/root/usr/lib64/libc-2.28.so:libc:memory_sbrk_more
usdt:/proc/2266/root/usr/lib64/libc-2.28.so:libc:memory_tcache_double_free
usdt:/proc/2266/root/usr/lib64/libc-2.28.so:libc:memory_tunable_tcache_count
usdt:/proc/2266/root/usr/lib64/libc-2.28.so:libc:memory_tunable_tcache_max_bytes
usdt:/proc/2266/root/usr/lib64/libc-2.28.so:libc:memory_tunable_tcache_unsorted_limit
usdt:/proc/2266/root/usr/lib64/libc-2.28.so:libc:setjmp
[...]

0.4.3 kprobe和kretprobe

这种探针类型用于内核的动态插桩,格式如下:

kprobe:function_name
kretprobe:function_name

kprobe对函数的开始进行插桩,其参数arg0,arg1,arg2…是进入函数时的参数,类型均为64位无符号整数。如果其是指向C结构体的指针,可以进行强制转换; kretprobe对函数的结束进行插桩,其内置参数retval是函数的返回值,类型永远是64位无符号整型,如果和上述整型不一致,需要通过类型强制转换为对应的类型

0.4.4 uprobe和uretprobe

这种探针类型用于用户态的动态插桩,格式如下:

uprobe:binary_path:function_name
uprobe:libary_path:function_name
uretprobe:binary_path:function_name
uretprobe:libary_path:function_name

uprobe对函数的开始进行插桩,其参数arg0,arg1,arg2…是进入函数时的参数,类型均为64位无符号整数。如果其是指向C结构体的指针,可以进行强制转换; uretprobe对函数的结束进行插桩,其内置参数retval是函数的返回值,类型永远是64位无符号整型,如果和上述整型不一致,需要通过类型强制转换为对应的类型

0.4.5 software和hardware

这些探针的类型是预先定义好的软件事件和硬件事件,类型如下:

software:event_name
software:event_name:count
hardware:event_name
hardware:event_name:count

这些事件跟跟踪点是类似的,不过更适合于基于计数器的指标和基于采样的探测。由于事件的频闭可能很高,所以这里存在一个count字段,当每发生count此事件之后,才会触发一次探针。如果没有指定count,则会采用默认的频率。

0.4.6 profile和interval

这些是基于定时器的事件,格式如下:

profile:hz:rate
profile:s:rate
profile:ms:rate
profile:us:rate
interval:s:rate
interval:ms:rate

profile类型会在全部cpu之上进行激活,可以用作对CPU的使用进行采样;interval类型只在单个CPU上激活,可以用于周期性的打印输出。比如说profile:hz:100就表示100hz周期会激活一次对CPU使用的采样。

1. 安装bpftrace

  • centos
curl https://repos.baslab.org/bpftools.repo --output /etc/yum.repos.d/bpftools.repo

sudo yum install bpftrace bpftrace-tools
  • ubuntu
sudo apt-get update
sudo apt-get install bpftrace

2. bpftrace 工具

2.1 bpftrace 编程

下面是个bpftrace编程的例子,其展示了bpftrace 编程的基本结构:

# 指定解释器
#!/usr/local/bin/bpftrace

# 定义的探针1
kprobe:vfs_read
{
	# 将当前进程调用vfs_read 的时间放入一个名为start的map之中
	@start[tid] = nsecs;
}

# 定义的探针2
kretprobe:vfs_read
# 过滤start 这个map,获取tid这一项
/@start[tid]/
{
	# 获取执行时间
	$duration_us = (nsecs - @start[tid]) / 1000;
	# 执行时间放入直方图的bucket之中
	@us = hist($duration_us);
	# 删除map中这个元素
	delete(@start[tid]);
}

下面是上面这个文件的输出展示,vfs_exec_time.bt是存放上面代码的文件,bt后缀只是为了辨别:

[root@bogon bpftrace]# bpftrace vfs_exec_time.bt 
Attaching 2 probes...
^C
@start[6165]: 6928640480723
@us: 
[0]                  185 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[1]                   56 |@@@@@@@@@@@@@@@                                     |
[2, 4)                91 |@@@@@@@@@@@@@@@@@@@@@@@@@                           |
[4, 8)                44 |@@@@@@@@@@@@                                        |
[8, 16)                1 |                                                    |
[16, 32)               3 |                                                    |
[32, 64)               2 |                                                    |
[64, 128)              0 |                                                    |
[128, 256)             0 |                                                    |
[256, 512)             0 |                                                    |
[512, 1K)              0 |                                                    |
[1K, 2K)               1 |                                                    |

2.1.1 bpftrace 用法

bpftrace主要有单行程序和批处理两种用法:

# bpftrace 单行程序
bpftrace -e 'program'

-e 参数表示bpftrace会执行program,并开始跟踪其中定义的所有时间。程序会持续运行,直达Ctrl+C组合键被按下或者程序显示的调用exit()为止。

bpftrace file.bt

程序还可被保存到一个文件之中,然后使用bpftrace来执行。或者我们也可以使用chmod +x file.bt 来赋予文件可执行权限,这样就可以像其他任何程序一样运行。

2.1.2 程序结构

bpftrace程序的结构是一系列探针+对应的动作,当探针被激活后,相应的动作就会被执行。

probe { actions }

: 动作
动作既可以是单条语句,也可以是使用分号分割的多条语句{ action1,action2、action3... }

probe /filter_pattern/ { actions }

: 过滤器
filter_pattern是个布尔表达式,其会决定一个动作是否被执行,比如说pid==123表示只有进程123触碰探针才会执行动作。同时,在布尔表达式中也可以运用 &&||等布尔运算符

如果我们希望对多个探针实行同一动作处理,可以在探针之间加上,来分割,如下:

probe1,probe2,... { actions }

同时,探针也支持通配符,如下:

# 匹配probe1、probeabc等等...
probe* { actions }

2.1.3 探针格式

探针以类型名字开始,然后是一系列以:为分隔的标识符:

# ...表示可选
type:identifier1[:identifier2[...]]

一般来说,内核探针kprobe探针类型对内核态函数进行插桩,只需要一个标识符:内核函数名。uprobe探针类型对用户态函数进行插桩,需要两个标识符: 二进制文件的路径和函数名

2.1.4 变量

在bpftrace之中有三种类型的变量:

  • 内置变量:由bpftrace定义,通常是个只读的信息源,主要有以下几种:
变量 作用
pid 进程id
comm 进程名称
nsecs 时间戳(纳秒)
curtask 当前线程的 task_struct 结构体
  • 临时变量:可以用于临时的计算,以$作为前缀
# 定义变量x并赋值1
$x = 1
  • 映射表变量:使用BPF映射表来存储对象,名字带有@前缀,可以用于全局存储,在不同动作之间传递数据。
# 定义全局变量a 并赋值1
probe1 { @a = 1;}

# 定义array这个映射表,其为数组,给数组中对应的元素赋值
probe2 { @array[tid] = nsecs}

# 复合数组映射表
probe2 { @muti_array[pid,tid] = nsecs}

2.1.5 控制流

bpftrace支持三种控制流:filter、ternarg和if语句,这些都依靠布尔表达式来有条件的改变对程序执行的流向。

2.1.5.1 filter

probe /filter_pattern/ { actions }

: 过滤器
filter_pattern是个布尔表达式,其会决定一个动作是否被执行,比如说pid==123表示只有进程123触碰探针才会执行动作。同时,在布尔表达式中也可以运用 &&||等布尔运算符

2.1.5.2 if 语句

bpftrace的 if 语法如下:

if (test) { 
	actions
}

if (test) { 
	actions 
} else {
	other_actions
}

2.1.5.3 unroll 循环语句

由于bpftrace运行在受限的环境中,所以这里使用了unroll语句来保证循环时有限的。

2.1.5 函数

bpftrace 的内置函数主要如下:

函数 类型 作用
exit() 普通函数 退出bpftrace
str(char*) 普通函数 输出一个指针,返回字符串
system(format[,arguments …]) 普通函数 在shell之中运行命令
count() 映射表函数 累计统计
sum($var) 映射表函数 求和
hist($var) 映射表函数 直方图展示
delete($var) 映射表函数 删除映射表元素

2.2 bpftrace 示例

2.2.1 单行程序示例

  • hello world:
bpftrace -e 'BEGIN { printf("hello world!\n"); }'
[root@bogon bpftrace]# bpftrace -e 'BEGIN { printf("hello world!\n"); }'
Attaching 1 probe...
hello world!
  • 展示系统谁在执行什么命令:
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(args->filename)); }'
# 在这里,笔者打开了另外一个中断,执行了ls 命令,也就是这里最后一行。
[root@bogon bpftrace]# bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(args->filename)); }'
Attaching 1 probe...
ksmtuned -> /usr/bin/awk
ksmtuned -> /usr/bin/pgrep
ksmtuned -> /usr/bin/awk
ksmtuned -> /usr/bin/sleep
bash -> /usr/bin/ls

: 什么是ksmtuned?
KSM 是一种节省内存的重复数据删除功能,由 CONFIG_KSM=y 启用,在 2.6.32 中添加到 Linux 内核中。
KSM 最初是为与 KVM(在那里称为内核共享内存)一起使用而开发的,通过共享它们之间的公共数据,将更多虚拟机放入物理内存中。但它对于生成相同数据的许多实例的任何应用程序都非常有用。

  • 展示新进程的创建:
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { join(args->argv); }' 
# 下面代码中的第一行展示了笔者在另外的终端运行ls的过程,其余代码则是KSM机制所带来的
[root@bogon bpftrace]# bpftrace -e 'tracepoint:syscalls:sys_enter_execve { join(args->argv); }'
Attaching 1 probe...
ls --color=auto
awk /^(MemFree|Buffers|Cached):/ {free += $2}; END {print free} /proc/meminfo
pgrep -d   -- ^qemu(-(kvm|system-.+)|:.{1,11})$
awk { sum += $1 }; END { print 0+sum }
sleep 60
  • 按照进程统计文件的开启情况:
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
[root@bogon bpftrace]# bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
Attaching 1 probe...
in:imjournal /run/log/journal/29545612d2ce4e319e6a9edaa0cff1d3/system.journa
systemd-journal /
systemd-journal sys
systemd-journal bus

: 什么是systemd-journal?
systemd-journald 是一个收集并存储各类日志数据的系统服务。 它创建并维护一个带有索引的、结构化的日志数据库, 并可以收集来自各种不同渠道的日志:

  1. 通过 kmsg 收集内核日志
  2. 通过 libc 的 syslog(3) 接口收集系统日志
  3. 通过 本地日志接口 sd_journal_print(3) 收集结构化的系统日志
  4. 捕获服务单元的标准输出(STDOUT)与标准错误(STDERR)
  5. 通过内核审计子系统收集审计记录
  • 按照程序统计系统调用次数:
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
[root@bogon bpftrace]# bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
Attaching 1 probe...
^C
@[NetworkManager]: 19
@[tuned]: 24
@[sshd]: 24
@[irqbalance]: 40
@[bpftrace]: 67
@[gdbus]: 93
@[in:imjournal]: 673
@[systemd-journal]: 7752
...
  • 按照探针名字统计系统调用次数:
bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'
# 下面的代码会统计运行期间符合‘tracepoint:syscalls:sys_enter_*’ 正则匹配的所有探针的调用次数
[root@bogon bpftrace]# bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[probe] = count(); }'
Attaching 341 probes...
^C
@[tracepoint:syscalls:sys_enter_waitid]: 1
@[tracepoint:syscalls:sys_enter_getpid]: 1
@[tracepoint:syscalls:sys_enter_timerfd_create]: 1
@[tracepoint:syscalls:sys_enter_socket]: 1
@[tracepoint:syscalls:sys_enter_rt_sigreturn]: 1
@[tracepoint:syscalls:sys_enter_munmap]: 2
@[tracepoint:syscalls:sys_enter_mmap]: 2
@[tracepoint:syscalls:sys_enter_sendmsg]: 5
@[tracepoint:syscalls:sys_enter_writev]: 5
@[tracepoint:syscalls:sys_enter_ftruncate]: 5
@[tracepoint:syscalls:sys_enter_inotify_add_watch]: 5
...
  • 按照进程统计系统调用数量:
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[pid, comm] = count(); }'
[root@bogon bpftrace]# bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[pid, comm] = count(); }'
Attaching 1 probe...
^C
@[3126, gpg-agent]: 1
@[3171, gpg-agent]: 1
@[3139, gpg-agent]: 1
@[985, sssd]: 1
@[1084, tuned]: 2
@[2848, gmain]: 3
@[2751, gmain]: 4
@[698, systemd-journal]: 2178
...
  • 按照进程展示运行期间内总的读取字节数:
bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret/ { @[comm] = sum(args->ret); }'
[root@bogon bpftrace]# bpftrace -e 'tracepoint:syscalls:sys_exit_read /args->ret/ { @[comm] = sum(args->ret); }'
Attaching 1 probe...
^C
@[sshd]: 54
@[in:imjournal]: 63
@[gmain]: 188
@[sedispatch]: 756
@[systemd-journal]: 18056
  • 按照进程展示read 返回结果大小的分布:
bpftrace -e 'tracepoint:syscalls:sys_exit_read { @[comm] = hist(args->ret); }'
[root@bogon ik]# bpftrace -e 'tracepoint:syscalls:sys_exit_read { @[comm] = hist(args->ret); }'
Attaching 1 probe...
^C
@[sshd]: 
[2, 4)                 1 |@@@@@@@@@@@@@@@@@@@@@@@@@@                          |
[4, 8)                 0 |                                                    |
[8, 16)                0 |                                                    |
[16, 32)               1 |@@@@@@@@@@@@@@@@@@@@@@@@@@                          |
[32, 64)               2 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
...
  • 展示进程的磁盘IO尺寸:
bpftrace -e 'tracepoint:block:block_rq_issue { printf("%d %s %d\n", pid, comm, args->bytes); }'
[root@bogon ik]# bpftrace -e 'tracepoint:block:block_rq_issue { printf("%d %s %d\n", pid, comm, args->bytes); }'
Attaching 1 probe...
# pid name bytes
251 kworker/2:1H 4096
284 kworker/4:1H 0
251 kworker/2:1H 0
288 kworker/7:1H 14848
288 kworker/7:1H 0
3684 kworker/2:0 8
3684 kworker/2:0 8
  • 按照进程展示页换入数量:
bpftrace -e 'software:major-faults:1 { @[comm] = count(); }'
[root@bogon ik]# bpftrace -e 'software:major-faults:1 { @[comm] = count(); }'
Attaching 1 probe...
^C
@[sssd_nss]: 2
  • 按照进程展示缺页中断数量:
bpftrace -e 'software:faults:1 { @[comm] = count(); }'
[root@bogon ik]# bpftrace -e 'software:faults:1 { @[comm] = count(); }'
Attaching 1 probe...
^C
@[systemd-journal]: 5
@[in:imjournal]: 5
  • 对pid为xxx的进程以yyhz抓取其用户态的调用栈信息:
bpftrace -e 'profile:hz:yy /pid == xxx/ { @[ustack] = count(); }'

你可能感兴趣的:(BPF,linux,开发语言,bpf,bcc,bpftrace)