LinuxBPF学习笔记 - bpftrace开发[7]

Posted 宣之于口

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LinuxBPF学习笔记 - bpftrace开发[7]相关的知识,希望对你有一定的参考价值。

bpftrace是基于BPF和BCC构建的开源跟踪程序。 与BCC一样,bpftrace附带了许多性能工具和支持文档。 但是,它也提供了高级编程语言,允许创建功能强大的单行代码和简短的工具。 bpftrace是使用自定义单行代码和简短脚本的临时工具的理想选择,而BCC是复杂工具和守护程序的理想选择

BPFTRACE 组件

bpftrace包含有关工具,手册页和示例文件的文档,以及bpftrace编程教程(单行教程)和编程语言参考指南。 bpftrace工具的扩展名为.bt. 前端使用lex和yacc解析bpftrace编程语言,并使用Clang解析结构。 后端将bpftrace程序编译为LLVM中间表示,然后由LLVM库编译为BPF

一、运行

将执行程序,检测其定义的所有事件。 该程序将一直运行到Ctrl-C或显式调用exit()为止

1. one-liner

-e参数运行的bpftrace程序称为one-liner

$ bpftrace -e program

2. 文件

可以将程序保存到文件并使用以下命令执行。.bt扩展名不是必需的,但有助于以后识别

$ bpftrace file.bt

也将文件设置为可执行文件(chmod a + x file.d)并像运行其他程序一样运行. 注意 bpftrace必须由root用户执行

$ sudo ./file.bt

二、程序结构

在文件顶部放置解释行

#!/usr/local/bin/bpftrace

bpftrace程序是一系列具有相关操作的探针(probes),当probes将执行相关的操作, 之前可以包含一个可选的过滤器表达式. 只有在过滤器表达式为true时,才会触发该操作

probes  actions 
// 可选的过滤器表达式
probes /filter/  actions 
...

1. Probe

探针以探针类型名称开头,然后以冒号分隔的层次结构

type:identifier1[:identifier2[...]]

层次结构由探针类型定义,例如:

  • kprobe探针类型检测内核函数调用,只需要一个标识符:内核函数名
  • uprobe探针类型用于检测用户级别的函数调用,并且需要binary路径和函数名称
kprobe:vfs_read
uprobe:/bin/bash:readline

可以使用逗号分隔符指定多个探针以执行相同的操作. 有两种不需要额外标识符的特殊探针类型:bpftrace程序的开头和结尾均使用BEGIN和END触发

probe1,probe2,...  actions 

a. Probe 通配符

**示例:**将检测所有以vfs_开头的kprobes(内核函数)

kprobe:vfs_*

检测过多的探针可能会导致不必要的性能开销。 为了避免意外发生,bpftrace具有将启用的可调的最大探针数,可以通过BPFTRACE_MAX_PROBES环境变量(当前默认为5125)进行设置

您可以在使用bpftrace -l之前测试通配符

$ bpftrace -l 'kprobe:vfs_*'
kprobe:vfs_fallocate
kprobe:vfs_truncate
[...]
$ bpftrace -l 'kprobe:vfs_*' | wc -l
56

2. Filters

过滤器是布尔表达式,用于决定是否执行某项操作,例如:

# 仅当pid等于123时,才执行操作
/pid == 123/
# 等同于 pid != 0
/pid/
# 可以与布尔运算符(例如 &&)结合使用
/pid > 100 && pid < 1000/

3. Action

Action可以是单个语句,也可以是多个由分号分隔的语句

 action one; action two; action three 

语句使用类似于C语言,并且可以操纵变量并执行bpftrace函数调用. 将变量$x设置为42,然后使用printf()打印该变量

 $x = 42; printf("$x is %d", $x); 

到这里, 我们可以理解以下代码

// 单行, 打印 Hello World!
$ bpftrace -e 'BEGIN  printf("Hello World!\\n") '
// 写作文件形式
#!/usr/local/bin/bpftrace
BEGIN 
    	printf("Hello World!\\n");

三、变量

共有三种变量类型:built-ins, scratch, 和 maps

1. Built-in

内置变量是bpftrace预先定义和提供的,通常是只读信息源。 其中包括进程号的pid,进程名的comm,时间戳 (以纳秒为单位) 和curtask (当前线程的 task_struct 地址)

变量含义变量含义
pid进程IDretval返回值
tid线程IDfunctrace函数名
uid用户IDprobeprobe的全名
nsecs纳秒级时间戳kstack多行字符串的形式返回内核级堆栈跟踪
cpu处理器IDustack多行字符串的形式返回用户级堆栈跟踪
commprocess nameargs参数

a. 位置参数

