eBPF—初探Linux内核扩展

eBPF(extended Berkeley Packet Filter),即扩展的伯克利包过滤器,可谓 Linux 社区的新宠,Goole、Facebook、Twitter等公司都开始投身于 eBPF 技术的研究和应用。

eBPF究竟有什么魅力让大家都关注它呢?一般来说,要向内核添加新功能,需要修改内核源代码或者编写内核模块来实现。而eBPF允许程序在不修改内核源代码或添加额外的内核模块情况下,以更灵活和便捷的方式扩展内核功能,类似一个通用内核执行引擎。本文就先带大家一起来到eBPF的世界,尝试一下如何编写我们的第一个eBPF程序。

一、概述

1. 什么是BPF

BPF(Berkeley Packet Filter),即伯克利包过滤器,最初构想提出于 1992 年,其目的是为了提供一种过滤包的方法,并且要避免从内核空间到用户空间的无用的数据包复制行为。

BPF 是基于寄存器虚拟机实现的,支持 JIT(Just-In-Time),比基于栈实现的性能高很多。它能载入用户态代码并且在内核环境下运行,内核提供 BPF 相关的接口,用户可以将代码编译成字节码,通过 BPF 接口加载到 BPF 虚拟机中,当然用户代码跑在内核环境中是有风险的,如有处理不当,可能会导致内核崩溃。因此在用户代码跑在内核环境之前,内核会先做一层严格的检验,确保没问题才会被成功加载到内核环境中。

BPF架构:

BPF最初是专门为过滤网络数据包而创造的,可以在不升级内核的情况下,对kernel的工作情况数据进行跟踪、收集、统计分析。

2. 什么是 eBPF

eBPF(extended Berkeley Packet Filter)起源于BPF,它提供了内核的数据包过滤机制。其扩充了 BPF 的功能,丰富了指令集,改善了它的性能。与此同时,将以前的 BPF 变成 cBPF( classic BPF)。新版本出现了如映射和尾调用tail call这样的新特性,并且 JIT 编译器也被重写了。新的语言比 cBPF 更接近于原生机器语言,并且在内核中创建了新的挂钩点,eBPF程序会注册到某些挂钩点,当内核运行到挂钩点后,就执行eBPF程序。这一机制使得eBPF 程序可以被用于各种各样的情形下,而不再仅限于过滤网络数据包。

挂钩点主要包括以下几类:

  • 网络事件,例如封包到达
  • Kprobes / Uprobes
  • 系统调用
  • 函数的入口/退出点

其分为两个主要应用领域:

  • 内核跟踪和事件监控:BPF 程序可以被附着到探针(kprobe),而且它与其它跟踪模式相比,有很多的优点(有时也有一些缺点)。
  • 网络编程:除了套接字过滤器外,eBPF 程序还可以附加到 tc(Linux 流量控制工具)的入站或者出站接口上,以一种很高效的方式去执行各种包处理任务。这种使用方式在这个领域开创了一个新的天地。
eBPF 架构:

如上,一般 eBPF 的工作逻辑是:

  1. BPF Program 通过 LLVM/Clang 编译成 eBPF 定义的字节码 prog.bpf。
  2. 通过系统调用 bpf() 将 bpf 字节码指令传入内核中。
  3. 经过 verifier 检验字节码的安全性、合规性。
  4. 在确认字节码安全后将其加载对应的内核模块执行,通过 Helper/hook 机制,eBPF 与内核可以交换数据/逻辑。BPF 观测技术相关的程序程序类型可能是 kprobes/uprobes/tracepoint/perf_events 中的一个或多个,其中:
    • kprobes:实现内核中动态跟踪。kprobes 可以跟踪到 Linux 内核中的函数入口或返回点,但是不是稳定 ABI 接口,可能会因为内核版本变化导致,导致跟踪失效。理论上可以跟踪到所有导出的符号 /proc/kallsyms。
    • uprobes:用户级别的动态跟踪。与 kprobes 类似,只是跟踪的函数为用户程序中的函数。
    • tracepoints:内核中静态跟踪。tracepoints 是内核开发人员维护的跟踪点,能够提供稳定的 ABI 接口,但是由于是研发人员维护,数量和场景可能受限。
    • perf_events:定时采样和 PMC。
  5. 用户空间通过 BPF map 与内核通信。
    • BPF Maps以键值对形式的存储,通过文件描述符来定位,值是不透明的Blob(任意数据)。用于跨越多次调用共享数据,或者与用户空间应用程序共享数据。
    • 一个eBPF程序可以直接访问最多64个Map,多个eBPF程序可以共享同一Map。

