eBPF (extended Berkeley Packet Filter) 是一种革命性的内核技术,它允许我们在不修改内核源码或加载内核模块的情况下,安全、高效地在内核中执行一小段“沙盒”程序。这使得 eBPF 在网络、安全、性能分析和内核调试等领域大放异彩。
选择合适的开发工具是高效利用 eBPF 的关键。不同的工具在易用性、灵活性、性能和可移植性之间做出了不同的权衡。
以下是三个最核心、最具代表性的 eBPF 开发工具/框架:
- BCC (BPF Compiler Collection)
- bpftrace
- libbpf + BPF CO-RE
1. BCC (BPF Compiler Collection)
BCC 是一个用于创建高效内核追踪和操作程序的工具包,它包含了大量的实用工具和示例。它通过提供 Python/Lua 等高级语言的前端,极大地简化了 eBPF 程序的开发。
开发方式
BCC 的开发模式是“前端脚本 + 内嵌C代码”。
- eBPF C代码:你将eBPF的内核态程序(用C语言编写)作为一个字符串嵌入到你的Python(或Lua)脚本中。
- Python前端:
- 使用BCC提供的API来加载和编译上述C代码字符串。BCC会在运行时调用内置的LLVM/Clang库将C代码编译成BPF字节码。
- 将编译好的BPF程序附加(attach)到内核的指定挂载点(如 kprobe, tracepoint, network socket 等)。
- 创建和管理 BPF Maps(eBPF程序与用户态程序之间通信的桥梁)。
- 从BPF Maps中读取数据,或者通过
perf_buffer
接收事件,然后在Python脚本中对这些数据进行处理、聚合和展示。
代码示例(一个简单的execsnoop
):
#!/usr/bin/python3
from bcc import BPF
# 1. 内嵌的eBPF C代码
prog = """
#include <linux/sched.h>
// 定义用于数据传输的结构体
struct data_t {
u32 pid;
char comm[TASK_COMM_LEN];
};
// 定义一个BPF Map,类型为perf event output
BPF_PERF_OUTPUT(events);
// kprobe挂载到sys_execve函数执行的入口
int hello_exec(struct pt_regs *ctx) {
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&data.comm, sizeof(data.comm));
// 将数据提交到perf buffer,用户态程序可以从中读取
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
"""
# 2. Python用户态程序
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("execve"), fn_name="hello_exec")
print("Tracing execve syscalls... Ctrl-C to end.")
# 定义处理从内核收到的数据的回调函数
def print_event(cpu, data, size):
event = b["events"].event(data)
print(f"PID: {event.pid} Comm: {event.comm.decode('utf-8')}")
# 打开perf buffer并开始轮询数据
b["events"].open_perf_buffer(print_event)
while True:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
使用场景
- 快速原型开发和学习:BCC极大地降低了eBPF的入门门槛。如果你熟悉Python,可以很快上手写出有用的工具。
- Ad-hoc系统诊断:当你需要快速编写一个脚本来诊断线上特定问题时,BCC非常方便。例如,追踪特定函数的调用、文件打开、网络连接等。
- 功能丰富的现成工具集:BCC项目自身就提供了一百多个开箱即用的工具(如
execsnoop
,tcplife
,biolatency
等),这些工具本身就是解决各种性能问题的利器,也是极佳的学习范例。
优点:
- 开发效率高,上手快。
- Python生态丰富,方便进行数据后处理和可视化。
- 自带大量实用工具。
缺点:
- 依赖笨重:目标机器上需要安装完整的LLVM/Clang工具链以及内核头文件(
kernel-headers
)。这对于生产环境的大规模部署来说是个挑战。 - 可移植性差:由于在目标机器上实时编译,如果内核版本变化导致数据结构(如
struct task_struct
)变动,代码可能需要修改才能在不同内核上运行。 - 性能开销:Python的解释执行和数据拷贝开销相较于纯C/C++的用户态程序要大。
2. bpftrace
bpftrace
是一种用于内核动态追踪的高级语言。它的语法深受 awk
和 C
的启发,并借鉴了 DTrace
和 SystemTap
等传统追踪工具的设计。它专注于“一句话(one-liner)”解决问题。
开发方式
bpftrace
的开发模式是“编写专用的追踪脚本”。你不需要写C代码,也不需要写Python。你只需要编写 bpftrace
语言脚本,然后通过 bpftrace
命令行工具执行即可。
bpftrace
脚本的基本结构是: probe /filter/ { action }
- probe: 指定要探测的事件,例如
kprobe:do_sys_openat2
(内核函数入口),tracepoint:syscalls:sys_enter_open
(静态跟踪点),uprobe:/bin/bash:readline
(用户态函数) 等。 - filter: 可选的过滤条件,只有满足条件的事件才会触发action。例如
/pid == 1234/
。 - action: 事件触发时要执行的操作,例如打印信息、记录到map中。
bpftrace
提供了丰富的内置变量(如pid
,comm
,arg0-argN
)和函数(如printf
,hist
,count
)。
代码示例:
- 一行命令追踪所有进程打开文件的系统调用:
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s (%d) opening %s\n", comm, pid, str(args->filename)); }'
- 统计每个进程执行
execve
的次数:sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { @counts[comm]++; }'
(退出时会自动打印@counts
这个map的内容) - 显示VFS读操作的延迟直方图:
bash sudo bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; } kretprobe:vfs_read /@start[tid]/ { @latency = hist((nsecs - @start[tid]) / 1000); delete(@start[tid]); }'
使用场景
- 实时、交互式的系统分析:当你在线上发现问题,需要快速回答“谁在做某事?”、“某操作为何这么慢?”这类问题时,
bpftrace
是无敌的。 - 性能瓶颈排查:通过其强大的聚合功能(
count
,sum
,avg
,hist
),可以轻松地分析延迟、统计事件频率。 - 系统行为探索:对于不熟悉的子系统,可以用
bpftrace
探索其函数调用关系和行为模式。
优点:
- 极其简洁:表达能力强,通常几行代码就能实现BCC几十行代码才能完成的功能。
- 零样板代码:专注于追踪逻辑,无需关心编译、加载、map创建等细节。
- 交互性强:非常适合在命令行中即时使用。
缺点:
- 功能限制:虽然强大,但它主要面向“追踪与聚合”,对于需要复杂逻辑控制、精细化状态管理、或与外部系统深度交互的应用,
bpftrace
会显得力不从心。 - 依赖:和BCC一样,通常也需要LLVM/Clang和内核头文件。
3. libbpf + BPF CO-RE (Compile Once – Run Everywhere)
这是目前业界推荐的、用于构建生产级eBPF应用的现代化方法。它解决了BCC和bpftrace
在依赖和可移植性上的核心痛点。
开发方式
libbpf
的开发模式是“内核态与用户态代码分离,预编译,动态加载”。
- eBPF内核态程序 (
.bpf.c
):- 纯C语言编写,但包含
libbpf
提供的一些头文件(如bpf_helpers.h
)。 - 使用
SEC("...")
宏来定义程序段,指定程序的类型和挂载点。 - 通过
struct { ... } BPF_HASH_MAP(...)
的方式声明 maps。 - 核心是CO-RE:代码不直接
#include
内核头文件,而是依赖于内核提供的 BTF (BPF Type Format) 类型信息。通过bpf_core_read()
等辅助函数来访问数据结构成员,libbpf
会在加载时根据目标内核的BTF信息自动调整偏移量,实现可移植性。
- 纯C语言编写,但包含
- 用户态加载程序 (
.c
,.cpp
,.go
,.rust
…):- 一个常规的用户态应用程序。
- 首先,使用
clang
将.bpf.c
文件编译成一个轻量的.bpf.o
对象文件。这个步骤可以在开发机上完成。 - 用户态程序中使用
libbpf
库提供的API函数(如bpf_object__open
,bpf_object__load
,bpf_program__attach
,bpf_map__lookup_elem
等)来加载和操作.bpf.o
文件。 - 这个用户态程序负责整个eBPF应用的生命周期管理。
代码结构示例:
minimal.bpf.c
(内核态程序)
#include "vmlinux.h" // 由bpftool生成的、包含BTF信息的头文件
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, u32);
__type(value, u64);
} my_map SEC(".maps");
SEC("kprobe/sys_execve")
int BPF_KPROBE(handle_exec)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 *count, one = 1;
count = bpf_map_lookup_elem(&my_map, &pid);
if (count) {
__sync_fetch_and_add(count, 1);
} else {
bpf_map_update_elem(&my_map, &pid, &one, BPF_ANY);
}
return 0;
}
char LICENSE[] SEC("license") = "GPL";
minimal.c
(用户态程序)
#include <bpf/libbpf.h>
#include "minimal.skel.h" // 由bpftool根据.bpf.c自动生成的骨架头文件
int main(int argc, char **argv)
{
struct minimal_bpf *skel;
int err;
// 打开、加载、验证BPF程序
skel = minimal_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
// 附加到kprobe
err = minimal_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
"to see output of the BPF programs.\n");
// ... 在这里可以轮询map,处理数据 ...
cleanup:
minimal_bpf__destroy(skel);
return -err;
}
使用场景
- 生产环境的可观测性(Observability)和安全工具:例如 Prometheus eBPF exporter, Parca, Cilium, Falco 等项目都深度使用 libbpf + CO-RE。
- 需要长期运行的守护进程(Daemon):构建需要稳定运行、资源占用可控的监控或安全代理。
- 大规模部署:当你需要将一个eBPF工具部署到成百上千台不同内核版本的服务器上时,CO-RE是唯一的选择。
优点:
- 极致的可移植性(CO-RE):真正做到“一次编译,到处运行”,无需在目标机上重新编译。
- 最小化依赖:目标机器上只需要启用BTF的内核(现代主流发行版内核都已支持),无需LLVM/Clang和内核头文件。
- 高性能:用户态程序可以用C/C++/Go/Rust等高性能语言编写,开销极小。
- 强大的生态:
libbpf
已经成为事实上的标准库,得到了内核社区的大力支持。
缺点:
- 学习曲线陡峭:需要理解eBPF程序的生命周期、libbpf的API、BTF和CO-RE的概念,开发流程更复杂。
- 开发效率较低:相较于
bpftrace
的“一句话”,libbpf
需要编写更多的样板代码。
总结与选择建议
特性 / 工具 | BCC | bpftrace | libbpf + BPF CO-RE |
---|---|---|---|
易用性 | 中 (Python接口友好) | 高 (专用高级语言) | 低 (C/C++ + API) |
开发效率 | 高 (快速原型) | 极高 (即时分析) | 低 (工程化) |
运行时依赖 | 重 (LLVM/Clang, Kernel Headers) | 重 (LLVM/Clang, Kernel Headers) | 轻 (只需内核支持BTF) |
可移植性 | 差 | 差 | 极好 (CO-RE) |
性能 | 中 (Python开销) | 中 (解释器开销) | 高 (原生C/C++/Go/Rust) |
主要应用场景 | 学习、工具原型、快速诊断 | 线上即时调试、性能瓶颈排查 | 生产级监控/安全产品、大规模部署 |