目录
摘要
1 什么是 eBPF
2 eBPF 支持的功能
3 BCC
4 编写脚本
5 总结
6 附
摘要
ftrace 和 perf 与 ebpf 同为 linux 内核提供的动态追踪工具,其中 ftrace 侧重于事件跟踪和内核行为的实时分析,perf 更侧重于性能分析和事件统计,与二者相比,ebpf 则更为强大,拥有更高的灵活性。
1 什么是 eBPF
eBPF 可以理解为是 kernel 中实现的一个虚拟机机制,其拥有自定义的 64 位 RISC 指令集,可以再 linux 内核中运行即时编译的 BPF 程序,支持访问内核功能和内存的部分子集。具体原理为它会将类 C 代码编译为 BPF 字节码,类似 Java 字节码。然后将对应的程序代码挂到内核的钩子上,当钩子函数被调用时,内核将在 eBPF 虚拟机这个沙盒中执行字节码。
在内核内运行一个完整的虚拟机主要是考虑便利和安全。虽然 eBPF 程序所做的操作都可以通过正常的内核模块来处理,但直接的内核编程是一件非常危险的事情 - 这可能会导致系统锁定、内存损坏和进程崩溃,从而导致安全漏洞和其他意外的效果,特别是在生产设备上(eBPF 经常被用来检查生产中的系统),所以通过一个安全的虚拟机运行本地 JIT 编译的快速内核代码对于安全监控和沙盒、网络过滤、程序跟踪、性能分析和调试都是非常有价值的。部分简单的样例可以在这篇优秀的 Linux Extended BPF (eBPF) Tracing Tools 中找到。
基于设计,eBPF 虚拟机和其程序有意地设计为不是图灵完备的:即不允许有循环(正在进行的工作是支持有界循环【译者注:已经支持有界循环,#pragma unroll 指令】),所以每个 eBPF 程序都需要保证完成而不会被挂起、所有的内存访问都是有界和类型检查的(包括寄存器,一个 MOV 指令可以改变一个寄存器的类型)、不能包含空解引用、一个程序必须最多拥有 BPF_MAXINSNS 指令(默认 4096)、“主"函数需要一个参数(context)等等。当 eBPF 程序被加载到内核中,其指令被验证模块解析为有向环状图,上述的限制使得正确性可以得到简单而快速的验证。
2 eBPF 支持的功能
eBPF 常用来编写网络程序,将该网络程序附加到网络socket,进行流量过滤,流量分类以及执行网络分类器的动作。eBPF程序甚至可以修改一个已建链的网络socket的配置。例如 XDP 会在网络栈的底层运行 eBPF 程序,高性能地进行处理接收到的报文。另外就是用来调试了。
下面这张图展示了 eBPF 的工作原理,大致可以分为 4 步:
- 编写 eBPF 代码,并生成字节码
- 将 eBPF 代码加载到内核的 eBPF 框架(注册 eBPF 程序到 kernel 钩子上)
- 事件发生,开始执行 eBPF 程序,记录数据到 maps 中
- 读取 maps 数据
由于 eBPF 偏底层,平常如果是调式使用直接用底层 kernel 的接口写是比较麻烦的,幸运的是 BCC 工具集提供了好用的封装。
3 BCC
在 eBPF 执行过程中,对于编译、加载还有 maps 等操作,对所有的跟踪程序来说都是通用的。把这些过程通过 Python 抽象起来,也就诞生了 BCC(BPF Compiler Collection)。
BCC 把 eBPF 中的各种事件源(比如 kprobe、uprobe、tracepoint 等)和数据操作(称为 Maps),也都转换成了 Python 接口(也支持 lua)。这样,使用 BCC 进行动态追踪时,编写简单的脚本就可以了。因为需要跟内核中的数据结构交互,所以真正核心的事件处理逻辑,还是需要用 C 语言来编写。
安装 BCC 后,在 tools 目录下已经提供了一系列的工具,并在 tools/doc 目录下提供了各工具对应的使用说明了:
[root@centos ~]# yum install bcc-tools
[root@centos ~]# ls /usr/share/bcc/tools/
argdist capable drsnoop hardirqs memleak perlcalls pythonstat slabratetop tclobjnew tplist
bashreadline cobjnew execsnoop javacalls mountsnoop perlflow reset-trace sofdsnoop tclstat trace
...
[root@centos ~]# ls /usr/share/bcc/tools/doc/
argdist_example.txt execsnoop_example.txt mountsnoop_example.txt reset-trace_example.txt tclstat_example.txt
bashreadline_example.txt ext4dist_example.txt mysqld_qslower_example.txt rubycalls_example.txt tcpaccept_example.txt
biolatency_example.txt ext4slower_example.txt nfsdist_example.txt rubyflow_example.txt tcpconnect_example.txt
biosnoop_example.txt filelife_example.txt nfsslower_example.txt rubygc_example.txt tcpconnlat_example.txt
...
例如直接用 BCC 提供的 trace 工具跟踪 do_sys_open 函数调用:
[root@centos ~]# /usr/share/bcc/tools/trace 'do_sys_open "%s", arg2' -T
TIME PID TID COMM FUNC -
14:30:06 2899 2954 YDService do_sys_open /proc/sys/kernel/random/uuid
14:30:06 32584 32584 sh do_sys_open ../lib/tls/x86_64/libtinfo.so.5
14:30:06 32584 32584 sh do_sys_open ../lib/tls/libtinfo.so.5
14:30:06 32584 32584 sh do_sys_open ../lib/x86_64/libtinfo.so.5
14:30:06 32584 32584 sh do_sys_open ../lib/libtinfo.so.5
4 编写脚本
有时候提供的工具不能很好的满足我们的需求,可能需要手动开发一个脚本来方便调试。假设还是跟踪 do_sys_open,它长这样:
// include/linux/fs.h
extern long do_sys_open(int dfd, const char __user *filename, int flags,
umode_t mode);
编写这样一个脚本同之前讲的 eBPF 原理一一对应,大体分为四步:
1. 定义事件处理函数
#!/bin/python
from bcc import BPF
# 1.define BPF program (""" is used for multi-line string).
prog = """
#include <uapi/linux/ptrace.h>
#include <uapi/linux/limits.h>
#include <linux/sched.h>
// define output data structure in C
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
int dfd;
char fname[NAME_MAX];
int flags;
unsigned short mode;
};
BPF_PERF_OUTPUT(events);
// define the handler for do_sys_open.
// ctx is required, while other params depends on traced function.
int user_cb(struct pt_regs *ctx, int dfd, const char __user *filename, int flags, umode_t mode){
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0) {
data.dfd = dfd;
bpf_probe_read(&data.fname, sizeof(data.fname), (void *)filename);
data.flags = flags;
data.mode = mode;
}
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
"""
这个函数以受限 C 语言来编写,作用是定义事件处理的逻辑。这部分代码需要以字符串的形式送到 eBPF 模块中处理。
2. 将 eBPF 加载到内核中
# 2.load BPF program
bpf = BPF(text=prog)
# attach the kprobe for do_sys_open, and set handler to user_cb
bpf.attach_kprobe(event="do_sys_open", fn_name="user_cb")
3. 读取 maps 数据
# 3.process event
start = 0
def print_event(cpu, data, size):
global start
# event's type is data_t
event = bpf["events"].event(data)
if start == 0:
start = event.ts
time_s = (float(event.ts - start)) / 1000000000
print("%-18.9f %-16s %-6d %-8s %-8s %-8s %-16s" % (time_s, event.comm, event.pid, hex(event.dfd), hex(event.flags), hex(event.mode), event.fname))
# loop with callback to print_event
bpf["events"].open_perf_buffer(print_event)
print("%-18s %-16s %-6s %-8s %-8s %-8s %-16s" % ("TIME(s)", "COMM", "PID", "DFD", "FLAGS", "MODE", "FILE"))
4. 等待内核事件的发生
# 4 start the event polling loop
while 1:
try:
bpf.perf_buffer_poll()
except KeyboardInterrupt:
exit()
将上面的几个步骤完整的串起来执行,不出意外的话就可以得到执行结果了:
[root@centos ~]# ./ebpf_test.py
TIME(s) COMM PID DFD FLAGS MODE FILE
0.000000000 barad_agent 6364 -0x64 0x8000 0x1b6 /proc/meminfo
0.000116077 barad_agent 6364 -0x64 0x8000 0x1b6 /proc/meminfo
0.000261420 barad_agent 6364 -0x64 0x8000 0x1b6 /proc/vmstat
0.000464681 ebpf_test.py 6363 -0x64 0x8000 0x1b6 /usr/lib64/python2.7/encodings/ascii.so
0.000476553 ebpf_test.py 6363 -0x64 0x8000 0x1b6 /usr/lib64/python2.7/encodings/asciimodule.so
0.000482585 ebpf_test.py 6363 -0x64 0x8000 0x1b6 /usr/lib64/python2.7/encodings/ascii.py
0.000492303 ebpf_test.py 6363 -0x64 0x8000 0x1b6 /usr/lib64/python2.7/encodings/ascii.pyc
从输出中可以看到 ebpb_test.py 自身为了打印也是打开了一堆的 so 库~~通
5 总结
通过对 eBPF 相关资料的查询,了解了它的概念及基本原理,下一阶段的学习路径就是要熟练使用 BCC 提供的工具集,以及必要时刻能够改造 BCC 提供的工具或者自己手写一个合适的工具以方便排查问题。
6 附
贴上完整的源码:Linux/ebpf_test.py at master · Fireplusplus/Linux · GitHub