eBPF(extended Berkeley Packet Filter)
可谓 Linux 社区的新宠,很多大公司都开始投身于 eBPF
技术,如 Goole、Facebook、Twitter 等。
eBPF 究竟有什么魅力让大家都关注它呢?
这是因为 eBPF 增加了内核的可扩展性,让内核变得更加灵活和强大。
如果大家玩过 乐高积木
的话就会深有体会,乐高积木就是通过不断向主体添加积木来组合出更庞大的模型。
而 eBPF 就像乐高积木一样,可以不断向内核添加 eBPF 模块来增强内核的功能。
本文分为3篇:
- eBPF 的简单使用
- eBPF 的实现原理
- kprobes 在 eBPF 中的实现原理
看完这3篇文章,估计对 eBPF 也有较深的理解了。
什么是 eBPF
eBPF 全称 extended Berkeley Packet Filter,中文意思是 扩展的伯克利包过滤器
。一般来说,要向内核添加新功能,需要修改内核源代码或者编写 内核模块
来实现。而 eBPF 允许程序在不修改内核源代码,或添加额外的内核模块情况下运行。
从 eBPF 的名字看,好像是专门为过滤网络包而创造的。其实,eBPF 是从 BPF(也称为 cBPF:classic Berkeley Packet Filter)发展而来的,BPF 是专门为过滤网络数据包而创造的。
但随着 eBPF 不断完善和加强,现在的 eBPF 已经不再限于过滤网络数据包了。
eBPF 架构
我们先来看看 eBPF 的架构,如下图所示:
下面用文字来描述一下:
用户态
- 用户编写 eBPF 程序,可以使用 eBPF 汇编或者 eBPF 特有的 C 语言来编写。
- 使用 LLVM/CLang 编译器,将 eBPF 程序编译成 eBPF 字节码。
- 调用
bpf()
系统调用把 eBPF 字节码加载到内核。
内核态
- 当用户调用
bpf()
系统调用把 eBPF 字节码加载到内核时,内核先会对 eBPF 字节码进行安全验证。 - 使用
JIT(Just In Time)
技术将 eBPF 字节编译成本地机器码(Native Code)。 - 然后根据 eBPF 程序的功能,将 eBPF 机器码挂载到内核的不同运行路径上(如用于跟踪内核运行状态的 eBPF 程序将会挂载在
kprobes
的运行路径上)。当内核运行到这些路径时,就会触发执行相应路径上的 eBPF 机器码。
如果大家使用过 Java 编写程序的话,会发现 eBPF 与 Java 的AOP(Aspect Oriented Programming 面向切面编程)概念很像。
为了让有 Java 经验的同学更容易接受 eBPF 技术。我们先介绍一下 Java 中的 AOP 概念。
在 AOP 概念中,有两个很重要的角色:切点
和 拦截器
。
切点
:程序中某个具体的业务点(方法)。拦截器
:拦截器其实是一段 Java 代码,用于拦截切点在执行前(或执行后),先运行这段 Java 代码。
eBPF 程序就像 AOP 中的拦截器,而内核的某个运行路径就像 AOP 中的切点。
根据挂载点功能的不同,大概可以分为以下几个模块:
- 性能跟踪
- 网络
- 容器
- 安全
eBPF 使用
在介绍 eBPF 的实现前,我们先来介绍一下如何使用 eBPF 来跟踪 fork()
系统调用的运行情况。
编写 eBPF 程序有多种方式,比如使用原生 eBPF 汇编来编写,但使用原生 eBPF 汇编编写程序的难度较大,所以一般不建议。
也可以使用 eBPF 受限的 C 语言来编写,难度比使用原生 eBPF 汇编简单些,但对初学者来说也不是十分友好。
最简单是使用 BCC 工具来编写,BCC 工具帮我们简化了很多繁琐的工作,比如不用编写加载器。
下面我们将使用 BCC 工具来介绍怎么编写一个 eBPF 程序。
注意:由于 eBPF 对内核的版本有较高的要求,不同版本的内核对 eBPF 的支持可能有所不相同。所以使用 eBPF 时,最好使用最新版本的内核。 本文使用
Ubuntu 20.20
(内核版本为5.8.1)作为解说。
1. BCC 工具安装
在 Ubuntu 系统中安装 BCC 工具是比较简单的,可以使用以下命令:
代码语言:javascript复制$ sudo apt-get install bpfcc-tools linux-headers-$(uname -r)
BCC 工具可以让你使用 Python 和 C 语言组合来编写 eBPF 程序。
安装完成后,可以使用命令 bcc -v
来测试是否安装成功。如果安装失败,可以参考官网安装文档,如下:
https://github.com/iovisor/bcc/blob/master/INSTALL.md
2. 编写 eBPF 版的 hello world
一般编程课的第一步都是编写著名的 hello world
程序,所以我们也以编写 hello world
程序作为第一步吧。
使用 BCC 编写 eBPF 程序的步骤如下:
- 使用 C 语言编写 eBPF 程序的内核态功能(也就是运行在内核态的 eBPF 程序)。
- 使用 Python 编写加载代码和用户态功能。
为什么不能全部使用 Python 编写呢?这是因为 LLVM/Clang 只支持将 C 语言编译成 eBPF 字节码,而不支持将 Python 代码编译成 eBPF 字节码。
所以,eBPF 内核态程序只能使用 C 语言编写。而 eBPF 的用户态程序可以使用 Python 进行编写,这样就能简化编写难度。
所以,第一步就是编写 eBPF 内核态程序。
使用 C 编写 eBPF 程序
新建一个 hello.c 文件,并输入下面的内容:
代码语言:javascript复制int hello_world(void *ctx)
{
bpf_trace_printk("Hello, World!");
return 0;
}
使用 Python 和 BCC 工具开发一个用户态程序
新建一个 hello.py 文件,并输入下面的内容:
代码语言:javascript复制#!/usr/bin/env python3
# 1) 加载 BCC 库
from bcc import BPF
# 2) 加载 eBPF 内核态程序
b = BPF(src_file="hello.c")
# 3) 将 eBPF 程序挂载到 kprobe
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
# 4) 读取并且打印 eBPF 内核态程序输出的数据
b.trace_print()
下面我们来看看每一行代码的具体含义:
- 导入了 BCC 库的 BPF 模块,以便接下来调用。
- 调用 BPF() 函数加载 eBPF 内核态程序(也就是我们编写的hello.c)。
- 将 eBPF 程序挂载到内核探针(简称 kprobe),其中
do_sys_openat2()
是系统调用openat()
在内核中的实现。 - 读取内核调试文件
/sys/kernel/debug/tracing/trace_pipe
的内容(bpf_trace_printk()
函数会将信息写入到此文件),并打印到标准输出中。
运行 eBPF 程序
用户态程序开发完成之后,最后一步就是执行它了。需要注意的是,eBPF 程序需要以 root
用户来运行:
$ sudo python3 hello.py
运行后,可以看到如下输出:
代码语言:javascript复制$ sudo python3 hello.py
b' python3-31683 [001] .... 614653.225903: 0: Hello, World!'
b' python3-31683 [001] .... 614653.226093: 0: Hello, World!'
b' python3-31683 [001] .... 614653.226606: 0: Hello, World!'
b' <...>-31684 [000] .... 614654.387288: 0: Hello, World!'
b' irqbalance-669 [000] .... 614658.232433: 0: Hello, World!'
...
到了这里,我们已经成功开发并运行了第一个 eBPF 程序。当然,这个程序很简单,并且也没有实际的用途。
但通过这个程序,我们大概可以知道使用 BCC 开发一个 eBPF 程序的步骤。
因为本系列文章并不是介绍如何开发 eBPF 程序,而是介绍 eBPF 的原理和实现。如果大家有兴趣学习如何开发 eBPF 程序,那么建议大家看看《BPF性能之巅》这本书,这本书详细地介绍了如何开发 eBPF 程序。
在下篇文章中,我们将介绍 eBPF 的实现原理,敬请期待。