通过命令行传递给程序的,并且基于Shell脚本中使用的位置参数, $1表示第一个参数,$2表示第二个参数,以此类推

格式

bpftrace ./watchconn.bt 181
bpftrace -e 'program' 181

**示例1: **监视在命令行上传递的PID ./watchconn.bt 181

BEGIN

       printf("Watching connect() calls by PID %d\\n", $1);


tracepoint:syscalls:sys_enter_connect
/pid == $1/

       printf("PID %d called connect()\\n", $1);

示例2: 默认情况下,它们是整数。 如果将字符串用作参数,则必须通过str()调用对其进行访问

$ bpftrace -e 'BEGIN  printf("Hello, %s!\\n", str($1)); ' Reader

2. Scratch

临时变量可用于临时计算,并具有$前缀。 他们的名称和类型是在他们第一次分配时设定的. 这些只能在分配了它们的操作块中使用。 如果在没有赋值的情况下引用变量,则bpftrace将出错

# 将$x声明为整数
$x = 1;
# 将$y声明为字符串
$y = "hello";
# 将$z声明为指向结构task_struct的指针
$z = (struct task_struct *)curtask;

3. Map

映射变量使用BPF map 存储对象,并具有@前缀。 它们可用于全局存储,在操作之间传递数据。

格式:

@name
@name[key]
@name[key1, key2[, ...]]

**示例1:**当probe1触发时,将1分配给@a,然后当probe2触发时,将@a分配给$x。 如果先触发probe1,然后触发probe2,则$x将设置为1,否则将设置为0(未初始化)

probe1  @a = 1; 
probe2  $x = @a; 

示例2: 将nsecs内置变量,分配给名为@start的映射,并且将tid(当前线程ID)设为键值, 时间戳为对应的值。 这样一来,线程就可以存储不会被其他线程覆盖的自定义时间戳

@start[tid] = nsecs;

示例3: 其中同时使用pid内置变量和$fd变量作为键

@path[pid, $fd] = str(arg0);

四、函数

1.内置函数

除了用于打印格式化输出的printf(),其他内置函数包括

printf(): 输出格式与C语言类似, %d

printf("%16s %-6d\\n", comm, pid)

join(char *arr[]): 将带有空格字符的字符串数组连接起来并打印出来

$ bpftrace -e 'tracepoint:syscalls:sys_enter_execve  join(args->argv); '
Attaching 1 probe...
ls -l
df -h
date
ls -l bashreadline.bt biolatency.bt biosnoop.bt bitesize.bt

str(char *): 从指针返回字符串

$ bpftrace -e 'ur:/bin/bash:readline  printf("%s\\n", str(retval)); '

kstack(mode [, limit]) / ustack: 返回内核级/用户级堆栈

// 通过跟踪block:block_rq_insert跟踪点,显示导致创建块I/O的前三个内核框架
$ bpftrace -e 't:block:block_rq_insert  @[kstack(3), comm] = count(); '
// mode参数允许堆栈输出采用不同的格式。 当前仅支持两种模式:bpftrace (默认模式) 和 perf,其生成的堆栈格式类似于Linux perf
$ bpftrace -e 'k:do_nanosleep  printf("%s", ustack(perf)); '

ksym(), usym() : 将地址解析为其符号名称

$ bpftrace -e 'tracepoint:timer:hrtimer_start  @[args->function] = count(); '
[...]
@[-1169114960]: 2517
@[-1169048384]: 8237// 这些是原始地址, 使用ksym()将它们转换为内核函数名称
$ bpftrace -e 'tracepoint:timer:hrtimer_start  @[ksym(args->function)] = count(); '
[...]
@[it_real_fn]: 2269
@[hrtimer_wakeup]: 7714
@[tick_sched_timer]: 27092

system(format[, arguments ...]): 在shell上运行命令

$ bpftrace --unsafe -e 't:syscalls:sys_enter_nanosleep  system("ps -p %d\\n", pid); '
Attaching 1 probe...
  PID TTY          TIME CMD
 1148 ?        00:02:43 google_osconfig
  PID TTY          TIME CMD
 1148 ?        00:02:43 google_osconfig
  PID TTY          TIME CMD
 1148 ?        00:02:43 google_osconfig

2. Map 函数

map 也可以分配给特殊功能,这些以自定义方式存储和打印数据。

count(): 计数事件,在打印时将打印计数。 这使用了per-CPU map@x成为类型计数的特殊对象

$ bpftrace -e 'tracepoint:block:*  @[probe] = count(); '
Attaching 18 probes...
^C

@[tracepoint:block:block_rq_issue]: 1
@[tracepoint:block:block_rq_insert]: 1
@[tracepoint:block:block_dirty_buffer]: 24
[...]

