概述

eBPF(Extended Berkeley Packet Filter)是 Linux 内核领域近十年来最具革命性的技术之一。它允许开发者在不修改内核源代码、不加载内核模块的前提下,安全高效地在内核空间运行自定义程序。从网络包过滤到系统调用追踪、从性能分析到安全审计,eBPF 已经成为现代可观测性和网络数据面的基石。

我将从 eBPF 的基本原理出发,逐步深入到开发环境搭建、程序类型选择、BCC 与 libbpf+CO-RE 的工程对比,最后给出一个完整的生产级 eBPF 工具开发流程。适合有一定 Linux 内核基础的 SRE 工程师和系统开发者阅读。

一、eBPF 的演进:从 BPF 到 eBPF

1.1 经典 BPF(cBPF)

1992 年,Steven McCanne 和 Van Jacobson 在论文《The BSD Packet Filter: A New Architecture for User-level Capture》中提出了 BPF。其核心思想是将一个基于寄存器的虚拟机嵌入内核,允许用户将过滤程序注入内核空间,只有匹配的网络包才会被复制到用户态,从而大幅减少不必要的上下文切换开销。

经典 BPF 的局限性很明显:

特性cBPFeBPF
寄存器数量2 个 32 位11 个 64 位
指令集简单,仅支持包过滤丰富,支持函数调用、64 位运算
程序大小严格限制(4096 指令)百万级指令(经验证器检查)
挂载点仅网络包系统调用、内核函数、跟踪点、网络等
安全机制基本检查验证器 + JIT 编译

1.2 eBPF 的诞生

2014 年,Alexei Starovoitov 向 Linux 内核社区提交了 eBPF 补丁。这次重新设计带来了几个关键突破:

  • 11 个 64 位寄存器:r0 用作返回值,r1-r5 用于函数参数传递,r6-r9 在函数调用间保持,r10 是只读帧指针
  • 验证器(Verifier):所有 eBPF 程序在加载时必须通过验证器的严格审查,确保不存在无限循环、越界内存访问、未初始化寄存器使用等安全问题
  • JIT 编译器:验证通过后,eBPF 字节码会被 JIT 编译为本地机器码,执行效率接近原生
  • BPF Maps:内核与用户态之间的键值存储,支持多种数据结构(hash、array、ringbuf、perf buffer 等)

参考:BPF and XDP Reference Guide — Cilium 官方对 eBPF 架构的权威解释

1.3 eBPF 在内核中的执行流程

用户态程序
┌─────────────┐
│  编写 BPF C  │
│  源码        │
└──────┬──────┘
       │ clang -target bpf
┌─────────────┐
│  eBPF 字节码 │
│  (ELF 文件)  │
└──────┬──────┘
       │ bpf() 系统调用
┌─────────────────┐
│  内核验证器       │
│  - 控制流图分析   │
│  - 类型检查       │
│  - 安全性验证     │
└──────┬──────────┘
       │ 验证通过
┌─────────────┐
│  JIT 编译器  │
│  字节码→机器码│
└──────┬──────┘
┌─────────────────┐
│  内核事件挂载点    │
│  (kprobe/tracepoint│
│   /XDP/等)        │
└─────────────────┘

二、eBPF 程序类型与挂载点

eBPF 的强大之处在于它不局限于网络。通过不同的程序类型(Program Type)和挂载点(Hook Point),eBPF 可以附着在内核的各个关键路径上。

2.1 常见程序类型

程序类型挂载点典型场景内核版本要求
kprobe内核函数入口函数参数监控、调用统计4.1+
kretprobe内核函数返回函数执行耗时、返回值检查4.1+
tracepoint内核静态跟踪点系统事件追踪(调度、中断)4.7+
uprobe用户空间函数入口用户进程函数追踪4.14+
uretprobe用户空间函数返回用户函数耗时分析4.14+
XDP网卡驱动层高性能包处理、DDoS 防护4.8+
tc流量控制层网络分类、标记、重定向4.1+
perf_event性能事件CPU profiling、硬件计数器4.9+
LSMLinux 安全模块安全策略、访问控制5.7+
cgroup_skbcgroup 网络包容器网络监控4.10+

