Linux eBPF 入门:内核可观测性新范式
传统的 Linux 内核观测手段——printf 调试、内核模块、ftrace——要么侵入性强,要么能力有限。eBPF(extended Berkeley Packet Filter)改变了这一切:你可以在不修改内核源码、不加载内核模块的情况下,安全地在内核态运行自定义逻辑。它已经成为云原生可观测性、网络和安全领域的基石技术。
适合谁读:后端工程师想做性能分析、SRE 需要生产级可观测性、云原生开发者想理解 Cilium/Falco 底层原理、对 Linux 内核感兴趣但不想写内核模块的工程师。
一、eBPF 是什么
eBPF 源自 BSD 包过滤器(BPF),最初用于 tcpdump 中的包过滤。2014 年 Linux 3.18 将其扩展为通用内核沙箱运行时——允许用户在内核事件触发时安全执行预编译的字节码。
核心思想:事件驱动。你把 BPF 程序挂载到内核钩子(hook)上,当特定事件发生时,内核自动执行你的程序。
与传统方案对比
| 方案 | 侵入性 | 安全性 | 灵活性 | 性能开销 |
|---|---|---|---|---|
| 内核模块(LKM) | 高 | 低(可 panic 内核) | 高 | 低 |
| ftrace / perf | 低 | 高 | 低(固定探针) | 中 |
| ptrace / strace | 中 | 高 | 低 | 高(2-10x 减速) |
| eBPF | 低 | 高(验证器保障) | 高 | 极低 |
eBPF 程序的生命周期
1. 编写 BPF 程序(C / BTF / bpftrace)
2. 编译为 BPF 字节码(clang -target bpf)
3. 加载到内核 → 验证器(Verifier)安全检查
4. JIT 编译为本地机器码
5. 挂载到内核钩子(kprobe / tracepoint / XDP 等)
6. 事件触发时自动执行
7. 通过 Map 与用户态通信
8. 不需要时卸载
关键保障:验证器会在加载时静态分析 BPF 程序,确保:无无限循环、无越界内存访问、所有路径都有返回、不会 panic 内核。这是 eBPF 安全性的根基。
二、eBPF 程序类型与挂载点
截至 Linux 6.x,eBPF 支持数十种程序类型,以下是最常用的几类:
| 程序类型 | 挂载点 | 典型场景 |
|---|---|---|
| kprobe / kretprobe | 内核函数入口/返回 | 追踪内核行为 |
| tracepoint | 内核静态追踪点 | 稳定 API,推荐优先使用 |
| uprobe / uretprobe | 用户态函数入口/返回 | 追踪应用层行为 |
| XDP | 网卡驱动层 | 高性能包处理(DDoS 防御、LB) |
| tc | 流量控制层 | 网络策略、NAT |
| cgroup_skb | cgroup 网络事件 | 容器网络策略 |
| perf_event | 性能计数器 | CPU profiling、缓存分析 |
| lsm | Linux 安全模块 | 运行时安全策略 |
选择建议:能用
tracepoint 就不用 kprobe。tracepoint 是内核稳定的 ABI,不会随版本变化而失效;kprobe 依赖内核函数名,版本升级可能失效。
三、第一个 eBPF 程序:用 bpftrace 一行命令
bpftrace 是 eBPF 的高级追踪语言,类似 awk 风格,一行命令就能完成复杂的内核追踪。不需要写 C、不需要编译。
安装 bpftrace
# Ubuntu / Debian
sudo apt install bpftrace
# CentOS / RHEL
sudo yum install bpftrace
# 从源码编译(获取最新版)
git clone https://github.com/bpftrace/bpftrace
cd bpftrace && mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc) && sudo make install
一行命令实战
# 追踪所有 openat 系统调用,看看谁在打开文件
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s -> %s\n", comm, str(args->filename)); }'
# 统计每个进程的系统调用次数
sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
# 追踪 TCP 连接建立
sudo bpftrace -e 'kprobe:tcp_connect { printf("PID %d (%s) connecting\n", pid, comm); }'
# 统计内核函数调用耗时分布
sudo bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; } kretprobe:vfs_read /@start[tid]/ { @ns = hist(nsecs - @start[tid]); delete(@start, tid); }'
# 实时监控 OOM Kill
sudo bpftrace -e 'kprobe:oom_kill_process { printf("OOM killed PID %d\n", pid); }'
输出示例:
Attaching 5 probes...
nginx -> /var/log/nginx/access.log
python3 -> /tmp/data.csv
mysqld -> /var/lib/mysql/ibdata1
^C
@ns:
[256, 512) 43 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[512, 1K) 28 |@@@@@@@@@@@@@@@@@@@@@@@@ |
[1K, 2K) 12 |@@@@@@@@@@@@@@ |
[2K, 4K) 3 |@@@ |
四、BCC 工具链:开箱即用的性能分析
BCC(BPF Compiler Collection)提供了 70+ 个预构建的 eBPF 工具,覆盖 CPU、内存、网络、文件 I/O 等场景。大部分工具一行命令就能用。
安装 BCC
# Ubuntu 22.04+
sudo apt install bpfcc-tools linux-headers-$(uname -r)
# CentOS 8+
sudo yum install bcc-tools
常用工具速查
| 工具 | 功能 | 示例 |
|---|---|---|
| execsnoop | 追踪新进程创建 | execsnoop-bpfcc |
| opensnoop | 追踪文件打开 | opensnoop-bpfcc -n nginx |
| tcpconnect | 追踪 TCP 主动连接 | tcpconnect-bpfcc |
| tcpretrans | 追踪 TCP 重传 | tcpretrans-bpfcc |
| biolatency | 块 I/O 延迟直方图 | biolatency-bpfcc |
| biosnoop | 追踪每次块 I/O | biosnoop-bpfcc |
| memleak | 检测内存泄漏 | memleak-bpfcc -p $(pidof myapp) |
| offcputime | off-CPU 时间分析 | offcputime-bpfcc -p 1234 |
| profile | CPU profiling(采样) | profile-bpfcc -F 99 -p 1234 |
| slabratetop | 内核 slab 分配速率 | slabratetop-bpfcc |
实战:定位慢请求根因
# 1. 发现哪个进程在做大量 I/O
sudo biotop-bpfcc
# 2. 看 I/O 延迟分布
sudo biolatency-bpfcc
# 3. 追踪具体文件被谁打开
sudo opensnoop-bpfcc -n mysqld
# 4. 看进程 off-CPU 在等什么
sudo offcputime-bpfcc -p $(pidof myapp) 5
# 5. 检查是否有内存泄漏
sudo memleak-bpfcc -p $(pidof myapp) -t
五、编写 C 语言 eBPF 程序
bpftrace 适合快速追踪,但复杂场景需要写 C 语言 eBPF 程序。使用 libbpf 框架是当前推荐的方式。
项目结构
hello-ebpf/
├── src/
│ ├── hello.bpf.c # 内核态 BPF 程序
│ └── hello.c # 用户态加载器
├── vmlinux.h # 内核类型定义(BTF 生成)
└── Makefile
内核态程序:hello.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
// 定义一个 Perf Event Array Map,用于向用户态发送事件
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(".maps");
// 事件结构体
struct event {
u32 pid;
char comm[16];
char filename[256];
};
// 挂载到 openat 系统调用的 tracepoint
SEC("tracepoint/syscalls/sys_enter_openat")
int handle_openat(struct trace_event_raw_sys_enter *ctx)
{
struct event e = {};
u64 id = bpf_get_current_pid_tgid();
e.pid = id >> 32;
bpf_get_current_comm(&e.comm, sizeof(e.comm));
// 从 tracepoint 参数中读取文件名
const char *filename = (const char *)ctx->args[1];
bpf_probe_read_user_str(e.filename, sizeof(e.filename), filename);
// 发送事件到用户态
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &e, sizeof(e));
return 0;
}
char LICENSE[] SEC("license") = "GPL";
用户态加载器:hello.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
struct event {
unsigned int pid;
char comm[16];
char filename[256];
};
static void perf_event_handler(void *ctx, int cpu, void *data, __u32 size)
{
struct event *e = data;
printf("[%-6d] %-16s opened %s\n", e->pid, e->comm, e->filename);
}
int main(int argc, char **argv)
{
struct bpf_object *obj;
struct bpf_program *prog;
struct perf_buffer *pb;
int err;
// 1. 打开并加载 BPF 程序
obj = bpf_object__open_file("hello.bpf.o", NULL);
if (libbpf_get_error(obj)) {
fprintf(stderr, "Failed to open BPF object\n");
return 1;
}
err = bpf_object__load(obj);
if (err) {
fprintf(stderr, "Failed to load BPF object: %d\n", err);
return 1;
}
// 2. 找到 BPF 程序并挂载
prog = bpf_object__find_program_by_name(obj, "handle_openat");
if (!prog) {
fprintf(stderr, "Failed to find BPF program\n");
return 1;
}
// libbpf 会自动根据 SEC() 声明挂载到对应 tracepoint
// 3. 设置 Perf Buffer 接收事件
struct bpf_map *events_map = bpf_object__find_map_by_name(obj, "events");
pb = perf_buffer__new(bpf_map__fd(events_map), 64,
perf_event_handler, NULL, NULL, NULL);
if (!pb) {
fprintf(stderr, "Failed to create perf buffer\n");
return 1;
}
printf("Tracing openat() calls... Hit Ctrl-C to stop.\n");
// 4. 事件循环
while (1) {
err = perf_buffer__poll(pb, 100);
if (err < 0 && err != -EINTR) {
fprintf(stderr, "Error polling perf buffer: %d\n", err);
break;
}
}
perf_buffer__free(pb);
bpf_object__close(obj);
return 0;
}
Makefile
CLANG ?= clang
ARCH := $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/')
BPF_CFLAGS := -g -O2 -target bpf \
-D__TARGET_ARCH_$(ARCH) \
-I/usr/include/$(shell uname -m)-linux-gnu
.PHONY: all clean
all: hello.bpf.o hello
hello.bpf.o: src/hello.bpf.c
$(CLANG) $(BPF_CFLAGS) -c $< -o $@
hello: src/hello.c
gcc -O2 -o $@ $< -lbpf -lelf -lz
clean:
rm -f hello hello.bpf.o
编译与运行
# 生成 vmlinux.h(如果还没有)
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 编译
make
# 运行(需要 root 或 CAP_BPF)
sudo ./hello
# 输出示例:
# [3245 ] bash opened /etc/passwd
# [1234 ] nginx opened /var/www/html/index.html
# [5678 ] python3 opened /tmp/data.json
六、BPF Map:内核态与用户态的桥梁
BPF Map 是 eBPF 程序与用户态通信的核心机制。它本质上是内核中的一个键值存储。
| Map 类型 | 用途 | 典型场景 |
|---|---|---|
| HASH | 哈希表 | 统计计数、状态跟踪 |
| ARRAY | 数组 | 配置项、索引查找 |
| PERF_EVENT_ARRAY | Per-CPU 事件流 | 向用户态发送事件 |
| RINGBUF | 环形缓冲区 | 替代 PERF_EVENT_ARRAY,性能更优 |
| PERCPU_HASH | Per-CPU 哈希表 | 无锁高频更新 |
| LRU_HASH | 自动淘汰的哈希表 | 限制 Map 大小 |
| STACK_TRACE | 调用栈存储 | 火焰图数据 |
Map 操作示例
// 内核态
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10240);
__type(key, u32); // PID
__type(value, u64); // 计数
} pid_count SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_read")
int count_reads(void *ctx)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 *count = bpf_map_lookup_elem(&pid_count, &pid);
if (count) {
__sync_fetch_and_add(count, 1); // 原子递增
} else {
u64 init = 1;
bpf_map_update_elem(&pid_count, &pid, &init, BPF_ANY);
}
return 0;
}
Ringbuf vs Perf Event Array:新项目推荐使用
BPF_MAP_TYPE_RINGBUF。它支持变长记录、更少的内存拷贝、更好的数据保序。Perf Event Array 是旧方案,仍然可用但不再推荐。
七、实战场景:网络可观测性
1. TCP 连接延迟监控
# bpftrace 追踪 TCP 握手延迟
sudo bpftrace -e '
kprobe:tcp_v4_connect { @start[tid] = nsecs; }
kretprobe:tcp_v4_connect /@start[tid]/ {
$latency = nsecs - @start[tid];
printf("TCP connect PID %d latency: %d us\n", pid, $latency / 1000);
delete(@start, tid);
}'
2. XDP 包过滤(高性能 DDoS 防御)
// xdp_drop.c - XDP 程序丢弃特定 IP 的包
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10000);
__type(key, __u32); // 攻击者 IP
__type(value, __u8); // 占位
} blocklist SEC(".maps");
SEC("xdp")
int xdp_drop_attackers(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return XDP_PASS;
if (eth->h_proto != __constant_htons(ETH_P_IP))
return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end)
return XDP_PASS;
__u32 src_ip = ip->saddr;
if (bpf_map_lookup_elem(&blocklist, &src_ip))
return XDP_DROP; // 在网卡驱动层直接丢弃
return XDP_PASS;
}
char LICENSE[] SEC("license") = "GPL";
XDP 性能:XDP 在网卡驱动层执行,比传统 iptables 快 5-10 倍。Cloudflare 用 XDP 处理 L3/L4 DDoS 防御,单核可达 10M+ pps。
3. HTTP 请求追踪(无侵入 APM)
# 追踪 nginx 的 HTTP 请求处理时间
sudo bpftrace -e '
uprobe:/usr/sbin/nginx:ngx_http_process_request { @start[tid] = nsecs; }
uretprobe:/usr/sbin/nginx:ngx_http_process_request /@start[tid]/ {
$latency = (nsecs - @start[tid]) / 1000000;
printf("HTTP request: %d ms\n", $latency);
delete(@start, tid);
}'
八、实战场景:安全监控
1. 文件权限变更监控
# 追踪 chmod/fchmod 调用
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_fchmod {
printf("chmod by %s (PID %d): fd=%d mode=0%o\n",
comm, pid, args->fd, args->mode);
}'
2. 敏感文件读取监控
# 监控 /etc/shadow 被谁读取
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_openat
/str(args->filename) == "/etc/shadow"/ {
printf("ALERT: %s (PID %d) reading /etc/shadow\n", comm, pid);
}'
3. 进程执行审计
# 实时追踪所有命令执行
sudo bpftrace -e '
tracepoint:sched:sched_process_exec {
printf("EXEC: %s -> %s (UID %d)\n",
comm, str(args->filename), args->uid);
}'
九、云原生 eBPF 生态
以下是云原生场景中最常用的 eBPF 项目:
| 项目 | 领域 | 说明 |
|---|---|---|
| Cilium | 网络 + 安全 | K8s CNI,基于 eBPF 实现网络策略、可观测性、LB |
| Falco | 运行时安全 | 检测异常行为,新版本默认 eBPF 驱动 |
| Hubble | 可观测性 | Cilium 的网络可观测性组件,服务地图 + 流量指标 |
| Parca | 持续 Profiling | eBPF 驱动的全栈 CPU profiling |
| Pyroscope | 持续 Profiling | Grafana 旗下,eBPF 采集 + 火焰图展示 |
| Tetragon | 安全可观测性 | Cilium 团队出品,实时安全事件追踪 |
| Katran | 负载均衡 | Meta 开源,基于 XDP 的 L4 LB |
| Bumblebee | eBPF 打包分发 | 将 eBPF 程序打包为 OCI 镜像 |
2026 年趋势:eBPF 已成为云原生基础设施的标配。Cilium 是 Kubernetes 最流行的 CNI 之一,Falco + Tetragon 构成运行时安全双引擎,持续 Profiling 正在替代传统的采样式 APM。
十、CO-RE 与 BTF:一次编译,到处运行
传统 eBPF 程序需要针对每个内核版本单独编译,因为内核数据结构布局不同。CO-RE(Compile Once – Run Everywhere)解决了这个问题:
- 内核开启
CONFIG_DEBUG_INFO_BTF,在/sys/kernel/btf/vmlinux暴露 BTF(BPF Type Format)类型信息 - 编译时嵌入 BTF 重定位信息
- 加载时 libbpf 根据当前内核的 BTF 自动重定位字段偏移
# 检查内核是否支持 BTF
ls /sys/kernel/btf/vmlinux
# 生成 vmlinux.h
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 编译 CO-RE 程序(关键标志)
clang -g -O2 -target bpf \
-D__TARGET_ARCH_x86 \
-c hello.bpf.c -o hello.bpf.o
内核版本要求:BTF 从 Linux 5.2 开始支持,5.4+ 默认启用。如果你的生产环境内核低于 5.4,需要手动开启
CONFIG_DEBUG_INFO_BTF=y。
十一、调试与排错
常用调试命令
# 查看已加载的 BPF 程序
sudo bpftool prog list
# 查看程序详情(含字节码)
sudo bpftool prog show id 42
# 查看 BPF Map
sudo bpftool map list
sudo bpftool map dump id 10
# 查看挂载点
sudo bpftool perf list
# 查看内核支持的 tracepoint
sudo bpftool prog profile
# 验证器日志(加载失败时)
sudo cat /sys/kernel/debug/tracing/trace_pipe
常见错误排查
| 错误 | 原因 | 解决 |
|---|---|---|
| Invalid mem access | 越界读内存 | 加边界检查、用 bpf_probe_read |
| Unreachable instruction | 验证器无法证明程序会终止 | 消除无限循环、限制循环次数 |
| Map value size mismatch | Map 定义与使用不一致 | 检查 key/value 类型大小 |
| Cannot mount bpffs | Pinning Map 需要 bpffs | mount bpffs /sys/fs/bpf -t bpf |
| Operation not permitted | 权限不足 | 使用 root 或 CAP_BPF + CAP_PERFMON |
十二、最佳实践总结
- 优先使用 tracepoint:比 kprobe 更稳定,不会随内核版本变化
- 从 bpftrace 开始:快速验证假设,再决定是否写 C 程序
- 使用 libbpf + CO-RE:一次编译到处运行,不再为内核版本头疼
- 善用 BCC 工具:70+ 开箱即用工具,不要重复造轮子
- 注意验证器限制:指令数上限(100 万)、栈大小(512 字节)、循环次数
- Map 选择:高频更新用 PERCPU_HASH,事件传递用 RINGBUF
- 生产环境最小权限:CAP_BPF + CAP_PERFMON 替代 root
- 关注内核版本:5.4+ 是当前推荐的最低版本,5.10+ 支持更完整
# 检查内核 eBPF 特性支持
sudo bpftool feature probe
# 一键查看当前内核支持的 BPF 能力
sudo bpftool feature probe | grep "bpf_"
一句话总结:eBPF 是 Linux 内核的可编程层,用安全、低开销的方式把可观测性、网络和安全能力从内核模块迁移到了沙箱运行时。掌握 bpftrace + BCC + libbpf 三件套,足以覆盖 90% 的生产场景。