eBPF 分用户空间和内核空间,用户空间和内核空间的交互有 2 种方式:

  • BPF map:统计摘要数据
  • perf-event:用户空间获取实时监测数据

简单总结下,就是eBPF可以在不升级linux内核的情况下,将用户自定义代码插入到内核执行过程中,从而对某些特定的内核或用户程序函数符号/偏移量等的触发过程进行监控、采集等自定义操作,并支持将输出数据暂存在bpf maps中,由reader读取后进行后续的落盘、数据分析等。

 

二、eBPF的具体能力

1. eBPF 主要能力

  • kprobe:内核插探,在指定的内核函数前或后执行。
    • kretprobe:内核函数返回插探,在指定的内核函数返回时执行。
  • tracepoint:跟踪点,在指定的内核跟踪点处执行。
    • tracepoint_return:跟踪点返回,在指定的内核跟踪点返回时执行。
  • raw_tracepoint:原始跟踪点,在指定的内核原始跟踪点处执行。
    • raw_tracepoint_return:原始跟踪点返回,在指定的内核原始跟踪
  • xdp:网络数据处理,拦截和处理网络数据包。
  • perf_event:性能事件,用于处理内核性能事件。

2. eBPF与直接编写内核模块的对比

维度 Linux 内核模块 eBPF
kprobes/tracepoints 支持 支持
安全性 可能引入安全漏洞或导致内核 Panic 通过验证器进行检查,可以保障内核安全
内核函数 可以调用内核函数 只能通过 BPF Helper 函数调用
编译性 需要编译内核 不需要编译内核,引入头文件即可
运行 基于相同内核运行 基于稳定 ABI 的 BPF 程序可以编译一次,各处运行
与应用程序交互 打印日志或文件 通过 perf_event 或 map 结构
数据结构丰富性 一般 丰富
入门门槛
升级 需要卸载和加载,可能导致处理流程中断 原子替换升级,不会造成处理流程中断
内核内置 视情况而定 内核内置支持

三、eBPF的使用

eBPF 提供多种使用方式:BCC、BPFTrace、libbpf C/C++ Library、eBPF GO library 等。

更早期的工具使用 eBPF 受限的 C 语言来编写 BPF 程序,使用 LLVM clang 编译成 BPF 代码,这对于普通使用者上手有不少门槛。对于大多数开发者而言,更多的是基于 BPF 技术之上编写解决我们日常遇到的各种问题。

BCC 作为 BPF 的前端,在观测和性能分析上已经有了诸多灵活且功能强大的工具箱,完全可以满足我们日常使用。而libbpf C/C++ library则主要负责充当 BPF 程序加载器,它加载、检查和重新定位 BPF 程序,整理映射和钩子。libbpf允许创建在不同内核版本上运行的二进制文件,通过内置的vmheader兼容多种内核类型,编译一次到处运行,而不是像BCC那样嵌入LLVM/Clang编译器组件并在运行时编译程序(这需要额外的 CPU 和内存)。同时它在资源使用方面更好,输出更小的二进制文件,并且使用更少的内存,这使得它非常适合系统关键任务。它对性能的影响有限,因此也非常适合监控、安全和分析。

他们的对比情况如下:

BCC(BPF Compiler Collection):

  • 提供了更高阶的抽象,可以让用户采用 Python、C++ 和 Lua 等高级语言快速开发 BPF 程序
  • 提供了许多有用的资源和示例来构建有效的内核跟踪和操作程序
  • 当工具启动时,需要占用大量CPU和内存资源来编译BPF程序,如果它在资源受限的服务器上运行,可能会引发问题。
  • 由于 BPF 程序是在运行时编译的,因此许多简单的编译错误只能在运行时检测到。

libbpf + BPF CO-RE(Compile Once – Run Everywhere):

  • 在资源使用方面更好,输出更小的二进制文件,并且使用更少的内存(BPF CO-RE内存开销约为bcc-python的1/10),非常适合系统关键任务
  • 它对性能的影响有限,因此也非常适合监控、安全和分析,并能够支持更多的硬件环境
  • 从BCC迁移到Libbpf并不复杂

1. bcc-tools

为了快速学习,我们先使用 BCC 工具来编写我们的第一个eBPF程序,它内置非常丰富的工具库,可以帮我们简化了很多繁琐的工作。

下面我们将使用 BCC 工具来介绍怎么编写一个 eBPF 程序。

注意:由于 eBPF 对内核的版本有较高的要求,不同版本的内核对 eBPF 的支持可能有所不相同。所以使用 eBPF 时,最好使用最新版本的内核。本文使用 Ubuntu 20.04.6(内核版本为5.15.0)。

1.1 安装bcc-tools

经过测试Ubuntu20.04使用apt安装的bpfcc-tools无法正常工作,我们这里采用源码安装

# 安装依赖 For Focal (20.04.1 LTS)
sudo apt install -y zip bison build-essential cmake flex git libedit-dev  libllvm12 llvm-12-dev libclang-12-dev python zlib1g-dev libelf-dev libfl-dev python3-setuptools liblzma-dev arping netperf iperf
# clang13
vim /etc/apt/sources.list
  deb http://archive.ubuntu.com/ubuntu/ focal-proposed universe
sudo apt-get update
sudo apt-get install clang clang-13 libclang-13-dev
cd /usr/bin
sudo ln -sf ../lib/llvm-13/bin/clang clang
sudo ln -sf ../lib/llvm-13/bin/clang++ clang++
sudo ln -sf ../lib/llvm-13/bin/clang-format clang-format
sudo ln -sf llvm-strip-13 llvm-strip
export LLVM_ROOT=/usr/lib/llvm-13

# 编译bcc
git clone https://github.com/iovisor/bcc.git
mkdir bcc/build; cd bcc/build
cmake ..
make
sudo make install
cmake -DPYTHON_CMD=python3 ..     # build python3 binding
pushd src/python/
make
sudo make install
popd
1.2 编写第一个eBPF程序

使用 BCC 编写 eBPF 程序的步骤如下:

  1. 使用 C 语言编写 运行在内核态的 eBPF 程序。
  2. 使用 Python 编写加载代码和用户态功能。

因为 LLVM/Clang 只支持将 C 语言编译成 eBPF 字节码,而不支持将 Python 代码编译成 eBPF 字节码。所以,eBPF 内核态程序只能使用 C 语言编写。而 eBPF 的用户态程序可以使用 Python 进行编写,这样就能简化编写难度。

所以,第一步就是编写 eBPF 内核态程序。

a. 使用 C 编写 eBPF 程序

vim hello.c

int hello_ebpf(void *ctx)
{
    bpf_trace_printk("Hello, eBPF!"); // 将信息输出到trace_pipe(/sys/kernel/debug/tracing/trace_pipe)
    return 0;
}

b. 使用 Python 和 BCC 工具开发一个用户态程序

vim hello.py 

#!/usr/bin/env python3

# 导入了 BCC 库的 BPF 模块,以便接下来调用
from bcc import BPF

# 调用 BPF() 函数加载 eBPF 内核态程序(也就是我们编写的hello.c)
b = BPF(src_file="hello.c")

# 将 eBPF 程序挂载到内核探针(简称 kprobe),其中 do_sys_openat2() 是系统调用 openat() 在内核中的实现
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_ebpf")

