文章目录
一、eBPF基础认识
1.1 eBPF历史演进
BPF
(Berkeley Packet Filter,伯克利数据包过滤器) 的概念最早源自于1992 伯克利实验室的一篇论文《The BSD Packet Filter: A New Architecture for User-level Packet Capture》,这篇论文描述了如何更加高效灵活地从操作系统内核中抓取网络数据包。
我们熟悉的 tcpdump 工具,就是利用了 BPF技术来抓取 Unix/Linux 操作系统中的网络包。
随着内核的发展,BPF 逐步从最初的数据包过滤扩展到了网络、内核、安全、跟踪等,而且它的功能特性还在快速发展中,这种扩展后的 BPF 被简称为 eBPF
(即extended BPF)。早期的 BPF 被称为经典 BPF,简称 cBPF
(即classic BPF)。实际上,现代内核所运行的都是 eBPF。
简言之,eBPF是一种不需要修改内核代码或加载内核模块,就可以在 Linux 内核中运行用户程序的技术。通过eBPF技术,使得Linux内核实现可动态编程化。
1.2 eBPF特点和使用场景
eBPF的特点(优势)
-
稳定:有循环次数和代码路径触达限制,保证程序可以固定时间结束
-
高效:可通过 JIT 方式翻译成本地机器码,执行效率高效
-
安全:验证器会对 eBPF 程序的可访问函数集合和内存地址有严格限制,不会导致内核Panic
-
热加载/卸载:可热加载/卸载 eBPF 程序,无需重启 Linux 系统
-
内核内置:eBPF 自身提供了稳定的 API
eBPF的限制(安全性的体现)
-
eBPF程序不能调用任意的内核函数,只限于内核模块中列出的BPF辅助函数,函数支持列表也随着内核的演进在不断增加
-
eBPF程序不允许包含无法到达的指令,防止加载无效代码,延迟程序的终止
-
eBPF程序中循环次数限制且必须在有限时间内结束
eBPF vs 内核模块
kprobes和tracepoint技术已经存在很多年了,内核模块和BPF都能够通过它们来对内核进行跟踪和修改,但是相对而言,eBPF具有如下优势:
-
eBPF程序会通过验证器的安全性检查,而内核模块可能引入bug,导致内核崩溃。
-
eBFP通过映射表提供丰富的数据支持。
-
eBPF程序通过一次编译,然后在任何地方运行。
-
eBPF程序的编译不依赖内核编译过程的中间结果。
-
eBPF程序的开发难度更低。
-
eBPF程序具备原子性替换特性
当然,内核模块的一个好处是:模块中可以使用其他内核函数和内核设施,而eBPF只能使用有限的API函数。
应用场景
基于上述特点,eBPF可用于如下场景:
-
性能监控:传统的系统分析工具(如top、vmstat、pidstat、netstat、iostat等),多是从 /proc、/sys、/dev 中获取信息,会对系统产生一定的开销,不适合频繁调用。相比之下,使用eBPF采集系统信息的开销小的多。
-
性能调优:eBPF发现系统/网络性能抖动,特别是一些毛刺情况,会主动采集当时系统运行状态。
-
程序分析:在程序无感的情况下做用户程序活体分析,比如程序内存使用监控,程序运行状态的查看。
-
防御攻击:在网络包未到网络栈之前就处理掉,而且开销较小。
1.3 eBPF工作原理
Linux 内核提供了各种 hook point
。eBPF 程序是基于事件驱动模型,通过实现指定hook point
的“callback函数”,并把这些“callback函数”注册到相应hook point
来完成“内核编程”。
Tips:Linux 内核提供的 hook point
,主要分为三类:
-
静态跟踪类型: 网络套接字,跟踪点,用户态标记(USDT)。
-
动态跟踪类型:kprobe, uprobes。
-
采样和PMC:perf_events。
eBPF程序执行过程
eBPF程序的一般开发流程:
-
用户态
-
编写:可以使用 eBPF汇编 或者 C语言 来编写eBPF程序(注:一般很少会用 eBPF 汇编来写程序)。
-
编译:使用 LLVM 或者 GCC 编译工具将 eBPF 源程序编译成 eBPF 字节码。
-
加载:通过
sys_bpf()
系统调用,把 eBPF 字节码提交给内核。
-
-
内核态
-
验证:内核会对eBPF字节码进行验证,保证其是安全无害的。
-
翻译:内核使用 JIT(Just In Time,即时编译)技术将 eBPF 字节编译成本地机器码(Native Code)。
-
绑定:内核将eBPF程序绑定到具体的
hook point
。 -
触发运行:在内核对应事件触发时,运行 eBPF程序,可通过 map 或者 perf_event 与用户态程序进行数据交互。
-
eBPF的开销
eBPF工具分析性能问题时,对于内核层的trace,产生的额外开销就是在tracepoint处,触发单步中断(或者jmp),多调用一次“callback”函数。而对于应用层的trace,额外开销是在tracepoint处,触发单步中断,陷入内核,在内核中执行“callback”,这个开销是比较大的,对与这类问题,需要关注eBPF工具本身对应用程序性能的影响。
二、eBPF简单实践(Hello World)
这里,我们使用 BCC 开发一个跟踪 openat() 系统调用的 eBPF 程序。
注:BCC(BPF Compiler Collection,BPF编译器集合)是一个知名的eBPF前端(另一个是bpftrace
,后面会专文介绍),包含了用于构建 BPF 程序的编程框架和库,提供了大量可以直接使用的工具,并提供了 Python、C++ 等编程语言接口,可以通过 Python/C++ 等高级语言去跟 eBPF 的各种事件和数据进行交互。
2.1 准备环境
虽然 Linux 内核很早就已经支持了 eBPF,但很多新特性都是在 4.x 版本中逐步增加的。在开发和学习 eBPF 时,为了体验和掌握最新的 eBPF 特性,推荐使用 5.x 或更新的内核。本文使用Ubuntu 20.04.6
进行演示。
安装工具
sudo apt install -y make clang llvm libelf-dev libbpf-dev bpfcc-tools libbpfcc-dev python3-bpfcc linux-tools-$(uname -r) linux-headers-$(uname -r)
2.2 使用 C 语言开发eBPF程序
使用C语言编写 hello.c
如下:
int hello(void *ctx)
{
bpf_trace_printk("Hello, World!");
return 0;
}
注:bpf_trace_printk()
是一个常用的 BPF 函数,它的作用是输出一段字符串。由于 eBPF 运行在内核中,它的输出并不是通常的标准输出(stdout
),而是内核调试文件 /sys/kernel/debug/tracing/trace_pipe
。
2.3 使用 Python 语言开发一个用户态程序
使用Python语言编写 hello.py
如下:
#!/usr/bin/env python3
# 1) import bcc library
from bcc import BPF
# 2) load BPF program
b = BPF(src_file="hello.c")
# 3) attach kprobe
b.attach_kprobe(event="do_sys_openat2", fn_name="hello")
# 4) read and print /sys/kernel/debug/tracing/trace_pipe
b.trace_print()
注:
-
b = BPF(src_file=“hello.c”) :调用 BPF() 加载第一步开发的 BPF 源代码。
-
b.attach_kprobe(event=“do_sys_openat2”, fn_name=“hello”):将 BPF 程序挂载到内核探针。
do_sys_openat2
是系统调用openat()
在内核中的实现。 -
b.trace_print():读取
/sys/kernel/debug/tracing/trace_pipe
文件的内容,并打印到标准输出中。 -
在运行的时候,BCC 会调用 LLVM,把 BPF 源代码编译为字节码,再加载到内核中运行。
2.4 执行eBPF程序
eBPF 程序需要以 root 用户来运行。
chmod a+x hello.py
sudo ./hello.py
该eBPF程序会持续跟踪使用openat
系统调用的进程,并将其信息打印出来,需按Ctrl-C组合键停止该程序。
终端输出如下(省略部分内容):
b' systemd-udevd-127691 [014] ....1 494931.727453: bpf_trace_printk: Hello, World!'
b' ThreadPoolForeg-4973 [001] ....1 494932.004075: bpf_trace_printk: Hello, World!'
b' Chrome_ChildIOT-4974 [013] ....1 494932.031537: bpf_trace_printk: Hello, World!'
b' docker-1297951 [009] ....1 494932.402515: bpf_trace_printk: Hello, World!'
b' <...>-1282206 [018] ....1 494932.611929: bpf_trace_printk: Hello, World!'
b' systemd-udevd-513 [010] ....1 494932.694446: bpf_trace_printk: Hello, World!'
注:
-
第一列
systemd-udevd-127691
: 进程名字和PID -
第二列
[014]
: CPU编号 -
第三列
....1
: 一系列的选项 -
第四列
494931.727453
: 时间戳 -
第五列
bpf_trace_printk
: 函数名 -
第六列
Hello, World!
: 调用函数时的传参
参考: