极客时间:eBPF核心技术与实战
目录:
学习准备
eBPF发展历程及工作原理
eBPF技术是在BPF技术上扩展而来
在1992年的USENIX会议上,Steven McCanne和Van Jacobson发布的论文为BSD操作系统带来了革命性的包过滤机制BSD Packet Filter(简称为 BPF),比当时最先进的数据包过滤技术还快20倍
速度快得益于BPF的两大设计:
- 内核态引入虚拟机
- 用户态使用BPF字节码定义过滤的表达式,传递给内核,进而由虚拟机执行
包过滤可以直接在内核中进行,避免了向用户态的拷贝,提高了包过滤的性能
历史为
- 在BPF诞生5年后,Linux2.1.75首次引入了BPF
- 在Linux3.0添加了BPF即时编译器,优化了BPF指令运行的效率,但是整体还是在网络包过滤范畴
- 2014年为了研究软件定义网络,Alexei Starovoitov为BPF带来了eBPF,eBPF扩展了寄存器的数量,引入了BPF映射,在4.x内核将网络数据包扩展到内核函数、用户函数、跟踪点、性能事件和安全控制等
eBPF使BPF不再局限与网络栈,而是称为内核的顶级子系统
带来的有
- 跟踪和排错领域:BCC、bpftrace等
- 优化网络和安全:Cilium、Katran、Falco和最新版本的Calico等
BBC工具都是基于eBPF开发的
历史链路
eBPF工作原理
eBPF程序和常规线程不一样,启动就一直运行,而是需要事件进行触发
这些事件包括
- 系统调用
- 内核跟踪点
- 内核函数和用户态函数的调用退出
- 网络事件
借助内核态插桩(kprobe)和用户态插桩(uprobe)可以在内核和应用的任意位置插桩
不安全的eBPF不能被提交到内核虚拟机运行
eBPF执行过程
- 借助LLVM编写eBPF程序
- eBPF程序转换为BPF字节码
- BPF字节码通过bpf系统调用提交到内核
- 内核接收BPF字节码之前会进行校验
- 通过校验的BPF字节码由内核的即时编译器执行
流程图
常见的验证过程
- 只有特权进程才可以执行bpf系统调用
- BPF程序不能包含无限循环
- BPF程序不能导致内核崩溃
- BPF程序必须在有限时间内完成
BPF程序可以利用BPF映射(map)来进行存储,用户程序也通过映射与内核的BPF进行交互
示例性能观测中,BPF程序收集内核运行状态存储在映射中,用户程序再从映射中读出这些状态
eBPF程序需要编译、加载、验证和内核执行等过程,用户态借助BPF的映射获取eBPF程序运行状态
eBPF的局限
一些限制
- eBPF程序必须被验证器校验通过后才能执行,且不能包含无法到达的指令
- eBPF程序不能随意调用内核函数,只能调用在API中定义的辅助函数
- eBPF程序栈空间只有512字节,更大存储需要依赖映射存储
- 在5.2内核前,eBPF字节码只支持4096条指令,而5.2提高到100W
- 在不同版本内核运行,可能需要调整源码进行编译
特性支持版本参考kernel version,为了稳定运行建议4.9,或者5以上版本
如何高效学习eBPF
- 理解eBPF的基本原理
- 掌握eBPF的编程接口
- 通过实践把eBPF应用到真正的工作场景中
学习路径有
基础入门
开发并运行第一个eBPF程序
开发环境准备
内核需要开启
- CONFIG_DEBUG_INFO_BTF=y
- CONFIG_DEBUG_INFO=y
推荐使用默认开这些选项的发行版
- Ubuntu 20.10+
- Fedora 31+
- RHEL 8.2+
- Debian 11+
# 创建和启动Ubuntu 21.10虚拟机
vagrant box add ubuntu/impish64
vagrant init ubuntu/impish64
vagrant up
# 登录到虚拟机
vagrant ssh
准备的开发工具
- 将eBPF程序编译成字节码的LLVM
- C语言程序编译工具make
- 最流行的eBPF工具集BCC和它依赖的内核头文件
- 与内核代码仓库实时同步的libbpf
- 内核代码提供的eBPF程序管理工具bpftoo
安装命令
# For Ubuntu20.10+
sudo apt-get install -y make clang llvm libelf-dev libbpf-dev bpfcc-tools libbpfcc-dev linux-tools-$(uname -r) linux-headers-$(uname -r)
# For RHEL8.2+
sudo yum install libbpf-devel make clang llvm elfutils-libelf-devel bpftool bcc-tools bcc-devel
BBC是一个BPF编译器集合,包含用于构建BPF程序的编程框架和库,并提供了大量可以直接使用的工具,提供了Python、C++等编程语言接口
示例跟踪打开文件的openat系统调用
用C开发一个eBPF程序
hello.c
int hello_world(void *ctx)
{
bpf_trace_printk("Hello, World!");
return 0;
}
这里是一个常用的辅助函数,输出的字符串到内核调试文件/sys/kernel/debug/tracing/trace_pipe
,可以通过cat来查看
使用Python和BCC库开发一个用户态程序
#!/usr/bin/env python3
# 1) import bcc library
from bcc import BPF
# 2) load BPF program
b = BPF(src_file="hello.c")
# 3) attach kprobe
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
# 4) read and print /sys/kernel/debug/tracing/trace_pipe
b.trace_print()
整体的步骤为
- 导入BCC库的BPF模块
- 调用BPF()加载BPF源代码
- 将BPF程序挂载到内核探针kprobe,
do_sys_openat2
是系统调用openat在内核的实现 - 读取内核调试文件的内容打印到标准输出
在运行的时候,BCC会调用LLVM把BPF源代码编译为字节码,加载到内核中运行
执行eBPF程序
sudo python3 hello.py
在有文件打开之后能看到如下输出
b' cat-10656 [006] d... 2348.114455: bpf_trace_printk: Hello, World!'
输出的格式由/sys/kernel/debug/tracing/trace_options
更改
默认输出的每个字段
cat-10656
表示进程的名字和PID[006]
表示CPU编号d…
表示一系列的选项2348.114455
表示时间戳bpf_trace_printk
表示函数名
不过并不推荐通过内核文件输出日志,一方面有性能问题,另一方面很多eBPF程序都写入同一位置不好区分
使用map映射的方式输出eBPF的输出,BCC提供了一系列的库函数和辅助宏定义
eBPF程序
// 包含头文件
#include <uapi/linux/openat2.h>
#include <linux/sched.h>
// 定义数据结构
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
char fname[NAME_MAX];
};
// 定义性能事件映射
BPF_PERF_OUTPUT(events);
// 定义kprobe处理函数
int hello_world(struct pt_regs *ctx, int dfd, const char __user * filename, struct open_how *how)
{
struct data_t data = { };
// 获取PID和时间
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
// 获取进程名
if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0)
{
bpf_probe_read(&data.fname, sizeof(data.fname), (void *)filename);
}
// 提交性能事件
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
BPF_PERF_OUTPUT
来定义一个Perf事件类型的BPF映射bpf_get_current_pid_tgid
用于获取进程的TGID和PID,因为定义的data.pid数据类型为u32,所以高32位舍弃掉后就是进程的PIDbpf_ktime_get_ns
用于获取系统自启动以来的时间,单位是纳秒bpf_get_current_comm
用于获取进程名,并把进程名复制到预定义的缓冲区中bpf_probe_read
用于从指定指针处读取固定大小的数据,这里则用于读取进程打开的文件名
用户态程序
使用用户态的辅助程序open_perf_buffer()
来获取BPF_PERF_OUTPUT
对应的数据,需要一个回调函数用于处理从Perf事件类型的BPF映射中读取到数据
from bcc import BPF
# 1) load BPF program
b = BPF(src_file="trace-open.c")
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
# 2) print header
print("%-18s %-16s %-6s %-16s" % ("TIME(s)", "COMM", "PID", "FILE"))
# 3) define the callback for perf event
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 %-16s" % (time_s, event.comm, event.pid, event.fname))
# 4) loop with callback to print_event
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
整体步骤
- 加载eBPF程序并挂载到内核探针
- 数据Header字符串
print_event
定义数据处理函数- 定义event的perf映射,循环通过
perf_buffer_poll
读取映射内容,并执行回调的处理函数
输出信息
TIME(s) COMM PID FILE
2.384485400 b'irqbalance' 991 b'/proc/interrupts'
2.384750400 b'irqbalance' 991 b'/proc/stat'
2.384838400 b'irqbalance' 991 b'/proc/irq/0/smp_affinity'
ebpf运行原理
系统虚拟化与ebpf的一些区别
- 系统虚拟化基于x86或者arm64等通用指令集,具备完整的计算机功能
- ebpf只提供了有限的指令集,只能完成一部分内核功能,并采用C调用约定提供辅助函数在C语言中调用
ebpf在内核中运行时主要由5个模块组成
- ebpf辅助函数,用于ebpf程序与内核其他模块进行交互的函数
- ebpf验证器,用于确保ebpf程序的安全,会将待执行指令创建为一个有向无环图,保证不包含不可达指令,模拟执行没有无效指令
- 11个64位寄存器、一个程序计数器和一个512字节的栈组成的存储模块,R0寄存器用于存储函数调用和ebpf程序的返回值,意味着只能有一个返回值,R1~R5用于存储函数的参数,意味着参数不能超过5个,R10寄存器是一个只读寄存器,用于从栈中读取数据
- 即时编译器,将ebpf字节码编译为本地机器指令
- BPF映射,提供大块存储空间,可以被用户进程访问
bpftool可以查看运行的ebpf程序的状态
# sudo bpftool prog list
89: kprobe name hello_world tag 38dd440716c4900f gpl
loaded_at 2021-11-27T13:20:45+0000 uid 0
xlated 104B jited 70B memlock 4096B
btf_id 131
pids python3(152027)
- 89为ebpf程序编号
- kprobe为程序类型
- hello_world为程序名
有了ebpf编号,可以执行以下命令导出ebpf的指令
# sudo bpftool prog dump xlated id 89
int hello_world(void * ctx):
; int hello_world(void *ctx)
0: (b7) r1 = 33 /* ! */
; ({ char _fmt[] = "Hello, World!"; bpf_trace_printk_(_fmt, sizeof(_fmt)); });
1: (6b) *(u16 *)(r10 -4) = r1
2: (b7) r1 = 1684828783 /* dlro */
3: (63) *(u32 *)(r10 -8) = r1
4: (18) r1 = 0x57202c6f6c6c6548 /* W ,olleH */
6: (7b) *(u64 *)(r10 -16) = r1
7: (bf) r1 = r10
;
8: (07) r1 += -16
; ({ char _fmt[] = "Hello, World!"; bpf_trace_printk_(_fmt, sizeof(_fmt)); });
9: (b7) r2 = 14
10: (85) call bpf_trace_printk#-61616
; return 0;
11: (b7) r0 = 0
12: (95) exit