eBPF 不是在 Linux 内核中添加了一个新功能,而是给内核装上了一个「可编程的虚拟机」。 它不修改内核源码、不加载内核模块,就能安全地在 Kernel Space 执行你的代码——在包到达用户态前拦截、在磁盘写入前审计、在函数调用发生时记录。这是过去只有内核开发者才能做到的事。
一、eBPF 核心架构
1.1 执行模型
用户态程序 内核态 BPF VM
┌─────────────┐ ┌──────────────────────────────────┐
│ clang/llvm │ 编译 │ ┌──────────────┐ │
│ (C → BPF) │ ───────→ │ │ BPF Verifier │ ← 安全检查 │
└─────────────┘ │ └──────┬───────┘ │
│ ↓ │
┌─────────────┐ loaded │ ┌──────────────┐ │
│ loader tool │ ──────→ │ │ JIT Comp │ ← x86/aarch64 │
│ (libbpf) │ (bpf syscall)│ └──────┬───────┘ │
└─────────────┘ │ ↓ │
↑ │ ┌──────────────┐ │
│ read maps │ │ BPF Program │ ← 执行业务逻辑 │
│ (user ring buffer) │ └──────────────┘ │
│ └──────────────────────────────────┘
┌────┴────┐
│ Maps │ ← BPF 和用户态共享的 KV store
│ (ring │ (BPF_MAP_TYPE_HASH, ARRAY, RINGBUF, PERCPU_HASH...)
│ buffer)│
└─────────┘
1.2 安全机制
eBPF 程序加载前的受限执行环境:
1. Verifier(验证器)
├── 循环展开或有限循环(max_iterations)
├── 禁止空指针解引用
├── 禁止越界访问
├── 指令数限制(默认 100 万条)
├── 调用深度限制(max 32 层)
└── 状态空间搜索证明无死循环
2. JIT 编译
├── BPF 字节码 → 原生机器码
├── 无解释执行开销
└── 运行效率接近原生内核代码
3. 权限控制
├── CAP_BPF / CAP_SYS_ADMIN(执行权限)
├── 特权级 vs 非特权级(unprivileged BPF)
└── Kernel Lockdown / LSM 限制
二、BPF 程序类型
| 类型 | 触发点 | 典型场景 |
|---|---|---|
| kprobe/kretprobe | 内核函数进入/返回 | 函数追踪、延迟测量 |
| uprobe/uretprobe | 用户态函数进入/返回 | 用户态库函数追踪 |
| tracepoint | 内核静态追踪点 | 标准化事件采集(调度/网络/Syscalls) |
| fentry/fexit | 内核函数(BPF 专用,比 kprobe 快) | Go 应用追踪 |
| XDP | 网卡驱动层 | DDoS 防护、负载均衡 |
| TC (Traffic Control) | 网络栈入口/出口 | 流量整形、网络监控 |
| socket/sockops | Socket 事件 | TCP 状态追踪、连接监控 |
| LSM (Linux Security Module) | 安全钩点 | 安全审计、文件访问控制 |
| BPF Iterator | /proc/bpf 文件读取 | 内核状态导出 |
三、BCC 快速原型
3.1 Hello eBPF
# hello.py (BCC)
from bcc import BPF
# C 代码定义 BPF 程序
prog = """
#include <linux/sched.h>
// 定义 ring buffer 输出结构
struct data_t {
u32 pid;
u32 uid;
char comm[16];
char message[12];
};
BPF_RINGBUF_OUTPUT(events, 1 << 4);
int hello(void *ctx) {
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid() >> 32;
data.uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;
bpf_get_current_comm(&data.comm, sizeof(data.comm));
bpf_probe_read_kernel_str(&data.message, sizeof(data.message), "Hello eBPF!");
events.ringbuf_output(&data, sizeof(data), 0);
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")
print("Tracing clone()... Ctrl+C to exit")
try:
while True:
event = b["events"].event()
print(f"PID={event.pid} UID={event.uid} COMM={event.comm.decode()} MSG={event.message.decode()}")
except KeyboardInterrupt:
pass
sudo python3 hello.py
# 输出:
# PID=1234 UID=1000 COMM=bash MSG=Hello eBPF!
3.2 延迟追踪(kprobe + kretprobe)
# tcp_connect_latency.py
from bcc import BPF
prog = """
#include <net/sock.h>
BPF_HASH(start, u32);
BPF_HISTOGRAM(dist);
int trace_connect_entry(struct pt_regs *ctx, struct sock *sk) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 ts = bpf_ktime_get_ns();
start.update(&pid, &ts);
return 0;
}
int trace_connect_return(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 *tsp = start.lookup(&pid);
if (tsp == 0) return 0;
u64 delta = bpf_ktime_get_ns() - *tsp;
dist.increment(bpf_log2l(delta / 1000000)); // ms
start.delete(&pid);
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event="tcp_v4_connect", fn_name="trace_connect_entry")
b.attach_kretprobe(event="tcp_v4_connect", fn_name="trace_connect_return")
print("Tracing TCP connect latency...")
try:
sleep(999999)
except KeyboardInterrupt:
b["dist"].print_log2_hist("ms")
四、libbpf + BPF CO-RE
4.1 为什么用 libbpf
| 维度 | BCC | libbpf + CO-RE |
|---|---|---|
| 依赖 | 庞大(Clang/LLVM) | 轻量(只依赖 libbpf) |
| 编译 | 运行时编译 | 预编译为 BPF ELF |
| 可移植 | 差(需内核头文件) | CO-RE: Compile Once, Run Everywhere |
| 性能 | 中 | 高(无运行时开销) |
| 生产部署 | 不便 | BPF ELF 直接加载 |
4.2 libbpf C 程序
// tcpconnect.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
char LICENSE[] SEC("license") = "GPL";
struct event {
u32 pid;
u32 saddr;
u32 daddr;
u16 sport;
u16 dport;
};
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
SEC("kprobe/tcp_v4_connect")
int BPF_KPROBE(tcp_v4_connect, struct sock *sk) {
struct event e = {};
e.pid = bpf_get_current_pid_tgid() >> 32;
BPF_CORE_READ_INTO(&e.saddr, sk, __sk_common.skc_rcv_saddr);
BPF_CORE_READ_INTO(&e.daddr, sk, __sk_common.skc_daddr);
BPF_CORE_READ_INTO(&e.sport, sk, __sk_common.skc_num);
BPF_CORE_READ_INTO(&e.dport, sk, __sk_common.skc_dport);
bpf_ringbuf_output(&events, &e, sizeof(e), 0);
return 0;
}
# 构建(Makefile 简化)
clang -g -O2 -target bpf -D__TARGET_ARCH_x86_64 \
-c tcpconnect.bpf.c -o tcpconnect.bpf.o
go build -o tcpconnect tcpconnect.go
五、Go eBPF 开发
5.1 cilium/ebpf(推荐)
package main
import (
"fmt"
"log"
"net"
"os"
"os/signal"
"github.com/cilium/ebpf"
"github.com/cilium/ebpf/link"
"github.com/cilium/ebpf/ringbuf"
"github.com/cilium/ebpf/rlimit"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target bpfel -cc clang tcpconnect ./tcpconnect.bpf.c
type tcpconnectEvent struct {
PID uint32
Saddr uint32
Daddr uint32
Sport uint16
Dport uint16
}
func main() {
// 解除 memlock 限制
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal(err)
}
// 加载 BPF ELF
objs := tcpconnectObjects{}
if err := loadTcpconnectObjects(&objs, nil); err != nil {
log.Fatalf("loading objects: %v", err)
}
defer objs.Close()
// 附加 kprobe
kp, err := link.Kprobe("tcp_v4_connect", objs.TcpV4Connect, nil)
if err != nil {
log.Fatalf("opening kprobe: %v", err)
}
defer kp.Close()
// 读取 ring buffer
rd, err := ringbuf.NewReader(objs.Events)
if err != nil {
log.Fatalf("opening ringbuf reader: %v", err)
}
defer rd.Close()
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt)
go func() {
<-sig
rd.Close()
}()
var event tcpconnectEvent
for {
record, err := rd.Read()
if err != nil {
if err == ringbuf.ErrClosed {
return
}
log.Printf("reading from reader: %s", err)
continue
}
event = tcpconnectEvent{}
if err := binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &event); err != nil {
log.Printf("parsing ringbuf event: %s", err)
continue
}
fmt.Printf("pid=%d %s:%d -> %s:%d\n",
event.PID,
intToIP(event.Saddr), event.Sport,
intToIP(event.Daddr), ntohs(event.Dport))
}
}
func intToIP(ip uint32) net.IP {
b := make([]byte, 4)
binary.LittleEndian.PutUint32(b, ip)
return net.IP(b)
}
func ntohs(port uint16) uint16 {
return (port>>8)&0xff | (port&0xff)<<8
}
六、可观测性场景
6.1 火焰图(Off-CPU / On-CPU)
# On-CPU 火焰图(谁在占用 CPU)
git clone https://github.com/iovisor/bcc
cd bcc/tools
sudo ./profile -F 99 -a -g -- 30 > out.stacks
# 生成火焰图
./flamegraph.pl out.stacks > oncpu.svg
# Off-CPU 火焰图(谁在等待)
sudo ./offcputime -f -p PID 30 > out.offcpu
./flamegraph.pl out.offcpu > offcpu.svg
6.2 文件 IO 追踪
# 用 BCC 追踪文件打开
sudo ./opensnoop
# PID COMM FD ERR PATH
# 1234 nginx 3 0 /var/log/nginx/access.log
# 1234 nginx 4 0 /etc/nginx/nginx.conf
# 追踪文件系统同步
sudo ./syncsnoop
# 追踪磁盘 IO
sudo ./biosnoop
# TIME(s) COMM PID DISK T SECTOR BYTES LAT(ms)
# 0.000001 wget 4567 sda W 1234567 4096 0.85
6.3 网络丢包分析
# 追踪 TCP 丢包原因
sudo ./tcpretrans
# TIME PID IP LADDR:LPORT T> RADDR:RPORT STATE
# 14:32:01 2345 4 10.0.1.2:443 R> 172.16.0.1:54321 ESTABLISHED
# 追踪 TCP 生命周期
sudo ./tcptracer
# T PID COMM IP SADDR DADDR SPORT DPORT
# A 1234 curl 4 10.0.1.2 172.217.0.1 54321 443
# X 1234 curl 4 10.0.1.2 172.217.0.1 54321 443
七、与 Prometheus/Grafana 集成
// 将 eBPF 采集的数据导出为 Prometheus metrics
// ebpf_exporter(开源项目)
// 配置:
// ebpf_exporter.yml
programs:
- name: tcpconnect
metrics:
counters:
- name: tcp_connections_total
help: Total TCP connections by destination
table: events
labels:
- name: daddr
size: 4
decoders:
- name: inet_ip
# 查询 eBPF 导出的指标
sum by (daddr) (rate(tcp_connections_total[5m]))
# 与现有指标关联
sum by (pod) (
rate(container_network_transmit_packets_total[5m])
) * on (pod) group_left (daddr)
sum by (pod, daddr) (rate(tcp_connections_total[5m]))
八、eBPF Checklist
| 检查项 | 说明 |
|---|---|
| 内核版本 | ≥ 4.18(CO-RE 需要 ≥ 5.2) |
| 权限 | CAP_BPF 或 root |
| 安全策略 | SELinux/AppArmor 可能阻止加载 |
| 不修改生产 | 先在测试环境验证 Verifier 通过 |
| 性能开销 | 每个 kprobe ~50-200ns,高频率注意 |
| 资源清理 | defer 确保 program/link/map 关闭 |
| CO-RE | 使用 BTF(BPF Type Format),程序可移植 |
参考与延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。