eBPF 入门

eBPF 是服务器和系统管理员的一项强大工具,通常被描述为运行在内核中的轻量沙箱虚拟机。它常用于性能监控、安全和网络流量处理,无需修改或重新构建内核。

由于程序在内核空间运行,不需要上下文切换,相比用户空间方案速度很快。它还能访问内核数据结构,比只能使用内核暴露给用户空间接口的工具拥有更多能力。

BPF 是 Berkeley Packet Filter(伯克利包过滤器)的缩写,最初用于网络包过滤。它逐渐发展为扩展伯克利包过滤器 eBPF,增加了更多寄存器、64 位寄存器支持、数据存储结构(Maps)等能力。

因此,eBPF 的用途已经超出内核网络子系统,除了改善网络功能,还能用于跟踪、性能剖析、可观测性与安全。如今 BPF 和 eBPF 两个术语常常互换使用,都指 eBPF。

eBPF 如何工作

用户空间应用可以将 eBPF 字节码程序加载到内核。虽然可以直接编写字节码,但许多工具在 eBPF 之上提供了抽象层,替你生成字节码,再将它加载到内核,因而无需手写。

程序加载到内核后,运行之前必须通过内核验证。检查包括:

  • 验证程序会终止,不会陷入无限循环。
  • 检查整个程序中寄存器和栈状态的有效性,确保不会崩溃。
  • 确保加载程序的进程拥有运行该 eBPF 程序所需的全部 capabilities。

验证通过后,字节码会即时编译(JIT)为机器码,以优化执行效率。

Ubuntu Server 中的 eBPF

从 Ubuntu 24.04 LTS 开始,每次 Ubuntu Server 安装都会默认提供 bpftrace 和 bpfcc-tools(BPF Compiler Collection,简称 BCC),这是 Ubuntu 改善应用开发者和系统管理员体验的一部分。

在 Ubuntu 中,可以使用 BCC 等工具识别瓶颈、调查性能下降、跟踪特定函数调用,以及创建自定义监控工具,收集特定内核或用户空间进程的数据,而不中断正在运行的服务。

bpftrace 和 bpfcc-tools 都会安装一组完成不同任务的工具。除 bpftrace 本身外,可以用以下命令列出这些工具:

dpkg -L bpftrace bpfcc-tools | grep -E '/s?bin/.*$' | xargs -n1 basename

大多数工具都有相当完整的文档,手册通常包含可以立即尝试的示例。以 .bt 结尾的是 bpftrace 安装的脚本;它们是文本文件,可以直接阅读以理解具体任务的实现。以 -bpfcc 结尾的是 BCC 工具,使用 Python 编写,也可以像 .bt 文件一样查看。

bpftrace 脚本常展示如何以简洁方式完成复杂任务;对应的 -bpfcc 版本往往更高级,提供更多选项和定制能力。

在 /usr/share/doc/bpftrace/examples/ 与 /usr/share/doc/bpfcc-tools/examples/ 下,还可以找到描述用例及输出的文本文件。

例如:

# bashreadline.bt

该工具会打印系统中所有正在运行的 Bash shell 所执行的命令。eBPF 获得的信息可能是机密信息,因此这些操作需要以 root 身份运行。

bpfcc-tools 还提供 bashreadline-bpfcc,功能类似。按照上述命名,-bpfcc 工具是使用 BCC 的 Python 程序,.bt 工具是 bpftrace 脚本。许多工具同时提供这两个版本;阅读手册,必要时查看脚本,以选择更适合需求的一种。

示例:确定执行了哪些命令

execsnoop-bpfcc 简单却强大,可以回答一个常见、却本不该难以回答的问题:“这个操作最终还调用了哪些程序?”它可以帮助判断某个程序是否过于频繁地调用另一工具,或是否调用了意料之外的程序。

在 Ubuntu 中,它也常用于理解 .deb 软件包维护脚本执行时发生了什么。使用以下参数:

  • -Uu root:减少噪声,只关注 root 上下文中的操作,例如这里的软件包安装。
  • -T:在日志中显示时间。

在一个控制台中运行:

sudo execsnoop-bpfcc -Uu root -T

在另一个控制台触发希望观察的操作:

sudo apt install --reinstall vim-nox

第一个控制台中的 execsnoop 可能报告比预想更多的内容。原文示例输出如下:

TIME     UID   PCOMM            PID     PPID    RET ARGS
10:58:07 1000  sudo             1323101 1322857   0 /usr/bin/sudo apt install --reinstall vim-nox
10:58:10 0     apt              1323107 1323106   0 /usr/bin/apt install --reinstall vim-nox
10:58:10 0     dpkg             1323108 1323107   0 /usr/bin/dpkg --print-foreign-architectures
...
10:58:12 0     sh               1323134 1323107   0 /bin/sh -c /usr/sbin/dpkg-preconfigure --apt || true
...
10:58:13 0     tar              1323155 1323152   0 /usr/bin/tar -x -f  --warning=no-timestamp
10:58:14 0     vim-nox.prerm    1323157 1323150   0 /var/lib/dpkg/info/vim-nox.prerm upgrade 2:9.1.0016-1ubuntu7.3
10:58:14 0     dpkg-deb         1323158 1323150   0 /usr/bin/dpkg-deb --fsys-tarfile /var/cache/apt/archives/vim-nox_2%3a9.1.0016-1ubuntu7.3_amd64.deb
...
10:58:14 0     update-alternat  1323171 1323163   0 /usr/bin/update-alternatives --install /usr/bin/vimdiff vimdiff /usr/bin/vim.nox 40
...
10:58:17 0     snap             1323218 1323217   0 /usr/bin/snap advise-snap --from-apt
10:58:17 1000  git              1323224 1323223   0 /usr/bin/git rev-parse --abbrev-ref HEAD
10:58:17 1000  git              1323226 1323225   0 /usr/bin/git status --porcelain
10:58:17 1000  vte-urlencode-c  1323227 1322857   0 /usr/libexec/vte-urlencode-cwd

按需求修改 eBPF 工具

下面再看一个实际用途,然后通过修改工具,将这个例子发展为更复杂的用法。

示例:找出 QEMU 加载的文件

假设要验证某条 QEMU 命令运行时加载了哪些二进制文件。QEMU 非常复杂,有时很难从命令行判断它会使用 /usr/share/qemu 中哪些文件。直接指定 QEMU 命令行时已经不容易;再通过 libvirt、LXD,甚至更上层的 OpenStack 等抽象层使用时,问题会更复杂。

虽然可以用 strace,但它使用 ptrace,可能需要上下文切换,会为排查过程增加额外开销。若要持续监控系统,尤其是运行大量虚拟机的宿主机,strace 很快就会遇到限制。

可以改用 opensnoop 跟踪 open() 系统调用。这里选择 opensnoop-bpfcc,以获得更多参数来满足需求:

  • --full-path:对使用相对路径的 open 调用显示完整路径。
  • --name qemu-system-x86:只关注 QEMU 打开的文件。这里没有写 qemu-system-x86_64,是因为 opensnoop 未过滤的输出中,进程名称有长度限制,只能使用截断后的名称。

以下命令收集 QEMU 打开的文件:

sudo /usr/sbin/opensnoop-bpfcc --full-path --name qemu-system-x86

此时,无论在另一控制台,还是系统中的其他位置运行 QEMU,打开的文件都会被记录。例如通过 LXD 启动临时虚拟机:

lxc launch ubuntu-daily:n n-vm-test --ephemeral --vm

opensnoop 中会出现大量文件打开记录:

1308728 qemu-system-x86    -1   2 PID    COMM               FD ERR PATH
/snap/lxd/current/zfs-2.2/lib/glibc-hwcaps/x86-64-v3/libpixman-1.so.0
1308728 qemu-system-x86    -1   2 /snap/lxd/current/zfs-2.2/lib/glibc-hwcaps/x86-64-v2/libpixman-1.so.0
1308728 qemu-system-x86    -1   2 /snap/lxd/current/zfs-2.2/lib/tls/haswell/x86_64/libpixman-1.so.0
...
1313104 qemu-system-x86    58   0 /sys/dev/block/230:16/queue/zoned
1313104 qemu-system-x86    20   0 /dev/fd/4

QEMU 自然会打开很多对象,包括共享库、配置文件,以及 /{sys,dev,proc} 中的条目。借助 opensnoop-bpfcc,可以在整个系统范围内实时看到它们。

聚焦特定文件类型

假设只想知道加载了哪些 .bin 文件。虽然可以直接用 grep 过滤输出,但这里希望通过示例帮助你入门 eBPF,因此做一个最简单的改动:修改跟踪代码外层的 Python 包装程序。理解这一过程后,可以进一步修改 eBPF 代码本身,甚至从零编写适合自己需求的 eBPF 方案。

原文所用的 opensnoop-bpfcc 版本还没有按文件名过滤的选项,不过可以加上:

sudo cp /usr/sbin/opensnoop-bpfcc /usr/sbin/opensnoop-bpfcc.new
$ sudo vim /usr/sbin/opensnoop-bpfcc.new
...
$ diff -Naur /usr/sbin/opensnoop-bpfcc /usr/sbin/opensnoop-bpfcc.new
--- /usr/sbin/opensnoop-bpfcc	2025-10-24 12:17:00.000000000 +0000
+++ /usr/sbin/opensnoop-bpfcc.new	2026-01-19 10:45:13.620367441 +0000
@@ -40,6 +40,7 @@
     ./opensnoop -u 1000                # only trace UID 1000
     ./opensnoop -d 10                  # trace for 10 seconds only
     ./opensnoop -n main                # only print process names containing "main"
