泛读Linux内核观测技术BPF-02

【欢迎关注微信公众号:qiubinwei-1986】

前后花了两天多时间,完成了第二章的泛读,效率感觉有点低(毕竟是泛读),结合前天总结的内容,总结成第二章的泛读总结。整个第二章从编写一个BPF程序开始,先介绍BPF程序编译执行的过程,大致分C语言编写,LLVM编译成BPF字节码,在通过bfp调用变成BPF字节码,通过BPF验证器后使用JIT编译为机器码并执行。随后详细介绍了各种BFP的程序类型,用于各种场景的可观测数据抓取的需求。接着介绍了BFP验证器的作用和工作原理,接着讲了BPF类型格式,至少到目前为止我还没理解这个的价值和作用,有待后续解惑。最后是讲了BPF尾部调用,从内核版本5.2开始,单个BPF程序可以支持100万个指令,可支持32次尾部调用,即一个大型的BPF程序总计可执行3300万条指令。

泛读Linux内核观测技术BPF-02_第1张图片

1.编写BPF程序

开发编译执行一个BPF程序,大致需要5个环节

1.用C语言编写一个BPF程序

2.使用LLVM编译器进行编译,把BPF程序编译成BPF字节码

3.通过BPF系统调用,把BPF字节码提交给内核

4.内核通过BPF验证器完成验证并运行通过JIT编译成机器码执行,将相应的状态保存到BPF映射中

5.用户程序通过BPF映射查询BPF字节码的运行状态

泛读Linux内核观测技术BPF-02_第2张图片

如果以上的图看不明白,可以参考如下的图,来自一文看懂eBPF|eBPF实现原理

泛读Linux内核观测技术BPF-02_第3张图片

此处借助python进行一个大致的演示。

c的代码

int hello_world(void *ctx){     bpf_trace_printk("Hello, World!");     return 0;}

python的代码

#!/usr/bin/python3# 导入bcc下的BPF模块from bcc import BPF# 加载c语言文件b = BPF(src_file="ebpf.c")# 附加到do_sys_openat2函数上,一旦有调用则会调用ebpf.c中定义的hello_world函数b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")#输出/sys/kernel/debug/tracing/trace_pipe下的内容b.trace_print()

执行python时的输出,python3 ebpf.py

泛读Linux内核观测技术BPF-02_第4张图片

2. BPF程序类型

在业内针对BPF没有明确的分类,在本书中根据主要目的,大致分成了两类,跟踪和网络。

在跟踪类别上,通过编写程序了解系统当前的行为,可以准确提供系统行为和系统硬件的直接信息。同时可以访问特定程序的内存区域,从运行的进程中提取执行跟踪信息。此外也可以直接访问每个特定进程所用的一些资源,如CPU、内存、文件描述符(fd)等。

网络是BPF的根基,从之前的BPF的历史可以了解到,最初BPF就是从UNIX的网络数据包过滤这个需求中演化出来的,大大提升了当时系统的网络包过滤的性能。

在网络类别上,主要可以检测和控制系统的网络流量,包括对网络接口的数据包进行过滤或是直接拒绝数据包(可以想象成iptables,不过比iptables更底层)。通过不同的程序类型,可以将程序附加到内核网络处理的不同阶段上。例如:

  • 附加在网络驱动程序接收数据包的网络事件上(可观测到的信息较少)

  • 附加在数据包传递给用户空间的网络事件上(可观测到的信息较多)

需要关注到的一点就是,可观测到的数据越多,系统开销的代价也会越大。

接着作者根据各种类型添加到内核的时间顺序展开不同BPF程序的介绍,这也等于为我们打开了一条深入理解BPF发展的视角。

(接下去的内容比较晦涩,请再次做好心理准备。这也是为什么eBPF的学习成本比较高的原因,我是反复看了几遍大致理解后以后开始总结的)

2.1 套接字过滤器程序