2.2 probe 类型选择指南

# kprobe:动态追踪内核函数入口
# 优势:可追踪任意内核函数,灵活
# 劣势:依赖内核符号,函数签名变更可能导致问题

# tracepoint:静态追踪点
# 优势:内核 ABI 稳定,不会因版本变更而失效
# 劣势:覆盖范围有限,只存在于内核预定义的位置

# 经验法则:优先使用 tracepoint,当 tracepoint 不覆盖时才用 kprobe

2.3 验证可用的 tracepoint

# 列出所有可用的 tracepoint
sudo ls /sys/kernel/debug/tracing/events/

# 查看特定子系统的 tracepoint
sudo ls /sys/kernel/debug/tracing/events/sched/
# common-data  sched_process_exec  sched_process_fork  sched_process_exit
# sched_switch  sched_waking        sched_wakeup_new    ...

# 查看某个 tracepoint 的参数格式
sudo cat /sys/kernel/debug/tracing/events/sched/sched_switch/format
# name: sched_switch
# ID: 325
# format:
#     field:char prev_comm[16];       offset:8;   size:16;  signed:0;
#     field:pid_t prev_pid;            offset:24;  size:4;   signed:1;
#     field:int   prev_prio;           offset:28;  size:4;   signed:1;
#     field:long  prev_state;          offset:32;  size:8;   signed:1;
#     ...

三、开发环境搭建

3.1 系统要求检查

# 检查内核版本(建议 5.4+)
uname -r
# 5.15.0-91-generic

# 检查 BTF 支持(CO-RE 的前提条件)
ls -la /sys/kernel/btf/vmlinux
# -r--r--r-- 1 root root 5283547 Jul 11 10:00 /sys/kernel/btf/vmlinux

# 检查 BPF 相关内核配置
zcat /proc/config.gz | grep CONFIG_DEBUG_INFO_BTF
# CONFIG_DEBUG_INFO_BTF=y

# 如果没有 /proc/config.gz,尝试:
grep CONFIG_DEBUG_INFO_BTF /boot/config-$(uname -r)

3.2 安装开发工具链

# Ubuntu / Debian
sudo apt-get update
sudo apt-get install -y \
    clang llvm \
    libbpf-dev \
    libelf-dev \
    zlib1g-dev \
    linux-tools-common \
    linux-tools-$(uname -r) \
    bpftool

# CentOS / RHEL / Rocky Linux
sudo dnf install -y \
    clang llvm \
    libbpf-devel \
    elfutils-libelf-devel \
    zlib-devel \
    bpftool

# 验证安装
clang --version       # 需要 clang 10+
llc --version
bpftool version

3.3 关键工具说明

工具用途来源
clang将 BPF C 源码编译为 eBPF 字节码LLVM
llcLLVM 后端编译器LLVM
bpftoolBPF 程序和 map 的运行时管理内核源码
libbpf用户态 BPF 加载库内核源码
paholeBTF 生成工具(编译内核时使用)dwarves 包

四、BCC vs libbpf+CO-RE:工程选型

4.1 BCC 架构

BCC(BPF Compiler Collection)是最早的 eBPF 开发框架,采用运行时即时编译模式:

BCC 工具运行流程:
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│ Python   │───▶│ 嵌入     │───▶│ Clang/LLVM│───▶│ 加载到   │
│ 脚本启动 │    │ BPF C    │    │ JIT 编译  │    │ 内核     │
└──────────┘    └──────────┘    └──────────┘    └──────────┘

BCC 的核心问题:

  1. 内存黑洞:每个 BCC 工具运行时需要携带完整的 LLVM/Clang 运行时,单个工具的内存占用通常在 80 MB 以上
  2. 依赖噩梦:目标机器必须安装 kernel-devel 包和 clang 编译器
  3. 冷启动延迟:每次运行都需要重新编译 BPF 程序,启动时间通常在 1-2 秒
  4. 版本陷阱:开发环境与生产环境内核版本差异容易导致编译失败