// 使用interval 探针, 可以打印每个间隔的速率
$ bpftrace -e 'tracepoint:block:block_rq_i*  @[probe] = count(); 
    interval:s:1  print(@); clear(@); '
Attaching 3 probes...
@[tracepoint:block:block_rq_issue]: 1
@[tracepoint:block:block_rq_insert]: 1

@[tracepoint:block:block_rq_insert]: 6
@[tracepoint:block:block_rq_issue]: 8
[...]

sum(), avg(), min(), max(): 存储基本统计信息

@y = sum($x);

hist(int n): 将$x存储在 2 n 2^n 2n的直方图中,并且在打印时将打印存储区计数和ASCII直方图

$ bpftrace -e 'tracepoint:syscalls:sys_exit_read  @ret = hist(args->ret); '
Attaching 1 probe...
^C

@ret:
(..., 0)             237 |@@@@@@@@@@@@@@                                      |
[0]                   13 |                                                    |
[1]                  859 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[2, 4)                57 |@@@                                                 |
[4, 8)                 5 |                                                    |
[...]

lhist(int n, int min, int max, int step): 这会将值存储为线性直方图

$ bpftrace -e 'tracepoint:syscalls:sys_exit_read  @ret = lhist(args->ret, 0, 1000, 100); '
Attaching 1 probe...
^C

@ret:
(..., 0)             101 |@@@                                                 |
[0, 100)            1569 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[100, 200)             5 |                                                    |
[200, 300)             0 |                                                    |
[...]

delete(): 从@start map中删除键是 tid 的键/值对

delete(@start[tid]);

clear(), zero(): 清空和置0

clear(@map)
zero(@map)

3. 示例

Timing vfs_read(): 该程序vfsread.bt,测量vfs_read()内核函数中的时间,并将时间打印为直方图(以微秒为单位)

程序一直运行到输入Ctrl-C ,然后打印此输出并终止。 该直方图被命名为"us",因为它可以打印输出名称,因此可以在输出中包含单位,给map赋予有意义的名称

#!/usr/local/bin/bpftrace

// this program times vfs_read()

// 通过使用kprobe检测其开始并将时间戳记存储在以线程ID为键的@start map中
kprobe:vfs_read

       @start[tid] = nsecs;

// 使用kretprobe检测其结束并计算增量为vfs_read()内核函数的持续时间. 使用过滤器来确保记录了开始时间
kretprobe:vfs_read
/@start[tid]/

       $duration_us = (nsecs - @start[tid]) / 1000;
       @us = hist($duration_us);
       delete(@start[tid]);

此脚本可以根据需要进行自定义. 这说明了bpftrace最有用的功能之一。 使用传统的系统工具(如iostat和vmstat),输出是固定的,无法轻松自定义。 但是用bpftrace,您可以将看到的指标进一步细分为多个部分,并通过其他探针的指标进行增强,直到获得所需的答案为止

@us[pid, comm] = hist($duration_us);
// output
@us[1847, gdbus]:
[1]                    2 |@@@@@@@@@@                                          |
[2, 4)                10 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[4, 8)                10 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|

@us[1630, ibus-daemon]:
[2, 4)                 9 |@@@@@@@@@@@@@@@@@@@@@@@@@@@                         |
[4, 8)                17 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[...]

五、Probe类型

1. tracepoint - 内核静态插桩

a. 格式

tracepoint_name是跟踪点的全名,包括冒号,它将冒号分隔成自己的类和事件名称的层次结构,例如net:netif_rx

tracepoint:tracepoint_name

b. 参数

跟踪点提供可通过内置args在bpftrace中访问的信息字段。

例如,net:netif_rx有一个称为len的字段,表示可以使用args -> len访问的数据包长度

如果您不熟悉bpftrace和跟踪,则系统调用跟踪点是很好的检测目标。 它们提供了广泛的内核资源使用范围,并具有文档齐全的API。例如: 检测read系统调用的开始和结束

syscalls:sys_enter_read
syscalls:sys_exit_read

对于sys_enter_read跟踪点, 可以使用 -l and -v 查看详情. 如下所示, 其参数应为args-> fd

$ bpftrace -lv tracepoint:syscalls:sys_enter_read
tracepoint:syscalls:sys_enter_read
    int __syscall_nr;
    unsigned int fd;
    char * buf;
    size_t count;

使用示例:

$ bpftrace -e 'tracepoint:syscalls:sys_enter_read 
    printf("-> count() by %s PID %d\\n", comm, pid); 
  tracepoint:syscalls:sys_exit_read 
    printf("<- count() return %d, %s PID %d\\n", args->ret, comm, pid); '

2. usdt - 用户级静态插桩

