几个基础背景知识
内核空间和用户空间
要想充分理解eBPF的设计思想,对于Unix和Linux的内核空间和用户空间需要有一定的理解,这里复习下。
操作系统(特指Unix)的内存主要包括以下程序:
- 负责响应中断的中断服务程序
- 负责管理多个进程从而分享处理器时间的调度程序
- 负责管理进程地址空间的内存管理程序
- 网络、进程间等系统服务程序
内核拥有受保护的内存空间和访问硬件设备的所有权限。
每个处理器在任何指定时间点上的活动必然包括以下三者之一:
- 运行于用户空间,执行用户进程。
- 运行于内核空间,处理进程上下文,代表某个特定的进程执行。
- 运行于内核空间,处理中断上下文,与任何进程无关,处理某个特定的中断。
Unix的发展历史
BPF是Berkeley实验室设计和实现的,说到Berkeley就一定会想到Berkeley在Unix基础上开发的操作系统BSD,这里回顾下Unix的发展历史。
BPF
BPF是在类 Unix 系统上数据链路层的一种原始接口,提供原始链路层封包的收发。比较典型的工具是tcpdump,tcpdump就是利用了 BPF 的技术来抓取 Unix 操作系统节点上的网络包。Linux 系统中也沿用了 BPF 的技术。
下图是The BSD Packet Filter: A New Architecture for User-level Packet Capture论文中关于BPF的设计图。
BPF 在数据包过滤上引入了两大革新:
- 一个新的虚拟机 (VM) 设计,可以有效地工作在基于寄存器结构的 CPU 之上;
- 应用程序使用缓存只复制与过滤数据包相关的数据,不会复制数据包的所有信息。这样可以最大程度地减少BPF 处理的数据;
BPF设计特点
- Kernel中实现了一个虚拟机,用户态程序通过系统调用,把数据包过滤代码载入到个内核态虚拟机中运行,这样就实现了内核态对数据包的过滤。这一块对应图中灰色的大方块,也就是 BPF 的核心。
- BPF 模块和网络协议栈代码是相互独立的,BPF 只是通过简单的几个 hook 点,就能从协议栈中抓到数据包。内核网络协议代码变化不影响 BPF 的工作,图中右边的
protocol stack方块就是指内核网络协议栈。 - Kernel中的 BPF filter 模块使用 buffer 与用户态程序进行通讯,把 filter 的结果返回给用户态程序(例如图中的 network monitor),这样就不会产生内核态与用户态的上下文切换(context switch)。
eBPF
eBPF的全称是Extended Berkeley Packet Filter。
eBPF的概念最早源自于BSD操作系统中的BPF(Berkeley Packet Filter),1992 伯克利实验室的一篇论文 The BSD Packet Filter: A New Architecture for User-level Packet Capture。描述了BPF是如何更加高效灵活地从操作系统内核中抓取网络数据包的。
在 BPF 实现的基础上,Linux 在 2014 年内核 3.18 的版本上实现了 eBPF,全名是 Extended BPF,也就是 BPF 的扩展。
eBPF可以将强大的安全性、可见性和网络控制逻辑动态插入 Linux 内核。同时eBPF 允许开发者不侵入的,动态向内核插入代码。Cilium 是 eBPF 的一个封装。eBPF 实现的最初目标是优化处理网络过滤器的内部 BPF 指令集。eBPF 用于提供高性能网络、多集群和多云功能、高级负载平衡、透明加密、广泛的网络安全功能、透明的可观察性。最初仅仅工作的内核中,2014 年 6 月,eBPF 扩展到用户空间,这也成为了 BPF技术的转折点。
PS:从上图可以看出,GO在用户空间原生支持eBPF,也丰富了GO在云原生场景下的应用。
eBPF对BPF的增强点
最初仅仅工作的内核中,2014 年 6 月,eBPF 扩展到用户空间,这也成为了 BPF技术的转折点。
- 首先,对虚拟机做了增强,扩展了寄存器和指令集的定义,提高了虚拟机的性能,并且可以处理更加复杂的程序。
- 其次,增加了
eBPF maps,这是一种存储类型,可以保存状态信息,从一个BPF事件的处理函数传递给另一个,或者保存一些统计信息,从内核态传递给用户态程序。 - 最后,
eBPF可以处理更多的内核事件,不再只局限在网络事件上。你可以这样来理解,eBPF的程序可以在更多内核代码hook点上注册了,比如tracepoints、kprobes等。
这些增强点带来的好处:
- 稳定:有循环次数和代码路径触达限制,保证程序可以固定时间结束。
- 高效:可以通过JIT方式编译成本地机器码,执行效率高。
- 安全:验证器会对eBPF程序的可访问函数集合和内存地址有严格限制,不会导致内核Panic。
- 热加载和热卸载:可以热加载和热卸载eBPF程序,无需重启Linux系统。
- 内核内置:eBPF自身提供了稳定的API。
- VM通用引擎足够简洁。
- 数据与功能分离
eBPF与BPF的对比
| 维度 | cBPF | eBPF |
|---|---|---|
| 内核版本 | Linux 2.1.75(1997年) | Linux 3.18(2014年)[4.x for kprobe/uprobe/tracepoint/perf-event] |
| 寄存器数目 | 2个:A, X | 10个: R0–R9, 另外 R10 是一个只读的帧指针 |
| 寄存器宽度 | 32位 | 64位 |
| 存储 | 16 个内存位: M[0–15] | 512 字节堆栈,无限制大小的 “map” 存储 |
| 限制的内核调用 | 非常有限,仅限于 JIT 特定 | 有限,通过 bpf_call 指令调用 |
| 目标事件 | 数据包、 seccomp-BPF | 数据包、内核函数、用户函数、跟踪点 PMCs 等 |
用户空间和内核交互
eBPF 分为用户空间程序和内核程序两部分:
- 用户空间程序负责加载
BPF字节码至内核,如需要也会负责读取内核回传的统计信息或者事件详情; - 内核中的
BPF字节码负责在内核中执行特定事件,如需要也会将执行的结果通过maps或者perf-event事件发送至用户空间;
用户空间程序与内核中的 BPF 字节码交互的流程主要如下:
-
使用
LLVM或者GCC工具将编写的BPF代码程序编译成BPF字节码; -
然后使用加载程序
Loader将字节码加载至内核;内核使用验证器(verfier) 组件保证执行字节码的安全性,以避免对内核造成灾难,在确认字节码安全后将其加载对应的内核模块执行;BPF观测技术相关的程序程序类型可能是 kprobes/uprobes/tracepoint/perf_events 中的一个或多个,其中:kprobes:实现内核中动态跟踪。kprobes可以跟踪到 Linux 内核中的导出函数入口或返回点,但是不是稳定 ABI 接口,可能会因为内核版本变化导致,导致跟踪失效。uprobes:用户级别的动态跟踪。与kprobes类似,只是跟踪用户程序中的函数。tracepoints:内核中静态跟踪。tracepoints是内核开发人员维护的跟踪点,能够提供稳定的 ABI 接口,但是由于是研发人员维护,数量和场景可能受限。perf_events:定时采样和PMC。
-
内核中运行的 BPF 字节码程序可以使用两种方式将测量数据回传至用户空间
maps方式可用于将内核中实现的统计摘要信息(比如测量延迟、堆栈信息)等回传至用户空间;perf-event用于将内核采集的事件实时发送至用户空间,用户空间程序实时读取分析;
eBPF的限制
-
eBPF程序不允许调用任意的内核函数,需要通过
BPF Helper函数。 -
eBPF程序不允许包含无法到达的指令,防止加载无效代码,延迟程序的终止。
-
eBPF程序中循环次数限制且必须在有限时间内结束。
-
eBPF堆栈大小被限制在MAX_BPF_STACK
- Root用户的BPF程序,指令数从4096逐步放宽到100万指令。
- 非Root用户,让被限制到4096.
eBPF的实现原理
5大模块
eBPF在内核主要由5个模块协作:
BPF Verifier(验证器)
确保 eBPF 程序的安全。验证器会将待执行的指令创建为一个有向无环图(DAG),确保程序中不包含不可达指令;接着再模拟指令的执行过程,确保不会执行无效指令,这里通过和个别同学了解到,这里的验证器并无法保证100%的安全,所以对于所有BPF程序,都还需要严格的监控和评审。
BPF JIT
将 eBPF 字节码编译成本地机器指令,以便更高效地在内核中执行。
存储模块
多个 64 位寄存器、一个程序计数器和一个 512 字节的栈组成的存储模块,用于控制eBPF程序的运行,保存栈数据,入参与出参。
BPF Helpers(辅助函数)
eBPF 程序不能调用任意的内核参数,只限于内核模块中列出的 BPF Helper 函数,函数支持列表也随着内核的演进在不断增加。
提供了一系列用于 eBPF 程序与内核其他模块进行交互的函数。这些函数并不是任意一个 eBPF 程序都可以调用的,具体可用的函数集由 BPF 程序类型决定。注意,eBPF里面所有对入参,出参的修改都必须符合BPF规范,除了本地变量的变更,其他变化都应当使用BPF Helpers完成,如果BPF Helpers不支持,则无法修改。
bpftool feature probe
通过以上命令可以看到不同类型的eBPF程序可以运行哪些BPF Helpers。具体方法可以查看源码:github.com/torvalds/li…
const struct bpf_func_proto bpf_get_prandom_u32_proto = {
.func = bpf_user_rnd_u32,
.gpl_only = false,
.ret_type = RET_INTEGER,
};
BPF Map & context
用于提供大块的存储,这些存储可被用户空间程序用来进行访问,进而控制 eBPF 程序的运行状态。
bpftool feature probe | grep map_type
通过以上命令可以看到系统支持哪些类型的map。
3个动作
先说下重要的系统调用bpf:
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
- cmd是指令类型
- attr是cmd的参数
- size是参数大小
// 5.11内核
enum bpf_cmd {
BPF_MAP_CREATE,
BPF_MAP_LOOKUP_ELEM,
BPF_MAP_UPDATE_ELEM,
BPF_MAP_DELETE_ELEM,
BPF_MAP_GET_NEXT_KEY,
BPF_PROG_LOAD,
BPF_OBJ_PIN,
BPF_OBJ_GET,
BPF_PROG_ATTACH,
BPF_PROG_DETACH,
BPF_PROG_TEST_RUN,
BPF_PROG_GET_NEXT_ID,
BPF_MAP_GET_NEXT_ID,
BPF_PROG_GET_FD_BY_ID,
BPF_MAP_GET_FD_BY_ID,
BPF_OBJ_GET_INFO_BY_FD,
BPF_PROG_QUERY,
BPF_RAW_TRACEPOINT_OPEN,
BPF_BTF_LOAD,
BPF_BTF_GET_FD_BY_ID,
BPF_TASK_FD_QUERY,
BPF_MAP_LOOKUP_AND_DELETE_ELEM,
BPF_MAP_FREEZE,
BPF_BTF_GET_NEXT_ID,
BPF_MAP_LOOKUP_BATCH,
BPF_MAP_LOOKUP_AND_DELETE_BATCH,
BPF_MAP_UPDATE_BATCH,
BPF_MAP_DELETE_BATCH,
BPF_LINK_CREATE,
BPF_LINK_UPDATE,
BPF_LINK_GET_FD_BY_ID,
BPF_LINK_GET_NEXT_ID,
BPF_ENABLE_STATS,
BPF_ITER_CREATE,
BPF_LINK_DETACH,
BPF_PROG_BIND_MAP,
};
最核心的就是PROG,MAP相关的cmd,就是程序加载和映射处理。
程序加载
调用BPF_PROG_LOAD cmd,会将BPF程序加载到内核,但eBPF 程序并不像常规的线程那样,启动后就一直运行在那里,它需要事件触发后才会执行。这些事件包括系统调用、内核跟踪点、内核函数和用户态函数的调用退出、网络事件,等等,所以需要第2个动作。
绑定事件
b.attach_kprobe(event="xxx", fn_name="yyy")
以上就是将特定的事件绑定到特定的BPF函数,实际实现原理如下:
(1)借助 bpf 系统调用,加载 BPF 程序之后,会记住返回的文件描述符;
(2)通过attach操作知道对应函数类型的事件编号;
(3)根据attach的返回值调用 perf_event_open 创建性能监控事件;
(4)通过 ioctl 的 PERF_EVENT_IOC_SET_BPF 命令,将 BPF 程序绑定到性能监控事件。
映射操作
通过MAP相关的cmd,控制MAP增删,然后用户态基于该MAP与内核状态进行交互。
一个简单程序
#include <uapi/linux/ptrace.h>
BPF_PERF_OUTPUT(events);
typedef struct {
u64 arg1;
char arg2;
char pad[3];
float arg3;
} args_event_t;
inline int get_arguments(struct pt_regs *ctx) {
void* stackAddr = (void*)ctx->sp;
args_event_t event = {};
bpf_probe_read(&event.arg1, sizeof(event.arg1), stackAddr+8);
bpf_probe_read(&event.arg2, sizeof(event.arg2), stackAddr+16);
bpf_probe_read(&event.arg3, sizeof(event.arg3), stackAddr+20);
long tmp = 2021;
bpf_probe_write_user(stackAddr+8, &tmp, sizeof(tmp));
events.perf_submit(ctx, &event, sizeof(event));
return 0;
}
eBPF应用
可以做哪些事情?
-
系统观测探测、访问控制列表,包括内核态和用户态
- 绝大部分内核函数,函数入口和出口可以跟踪。如何实现的呢?
- 内存分配的模块目前不允许跟踪观测。
-
技术观测和故障演练拓扑,流量控制。
-
安全应用
基于eBPF实现的开源项目
-
Facebook高性能 4 层负载均衡器Katran; -
IO Visor项目开源的BCC、BPFTrace和Kubectl-Trace: -
CloudFlare公司开源的eBPF Exporter和bpf-toolseBPF Exporter将 eBPF 技术与监控 Prometheus 紧密结合起来bpf-tools可用于网络问题分析和排查;
-
- memcache+eBPF,基于eBPF实现在内核层面的缓存。
- Running memcached with BMC improves throughput by up to 18x compared to the vanilla memcached application.
-
Cilium为下一代微服务ServiceMesh打造了具备API感知和安全高效的容器网络方案;底层主要使用 XDP 和 TC 等相关技术;- 实际的开发中对于微服务涉及的更多一些,因此后续小节会着重介绍下
Cilium。
- 实际的开发中对于微服务涉及的更多一些,因此后续小节会着重介绍下
Cilium
Cilium 是一个开源项目,地址:cilium.io/,旨在为 Kubernetes 集群和其他容器编排平台等云原生环境提供网络、安全性和可观察性。
Cilium is an open source software for providing, securing and observing network connectivity between container workloads - cloud native, and fueled by the revolutionary Kernel technology eBPF.
大体上可以理解为 Cilium 为 Service Mesh 打造了具备 API 感知和安全高效的容器网络方案, Cilium的底层正是基于 eBPF 技术实现。
参考文献
- ebpf.io/
- What is eBPF? An Introduction and Deep Dive into the eBPF Technology
- zhuanlan.zhihu.com/p/378766217
- developer.aliyun.com/article/947…
- cloudnative.to/blog/bpf-in…
- www.tcpdump.org/papers/bpf-…
- Cilium - Linux Native, API-Aware Networking and Security for Containers
- www.ebpf.top/post/head_f…
- kerneltravel.net/blog/2021/e…