BPF_PROG_TYPE_SOCKET_FILTER类型是添加到Linux内核的第一个程序类型。

套接字过滤器程序会附加到原始套接字上,用于访问所有套接字处理的数据包。套接字过滤器程序只能用于对套接字的观测,不允许修改数据包内容或者更改其目的地。BPF程序会接收与网络协议栈信息相关的元数据。如发送数据包的协议类型。

【名字解释】套接字:套接字(Socket)是计算机网络编程中的一种抽象概念,它允许不同计算机之间的进程通过网络进行通信和数据交换。套接字提供了一种编程接口,允许应用程序使用标准的网络协议(如TCP和UDP)进行网络通信。套接字可以理解为一种通信端点,它由IP地址,端口,通讯协议三个要素组成。通过套接字,应用程序可以创建一个网络连接,向其他计算机发送数据或接收数据。在服务器端,套接字侦听来自客户端的连接请求,并与客户端建立通信。在客户端,套接字用于与服务器进行通信。

2.2 kprobe程序

程序类型为BPF_PROG_TYPE_KPROBE。 kprobe是动态附加到内核调用点的函数。BPF kprobe程序类型允许使用BPF程序作为kprobe处理器。

BPF虚拟机会确保kprobe程序能够安全运行,但在内核中kprobe被认为是不稳定的入口点,因此在实际使用kprobe BPF程序时需要先确认内核版本与其是否兼容。

之前有提到过,BPF程序是触发式执行,所以在kprobe程序编写是需要明确是要在系统函数调用的第一条指令时就触发执行,还是要等到函数调用完成后执行。例如

  • 如果在系统函数exec调用前做参数检查,那么需要在BPF程序设置SEC头部 :SEC("kprobe/sys_exec")

  • 如果是想获取函数exec调用后的返回值,那么就是在BPF程序设置SEC头部:SEC("kretproble/sys_exec")

2.3 跟踪点程序

程序类型为BPF_PROG_TYPE_TRACEPOINt。跟踪点程序会附加到内核提供的跟踪点处理程序上。跟踪点tracepoint是内核代码的静态标记,允许注入跟踪和调试相关的任意代码。其优点是因为跟踪点是预设的,所以稳定性较好,可以提供比较好的可观测性。但缺点就是不够灵活,能够跟踪哪些数据已经提前定义好,不像kprobe是可以全程自定义。

2.4 XDP程序

程序类型为BPF_PROG_TYPE_XDP。当网络包达到内核时,XDP程序会在早期被执行。由于XDP程序是在初期被调用,类似于网络分类中提到的BPF程序附加在网络驱动程序接收数据包的网络事件上,因此可观测性较差。

但是存在必然是有意义的,XDP程序定义对数据包控制的操作,用于定义如何处理数据包,在网络攻击中可以作为系统的第一道预置防线。如果XDP程序返回XDP_PASS,则表示数据包将传递到内核下一个子系统。如果XDP程序返回XDP_DROP,则表示内核会决绝数据包,不做任何处理。XDP程序返回XDP_TX,则表示数据包已经被转发回到最初收到数据包的网卡设备上。

【名词解释】XDP:XDP(eXpress Data Path)是一种数据包处理技术,旨在提供高性能的数据包转发和过滤。XDP是一种在Linux内核中运行的程序,它以极低的延迟在网络接口上处理数据包。与传统的数据包处理方式相比,XDP能够在网络接口接收数据包时就立即处理,而无需将数据包传递到用户空间进行处理。这使得它能够实现非常高的数据包处理速度和低延迟。XDP主要用于数据包过滤和转发,在数据包到达网络接口时,XDP程序可以在内核空间中进行各种操作,如过滤、修改、丢弃、重定向等,而无需将数据包传递到用户空间。这对于高性能网络应用非常有用,例如网络功能虚拟化、防火墙、负载均衡等。

2.5 Perf事件程序

