ebpf: CO-RE, BTF, and Libbpf(三)
本文内容主要来源于Learning eBPF,可阅读原文了解更全面的内容。
本文涉及源码也来自于书中对应的github:https://github.com/lizrice/learning-ebpf/
CO-RE 用户空间代码
ebpf 程序主要包含两部分: 会被载入到内核空间中运行的主要功能逻辑代码; 以及控制 ebpf 程序的载入, 生命周期等的用户空间代码. 接下来展示用户空间部分的代码及讲解.
ebpf 提供了一些供用户空间使用的非常方便的接口, 用于执行加载 ebpf 程序, 将其附着到对应事件上, 或者访问 map 信息等. 这些接口在一个叫做 skeleton 的文件中. 我们可以用 bpftool 通过 ebpf 的 elf 文件生成. 指令如下:
bpftool gen skeleton hello-buffer-config.bpf.o > hello-buffer-config.skel.h
这部分已经在 Makefile 中写好了:
TARGET = hello-buffer-config
BPF_OBJ = ${TARGET:=.bpf.o}
USER_C = ${TARGET:=.c}
USER_SKEL = ${TARGET:=.skel.h}
...
$(USER_SKEL): $(BPF_OBJ)bpftool gen skeleton $< > $@
hello-buffer-config.skel.h 中封装了我们编写用户空间代码需要用到的一些函数, 如下图所示, 都是以 hello_buffer_config_bpf__
为前缀的. 比如这里将 open 和 load 封装为一个函数 hello_buffer_config_bpf__open_and_load
, 更便于使用.
最终还是调用的 libbpf 中的函数, 定义在 libbpf/src/libbpf.c
中.
main函数部分如下所示:
-
用
libbpf_set_print
设定了一个回调函数, 在该程序打印 log 时会调用libbpf_print_fn
-
调用
hello_buffer_config_bpf__open_and_load
, 会生成一个 skel 结构体, 包含所有的 map 和程序代码, 并将其加载到内核中 -
将程序自动附着在对应的时间中, 这里著需要传入 skel , 因为 skel 已经包含整个 ebpf 程序所有信息, 自然也包括 section , event 信息. 如果没有明确定义
SEC
的话, 将会自动附着到所有的事件上, 如 kprobe, xdp 等等. -
创建一个结构体, 用来存储 perf buffer output. handle_event 是在perf buffer 中有新数据的时候会调用的回调函数; lost_event 是在 perf buffer 中预警没有足够的空间来荣达新数据时调用的回调函数. 这里只是简单的打印相关信息.
-
轮询 perf buffer. 回调函数就是上面定义的
handle_event
和lost_event
-
清理代码, 释放 perf buffer 并销毁 eBPF 程序和 map.
这个程序的最终执行结果如下所示:
总结
CO-RE 可以使 eBPF 程序可以在一个机器上编译, 而且另外一个 (拥有不同内核版本的) 机器上运行. 这极大的提升了 eBPF 程序的可移植性.
编译器在编译时保存完整的程序和结构体相关信息, 并在将程序加载到内核中时, 使用保存的重定位信息来重写指令, 以确保在不同内核版本也能成功访问到相关的结构体成员 (即使成员位置, 名字发生变化, 甚至被删除).
此外, 用户空间程序也至关重要, 管理着 eBPF 程序的加载, 撤销, 生命周期等. 我们可以通过使用 bpftool 生成的 BPF skeleton 中封装好的更为便捷的函数来编写用户空间代码.