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)解决了这个问题:

  1. 内核开启 CONFIG_DEBUG_INFO_BTF,在 /sys/kernel/btf/vmlinux 暴露 BTF(BPF Type Format)类型信息
  2. 编译时嵌入 BTF 重定位信息
  3. 加载时 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

十二、最佳实践总结

  1. 优先使用 tracepoint:比 kprobe 更稳定,不会随内核版本变化
  2. 从 bpftrace 开始:快速验证假设,再决定是否写 C 程序
  3. 使用 libbpf + CO-RE:一次编译到处运行,不再为内核版本头疼
  4. 善用 BCC 工具:70+ 开箱即用工具,不要重复造轮子
  5. 注意验证器限制:指令数上限(100 万)、栈大小(512 字节)、循环次数
  6. Map 选择:高频更新用 PERCPU_HASH,事件传递用 RINGBUF
  7. 生产环境最小权限:CAP_BPF + CAP_PERFMON 替代 root
  8. 关注内核版本:5.4+ 是当前推荐的最低版本,5.10+ 支持更完整
# 检查内核 eBPF 特性支持
sudo bpftool feature probe

# 一键查看当前内核支持的 BPF 能力
sudo bpftool feature probe | grep "bpf_"
一句话总结:eBPF 是 Linux 内核的可编程层,用安全、低开销的方式把可观测性、网络和安全能力从内核模块迁移到了沙箱运行时。掌握 bpftrace + BCC + libbpf 三件套,足以覆盖 90% 的生产场景。