a. 格式

usdt可以通过提供完整路径来检测可执行binaries或shared库。 probe_name是binary中的USDT探针名称

usdt:binary_path:probe_name
usdt:library_path:probe_name
usdt:binary_path:probe_namespace:probe_name
usdt:library_path:probe_namespace:probe_name

在不指定探针namespace的情况下,它的默认名称与binary或库的名称相同。 如果它有许多不同的探针,则必须有namespace。 一个示例是libjvm(JVM库)中的hotspot namespace 探针。 例如(完整库路径被截断)

usdt:/.../libjvm.so:hotspot:method__entry

b. 参数

USDT探针的任何参数都可以作为内置args的成员使用,可以使用-l列出binary中的可用探针

$ bpftrace -l 'usdt:/usr/local/cpython/python'
usdt:/usr/local/cpython/python:line
usdt:/usr/local/cpython/python:function__entry
[...]

3. kprobe, kretprobe - 内核动态插桩

a. 格式

kprobe表示函数的开始(入口),而kretprobe表示函数的结束(返回). function_name是内核函数名称。 例如,可以使用kprobe:vfs_readkretprobe:vfs_read来检测vfs_read()内核函数。

kprobe:function_name
kretprobe:function_name

b. 参数

kprobe的参数: arg0, arg1, …, argN是函数的输入参数,为uint64。 如果它们是C结构的指针,则可以将它们强制转换为该struct.

kretprobe的参数:retval内置函数具有该函数的返回值。 retval始终是uint64; 如果这与该函数的返回类型不匹配,则需要将其强制转换为该类型。

c. 示例

例如,使用kretprobe将vfs_read()返回值(字节或错误值)汇总为直方图

$ bpftrace -e 'kretprobe:vfs_read  @bytes = hist(retval); '

4. uprobe, uretprobe - 用户级动态插桩

a. 格式

uprobe表示函数的开始(入口),而uretprobe表示函数的结束(返回)

uprobe:binary_path:function_name
uprobe:library_path:function_name
uretprobe:binary_path:function_name
uretprobe:library_path:function_name

b. 参数

uprobe的参数: arg0, arg1, …, argN是函数的输入参数,为uint64。 如果它们是C结构的指针,则可以将它们强制转换为该struct.

uretprobe的参数: retval内置函数具有该函数的返回值。 retval始终是uint64; 如果这与该函数的返回类型不匹配,则需要将其强制转换为该类型

5. software, hardware

这些是预定义的软件和硬件事件

a. 格式

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

软件事件类似于跟踪点,但适用于基于计数的指标和基于样本的检测。 硬件事件是用于处理器级分析的PMC的选择。两种事件类型都可能发生得如此频繁,以至于对每个事件进行检测都会产生大量开销,从而降低系统性能。 通过使用采样和计数字段可以避免这种情况,该字段会触发探针在每个[count]个事件中触发一次。 如果未提供计数,则使用默认值

b. 可用的事件

软件

硬件

6. profile, interval

这些是基于计时器的事件

a. 格式

profile 文件类型会在所有CPU上触发,并且可用于采样CPU使用率。 interval 类型仅在一个CPU上触发,可用于打印基于间隔的输出

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

六、Operators

1. 运算符

=, +, -, *, /, ++, --, &, |, ^, !
// 左移,右移
<<, >>
// 比较
+=, -=, *=, /=, %=, &=, ^=, <<=, >>=

2. 三元运算符

test ? true_statement : false_statement

示例: 计算绝对值

$abs = $x >= 0 ? $x : - $x;

3. IF

当前不支持else if语句。

if (test)  true_statements 
if (test)  true_statements  else  false_statements 

示例: 在IPv4和IPv6上执行不同动作的程序

if ($inet_family == $AF_INET) 
    // IPv4
   ...
 else 
    // IPv6
    ...

4. Unrolled 循环

BPF在受限的环境中运行,在该环境中必须能够验证程序是否已结束,并且不会陷入无限循环中。 对于需要某些循环功能的程序,bpftrace通过unroll()支持展开循环.

unroll (count)  statements 

count是一个整数常量,最大为20。不支持将计数作为变量提供,因为必须在BPF编译阶段知道循环迭代的次数

以上是关于LinuxBPF学习笔记 - bpftrace开发[7]的主要内容,如果未能解决你的问题,请参考以下文章

LinuxBPF学习笔记 - bpftrace开发[7]

LinuxBPF学习笔记 - 调试技术[4]

LinuxBPF学习笔记 - 调试技术[4]

LinuxBPF学习笔记 - BCC工具[6]

LinuxBPF学习笔记 - BCC工具[6]

LinuxBPF学习笔记 - 基本概念 [1]