什么是eBPF

从Linux Tracing说起

在我们做程序调试和性能分析时候经常需要跟踪(Tracing)程序执行过程,收集执行信息,综合收集到的信息分析程序问题。基于这样的需求,Linux生态便出现了各种各样的工具,我们在编程开发中或多或少已经接触到了这样的工具,比如GDB、perf,甚至自己代码中的Printf也算工具之一。概括来讲,Tracing工具是实现“采集目标程序执行信息目的”的程序。

那么实现Tracing,需要解决什么问题呢?

  1. 数据源在哪里。
  2. 采集方式。

以GDB为例,我们知道GDB调试,需要开发人员编译加特定参数,编译器会在编译结果程序中加入特定Debug信息。这个加参数暴露Debug信息是一个下探针的过程,探针是Tracing数据的来源。然后我执行GDB命令,单步执行、输出状态信息,这是采集和使用Tracing数据的过程。

先加探针,然后采集调试的过程,在线上程序出性能问题的时候,很难实现,而且很多程序也是到线上场景才暴露性能问题的,能不能随时加探针,随时跟踪目标程序呢?操作系统作为应用程序的执行环境,可以拥有上帝视角,借助内核加探针使我们拥有随时加探针的超能力!

Linux Tracing技术不断发展,诞生了各种各样的工具,形成了一个Tracing系统生态。

在Linux内核中已经有了各种各样的探针,按照形态分为:

  • 硬件探针,硬件直接暴露信息,比如CPU cycles数。
  • 静态探针,重新编译内核或者通过加载内核模块形式下的探针,代表技术: Tracepoint。
  • 动态探针,可以动态在内核函数上添加Hook的探针机制,代表技术Kprobe。

简单深入下静态探针和动态探针,假设我们需要Hook一个内核函数,静态探针需要我们改内核代码,在目标函数前后加入我们采集数据的代码,动态探针则是我们动态指定对应函数和Hook脚本就可以了。相比静态探针需要重新编译内核添加新探针,动态探针灵活几乎可以Hook所有内核函数。

基于内核各种各样的探针,诞生了各种工具、库、框架(Kprobe,Tracepoint,LTTng,SystemTap,eBPF等),这些工具、库底层共用内核的探针技术,用户实现一个Tracing目的,可以选择使用不同的工具、库。

为什么是eBPF

eBPF并不是一个新的Tracing探针技术,而是新的Tracing框架技术,以更安全、更高效、更灵活的方式,方便用户实现Linux Tracing!

采用eBPF可以对接几乎已有的所有探针技术,eBPF的灵活形态,方便用户实现特定化需求。

eBPF工作原理

eBPF程序分为两部分:

  • 内核态代码,被加载进内核执行的代码,加载之前需要被验证器校验,确保不会对内核造成伤害。
  • 用户态代码,和我们正常实现一个应用代码没有区别,在用户进程空间执行,但是额外做一些帮助内核态代码load进内核的工作。

内核态代码会被限制可以调用的函数,通常干采集数据的工作,用户态代码调用函数不限,方便实现业务逻辑,那么内核态代码和用户态代码如何沟通数据呢?eBPF提供了bpf_maps,是用户态和内核态共享的数据空间,通过k-v结构共享存取数据。

内核态代码也不同与我们写正常C语言代码,编译成机器码二进制进入内核执行的,而是内核约定的一套Bytecode指令,eBPF的验证器验证ByteCode通过后,eBPF的JIT编译器将ByteCode编译成机器码进入内核执行。这样一套过程,验证确保安全,JIT确保高效。

我们写eBPF内核态代码不能直接写ByteCode(类似汇编)吧?目前Clang实现了将C语言代码编译成eBPF Bytecode的后端,所以可以使用C编写eBPF内核态代码。用户态代码是在eBPF程序用户进程执行,所以可以便随意选择编程语言。

例子:

内核态代码
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>


struct key_t {
u32 prev_pid;
u32 curr_pid;
};

// 创建与用户态代码共享数据的eBPF maps
BPF_HASH(stats, struct key_t, u64, 1024);
int count_sched(struct pt_regs *ctx, struct task_struct *prev) {
struct key_t key = {};
u64 zero = 0, *val;


key.curr_pid = bpf_get_current_pid_tgid();
key.prev_pid = prev->pid;


// could also use `stats.increment(key);`
val = stats.lookup_or_try_init(&key, &zero);
if (val) {
(*val)++;
}
return 0;
}
用户态代码

#!/usr/bin/python
# Copyright (c) PLUMgrid, Inc.
# Licensed under the Apache License, Version 2.0 (the "License")

from bcc import BPF
from time import sleep

# 帮助load 内核态代码
b = BPF(src_file="task_switch.c")
# 指定内核态代码Hook点,指定探针
b.attach_kprobe(event_re="^finish_task_switch$|^finish_task_switch\.isra\.\d$",fn_name="count_sched")

# generate many schedule events
for i in range(0, 100): sleep(0.01)

for k, v in b["stats"].items():
print("task_switch[%5d->%5d]=%u" % (k.prev_pid, k.curr_pid, v.value))

相关链接