大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺
文章目录
- 一、背景与基础
-
- 1.1 基础之基础
- 1.2 BPF基础和技术储备
- 1.3 eBPF可观测性方向基础
参考资料:
- https://davidlovezoe.club/wordpress/archives/1122#BPF%E7%A4%BE%E5%8C%BA%E5%92%8C%E7%94%9F%E6%80%81
- 《A thorough introduction to eBPF》
- https://davidlovezoe.club/wordpress/archives/874
- https://davidlovezoe.club/wordpress/archives/988
- 极客时间《eBPF核心技术与实战》
- https://zhuanlan.zhihu.com/p/44922656
- https://www.ebpf.top/post/ebpf-overview-part-3/
- 《BPF之巅-洞悉linux系统和应用性能》
一、背景与基础
1.1 基础之基础
在学习eBPF基础之前的基础
1. gcc、llvm、clang等是什么?
编译器分为三个:
- 前端
frontEnd
:词法和语法分析,将源代码转换为抽象语法树 - 优化器
Optimizer
: 在前端的基础上,对中间代码进行优化 - 后端
backEnd
:将优化后的中间代码转化为各自平台的机器码
GCC、llvm、clang
-
GCC
:GNU Compiler Collection
,GNU编译器套装,是一套由 GNU 开发的编程语言编译器,最先支持C语言,后来演进可处理C++、Fortran、Pascal、Objective-C、Java
等语言 -
llvm
:Low Level Virtual Machine
, 可用作多种编译器的后端使用(能够让程序语言的编译器优化、链接优化等),支持任何编程语言的静态和动态编译,可以使用它编写自己的编译器LLVM的命名最早源自于底层虚拟机(
Low Level Virtual Machine
)的首字母缩写,由于这个项目的范围并不局限于创建一个虚拟机,这个缩写导致了广泛的疑惑。LLVM开始成长之后,成为众多编译工具及低端工具技术的统称,使得这个名字变得更不贴切,开发者因而决定放弃这个缩写的意涵,现今LLVM已单纯成为一个品牌,适用于LLVM下的所有项目 -
clang
:clang
是llvm
的前端,可以用来编译c、c++、ObjectiveC
等语言,其以llvm
作为后端支持,高效易用,并且与IDE
有很好的结合
2. .elf对象文件处于程序编译的什么阶段?
可执行与可链接格式 (英语:Executable and Linkable Format,缩写 ELF,此前的写法是 Extensible Linking Format),常被称为 ELF格式,在计算中,是一种用于可执行文件、目标代码、共享库和核心转储(core dump)的标准文件格式。
-
编译:
高级语言 -> 汇编语言 -> 机器语言 : 高级语言最终变为机器语言这样的过程可以统称为编译,具体的方法基本有两种:编译型和解释型
据此也可分为两大类:一种是编译型语言,例如
C,C++,Java
,另一种是解释型语言,例如Python、Ruby、MATLAB 、JavaScript
-
四个步骤:
-
预处理(Preprocessing)
-
编译(Compilation)
-
汇编(Assembly)
-
链接(Linking)
-
.so
文件:动态链接库,也称为共享库, 不能直接运行
1.2 BPF基础和技术储备
1. 网络监控工具发展历程?发展原因?
-
监控内容:监控内核空间运行的数据(数据包), 抓取和过滤符合特定规则的网络包
-
发展历程:
-
发展原因:
- 1->2:
- 效率问题:用户态进程处理监控数据包需要将内核态大量数据包拷贝到用户进程地址空间从而进行分析,设计用户态内核态模式切换(上下文切换),无法处理现在日益增长的数据量(音频、流媒体数据)需求,急迫的需要一个在内核内部运行用户提供的程序的能力
- 2->3:
- 原始BPF的设计:设计较差,难以适配新的硬件环境更新。例如:BPF专注于提供少量的RISC指令的特点,因为当前64位寄存器以及多核处理器新指令的的出现,已不再与现代处理器的实际情况相匹配
- 不仅仅满足于内核数据包的监控,而将功能拓展到例如:性能分析、系统追踪、网络优化等多种类型的工具和平台
- 1->2:
Brendan Gregg,他在2017年的linux.conf.au大会上的演讲提到「内核虚拟机eBPF」,表示,“超能力终于来到了Linux操作系统“
因此,Alexei Starovoitov为了更好地利用的现代硬件,提出了扩展型BPF(eBPF)设计。eBPF虚拟机更类似于现代的处理器,允许eBPF指令映射到更贴近硬件的ISA以获得更好的性能
详细完整的历程:
2. BPF是什么?eBPF是什么?
- BPF
Berkeley Packet Filter
伯克利包过滤器,在伯克利大学诞生,为BSD操作系统而开发,后来一直沿用
原始的BPF
是设计用来抓取和过滤符合特定规则的网络包, 过滤器是通过程序实现的(用户定义过滤器表达式),并在基于寄存器的虚拟机上运行,使得包过滤直接在内核中执行,避免向用户态复制
基本原理是BPF
提供了一种在内核事件和用户程序事件发生时,安全注入代码的机制(运行一小段程序的机制), 这样就让非内核开发人员也可以对内核进行监控和控制
著名的
tcpdump
就是基于此实现:
- eBPF
extend Berkeley Packet Filter
拓展BPF
, 它演进成为了一套通用执行引擎
BPF的功能升级版,Alexei Starovoitov为了更好地利用的现代硬件,提出了扩展型BPF(eBPF)设计
eBPF
的基本架构在于:借助即时编译器JIT,在内核中运行了一个执行引擎(因为编译后直接在cpu上运行,所以相比虚拟机可能执行引擎更为合适),被验证安全的eBPF
指令最终都会被内核执行
- 老版本的
Berkeley Packet Filter
目前称为cBPF
:classic BPF
,目前基本废弃,新的Linux
内核只运行eBPF
, 内核会将cBPF
透明的转换为eBPF
- 特别的,当前
cBPF
和eBPF
都统称为BPF
了,或者说提到BPF
不做特殊说明就是eBPF
,BPF
应该看作是一个技术名词而不是单纯的缩写(或者说包过滤器了)
-
二者比较
3. eBPF做了哪些提升?
-
速度更快
-
更贴近硬件的指令集架构ISA,特别是适应64位寄存器以及提升使用的寄存器数量(从2个提升到10个),这样有助于即时编译提高性能;此外
eBPF
的指令仍然运行在内核中, 不需要向用户态复制数据(也是BPF拥有的),提高了事件处理效率。对于某些网络过滤器微基准测试上显示,
eBPF
在x86-64
架构上的速度比旧的经典BPF
(cBPF
)实现最高快四倍,大多数都在1.5倍
-
-
支持使用一些受限的系统调用
- 新的
BPF_CALL
指令,可以更廉价地调用内核函数
- 新的
-
拓展性
-
功能从包过滤拓展到更多(跟踪过滤、链路追踪、可观测)
2014年的daedfb22451d这次代码提交中,eBPF虚拟机直接暴露给了用户空间来调用(或者说运行用户空间代码)
-
4. eBPF感知代码流程?能够使用eBPF做什么?
eBPF
怎样感知代码?
- 将代码放在
eBPF
的指定的代码路径中,当代码路径被遍历到时,任何附加的eBPF
代码都会被执行
能够做什么?
-
网络数据包/流量的过滤转发
通过编写程序实现对网络数据包/流量的过滤分发,甚至是修改
socket
的设置实例:
- XDP这个项目就是专门使用eBPF来执行高性能数据包处理,方法是在收到数据包之后,立即在网络栈的最低层执行eBPF程式
- seccomp BPF实现了限制一个进程的系统调用方法的使用
-
调试内核/性能分析
程序可以添加跟踪点、
kprobes
和perf
事件; 对于实时运行的系统,可以不重新编译内核而实现编写和测试新的调试代码甚至可以使用
eBPF
通过「用户空间静态定义的跟踪点」来调试用户空间程序
5. eBPF如何保证安全性?(内核验证器)
如果允许用户空间代码在内核中运行,eBPF
如何保证安全性?
eBPF
分为两个阶段的检查:- 第一阶段:加载每个
eBPF
程序之前- 禁止内核锁定:确保
eBPF
终止时不包含任何可能导致内核锁定的循环逻辑(就是不能有循环),通过程序控制流图CFG
来实现 - 禁止不可达指令:任何有不可达指令的程序都无法加载
- 禁止内核锁定:确保
- 第二阶段:模拟执行
- 禁止越界跳转和越界数据访问:验证器模拟执行
eBPF
程序,每执行完一次指令就在指令执行之前和之后检查虚拟机的状态,确保寄存器和堆栈状态的有效性,禁止越界跳转和越界数据访问
- 禁止越界跳转和越界数据访问:验证器模拟执行
- 第一阶段:加载每个
- 检查时的优化:裁剪
eBPF
验证器会智能的检测出已经检查过程序的子集,从而裁剪分支跳过模拟验证的过程
- 禁止指针运算的安全模式
- 时机:当没有使用
CAP_SYS_ADMIN
特权加载eBPF
程序的时候就会进入安全模式,安全模式下会确保内核地址不会泄露给没有特权的用户,并且指针不能写入到内存 - 如果未启用安全模式,则必须在通过检查之后才允许指针运算(检查计算后的指针是否出现类型、位置、边界违反情况等)
- 时机:当没有使用
- 无法读取未被初始化(从未被写入内容)的寄存器
- 寄存器R0-R5的内容在函数调用时会被标记为不可读
- 对读取栈上的变量也进行了类似的检查,以确保没有指令写入只读类型的帧指针寄存器
- 最后,验证器使用**
eBPF
程序类型**(后面将介绍)来限制可以从eBPF
程序调用哪些内核函数以及可以访问哪些数据结构
6. eBPF虚拟机的内部架构是什么?
BPF虚拟机的内部架构
7. eBPF的执行流程是什么?
下面就是整个eBPF
程序的工作流程图:
或者是这张图:
整体的流程可以总结为:
- 通过
llvm
将程序编译为eBPF
字节码 - 通过
bpf
系统调用交给内核(也叫load
加载) - 内核在接受字节码之前会进行检查检验
- 检验通过的字节码提交到即时编译器中运行
8. eBPF的插桩类型有哪些?
插桩是什么?
- 也就是对某个函数进行埋点为了跟踪系统某些事件的发生
具体的事件源:
具体类型?
动态插桩:kprobes && uprobes
动态插桩:对正在运行的软件插入观测点的能力;如果软件未启动,那么动态插桩的开销为0;具体插桩的位置可以是软件栈中所有函数中任一个
与debugger
调试器的区别?
- 调试器是在任意指令地址插入断点的技术,动态插桩则是在软件记录完信息后自动继续执行,不会把控制权交给调试器(无侵入?)
动态插桩有两种探针:
-
内核态插桩
kprobes
-
可以对任意内核函数进行插桩,还可以对内部指令进行插桩,可以在实时生产环境中使用无需重启系统或内核
-
kretprobes
: 对内核函数返回时进行插桩以获取返回值;两者配合可以用来计算函数执行时间 -
上层追踪器/前端的使用
BCC
:attach_kprobe()
和attach_kretprobe()
bpftrace
:kprobe
和kretprobe
探针类型- 注:
BCC
的kprobe
支持函数开始以及某一偏移量放置探针,而bpftrace
只支持函数开始位置插桩
-
-
用户态插桩
uprobes
- 几乎类似于
kprobes
,但是是对用户态程序 - 上层追踪器/前端的使用
BCC
:attach_uprobe()
和attach_uretprobe()
bpftrace
:uprobe
和uretprobe
探针类型- 注:
BCC
的uprobe
支持函数开始以及某一偏移量放置探针,而bpftrace
只支持函数开始位置插桩
- 几乎类似于
缺点:
- 注意:虽然插桩性能消耗很小(已做了优化), 但是超高频事件(百万级别)插桩后还是会影响性能,所以建议插桩低频函数或者在测试环境下插桩高频函数
- 可能随着软件的更新,被插桩的函数也可能会被重新命名或移除(并且在旧版插桩使用的时候可能没有任何输出提示),要重新进行适配
- 编译器的内联优化,将函数内联处理导致被插桩函数无法被插桩
静态插桩:tracepoint && USDT
为了解决动态插桩的问题,出现了静态插桩,静态插桩会将稳定的事件名字编码到软件代码中,由开发者维护
具体有两种:
-
tracepoint
(内核跟踪点)- 用来对内核进行静态插桩,内核开发者在某些位置特定放了插桩点,最终会被编译到内核的二进制文件中,用于跟踪
- 如果有内核跟踪点可以满足需求,优先使用它而不是
kprobes
, 因为前者更加稳定 - 跟踪点一般格式:
<子系统>:<事件名>
- 上层追踪器/前端的使用
BCC
:TRACEPOINT_PROBE()
bpftrace
:跟踪点探针类型
- 原始跟踪点
BPF_RAW_TRACEPOINT
: 避免一些没必要的参数传递,提高性能 (比kprobes
稳定,因为其探针函数名是稳定的,参数可能是不稳定的)
-
USDT
(用户态静态定义跟踪插桩技术user-level statically defined tracing
)-
用户空间版的跟踪点机制
-
依赖于外部系统跟踪器唤起,如果没有,应用中的
USDT
也不会做任何事 (不用担心写在源代码中的探针会带来性能开销,如果外部没启用,那么该代码会被直接跳过) -
上层追踪器/前端的使用
BCC
:USDT().enable_probe()
bpftrace
:USDT
探针类型
-
动态
USDT
:- 对于编译型语言在静态
USDT
直接编译到二进制文件中,而对于动态编译型语言(运行时编译,例如java/jvm
),动态USDT
就起作用了 JVM
已经内置在C++
代码库中许多动态USDT
探针, 包括GC、类加载等;但是java
这种依靠JIT
即时编译的方式难以使用动态USDT
(因为动态USDT
是一个动态链接库,需要提前编译好、带一个包含了探针描述的notes
的ELF
文件)
- 对于编译型语言在静态
-
缺点:增加维护成本,数量一般较少
动态插桩、静态插桩的基本原理将在后续介绍
9. 如何理解eBPF中的Map?
map
又称为映射,BPF
程序可以利用其进行存储
核心职责:存储eBPF运行时状态即用户程序与运行在内核的eBPF
程序交互载体
运行在内核的eBPF
程序收集目标状态存储在map
中,随后用户程序再从映射中读取这些状态
10. eBPF程序的限制有哪些?
保证安全性就意味着限制,所以eBPF
并不是万能的
具体的限制包括:
- 校验限制:必须通过校验才可以执行,并且不能包含不可到达的指令
- 内核函数限制:内核函数只可以调用API定义的辅助函数
- 存储限制:
eBPF
程序栈最大只有512字节,需要更大的存储要借助映射存储 - 指令数量限制:内核5.2之前支持4096条指令,之后支持100万条
- 移植性/适配性:不同内核版本下的移植可能需要修改源码并重新编译
具体内核版本要求和函数功能支持见:https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md
11. eBPF程序编写的组件层次是什么样的?相关工具的实现程度?
eBPF程序编写的整体的组织架构:
- 后端:这是在内核中加载和运行的 eBPF 字节码。它将数据写入内核 map 和环形缓冲区的数据结构中。
- 加载器:它将字节码后端加载到内核中。通常情况下,当加载器进程终止时,字节码会被内核自动卸载。
- 前端:从数据结构中读取数据(由之前的后端写入)并将其显示给用户。
- 数据结构:这些是后端和前端之间的通信手段。它们是由内核管理的 map 和环形缓冲区,可以通过文件描述符访问,并需要在后端被加载之前创建。它们会持续存在,直到没有更多的后端或前端进行读写操作
eBPF 程序可以更加复杂:多个后端可以由一个(或单独的多个)加载器进程加载,写入多个数据结构,然后由多个前端进程读取,所有这些都可以发生在一个跨越多个进程的用户 eBPF 应用程序中。
12. 工具集llvm、BCC、bpftrace、IOVisor层次架构与比较
与之相关的知名工具包括:
-
层级一:
llvm
一个编译器,帮助高级语言(
c、GO、Rust
)的子集被编译成为eBPF
字节码程序;将“”受限的C语言“”(符合eBPF验证规范的)编译为ELF
对象文件,随后即可通过bpf
等系统调用实现加载到内核中;受限的c语言的引入带来的好处是更加容易用高级语言编写,带来的坏处在于加载器程序的复杂性变高(需要解析ELF
对象) -
层级二:
BCC
一个
BPF
工具链集合(libbcc、libbpf
的前身), 解决了上述整体四个组织架构之间的整合关系,尽量实现自动化和标准化,其本身组成分为两个部分:- 编译器集合(BCC 本身):这是用于编写 BCC 工具的框架
BCC-tools
:这是一个不断增长的基于eBPF
且经过测试的程序集,提供了使用的例子和手册(基于BCC
开发的成熟工具)
重新定义了组织结构,
eBPF
程序组件在BCC
组织方式如下: -
层级三:
bpftrace
在某些用例中,
BCC
仍然过于底层,例如在事件响应中检查系统时,时间至关重要,需要快速做出决定,而编写python/“限制性 C”
会花费太多时间,因此BPFtrace
建立在BCC
之上,通过特定领域语言(受AWK
和C
启发实现的一种自定义的高级语言)提供更高级别的抽象,根据声明帖,该语言类似于 DTrace 语言实现,也被称为 DTrace 2.0,并提供了良好的介绍和例子。例如:这个单行 shell 程序统计了每个用户进程系统调用的次数(访问内置变量、map 函数 和count()文档获取更多信息)
bpftrace -e 'tracepoint:raw_syscalls:sys_enter {@[pid, comm] = count();}'
局限性:上层的封装抽象会受限于特殊的功能需求,在某些场景下很难直接用一个
bpftrace
命令实现,所以还是需要BCC
工具BCC
与bpftrace
适用场景对比:BCC
: 开发复杂的脚本和作为后台进程使用bpftrace
:编写强大的单行程序、短小的脚本使用
-
层级四:云环境中的
eBPF
–IOVisor
IOVisor 是 Linux 基金会的一个合作项目,基于本系列文章中介绍的 eBPF 虚拟机和工具。它使用了一些非常高层次的热门概念,如 “通用输入/输出”,专注于向云/数据中心开发人员和用户提供 eBPF 技术。其重新定义了概念,更加模块化、组件化:
- 内核
eBPF
虚拟机成为 “IO Visor 运行时引擎” - 编译器后端成为 “
IO Visor
编译器后端” - 一般的
eBPF
程序被重新命名为 “IO
模块” ,例如:实现包过滤器的特定eBPF
程序称为 “IO
数据平面模块/组件”等等 IOVisor
项目创建了 Hover 框架,也被称为 “IO
模块管理器”,专门用于管理eBPF
程序/IO
模块的用户空间的后台服务程序, 目标是类似于Docker daemon
发布/获取镜像的方式, 能够将IO
模块推送和拉取到云端,分发和部署到多台主机
- 内核
13. BPF运行时模块组成?
- 解释器
JIT
即时编译器:将BPF指令动态的转换为本地化指令verifer
验证器:用于eBPF程序指令的安全检查, 保护内核安全
1.3 eBPF可观测性方向基础
1. eBPF可观测性的术语
目前eBPF
应用领域分别是网络、可观测性、安全。对于可观测性有以下术语解释:
- 跟踪
trace、snoop
: 基于事件的跟踪,记录追踪系统发生的一系列事件 - 采样
sampling、profiling
: 通过获取全部观测信息的子集(定时采样)来描绘大值的图像; 采样工具的优点在于性能开销比跟踪工具小,缺点就在于可能会遗漏部分关键事件 - 可观测性
observability
:通过全面观测来理解一个系统,只要可以实现此目标就可归为此类;上面两种和固定计数器工具都包括在其内,但是不包括基准(benchmark
)测量工具
2. 基础库libbcc、libbpf的理解?
基于这两个基础库分别实现了:BCC
、bpftrace
, libbpf
目前已经是内核代码的一部分
3. eBPF与传统的性能分析工具的不同点?
eBPF是在内核层面使用非常低的损耗实现观测,这与传统的用户态性能分析工具有着本质区别, :
- 高效:
eBPF
同时具备高效率 - 安全性:
eBPF
同时具备生产环境安全性特点 - 侵入性:
eBPF
已经在内核中,可以直接在生产环境中使用,无需添加新的内核组件
为什么效率高?
-
举个例子,需要追踪当前内核I/O尺寸分布的数据,区别就在于:(将工作都移植到到了内核中,减少内核、用户态之间数据复制量)
- 不使用
BPF
:对于每个事件都需要向缓冲区中写一条记录到perf buffer
, 然后用户程序周期性拷贝所有缓冲区数据到用户态生成直方图 - 使用
BPF
: 对于每次事件,运行BPF程序,只获取字节字段,保存在自定义的Map
映射数据结构中, 用户空间一次性读取BPF直方图映射表并输出结果
效率提升显著,以至于工具的额外开销减小到可以在生产环境下直接使用
- 不使用
为什么安全?
- 相比于传统性能检测工具可能会为了高效而需要修改内核等,
BPF
采用的验证器和受限的调用更加正规和安全
相比于内核模块(tracepoint、kprobes
这些内核模块已经出现很多年了):
- 安全性:有自带的安全性检查
- 丰富的数据结构支持
- 稳定性:
eBPF
程序CO-RE
一次编译到处运行,BPF指令集、映射表结构、辅助函数等相关基础设施都是稳定的ABI
;(当然也包括不稳定的因素,例如kprobes
,但是也有对应的解决方案) - 原子性替换
BPF
程序的能力
所以适用场景也有所区别:
- 传统性能分析工具可以作为分析的起点
- 然后再使用
BPF
跟踪工具进一步分析
4. 调用栈回溯(stack-trace)
1. 函数调用栈作用?
- 用于理解某些事件产生的代码路径
- 剖析内核和用户代码,观测执行开销的具体位置
BPF
的支持:
- 专用的存储调用栈信息的映射表结构
- 保存基于帧指针或基于
ORC
的调用栈回溯信息
2. 基于帧指针的栈回溯原理
一个惯例:
- 函数调用栈的头部始终保存在寄存器
RBP
中(x86_64
) - 函数调用的返回地址永远位于
RBP
的值指向的位置+固定偏移量(+8
)
追溯过程:
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/193160.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...