了解BPF技术

插桩技术

BPF支持多种事件源,可以在整个软件栈中提供能见度,其实现目前主要通过两种技术,分别为动态插桩静态插桩,有时候只用其中一种方式是无法实现我们的需求,这时就需要两者相互配合使用了。

动态插桩: kprobes 和 uprobes

动态插桩方式可以做到在程序运行期间,动态的插入观察点,而软件的运行不会受到任何影响,在这点做到了零开销。

kprobes 一般指内核态级别的函数插桩,而 uprobes 则指用户态级别的函数插桩。

动态插桩技术一般需要在 内核函数应用函数开始位置结束位置进行插桩。例如

#!/usr/local/bin/bpftrace

// this program times vfs_read()

kprobe:vfs_read
{
@start[tid] = nsecs;
}

kretbrobe:vfs_read
/@start[tid]/
{
$duration_us = (nsecs - @start[tid]) / 1000;
@us = hist($duration_us);
delete(@start[tid]));
}

这里kprobe:vfs_read 表示是内核函数bfs_read的起始插桩,而kretbroke:vfs_read则是函数的结果插桩,通过对这两个地方分别插桩就可以计算出当前函数的执行时间。

被插桩的函数在整个软件栈中有成千上万个,所以我们可以在任意关注的地方进行插桩,并编写自己的代码实现想要的功能。

动态插桩技术有一点不好的地方就是随着软件版本的迭代变更,被插桩的函数有可能会被重命名或者被移除,这时候会导致一些BPF工具没有办法直接使用,要想继续使用这些BPF工具只能跟着将其进行调整,这个问题十分令人头疼;除此之外还有一个问题就是编译器可能会将一些函数进行 inline 化,这时候会导致这些函数无法使用 kprobesuprobes 动态插桩。

静态插桩: tracepoint 和 USDT

静态插桩会将一些稳定的事件名字编码到软件代码中,由开发者自行维护。BPF跟踪工具支持内核的静态插桩技术,也支持用户态的静态定义跟踪插桩 USDT(user level statically defined tracing)。

不过静态插桩技术也存在一定的问题,那就是会增加开发者的维护成本,同时静态插桩点数量一般很有限。

所有如果我们要开发BPF工具的话,一般都推荐优先使用静态插桩技术,如果仍无法满足需求的话,再考虑使用动态插桩技(kprobes 或 uprobes)

开发工具

直接通过BPF指令编写BPF程序是件非常繁琐的一件事,因此出现了一些高级语言支持的BPF前端工具,主流开发工具主要是 BCCbpftrade

bcc、bpftrace 与bpf
bcc、bpftrace 与bpf

BCC

BBC(BPF编译器集合,BPF Compiler Collection) 是最早的开发BPF的开发框架。它提供了一个编写内核BPF程序的C语言环境,同时还提供了其它高级语言如Python、Lua 或 C++环境来实现用户端接口,它也是 libbcclibbpf 库的前身,这两个库提供了使用BPF程序对事件进行观测的库函数。

用户可以直接在系统上安装BCC即可,不用手动亲自编码代码就可以直接使用自带的一些工具。

bpftrace

bpftrace是一个新兴的前端,其源代码非常简洁,它同样也是基于 libbcclibbpf 库构建的。

对比

BCC 与 bpftrace 两者具有互补性,bpftrace在编写功能强大的单行程序或短小的脚本方便十分理解;BCC则更适合开发大型复杂的脚本和作为后台进程使用,同时它还可以调用其它的库。比如目前很多用Python来开发BCC程序,它们使用Python的 argparse 库来提供复杂的命令行参数运行。

BCC 和 bpftrace 它们并不属于内核代码仓库的项目,而是托管在 Github 上一个名为 IO Visor的Linux基金会。

推荐阅读:https://www.brendangregg.com/ebpf.html

参考资料