当你需要在内核中添加新功能时,传统做法只有两条路:修改内核源码并重新编译,或者编写内核模块(LKM)。前者周期漫长——一个补丁从提交到合入主线可能需要数年;后者风险极高——一个有 Bug 的内核模块可以让整个系统崩溃。eBPF 提供了第三条路:安全地在内核中运行自定义逻辑,无需修改内核源码,无需加载内核模块。
这不是渐进式改进,而是范式转换。eBPF 正在重新定义人与内核交互的方式——从网络数据包处理到安全策略执行,从性能分析到分布式追踪,eBPF 的触角已经延伸到内核的每一个角落。
一、从 BPF 到 eBPF:一段跨越二十年的进化史
1.1 古典 BPF:数据包过滤的利器
1992 年,Steven McCanne 和 Van Jacobson 在论文《The BSD Packet Filter: A New Architecture for User-Level Packet Capture》中提出了 BPF(Berkeley Packet Filter)。当时的痛点是:tcpdump 等抓包工具使用 PF_NIT 方案,每个数据包都要从内核完整拷贝到用户空间再过滤,效率极低。
BPF 的核心创新是将过滤器下推到内核——在数据包从内核拷贝到用户空间之前,先在内核中执行 BPF 程序进行过滤,只有匹配的数据包才会被传递到用户空间:
古典 BPF 的设计精巧而克制:
- 寄存器模型:2 个 32 位寄存器(A 累加器、X 索引寄存器)
- 指令集:约 20 条指令,涵盖算术、跳转、数据包访问
- 安全保证:结构化程序(无任意跳转)、有界执行(最大 4096 条指令)
1.2 eBPF 的诞生:从过滤器到可编程引擎
2014 年,Alexei Starovoitov 提出了 eBPF(extended BPF),将 BPF 从一个简单的包过滤器扩展为通用的内核可编程引擎。关键扩展包括:
| 特性 | 古典 BPF | eBPF |
|---|---|---|
| 寄存器数量 | 2 个(A、X) | 10 个(r0-r9 + r10 帧指针) |
| 寄存器宽度 | 32 位 | 64 位 |
| 最大指令数 | 4096 | 100 万(5.2+) |
| Map 支持 | 无 | Hash、Array、Ring Buffer 等 20+ 种 |
| Helper 函数 | 无 | 200+ 个内核 Helper |
| 程序类型 | 仅包过滤 | 30+ 种(网络、追踪、安全等) |
| JIT 编译 | 部分架构 | 全架构支持 |
| 尾调用 | 无 | 支持(最多 8 层) |
| BTF/CO-RE | 无 | 完整支持 |
eBPF 中的 “e” 代表 “extended”,但今天的 eBPF 已经远远超越了 “扩展” 的范畴——它是一个全新的内核可编程子系统。在社区中,“BPF” 和 “eBPF” 通常互换使用,内核代码中统一使用 “BPF”。
1.3 eBPF 的设计哲学
eBPF 的设计遵循三个核心原则:
- 安全第一:验证器在加载时静态分析程序,确保不会崩溃内核
- 高性能:JIT 编译为本地机器码,接近原生执行速度
- 可观测:通过 Map 与用户态高效交互,不污染内核代码
二、eBPF 架构总览
2.1 整体架构
eBPF 系统由三个核心部分组成:eBPF 程序(运行在内核态)、Map(内核态与用户态的共享数据结构)、用户态加载器(负责编译、加载、管理 eBPF 程序)。
2.2 eBPF 程序的生命周期
一个 eBPF 程序从编写到运行经历以下阶段:
- 编写:使用 C(或 Rust)编写 eBPF 程序源码
- 编译:通过 clang/LLVM 编译为 eBPF 字节码(.o 文件)
- 加载:用户态加载器通过
bpf()系统调用将字节码提交给内核 - 验证:内核验证器对字节码进行静态分析,确保安全性
- JIT 编译:验证通过后,JIT 编译器将字节码编译为本机机器码
- 挂载:将程序附加到指定的 Hook 点
- 执行:Hook 点触发时,执行 eBPF 程序
- 卸载:用户态加载器卸载程序,释放资源
// bpf() 系统调用的核心逻辑(简化)SYSCALL_DEFINE5(bpf, int, cmd, union bpf_attr *, attr, unsigned int, size){ switch (cmd) { case BPF_PROG_LOAD: // 1. 验证 eBPF 字节码 err = bpf_prog_load(attr, &prog); // 2. JIT 编译 bpf_prog_select_runtime(prog, &err); // 3. 返回程序 fd return prog->fd; case BPF_MAP_CREATE: return bpf_map_create(attr); case BPF_PROG_ATTACH: return bpf_prog_attach(attr); // ... 更多命令 }}三、eBPF 程序类型
eBPF 支持的程序类型决定了程序可以挂载的 Hook 点、可以访问的上下文数据、可以调用的 Helper 函数。截至 Linux 6.x,已有 30+ 种程序类型:
3.1 主要程序类型分类
| 类别 | 程序类型 | 典型用途 | 首个内核版本 |
|---|---|---|---|
| 网络 | BPF_PROG_TYPE_XDP | 驱动层数据包处理 | 4.8 |
| 网络 | BPF_PROG_TYPE_SCHED_CLS | TC 流量控制 | 4.1 |
| 网络 | BPF_PROG_TYPE_SOCKET_FILTER | Socket 过滤 | 4.4 |
| 网络 | BPF_PROG_TYPE_SOCK_OPS | Socket 操作 | 4.13 |
| 网络 | BPF_PROG_TYPE_SK_SKB | Socket 数据转发 | 4.14 |
| 追踪 | BPF_PROG_TYPE_KPROBE | 内核函数追踪 | 4.1 |
| 追踪 | BPF_PROG_TYPE_TRACEPOINT | 静态追踪点 | 4.7 |
| 追踪 | BPF_PROG_TYPE_PERF_EVENT | 性能计数器 | 4.9 |
| 安全 | BPF_PROG_TYPE_LSM | Linux 安全模块 | 5.7 |
| 安全 | BPF_PROG_TYPE_CGROUP_SKB | Cgroup 网络控制 | 4.10 |
| Cgroup | BPF_PROG_TYPE_CGROUP_DEVICE | 设备访问控制 | 4.15 |
| Cgroup | BPF_PROG_TYPE_CGROUP_SOCK | Socket 创建控制 | 4.10 |
3.2 程序类型决定了什么
不同程序类型有三个关键差异:
选择错误的程序类型会导致加载失败。例如,在 BPF_PROG_TYPE_XDP 类型的程序中调用 bpf_skb_store_bytes() 会被验证器拒绝——因为 XDP 层还没有 sk_buff 结构。程序类型与 Helper 函数的对应关系在内核源码 kernel/bpf/verifier.c 中定义。
四、eBPF Map 概览
Map 是 eBPF 程序与用户态(以及其他 eBPF 程序)之间共享数据的核心机制。它本质上是一个内核中的键值存储,支持多种数据结构:
4.1 主要 Map 类型
| Map 类型 | 特点 | 典型用途 |
|---|---|---|
| BPF_MAP_TYPE_HASH | 通用哈希表 | 连接跟踪表、进程信息 |
| BPF_MAP_TYPE_ARRAY | 固定大小数组 | 配置项、全局计数器 |
| BPF_MAP_TYPE_PERCPU_HASH | 每 CPU 哈希表 | 高频更新统计,无锁 |
| BPF_MAP_TYPE_PERCPU_ARRAY | 每 CPU 数组 | 每 CPU 计数器 |
| BPF_MAP_TYPE_RINGBUF | 环形缓冲区 | 高效事件传输到用户态 |
| BPF_MAP_TYPE_STACK_TRACE | 调用栈存储 | 性能分析、火焰图 |
| BPF_MAP_TYPE_LRU_HASH | LRU 淘汰哈希 | 有容量限制的缓存 |
| BPF_MAP_TYPE_SOCKHASH | Socket 哈希 | Socket 重定向 |
| BPF_MAP_TYPE_PROG_ARRAY | 程序数组 | 尾调用跳转表 |
| BPF_MAP_TYPE_PERF_EVENT_ARRAY | 性能事件数组 | 追踪数据传输 |
4.2 Map 的操作方式
// 内核态 eBPF 程序中操作 Mapstruct bpf_map_def SEC("maps") my_map = { .type = BPF_MAP_TYPE_HASH, .key_size = sizeof(u32), .value_size = sizeof(u64), .max_entries = 1024,};
SEC("kprobe/do_unlinkat")int bpf_prog(void *ctx){ u32 key = 1; u64 value = 42;
// 查找 u64 *val = bpf_map_lookup_elem(&my_map, &key); if (val) { // 更新 __sync_fetch_and_add(val, 1); } else { // 插入 bpf_map_update_elem(&my_map, &key, &value, BPF_ANY); }
// 删除 bpf_map_delete_elem(&my_map, &key);
return 0;}// 用户态操作 Map(通过 bpf() 系统调用)int map_fd = bpf_map_get_fd_by_id(map_id);
// 查找__u64 value;__u32 key = 1;bpf_map_lookup_elem(map_fd, &key, &value);
// 更新value = 100;bpf_map_update_elem(map_fd, &key, &value, BPF_ANY);
// 遍历while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) { bpf_map_lookup_elem(map_fd, &next_key, &value); printf("key=%u value=%lu\n", next_key, value); key = next_key;}五、eBPF 开发工具链
5.1 工具链全景
| 工具 | 用途 | 特点 |
|---|---|---|
| clang/LLVM | 编译 eBPF 字节码 | 后端支持 bpf target |
| bpftool | 程序/Map 管理 | 内核官方工具 |
| libbpf | C 语言开发库 | CO-RE 基石 |
| bpftrace | 高级追踪语言 | 一行命令追踪内核 |
| BCC | Python/Lua 工具集 | 丰富的现成工具 |
| bpf2go | Go 代码生成 | cilium/ebpf 配套 |
| Aya | Rust eBPF 框架 | 纯 Rust,无 libbpf 依赖 |
5.2 开发流程对比
BCC 方式在目标机器上需要安装 clang 和内核头文件,且每次运行时重新编译,启动慢、依赖重。libbpf CO-RE 方式预编译字节码,运行时只需 libbpf 库,是当前推荐的开发方式。详见第 6 章:CO-RE。
六、Hello World:你的第一个 eBPF 程序
6.1 使用 bpftrace(最简方式)
# 一行命令追踪 execve 系统调用sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s called execve\n", comm); }'
# 输出示例:# bash called execve# ls called execve# ps called execve6.2 使用 libbpf(生产级方式)
eBPF 程序(hello.bpf.c):
#include <linux/bpf.h>#include <bpf/bpf_helpers.h>
/* 定义 Map:传递事件数据到用户态 */struct event { u32 pid; char comm[16];};
struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); /* 256 KB */} events SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_execve")int hello_execve(struct trace_event_raw_sys_enter *ctx){ struct event *e;
/* 从 Ring Buffer 预留空间 */ e = bpf_ringbuf_reserve(&events, sizeof(*e), 0); if (!e) return 0;
/* 填充事件数据 */ e->pid = bpf_get_current_pid_tgid() >> 32; bpf_get_current_comm(&e->comm, sizeof(e->comm));
/* 提交事件 */ bpf_ringbuf_submit(e, 0);
return 0;}
char LICENSE[] SEC("license") = "GPL";用户态加载器(hello.c):
#include <stdio.h>#include <stdlib.h>#include <signal.h>#include <unistd.h>#include <bpf/libbpf.h>#include <bpf/bpf.h>#include "hello.skel.h" /* libbpf 骨架自动生成 */
static volatile bool exiting = false;
static void sig_handler(int sig) { exiting = true; }
/* Ring Buffer 事件回调 */static int handle_event(void *ctx, void *data, size_t len){ struct event { unsigned int pid; char comm[16]; } *e = data;
printf("%-8u %-16s\n", e->pid, e->comm); return 0;}
int main(int argc, char **argv){ struct hello_bpf *skel; struct ring_buffer *rb; int err;
signal(SIGINT, sig_handler); signal(SIGTERM, sig_handler);
/* 1. 打开并加载 eBPF 程序 */ skel = hello_bpf__open_and_load(); if (!skel) { fprintf(stderr, "Failed to open and load BPF skeleton\n"); return 1; }
/* 2. 附加到 tracepoint */ err = hello_bpf__attach(skel); if (err) { fprintf(stderr, "Failed to attach BPF program\n"); goto cleanup; }
/* 3. 设置 Ring Buffer 轮询 */ 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; }
printf("%-8s %-16s\n", "PID", "COMM");
/* 4. 事件循环 */ while (!exiting) { err = ring_buffer__poll(rb, 100); if (err < 0 && err != -EINTR) { fprintf(stderr, "Error polling ring buffer: %d\n", err); break; } }
ring_buffer__free(rb);
cleanup: hello_bpf__destroy(skel); return err != 0;}6.3 编译与运行
# 生成骨架头文件bpftool gen skeleton hello.bpf.o > hello.skel.h
# 编译用户态程序gcc -o hello hello.c -lbpf -lelf -lz
# 运行sudo ./hello# PID COMM# 28451 bash# 28452 ls# 28453 ps七、eBPF 与内核模块的对比
理解 eBPF 的定位,最好的方式是将其与内核模块(LKM)对比:
| 维度 | 内核模块 (LKM) | eBPF |
|---|---|---|
| 安全性 | 可崩溃内核 | 验证器保证安全 |
| 开发门槛 | 需要内核开发经验 | C 语言基础即可 |
| 部署方式 | insmod/modprobe | bpf() 系统调用 |
| 热更新 | 需卸载再加载 | 原子替换 |
| 权限要求 | root / CAP_SYS_MODULE | root / CAP_BPF (5.8+) |
| 调试难度 | kgdb / printk | bpftool / bpf_trace_printk |
| 性能 | 原生 | JIT 后接近原生 |
| 灵活性 | 可做任何事 | 受限于 Helper 和验证器 |
| 可移植性 | 需针对内核版本编译 | CO-RE 一次编译到处运行 |
eBPF 不是内核模块的替代品,而是互补。当你需要的功能超出了 eBPF 的能力范围(如添加新的系统调用、修改内核数据结构),仍然需要内核模块。eBPF 的价值在于:在大多数场景下,你不需要内核模块就能实现内核级功能。
八、eBPF 的应用场景
8.1 四大核心场景
8.2 典型项目
| 项目 | 场景 | 说明 |
|---|---|---|
| Cilium | 网络/安全 | eBPF 驱动的 K8s CNI,替代 kube-proxy |
| Tetragon | 安全 | 基于 eBPF 的运行时安全监控 |
| Beyla | 可观测性 | 零侵入应用性能监控 |
| Katran | 网络 | Facebook 的 L4 负载均衡器 |
| Pixie | 可观测性 | K8s 应用自动可观测性 |
| Falco | 安全 | 云原生运行时安全(已支持 eBPF) |
| Hubble | 可观测性 | Cilium 的网络可观测性组件 |
九、动手实践
9.1 使用 bpftool 探索系统中的 eBPF 程序
# 列出所有已加载的 eBPF 程序sudo bpftool prog list
# 查看特定程序的详细信息sudo bpftool prog show id 123
# 查看程序的 xlated 字节码(JIT 前)sudo bpftool prog dump xlated id 123
# 查看程序的 JIT 机器码sudo bpftool prog dump jited id 123
# 列出所有 Mapsudo bpftool map list
# 查看 Map 内容sudo bpftool map dump id 4569.2 使用 bpftrace 追踪系统调用
# 追踪所有 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 { @syscalls[comm] = count();}'
# 追踪进程创建sudo bpftrace -e 'tracepoint:sched:sched_process_exec { printf("exec: %s → %s\n", comm, str(args->filename));}'9.3 查看 eBPF 特性支持
# 检查内核对 eBPF 的支持情况sudo bpftool feature probe
# 检查特定程序类型是否支持sudo bpftool feature probe | grep "program_type"
# 检查 Map 类型支持sudo bpftool feature probe | grep "map_type"
# 检查 Helper 函数支持sudo bpftool feature probe | grep "helper"十、本章小结
| 主题 | 核心要点 | 关键词 |
|---|---|---|
| eBPF 的本质 | 安全、高性能的内核可编程引擎,不修改内核源码即可扩展内核功能 | 内核可编程, 安全执行 |
| 核心架构 | eBPF 程序(内核态执行)+ Map(数据共享)+ 用户态加载器(管理生命周期) | 程序, Map, 加载器 |
| 程序类型 | 30+ 种程序类型,每种类型决定可用的上下文、Helper 和 Hook 点 | 程序类型, 上下文 |
| Map 机制 | 20+ 种 Map 类型,覆盖哈希、数组、环形缓冲区等数据结构 | Hash, Ring Buffer |
| 开发工具链 | 从 bpftrace(一行命令)到 libbpf(生产级)到 Aya(Rust),选择适合的方式 | bpftrace, libbpf, Aya |
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






