酝酿已久,终于有时间详细介绍eBPF。本文旨在简要阐述eBPF的核心功能,并通过实例展示其应用之道。这项技术极其强大,赋予Linux操作系统前所未有的观测能力,在BCC(BPF Compiler Collection)的加持下,系统内部运行状态变得一目了然。eBPF并非普通运维或系统管理员所能轻易驾驭,它要求使用者具备扎实的底层系统知识和相应的开发技能。
eBPF,即“extended Berkeley Packet Filter”,是一种先进的内核技术。它赋予开发者在不修改内核源代码的情况下,执行特定定制化功能的权限。eBPF的起源可追溯至由贝尔实验室开发的Berkeley Packet Filter(BPF),后者最初被设计用于高效捕获和过滤网络数据包。
为满足对更优Linux追踪工具的需求,eBPF从DTrace中汲取灵感。DTrace是一种主要应用于Solaris和BSD操作系统的动态追踪工具。与DTrace能够提供系统全面视角不同,早期的Linux系统在观测方面有其局限,仅限于系统调用、库调用及特定函数框架。在此背景下,一群工程师开始在Berkeley Packet Filter(BPF)的基础上,扩展其后端功能,以期实现与DTrace相似的能力集。eBPF便由此诞生。它最初于2014年随Linux 3.18版本发布,但要充分发挥eBPF的潜力,通常需要Linux 4.4及以上版本。
与传统的BPF仅限于网络过滤不同,eBPF的应用场景更为广泛,涵盖网络监控、安全过滤和性能分析等众多领域。eBPF的关键在于,它允许用户空间应用程序将预编写的逻辑以字节码形式提交至Linux内核执行。当特定事件(即“挂钩点”)触发时,内核会自动调用这些eBPF程序。典型的挂钩点包括系统调用、网络事件等。目前,编写和调试eBPF程序最流行的工具链是基于LLVM和Clang的BPF Compiler Collection (BCC)。
与eBPF功能相似的工具还有SystemTap和DTrace。SystemTap是一个开源工具,通过动态加载内核模块来收集Linux内核的运行时数据,其工作原理与eBPF有相似之处。DTrace则是一种用途广泛的动态跟踪和分析工具,也能收集系统的运行时数据,功能上与eBPF和SystemTap相仿。
eBPF、SystemTap和DTrace三者各有特点:eBPF作为内核技术,以其灵活性、安全性及广泛应用场景见长,但学习曲线较陡峭,安全性依赖于编译器的正确性;SystemTap作为内核模块工具,功能强大且提供可视化界面,但学习复杂,安全性同样依赖于内核模块的健全;DTrace则以其高性能、强大功能和多语言支持著称,但配置复杂,且可能对系统性能造成较大影响。
eBPF凭借其高度的灵活性和强大功能,在多种应用场景中发挥着核心作用。其常见用途主要包括:
网络监控:eBPF能够捕获网络数据包,并执行自定义逻辑来分析网络流量。例如,可利用eBPF程序实时监控网络数据,并在检测到异常流量时发出警报。
安全过滤:eBPF可用于实施高效的网络数据包安全过滤。例如,它能有效阻止恶意流量的传播,或在发现恶意流量时立即进行拦截。
性能分析:eBPF在分析内核性能方面表现卓越。通过eBPF程序收集内核的性能指标,并可将其通过特定接口可视化展示,从而帮助识别内核性能瓶颈并进行优化。
虚拟化应用:eBPF也能应用于虚拟化技术。例如,可使用eBPF程序收集虚拟机的性能指标,并进行负载均衡,从而更有效地利用虚拟化环境资源,提升系统性能与稳定性。
简而言之,eBPF的常见用途极为广泛,涵盖了网络监控、安全过滤、性能分析以及虚拟化等多个关键领域。
eBPF程序的工作流程可概括为三个核心阶段:加载、编译与执行。
eBPF程序的运行始于用户态应用程序通过系统调用将其加载至内核。在加载过程中,eBPF程序的字节码会被拷贝到内核空间。
接下来,eBPF程序需要经过编译和执行过程。通常,Clang/LLVM编译器会将程序转换成字节码,随后由用户态程序将其加载至内核。在注入内核之前,验证器(Verifier)会对eBPF程序进行严格的安全检查,以确保其不会损害内核的稳定性与安全性。此检查会分析程序代码,杜绝任何恶意操作,例如非授权的系统调用或内存访问。如果eBPF程序通过了所有安全检查,它将通过即时编译(JIT)转换为机器特定指令集,以优化执行速度,进而在内核中正常运行。
在内核中执行时,eBPF程序通常会挂载到特定的内核钩子(hook)上,以便在特定事件发生时被触发。这些钩子可以包括:系统调用发生时(当用户空间函数请求内核服务)、函数入口和出口(拦截预定义函数的调用)、网络事件(例如接收数据包时执行),以及Kprobes和Uprobes(附加到内核或用户空间函数的探测点)。
此外,eBPF Maps是eBPF程序实现状态维护的关键机制,它允许程序在不同调用之间保持状态,进行数据统计,并与用户空间应用程序共享数据。eBPF Map本质上是一个键值存储结构,其中值通常被视为任意数据的二进制块。它们通过bpf_cmd系统调用和BPF_MAP_CREATE参数创建,并通过文件描述符进行访问和操作,实现查找、更新和删除等功能。
概括而言,eBPF的工作原理是通过动态加载、执行并严格检查经过无损编译的代码来实现的。
eBPF在内核性能分析方面提供了强大的能力。以下是一个基于eBPF进行性能分析的逐步示例:
第一步是准备工作:首先,需确认当前Linux内核已支持eBPF功能。这通常涉及在内核配置文件中启用eBPF相关选项并重新编译内核。您可以通过`ls /sys/fs/bpf`和`lsmod | grep bpf`这两个命令来检查eBPF的支持情况。
第二步是编写eBPF程序:接下来,需要编写用于收集内核性能指标的eBPF程序。eBPF程序可以使用C或Python语言编写,它通过特定的接口访问内核数据结构,并将收集到的数据存储到指定位置。
下面是一个Python示例(实质上是利用Python加载一段C语言程序到Linux内核):
```python #!/usr/bin/python3
from bcc import BPF from time import sleep
bpf_text = """ #include <uapi/linux/ptrace.h> BPF_HASH(stats, u32);
int count(struct pt_regs *ctx) { u32 key = 0; u64 *val, zero=0; val = stats.lookup_or_init(&key, &zero); (*val)++; return 0; } """
b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"]) b.attach_kprobe(event="tcp_sendmsg", fn_name="count")
name = { 0: "tcp_sendmsg" }
while True: try: for k, v in b["stats"].items(): print("{}: {}".format(name[k.value], v.value)) sleep(1) except KeyboardInterrupt: exit() ```
此eBPF程序旨在统计网络中传输的数据包数量。它利用`BPF_HASH`数据结构存储统计结果(即eBPF Maps),并通过捕获`tcp_sendmsg`事件实现实时计数。程序每秒输出一次统计结果。这只是一个基础示例,实际应用中可能需要更复杂的统计和分析逻辑。
第三步是运行eBPF程序:接下来,利用eBPF编译器将eBPF程序编译成内核可执行格式。这一过程通常通过BPF Compiler Collection(BCC)工具完成。BCC工具能够以命令行方式将eBPF程序编译为内核可执行格式,并加载到内核中。
要运行上述Python程序,请执行以下步骤:
首先安装`python3-bpfcc`:`sudo apt install python3-bpfcc`。注意,在Python3环境下,请勿使用`pip3 install bcc`。
对于Ubuntu 20.10及更高版本,建议通过源码安装BCC以避免编译问题。具体步骤如下:
卸载现有版本:`apt purge bpfcc-tools libbpfcc python3-bpfcc` 下载源码:`wget <bcc-source-tarball-url>` (此处省略具体URL) 解压:`tar xf bcc-src-with-submodule.tar.gz` 进入目录:`cd bcc/` 安装依赖:`apt install -y python-is-python3 bison build-essential cmake flex git libedit-dev libllvm11 llvm-11-dev libclang-11-dev zlib1g-dev libelf-dev libfl-dev python3-distutils checkinstall` 创建并进入构建目录:`mkdir build && cd build/` 配置CMake:`cmake -DCMAKE_INSTALL_PREFIX=/usr -DPYTHON_CMD=python3 ..` 编译:`make` 安装:`checkinstall`
将上述Python程序保存为`netstat.py`。然后执行:
`$ chmod +x ./netstat.py` `$ sudo ./netstat.py`
程序运行后,控制台将显示类似以下的TCP发送消息统计信息:
``` tcp_sendmsg: 29 tcp_sendmsg: 216 tcp_sendmsg: 277 tcp_sendmsg: 379 tcp_sendmsg: 419 tcp_sendmsg: 468 tcp_sendmsg: 574 tcp_sendmsg: 645 tcp_sendmsg: 29 ```
按下`Ctrl+C`即可结束程序运行。
下面看一个更为复杂的示例,该程序用于计算TCP数据包的传输时间(参考自GitHub上的一个issue):
```python #!/usr/bin/python3
from bcc import BPF import time
bpf_text = """ #include <uapi/linux/ptrace.h> #include <net/sock.h> #include <net/inet_sock.h> #include <bcc/proto.h>
struct packet_t { u64 ts, size; u32 pid; u32 saddr, daddr; u16 sport, dport; };
BPF_HASH(packets, u64, struct packet_t);
int on_send(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size) { u64 id = bpf_get_current_pid_tgid(); u32 pid = id;
struct packet_t pkt = {}; pkt.ts = bpf_ktime_get_ns(); pkt.size = size; pkt.pid = pid; pkt.saddr = sk->__sk_common.skc_rcv_saddr; pkt.daddr = sk->__sk_common.skc_daddr; struct inet_sock *sockp = (struct inet_sock *)sk; pkt.sport = sockp->inet_sport; pkt.dport = sk->__sk_common.skc_dport;
packets.update(&id, &pkt); return 0; }
int on_recv(struct pt_regs *ctx, struct sock *sk) { u64 id = bpf_get_current_pid_tgid(); u32 pid = id;
struct packet_t *pkt = packets.lookup(&id); if (!pkt) { return 0; }
u64 delta = bpf_ktime_get_ns() - pkt->ts; bpf_trace_printk("tcp_time: %llu.%llums, size: %llu\n", delta/1000, delta%1000%100, pkt->size);
packets.delete(&id); return 0; } """
b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"]) b.attach_kprobe(event="tcp_sendmsg", fn_name="on_send") b.attach_kprobe(event="tcp_v4_do_rcv", fn_name="on_recv")
print("Tracing TCP latency... Hit Ctrl-C to end.")
while True: try: (task, pid, cpu, flags, ts, msg) = b.trace_fields() print("%-18.9f %-16s %-6d %s" % (ts, task, pid, msg)) except KeyboardInterrupt: exit() ```
这个程序通过捕获每个数据包的时间戳来测量传输时间。它在`tcp_sendmsg`事件发生时记录数据包的发送时间,并在`tcp_v4_do_rcv`事件发生时记录接收时间,最终通过两者的差值计算出传输延迟。
从上述两个例子中可以看出,eBPF编程的基本模式是在Python程序中将一段“C语言”代码挂载到内核的特定事件上。坦白说,这类代码的编写难度较大,且包含许多微妙之处,普通开发者难以轻松驾驭。幸运的是,许多此类代码已被开源社区的大牛们编写并分享,例如在GitHub上的BCC库的`tools`目录下,包含了大量可直接使用的示例和工具。
BCC(BPF Compiler Collection)是一套开源工具集,专门用于在Linux系统上利用BPF程序进行系统级性能分析和监控。BCC包含了众多实用工具,例如:
`bcc-tools`:一个包含了许多常用BCC工具的软件包。 `bpftrace`:一种高级语言,用于编写和执行BPF程序。 `tcptop`:实时监控和分析TCP流量的工具。 `execsnoop`:用于监控进程执行情况的工具。 `filetop`:实时监控和分析文件系统流量的工具。 `trace`:用于跟踪和分析函数调用的工具。 `funccount`:统计函数调用次数的工具。 `opensnoop`:监控文件打开操作的工具。 `pidstat`:监控进程性能的工具。 `profile`:分析系统CPU使用情况的工具。
您可能多次见过一张图,它展示了BCC工具能进行多少项工作,通过它们,内核内部发生的一切都变得清晰可见。
关于eBPF的进一步学习,推荐阅读以下经典文章和书籍:
Brendan Gregg撰写的《BPF Performance Tools: Linux System and Application Observability》是一本全面指南,详细阐述了eBPF的基础知识与实践应用。
由Cilium团队创建的eBPF官方网站,提供了丰富的资源。
Cilium's BPF and XDP Reference Guide。
BPF Documentation。
BPF Design Q&A。
以及GitHub上的Awesome eBPF项目,汇集了大量eBPF相关资源。
最后,揭晓一个彩蛋。鉴于ChatGPT的普及,我曾尝试利用它辅助撰写本文,包括列提纲并根据提纲生成内容,以及进行资料检索。