+    ./opensnoop -c path                # only print paths containing "fname"
     ./opensnoop -e                     # show extended fields
     ./opensnoop -f O_WRONLY -f O_RDWR  # only print calls for writing
     ./opensnoop -F                     # show full path for an open file with relative path
@@ -71,6 +72,9 @@
 parser.add_argument("-n", "--name",
     type=ArgString,
     help="only print process names containing this name")
+parser.add_argument("-c", "--contains",
+    type=ArgString,
+    help="only print paths containing this string (implies --full-path)")
 parser.add_argument("--ebpf", action="store_true",
     help=argparse.SUPPRESS)
 parser.add_argument("-e", "--extended_fields", action="store_true",
@@ -83,6 +87,8 @@
     help="size of the perf ring buffer "
         "(must be a power of two number of pages and defaults to 64)")
 args = parser.parse_args()
+if args.contains is not None:
+    args.full_path = True
 debug = 0
 if args.duration:
     args.duration = timedelta(seconds=int(args.duration))
@@ -478,6 +484,12 @@
         if args.name and bytes(args.name) not in event.comm:
             skip = True
 
+        paths = entries[event.id]
+        paths.reverse()
+        entire_path = os.path.join(*paths)
+        if args.contains and bytes(args.contains) not in entire_path:
+            skip = True
+
         if not skip:
             if args.timestamp:
                 delta = event.ts - initial_ts
@@ -502,9 +514,7 @@
             if not args.full_path:
                 printb(b"%s" % event.name)
             else:
-                paths = entries[event.id]
-                paths.reverse()
-                printb(b"%s" % os.path.join(*paths))
+                printb(b"%s" % entire_path)
 
         if args.full_path:
             try:

上面命令示例中的 $ 是 shell 提示符,输入命令时不要复制它。修改后的版本可以查找特定文件名,例如所有 .bin 文件:

sudo /usr/sbin/opensnoop-bpfcc.new --contains '.bin' --name qemu-system-x86
PID     COMM               FD ERR PATH
1316661 qemu-system-x86    21   0 /snap/lxd/current/share/qemu//kvmvapic.bin
1316661 qemu-system-x86    39   0 /snap/lxd/current/share/qemu//vgabios-virtio.bin
1316661 qemu-system-x86    39   0 /snap/lxd/current/share/qemu//vgabios-virtio.bin

推广到其他用途

与其他工具和示例一样,用途取决于你的需求与想象力。想知道复杂、相互关联的 Apache 配置到底加载了 /etc 中哪些文件?可以这样查看:

sudo /usr/sbin/opensnoop-bpfcc.new --name 'apache2' --contains '/etc'
PID     COMM               FD ERR PATH
1319357 apache2             3   0 /etc/apache2/apache2.conf
1319357 apache2             4   0 /etc/apache2/mods-enabled
1319357 apache2             4   0 /etc/apache2/mods-enabled/access_compat.load
...
1319357 apache2             4   0 /etc/apache2/ports.conf
1319357 apache2             4   0 /etc/apache2/conf-enabled
1319357 apache2             4   0 /etc/apache2/conf-enabled/charset.conf
1319357 apache2             4   0 /etc/apache2/conf-enabled/localized-error-pages.conf
...
1319357 apache2             4   0 /etc/apache2/sites-enabled/000-default.conf

eBPF 的限制

阅读上面的代码改动时要注意,与已有 --name 选项一样,这里的过滤发生在结果报告一侧,而不是事件生成一侧。因此应理解为什么可能收到这样的消息:

Possibly lost 84 samples

eBPF 程序在环形缓冲区中生成事件。如果事件产生速度超过用户空间进程的消费速度,部分事件会被覆盖而丢失。“Possibly lost … samples” 表示可能发生了这种情况。

几乎所有内核跟踪设施在概念上都有同样的问题。由于不能拖慢内核,不能要求工具“等我消费完再继续”。多数时候这可以接受;高级用法可能需要在 eBPF 一侧聚合数据,减少向用户空间传递的数据量。即便开销较低,eBPF 工具仍必须在缓冲、事件丢弃和 CPU 消耗之间取得平衡。上述工具的示例文档中也有相同讨论。

结语

eBPF 提供多种方式,可直接在内核空间中监控、调试和保护系统,兼具速度与对系统内部状态的可见性,无需中断运行中的服务。对于系统管理员和软件工程师,它是一项很有价值的工具。

延伸阅读

  • Ubuntu Summit 2024 的 eBPF 入门视频,后半部分介绍了面向 Kubernetes 的 eBPF 框架 Inspector Gadget。
  • eBPF 社区的“什么是 eBPF”,适合深入理解概念。
  • 内核上游文档,提供 eBPF 内部机制和接口的完整说明。
  • eBPF 社区整理的解决方案列表,其中许多与 Kubernetes 生态相关。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容