长久以来,撰写一篇关于eBPF的介绍性文章一直是我的计划,近日终于得以落实。本文旨在简要阐述eBPF的核心用途,并通过具体实例展示其运作方式。这项技术堪称非凡,它赋予了Linux操作系统前所未有的观测能力,在BPF编译器集合(BCC)的助力下,系统内部运行细节变得一览无余。然而,掌握这项强大的技术并非普通运维人员或系统管理员所能,它要求技术人员对底层系统有深入理解,并具备较高的开发技能。
eBPF(extended Berkeley Packet Filter)是一项Linux内核技术,其核心能力在于开发者无需改动内核源代码,即可在内核空间执行特定功能。它的概念根植于贝尔实验室开发的伯克利包过滤器(BPF),初代BPF主要用于捕获和过滤网络数据包。由于对更优越的Linux追踪工具的迫切需求,eBPF从DTrace中汲取灵感。DTrace是主要应用于Solaris和BSD操作系统的动态追踪工具,而Linux此前在系统整体洞察力方面有所欠缺,仅限于系统调用、库调用及特定函数框架。在此背景下,一群工程师决定扩展BPF的后端,以提供类似DTrace的功能集,eBPF便由此诞生。它于2014年随Linux 3.18版本首次有限发布,要全面利用eBPF的强大功能,通常需要Linux 4.4及以上版本。
与传统BPF仅限于网络过滤相比,eBPF的应用场景更为广泛,涵盖网络监控、安全过滤、性能分析等。其独特之处在于,eBPF允许用户空间的应用程序将预设逻辑以字节码形式提交至Linux内核执行。当特定事件(即“钩子”)触发时,内核便会调用相应的eBPF程序。这些钩子可以包括系统调用、网络事件等。目前,编写和调试eBPF程序最流行的工具链是BCC(BPF Compiler Collection),它构建于LLVM和Clang之上。
eBPF与其他一些工具存在异曲同工之处。例如,SystemTap是一个开源工具,通过动态加载内核模块来收集Linux内核的运行时数据,这与eBPF的工作方式类似。同样,DTrace也是一个动态追踪和分析工具,能用于获取系统运行时数据,与eBPF和SystemTap功能相近。
一个对比表格可以更清晰地展示eBPF、SystemTap和DTrace之间的差异:
| 工具 | eBPF | SystemTap | DTrace | | :-------- | :----------------------- | :----------------------- | :----------------------- | | 定位 | 内核技术,多应用场景 | 内核模块 | 动态追踪和分析工具 | | 工作原理 | 动态加载执行无损编译代码 | 动态加载内核模块 | 动态插接分析器,探针取数据 | | 常见用途 | 网络监控、安全、性能分析 | 系统性能分析、故障诊断 | 系统性能分析、故障诊断 | | 优点 | 灵活、安全、多应用场景 | 功能强大、可视化界面 | 功能强大、高性能、多编程语言 | | 缺点 | 学习曲线高,安全性依赖编译器 | 学习曲线高,安全性依赖内核模块 | 配置复杂,对系统性能影响大 |
由上可见,eBPF、SystemTap和DTrace都是功能强大的工具,各自在系统运行情况数据的收集与分析方面各有所长。
eBPF作为一项高度灵活且强大的内核技术,其应用范围极为广泛,包括:
**网络监控:** eBPF能够捕获网络数据包,并执行自定义逻辑来分析网络流量。例如,可以使用eBPF程序监控流量,并在检测到异常时发出警报。
**安全过滤:** eBPF可用于对网络数据包进行安全筛选。例如,eBPF程序可以阻止恶意流量传播,或在发现恶意流量时进行拦截。
**性能分析:** eBPF能够对内核性能进行深度分析。通过eBPF程序收集内核性能指标,并经由特定接口将其可视化,有助于发现性能瓶颈并进行优化。
**虚拟化:** eBPF在虚拟化技术中也有应用。例如,它可以收集虚拟机的性能指标,并进行负载均衡,从而更有效地利用虚拟化环境资源,提升系统性能和稳定性。
总之,eBPF以其广泛的用途,在网络监控、安全过滤、性能分析和虚拟化等领域发挥着关键作用。
eBPF的工作原理主要涉及加载、编译和执行三个步骤。
eBPF程序必须在内核中运行。这通常由用户态应用程序通过系统调用完成,应用程序会将eBPF程序代码复制到内核空间。
eBPF程序需要经过编译和执行。Clang/LLVM编译器负责将程序编译为字节码。随后,用户态的字节码被加载进内核。此时,验证器(Verifier)会对程序进行安全检查,以确保其不会损害内核的稳定性和安全性。检查过程中,内核会分析代码,确认其不会执行恶意操作(如非法系统调用或内存访问)。一旦通过安全检查,eBPF程序便可在内核中正常运行,并通过JIT编译将通用字节码转换为机器特定指令,以优化执行速度。
其架构图如下所示:
(此处原包含图片链接,已省略。)
eBPF程序在内核中运行时,通常会挂载到某个内核钩子(hook)上,以便在特定事件发生时被触发执行。这些钩子包括但不限于:
系统调用:当用户空间函数将控制权转移给内核时插入。
函数进入和退出:拦截对现有函数的调用。
网络事件:在接收到数据包时执行。
Kprobes和Uprobes:附着于内核或用户函数的探测点。
最后是eBPF Maps,它允许eBPF程序在不同调用间保持状态,便于数据统计,并与用户空间应用程序共享数据。eBPF Map本质上是一个键值存储,其值通常被视作任意数据的二进制块。它们通过带有BPF_MAP_CREATE参数的bpf_cmd系统调用创建,并像Linux中的其他对象一样,通过文件描述符进行寻址。与Map的交互通过查找/更新/删除系统调用完成。
综上,eBPF通过动态加载、执行和检查无损编译代码来实现其功能。
以下是一个基于eBPF进行性能分析的详细示例:
**第一步:准备工作**
首先,确认内核已支持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程序编译成内核可执行格式(如上述Python程序所示:Python通过导入BCC包,将C语言程序编译为字节码并加载到内核中,然后将特定函数附加到相应事件上)。这一过程可借助BPF编译器集合(BCC)工具完成。BCC工具能够通过命令行将eBPF程序编译为内核可执行格式并加载到内核。
运行上述Python3程序的步骤如下:
安装Python3-bpfcc:`sudo apt install python3-bpfcc`。
请注意,在Python3环境下,不建议使用`pip3 install bcc`。
对于Ubuntu 20.10及更高版本,建议通过源码安装BCC以避免编译问题:
首先,完全移除现有BCC相关包:`apt purge bpfcc-tools libbpfcc python3-bpfcc`。
下载并解压源码包:`wget [下载链接] && tar xf bcc-src-with-submodule.tar.gz`。
进入BCC目录:`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 -DCMAKE_INSTALL_PREFIX=/usr -DPYTHON_CMD=python3 ..`。
编译并安装:`make && sudo checkinstall`。
将上述Python程序保存至本地文件,例如`netstat.py`。然后,通过以下命令运行程序:
`$ chmod +x ./netstat.py`
`$ sudo ./netstat.py`
程序启动后,控制台将持续输出网络数据包的统计信息。按下`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`目录下有大量现成的eBPF工具,我们无需从零开始。
BCC(BPF Compiler Collection)是一套开源工具集,它使Linux系统能够利用BPF程序进行系统级性能分析和监控。BCC包含众多实用工具,例如:
`bcc-tools`:一个涵盖多种常用BCC工具的软件包。
`bpftrace`:一种高级语言,用于编写和执行BPF程序。
`tcptop`:实时监控和分析TCP流量的工具。
`execsnoop`:用于监控进程执行情况的工具。
`filetop`:实时监控和分析文件系统流量的工具。
`trace`:用于追踪和分析函数调用的工具。
`funccount`:统计函数调用次数的工具。
`opensnoop`:监控文件打开操作的工具。
`pidstat`:监控进程性能的工具。
`profile`:分析系统CPU使用情况的工具。
您可能多次见过以下这张图,它形象展示了eBPF的强大能力,让内核中发生的一切都变得清晰可见。
(此处原包含图片链接,已省略。)
**延伸阅读**
关于eBPF的经典文章和书籍包括:
Brendan Gregg所著的《BPF Performance Tools: Linux System and Application Observability》是一本全面指南,涵盖了eBPF的基础知识和实际应用。
eBPF官方网站:由Cilium项目创建。
Cilium的BPF和XDP参考指南。
BPF官方文档。
BPF设计问答集。
以及GitHub上的Awesome eBPF资源列表。
**彩蛋**
最后是彩蛋环节。鉴于最近ChatGPT风靡一时,我尝试借助它来辅助本文的撰写。最初,我让ChatGPT帮我列出文章提纲,并根据提纲生成内容,并进行信息查找。
