前言
eBPF起源于linux内核,它可以以砂箱程序运行在操作系统内核的特权上下文,高效,安全,易于扩展而不需要修改内核源码或者加载内核模块。
操作系统一直是实现观测,安全和网络功能的最理想的地方,因为内核的特权可以查看和控制整个系统。同时,一个操作系统的内核也是很难进化的,因为核心处理的角色,稳定性和安全方面都是高需求的。所以相对于应用而言,操作系统级的创新是很难的。
而eBPF改变了这一情况。通过允许在操作系统中运行沙盒程序,应用程序开发人员可以运行eBPF程序,在运行时为操作系统添加额外的功能。然后,操作系统保证了安全性和执行效率,就好像是在实时(JIT)编译器和验证引擎的帮助下进行本机编译一样。这导致了一波基于eBPF的项目,涵盖了广泛的用例,包括下一代网络、可观察性和安全功能。
如今,eBPF被广泛用于驱动各种各样的用例:在现代数据中心和云原生环境中提供高性能的网络和负载平衡,以低开销提取细粒度的安全可观测性数据,帮助应用程序开发人员跟踪应用程序,为性能故障排除提供见解,预防性应用程序和容器运行时安全强制,等等。可能性是无限的,eBPF正在解锁的创新才刚刚开始。
底层概念与架构
Hook概述
eBPF程序是事件驱动的,当内核或应用程序 通过某个挂接点 时 运行。
预定义的挂钩包括:系统调用、函数进入/退出、内核跟踪点、网络事件和其他一些。
如果不存在用于特定需求的预定义挂钩,则可以创建内核探针(kprobe)或用户探针(uprobe),以将eBPF程序连接到内核或用户应用程序中的几乎任何位置。
eBPF程序是如何编写的?
在很多情况下,eBPF不是直接使用,而是通过项目( Cilium, bcc, or bpftrace)间接使用提供了在eBPF之上的抽象,不需要直接编写程序,而是提供了指定基于意图的定义的能力,然后使用eBPF实现这些定义。如果不存在更高级别的抽象,则需要直接编写程序。Linux 内核期望 eBPF 程序以字节码的形式加载。虽然当然可以直接编写字节码,但更常见的开发实践是利用编译器套件,例如LLVM将伪 C 代码编译为 eBPF 字节码。
当确定了所需的钩子后,可以使用 bpf 系统调用将 eBPF 程序加载到 Linux 内核中。这通常使用可用的 eBPF 库来完成。当程序加载到 Linux 内核中时,它会在附加到请求的钩子之前经过两个步骤:Verification和JIT
Verification:
是确保eBPF程序安全的关键环节,验证包括:
- 加载 eBPF 程序的进程具有所需的功能(权限)。除非启用了非特权 eBPF,否则只有特权进程才能加载 eBPF 程序。
- 该程序不会崩溃或以其他方式损坏系统。
- 程序总是运行到完成(即程序不会永远处于循环中,从而阻止进一步的处理)。
JIT:
是确保程序性能的关键之一
即时 (JIT) 编译步骤将程序的通用字节码转换为特定于机器的指令集,以优化程序的执行速度。这使得 eBPF 程序的运行效率与本机编译的内核代码或作为内核模块加载的代码一样高效。
Map:
eBPF 程序的一个重要方面是共享收集的信息和存储状态的能力。为此,eBPF程序可以利用eBPF映射的概念在广泛的数据结构中存储和检索数据。eBPF 映射可以从 eBPF 程序访问,也可以通过系统调用从用户空间中的应用程序访问。
以下是支持的映射类型的不完整列表,以便了解数据结构的多样性。对于各种映射类型,共享和每 CPU 变体都可用。
- 哈希表、数组
- LRU(最近最少使用)
- 环形缓冲器
- 堆栈跟踪
- LPM(最长前缀匹配)
- ...
Helper
eBPF 程序不能调用任意内核函数。允许这样做会将 eBPF 程序绑定到特定的内核版本,并使程序的兼容性复杂化。相反,eBPF 程序可以将函数调用到帮助程序函数中,这是内核提供的众所周知且稳定的 API。
可用的帮助程序调用集在不断发展。可用的帮助程序调用示例:
- 生成随机数
- 获取当前时间和日期
- eBPF 地图访问
- 获取进程/c组上下文
- 操作网络数据包和转发逻辑
Tail & Function Calls
eBPF 程序可以用尾部和函数调用的概念组合。函数调用允许在 eBPF 程序中定义和调用函数。尾部调用可以调用和执行另一个 eBPF 程序并替换执行上下文,类似于 execve() 系统调用对常规进程的操作方式。
eBPF对Linux内核的影响
现在让我们回到eBPF。为了理解 eBPF 对 Linux 内核的可编程性影响,它有助于对 Linux 内核的架构以及它如何与应用程序和硬件交互有一个高层次的了解。
Linux 内核的主要目的是抽象硬件或虚拟硬件,并提供一致的 API(系统调用),允许应用程序运行和共享资源。为了实现这一目标,需要维护一组广泛的子系统和层来分配这些职责。每个子系统通常允许进行某种级别的配置,以满足用户的不同需求。如果无法配置所需的行为,则需要更改内核,从历史上看,留下两个选项:
使用 eBPF,可以使用一个新选项,允许重新编程 Linux 内核的行为,而无需更改内核源代码或加载内核模块。在许多方面,这与JavaScript和其他脚本语言如何解锁系统的发展非常相似,这些系统已经变得难以改变或成本高昂。