引言 Link to heading
1992年,Steven McCanne 和 Van Jacobson 写了一篇名为 “The BSD Packet Filter:A New Architecture for User-Level Packet Capture” 的论文。在文中,作者描述了他们如何在 Unix 内核实现网络数据包过滤,这种新的技术比当时最先进的数据包过滤技术快 20 倍。数据包过滤有一个特定的目的:可以编写应用程序直接使用内核信息来监控系统网络。有了这些内核信息,应用程序就可以决定如何处理这些数据包。BPF 在数据包过滤上引入了两大革新:
- 一个新的虚拟机(VM)设计,可以有效地工作在基于寄存器结构的 CPU 之上。
- 应用程序使用缓存只复制与过滤数据包相关的数据,不会复制数据包的所有信息。这样可以最大限度地减少 BPF 处理的数据。
2014 年初,Alexei Starovoitov 实现了 eBPF。新的设计针对现代硬件进行了优化,所以 eBPF 生成的指令集比旧的 BPF 解释器生成的机器码执行得更快。扩展版本也增加了虚拟机中的寄存器数量,将原有的 2 个 32 位寄存器增加到 10 个 64 位寄存器。由于寄存器数量和宽度的增加,开发人员可以使用函数参数自由交换更多的信息,编写更复杂的程序。总之,这些改进使 eBPF 版本的速度比原来的 BPF 提高了 4 倍。
BPF 不再局限于网络栈,已经成为内核顶级的子系统。BPF程序架构强调安全性和稳定性,看上去更像内核模块,但与内核模块不同,BPF程序不需要重新编译内核,并且可以确保 BPF 程序运行完成,而不会造成系统的崩溃。
而 eBPF 更是一项革命性的内核技术,它允许开发者编写自定义代码,并能动态加载到内核中,从而改变内核的行为。这项技术催生了新一代高性能的网络、可观测性和安全工具。而且,正如你将看到的,如果你想用这些基于 eBPF 的工具来监测应用程序,你完全无需修改或重新配置应用程序,这得益于 eBPF 在内核中的独特优势。
BPF 架构 Link to heading
- 编译流程:用户空间的编译器将 C 代码编译为 BPF 字节码,经过验证器安全检查后,通过 JIT 编译器生成机器码供 BPF 虚拟机执行
- 执行上下文:BPF 程序通过特定的执行点类型(如网络钩子、系统调用等)与内核事件绑定
- 数据交互:BPF 映射作为内核与用户空间的桥梁,支持双向数据共享(如统计结果输出、配置参数输入)
- 核心组件:BPF 虚拟机负责程序执行,验证器确保内核安全,JIT 编译器优化执行效率
BPF 是一种高级虚拟机,可以在隔离的环境执行代码指令。从某种意义上看,BPF 和 Java 虚拟机(JVM)功能类似,我们可以将高级编程语言编译成机器代码,JVM 是一种运行这种机器代码的专用程序。编译器 LLVM 和 GNU GCC 可提供对 BPF 的支持,将 C 代码编译成 BPF 指令。代码编译后,BPF 使用 BPF 验证器来确保程序在内核中安全运行。BPF 验证器能阻止可能使内核崩溃的代码。如果代码是安全的,BPF 程序将被加载到内核中。Linux 内核也为 BPF 指令集成了即时编译器(JIT)。在程序被验证后,JIT 编译器会直接将 BPF 字节码转换为机器代码,从而减少运行时的时间开销。该架构具有一个非常有用的特点就是加载 BPF 程序无须重启系统,我们不仅可以在系统启动时通过初始化脚本加载 BPF 程序,也可以按需随时加载程序。
在内核运行 BPF 程序之前,我们需要知道程序附加的执行点。内核中有诸多执行点,数量也在持续增长。程序执行点是由 BPF 程序类型确定,当选择了特定的执行点时,内核会提供一些可用的帮助函数,这些帮助函数可用于处理程序接收的数据,从而使执行点和BPF程序能够紧密地配合。
BPF 架构中的最后一个组件称作 BPF 映射,BPF 映射负责在内核和用户空间之间共享数据。BPF 映射提供双向的数据共享,这意味着我们可以分别从内核和用户空间写入和读取数据。BPF 映射包括一些数据结构类型,从简单数组、哈希映射到自定义的映射,我们甚至可以将整个 BPF 程序保存在 BPF 映射中。