eBPF 提供了强大的跟踪、探测以及高效内核网络等功能,但由于其接口处于操作系统底层,新手入门起来还是有很大难度,特别是如何编写 eBPF 程序是入门的一大难点。本文将介绍一些常用的 eBPF 编程框架。
BCC
上篇文章介绍的 BCC 其实就提供了对 eBPF 的封装,前端提供 Python API,而后端的 eBPF 程序还是通过 C 来实现。在运行的时候,BCC 会把 eBPF 程序编译成字节码、加载到内核执行,最后再通过用户空间的前端获取执行状态。
BCC 的优点就是简单易用,但也有很多缺点:
- 启动时编译,导致启动缓慢,且编译也需要耗费较高的 CPU 和内存资源。
- 编译 eBPF 要求所有主机上都安装内核头文件。
- 编译错误只有在运行的时候才能检测到,排错困难。
由于这些问题存在,BCC 正在基于 libbpf 将所有工具 转换 为可直接执行的二进制文件,无需外部依赖,从而更易分发到实际生产环境中。转换后的工具,因无需动态编译和接口转换,可以获得更高的性能和更少的资源占用。
除此之外,libbpf 还基于 BTF 和 CO-RE (Compile-Once Run-Everywhere) 提供了更好的便携性(兼容新旧内核版本):
- BTF 是 BPF 类型格式,用于避免依赖 Clang 和内核头文件。
- CO-RE 则使得 BTF 字节码支持重定位,避免 LLVM 重新编译的需要。
借助于 BTF 和 CO-RE 的优势,你也可以在 /sys/kernel/btf/vmlinux
找到内核的 BTF 信息,甚至可以通过 bpftool 将其导出(一般放到文件 vmlinux.h
中):
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
你可以在 libbpf-tools 找到 BCC 目前已迁移完成的工具。
libbpf-bootstrap
libbpf 在使用上并不是很直观,所以 eBPF 维护者开发了一个脚手架项目 libbpf-bootstrap。它结合了 BPF 社区的最佳开发实践,为初学者提供了一个简单易用的上手框架。
在使用 libbpf-bootstrap 时,需要首先安装 LLVM 和依赖库文件:
sudo apt install -y bison build-essential cmake flex git libedit-dev pkg-config libmnl-dev \
python zlib1g-dev libssl-dev libelf-dev libcap-dev libfl-dev llvm clang pkg-config \
gcc-multilib luajit libluajit-5.1-dev libncurses5-dev libclang-dev clang-tools
然后检出其脚手架代码,检查示例代码是否可以编译通过:
# checkout libbpf-bootstrap
git clone https://github.com/libbpf/libbpf-bootstrap
# update submodules
git submodule update --init --recursive
# build existing samples
cd src && make
接下来,创建两个文件,分别是用户空间的 hello.c 以及 BPF 程序 hello.bpf.c(libbpf-bootstrap 要求 BPF 文件的格式总是 <APP-NAME>.bpf.c
)。
/* cat hello.bpf.c */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
SEC("tracepoint/syscalls/sys_enter_execve")
int handle_tp(void *ctx)
{
int pid = bpf_get_current_pid_tgid()>> 32;
char fmt[] = "BPF triggered from PID %d.\n";
bpf_trace_printk(fmt, sizeof(fmt), pid);
return 0;
}
char LICENSE[] SEC("license") = "Dual BSD/GPL";
/* cat hello.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "hello.skel.h"
#define DEBUGFS "/sys/kernel/debug/tracing/"
/* logging function used for debugging */
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
#ifdef DEBUGBPF
return vfprintf(stderr, format, args);
#else
return 0;
#endif
}
/* read trace logs from debug fs */
void read_trace_pipe(void)
{
int trace_fd;
trace_fd = open(DEBUGFS "trace_pipe", O_RDONLY, 0);
if (trace_fd < 0)
return;
while (1) {
static char buf[4096];
ssize_t sz;
sz = read(trace_fd, buf, sizeof(buf) - 1);
if (sz> 0) {
buf[sz] = 0;
puts(buf);
}
}
}
/* set rlimit (required for every app) */
static void bump_memlock_rlimit(void)
{
struct rlimit rlim_new = {
.rlim_cur = RLIM_INFINITY,
.rlim_max = RLIM_INFINITY,
};
if (setrlimit(RLIMIT_MEMLOCK, &rlim_new)) {
fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit!\n");
exit(1);
}
}
int main(int argc, char **argv)
{
struct hello_bpf *skel;
int err;
/* Set up libbpf errors and debug info callback */
libbpf_set_print(libbpf_print_fn);
/* Bump RLIMIT_MEMLOCK to allow BPF sub-system to do anything */
bump_memlock_rlimit();
/* Open BPF application */
skel = hello_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
/* Load & verify BPF programs */
err = hello_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
goto cleanup;
}
/* Attach tracepoint handler */
err = hello_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
printf("Hello BPF started, hit Ctrl+C to stop!\n");
read_trace_pipe();
cleanup:
hello_bpf__destroy(skel);
return -err;
}
更新 Makefile 的 APPS 列表
APPS = minimal bootstrap uprobe hello
最后,编译运行 hello 程序:
$ make
BPF .output/hello.bpf.o
GEN-SKEL .output/hello.skel.h
CC .output/hello.o
BINARY hello
$ ./hello
Hello BPF started, hit Ctrl+C to stop!
<...>-241424 [006] d... 202520.596987: bpf_trace_printk: BPF triggered from PID 241424.
可以发现,用 libbpf-bootstrap 开发 BPF 程序非常方便。其源码库中三个示例的解析可以参考 Building BPF applications with libbpf-bootstrap,而更多的示例则可以查看 BCC 中的 libbpf-tools。
注意: libbpf 需要开启内核选项
CONFIG_DEBUG_INFO_BTF=y
以及CONFIG_DEBUG_INFO=y
。在编译内核时,推荐安装 pahole 1.16+,否则的话,就无法生成 BTF。 或者,也可以从 https://kernel.ubuntu.com/~kernel-ppa/mainline/ 直接下载已经默认开启这些选项的内核 DEB 包(比如 v5.10.9)。
内核源码
除了以上两种方法,最后一种门槛更高一些的方法是从内核源码中直接编译 BPF 程序。这种方法需要对内核编译有一定了解,且需要善于运用搜索引擎解决编译过程中的各种问题。
首先安装必要的依赖:
sudo apt install -y bison build-essential cmake flex git libedit-dev pkg-config libmnl-dev \
python zlib1g-dev libssl-dev libelf-dev libcap-dev libfl-dev llvm clang pkg-config \
gcc-multilib luajit libluajit-5.1-dev libncurses5-dev libclang-dev clang-tools
然后检出内核源码:
apt install linux-source
cd /usr/src/linux-source-5.4.0 # 版本取决于具体系统
tar jxf linux-source-5.4.0.tar.bz2
cd linux-source-5.4.0
最后编译内核 BPF 程序示例:
cp /boot/config-5.4.0-40-generic .config
make headers_install
make M=samples/bpf
而具体的 Hello World 可以参考 eBPF 环境搭建。
内核中的程序 hello_kern.c
:
#include <linux/bpf.h>
#include "bpf_helpers.h"
#define SEC(NAME) __attribute__((section(NAME), used))
SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx)
{
char msg[] = "Hello BPF!\n";
bpf_trace_printk(msg, sizeof(msg));
return 0;
}
char _license[] SEC("license") = "GPL";
用户态的程序 hello_user.c
:
#include <stdio.h>
#include "bpf_load.h"
int main(int argc, char **argv)
{
if(load_bpf_file("hello_kern.o") != 0)
{
printf("The kernel didn't load BPF program\n");
return -1;
}
read_trace_pipe();
return 0;
}
在对应的位置修改 Makefile 文件,添加以下三行:
hostprogs-y += hello
hello-objs := bpf_load.o hello_user.o
always += hello_kern.o
最后编译运行:
# V=1 查看详细编译输出
make M=samples/bpf V=1
cd samples/bpf
./hello
小结
本文介绍了三种 eBPF 入门的编程方法,分别是 BCC、libbpf-bootstrap 以及内核源码。对于入门者来说,推荐用 libbpf-bootstrap 作为入门学习参考。
欢迎扫描下面的二维码关注漫谈云原生公众号,回复任意关键字查询更多云原生知识库,或回复联系加我微信。