eBPF的 HelloWorld 示例
前言
极客时间 eBPF 核心技术与实战 的学习笔记.
本章简单写一个入门的 ebpf 程序
环境安装
每个版本都不太一样, 所以这里只是做个参考
如果在安装环境上遇到问题只能自行解决了
我自己使用的是 debian12
root@VM-4-12-debian:~/ebpf# uname -a
Linux VM-4-12-debian 6.1.0-28-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.119-1 (2024-11-22) x86_64 GNU/Linux
推荐安装最新或者次版本
# 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
helloworld
教程以 python+c 作为例子
代码意思配合注释看
在某个目录中新建两个文件
hello.c(内核态代码)
// 定义 hello 函数
int hello_world(void *ctx)
{
// bpf_trace_printk 打印输出到 调试文件
bpf_trace_printk("Hello, World!");
return 0;
}
hello.py(用户态代码)
# 加载 bcc 包,需要先 pip install bcc
from bcc import BPF
# 2) 加载 hello.c 代码
b = BPF(src_file="hello.c")
# 3) 挂载到 BPF 系统
# 指定在 do_sys_openat2 事件发生时, 触发 hello.c 中的 hello_world 函数
# do_sys_openat2 在 打开文件 等操作时被触发
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
# 4) 读取和输出内核调试文件 /sys/kernel/debug/tracing/trace_pipe
b.trace_print()
执行
sudo python3 hello.py
结果解析
运行时我们会发现打印了很多信息, 每一行类似
b'python3-28840 [001] ...21 4786.665982: bpf_trace_printk: Hello, World!'
其中:
python3-28840
标识调用的进程和 pid[001]
代表 CPU 编号(从0开始)[...21]
代表若干选项[4786.665982]
标识发生时间戳bpf_trace_printk
代表触发的函数名Hello, World!
代表输出的内容
然而通常我们不会使用这种方式来进行业务的处理, 这是因为 调试日志不仅包含了我们的信息, 还会有一些其他的日志. 另外还可能带来性能问题.
使用 BPF 映射(map)来传输数据
BCC自带的宏和辅助函数可以查看 bcc/docs/reference_guide.md at master · iovisor/bcc
如果是写过 openresty 的开发者可以把他视作 openresty+lua 中的 lua 辅助函数
map.c
// 引入头文件
#include <uapi/linux/openat2.h> // 文件打开的相关定义
#include <linux/sched.h> // 进程调度相关
// 自定义的 map 数据结构
struct data_t {
u32 pid; // pid
u64 ts; // 时间戳
char comm[TASK_COMM_LEN]; // 进程名数组, TASK_COMM_LEN 是 sched.h 引入的宏
char fname[NAME_MAX]; // 文件名数组
};
// BCC 自带的宏, 可以将数据发送到用户态代码中
// 通过perf环缓冲区将自定义事件数据推送到用户空间, 自定义其代号为 events
BPF_PERF_OUTPUT(events);
// 定义kprobe处理函数
int hello_world(struct pt_regs *ctx, int dfd, const char __user * filename, struct open_how *how)
{
// 组装data
struct data_t data = { };
// 获取PID和时间
data.pid = bpf_get_current_pid_tgid(); // BCC 自带的辅助函数, 可以获取当前调用的 pid 相关信息
data.ts = bpf_ktime_get_ns(); // BCC 自带的辅助函数, 可以获取当前的 nano 时间戳(从系统启动开始算)
// 获取进程信息
if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0) // BCC自带的辅助函数, 获取当前触发的进程名
{
bpf_probe_read(&data.fname, sizeof(data.fname), (void *)filename); // 从指针filename读取文件名
}
// 调用 events 发送数据
events.perf_submit(ctx, &data, sizeof(data)); // 将 data 发送到用户态代码中
return 0;
}
map.py
from bcc import BPF
# 加载 map.c 到BPF 中, 在 do_sys_openat2 调用时触发
b = BPF(src_file="map.c")
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
# 打印 header 日志
print("%-18s %-16s %-6s %-16s" % ("TIME(s)", "COMM", "PID", "FILE"))
# 定义从内核态代码中获取到数据后的处理逻辑
start = 0
# data 是接受到的数据
# 这三个参数是 BCC 传输定义的, https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md#2-open_perf_buffer
def print_event(cpu, data, size):
global start
event = b["events"].event(data) # events map
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 的数据映射, 获取数据并交由 print_event 函数处理
b["events"].open_perf_buffer(print_event)
while 1: # 用户态死循环不会被校验器拦截
try:
# 从缓冲区中读取数据进行处理
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
运行发现用户态已经可以接收并处理数据了
root@VM-4-12-debian:~/ebpf# python3 map.py
TIME(s) COMM PID FILE
0.000000000 b'YDService' 172339 b'/proc/sys/kernel/random/uuid'
0.000081607 b'YDService' 172339 b'/proc/sys/kernel/random/uuid'
0.352421903 b'barad_agent' 575490 b'/proc/meminfo'
0.352921024 b'barad_agent' 575490 b'/proc/meminfo'
0.353480201 b'barad_agent' 575490 b'/proc/vmstat'
0.356383610 b'YDService' 172316 b'/proc/575495/cmdline'