eBPF(extended Berkeley Packet Filter)是 Linux 内核中一个安全可编程的虚拟机框架,允许在不修改内核源码、不重新编译内核的前提下,将自定义逻辑注入内核执行。它从最初的包过滤演进为覆盖网络、可观测性、安全控制的通用内核扩展平台,是当前最活跃的 Linux 内核子系统之一。
Linux 系列:进程与线程 · 内存管理 · 文件系统 · 网络与 IO 模型 · 性能分析工具
目录
| 章节 | 说明 |
|---|---|
| eBPF 是什么 | 发展历程与核心定位 |
| 工作原理 | 虚拟机、验证器、JIT 编译 |
| 程序类型 | kprobe/tracepoint/XDP/socket 等 |
| BPF Map | 内核态与用户态共享数据 |
| 工具链对比 | BCC vs libbpf vs bpftrace |
| 实战:内核跟踪 | kprobe/tracepoint 追踪系统调用 |
| 实战:用户态跟踪 | uprobe/USDT 追踪应用函数 |
| 实战:网络可观测性 | 追踪网络丢包与连接延迟 |
| 实战:XDP 高性能包过滤 | 超越 iptables 的包处理 |
| CO-RE 原理 | 一次编译到处运行 |
| 局限性 | eBPF 不是万能的 |
eBPF 是什么
发展历程
1992 年 BPF(BSD Packet Filter)诞生,用于网络包过滤,比当时最快方案快 20 倍
核心设计:内核态虚拟机 + 用户态字节码,避免每包复制到用户空间
2014 年 eBPF 诞生(Alexei Starovoitov)
扩展寄存器数量(11 个 64 位寄存器)
引入 BPF Map(内核/用户态共享存储)
从网络包过滤扩展到内核函数、用户函数、跟踪点、性能事件、安全控制
2016 年+ BCC、bpftrace 工具链成熟,大幅降低 eBPF 开发门槛
Cilium(K8s 网络)、Katran(负载均衡)、Falco(安全)等开源项目涌现
2021 年 eBPF 基金会成立;微软发布 eBPF for Windows
核心价值:
eBPF 开启了一种新的内核扩展范式:无需修改内核源码,无需重新编译,无需加载内核模块,即可在内核中安全运行自定义逻辑。
工作原理
执行流程
开发者写 C 代码(eBPF 程序)
↓
LLVM/Clang 编译为 BPF 字节码
↓
bpf() 系统调用提交到内核
↓
验证器(Verifier)安全检查
├── 构建有向无环图,确保无不可达指令
├── 模拟执行,确保无无效指令
├── 不含无限循环
└── 必须在有限时间内完成
↓
JIT 编译器将字节码编译为本地机器指令
↓
绑定到事件(kprobe/tracepoint/XDP 等)
↓
事件触发时自动执行 eBPF 程序
内核运行时组件
| 组件 | 职责 |
|---|---|
| eBPF 辅助函数 | 提供与内核交互的 API(不同程序类型可用的函数集不同) |
| 验证器 | 安全检查,拒绝不安全的程序 |
| 寄存器 + 栈 | 11 个 64 位寄存器(R0~R10),512 字节栈空间 |
| JIT 编译器 | 将字节码编译为本地机器码,提升执行效率 |
| BPF Map | 大块持久化存储,用于内核态/用户态数据交换 |
安全保障
- 只有特权进程(
CAP_BPF或 root)才能加载 eBPF 程序 - 验证器拒绝包含无限循环、越界访问、空指针解引用的程序
- eBPF 程序栈空间最多 512 字节,超出必须用 Map
- 内核 5.2 前最多 4096 条指令,5.2 后提升到 100 万条
程序类型
Linux 内核 v5.13 支持 30 种程序类型,按场景分为三大类:
跟踪类(Tracing)
| 类型 | 触发时机 | 典型用途 |
|---|---|---|
BPF_PROG_TYPE_KPROBE |
内核函数入口/返回 | 追踪任意内核函数调用 |
BPF_PROG_TYPE_TRACEPOINT |
内核预定义静态跟踪点 | 稳定 API,推荐优先使用 |
BPF_PROG_TYPE_PERF_EVENT |
性能计数器事件(PMU) | CPU 周期、Cache Miss 等 |
BPF_PROG_TYPE_RAW_TRACEPOINT |
原始跟踪点(参数未处理) | 性能更高 |
kprobe vs tracepoint:kprobe 挂载到任意内核函数,灵活但不稳定(内核升级可能函数改名);tracepoint 是内核显式定义的稳定接口,推荐优先使用。
网络类(Networking)
| 类型 | 触发位置 | 典型用途 |
|---|---|---|
BPF_PROG_TYPE_XDP |
网卡驱动收包最早期(协议栈之前) | DDoS 防御、高性能包过滤、4 层负载均衡 |
BPF_PROG_TYPE_SCHED_CLS/ACT |
TC 流量控制(协议栈内) | 流量整形、包改写、重定向 |
BPF_PROG_TYPE_SOCKET_FILTER |
套接字收包 | 包过滤(tcpdump 底层就是这个) |
BPF_PROG_TYPE_SOCK_OPS |
TCP 连接状态变化 | 调整 TCP 参数、连接追踪 |
BPF_PROG_TYPE_CGROUP_SKB |
cgroup 网络包 | 容器网络控制 |
XDP 返回码
| 返回码 | 含义 |
|---|---|
XDP_DROP |
丢弃包(最快,直接在驱动层丢弃) |
XDP_PASS |
传递给内核协议栈正常处理 |
XDP_TX |
从同一网卡发回(用于反射攻击防御) |
XDP_REDIRECT |
重定向到其他网卡或 CPU |
XDP_ABORTED |
程序错误,丢弃并记录 |
BPF Map
BPF Map 是 eBPF 程序与用户态程序之间的共享数据结构,也是 eBPF 程序在多次调用之间保存状态的唯一方式。
常用 Map 类型
| 类型 | 数据结构 | 典型用途 |
|---|---|---|
BPF_MAP_TYPE_HASH |
哈希表(key-value) | 统计每个进程/IP 的计数 |
BPF_MAP_TYPE_ARRAY |
定长数组(index-value) | 全局计数器、配置项 |
BPF_MAP_TYPE_PERF_EVENT_ARRAY |
环形缓冲区 | 将事件流式传输到用户态 |
BPF_MAP_TYPE_RINGBUF |
无锁环形缓冲区(推荐) | 高性能事件上报(内核 5.8+) |
BPF_MAP_TYPE_LRU_HASH |
LRU 淘汰哈希表 | 连接追踪(自动淘汰旧条目) |
BPF_MAP_TYPE_PROG_ARRAY |
eBPF 程序数组 | 尾调用(tail call),绕过指令数限制 |
BPF_MAP_TYPE_STACK_TRACE |
调用栈存储 | 火焰图生成 |
用户态操作 Map
# 查看系统中所有 BPF Map
bpftool map list
# 查看特定 Map 的内容
bpftool map dump id <map_id>
# 更新 Map 条目
bpftool map update id <map_id> key 0x01 0x00 0x00 0x00 value 0x0a 0x00 0x00 0x00
工具链对比
| 工具 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| bpftrace | 快速排查、单行脚本 | 语法简洁,无需编译,即写即用 | 功能有限,不适合复杂程序 |
| BCC | 开发复杂 eBPF 程序 | 内置丰富工具,Python/C++ 接口友好 | 依赖 LLVM + 内核头文件,每次运行时编译 |
| libbpf | 生产环境分发 | 编译一次即可分发,不需要目标机安装 LLVM | 需内核开启 BTF,开发复杂度高 |
| 内核源码 samples/bpf | 学习内核 eBPF 原理 | 最接近底层 | 不适合生产使用 |
选择建议:
- 临时排查问题 → bpftrace
- 开发工具并在内部使用 → BCC
- 开发需要分发到生产环境的工具 → libbpf + CO-RE
# 安装 bpftrace(Ubuntu 19.04+)
sudo apt-get install -y bpftrace
# 安装 BCC
sudo apt-get install -y bpfcc-tools linux-headers-$(uname -r)
# 查看当前内核支持的 eBPF 特性
bpftool feature probe | grep program_type
实战:内核跟踪
查询跟踪点
# 查询所有内核插桩和跟踪点
sudo bpftrace -l
# 查询所有系统调用跟踪点
sudo bpftrace -l 'tracepoint:syscalls:*'
# 查询包含 execve 的跟踪点
sudo bpftrace -l '*execve*'
# 查询跟踪点的参数格式
sudo bpftrace -lv tracepoint:syscalls:sys_enter_execve
# 输出:
# int __syscall_nr
# const char * filename
# const char *const * argv
# const char *const * envp
用 bpftrace 追踪短时进程
# 追踪所有 execve 调用(短时进程排查利器)
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_execve,
tracepoint:syscalls:sys_enter_execveat {
printf("%-6d %-8s ", pid, comm);
join(args->argv);
}'
用 bpftrace 追踪 syscall 延迟
# 追踪 read 系统调用的延迟分布(单位:纳秒)
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_read { @start[tid] = nsecs; }
tracepoint:syscalls:sys_exit_read /@start[tid]/
{
@latency = hist(nsecs - @start[tid]);
delete(@start[tid]);
}
interval:s:5 { print(@latency); clear(@latency); }'
用 bpftrace 追踪内核函数调用
# 追踪内核 kfree_skb(网络丢包)的调用栈
sudo bpftrace -e '
kprobe:kfree_skb /comm=="curl"/ {
printf("kstack: %s\n", kstack);
}'
# 追踪 do_sys_openat2 调用(哪些进程在打开文件)
sudo bpftrace -e '
kprobe:do_sys_openat2 {
printf("%-6d %-16s %s\n", pid, comm, str(arg1));
}'
BCC 工具直接使用
# 追踪所有 execve(短时进程)
execsnoop-bpfcc
# 追踪 TCP 连接建立
tcpconnect-bpfcc
# 追踪 open() 系统调用
opensnoop-bpfcc
# 追踪 I/O 延迟
biolatency-bpfcc
# 追踪 CPU 热点函数(On-CPU 分析)
profile-bpfcc -F 99 30
实战:用户态跟踪
uprobe 将 eBPF 程序插桩到用户程序的任意函数,不需要修改应用代码。
追踪编译型语言(C/Go)
# 追踪 Bash 中执行的命令(通过 uretprobe 获取 readline 返回值)
sudo bpftrace -e '
uretprobe:/usr/bin/bash:readline {
printf("User %d executed \"%s\" command\n", uid, str(retval));
}'
# 追踪 Go 程序的函数调用(需要 -g 编译选项保留符号)
# 查询 Go 二进制的符号表
readelf -Ws /path/to/go-binary | grep -i "your_func"
# 用 bpftrace 挂载 uprobe
sudo bpftrace -e '
uprobe:/path/to/go-binary:main.yourFunction {
printf("called with arg0=%d\n", arg0);
}'
追踪 Python(解释型语言,通过 USDT)
# 查询 Python3 的 USDT 跟踪点
bpftrace -l 'usdt:/usr/bin/python3:*'
# 输出:
# usdt:/usr/bin/python3:python:function__entry
# usdt:/usr/bin/python3:python:function__return
# ...
# 追踪 Python 函数调用(文件名:行号 函数名)
sudo bpftrace -e '
usdt:/usr/bin/python3:function__entry {
printf("%s:%d %s\n", str(arg0), arg2, str(arg1));
}'
追踪 Java(JIT 编译型语言)
# Java 需要开启 USDT(--enable-dtrace 编译选项)
# 或使用 async-profiler 绕过 JIT 限制
# 用 async-profiler 生成火焰图(推荐)
./profiler.sh -d 30 -f flamegraph.html <java_pid>
# 追踪 JVM 的 GC 事件(通过 USDT)
sudo bpftrace -e '
usdt:/usr/lib/jvm/java-11-openjdk-amd64/lib/server/libjvm.so:hotspot:gc__begin {
printf("GC started at %llu\n", nsecs);
}'
实战:网络可观测性
追踪 TCP 连接
# 实时追踪新建 TCP 连接(源/目的 IP 和端口)
sudo bpftrace -e '
kprobe:tcp_connect {
$sk = (struct sock *)arg0;
printf("%-16s:%-5d → %-16s:%-5d\n",
ntop(2, $sk->__sk_common.skc_rcv_saddr),
$sk->__sk_common.skc_num,
ntop(2, $sk->__sk_common.skc_daddr),
$sk->__sk_common.skc_dport >> 8);
}'
# 或直接用 BCC 工具
tcpconnect-bpfcc # 追踪 connect() 调用
tcpaccept-bpfcc # 追踪 accept() 调用
tcpretrans-bpfcc # 追踪 TCP 重传
追踪网络丢包(调用栈分析)
# 保存为 dropwatch.bt 文件后执行:sudo bpftrace dropwatch.bt
# 追踪 kfree_skb 的内核调用栈,定位丢包原因
# 简化版单行命令(过滤 curl 进程的 TCP 丢包)
sudo bpftrace -e '
#include <linux/skbuff.h>
#include <linux/ip.h>
kprobe:kfree_skb /comm=="curl"/ {
$skb = (struct sk_buff *)arg0;
$iph = (struct iphdr *)($skb->head + $skb->network_header);
if ($iph->protocol == 6) { // 6 = TCP
printf("DROP: %s→%s\n%s\n",
ntop(AF_INET, $iph->saddr),
ntop(AF_INET, $iph->daddr),
kstack);
}
}'
追踪 TCP 延迟分布
# 追踪 TCP 接收延迟(从数据到达到 read() 返回)
sudo bpftrace -e '
kprobe:tcp_rcv_established { @start[tid] = nsecs; }
kretprobe:tcp_rcv_established /@start[tid]/ {
@tcp_latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}
interval:s:10 { print(@tcp_latency_us); }'
实战:XDP 高性能包过滤
XDP 在网卡驱动层处理数据包,无需经过内核协议栈,性能远超 iptables。
XDP vs iptables 性能对比
| 方案 | 处理位置 | 典型吞吐 | 适用场景 |
|---|---|---|---|
| iptables | 内核协议栈 Netfilter | ~1 Mpps | 通用防火墙 |
| nftables | 内核协议栈 Netfilter | ~2 Mpps | 通用防火墙 |
| XDP(原生模式) | 网卡驱动层 | ~10-25 Mpps | DDoS 防御、高性能过滤 |
| XDP(卸载模式) | 网卡固件 | >100 Mpps | 超高性能场景 |
简单 XDP 包过滤示例
// xdp_drop_icmp.c — 丢弃所有 ICMP 包
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <bpf/bpf_helpers.h>
SEC("xdp")
int xdp_drop_icmp(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 *iph = (void *)(eth + 1);
if ((void *)(iph + 1) > data_end)
return XDP_PASS;
// 丢弃 ICMP(协议号 1)
if (iph->protocol == 1)
return XDP_DROP;
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
# 编译并加载 XDP 程序(通用模式,无需网卡驱动支持)
clang -O2 -target bpf -c xdp_drop_icmp.c -o xdp_drop_icmp.o
sudo ip link set dev eth0 xdpgeneric object xdp_drop_icmp.o sec xdp
# 卸载 XDP 程序
sudo ip link set dev eth0 xdpgeneric off
# 用 BCC 加载 XDP(更简单)
from bcc import BPF
b = BPF(src_file="xdp_drop_icmp.c")
fn = b.load_func("xdp_drop_icmp", BPF.XDP)
b.attach_xdp("eth0", fn, 0)
XDP 运行模式
| 模式 | 要求 | 性能 | 说明 |
|---|---|---|---|
| 通用模式(xdpgeneric) | 无特殊要求 | 最低 | 在协议栈中模拟,用于测试 |
| 原生模式(xdpdrv) | 网卡驱动支持 | 高 | 在驱动早期路径运行 |
| 卸载模式(xdpoffload) | 网卡固件支持 | 最高 | 直接在网卡上运行 |
CO-RE 原理
CO-RE(Compile Once - Run Everywhere,一次编译到处运行) 解决了 eBPF 程序的可移植性问题。
问题背景
eBPF 程序常需要访问内核数据结构(如 task_struct),但不同内核版本的结构体字段偏移不同。传统方式(BCC)在每台机器运行时动态编译,需要安装 LLVM 和内核头文件。
CO-RE 解决方案
内核编译时生成 BTF(BPF Type Format)信息
↓
eBPF 程序使用 BTF 描述所需的类型信息
↓
libbpf 在加载时根据目标内核的 BTF 重定位字段偏移
↓
同一个编译好的 eBPF 二进制可在不同内核版本上运行
| 方案 | 每机器需要 LLVM | 需要内核头文件 | 可移植性 |
|---|---|---|---|
| BCC 动态编译 | 是 | 是 | 差(每机器编译) |
| libbpf + CO-RE | 否 | 否 | 好(一次编译) |
# 检查内核是否支持 BTF(CO-RE 前提)
ls /sys/kernel/btf/vmlinux
# 查看 BTF 信息
bpftool btf dump file /sys/kernel/btf/vmlinux format raw | head -20
# CO-RE 程序使用 vmlinux.h(包含所有内核类型定义)
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
局限性
- 内核版本要求:稳定运行 eBPF 需要 Linux 4.9+,建议使用 5.x 内核体验完整特性
- 栈空间限制:eBPF 程序栈最多 512 字节,复杂逻辑需借助 Map 存储
- 指令数限制:5.2 前最多 4096 条,5.2+ 提升到 100 万条
- 不能调用任意内核函数:只能调用 BPF 辅助函数白名单中的函数
- uprobe 性能开销:高频用户态函数(如内存分配)使用 uprobe 会带来显著开销,应避免
- BTF 依赖:CO-RE 需要内核开启 BTF,较老的发行版默认不开启
- 调试困难:eBPF 程序在内核中运行,调试手段有限(主要靠 bpf_trace_printk 和 Map 输出)
参考资料
- 《eBPF 核心技术与实战》— 倪朋飞,极客时间
- Brendan Gregg — BPF Performance Tools(brendangregg.com/bpf-performance-tools-book.html)
- eBPF 官方文档(ebpf.io)
- BCC 参考手册(github.com/iovisor/bcc/blob/master/docs/reference_guide.md)
- bpftrace 参考手册(github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md)
评论 (0)
发表评论