文章介绍几种常用的内核动态追踪技术,对 ftrace、perf 及 eBPF 的使用方法进行案例说明。
1.什么是动态追踪
动态追踪技术,是通过探针机制来采集内核或者应用程序的运行信息,从而可以不用修改内核或应用程序的代码,就获得调试信息,对问题进行分析、定位。
通常在排查和调试异常问题时,我们首先想到的是使用 GDB 在程序运行路径上设置断点,然后结合命令进行分析定位; 或者,在程序源码中增加一系列的日志,从日志输出中寻找线索。 不过,断点往往会中断程序的正常运行; 而增加新的日志,往往需要重新编译和部署。 在面对偶现问题以及对时延要求严格的场景下,GDB 和增加日志的方式就不能满足需求了。
动态追踪为这些问题提供了完美的方案:它既不需要停止服务,也不需要修改程序的代码; 程序还按照原来的方式正常运行时,就可以分析出问题的根源。 同时,相比以往的进程级跟踪方法(比如 ptrace),动态追踪往往只会带来很小的性能损耗。
2.动态追踪技术分类
动态追踪的工具很多,systemtap、perf、ftrace、sysdig、eBPF 等。 动态追踪的事件源根据事件类型不同,主要分为三类:静态探针, 动态探针以及硬件事件。
3动态追踪之ftrace
ftrace 最早用于函数跟踪,后来扩展支持了各种事件跟踪功能。 ftrace 通过 debugfs 以普通文件的形式,向用户空间提供访问接口,这样不需要额外的工具,就可以通过挂载点(通常为 /sys/kernel/debug/tracing 目录)的文件读写,来跟 ftrace 交互,跟踪内核或者应用程序的运行事件。
在使用 ftrace 之前,首先要确定当前系统是否已经挂载了 debugfs,可以使用如下方式进行确认。
#方式一:查看挂载信息
mount | grep debugfs
#方式二:查看挂载点
ls /sys/kernel/debug/tracing
如果在上面的查找结果中有输出,说明当前系统已经挂载了 debugfs,如果系统未挂载 debugfs,则使用如下的命令进行挂载。
mount -t debugfs nodev /sys/kernel/debug
在 /sys/kernel/debug/tracing 目录下提供了各种跟踪器(tracer)和事件(event),一些常用的选项如下。
ftrace 提供了多个跟踪器,用于跟踪不同类型的信息,比如函数调用、中断关闭、进程调度等。 具体支持的跟踪器取决于系统配置,使用如下的命令来查询当前系统支持的跟踪器。
root@ubuntu:/sys/kernel/debug/tracing# cat available_tracers
hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop
以上是系统支持的所有跟踪器,function 表示跟踪函数的执行,function_graph 则是跟踪函数的调用关系,也就是生成直观的调用关系图,这是最常用的两种跟踪器。 也可以通过配置内核,使系统支持更多类型的跟踪器。
在使用跟踪器前,需要确定好跟踪目标,包括内核函数和事件,函数是指内核中的函数名,事件是指内核中预先定义的跟踪点。 可以使用如下方式查看内核支持跟踪的函数和事件。
#查看内核支持追踪的函数
cat available_filter_functions
#查看内核支持追踪的事件
cat available_events
接下来以一个简单的示例说明 ftrace 的基本用法,比如我们需要跟踪 open 的系统调用,open 在内核中的实现接口为 do_sys_open,所以需要将跟踪的函数设置为 do_sys_open,以下是跟踪过程的步骤。
#设置函数跟踪点为 do_sys_open
echo do_sys_open > set_graph_function
#设置当前跟踪器为 function_graph
echo function_graph > current_tracer
#配置 trace 属性:显示当前的进程
echo funcgraph-proc > trace_options
#清除 trace 缓存
echo > trace
#开启跟踪
echo 1 > tracing_on
#产生 do_sys_open 调用
ls
#关闭跟踪
echo 0 > tracing_on
在关闭跟踪后,使用 cat 命令查看跟踪结果,如下所示,在 trace 文件中保存了跟踪到的信息,第一列表示接口执行的 CPU,第二列表示任务名称和进程 PID,第三列是函数执行延迟,最后一列是函数调用关系图。
root@ubuntu:/sys/kernel/debug/tracing# cat trace
# tracer: function_graph
#
# CPU TASK/PID DURATION FUNCTION CALLS
# | | | | | | | | |
2) ls-46073 | | do_sys_open() {
2) ls-46073 | | getname() {
2) ls-46073 | | getname_flags() {
2) ls-46073 | | kmem_cache_alloc() {
2) ls-46073 | | _cond_resched() {
2) ls-46073 | 0.119 us | rcu_all_qs();
2) ls-46073 | 0.416 us | }
2) ls-46073 | 0.095 us | should_failslab();
2) ls-46073 | 0.112 us | memcg_kmem_put_cache();
2) ls-46073 | 1.041 us | }
2) ls-46073 | | __check_object_size() {
2) ls-46073 | 0.097 us | check_stack_object();
2) ls-46073 | 0.100 us | __virt_addr_valid();
2) ls-46073 | 0.097 us | __check_heap_object();
2) ls-46073 | 0.662 us | }
2) ls-46073 | 2.109 us | }
2) ls-46073 | 2.414 us | }
ftrace 的输出通过不同级别的缩进,直观展示了各函数间的调用关系。 但是 ftrace 的使用需要好几个步骤,用起来并不方便,不过,trace-cmd 已经把这些步骤给包装了起来,这样,就可以通过一行命令,完成上述所有过程。 trace-cmd 的安装方式如下。
# Ubuntu
apt install trace-cmd
# CentOS
yum install trace-cmd
trace-cmd安装好之后,可以通过执行如下的命令输出类似的结果,值得注意的是 trace-cmd 的执行不能在 /sys/kernel/debug/tracing 路径,否则执行出出错,提示信息为“trace-cmd: Permission denied”。
trace-cmd record -p function_graph -g do_sys_open -O funcgraph-proc ls
trace-cmd report
ftrace 的追踪功能不止于此,它不仅能追踪到接口的调用关系,还能抓取接口调用的时间戳,用于性能分析; 还可根据需要追踪的接口进行模糊过滤,众多的功能不在这里详细介绍,如果项目中需要用到再进行具体了解和总结。
4.动态追踪之perf
perf 的功能强大,可以统计分析出应用程序或者内核中的热点函数,从而用于程序性能分析; 也可以用来分析 CPU cache、CPU 迁移、分支预测、指令周期等各种硬件事件; 还可以对感兴趣的事件进行动态追踪。 下面以 do_sys_open 为例,设置目标函数追踪。 执行如下命令,可以查询所有支持的事件。
perf list
在 perf 的各个子命令中添加 --event 选项,设置追踪感兴趣的事件。 如果这些预定义的事件不满足实际需要,可以使用 perf probe 来动态添加。 而且,除了追踪内核事件外,perf 还可以用来跟踪用户空间的函数。 执行如下代码添加 do_sys_open 探针。
#执行指令
perf probe --add do_sys_open
#输出
Added new event:
probe:do_sys_open (on do_sys_open)
You can now use it in all perf tools, such as:
perf record -e probe:do_sys_open -aR sleep 1
探针添加成功后,就可以在所有的 perf 子命令中使用。 比如,上述输出就是一个 perf record 的示例,执行它就可以对 1s 内的 do_sys_open 进行采样,如下所示。
#执行命令
perf record -e probe:do_sys_open -aR sleep 1
#输出
[ perf record: Woken up 1 times to write data ][ perf record: Captured and wrote 0.810 MB perf.data (18 samples) ]
执行如下命令显示采样结果,输出结果中列出了调用 do_sys_open 的任务名称、进程 PID 以及运行的 CPU 等信息。
#执行命令
perf script
#输出
perf 3676 [003] 7619.618568: probe:do_sys_open: (ffffffffa92e78e0)
sleep 3677 [000] 7619.621118: probe:do_sys_open: (ffffffffa92e78e0)
sleep 3677 [000] 7619.621130: probe:do_sys_open: (ffffffffa92e78e0)
vminfo 1403 [001] 7619.864117: probe:do_sys_open: (ffffffffa92e78e0)
dbus-daemon 749 [001] 7619.864222: probe:do_sys_open: (ffffffffa92e78e0)
dbus-daemon 749 [001] 7619.864310: probe:do_sys_open: (ffffffffa92e78e0)
irqbalance 743 [000] 7620.013548: probe:do_sys_open: (ffffffffa92e78e0)
irqbalance 743 [000] 7620.013687: probe:do_sys_open: (ffffffffa92e78e0)
在使用结束后,使用如下命令删除探针。
#删除 do_sys_open 探针
perf probe --del probe:do_sys_open
5.动态追踪之eBPF
eBPF 相对于 ftrace 和 perf 更加灵活,它可以通过 C 语言自由扩展,这些扩展通过 LLVM (Low Level Virtual Machine) 转换为 BPF 字节码后,加载到内核中执行。
5.1 搭建 eBPF 开发环境
虽然 Linux 内核很早就已经支持了 eBPF,但很多新特性都是在 4.x 版本中逐步增加的。 所以,想要稳定运行 eBPF 程序,内核至少需要 4.9 或者更新的版本。 而在开发和学习 eBPF 时,为了体验和掌握最新的 eBPF 特性,推荐使用更新的 5.x 内核,接下来的案例是基于 Ubuntu20.04 系统,内核版本为 5.15.0-56-generic,eBPF 开发和运行需要相关的开发工具如下。
可运行如下命令进行相关工具的安装。
apt install -y make clang llvm libelf-dev libbpf-dev bpfcc-tools libbpfcc-dev linux-tools-$(uname -r) linux-headers-$(uname -r)
5.2 开发 eBPF 程序的步骤
一般来说, eBPF 程序的开发分为如下 5 个步骤。
eBPF 程序执行过程
以上的每一步,都可以自己动手去完成。 但为了方便,推荐从 BCC(BPF Compiler Collection)开始学起。 BCC 是一个 BPF 编译器集合,包含了用于构建 BPF 程序的编程框架和库,并提供了大量可以直接使用的工具。 使用 BCC 的好处是,它把上述的 eBPF 执行过程通过内置框架抽象了起来,并提供了 Python、C++ 等编程语言接口。 这样,就可以直接通过 Python 语言去跟 eBPF 的各种事件和数据进行交互。
5.3 借助 BCC 开发 eBPF 程序案例
接下来,就以跟踪 openat()(即打开文件)这个系统调用为例,说明如何开发并运行第一个 eBPF 程序。 使用 BCC 开发 eBPF 程序,可以把上面的五步简化为下面的三步。
5.3.1 使用 C 开发一个 eBPF 程序
新建一个 trace_open.c 文件,输入如下内容。
/* 包含头文件 */
#include
#include
/* 定义数据结构 */
struct data_t {
u32 pid;
u64 ts;
char comm[TASK_COMM_LEN];
char fname[NAME_MAX];
};
/* 定义性能事件映射 */
BPF_PERF_OUTPUT(events);
/* 定义 kprobe 处理函数 */
int trace_open(struct pt_regs *ctx, int dfd, const char __user *filename, struct open_how *how)
{
struct data_t data = {};
/* 获取 PID 和时间 */
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
/* 获取进程名 */
if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0)
{
bpf_probe_read(&data.fname, sizeof(data.fname), (void *)filename);
}
/* 提交性能事件 */
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
BPF 程序可以利用 BPF 映射(map)进行数据存储,而用户程序也需要通过 BPF 映射,同运行在内核中的 BPF 程序进行交互。 用户层为了获取内核打开文件名称时,就要引入 BPF 映射。 为了简化 BPF 映射的交互,BCC 定义了一系列的库函数和辅助宏定义。
如下是对上述代码的说明。
5.3.2 使用 Python 开发用户态程序
创建一个 trace_open.py 文件,并输入下面的内容。
#!/usr/bin/env python3
# 1) import bcc library
from bcc import BPF
# 2) load BPF program
b = BPF(src_file="trace_open.c")
# 3) attach kprobe
b.attach_kprobe(event="do_sys_openat2", fn_name="trace_open")
# 4) print header
print("%-18s %-16s %-6s %-16s" % ("TIME(s)", "COMM", "PID", "FILE"))
# 5) define the callback for perf event
start = 0
def print_event(cpu, data, size):
global start
event = b["events"].event(data)
if start == 0:
start = event.ts
time_s = (float(event.ts - start)) / 1000000000
print("%-18.9f %-16s %-6d %-16s" % (time_s, event.comm, event.pid, event.fname))
# 6) loop with callback to print_event
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
如下是对上述代码的说明。
5.3.3 执行 eBPF 程序
用户态程序开发完成之后,最后一步就是执行它了。 需要注意的是,eBPF 程序需要以 root 用户来运行,非 root 用户需要加上 sudo 来执行,输入如下命令执行程序。
python3 trace_open.py
命令执行后,可以在另一个终端中输入 cat 或 ls 命令(执行文件打开命令),然后回到运行 trace_open.py 脚本的终端,结果如下。
TIME(s) COMM PID FILE
0.000000000 b'ls' 5171 b'/etc/ld.so.cache'
0.000021937 b'ls' 5171 b'/lib/x86_64-linux-gnu/libselinux.so.1'
......
2.088971702 b'gsd-housekeepin' 1803 b'/etc/fstab'
2.089056747 b'gsd-housekeepin' 1803 b'/proc/self/mountinfo'
......
2.741662512 b'cat' 5172 b'/etc/ld.so.cache'
2.741681539 b'cat' 5172 b'/lib/x86_64-linux-gnu/libc.so.6'
......
5.4 eBPF 开发方式简介
除了 BCC 之外,eBPF 还有可以使用其他的方式进行辅助开发,比如 bpftrace 和 libbpf,每种方法都有自己的优点和适用范围,以下是这三种方式的对比。
在实际应用中,可以根据内核版本、内核配置、eBPF 程序复杂度,以及是否允许安装内核头文件和 LLVM 编译工具等,来选择最合适的方案。
全部0条评论
快来发表一下你的评论吧 !