程序类型为BPF_PROG_TYPE_EVENT, Perf事件程序会将BPF代码附加到Perf事件上。Perf是内核的内部分析器,会产生硬件和软件的性能数据时间。利用Perf事件程序可以监控很多新系统信息

2.6 cgroup套接字程序

程序类型为BPF_PROG_TYPE_CGROUP_SKB。 cgroup套接字程序将BPF逻辑附加到控制组(cgroup)上,允许cgroup在其包含的进程中控制网络流量。在网络数据包传入cgroup控制的进程之前,或者从cgroup控制的进程发出数据包,都会通过cgroup套接字程序,由该程序确定如何处理数据包。

在理解程度上,可以与2.1套接字过滤器程序相比(BPF_PROG_TYPE_SOCKET )。不同的点在于套接字过滤器是只是观测,并且是针对附加在的进程上,而cgroupo套接字程序是应用在附加到cgroup控制的所有进程上。该方式适用于对给定cgroup控制的进程上已创建的和未来创建的套接字进行控制。

cgroup套接字程序最适合的就是当前流行的容器化场景。在容器环境中,容器进程组受cgroup限制,当把BPF程序附加在cgroup上,即可实现对容器的网络流量观测。当前K8S中流行的Cillium网络开源项目(为K8S提供负载均衡和安全功能)就是采用了cgroup套接字程序将策略应用于进程组上,而不是隔离的容器上。

2.7 cgroup打开套接字程序

程序类型为BPF_PROG_TYPE_CGROUP_SOCK。这种程序类型允许cgroup内的任何进程在打开网络套接字时执行代码,类似于cgroup套接字缓冲区。

2.8 套接字选项程序

程序类型为BPF_PROG_TYPE_SOCK_OPS。 当数据包通过内核网络栈的多个节点中转时,这种类型程序允许运行时(runtime)修改套接字连接选项。

2.9 套接字映射程序

程序类型为BPF_PROG_TYPE_SK_SKB。该类型程序可以访问套接字映射和套接字重定向。套接字映射可以保留对一些套接字的引用,可以实现负载均衡的功能。通过跟踪多个套接字,可以在内核空间的多个套接字之间转发网络数据包。

Clillium项目和Facebook的Katran项目广泛使用这种程序类型对网络流量进行控制。

2.10 cgroup设备程序

程序类型为BPF_PROG_TYPE_CGROUP_DEVICE。这种类型程序主要用于决定是否能够在指定的设备上执行cgroup操作。

2.11 套接字消息传递程序

程序类型为BPF_PROG_TYPE_SK_MSG。这种类型程序主要用于控制是否将消息发送到套接字。当内核创建一个套接字时,会将套接字存储在套接字映射中。内核通过套接字映射可以快速访问特定的套接字组。当套接字消息BPF程序附加到套接字映射上时,发送到这些套接字的所有消息在发送前都将被过滤。但是在过滤前,内核会复制消息中的数据,以便读取并决定如何处理消息

2.12 原始跟踪点程序

程序类型为BPF_PROG_TYPE_RAW_TRACE。在2.3跟踪点程序的基础上,开发人员添加了新的跟踪点程序,即原始跟踪点程序,用于以内核原始格式方式跟踪点参数。该类型程序可提供较多执行任务的详细信息并占用少量性能开销。

2.13 cgroup套接字地址程序

程序类型为BPF_PROG_TYPE_CGROUP_SOCK_ADDR。这种类型程序允许操作cgroup控制的用户空间程序的IP和端口。当系统使用多个IP地址时,可以确保一组特定的用户空间程序使用相同的IP和端口。

2.14 套接字重用端口程序

程序类型为BPF_PROG_TYPE_SK_RESUSEPORT。 SO_REUSEADDR和SO_REUSEPORT都是内核中的端口重用参数(TCP和UDP),允许相同主机上多个进程绑定相同的端口,解决在高并发情况下,多线程并发处理时端口不足的问题,打开端口重用可以提高网络连接数量,获得较高单机性能

