文章目录
一、了解libbpf
在使用libbpf前,先使用bcc对eBPF相关知识进行学习运行,学习曲线将更平滑。相对于bcc,libbpf与BPF CO-RE的实际编译部署的难度增大了。
1. BPF的可移植性CO-RE (Compile Once – Run Everywhere)
BPF 可移植性面临的问题
BPF 程序是由用户提供的、经过验证之后在内核上下文中执行的程序。 BPF运行在内核内存空间(kernel memory space)执行,能访问大量的 内核内部状态(internal kernel state)。 这使得 BPF 程序功能极其强大,也是为什么它能成功地应用在大量不同场景的原因之一。
但另一方面,与强大能力相伴而生的是我们如今面临的可移植性问题:BPF 程序 并不控制它运行时所在内核的内存布局(memory layout)。 因此,BPF 程序只能运行在开发和编译这些程序时所在的内核。
另外,内核类型(kernel types)和数据结构(data structures)也在不断变化。 不同的内核版本中,同一结构体的同一字段所在的位置可能会不同 —— 甚至已经 移到一个新的内部结构体(inner struct)中。此外,字段还可能会被重命名、删除、 改变类型,或者(根据不同内核配置)被条件编译掉。
一旦需要查看原始的内核内部数据(raw internal kernel data)—— 例如 常见的表示进程或线程的 struct task_struct,这个结构体中有非常详细的进程信息 —— 那你就只能靠自己了。对于 tracing、monitoring 和 profiling 应用来说这个需求 非常常见,而这类 BPF 程序也是极其有用的。
内核版本不同:字段被重命名或移动位置
在这种情况下,如何保证读到的一定是我们期望读的那个字段呢 —— 例如,
- 原来的程序是从 struct task_struct offset 8 地址读取数据,
- 由于新内核加个了 16 字节新字段,那此时正确的方式应该是从 offset 24 地址读,
这还没完:如果这个字段被改名了呢?例如,thread_struct 的 fs 字段(获取 thread-local storage 用), 在 4.6 到 4.7 内核升级时就被重命名为了 fsbase。
内核版本相同但配置不同:字段在编译时被移除(compile out)
另一种情况:内核版本相同,但内核编译时的配置不同,导致 结构体的某些字段在编译器时被完全移除了。
总结: 依赖开发环境本地的内核头文件编译的 BPF 程序, 是无法直接分发到其他机器运行 —— 然后期待它们返回正确结果的。 这是由于不同版本的内核头文件所假设的内存布局是不同的。
这里有个很强的前提:内核头文件在目标机器上一定存在。 在大部分情况下这都不是问题,但有时可能会带来麻烦。
这对内核开发者来说也尤其头疼,因为他们经常要编译和部署一次性的内核,用于在 开发过程中验证某些问题。而机器上没有指定的、版本正确的内核头文件包,基于 BCC 的应用就无法正常工作。
这种方式会拖慢开发和迭代速度。
总体来说,虽然 bcc 是一个很伟大的工具 —— 尤其是用于快速原型、实验和开发小工具 —— 但 当用于广泛部署生产 BPF 应用时,它存在非常明显的不足。
为了更彻底地解决 BPF 移植性问题,我们设计了 BPF CO-RE,并相信这是BPF 程序的未来开发方式,尤其适用于开发复杂、真实环境中的 BPF 应用。
BPF的可移植性CO-RE (Compile Once – Run Everywhere)
官方:BPF CO-RE (Compile Once – Run Everywhere)
参考URL: https://github.com/libbpf/libbpf#bpf-co-re-compile-once–run-everywhere
BPF的可移植性和CO-RE (Compile Once – Run Everywhere)
参考URL: https://www.cnblogs.com/charlieroro/p/14206214.html
eBPF 的内核无关是有限的,需要 eBPF 机制和开发者共同努力实现。除了 BCC 这种即时编译的方案,还有另外一种名为 CO-RE (Compile Once – Run Everywhere) 的编译方式,其核心依赖于 BTF(更加先进的 DWARF 替代方案)。
文章BPF Portability and CO-R 指出,为了提高BPF程序的便携性,即在不同内核版本上正常工作,而无需为每个特定内核重新编译的能力,社区提出了一个称为BPF CO-RE(Compile Once – Run Everywhere)的解决方案。
Libbpf+BPF CO-RE的理念是,BPF程序与任何"正常"用户空间程序没有太大区别:它们应该汇编成小型二进制文件,然后以紧凑的形式进行部署,以瞄准主机。Libbpf 扮演 BPF 程序装载机的角色,执行平凡的设置工作(重定位、加载和验证 BPF 程序、创建 BPF map、连接到 BPF 挂钩等),让开发人员只担心 BPF 程序的正确性和性能。这种方法将开销保持在最低水平,消除沉重的依赖关系,使整体开发人员体验更加愉快。
BPF CO-RE的目标是帮助BPF开发者使用一个简单的方式解决简单的可移植性问题(如读取结构体字段),并使用它来定位复杂的可移植性问题(如不兼容的数据结构,复杂的用户空间控制条件等)。使得开发者的BPF程序能够"一次编译–随处运行"
Libbpf supports building BPF CO-RE-enabled applications, which, in contrast to BCC, do not require Clang/LLVM runtime being deployed to target servers and doesn’t rely on kernel-devel headers being available.
Libbpf 支持构建 BPF CO-RE-enabled 的应用程序,与BCC相比,它不需要部署的Clang/LLVM运行时,并且不依赖 kernel-devel 内核头文件。
It does rely on kernel to be built with BTF type information, though. Some major Linux distributions come with kernel BTF already built in:
不过,它确实依靠内核来使用BTF类型信息构建。一些主要的Linux发行版本已内置的内核BTF:
Fedora 31+
RHEL 8.2+
OpenSUSE Tumbleweed (in the next release, as of 2020-06-04)
Arch Linux (from kernel 5.7.1.arch1-1)
Manjaro (from kernel 5.4 if compiled after 2021-06-18)
Ubuntu 20.10
Debian 11 (amd64/arm64)
如果您的内核不支持内置的BTF附带,则需要构建自定义内核。你需要:
- pahole 1.16+ tool (part of dwarves package), which performs DWARF to BTF conversion;
- kernel built with CONFIG_DEBUG_INFO_BTF=y option;
- you can check if your kernel has BTF built-in by looking for /sys/kernel/btf/vmlinux file:
$ ls -la /sys/kernel/btf/vmlinux
-r--r--r--. 1 root root 3541561 Jun 2 18:16 /sys/kernel/btf/vmlinux
- 1
- 2
要开发和构建BPF程序,您将需要Clang/LLVM 10+。默认情况下,以下发行版具有clang/llvm 10+打包:
- Fedora 32+
- Ubuntu 20.04+
- Arch Linux
- Ubuntu 20.10 (LLVM 11)
- Debian 11 (LLVM 11)
- Alpine 3.13+
2. libbpf和bcc性能对比
性能优化大师 Brendan Gregg 在用 libbpf + BPF CO-RE 转换一个 BCC 工具后给出了性能对比数据:
As my colleague Jason pointed out, the memory footprint of opensnoopas CO-RE is much lower than opensnoop.py. 9 Mbytes for CO-RE vs 80 Mbytes for Python.
这句话原文暂未找到,TODO!
我们可以看到在运行时相比 BCC 版本,libbpf + BPF CO-RE 版本节约了近 9 倍的内存开销。
3. 了解libbpf
摆脱对内核头文件的依赖
除了使用内核的BTF信息进行字段的重定位意外,还可以将BTF信息生成一个大(基于5.10.1版本生成的长度有106382行)的头文件(“vmlinux.h”),其中包含了所有的内核内部类型,可以避免对系统范围的内核头文件的依赖。可以使用如下方式生成vmlinux.h:
$ bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
- 1
上述命令可以获得到一个可兼容的C头文件(即"vmlinux.h"),包含所有的内核类型("所有"意味着包含那些不会通过kernel-devel包暴露的头文件)。
当使用了vmlinux.h,此时就不需要依赖像#include <linux/sched.h>, #include <linux/fs.h>这样的头文件,仅需要#include "vmlinux.h"即可。该头文件包含了所有的内核类型:暴露了UAPI,通过kernel-devel提供的内部类型,以及其他一些更加内部的内核类型。
不幸的是,BTF(即DWARF)不会记录#define宏,因此在vmlinux.h中丢失一些常用的宏。但大多数通常不存在的宏可以通过libbpf的bpf_helpers.h(即libbpf提供的内核侧的库)头文件提供。
libbpf知道如何将BPF程序代码匹配到特定的内核。它会查看程序记录的BTF类型和重定位信息,然后将这些信息与内核提供的BTF信息进行匹配。 libbpf解析并匹配所有的类型和字段,更新必要的偏移以及重定位数据,确保BPF程序能够正确地运行在特定的内核上。如果一切顺利,则BPF应用开发人员会获得一个BPF程序,这种方式可以针对目标主机上的内核进行“量身定制”,就好像程序是专门针对这个内核编译的,但无需在应用程序中分发Clang以及在目标主机上的运行时中执行编译,就可以实现所有这些目标。
libbpf 是一个比BCC更新的 BPF 开发库,也是最新的 BPF 开发推荐方式!
2015年11月 Kernel 4.3 引入标准库 libbpf, 该标准库由Huawei 2012 OS内核实验室的王楠提交。
2018年 为解决BCC的缺陷,CO-RE(Compile Once, Run Everywhere)的想法被提出并实现,最后达成共识:libbpf + BTF + CO-RE代表了eBPF的未来,BCC底层实现逐步转向libbpf。
4. libbpf 库
libbpf/bpftool 项目地址:https://github.com/libbpf/libbpf
libbpf库是基于C / C ++的通用eBPF库,提供了一些加载bpf程序的方法,封装了内核提供的bpf()系统调用。
eBPF 程序加载的本质是 BPF 系统调用,Linux 内核通过 BPF 系统调用提供 eBPF 相关的一切操作,比如:程序加载、map 创建删除等。常见的 loader 都是对这个系统调用的封装,部分 loader 提供更加原生接近系统调用的操作,部分 loader 则是进行了更多封装使得编程更便捷。
内核实现的 libbpf 库,封装了 BPF 系统调用,使得加载 BPF 程序更便捷。libbpf 不像 iproute2,它能够使 BPF 相关操作更为便捷,没有做过多封装。如果要将程序加载到内核,则需要自己实现一个用户态程序,调用 libbpf 的 API 去加载到内核。如果要复用 pinned 在 BPF 文件系统的 MAP,也需要用户态程序调用 libbpf 的 API,在加载程序时进行相关处理。
典型案例:Facebook 开源的 katran 项目使用 libbpf 加载 eBPF 程序。
构建基于libbpf的BPF应用需要使用BPF CO-RE的步骤
构建基于libbpf的BPF应用需要使用BPF CO-RE包含的几个步骤:
- 生成带所有内核类型的头文件vmlinux.h;
- 使用Clang(版本10或更新版本)将BPF程序的源代码编译为.o对象文件;
- 使用 libbpfgo 将其编译为二进制文件,加载到内核中并监听输出。
5. libbpf-bootstrap
原文链接:Building BPF applications with libbpf-bootstrap
开始使用 BPF 在很大程度上仍然令人生畏,因为即使为简单的"Hello World"般的 BPF 应用程序设置构建工作流,也需要一系列步骤,对于新的 BPF 开发人员来说,这些步骤可能会令人沮丧和令人生畏。这并不复杂,但知道必要的步骤是一个(不必要的)困难的部分。
libbpf-bootstrap 就是这样一个 BPF 游乐场,它已经尽可能地为初学者配置好了环境,帮助他们可以直接步入到 BPF 程序的书写。它综合了 BPF 社区多年来的最佳实践,并且提供了一个现代化的、便捷的工作流。libbpf-bootstrap 依赖于 libbpf 并且使用了一个很简单的 Makefile。对于需要更高级设置的用户,它也是一个好的起点。即使这个 Makefile不会被直接使用到,也可以很轻易地迁移到别的构建系统上。
二、使用go开发ebpf程序
ebpf的核心程序是通过c编写,clang进行编译的。在编译好ebpf程序后,我们需要将其加载到内核中。目前有很多个项目对ebpf的编写调试运行的流程进行了优化,比较有名的是bcc和libbpf。很多时候我们希望能够更加方便的进行程序编写和部署,也希望程序能够在不同的linux发行版和内核上使用(即BPF CO-RE),bcc的运行依赖内核的头文件,也引入了繁重的整个clang llvm工具链,libbpf只能使用C/C++进行外部程序的开发。
BCC、libbpf都主要使用 C 语言开发 eBPF 程序,而实际的应用程序可能会以多种多样的编程语言进行开发。所以,开源社区也开发和维护了很多不同语言的接口,方便这些高级语言跟 eBPF 系统进行交互。
BCC 就提供了 Python、C++ 等多种语言的接口,而使用 BCC 的 Python 接口去加载 eBPF 程序,要比 libbpf 的方法简单得多。
随着ebpf的发展,开源社区中也诞生了各种编程语言的开发库,特别是 Go 和 Rust 这两种语言,其开发库尤为丰富。
1. Go 语言开发库以及选择
使用 Go 语言管理和分发 ebpf 程序
参考URL: https://www.ebpf.top/post/ebpf_go/
目前使用 Go 开发 eBPF 程序可以使用的框架有 IO Visor-gobpf、Dropbox-goebpf和 Cilium-ebpf等,考虑到 Cilium 的社区活跃度和未来的发展,使用 Cilium 的 ebpf 是一个比较不错的选择。
每个库都有各自的范围和限制:
- Calico 在用 bpftool 和 iproute2 实现的 CLI 命令基础上实现了一个 Go 包装器。
- Aqua 实现了对 libbpf C 库的 Go 包装器。
- Dropbox 支持一小部分程序,但有一个非常干净和方便的用户API。
- IO Visor 的 gobpf 是 BCC 框架的 Go 语言绑定,它更注重于跟踪和性能分析。
- Cilium 和 Cloudflare 维护一个 纯 Go 语言编写的库 (以下简称 “libbpf-go”),它将所有 eBPF 系统调用抽象在一个本地 Go 接口后面。
在使用这些 Go 语言开发库时需要注意,Go 开发库只适用于用户态程序中,可以完成 eBPF 程序编译、加载、事件挂载,以及 BPF 映射交互等用户态的功能,而内核态的 eBPF 程序还是需要使用 C 语言来开发的。
当涉及到选择库和工具来与 eBPF 进行交互时,会让人有所困惑。在选择时,你必须在基于 Python 的 BCC 框架、基于 C 的 libbpf 和一系列基于 Go 的 Dropbox、Cilium、Aqua 和 Calico 等库中选择。
库贡献者的活跃度
总结: cilium/ebpf > iovisor/gobpf > dropbox/goebpf > aquasecurity/libbpfgo
关于Cilium?
cilium
n. 纤毛;睫毛;
[例句]Cilium is the major power source of pallium to transport materials.
纤毛是外套膜进行物质运输的主要动力来源。
[其他] 复数:cilia
eBPF是一项革命性的技术,可以在Linux内核中运行沙盒程序,而无需重新编译内核或加载内核模块。在过去的几年中,eBPF已成为解决以前依赖于内核更改或内核模块的问题的标准方法。
eBPF 在动态跟踪、网络、安全以及云原生等领域的广泛应用。
Cilium是一个开源项目,它的基础是基于eBPF的Linux内核技术,用于透明地提供和保护使用Linux容器管理平台部署的应用程序服务之间的网络和API连接。以解决容器工作负载的新可伸缩性,安全性和可见性要求。Cilium超越了传统的容器网络接口(CNI),可提供服务解析,策略执行等功能。
Cilium已迅速成为Kubernetes生态系统中的领先技术,为Google Kubernetes Engine(GKE)提供了网络数据平面,并在其他领先的云原生最终用户(包括Adobe,DataDog,GitLab和DigitalOcean)中得到采用。
Cilium 母公司 Isovalent
Liz Rice,是Isovalent的首席开源官,这家公司是Cilium网络项目的幕后推手。也是CNCF技术监督委员会主席
Cilium 是一个用于容器网络领域的开源项目,主要是面向容器而使用,用于提供并透明地保护应用程序工作负载(如应用程序容器或进程)之间的网络连接和负载均衡。
2. cilium/ebpf库实践
cilium/ebpf 纯 Go 程序编写,从而实现了程序最小依赖;与此同时其还提供了 bpf2go 工具,可用来将 eBPF 程序编译成 Go 语言中的一部分,使得交付更加方便。
因此,本文也选择基于 cilium/ebpf 库来开发和实践。
cilium/ebpf 库
cilium/ebpf库
github: https://github.com/cilium/ebpf
ebpf的核心程序是通过c编写,clang进行编译的。在编译好ebpf程序后,我们需要将其加载到内核中。目前有很多个项目对ebpf的编写调试运行的流程进行了优化,比较有名的是bcc和libbpf。很多时候我们希望能够更加方便的进行程序编写和部署,也希望程序能够在不同的linux发行版和内核上使用(即BPF CO-RE),bcc的运行依赖内核的头文件,也引入了繁重的整个clang llvm工具链,libbpf只能使用C/C++进行外部程序的开发。
如果想使用go编写,有两个选择:cilium的ebpf项目和libbpf-go,考虑的社区活跃度和未来的发展,也许使用cilium的ebpf工具比较合适。
eBPF 程序加载的本质是 BPF 系统调用,Linux 内核通过 BPF 系统调用提供 eBPF 相关的一切操作,比如:程序加载、map 创建删除等。常见的 loader 都是对这个系统调用的封装,部分 loader 提供更加原生接近系统调用的操作,部分 loader 则是进行了更多封装使得编程更便捷。
cilium/ebpf 库是一个 GO 语言版本的 libbpf 库,它封装了 BPF 系统调用,与内核提供的 libbpf 类似。使用 cilium/ebpf 库实现用户态程序加载 eBPF 到内核,在很多方面都类似 libbpf,区别在于这个库是 GO 语言的,更加方便使用 GO 语言构建一套 eBPF 程序的控制面方案。
纯go库用于读取,修改和加载EBPF程序,并将其连接到Linux内核中的各种钩子上。
使用go开发ebpf程序思路
官方github: https://github.com/cilium/ebpf
使用 Go 语言开发 ebpf 程序
https://houmin.cc/posts/adca5ae5/
开发者只需要实现内核态C文件,用户态go文件,用户态event消息结构体三个文件即可,框架会自动加载执行。
- 运行在内核态用C写eBPF代码,llvm编译为eBPF字节码。
- 用户态使用golang编写,cilium/ebpf纯go类库,做eBPF字节码的内核加载,kprobe/uprobe HOOK对应函数。
- 用户态使用golang做事件读取、解码、处理。
cilium/ebpf是一个纯GO库,可提供用于加载,编译和调试EBPF程序的实用程序。它具有最小的外部依赖性,旨在用于长期运行的过程中。
官方demo: tracepoint_in_go
官方demo参考路径:
examples/tracepoint_in_go/main.go
//此程序演示如何将eBPF程序附加到跟踪点。
//程序附加到syscall/sys_enter_openat
跟踪点,并且
//每次 syscall 时,打印出整数123。
//基于预先存在的内核hook(tracepoint)打开跟踪事件。
//每次用户空间程序使用’openat()’ 系统调用时,eBPF
//将执行上面指定的程序,并显示“123”值 在 perf ring 中
纯go库用于读取,修改和加载EBPF程序,并将其连接到Linux内核中的各种钩子上。
这是官方完全go写的demo,演示了 syscall/sys_enter_openat
跟踪点。