目录
第二步:使用 Python 和 BCC 库开发一个用户态程序
如何选择 eBPF 开发环境?
虽然 Linux 内核很早就已经支持了 eBPF,但很多新特性都是在4.x 版本中逐步增加的。所以,想要稳定运行 eBPF 程序,内核至少需要 4.9 或者更新的版本。而在开发和学习 eBPF 时,为了体验和掌握最新的 eBPF 特性,我推荐使用更新的 5.x内核。
作为 eBPF 最重大的改进之一,一次编译到处执行(简称 CO-RE)解决了内核数据结构在不同版本差异导致的兼容性问题。不过,在使用 CO-RE 之前,内核需要开启
CONFIG_DEBUG_INFO_BTF=y 和 CONFIG_DEBUG_INFO=y 这两个编译选项。为了避免你在首次学习 eBPF 时就去重新编译内核,我推荐使用已经默认开启这些编译选项的发行版,作为你的开发环境,比如:
Ubuntu 20.10+
如何搭建 eBPF 开发环境?
接下来就需要安装 eBPF 开发和运行所需要的开发工具,这包括:
- 将 eBPF 程序编译成字节码的 LLVM;
- C 语言程序编译工具 make;
- 最流行的 eBPF 工具集 BCC 和它依赖的内核头文件;
- 与内核代码仓库实时同步的 libbpf;
- 同样是内核代码提供的 eBPF 程序管理工具 bpftool。
你可以执行下面的命令,来安装这些必要的开发工具:
sudo apt-get update
sudo apt-get install -y make clang llvm libelf-dev libbpf-dev bpfcc-tools libbpfcc-dev linux-tools-generic linux-headers-$(uname -r)
如何开发第一个 eBPF 程序?
第一步:使用 C 开发一个 eBPF 程序
int hello_world(void *ctx)
{
bpf_trace_printk("Hello, World!");
return 0;
}
第二步:使用 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()
- 第 1) 处导入了 BCC 库的 BPF 模块,以便接下来调用;
- 第 2) 处调用 BPF() 加载第一步开发的 BPF 源代码;
- 第 3) 处将 BPF 程序挂载到内核探针(简称 kprobe),其中 do_sys_openat2() 是系统调用 openat() 在内核中的实现;
-
第 4) 处则是读取内核调试文件 /sys/kernel/debug/tracing/trace_pipe 的内容,并打印到标准输出中。
第三步:执行 eBPF 程序
chmod +x hello.py
//sudo ./hello.py
sudo python3 hello.py
- python3-3042 表示进程的名字 和 PID
- [002] 表示CPU编号
- d... 表示一系列的选项
- 961.761010 表示时间戳
- bpf_trace_printk表示函数名
- 最后的 "Hello,World" 就是调用 bpf_trace_printk() 传入的字符串。
如何改进第一个 eBPF 程序?
我们知道,BPF 程序可以利用 BPF 映射(map)进行数据存储,而用户程序也需要通过 BPF 映射,同运行在内核中的 BPF 程序进行交互。所以,为了解决上面提到的第一个问题,即获取被打开文件名的问题,我们就要引入BPF映射。
// 包含头文件
#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_get_current_pid_tgid 用于获取进程的 TGID 和 PID。因为这儿定义的 data.pid 数据类型为 u32,所以高 32 位舍弃掉后就是进程的 PID;
-
bpf_ktime_get_ns 用于获取系统自启动以来的时间,单位是纳秒;
-
bpf_probe_read 用于从指定指针处读取固定大小的数据,这里则用于读取进程打开的文件名。
-
bpf_get_current_comm 用于获取进程名,并把进程名复制到预定义的缓冲区中。
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()
让我们来看看每一处的具体含义:
sudo python3 trace-open.py
稍等一会,你会看到类似下面的输出:
恭喜,你已经开发了第一个完整的 eBPF 程序。相对于前面的 Hello World,它的输出不仅格式更为清晰,还把进程打开的文件名输出出来了,这在排查频繁打开文件相关的性能问题时尤其有用。
源码附录
trace_open.c
#include <uapi/linux/openat2.h>
#include <linux/sched.h>
// define the output data structure.
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
char fname[NAME_MAX];
};
BPF_PERF_OUTPUT(events);
// define the handler for kprobe.
// refer https://elixir.bootlin.com/linux/latest/source/fs/open.c#L1196 for the param definitions.
int hello_world(struct pt_regs *ctx, int dfd, const char __user * filename,
struct open_how *how)
{
struct data_t data = { };
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;
}
trace-open.py
#!/usr/bin/env python3
# Tracing openat2() system call.
from bcc import BPF
from bcc.utils import printb
# 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
printb(b"%-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 开发时常用的工具和依赖库。并且,我从最简单的 Hello World 开始,带你借助 BCC 从零开发了一个跟踪 openat() 系统调用的 eBPF 程序。
通常,开发一个 eBPF 程序需要经过开发 C 语言 eBPF 程序、编译为 BPF 字节码、加载 BPF 字节码到内核、内核验证并运行 BPF 字节码,以及用户程序读取 BPF 映射五个步骤。使用 BCC 的好处是,它把这几个步骤通过内置框架抽象了起来,并提供了简单易用的 Python 接口,这可以帮你大大简化 eBPF 程序的开发。
除此之外,BCC 提供的一系列工具不仅可以直接用在生产环境中,还是你学习和开发新的 eBPF 程序的最佳参考示例。