【名词解析】端口重用:端口重用(Port Reuse)是一种特性,允许多个套接字(Sockets)绑定到同一IP地址和端口号的能力。通常情况下,当一个套接字在使用某个端口进行监听或连接时,其他套接字是无法绑定到相同的端口上的。但是通过启用端口重用特性,多个套接字可以共享同一端口。在Linux中,启用端口重用特性可以通过设置套接字选项来实现。对于TCP套接字,可以使用SO_REUSEADDR选项启用端口重用特性。对于UDP套接字,可以使用SO_REUSEPORT选项来实现端口重用。

2.15 流量解析程序

程序类型为BPF_PROG_TYPE_FLOW_DISSECTOR。

流量解析器是一个内核组件,用于跟踪网络数据包经过的不同层,从网络数据包到达系统再到网络数据包发送给用户空间程序。流量解析器允许使用不同分类方法对数据包进行控制。内核中内置流量解析器称为Flower分类器,被防火墙和其他过滤设备使用来决定如何处理特定数据包。

而流量解析程序则是将BPF程序挂钩到流量解析器路径上,以提供内置解析器无法提供的安全保障。如内置解析器不能确保程序终止,但该程序类型可以确保程序最终终止。

2.16 其他BPF程序

一些小众的使用场景,如网络分类程序、轻量级隧道程序、红外设备程序等。

3.BPF验证器

BPF允许在Linux内核中执行任意代码,为了防止恶意攻击,因此使用了BPF验证器来保证在生产环境中运行BPF程序的安全性。

BPF验证器执行的第一项检查是对BPF虚拟机加载的代码进行静态分析,确保程序能够按照预期结束。验证器代码将创建有向无环图(DAG)。验证器分析的每条指令将成为途中的一个节点,每个节点连接到下一条指令验证器生成此图后,执行深度优先搜索(DFS),以确保程序执行完成且代码不包括危险路劲。

【名词解释】 有向无环图 有向无环图指的是一个无回路的有向图。如果有一个非有向无环图,且A点出发向B经C可回到A,形成一个环。将从C到A的边方向改为从A到C,则变成有向无环图。 最简单的理解,一个二叉树,即为有向无环图

在第一项检查中涉及的具体内容

  • 确认程序不包括控制循环。为确保程序不会陷入无限循环,验证器将拒绝任何类型的控制循环。

  • 确保程序痐执行超过内核允许的最大指令数。内核版本5.2之前为4096, 从内核版本5.2开始则调整为100万。

  • 确保程序不包含任何无法到达的指令。例如未执行过的条件或功能,防止加载无效代码。

  • 程序不会超出程序界限

验证器执行的第二项检查是对BPF程序进行预运行。

分析程序执行的每条指令,确保不会执行无效指令。

检查所有内存指针是否可以正确访问

预运行将程序中控制流的执行结果告诉验证器,确保无论采用哪个控制路径都会达到BPF_EXIT指令。

4.BPF类型格式

BPF类型格式(BTF)是元数据结构的集合,可用来增强BPF程序、映射和函数的调试信息。

BTF包含源信息,可使用BPFTool工具对BPF数据进行详细的即使是。元数据存储在二进制程序中特殊的“.BFT”部分。BTF信息可以让程序更容易调试,但也会增加二进制文件的大小。

BTF仅用于注释C语言类型。BPF编译器(如LLVM)知道如何包含这些信息。

5.BPF尾部调用

BPF程序可以通过尾部调用来调用其他BPF程序。他允许通过组合较小的BPF功能来实现更复杂的程序。在5.2版本前,对于尾部调用没有较多限制,但是在5.2内核版本之后,尾部调用只能进行32次调用。

需要注意的一点是从一个BPF程序调用另外一个BPF程序是,内核会重置程序上下文,因此需要通过其他手段来实现多个BPF程序之间的信息共享。

【欢迎关注微信公众号:qiubinwei-1986】

你可能感兴趣的:(eBPF学习,数据库)