5.12 eBPF 基础——现代 Linux 可观测性
预计阅读时间:14 分钟
📖 目录
学习目标
学完本章后,你将能够:
- 理解 eBPF 的核心架构(JIT 编译、Verifier 验证、BPF maps)
- 区分 kprobe、uprobe、tracepoint、USDT 四种探针类型与适用场景
- 使用 bcc 工具集(execsnoop、biolatency、tcplife)做日常排障
- 使用 bpftrace 编写一行流 eBPF 程序做深度诊断
- 了解 Cilium 和 Tetragon 在企业级网络与安全中的角色
核心知识
- eBPF(extended Berkeley Packet Filter)——在内核中运行沙箱化程序的框架,无需修改内核源码或加载内核模块
- JIT(Just-In-Time)编译器——将 eBPF 字节码转换为本地机器码,运行速度接近原生内核代码
- Verifier(验证器)——静态分析 eBPF 程序,拒绝无限循环、越界访问、未初始化变量等不安全行为
- BPF maps——eBPF 程序与用户空间共享数据的键值存储结构,支持数组、哈希表、LRU 等类型
- 探针类型——kprobe(内核函数入口/返回)、uprobe(用户态函数)、tracepoint(内核静态跟踪点)、USDT(应用预埋跟踪点)
- bcc(BPF Compiler Collection)——将 C 写的 eBPF 代码嵌入 Python/Lua 脚本的编译加载框架
- bpftrace——类似 awk 语法的高阶 eBPF 脚本语言,一行即可写出探针程序
- Cilium / Tetragon——基于 eBPF 的 K8s CNI 网络插件与运行时安全监控平台
知识关联
- 前置知识:2.4:进程管理 进程管理(理解进程与系统调用关系)、4.5:网络故障排查 网络故障排查(理解网络栈层次)
- 后续影响:eBPF 是容器网络(6.2:Kubernetes 入门 K8s 网络)、运行时安全、深度性能调优(4.6:Linux 性能调优)的基础技术
- 配套技术:系统调用跟踪(strace)、性能分析(perf)、容器网络(CNI)
- 内核版本要求:eBPF 基础功能需 Linux 4.1+;BPF maps 需 4.4+;CO-RE/BTF 需 5.4+;fentry/fexit 需 5.5+。生产环境建议内核 ≥ 5.4
原理讲解
eBPF 程序生命周期
一个 eBPF 程序从编写到加载挂载的流程如下:
- 编写——用 C 语言编写 eBPF 内核态代码(限制:无循环、无 libc、有限栈大小)
- 编译——由 LLVM/Clang 编译为 eBPF 字节码(ELF 目标文件)
- 验证——加载时内核 Verifier 静态分析:控制流图确保有穷终止、模拟执行检查越界、类型检查参数
- JIT 编译——通过通过验证后,内核 JIT 编译器将字节码转为当前 CPU 架构的本地机器码
- 挂载——将 eBPF 程序附加到指定事件源(kprobe、tracepoint、XDP 等)
- 交互——eBPF 程序通过 BPF maps 与用户空间通信(读/写/更新键值数据)
- 卸载——程序不再使用时从内核卸载,释放资源
Verifier 的工作机制
Verifier 是 eBPF 安全性的核心保障。它以 DAG(有向无环图)方式遍历程序的每条执行路径,确保:
- 有穷终止——拒绝任何含有向后跳转的循环(除非有界循环被 LLVM 展开)
- 内存安全——每个内存访问都模拟检查指针范围和类型,禁止越界
- 栈深度限制——最大栈空间 512 字节,不允许递归调用
- 辅助函数白名单——eBPF 程序只能调用内核预定义的 bpf_* 辅助函数
探针类型对比
| 探针类型 | 语法前缀 | 跟踪目标 | 稳定性 | 典型用例 |
|---|---|---|---|---|
| kprobe | kprobe:func | 内核函数入口 | 中 | 调试内核函数调用参数 |
| kretprobe | kretprobe:func | 内核函数返回 | 中 | 获取函数返回值、测量内核函数耗时 |
| uprobe | uprobe:bin:func | 用户态函数入口 | 低 | 跟踪应用层函数如 malloc、SSL_read |
| tracepoint | tracepoint:syscalls:* | 内核静态跟踪点 | 高 | 跟踪系统调用、调度器事件(生产环境首选) |
| USDT | usdt:bin:provider:name | 应用预埋跟踪点 | 高 | 跟踪 PostgreSQL/Node.js 等应用内建事件 |
BPF maps 数据交互
BPF maps 是 eBPF 程序与用户空间之间的桥梁。内核态的 eBPF 程序写入 maps,用户态程序通过 bpf() 系统调用读取。常用 map 类型:
| Map 类型 | 描述 | 典型用法 |
|---|---|---|
BPF_MAP_TYPE_HASH | 哈希表(键值对) | 统计计数、跟踪连接状态 |
BPF_MAP_TYPE_ARRAY | 固定大小数组 | CPU 计数器、per-CPU 统计 |
BPF_MAP_TYPE_PERF_EVENT_ARRAY | 性能事件环形缓冲区 | 将事件逐条推送到用户空间 |
BPF_MAP_TYPE_LRU_HASH | LRU 淘汰哈希表 | 跟踪活跃连接、自动淘汰旧条目 |
BPF_MAP_TYPE_STACK_TRACE | 栈回溯 | 采集调用栈(配合 perf 事件) |
eBPF 程序类型与挂载点
除了探针(probe),eBPF 程序还能挂载到网络和追踪的关键位置。程序类型决定了它能调用哪些辅助函数、访问哪些上下文:
| 程序类型 | 挂载点 | 典型用途 |
|---|---|---|
BPF_PROG_TYPE_XDP | 网卡驱动最早入口 | DDoS 过滤、负载均衡(性能最高的包处理位置) |
BPF_PROG_TYPE_SCHED_CLS | TC(流量控制)钩子 | 容器网络策略、限速 |
BPF_PROG_TYPE_KPROBE | 内核函数入口/返回 | 函数级追踪 |
BPF_PROG_TYPE_TRACEPOINT | 内核静态跟踪点 | 生产可观测性(稳定 ABI) |
BPF_PROG_TYPE_CGROUP_SKB | cgroup 网络钩子 | 容器级网络隔离/统计 |
BPF_PROG_TYPE_LSM | LSM 安全钩子 | 内核安全策略(如阻止特定 syscall) |
BPF_PROG_TYPE_TRACING | fentry/fexit | 低开销函数级追踪(内核 5.5+) |
网络路径上 XDP 在收包最早期执行(驱动层、没有 skb 分配),性能比 TC 高一个数量级;XDP 放行/丢弃后包不进内核协议栈,常用于抗 DDoS 的丢包场景。Cilium 正是把 eBPF 挂在 TC 钩子上实现 Pod 网络策略,替代 iptables 链式遍历。
BTF 与 CO-RE
早期 eBPF 程序依赖内核头文件编译,换个内核版本就得重新编译(BPF 程序与内核强耦合)。BTF(BPF Type Format)把内核的类型信息以紧凑格式暴露给用户空间,CO-RE(Compile Once, Run Everywhere)让同一份编译产物在不同内核版本上直接加载——这是 eBPF 能被大规模分发到异构集群的前提。内核 5.4+ 默认开启 CONFIG_DEBUG_INFO_BTF,检查方法:
# 确认 BTF 支持
test -f /sys/kernel/btf/vmlinux && echo "BTF enabled" || echo "BTF missing"
# 输出: BTF enabled
# bpftool 查看 BTF 信息
bpftool btf dump file /sys/kernel/btf/vmlinux | head -5
# 为当前内核生成 vmlinux.h(写 CO-RE 程序时使用)
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
示例代码
1. 检查内核 eBPF 支持
# 查看内核是否开启了 eBPF 相关配置
cat /boot/config-$(uname -r) | grep BPF
# 输出:
# CONFIG_BPF=y
# CONFIG_BPF_SYSCALL=y
# CONFIG_BPF_JIT=y
# 查看已加载的 BPF 程序
bpftool prog list
# 查看 BPF 网络程序(XDP/TC)
bpftool net list
2. bcc 常用工具实战
# 安装 bcc 工具集
apt install -y bpfcc-tools
# 所有工具以 *-bpfcc 命名
# 追踪新进程创建(排查周期性高 CPU)
execsnoop-bpfcc
# 输出:
# PID COMM RET ARGS
# 1234 logrotate 0 /usr/sbin/logrotate /etc/logrotate.conf
# 追踪磁盘 I/O 延迟分布
biolatency-bpfcc
# 输出: 直方图显示 I/O 延迟分布区间
# 追踪 TCP 连接生命周期
tcplife-bpfcc
# 输出: 进程、源/目标 IP、端口、持续时间、收发字节数
# 追踪 OOM Killer
oomkill-bpfcc
# 输出: 谁触发了 OOM、kill 了哪个进程、分数多少
# 追踪慢文件 I/O(>10ms)
fileslower-bpfcc 10
3. bpftrace 一行流
# 安装 bpftrace
apt install -y bpftrace
# 统计每个进程的 read() 系统调用次数
bpftrace -e 'kprobe:sys_read { @[comm] = count(); }'
# 追踪 open 操作并打印文件名
bpftrace -e 'kprobe:do_sys_open { printf("%s: %s\n", comm, str(arg1)); }'
# 只追踪 >4KB 的大文件读取
bpftrace -e 'kprobe:vfs_read /arg2 > 4096/ { @[comm] = sum(arg2); }'
# 追踪新进程创建
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(args->filename)); }'
# 实时统计磁盘 I/O 按进程
bpftrace -e 'kprobe:submit_bio { @[comm] = count(); }'
# 查看可用探针列表
bpftrace -l 'tracepoint:syscalls:*'
bpftrace -l 'kprobe:tcp_*'
# 查看探针参数字段
bpftrace -lv 'tracepoint:syscalls:sys_enter_openat'
# 输出: filename, flags, mode
4. Cilium 与 Tetragon 部署
# Cilium — 基于 eBPF 的 Kubernetes CNI
# 替代 kube-proxy iptables,性能提升明显
helm repo add cilium https://helm.cilium.io/
helm install cilium cilium/cilium --namespace kube-system
# Tetragon — 基于 eBPF 的运行时安全监控
# 检测容器内新二进制执行、敏感文件读取、异常网络连接
# 无需修改应用代码即可获得安全可见性
5. bpftrace 生产排查示例集
# 场景一:CPU 突然飙高,找出谁在疯狂 fork 进程
bpftrace -e 'tracepoint:syscalls:sys_enter_fork { @[comm] = count(); }'
# 输出: @[java]: 12031, @[sh]: 342, ...
# 场景二:磁盘 IO 暴涨,定位读写进程与文件
bpftrace -e 'kprobe:blk_start_plug { @[comm] = count(); }'
bpftrace -e 'kprobe:vfs_read { @[comm, str(arg1)] = count(); }'
# 场景三:文件被谁删了(open 时打印调用栈)
bpftrace -e 'kprobe:do_unlinkat { printf("%s unlink %s\n", comm, str(arg1)); }'
# 场景四:网络重传排查(TCP 重传触发源)
bpftrace -e 'kprobe:tcp_retransmit_skb { @[comm, ntop(2, arg2)] = count(); }'
# 场景五:统计进程创建耗时分布(exec 延迟)
bpftrace -e 'kprobe:execve { @start[tid] = nsecs; } kretprobe:execve /@start[tid]/ { @ms = hist((nsecs - @start[tid]) / 1000000); delete(@start[tid]); }'
# 场景六:系统调用延迟直方图(按进程名分组)
bpftrace -e 'kprobe:do_sys_openat { @start[tid] = nsecs; } kretprobe:do_sys_openat { @ns[comm] = hist(nsecs - @start[tid]); delete(@start[tid]); }'
# 场景七:信号追踪(谁给谁发了什么信号)
bpftrace -e 'tracepoint:signal:signal_generate { printf("%s -> %s signal %d\n", comm, args->target, args->sig); }'
6. 用 Python + bcc 编写自定义工具
#!/usr/bin/env python3
# trace-opens.py —— 追踪指定进程的 openat 系统调用
from bcc import BPF
bpf_text = """
#include <uapi/linux/ptrace.h>
int trace_openat(struct pt_regs *ctx, const char __user *filename) {
bpf_trace_printk("open: %s\\n", filename);
return 0;
}
"""
bpf = BPF(text=bpf_text)
bpf.attach_kprobe(event="do_sys_openat", fn_name="trace_openat")
print("追踪 openat 调用,Ctrl+C 退出...")
bpf.trace_print()
# 运行:
# python3 trace-opens.py
# 输出: bash-1234 [000] .... 123.456789: open: /etc/passwd
#!/usr/bin/env python3
# vfs-latency.py —— 按进程统计 VFS 读写耗时
from bcc import BPF
bpf_text = """
#include <uapi/linux/ptrace.h>
BPF_HASH(start, u32);
BPF_HISTOGRAM(lat, u32);
int entry(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid();
start.update(&pid, &bpf_ktime_get_ns());
return 0;
}
int exit(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid();
u64 *t = start.lookup(&pid);
if (t) {
u64 delta = bpf_ktime_get_ns() - *t;
lat.increment(bpf_log2l(delta / 1000));
start.delete(&pid);
}
return 0;
}
"""
bpf = BPF(text=bpf_text)
bpf.attach_kprobe(event="vfs_read", fn_name="entry")
bpf.attach_kretprobe(event="vfs_read", fn_name="exit")
bpf["lat"].print_log2_hist("VFS read latency (us)")
# 输出: 直方图显示各延迟区间的调用次数
常见错误
| 错误表现 | 根因 | 正确做法 |
|---|---|---|
Failed to load BTF / libbpf: BTF is required | 内核未启用 BTF(BPF Type Format),缺少类型信息 | 升级内核到 5.4+ 并确认 CONFIG_DEBUG_INFO_BTF=y |
permission denied 加载 BPF 程序 | 非 root 用户,或 cgroup BPF 权限不足 | 以 root 执行,或为容器设置 CAP_BPF、CAP_SYS_ADMIN |
invalid func unknown#1 verifier 报错 | eBPF 程序调用了不被允许的辅助函数 | 只使用 bpf_* 官方辅助函数,检查程序类型是否匹配 |
BPF program is too large | eBPF 指令数超过内核上限(4096 或 100 万条) | 简化逻辑,拆分程序,或升级到 5.2+ 内核(放宽到 100 万条) |
unreachable instruction verifier 报错 | 代码中存在不可达路径(如死循环后的指令) | 确保程序是无环 DAG,有界循环用 #pragma unroll 展开 |
func #1 ptr R1=... access off=... size=... 越界 | BPF maps 或栈访问越界 | 检查数组索引范围、栈空间不超过 512 字节 |
bpftrace 输出全是 lost events | 事件速率超过 perf ring buffer 消费能力 | 减少打印内容、在内核态预聚合(map 计数替代 printf),或增大 buffer |
kprobe 挂载后报 symbol not found | 函数被内联或在不同内核版本中改名/删除 | 用 bpftrace -l 'kprobe:*func*' 查找实际符号名,或改用 tracepoint/fentry |
| 容器内无法运行 bcc/bpftrace | 容器缺少 CAP_BPF/CAP_SYS_ADMIN 或访问不了 /sys/kernel/btf | privileged 模式或给容器添加 capability;BPF 工具一般建议在宿主机运行 |
| bcc 工具输出乱码/数字地址 | 缺少内核符号表(kallsyms 未导出)或 kptr_restrict 开启 | 确认 /proc/kallsyms 可读、sysctl kernel.kptr_restrict=0(安全环境慎用) |
最佳实践
| 实践 | 原理 | 示例 |
|---|---|---|
| 生产环境优先用 tracepoint | tracepoint 是内核承诺稳定的 ABI,kprobe 的函数名可能因版本变化 | tracepoint:syscalls:* 替代 kprobe:sys_read |
| 使用 bcc 工具替代手写 eBPF | bcc 封装了编译+加载+map 交互样板代码,开箱即用 | execsnoop-bpfcc 替代手写 eBPF + C 加载器 |
| bpftrace 用于快速诊断 | 一行流脚本适合 ad-hoc 排查,不适合生产长期运行 | bpftrace -e 'kprobe:do_sys_open { printf("%s\n", str(arg1)); }' |
| 限制 eBPF 程序复杂度 | Verifier 对指令数和栈空间有严格限制,复杂逻辑应放用户空间 | 内核态只做采集和过滤,聚合计算在用户态完成 |
| 利用 BPF maps 做数据降噪 | 在内核态做预聚合可以减少 perf event 环形缓冲区压力 | 用 per-CPU hash map 做计数器,避免逐条上报 |
| 内核 >= 5.4 以获取完整特性 | BPF CO-RE(一次编译到处运行)从 5.4 开始稳定 | 使用 bpftool gen skeleton 生成 BTF 驱动的加载骨架 |
| 先 bpftrace -l 再写探针 | 内核版本不同,符号名与参数会有差异 | 写一行式前先 bpftrace -l / -lv 确认探针存在与字段名 |
| 追踪脚本设置明确的过滤条件 | 全量追踪(所有 PID、所有文件)日志量巨大 | 追加过滤器:/comm == "nginx"/、/pid == 1234/、/arg2 > 4096/ |
| 长期监控用 bcc 程序而非 bpftrace 一行流 | 一行流无持久化与告警能力,适合 ad-hoc 排查 | 生产监控落地为 bcc/Python 工具 + Prometheus 指标或定期任务 |
| 关注 Verifier 报错的第一个条目 | Verifier 报错是路径敏感的,只修后面报错会原地打转 | 按报错顺序逐个修复;复杂逻辑尽量移入用户空间 |
练习题
- (概念)eBPF Verifier 主要检查程序的哪三类安全性问题?
- (概念)tracepoint 和 kprobe 的区别是什么?生产环境为什么更推荐 tracepoint?
- (实操)安装 bcc 工具集,使用
execsnoop-bpfcc观察当你运行ls命令时触发的事件。 - (实操)用 bpftrace 写一行命令,统计系统上所有进程调用
write系统调用的次数(按进程名分组)。 - (实操)使用
tcplife-bpfcc观察当前系统的 TCP 连接,找到一条从本机到外部服务的连接记录并解释各字段。 - (🔍 挑战)在一个 K8s 集群上部署 Cilium 替换 CNI,用
cilium connectivity test验证网络连通性,对比替换前后kubectl exec跨节点通信延迟的变化。
点击查看答案
- 三类安全性检查:① 有穷终止(拒绝无限循环);② 内存安全(指针范围验证、禁止越界);③ 栈深度限制(512 字节,禁止递归)。
- tracepoint 是内核预定义稳定接口,参数和含义随内核版本契约固定,适合生产环境。kprobe 可挂载绝大多数内核函数(未被内联且未列入黑名单的函数),但无稳定 ABI,内核升级可能断裂。
execsnoop-bpfcc在另一个终端执行ls后,输出显示ls进程的 PID、PPID、命令行参数和返回码。bpftrace -e 'kprobe:sys_write { @[comm] = count(); }'按进程名统计 write 调用次数,按 Ctrl+C 输出汇总表。tcplife-bpfcc输出每行包含 LADDR/LPORT/RADDR/RPORT/SENT/RECV 等字段。可通过对比ss -tnp确认连接存在。- Cilium 安装后
cilium connectivity test跑通过 pod-to-pod 和 pod-to-service 测试。eBPF 替代 iptables 后延迟通常降低 30-50%。
学习检查点
学完本章后,请检验自己是否掌握以下内容:
| 检查项 | 自测问题 | 验证方法 |
|---|---|---|
| 概念理解 | 能用自己的话解释 eBPF 的沙箱执行模型和 BPF maps 机制 | 尝试向他人讲解 |
| 命令操作 | 能不查文档完成 bpftrace 脚本编写和 bpftool 使用 | 在终端实际执行 |
| 原理掌握 | 能说出 eBPF 程序的加载、验证和挂载流程 | 画出流程图 |
| 故障排查 | 能独立排查 eBPF 程序加载失败或 verifier 拒绝的问题 | 模拟故障并修复 |
| 最佳实践 | 能说明为什么 eBPF 是可观测性和网络性能调优的理想选择 | 对比不同方案 |
本章总结
eBPF 是 Linux 内核近十年最重要的创新之一,它通过安全的内核沙箱化执行框架,重新定义了操作系统可观测性。核心流程为:C 编写 → LLVM 编译为字节码 → Verifier 安全验证 → JIT 编译为本地码 → 挂载到事件源 → 通过 BPF maps 与用户空间交互。
速查路线:
- 日常排错 →
bcc 工具(execsnoop、biolatency、tcplife) - 深度诊断 →
bpftrace一行流脚本 - 企业部署 →
Cilium(网络)+Tetragon(安全)
速查表
| 命令/工具 | 用途 |
|---|---|
execsnoop-bpfcc | 追踪新进程创建 |
biolatency-bpfcc | 磁盘 I/O 延迟分布直方图 |
tcplife-bpfcc | TCP 连接生命周期追踪 |
oomkill-bpfcc | OOM Killer 触发追踪 |
fileslower-bpfcc <ms> | 追踪慢文件 I/O |
bpftrace -e '...' | 一行流 eBPF 程序 |
bpftrace -l 'tracepoint:*' | 列出可用 tracepoint |
bpftrace -lv '<probe>' | 查看探针参数字段 |
bpftool prog list | 列出已加载的 BPF 程序 |
学习路径建议
- 学完本章后,建议继续学习 6.2:Kubernetes 入门 Kubernetes 网络原理理解 Cilium 的角色
- 进阶阅读:Cilium 官方文档
- 安全审计方向可读 5.13:auditd 审计 auditd 审计系统,对比 eBPF 方案的优劣
延伸阅读
- eBPF 官方网站
- bcc 项目 GitHub
- bpftrace 项目 GitHub
- Cilium 官方文档
- Tetragon 文档
- 推荐书籍:BPF Performance Tools (Brendan Gregg, 2019)