4.2 libbpf+CO-RE 架构

CO-RE(Compile Once - Run Everywhere)的核心思想是:在开发机器上编译一次 BPF 字节码,然后分发到不同内核版本的生产机器上运行。

libbpf+CO-RE 工具流程:
┌──────────┐    ┌──────────┐    ┌──────────┐
│ 开发机    │───▶│ 预编译    │───▶│ 分发到   │
│ clang编译 │    │ BPF 字节码│    │ 生产环境  │
└──────────┘    │ + BTF    │    └────┬─────┘
                └──────────┘         │
                              ┌──────────────┐
                              │ libbpf 加载   │
                              │ BTF 重定位    │
                              │ 字段偏移修正  │
                              └──────────────┘

4.3 性能对比

指标BCC 方案libbpf+CO-RE 方案提升幅度
内存占用80 MB+~9 MB89%↓
启动时间1.2s0.15s87%↓
部署依赖clang + kernel-devel仅需 libbpf + BTF显著减少
跨内核兼容性需要匹配内核头文件仅需 BTF 支持质的飞跃
二进制大小不适用(运行时编译)~2 MB可分发

数据来源:Cilium 社区和 BCC 迁移实践文档中的对比测试

4.4 选型建议

# 选 BCC 的场景:
# - 快速原型验证,不需要生产部署
# - 开发环境与生产环境内核版本完全一致
# - 团队对 Python 更熟悉
# - 临时排查问题,一次性使用

# 选 libbpf+CO-RE 的场景:
# - 需要生产环境长期部署
# - 需要跨多个内核版本运行
# - 对资源消耗敏感(容器、边缘设备)
# - 需要作为独立二进制分发
# - 构建运维工具链

五、BTF 机制详解

BTF(BPF Type Format)是 CO-RE 的基石。它是一种紧凑的类型描述格式,记录了内核中所有数据结构的布局信息。

5.1 BTF 的作用

┌──────────────────────────────────────────────────────┐
│  开发机(内核 5.15)                                    │
│  struct task_struct {                                  │
│      int   pid;          // offset 0x4                │
│      char  comm[16];      // offset 0x2c8              │
│      ...                                               │
│  }                                                    │
│                                                       │
│  clang 编译 → BPF 字节码引用了 task_struct 的字段      │
│            → 生成的 BPF 指令中嵌入了字段偏移量           │
└───────────────────────┬──────────────────────────────┘
                        │ 分发
┌──────────────────────────────────────────────────────┐
│  生产机(内核 6.1)                                     │
│  struct task_struct {                                  │
│      int   pid;          // offset 0x4  (未变)         │
│      ...                                               │
│      char  comm[16];      // offset 0x2d0 (变了!)      │
│      ...                                               │
│  }                                                    │
│                                                       │
│  libbpf 加载时读取 /sys/kernel/btf/vmlinux             │
│  → 获取内核 6.1 中 task_struct 的真实布局               │
│  → 自动重定位 comm 字段的偏移量从 0x2c8 → 0x2d0        │
│  → BPF 程序在新内核上正确运行                           │
└──────────────────────────────────────────────────────┘

5.2 查看 BTF 信息

# 查看内核 BTF 文件大小
ls -lh /sys/kernel/btf/vmlinux
# -r--r--r-- 1 root root 5.1M Jul 11 10:00 /sys/kernel/btf/vmlinux

# 使用 bpftool 查看某个结构体的 BTF 定义
bpftool btf dump file /sys/kernel/btf/vmlinux format c \
    | grep -A 20 "struct task_struct"

# 查看 BTF 中的函数签名
bpftool btf dump file /sys/kernel/btf/vmlinux format c \
    | grep "FUNC.*do_unlinkat"

5.3 旧内核启用 BTF

对于 5.2 以下不支持 BTF 的内核,可以重新编译内核开启 BTF 支持:

# 在内核配置中开启
CONFIG_DEBUG_INFO_BTF=y

# 需要安装 pahole 工具来生成 BTF
sudo apt-get install dwarves
# 或
sudo dnf install dwarves

# 重新编译内核
make olddefconfig
make -j$(nproc)
sudo make modules_install
sudo make install
sudo reboot

六、libbpf 实战开发

6.1 项目结构

my-ebpf-tool/
├── Makefile
├── src/
   ├── bpf/
      └── exec_monitor.bpf.c    # 内核态 BPF 程序
   └── user/
       └── exec_monitor.c        # 用户态加载器
├── include/
   └── exec_monitor.h           # 共享头文件
└── vmlinux.h                    # 内核类型定义(由 bpftool 生成)

6.2 生成 vmlinux.h

# 从当前内核的 BTF 生成 vmlinux.h
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

# vmlinux.h 包含了内核中所有数据结构的定义
# CO-RE 程序不再需要 #include <linux/sched.h> 等头文件
# 而是直接 #include "vmlinux.h"
wc -l vmlinux.h
# 140000+ vmlinux.h

6.3 内核态 BPF 程序

// exec_monitor.bpf.c
// 监控所有 execve 系统调用,记录执行的命令

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "exec_monitor.h"

// 定义 BPF Map:环形缓冲区,用于向用户态传递事件
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);  // 256 KB
} events SEC(".maps");

// 定义 BPF Map:配置表,控制是否启用监控
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 1);
    __type(key, u32);
    __type(value, u32);
} config SEC(".maps");

// tracepoint: syscalls:sys_enter_execve
// 监控进程创建事件
SEC("tracepoint/syscalls/sys_enter_execve")
int trace_execve(struct trace_event_raw_sys_enter *ctx)
{
    // 读取配置:检查是否启用
    u32 key = 0;
    u32 *enabled = bpf_map_lookup_elem(&config, &key);
    if (!enabled || !*enabled) {
        return 0;
    }

    // 获取当前进程信息
    struct task_struct *task = (struct task_struct *)bpf_get_current_task();

    // 分配环形缓冲区中的事件空间
    struct event *e;
    e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) {
        return 0;
    }

    // 填充事件数据
    e->pid = bpf_get_current_pid_tgid() >> 32;
    e->ppid = BPF_CORE_READ(task, real_parent, tgid);
    bpf_get_current_comm(&e->comm, sizeof(e->comm));

    // 读取 execve 的 filename 参数(第一个参数)
    const char *filename = (const char *)BPF_CORE_READ(ctx, args[0]);
    bpf_probe_read_user_str(e->filename, sizeof(e->filename), filename);

    // 记录时间戳
    e->timestamp = bpf_ktime_get_ns();

    // 提交事件到环形缓冲区
    bpf_ringbuf_submit(e, 0);

    return 0;
}

// 定义 license,必须为 GPL 才能使用部分内核 helper 函数
char LICENSE[] SEC("license") = "GPL";

6.4 共享头文件

// exec_monitor.h
#ifndef __EXEC_MONITOR_H
#define __EXEC_MONITOR_H

// 事件结构体,内核态和用户态共享
struct event {
    u32 pid;            // 进程 ID
    u32 ppid;           // 父进程 ID
    char comm[16];      // 进程名
    char filename[256]; // 执行的命令路径
    u64 timestamp;      // 内核时间戳(纳秒)
};

#endif

6.5 用户态加载器

// exec_monitor.c
// 用户态程序:加载 BPF 程序、读取事件、输出结果

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include "exec_monitor.h"
#include "exec_monitor.skel.h"  // 由 skeleton 生成

static volatile bool exiting = false;

// 信号处理
static void sig_handler(int sig)
{
    exiting = true;
}