# 读取内核调试文件/sys/kernel/debug/tracing/trace_pipe的内容(bpf_trace_printk()函数会将信息写入到此文件),并打印到标准输出中
b.trace_print()

c. 运行 eBPF 程序

用户态程序开发完成之后,最后一步就是执行它了。需要注意的是,eBPF 程序需要以 root 用户来运行:

$ sudo python3 hello.py 
b' <...>-40799 [003] d...1 120070.935621: bpf_trace_printk: Hello, eBPF!'
b' <...>-26273 [004] d...1 120071.218366: bpf_trace_printk: Hello, eBPF!'
b' <...>-386 [005] d...1 120072.792249: bpf_trace_printk: Hello, eBPF!'
b' <...>-386 [005] d...1 120072.792364: bpf_trace_printk: Hello, eBPF!'
b' <...>-386 [005] d...1 120072.792416: bpf_trace_printk: Hello, eBPF!'
...

到了这里,我们已经成功开发并运行了第一个 eBPF 程序。这个程序很简单,也没什么实际的用途。但通过这个程序,我们大概可以知道使用 BCC 开发一个 eBPF 程序的步骤。

2. libbpf-tools

我们在使用bcc测试的过程中可以看到,BPF程序hello.c是在hello.py启动时现编译的,这对生产环境来说并不友好,所以这里我们尝试下使用libbpf对比下。

基于 libbpf C/C++ library 的开发架构:

2.1 安装libbpf-tools

libbpf+ BPF CO-RE不需要将 Clang/LLVM 运行时部署到目标服务器,不过它依赖使用BTF类型信息构建的内核。我们这里采用源码安装

# 安装依赖
sudo apt install -y make gcc libssl-dev bc gcc-multilib libncurses5-dev strace tar build-essential pkg-config libmnl-dev bison flex graphviz
sudo apt install -y clang llvm libelf-dev libcap-dev libbfd-dev bpfcc-tools cargo linux-headers-$(uname -r) 

# 检查内核是否支持btf(文件存在即表示支持)
ls -la /sys/kernel/btf/vmlinux

# 编译bpftool+libbpf
cd ~/project
# git clone --recurse-submodules https://github.com/libbpf/bpftool.git
git clone https://github.com/yanjingang/libbpf-demo
cd ~/project/libbpf-demo
git submodule update --init --recursive
cd bpftool/src/
make -j8
sudo make install

# libbpf+bpftool脚手架示例
cd ~/project/libbpf-demo/examples/c
mkdir -p build && cd build
cmake ..
make -j8
sudo ./bootstrap
    TIME EVENT COMM PID PPID FILENAME/EXIT CODE
    18:45:05 EXIT vim 68519 27493 [0]
    18:45:05 EXEC tracker-store 70267 25245 /usr/libexec/tracker-store
    18:45:08 EXEC ls 70273 27493 /usr/bin/ls
    18:45:08 EXIT ls 70273 27493 [0] (3ms)
2.2 使用libbpf + bpftool编写eBPF程序

编写eBPF程序:

vim hello.bpf.c

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// 使用SEC宏把下方函数插入到openant系统调用入口执行
SEC("kprobe/do_sys_openat2") 
int BPF_KPROBE(do_sys_openat2, int dfd, struct filename *name)
{
	pid_t pid;
	pid = bpf_get_current_pid_tgid() >> 32;
	// 将信息输出到trace_pipe(/sys/kernel/debug/tracing/trace_pipe) bpf_printk("Hello eBPF! kprobe entry pid = %d\n", pid); return 0; } 

编写eBPF loader程序:

vim hello.c

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "hello.skel.h"

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
	return vfprintf(stderr, format, args);
}

static volatile sig_atomic_t stop;

void sig_int(int signo)
{
	stop = 1;
}

