mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2484 字
7 分钟
eBPF 全景:内核可编程革命
2026-03-04

当你需要在内核中添加新功能时,传统做法只有两条路:修改内核源码并重新编译,或者编写内核模块(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 程序进行过滤,只有匹配的数据包才会被传递到用户空间:

flowchart LR subgraph 传统方案 N1[网卡] --> K1[内核协议栈] K1 --> C1[完整拷贝到用户空间] C1 --> F1[用户态过滤] F1 --> A1[应用] end subgraph BPF方案 N2[网卡] --> K2[内核协议栈] K2 --> B2[BPF 过滤器<br/>内核态执行] B2 -->|匹配| C2[拷贝到用户空间] B2 -->|不匹配| D2[丢弃] C2 --> A2[应用] end style B2 fill:#c8e6c9,stroke:#2e7d32 style D2 fill:#ffcdd2,stroke:#c62828

古典 BPF 的设计精巧而克制:

  • 寄存器模型:2 个 32 位寄存器(A 累加器、X 索引寄存器)
  • 指令集:约 20 条指令,涵盖算术、跳转、数据包访问
  • 安全保证:结构化程序(无任意跳转)、有界执行(最大 4096 条指令)

1.2 eBPF 的诞生:从过滤器到可编程引擎#

2014 年,Alexei Starovoitov 提出了 eBPF(extended BPF),将 BPF 从一个简单的包过滤器扩展为通用的内核可编程引擎。关键扩展包括:

特性古典 BPFeBPF
寄存器数量2 个(A、X)10 个(r0-r9 + r10 帧指针)
寄存器宽度32 位64 位
最大指令数4096100 万(5.2+)
Map 支持Hash、Array、Ring Buffer 等 20+ 种
Helper 函数200+ 个内核 Helper
程序类型仅包过滤30+ 种(网络、追踪、安全等)
JIT 编译部分架构全架构支持
尾调用支持(最多 8 层)
BTF/CO-RE完整支持
Note

eBPF 中的 “e” 代表 “extended”,但今天的 eBPF 已经远远超越了 “扩展” 的范畴——它是一个全新的内核可编程子系统。在社区中,“BPF” 和 “eBPF” 通常互换使用,内核代码中统一使用 “BPF”。

1.3 eBPF 的设计哲学#

eBPF 的设计遵循三个核心原则:

  1. 安全第一:验证器在加载时静态分析程序,确保不会崩溃内核
  2. 高性能:JIT 编译为本地机器码,接近原生执行速度
  3. 可观测:通过 Map 与用户态高效交互,不污染内核代码
flowchart TB subgraph 设计哲学 S[" 安全第一<br/>验证器保证"] P[" 高性能<br/>JIT 编译"] O[" 可观测<br/>Map 交互"] end subgraph 实现机制 V["DAG 验证<br/>有界执行"] J["本机码生成<br/>尾调用优化"] M["Ring Buffer<br/>Per-CPU Map"] end S --> V P --> J O --> M style 设计哲学 fill:#e8eaf6,stroke:#283593 style 实现机制 fill:#e0f2f1,stroke:#00695c

二、eBPF 架构总览#

2.1 整体架构#

eBPF 系统由三个核心部分组成:eBPF 程序(运行在内核态)、Map(内核态与用户态的共享数据结构)、用户态加载器(负责编译、加载、管理 eBPF 程序)。

flowchart TB subgraph 用户空间["用户空间"] APP["应用程序"] LOADER["eBPF 加载器<br/>libbpf / cilium/ebpf"] BPF2GO["bpf2go / bpftrace"] end subgraph 内核空间["内核空间"] subgraph BPF子系统["eBPF 子系统"] VER["验证器<br/>安全检查"] JIT["JIT 编译器<br/>生成本机码"] PROG["eBPF 程序<br/>内核态执行"] end subgraph Hook点["Hook 点"] KP["kprobe / kretprobe"] TP["tracepoint"] UP["uprobe / uretprobe"] XDP["XDP"] TC["TC"] LSM["LSM"] SK["Skb / Socket"] end MAP["Map 数据结构<br/>内核-用户态共享"] end APP --> LOADER LOADER -->|"bpf() 系统调用"| VER BPF2GO --> LOADER VER --> JIT JIT --> PROG KP --> PROG TP --> PROG UP --> PROG XDP --> PROG TC --> PROG LSM --> PROG SK --> PROG PROG <-->|"读写"| MAP APP <-->|"读写"| MAP style BPF子系统 fill:#e3f2fd,stroke:#1565c0 style Hook点 fill:#fff3e0,stroke:#e65100 style MAP fill:#e8f5e9,stroke:#2e7d32

2.2 eBPF 程序的生命周期#

一个 eBPF 程序从编写到运行经历以下阶段:

  1. 编写:使用 C(或 Rust)编写 eBPF 程序源码
  2. 编译:通过 clang/LLVM 编译为 eBPF 字节码(.o 文件)
  3. 加载:用户态加载器通过 bpf() 系统调用将字节码提交给内核
  4. 验证:内核验证器对字节码进行静态分析,确保安全性
  5. JIT 编译:验证通过后,JIT 编译器将字节码编译为本机机器码
  6. 挂载:将程序附加到指定的 Hook 点
  7. 执行:Hook 点触发时,执行 eBPF 程序
  8. 卸载:用户态加载器卸载程序,释放资源
// 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_CLSTC 流量控制4.1
网络BPF_PROG_TYPE_SOCKET_FILTERSocket 过滤4.4
网络BPF_PROG_TYPE_SOCK_OPSSocket 操作4.13
网络BPF_PROG_TYPE_SK_SKBSocket 数据转发4.14
追踪BPF_PROG_TYPE_KPROBE内核函数追踪4.1
追踪BPF_PROG_TYPE_TRACEPOINT静态追踪点4.7
追踪BPF_PROG_TYPE_PERF_EVENT性能计数器4.9
安全BPF_PROG_TYPE_LSMLinux 安全模块5.7
安全BPF_PROG_TYPE_CGROUP_SKBCgroup 网络控制4.10
CgroupBPF_PROG_TYPE_CGROUP_DEVICE设备访问控制4.15
CgroupBPF_PROG_TYPE_CGROUP_SOCKSocket 创建控制4.10

3.2 程序类型决定了什么#

不同程序类型有三个关键差异:

flowchart TB TYPE["eBPF 程序类型"] --> CTX["上下文数据<br/>ctx 结构体"] TYPE --> HELP["可用 Helper 函数"] TYPE --> HOOK["可挂载的 Hook 点"] CTX --> EX1["XDP → xdp_md<br/>TC → __sk_buff<br/>kprobe → pt_regs"] HELP --> EX2["XDP → bpf_xdp_redirect<br/>TC → bpf_skb_store_bytes<br/>LSM → bpf_inode_storage_get"] HOOK --> EX3["XDP → 网卡驱动层<br/>TC → qdisc 层<br/>LSM → 安全钩子"] style TYPE fill:#bbdefb,stroke:#1565c0 style CTX fill:#c8e6c9,stroke:#2e7d32 style HELP fill:#fff9c4,stroke:#f9a825 style HOOK fill:#ffccbc,stroke:#d84315
Warning

选择错误的程序类型会导致加载失败。例如,在 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_HASHLRU 淘汰哈希有容量限制的缓存
BPF_MAP_TYPE_SOCKHASHSocket 哈希Socket 重定向
BPF_MAP_TYPE_PROG_ARRAY程序数组尾调用跳转表
BPF_MAP_TYPE_PERF_EVENT_ARRAY性能事件数组追踪数据传输

4.2 Map 的操作方式#

// 内核态 eBPF 程序中操作 Map
struct 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 管理内核官方工具
libbpfC 语言开发库CO-RE 基石
bpftrace高级追踪语言一行命令追踪内核
BCCPython/Lua 工具集丰富的现成工具
bpf2goGo 代码生成cilium/ebpf 配套
AyaRust eBPF 框架纯 Rust,无 libbpf 依赖

5.2 开发流程对比#

flowchart LR subgraph BCC方式["BCC 方式(运行时编译)"] BC["C 源码"] -->|"Python 嵌入"| BCC["BCC 运行时<br/>clang 编译"] BCC -->|"加载"| BK["内核"] end subgraph libbpf方式["libbpf 方式(CO-RE)"] LC["C 源码"] -->|"clang 预编译"| LO[".o 字节码"] LO -->|"libbpf 加载"| LK["内核"] end subgraph Go方式["Go 方式(bpf2go)"] GC["C 源码"] -->|"bpf2go 编译"| GO["Go 源码 + .o"] GO -->|"cilium/ebpf 加载"| GK["内核"] end style BCC方式 fill:#ffcdd2,stroke:#c62828 style libbpf方式 fill:#c8e6c9,stroke:#2e7d32 style Go方式 fill:#bbdefb,stroke:#1565c0
Note

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 execve

6.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/modprobebpf() 系统调用
热更新需卸载再加载原子替换
权限要求root / CAP_SYS_MODULEroot / CAP_BPF (5.8+)
调试难度kgdb / printkbpftool / bpf_trace_printk
性能原生JIT 后接近原生
灵活性可做任何事受限于 Helper 和验证器
可移植性需针对内核版本编译CO-RE 一次编译到处运行
Warning

eBPF 不是内核模块的替代品,而是互补。当你需要的功能超出了 eBPF 的能力范围(如添加新的系统调用、修改内核数据结构),仍然需要内核模块。eBPF 的价值在于:在大多数场景下,你不需要内核模块就能实现内核级功能

八、eBPF 的应用场景#

8.1 四大核心场景#

flowchart TB EBPF["eBPF 应用场景"] EBPF --> NET[" 网络<br/>XDP / TC / Socket"] EBPF --> SEC[" 安全<br/>LSM / 进程监控"] EBPF --> OBS[" 可观测性<br/>bpftrace / BCC"] EBPF --> DBG[" 调试<br/>动态追踪"] NET --> N1["L4 负载均衡"] NET --> N2["防火墙 / DDoS 防护"] NET --> N3["Cilium CNI"] NET --> N4["Service Mesh 数据面"] SEC --> S1["运行时安全监控"] SEC --> S2["系统调用过滤"] SEC --> S3["文件访问控制"] SEC --> S4["Tetragon"] OBS --> O1["性能分析"] OBS --> O2["分布式追踪"] OBS --> O3["应用性能监控"] OBS --> O4["Beyla"] DBG --> D1["函数调用追踪"] DBG --> D2["延迟分析"] DBG --> D3["内存泄漏检测"] DBG --> D4["网络丢包定位"] style EBPF fill:#e8eaf6,stroke:#283593 style NET fill:#e3f2fd,stroke:#1565c0 style SEC fill:#fce4ec,stroke:#c62828 style OBS fill:#e8f5e9,stroke:#2e7d32 style DBG fill:#fff3e0,stroke:#e65100

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
# 列出所有 Map
sudo bpftool map list
# 查看 Map 内容
sudo bpftool map dump id 456

9.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

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

eBPF 全景:内核可编程革命
https://blog.souloss.com/posts/ebpf/ebpf-overview/
作者
Souloss
发布于
2026-03-04
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

相关文章 智能推荐
1
系列导读
eBPF 本系列从 eBPF 的底层原理出发,系统讲解 eBPF 虚拟机、验证器、Map 数据结构、Hook 机制、CO-RE 可移植性,再到 XDP/TC 网络处理、LSM 安全、Cilium 实践、Wasm 融合、Kubernetes 集成与生产部署,带你从「听说过 eBPF」进阶到「能用 eBPF 解决真实问题」。
2
eBPF 网络全景
eBPF eBPF 正在重新定义 Linux 网络栈——从连接跟踪、NAT 到 kube-proxy 替代,从 Socket Filter 到 Sk_msg 重定向,eBPF 提供了比 iptables/netfilter 更高性能、更灵活的网络方案。本章从宏观视角展示 eBPF 网络的全景,详解连接跟踪、NAT、kube-proxy 替代、Socket 层 eBPF,并对比 eBPF 与传统网络方案的架构差异。
3
eBPF 可观测性
eBPF eBPF 最大的应用场景是可观测性——零侵入、低开销、内核级的全链路追踪。本章详解三大可观测性工具链——bpftrace(一行命令追踪内核)、BCC(Python 前端 + 丰富工具集)、Beyla(零侵入应用性能监控),并通过实战展示性能分析、分布式追踪、应用性能监控的完整工作流。
4
eBPF 验证器:如何保证安全
eBPF eBPF 验证器是 eBPF 安全的基石——它在程序加载时进行静态分析,确保 eBPF 程序不会崩溃内核、不会越界访问内存、不会无限循环。从零讲透验证器的 DAG 验证算法、路径探索机制、安全检查规则,并解析常见的验证失败原因与修复方法。
5
eBPF Hook 点:kprobe/tracepoint/uprobe
eBPF eBPF 程序的价值在于它能挂载到内核的各种检查点——Hook 点。本章详解三大 Hook 机制——kprobe(动态内核函数追踪)、tracepoint(静态追踪点)、uprobe(用户态函数追踪),以及 USDT 静态用户态追踪点,并通过实战代码展示每种 Hook 的使用方式与适用场景。