// 环形缓冲区事件回调
static int handle_event(void *ctx, void *data, size_t data_sz)
{
    struct event *e = data;
    time_t ts = e->timestamp / 1000000000ULL;
    struct tm *tm = localtime(&ts);
    char time_buf[32];

    strftime(time_buf, sizeof(time_buf), "%H:%M:%S", tm);

    printf("%-8s %-7d %-7d %-16s %s\n",
           time_buf, e->pid, e->ppid, e->comm, e->filename);

    return 0;
}

int main(int argc, char **argv)
{
    struct exec_monitor_bpf *skel;
    struct ring_buffer *rb;
    int err;

    // 注册信号处理
    signal(SIGINT, sig_handler);
    signal(SIGTERM, sig_handler);

    // 1. 打开并加载 BPF skeleton
    skel = exec_monitor_bpf__open();
    if (!skel) {
        fprintf(stderr, "Failed to open BPF skeleton\n");
        return 1;
    }

    err = exec_monitor_bpf__load(skel);
    if (err) {
        fprintf(stderr, "Failed to load BPF skeleton: %d\n", err);
        goto cleanup;
    }

    // 2. 附加 BPF 程序
    err = exec_monitor_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "Failed to attach BPF program: %d\n", err);
        goto cleanup;
    }

    // 3. 启用监控(设置 config map)
    u32 key = 0, val = 1;
    bpf_map_update_elem(bpf_map__fd(skel->maps.config), &key, &val, BPF_ANY);

    // 4. 设置环形缓冲区
    rb = ring_buffer__new(bpf_map__fd(skel->maps.events),
                          handle_event, NULL, NULL);
    if (!rb) {
        fprintf(stderr, "Failed to create ring buffer\n");
        goto cleanup;
    }

    // 5. 事件循环
    printf("%-8s %-7s %-7s %-16s %s\n",
           "TIME", "PID", "PPID", "COMM", "FILENAME");
    printf("------------------------------------------------------------\n");

    while (!exiting) {
        // 轮询环形缓冲区,超时 300ms
        err = ring_buffer__poll(rb, 300);
        if (err == -EINTR) {
            break;
        }
        if (err < 0) {
            fprintf(stderr, "Error polling ring buffer: %d\n", err);
            break;
        }
    }

    ring_buffer__free(rb);

cleanup:
    exec_monitor_bpf__destroy(skel);
    return err != 0;
}

6.6 Makefile

# Makefile for exec_monitor eBPF tool

CLANG ?= clang
BPFTOOL ?= bpftool
ARCH := $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/')

BPF_SRC := src/bpf/exec_monitor.bpf.c
USER_SRC := src/user/exec_monitor.c
TARGET := exec_monitor

# 编译 BPF 程序为字节码
$(BPF_SRC:.bpf.c=.o): $(BPF_SRC) vmlinux.h
	$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) \
		-Iinclude -I/usr/include/$(shell uname -m)-linux-gnu \
		-c $< -o $@

# 生成 skeleton 头文件
exec_monitor.skel.h: $(BPF_SRC:.bpf.c=.o)
	$(BPFTOOL) gen skeleton $< > $@

# 编译用户态程序
$(TARGET): $(USER_SRC) exec_monitor.skel.h
	$(CC) -g -O2 -Wall \
		-Iinclude -I/usr/include/$(shell uname -m)-linux-gnu \
		$< -o $@ -lbpf -lelf -lz

# 生成 vmlinux.h
vmlinux.h:
	$(BPFTOOL) btf dump file /sys/kernel/btf/vmlinux format c > $@