int main(int argc, char **argv)
{
	struct hello_bpf *skel;
	int err;

	/* 设置 libbpf 错误和调试信息回调 */
	libbpf_set_print(libbpf_print_fn);

	/* 加载并验证 hello.bpf.c 应用程序 */
	skel = hello_bpf__open_and_load();
	if (!skel) {
		fprintf(stderr, "Failed to open BPF skeleton\n");
		return 1;
	}

	/* 附加 hello.bpf.c 程序到跟踪点 */
	err = hello_bpf__attach(skel);
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton\n");
		goto cleanup;
	}
	if (signal(SIGINT, sig_int) == SIG_ERR) {
		fprintf(stderr, "can't set signal handler: %s\n", strerror(errno));
		goto cleanup;
	}
	printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
	       "to see output of the BPF programs.\n");
	while (!stop) {
		fprintf(stderr, ".");
		sleep(1);
	}

cleanup:
	hello_bpf__destroy(skel);
	return -err;
}

编译运行:

# 编译
cmake .. 
make -j8

# 测试
sudo ./hello

# 查看输出
sudo cat /sys/kernel/debug/tracing/trace_pipe
    gsd-housekeepin-25624 [000] d...1 204926.010969: bpf_trace_printk: Hello eBPF! kprobe entry pid = 25624
    ThreadPoolForeg-26248 [011] d...1 204907.802283: bpf_trace_printk: Hello eBPF! kprobe entry pid = 26222
            thermald-1099 [000] d...1 204907.821950: bpf_trace_printk: Hello eBPF! kprobe entry pid = 877

一个简单的基于kprobe openat监听就实现了。

今天先到这里,下一篇我将带大家一起更深入的了解下附着点的分类,并实现一个具体的内核或用户层函数数据抓取功能。

 

四、其他

1. 常见问题

1.1 bcc: sudo python3 hello.py 报 AttributeError: /lib/x86_64-linux-gnu/libbcc.so.0: undefined symbol: bpf_module_create_b

排查方法:

# 查看找不到符号的so文件
$ ls -lh /lib/x86_64-linux-gnu/ |grep libbcc.so
lrwxrwxrwx  1 root root    11 Nov  2 21:17 libbcc.so -> libbcc.so.0
lrwxrwxrwx  1 root root    16 Nov  2 21:17 libbcc.so.0 -> libbcc.so.0.28.0
-rw-r--r--  1 root root   58M Feb  7  2020 libbcc.so.0.12.0
-rw-r--r--  1 root root  102M Nov  2 21:12 libbcc.so.0.28.0

# 检查符号
$ strings /lib/x86_64-linux-gnu/libbcc.so.0.28.0 |grep bpf_module_create_
bpf_module_create_c
bpf_module_create_c_from_string
bpf_module_create_c.cold
bpf_module_create_c_from_string.cold
bpf_module_create_c
bpf_module_create_c_from_string

# 旧版so里有这个符号(python3 bcc库依赖的so怎么是旧版本?)
$ strings /lib/x86_64-linux-gnu/libbcc.so.0.12.0 |grep bpf_module_create_
bpf_module_create_b
bpf_module_create_c
bpf_module_create_c_from_string

# 确认下python3 bcc库竟然都是20年的(说明python3的bcc库没有被正常安装和更新)
$ ll /usr/lib/python3/dist-packages/bcc/
total 188
-rw-r--r-- 1 root root 11728 Feb 7 2020 libbcc.py
...

解决方法:

# 手动把bcc build产出里的python3库拷贝到python3 dist-packages目录中后解决 
sudo cp -r /home/work/project/bcc/build/src/python/bcc-python3/bcc/* /usr/lib/python3/dist-packages/bcc/

 

yan 23.11.3

 

参考:

Linux BPF Superpowers

Meet cute-between-ebpf-and-tracing

深入理解 Linux eBPF:一个完整阅读清单

一文看懂eBPF

BCC – Tools for BPF-based Linux IO analysis, networking, monitoring, and more

Libbpf: a beginner’s guide

Why We Switched from bcc-tools to libbpf-tools for Linux BPF Performance Analysis

eBPF学习笔记

欢迎关注下方“非著名资深码农“公众号进行交流~

发表评论

邮箱地址不会被公开。