eBPF 的基本使用
Drunkbaby Lv6

eBPF 的基本使用,eBPF 学习(一)

eBPF 的基础使用

eBPF 简介:安全和有效地扩展内核

eBPF 是一项革命性的技术,起源于 Linux 内核,可以在操作系统的内核中运行沙盒程序。它被用来安全和有效地扩展内核的功能,而不需要改变内核的源代码或加载内核模块。eBPF 通过允许在操作系统内运行沙盒程序,应用程序开发人员可以在运行时,可编程地向操作系统动态添加额外的功能。然后,操作系统保证安全和执行效率,就像在即时编译(JIT)编译器和验证引擎的帮助下进行本地编译一样。eBPF 程序在内核版本之间是可移植的,并且可以自动更新,从而避免了工作负载中断和节点重启。

今天,eBPF 被广泛用于各类场景:在现代数据中心和云原生环境中,可以提供高性能的网络包处理和负载均衡;以非常低的资源开销,做到对多种细粒度指标的可观测性,帮助应用程序开发人员跟踪应用程序,为性能故障排除提供洞察力;保障应用程序和容器运行时的安全执行,等等。可能性是无穷的,而 eBPF 在操作系统内核中所释放的创新才刚刚开始。

eBPF 的简单使用

  • 编写一个 helloworld 程序

安装对应的开发工具

1
# 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)

在开发 eBPF 程序之前,我们先来看一下 eBPF 的开发和执行过程。一般来说,这个过程分为以下 5 步:

第一步,使用 C 语言开发一个 eBPF 程序;

第二步,借助 LLVM 把 eBPF 程序编译成 BPF 字节码;

第三步,通过 bpf 系统调用,把 BPF 字节码提交给内核;

第四步,内核验证并运行 BPF 字节码,并把相应的状态保存到 BPF 映射中;

第五步,用户程序通过 BPF 映射查询 BPF 字节码的运行状态。

这里我们使用 bcc 来开发 eBPF 程序,如果使用 bcc 来开发 eBPF 程序的话,可以将前面所说的五步简化为三步

第一步:使用 C 开发一个 eBPF 程序

  • 新建一个 hello.c 文件,并输入下面的内容:
1
2
3
4
5
int hello_world(void *ctx)
{
bpf_trace_printk("Hello, World!");
return 0;
}

输出 Hello,World,由于是在内核中进行运行所以结果输出到内核调试文件 /sys/kernel/debug/tracing/trace_pipe

第二步:使用 Python 和 BCC 库开发一个用户态程序

接下来,创建一个 hello.py 文件,并输入下面的内容

1
2
3
4
5
6
7
8
9
10
#!/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 程序

需要以 root 用户权限执行

1
sudo python3 hello.py

输出如图

这就是一个简单的 eBPF 程序了,但是我们可以看到这个 eBPF 程序当中有几个缺点

  1. 既然我们是通过 ebpf 监控系统打开文件的操作,那么正常情况输出结果应该有打开的文件名
  2. 现在读取结果是直接读 /sys/kernel/debug/tracing/trace_pipe 这个内核调试文件的,这就有一个问题,当存在多个 ebpf 程序的时候输出结果都是到这个文件,这样的话就使得输出结果很不清晰,并且这样性能也很差

针对上述两个问题,需要利用 BPF 映射来结果,通过映射来读取 ebpf 执行的结果并适当进行一些数据处理,因为上述截图中很多输出字段都是我们不需要的

改进第一个 eBPF 程序

BPF 程序可以利用 BPF 映射(map)进行数据存储,而用户程序也需要通过 BPF 映射,同运行在内核中的 BPF 程序进行交互。

所以,为了解决上面提到的第一个问题,即获取被打开文件名的问题,我们就要引入 BPF 映射。

BCC 定义了一系列的库函数和辅助宏定义。这里使用 BPF_PERF_OUTPUT 来定义一个 Perf 事件类型的 BPF 映射,这里先定义一个数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 包含头文件
#include <uapi/linux/openat2.h>
#include <linux/sched.h>

// 定义数据结构
struct data_t {
u32 pid; // 进程pid
u64 ts; // 时间
char comm[TASK_COMM_LEN]; // 进程名
char fname[NAME_MAX]; // 文件名
};

// 定义性能事件映射
BPF_PERF_OUTPUT(events);

然后,在 eBPF 程序中,填充这个数据结构,并调用 perf_submit() 把数据提交到刚才定义的 BPF 映射中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义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;
}

合起来就是下面这个文件,该文件最终在内核中进行运行,将采集到的数据输出到映射中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 包含头文件
#include <uapi/linux/openat2.h>
#include <linux/sched.h>

// 定义数据结构
struct data_t {
u32 pid; // 进程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;
}

然后我们需要编写一个用户态的调用程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from bcc import BPF

# 加载bpf代码
b = BPF(src_file="trace-open.c")
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")

# 格式化输出
print("%-18s %-16s %-6s %-16s" % ("TIME(s)", "COMM", "PID", "FILE"))

# 这里是一个回调函数来进行数据处理
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))

# 定义名为 “events” 的 Perf 事件映射,然后循环调用读取
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()

小结

简单接触一下 eBPF 的使用

Ref

极客时间 eBPF 核心技术与实战

 评论