clean:
	rm -f *.o src/bpf/*.o *.skel.h $(TARGET) vmlinux.h

.PHONY: clean

6.7 构建与运行

# 生成 vmlinux.h
make vmlinux.h

# 编译
make

# 运行(需要 root 权限)
sudo ./exec_monitor

# 输出示例:
# TIME      PID     PPID    COMM             FILENAME
# ------------------------------------------------------------
# 10:15:23  12345   1       bash             /bin/ls
# 10:15:23  12346   12345   ls               /usr/bin/dircolors
# 10:15:24  12347   1       systemd           /usr/lib/systemd/systemd-journal
# 10:15:25  12348   1000    bash             /usr/bin/git

七、使用 Go 和 cilium/ebpf 开发

对于 SRE 团队来说,Go 是构建运维工具的常用语言。cilium/ebpf 库提供了纯 Go 的 eBPF 开发体验,无需 CGO。

7.1 项目初始化

mkdir go-ebpf-tool && cd go-ebpf-tool
go mod init github.com/example/go-ebpf-tool

# 添加 cilium/ebpf 依赖
go get github.com/cilium/ebpf@latest

# 安装 bpf2go 工具
go install github.com/cilium/ebpf/cmd/bpf2go@latest

7.2 BPF 程序(共用同一个 .bpf.c)

# 将之前的 exec_monitor.bpf.c 复制到项目目录
mkdir -p bpf
cp ../src/bpf/exec_monitor.bpf.c bpf/
cp ../include/exec_monitor.h bpf/

7.3 Go 主程序

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go \
//    -cc clang \
//    -cflags "-O2 -g -Wall -Werror" \
//    bpf ./bpf/exec_monitor.bpf.c -- -I./bpf

package main

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/ringbuf"
    "github.com/cilium/ebpf/rlimit"
)

// Event 结构体,与 BPF 程序中的定义对应
type Event struct {
    Pid      uint32
    Ppid     uint32
    Comm     [16]byte
    Filename [256]byte
    Timestamp uint64
}

func main() {
    // 1. 取消内存限制
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatalf("Failed to remove memlock: %v", err)
    }

    // 2. 加载 BPF 程序
    var objs bpfObjects
    if err := loadBpfObjects(&objs, nil); err != nil {
        log.Fatalf("Failed to load BPF objects: %v", err)
    }
    defer objs.Close()

    // 3. 附加到 execve tracepoint
    tp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.TraceExecve, nil)
    if err != nil {
        log.Fatalf("Failed to attach tracepoint: %v", err)
    }
    defer tp.Close()

    // 4. 启用监控
    key := uint32(0)
    val := uint32(1)
    if err := objs.Config.Update(key, val, 0); err != nil {
        log.Fatalf("Failed to enable monitoring: %v", err)
    }

    // 5. 读取环形缓冲区
    rd, err := ringbuf.NewReader(objs.Events)
    if err != nil {
        log.Fatalf("Failed to create ringbuf reader: %v", err)
    }
    defer rd.Close()

    // 6. 信号处理
    stop := make(chan os.Signal, 1)
    signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)

    fmt.Printf("%-8s %-7s %-7s %-16s %s\n",
        "TIME", "PID", "PPID", "COMM", "FILENAME")
    fmt.Println("------------------------------------------------------------")

    // 7. 事件循环
    go func() {
        <-stop
        rd.Close()
    }()

    for {
        record, err := rd.Read()
        if err != nil {
            if err == ringbuf.ErrClosed {
                break
            }
            log.Printf("Read error: %v", err)
            continue
        }

        var event Event
        if err := binary.Read(bytes.NewReader(record.RawSample),
            binary.LittleEndian, &event); err != nil {
            log.Printf("Parse error: %v", err)
            continue
        }

        ts := time.Unix(0, int64(event.Timestamp))
        comm := bytes.TrimZeros(event.Comm[:])
        filename := bytes.TrimZeros(event.Filename[:])

        fmt.Printf("%-8s %-7d %-7d %-16s %s\n",
            ts.Format("15:04:05"),
            event.Pid, event.Ppid,
            string(comm), string(filename))
    }

    fmt.Println("\nMonitor stopped.")
}

7.4 构建与运行

# 生成 Go 绑定代码
go generate

# 编译
go build -o exec-monitor

# 运行
sudo ./exec-monitor

八、生产环境实践建议

8.1 验证器错误排查

eBPF 验证器是程序加载的第一道关卡。常见的验证器错误及解决方案:

# 查看验证器日志
sudo cat /sys/kernel/debug/tracing/trace_pipe

# 常见错误 1:程序过大
# "BPF program is too large. Processed 1000001 insn"
# 解决:拆分为多个 BPF 程序,减少分支复杂度

# 常见错误 2:未初始化寄存器
# "invalid read from stack off -8 size 1"
# 解决:确保所有变量在使用前初始化

# 常见错误 3:越界访问
# "invalid map access"
# 解决:使用 bpf_probe_read_kernel 安全读取

# 常见错误 4:无限循环
# "infinite loop detected"
# 解决:使用 bpf_for_each / bpf_for 循环辅助宏

8.2 性能优化要点

优化项方法效果
事件过滤在 BPF 程序内尽早过滤,减少 ringbuf 写入降低 CPU 和内存开销
Ring Buffer 大小根据事件频率调整 max_entries平衡丢包和内存
Map 类型选择高频更新用 PERCPU 类型避免锁竞争减少 CPU 争用
采样率高频事件使用采样而非全量采集降低整体开销
内联函数使用 __always_inline减少函数调用开销

8.3 安全注意事项

# 1. 限制 BPF 程序权限
# 使用 capabilities 而非全 root 运行
sudo setcap cap_bpf,cap_perfmon+ep ./exec-monitor

# 2. 签名验证(内核 5.15+)
# 开启 CONFIG_BPF_UNPRIV_DEFAULT_OFF=y
# 限制非特权用户加载 BPF 程序
echo 2 | sudo tee /proc/sys/kernel/unprivileged_bpf_disabled

# 3. 资源限制
# 限制 BPF 程序的指令数、map 大小等
# 通过 ulimit 和 cgroup 控制
ulimit -l 8192  # 限制锁定内存

# 4. 审计日志
# 开启 BPF 相关的 audit 日志
sudo auditctl -a always,exit -F arch=b64 -S bpf -k bpf_audit

8.4 容器环境部署

# Dockerfile for eBPF tool
FROM ubuntu:22.04

RUN apt-get update && apt-get install -y --no-install-recommends \
    libbpf0 \
    && rm -rf /var/lib/apt/lists/*

COPY exec-monitor /usr/local/bin/exec-monitor

# 容器运行 eBPF 程序需要特权或特定 capabilities
# docker run --privileged -v /sys/kernel/debug:/sys/kernel/debug ...
# 或更安全的做法:
# docker run --cap-add CAP_BPF --cap-add CAP_PERFMON ...
# Kubernetes DaemonSet 部署示例
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: ebpf-monitor
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: ebpf-monitor
  template:
    metadata:
      labels:
        app: ebpf-monitor
    spec:
      hostPID: true
      containers:
      - name: monitor
        image: registry.example.com/ebpf-monitor:latest
        securityContext:
          privileged: true
          capabilities:
            add:
            - CAP_BPF
            - CAP_PERFMON
            - CAP_SYS_RESOURCE
        volumeMounts:
        - name: debugfs
          mountPath: /sys/kernel/debug
        - name: btf
          mountPath: /sys/kernel/btf
          readOnly: true
        resources:
          requests:
            memory: "16Mi"
            cpu: "50m"
          limits:
            memory: "64Mi"
            cpu: "200m"
      volumes:
      - name: debugfs
        hostPath:
          path: /sys/kernel/debug
          type: Directory
      - name: btf
        hostPath:
          path: /sys/kernel/btf
          type: Directory

九、常用 eBPF 工具速查

工具功能来源
bpftrace高级追踪语言,一行命令追踪bpftrace.org
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s called execve\n", comm); }'快速追踪 execvebpftrace
bpftool prog show查看已加载的 BPF 程序内核源码
bpftool map show查看已加载的 BPF Maps内核源码
bpftool prog profile对 BPF 程序进行性能分析内核源码
llvm-objdump -d exec_monitor.o反汇编 BPF 字节码LLVM

常用 bpftrace 一行命令

# 统计系统调用频率(按进程分组)
bpftrace -e 'tracepoint:syscalls:sys_enter_* { @[comm] = count(); }'
# 按 Ctrl+C 输出统计结果

# 追踪进程创建
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(args->filename)); }'

# 统计磁盘 I/O 延迟分布
bpftrace -e 'tracepoint:block:block_rq_issue { @start[arg.dev] = nsecs; } tracepoint:block:block_rq_complete /@start[arg.dev]/ { @usecs = hist((nsecs - @start[arg.dev]) / 1000); delete(@start[arg.dev]); }'

# 追踪 TCP 连接建立
bpftrace -e 'kprobe:tcp_v4_connect { printf("PID %d (%s) connecting\n", pid, comm); }'

# 统计函数调用次数
bpftrace -e 'kprobe:vfs_read { @[func] = count(); }'

十、eBPF 生态与社区资源

项目描述GitHub
Cilium基于 eBPF 的网络、安全和可观测性平台cilium/cilium
bccBPF Compiler Collection 工具集iovisor/bcc
bpftrace高级追踪语言iovisor/bpftrace
libbpf官方 BPF 库libbpf/libbpf
cilium/ebpfGo 语言 eBPF 库cilium/ebpf
Pixie基于 eBPF 的 Kubernetes 可观测性pixie-io/pixie
Inspektor GadgetKubernetes 上的 eBPF 工具集inspektor-gadget/inspektor-gadget
KatranFacebook 的 L4 负载均衡器facebookincubator/katran

参考书目:Brendan Gregg《BPF Performance Tools》是 eBPF 性能分析领域的权威著作,详细介绍了如何使用 eBPF 工具进行系统性能分析。

总结

eBPF 已经从一个小众的内核技术发展为现代基础设施的关键组件。掌握 eBPF 开发能力,对于 SRE 工程师和系统开发者来说是一项高价值技能。

核心要点回顾:

  1. 架构选择:生产环境优先使用 libbpf+CO-RE,BCC 仅用于快速原型和临时排查
  2. BTF 是关键:确认内核支持 BTF(5.2+),这是 CO-RE 跨内核运行的前提
  3. 程序类型:优先使用 tracepoint(稳定 ABI),需要灵活追踪时才用 kprobe
  4. 安全第一:限制非特权 BPF 加载、使用 capabilities 而非全 root、开启审计日志
  5. 性能意识:在 BPF 程序内尽早过滤事件、合理设置 ringbuf 大小、使用 PERCPU map 避免锁竞争
  6. 工具链:bpftrace 用于快速排查,libbpf/cilium-ebpf 用于构建生产级工具
  7. 容器部署:K8s 环境下通过 DaemonSet + hostPath 挂载 BTF 和 debugfs,使用 CAP_BPF 替代 privileged

eBPF 生态仍在快速发展,建议持续关注 Cilium、bpftrace 和 libbpf 项目的更新。下一步可以深入学习 XDP 高性能网络处理、eBPF LSM 安全策略、以及使用 eBPF 构建 Service Mesh 数据面等进阶主题。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. BPF and XDP Reference Guide — Cilium 项目,参考了BPF and XDP Reference Guide相关内容
  2. bpftrace.org — bpftrace 社区,参考了bpftrace.org相关内容
  3. cilium/cilium — GitHub 开源社区,参考了cilium/cilium相关内容
  4. iovisor/bcc — GitHub 开源社区,参考了iovisor/bcc相关内容
  5. iovisor/bpftrace — GitHub 开源社区,参考了iovisor/bpftrace相关内容
  6. libbpf/libbpf — GitHub 开源社区,参考了libbpf/libbpf相关内容
  7. cilium/ebpf — GitHub 开源社区,参考了cilium/ebpf相关内容
  8. pixie-io/pixie — GitHub 开源社区,参考了pixie-io/pixie相关内容
  9. inspektor-gadget/inspektor-gadget — GitHub 开源社区,参考了inspektor-gadget/inspektor-gadget相关内容
  10. facebookincubator/katran — GitHub 开源社区,参考了facebookincubator/katran相关内容