第1章 BPF 简介
可观测性的新纪元
章节导读
想象你是一名医生,病人告诉你他胸口疼。你手里只有一支体温计——你只能测量体温,而体温正常并不能排除心梗。这就是 2010 年之前 Linux 性能工程师的处境:系统出了问题,延迟飙升、吞吐下降,但你手里的工具只能看到 CPU 利用率、内存占用、磁盘 IOPS 这些粗粒度的计数器。你知道"系统慢了",却不知道"为什么慢"、"慢在哪一行代码"、"哪一次系统调用花了 500 毫秒"。内核对你来说是一个黑盒,应用程序对你来说也是一个黑盒。
BPF(Berkeley Packet Filter)的出现彻底改变了这一切。它最初只是 1992 年 Steven McCanne 和 Van Jacobson 在劳伦斯伯克利国家实验室发明的一个小小的包过滤器,用来让 tcpdump 在内核里高效地筛选网络包。谁也没想到,二十多年后,这个技术会演化成 Linux 内核中最强大的通用编程框架——eBPF(extended BPF),被 Brendan Gregg 称为"Linux 的超级能力",被业界比作"内核里的 JavaScript"。
2014 年,Alexei Starovoitov 向 Linux 3.15+ 内核提交了 eBPF 的重构:寄存器从 2 个扩展到 11 个 64 位寄存器,指令集重新设计,加入了 BPF maps(内核态与用户态共享的数据结构)、辅助函数调用(可以调用内核的一小部分安全函数)、以及最关键的——验证器(verifier)。验证器在程序加载时静态分析字节码,证明程序不会崩溃内核、不会死循环、不会越界访问内存。有了这个安全保证,内核开发者才敢让用户程序在内核里运行。JIT 编译器再把验证过的字节码翻译成本地机器指令,使 BPF 程序的执行速度接近原生内核 C 代码。
这套机制的意义在于:你现在可以在不修改内核源码、不加载内核模块、不重启任何进程的前提下,把自定义的追踪逻辑"注入"到内核的任意函数、任意 tracepoint、任意系统调用的入口和出口,甚至用户态程序的任意函数。注入的程序以纳秒级的开销执行,把统计结果聚合在内核态的 map 里,用户态工具再定期读取。这就是现代 Linux 可观测性的技术基石。
Brendan Gregg 是这场革命的亲历者和推动者。他在 Sun Microsystems 期间是 DTrace 的核心开发者之一——DTrace 是 Solaris 在 2003 年推出的动态追踪框架,曾经让 Solaris 成为性能分析领域最令人羡慕的操作系统。当 Gregg 2010 年加入 Netflix 时,他发现 Linux 上没有任何东西能接近 DTrace 的能力:SystemTap 需要编译内核模块,有崩溃风险,部署周期以天计;perf_events 只能做采样剖析和有限的静态追踪;strace 基于 ptrace,每个系统调用都要两次上下文切换,开销高达数倍,在生产环境根本不可用。Gregg 看到了 eBPF 的潜力,从 2014 年开始在 Netflix 大力推动 BPF 追踪工具的开发,创造了 execsnoop、opensnoop、biosnoop、tcplife、runqlat 等数十个如今已成为行业标准的工具,并在 2016 年与 IO Visor 社区一起打造了 BCC(BPF Compiler Collection)工具链。
本书《BPF Performance Tools》正是这场十年革命的集大成之作。它不是一本讲 BPF 网络编程的书(虽然 BPF 在 XDP、tc、Cilium 等网络场景中同样大放异彩),而是专注于 BPF 最成熟、最广泛的应用——性能分析与可观测性。全书覆盖 CPU、内存、磁盘、文件系统、网络、应用程序、容器与云环境,介绍了 100 多个现成工具的用法和原理,并教你如何用自己的工具解决书上没有覆盖的问题。
为什么可观测性如此重要?Gregg 在书中反复强调一个观点:性能问题的排查效率取决于你能提出并回答问题的速度。传统工具只能回答预先设计好的问题(CPU 利用率是多少?),而当故障模式超出预期时(为什么这个进程的调度延迟偶尔达到 80 毫秒?),你就束手无策了。BPF 让你可以在故障现场即时编写追踪逻辑,回答任何你能想到的问题。这种"现场提问、现场回答"的能力,把性能排查从"猜测-验证"的漫长循环变成了"观察-定位"的直接过程。Netflix、Facebook、Google、阿里巴巴、字节跳动等公司都在生产环境的数十万台服务器上常态化运行 BPF 追踪工具,用于持续性能监控、异常检测和故障诊断。
本章将带你建立对 BPF 的整体认知:它从哪里来,解决了什么问题,与传统工具相比优势何在,生态系统由哪些部分组成,以及它的能力边界在哪里。后续章节将逐层深入技术原理和实战应用。
核心概念
- eBPF
- BCC
- bpftrace
- 动态追踪
- 内核虚拟机
- 可观测性
要点精析
【BPF 的历史演进:从包过滤器到内核虚拟机】BPF 的故事始于 1992 年,McCanne 和 Jacobson 发表论文《The BSD Packet Filter: A New Architecture for User-level Packet Capture》,提出了 cBPF(classic BPF):一个运行在内核里的极简虚拟机,只有 2 个寄存器、32 位字长,专门用于网络包过滤。tcpdump 的过滤表达式(如 tcp port 80)就是被编译成 cBPF 字节码在内核执行的。这个设计存活了二十多年,直到 2014 年 Alexei Starovoitov 提交 eBPF:寄存器扩展到 11 个 64 位寄存器(R0-R10,R10 是只读栈指针),指令集扩展到 11 条基础指令类别,支持函数调用、循环(后来放开)、map 数据结构和数百个辅助函数。eBPF 与 cBPF 的关系就像 Java 字节码与某种只有加减法的计算器——名字相同,本质已完全不同。如今内核中 cBPF 程序也会被透明地转换为 eBPF 执行。理解这段历史很重要:当人们说"BPF"时,现代语境下几乎总是指 eBPF。
【BPF 的革命性意义:安全、高效、无需修改内核】在 BPF 之前,想在内核里执行自定义逻辑只有三条路:一是修改内核源码重新编译,周期以周计,且云环境根本不可行;二是编写内核模块(LKM),一个空指针就能让整台机器 panic,生产环境严禁随意加载;三是使用 SystemTap 这类把脚本编译成内核模块的框架,继承了模块的所有风险,2009 年 Red Hat 甚至因为稳定性问题在某些场景禁用了它。BPF 的突破在于验证器提供了数学层面的安全证明:程序加载前,验证器会模拟执行所有可能路径,检查无越界内存访问、无未初始化寄存器读取、指令数有限(早期 4096 条,现在 100 万条)、循环必然终止(有界循环)。通过验证的程序被 JIT 编译成本地机器码,执行开销以纳秒计。这意味着 BPF 程序"不可能搞垮内核"(理论上),从而第一次让在生产环境内核里动态注入自定义逻辑成为可接受的工程实践。
【BPF 能做什么:五大应用领域】BPF 的应用已远超性能追踪,书中聚焦可观测性,但完整的图景包括:(1) 性能追踪与剖析——kprobe/uprobe/tracepoint 挂载,事件计数、延迟直方图、火焰图,这是本书的主线;(2) 网络——XDP(eXpress Data Path)在网卡驱动层以每秒数千万包的速度处理数据包,用于 DDoS 防御和负载均衡(Facebook 的 Katran 单机转发 4000 万 pps),tc(traffic control)层做流量整形,Cilium 用 BPF 完全替代了 kube-proxy 的 iptables;(3) 安全——seccomp-BPF 过滤系统调用,LSM BPF(5.7+)实现内核级安全策略,Tetragon、Falco 等运行时安全产品基于 BPF 检测入侵行为;(4) 调度器扩展——sched_ext(6.12+)允许用 BPF 编写自定义 CPU 调度策略;(5) 硬件与驱动——hid、rdma 等子系统的可编程化。对性能工程师而言,最重要的是第一类,但了解全貌有助于理解为什么 BPF 社区发展如此迅速。
【与传统工具对比之一:strace 的致命开销】strace 基于 ptrace 机制:目标进程每执行一个系统调用,都会陷入停止状态(PTRACE_SYSCALL),等待 strace 进程被调度起来读取寄存器、格式化输出,然后再恢复目标进程。每个系统调用引入至少两次上下文切换和两次调度延迟,实测开销可达 2 倍以上——一个每秒执行 10 万次系统调用的应用,用 strace 追踪后吞吐量可能下降 50% 甚至更多。更糟的是,ptrace 会改变目标进程的调度行为,可能掩盖或制造竞态问题。这就是为什么生产环境几乎禁用 strace。BPF 追踪系统调用(如 syscount、trace 工具)在内核态直接聚合统计,每个事件的开销约几百纳秒到 1 微秒,对应用吞吐的影响通常在 1% 以内。书中给出的对比数据:追踪全部系统调用时,strace 使目标应用慢 2.14 倍,而 BPF 的 syscount 只慢 1% 左右。这个数量级差异是 BPF 能在生产环境常态化运行的根本原因。
【与传统工具对比之二:perf、SystemTap 与 DTrace】perf_events 是 Linux 内核自带的性能分析框架,支持硬件 PMC 采样、软件事件、静态 tracepoint 和 kprobe 动态追踪,功能强大但使用复杂,且其 kprobe 输出是原始事件流,缺乏内核态聚合能力——事件必须逐个传回用户态处理,高频事件下丢样本严重。SystemTap 把脚本编译成内核模块,能力接近 DTrace,但编译需要 kernel-devel 包和数分钟时间,模块加载有崩溃风险,在容器化和云环境几乎无法部署。DTrace 是 Solaris 的标杆(后移植到 macOS 和 FreeBSD,Linux 移植始终不成功),提供了生产级安全的动态追踪,Gregg 曾称其为"性能分析的显微镜"。eBPF 本质上是把 DTrace 的理念以 Linux 原生的方式重新实现:动态挂载探针、内核态聚合、生产环境安全。今天可以说 Linux 的 BPF 生态在工具丰富度上已经超越了 DTrace。
【BPF 生态:BCC、bpftrace、libbpf 与 CO-RE】BPF 程序用受限的 C 语言编写,编译成字节码需要 LLVM/Clang 工具链,直接面向普通工程师门槛太高。生态层解决了这个问题:(1) BCC(BPF Compiler Collection)——2016 年由 IO Visor 推出,Python/Lua 前端 + 内嵌 C 编译器,运行时把 C 代码编译成 BPF 字节码,附带 100+ 现成工具,缺点是目标机器需要安装编译工具链(约 200MB),启动慢;(2) bpftrace——高级追踪语言,类似 DTrace 的单行脚本语法,一行命令即可完成复杂追踪,底层仍用 LLVM 编译,适合交互式排查;(3) libbpf——C 库,加载预编译的 BPF 对象文件,无运行时编译依赖,启动毫秒级,适合产品化集成;(4) CO-RE(Compile Once, Run Everywhere)——基于 BTF(BPF Type Format)类型信息,配合 libbpf 实现"一次编译、到处运行",解决了 BPF 程序依赖内核数据结构布局、换内核就要重编译的老大难问题。学习路径建议:先用 BCC 工具和 bpftrace 解决问题,再学 libbpf+CO-RE 做产品化。
【BPF 程序的基本工作原理】一个 BPF 追踪程序的完整生命周期:用户态工具(如 bpftrace)把脚本编译成 BPF 字节码,通过 bpf() 系统调用提交给内核;内核验证器检查字节码安全性,通过后 JIT 编译成机器码;程序被 attach 到指定挂载点——kprobe(内核函数入口)、kretprobe(内核函数返回)、tracepoint(内核静态追踪点)、uprobe(用户态函数入口)、perf_event(定时器或 PMC 事件)等;挂载点触发时,BPF 程序以内核态权限执行,可以读取寄存器、内核数据结构、调用辅助函数,把结果写入 map;用户态程序通过共享的 map 读取聚合结果。整个过程目标应用毫无感知,不需要重启、不需要符号表配合(虽然符号解析需要)、不需要任何插桩。一个典型的 bpftrace 单行脚本从敲下回车到开始输出数据只需要一两秒。
【真实案例:Netflix 的 BPF 生产实践】Netflix 是 BPF 性能工具最早的大规模生产用户。他们的典型用法包括:(1) 持续剖析——在全部服务器上以 99Hz 频率运行 profile 工具采集 CPU 栈,生成全机群的火焰图,用于容量规划和异常检测,CPU 开销低于 1%;(2) 异常检测——用 runqlat 监控调度延迟直方图,一旦尾延迟异常立即告警,曾借此发现某代硬件的固件 bug 导致的偶发 100ms 级调度停顿;(3) 故障诊断——当某个服务延迟毛刺时,工程师用 offcputime 直接看到线程阻塞在哪个内核栈上,把原本需要数天的排查缩短到数分钟。Gregg 在书中强调,BPF 工具的设计目标就是"生产环境安全":所有工具默认在内核态聚合数据,只把统计结果传回用户态,避免高频事件淹没系统;工具本身有超时和速率保护。这种设计哲学使得 BPF 工具可以像 top 一样随手使用,而不需要审批和变更窗口。
【BPF 的局限性与适用边界】BPF 不是万能的,书中诚实地列出了它的边界:(1) 内核版本依赖——eBPF 能力随内核版本演进,4.1 以下几乎不可用,4.9 是第一个"够用"的版本(支持 bpf_probe_read_str、perf buffer 改进等),5.x 之后才有 CO-RE、BTF、环形缓冲等现代特性,老系统(如 CentOS 6)无法享受;(2) 验证器限制——程序复杂度受限,早期不支持循环,栈只有 512 字节,验证器错误信息晦涩难懂,复杂逻辑需要绕着写;(3) 符号依赖——uprobe 追踪用户态函数需要二进制文件保留符号表或有 debuginfo,strip 过的 Go 程序或 PIE 可执行文件会增加追踪难度;(4) 无法修改执行逻辑——追踪型 BPF 程序主要读取状态,虽然 bpf_override_return 可以改变内核函数返回值,但仅限白名单函数且需内核开启 CONFIG_BPF_KPROBE_OVERRIDE;(5) 高频事件的输出瓶颈——每秒百万级事件时,perf buffer 传输和用户态处理会成为瓶颈,需要更激进的内核态聚合。理解这些边界,才能在正确的场景选择正确的工具。
【可观测性方法论:工具只是手段】Gregg 在书中反复强调,BPF 工具必须结合方法论使用,否则会陷入"拿着锤子找钉子"的困境。他提出的经典方法论包括:(1) USE 方法——对每种资源(CPU、内存、磁盘、网络)检查利用率(Utilization)、饱和度(Saturation)、错误(Errors),BPF 工具极大增强了饱和度维度的测量能力(如 runqlat 测 CPU 饱和、biolatency 测磁盘饱和);(2) 静态性能追踪点清单——利用内核已有的 tracepoint 建立检查清单;(3) 60 秒检查法——uptime、dmesg、vmstat、mpstat、pidstat、iostat、free、sar、top 的快速巡检,BPF 工具(如 execsnoop、opensnoop、tcplife)可以补充传统命令看不到的瞬态事件。方法论的核心思想是:先系统性地排除大部分可能性,再用 BPF 的显微镜深入观察可疑点。工具给你数据,方法论告诉你该看哪里、数据意味着什么。本书每一章都遵循"方法论 → 工具 → 原理 → 实验"的结构。
代码示例
/* 示例1:bpftrace 统计系统调用次数(syscount 的极简版) */
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
/* 按 Ctrl-C 结束,输出示例:
@[sshd]: 12
@[bash]: 340
@[node]: 15234
*/这个单行脚本展示了 bpftrace 的核心编程模型:探针-动作对。tracepoint:raw_syscalls:sys_enter 是挂载点,即内核中所有系统调用的统一入口 tracepoint;花括号内是动作,comm 是内置变量(当前进程名),@[comm] 是一个以进程名为键的 map,count() 是聚合函数。整个统计在内核态完成,用户态只在结束时读取 map 并排序输出。对比 strace -c 得到同样的统计,strace 会让被追踪应用慢数倍,而这个脚本的开销几乎可以忽略——每个系统调用只增加约 0.5-1 微秒的 BPF 程序执行时间。注意 @[comm] 中的 @ 表示这是一个 BPF map(持久化到脚本结束),而普通变量如 $x 则是每次触发临时的。这个五行都不到的脚本,用 SystemTap 需要写十几行并编译一分钟,用内核模块则需要几百行 C 代码加上一天的开发和测试。这就是 BPF 生态把动态追踪门槛降到"一行命令"的意义。/* 示例2:追踪进程执行(execsnoop 的核心逻辑) */
bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(args->filename)); }'
/* 输出示例:
bash -> /usr/bin/ls
cron -> /usr/sbin/sendmail
*/execsnoop 追踪所有新进程的执行,这在排查"谁在偷偷启动进程"时极其有用——短生命周期进程(如 cron 任务、监控脚本、甚至恶意软件)在 top/ps 的采样间隔中往往一闪而过,完全不可见,而 execsnoop 基于事件触发,一个都不会漏。这个脚本挂载在 syscalls:sys_enter_execve 这个静态 tracepoint 上,args->filename 是 tracepoint 提供的参数(要执行的文件路径),str() 把内核指针转换为字符串。comm 是触发 execve 的父进程名。生产案例:某公司发现服务器 CPU 周期性尖峰,top 抓不到元凶,用 execsnoop 一跑,发现是某个遗留监控 agent 每 10 秒 fork 十几个子进程做采集,每次 fork+exec 的成本叠加造成了尖峰。tracepoint 与 kprobe 的区别:tracepoint 是内核开发者显式埋设的稳定接口,跨版本兼容;kprobe 可以挂任意函数但接口随内核版本变化。优先使用 tracepoint。/* 示例3:BCC 工具直接使用——opensnoop 查看文件打开事件 */
/usr/share/bcc/tools/opensnoop
/* 输出示例:
PID COMM FD ERR PATH
1479 supervisord 12 0 /var/log/app/error.log
2033 nginx -1 2 /etc/nginx/conf.d/
*/opensnoop 是 BCC 工具集中最常用的工具之一,追踪所有 open/openat 系统调用。输出的 FD 列是返回的文件描述符,ERR 列是 errno——FD 为 -1 且 ERR 为 2 表示 ENOENT(文件不存在),这是排查"应用为什么报错找不到配置文件"的最快方法。上面的例子中 nginx 尝试打开 /etc/nginx/conf.d/ 目录失败(open 不能打开目录,返回 EISDIR 或被后续处理),而 supervisord 正常打开了日志文件。BCC 工具都安装在 /usr/share/bcc/tools/ 下,Ubuntu 上 apt install bpfcc-tools 即可(命令带 -bpfcc 后缀),CentOS 上 yum install bcc-tools。每个工具都带 --help 和 man 手册。opensnoop 的底层实现是挂载 tracepoint:syscalls:sys_enter_openat 和 sys_exit_openat,用 map 在 enter/exit 之间传递参数,只把结果通过 perf buffer 发回用户态。注意:在文件操作极频繁的服务器上,opensnoop 的输出量可能很大,可以用 -p 限定 PID 或 -n 按进程名过滤。/* 示例4:bpftrace 统计块设备 I/O 延迟直方图(biolatency 核心逻辑) */
bpftrace -e 'kprobe:blk_account_io_start { @start[arg0] = nsecs; }
kprobe:blk_account_io_done { @ns = hist(nsecs - @start[arg0]); delete(@start[arg0]); }'
/* 输出示例:
@ns:
[256, 512) 12 |@@@ |
[512, 1K) 45 |@@@@@@@@@@@@@ |
[1M, 2M) 3 | | <- 异常慢的 I/O
*/这个脚本展示了 BPF 性能工具的标志性模式——延迟直方图。第一个 kprobe 挂在块层 I/O 开始函数上,把当前时间戳(nsecs 内置变量)以请求结构指针 arg0 为键存入 map;第二个 kprobe 挂在 I/O 完成函数上,用当前时间减去开始时间得到延迟,hist() 聚合成 2 的幂次分桶的直方图。直方图是性能分析的利器:平均值会掩盖问题,一个平均延迟 1ms 的磁盘可能 99.9% 的 I/O 在 0.5ms 内完成,但 0.1% 的 I/O 花了 100ms——对延迟敏感的应用来说这 0.1% 就是灾难。直方图让长尾一目了然。上面输出中 [1M, 2M) 桶里的 3 个 I/O(延迟 1-2 毫秒以上量级)就是需要关注的异常。注意 kprobe 的函数名和参数随内核版本变化(blk_account_io_start 在新内核中已被移除,改用 block tracepoint),这正是第 3 章要讲的 CO-RE 试图解决的问题之一。生产环境建议直接使用成熟的 biolatency 工具而非手写脚本。/* 示例5:用 bpftrace 追踪用户态函数(uprobe)——统计 libc malloc 调用 */
bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc { @[ustack, comm] = count(); }'
/* 输出示例:
@[
malloc+0
PyEval_EvalFrameEx+0x1234
...
], python: 8923
*/uprobe 让 BPF 的触角延伸到用户态:任何共享库或可执行文件的任何函数(只要有符号)都可以被追踪。这个脚本统计 malloc 调用的来源栈,ustack 内置变量抓取用户态调用栈,配合 comm 可以回答"哪个进程的哪段代码在疯狂分配内存"。这是排查内存泄漏的第一步——第 7 章的 memleak 工具就是这个思路的完整实现。uprobe 的原理是在目标函数入口注入断点指令(int3),进程执行到该处触发陷入,内核运行 BPF 程序后恢复执行。注意开销:每次函数调用触发一次断点陷阱,约 1-2 微秒,对 malloc 这种每秒可能调用百万次的热函数,全量追踪会显著拖慢应用,生产环境应谨慎或改用采样。uprobe 的限制:目标二进制需要有符号表(strip 后只能按地址追踪);对静态链接的 Go 程序,函数名格式是 main.foo 而非 C 风格;多线程高频调用的函数追踪开销会放大。/* 示例6:BCC Python 脚本骨架——理解工具的内部结构 */
from bcc import BPF
prog = """
int kprobe__blk_account_io_done(struct pt_regs *ctx, struct request *req) {
u64 ts = bpf_ktime_get_ns();
bpf_trace_printk("io done: %llu\\n", ts);
return 0;
}
"""
b = BPF(text=prog)
b.trace_print()这段 Python 代码是 BCC 工具的最小骨架:内嵌的 C 字符串是 BPF 程序,BPF(text=prog) 触发运行时编译(调用 Clang 把 C 编译成 BPF 字节码)并加载进内核,函数名 kprobe__blk_account_io_done 的双下划线前缀是 BCC 的约定,自动把程序 attach 到同名内核函数的 kprobe 上。bpf_trace_printk 把数据写入内核的 trace_pipe,用户态的 trace_print() 循环读取并打印——这是最简单的数据传输方式,但开销较大(每次 printk 约几微秒且有全局锁),正式工具都改用 perf buffer 或 ring buffer。理解这个骨架很重要:BCC 的 100+ 工具本质上都是"C 语言 BPF 程序 + Python 胶水代码"的组合,Python 负责参数解析、编译加载、读取 map、格式化输出。第 12 章将手把手教你写完整的 BCC 工具。学习建议:挑一个你常用的 BCC 工具(比如 execsnoop.py),直接阅读它的源码——通常只有三四百行,是最好的教材。实验笔记
本实验的目标是搭建 BPF 开发环境,亲手运行第一批 BPF 工具,并定量感受 BPF 与传统工具的开销差异。预计耗时 2-3 小时。
【环境准备】你需要一台内核版本 4.9 以上的 Linux 机器(推荐 5.x+),物理机、虚拟机或云主机均可(容器内运行 BPF 需要特权模式,不推荐初学者使用)。检查内核版本:uname -r。Ubuntu 20.04+/Debian 11+ 用户执行:sudo apt install -y bpfcc-tools linux-headers-$(uname -r) bpftrace,注意 Ubuntu 上 BCC 工具命令带 -bpfcc 后缀(如 execsnoop-bpfcc)。CentOS/RHEL 8+ 用户:sudo dnf install -y bcc-tools bpftrace kernel-devel-$(uname -r),工具位于 /usr/share/bcc/tools/。安装后运行 hello world 验证:sudo bpftrace -e 'BEGIN { printf("hello BPF\n"); }',看到输出即环境就绪。如果 bpftrace 报 CO-RE/BTF 相关错误,检查 /sys/kernel/btf/vmlinux 是否存在,不存在则安装 pahole 1.16+ 并确认内核开启 CONFIG_DEBUG_INFO_BTF。
【实验一:观察系统的"暗物质"】打开两个终端。终端 1 运行 sudo execsnoop-bpfcc(或 execsnoop),终端 2 随便执行几条命令(ls、cat、ps)。观察终端 1 实时打印的每个新进程。然后思考:在不用 execsnoop 的情况下,你能发现一个只存活 50 毫秒的进程吗?再运行 sudo opensnoop-bpfcc -T(-T 显示时间戳),观察 10 秒钟内系统打开了哪些文件——你会惊讶地发现即使"空闲"的系统也有大量文件访问(监控 agent、日志轮转、systemd 定时器)。这就是事件追踪与采样观察的本质区别:ps/top 是每隔几秒拍一张照片,而 BPF 工具是全程录像。
【实验二:定量对比 strace 与 BPF 的开销】写一个简单的系统调用密集型程序:用 C 或 Python 循环调用 getpid() 一百万次并计时(Python 版本:import os,time; t=time.time(); [os.getpid() for _ in range(1000000)]; print(time.time()-t))。分三种情况测量耗时:(1) 裸跑;(2) strace -c 包裹运行;(3) 运行时另一个终端执行 sudo bpftrace -e 'tracepoint:raw_syscalls:sys_enter /pid == TARGET_PID/ { @ = count(); }'(把 TARGET_PID 换成目标进程 PID,或者不加过滤直接统计)。记录三组时间。典型结果:strace 使耗时膨胀 5-50 倍(Python 的 getpid 极轻量,strace 开销占比巨大),而 BPF 追踪的额外开销通常低于 10%。把数据写进实验报告,这个数量级差异就是"BPF 可用于生产、strace 不可"的实证依据。
【实验三:第一次延迟直方图】运行 sudo /usr/share/bcc/tools/biolatency 10 1(每 10 秒输出一次直方图),同时用 fio 或 dd 制造磁盘 I/O(如 fio --name=test --rw=randread --bs=4k --iodepth=32 --filename=/dev/sda --direct=1 --runtime=30 --time_based,注意会读裸设备,虚拟机环境请改用文件)。观察直方图分布如何随 I/O 压力变化。再运行 sudo biolatency -Q 10 1 对比队列时间。思考:为什么直方图用 2 的幂次分桶?(答案在第 4 章:对数分桶能同时展示微秒级和秒级延迟,且内核态计算只需一次位移。)
【实验四:阅读一个真实工具的源码】打开 /usr/share/bcc/tools/execsnoop 的 Python 源码(约 300 行),找到内嵌的 C 程序部分,识别出:tracepoint 挂载点、events map 的定义、perf buffer 的使用。对照 man execsnoop 理解每个命令行参数如何映射到 BPF 程序的过滤逻辑。这一步是从"工具使用者"迈向"工具开发者"的关键。
【实验记录要求】记录你的内核版本、每个实验的命令与输出摘录、实验二的三组耗时数据、以及至少三个"我以前不知道系统在做这件事"的观察。遇到问题先查 bpftrace 的报错信息(验证器错误会在第 3 章详细解读),再搜索 iovisor/bcc 的 GitHub issues。
延伸阅读
- Brendan Gregg, "BPF Performance Tools" 第 1 章原文——本章内容基于该书第 1 章扩展,原书还包含更详细的工具分类表(按 USE 方法的资源维度组织)和 BPF 依赖的内核版本特性对照表,建议对照阅读。
- Steven McCanne & Van Jacobson, "The BSD Packet Filter: A New Architecture for User-level Packet Capture" (1993)——cBPF 的原始论文,只有 10 页,了解 BPF 的起点,体会当年"让用户程序在内核安全运行"这个想法有多超前。
- Brendan Gregg 的博客文章 "Linux eBPF" 与 "eBPF: One Small Step" (brendangregg.com)——Gregg 本人撰写的 eBPF 科普与历史回顾,包含 DTrace 与 eBPF 的第一手对比视角,文风生动,是理解作者思想的最好材料。
- Alexei Starovoitov 的 eBPF 原始补丁集说明 (lwn.net, 2014)——搜索 "eBPF: the next generation of BPF",了解设计者本人的动机:为什么寄存器要 11 个、为什么必须有验证器、map 的设计考量。
- bpftrace 官方参考指南 (github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md)——bpftrace 语言的权威文档,涵盖全部探针类型、内置变量、函数和 map 操作,建议通读一遍建立语法全景,后续章节会频繁引用。
- BCC 项目主页与工具索引 (github.com/iovisor/bcc)——README 中有全部 100+ 工具的分类清单和一句话说明,这张表值得打印出来贴在工位上,遇到性能问题时先扫一遍寻找现成工具。
- Cilium eBPF 文档 (docs.cilium.io)——虽然本书聚焦可观测性,但 Cilium 项目维护的 eBPF 文档对理解 BPF 在网络领域的应用(以及为什么 Kubernetes 社区全面拥抱 eBPF)非常有价值,可作横向拓展。
- Liz Rice, "Learning eBPF" (O'Reilly, 2023)——较新的 eBPF 入门书,覆盖了本书出版后(2019)的新进展:ring buffer、CO-RE 成熟化、sched_ext 等,适合作为本书的补充读物。