前两天仿照 strings
工具写了个打印进程运行时堆栈可打印字符的工具 ffstrings 。结果没几天就被告知在 CentOS 上跑不通(uid: root, errno: EPERM):<
在反复调试无果之后,希望得到一种可以跟踪内核执行流的工具。功夫不负有心人,在内核文档中找到了—— ftrace
。
Ftrace 是位于内核内部的跟踪器,可以用来调试和分析发生在用户空间(内核空间)之外的延迟和性能问题。
虽然 Ftrace 是一款函数跟踪器,但也支持基于其它目的的跟踪,例如:跟踪上下文切换、跟踪高优先级任务的执行时间等等。另外还可以通过插件的方式自定义更多的跟踪。
当然,目前仅仅是做简单的介绍。毕竟最初的目的是为了确认 ffstrings
出现 EPERM
的原因。
启用 Ftrace
Ftrace
自版本 2.6.28 开始加入内核。首先要做的就是确保内核已经集成了 Ftrace
。当然,目前的内核版本都已经到 5.X 了,而且云服务器的普及,相信已经没有太多还在使用7、8年前 Linux 发行版的机器了吧 :)
Ftrace
使用 debugfs 文件系统以文件的形式来维护控制项和跟踪器输出。一般来说,这个文件系统将挂载在 /sys/kernel/debug
目录下。如果没有找到,那就主动先挂载上 debugfs 文件系统:
mount -t debugfs nodev /sys/kernel/debug
挂载文件系统之后,可以检查下 /sys/kernel/debug/tracing
目录,这里就是 Ftrace
全部的可配置项和输出。
几个重要的文件包括:
current_tracer
, 配置当前跟踪器进行哪些项目的跟踪available_tracers
, 当前内核支持的所有跟踪项目tracing_on
, 开启/关闭跟踪器 (0 表示关闭, 1 表示开启)trace
, 用户可读的跟踪结果文件trace_pipe
, 与trace
文件静态呈现的形式相对,里面的数据只能消费一次trace_options
, 配置输出文件的格式tracing_max_latency
, 记录最大延时buffer_size_kb
, 用于设置单个CPU使用的跟踪器缓存大小。跟踪器缓存使用环形缓存,老的缓存会在空间不足时被新缓存覆盖tracing_cpumask
, 用来配置需要监控的CPUset_ftrace_filter
, 显示指定跟踪特定的函数set_ftrace_notrace
, 与set_ftrace_filter
功能恰好相反set_ftrace_pid
, 只跟踪特定的任务set_graph_function
, 设置要清晰显示调用关系的函数,以 C 语言缩进式地呈现available_filter_functions
, 显示目前可以跟踪的所有内核函数
使用 Ftrace
配置 current_tracer
,跟踪内核所有函数的调用
$ echo function > current_tracer
配置 tracing_enabled
,开启跟踪器
$ echo 1 > tracing_on
查看 trace
文件中记录的跟踪器输出
$ head -15 trace
# tracer: function
#
# _-----=> irqs-off
# / _----=> need-resched
# | / _---=> hardirq/softirq
# || / _--=> preempt-depth
# ||| / delay
# TASK-PID CPU# |||| TIMESTAMP FUNCTION
# | | | |||| | |
zsh-23126 [000] d... 5252243.233084: do_syscall_64 <-entry_SYSCALL_64_after_hwframe
zsh-23126 [000] .... 5252243.233084: SyS_rt_sigprocmask <-do_syscall_64
zsh-23126 [000] .... 5252243.233084: sigprocmask <-SyS_rt_sigprocmask
zsh-23126 [000] .... 5252243.233084: __set_current_blocked <-sigprocmask
zsh-23126 [000] .... 5252243.233084: _raw_spin_lock_irq <-__set_current_blocked
zsh-23126 [000] d... 5252243.233085: __set_task_blocked <-__set_current_blocked
OK,如果觉得这个结果并不直观,可以重新配置 current_tracer
,使用 function_graph
$ echo function_graph > current_tracer
$ head -15 trace
# tracer: function_graph
#
# CPU DURATION FUNCTION CALLS
# | | | | | | |
0) | __fdget_pos() {
0) 0.072 us | __fget_light();
0) 0.377 us | }
0) | vfs_read() {
0) | rw_verify_area() {
0) | security_file_permission() {
0) | apparmor_file_permission() {
0) | common_file_perm() {
0) 0.079 us | aa_file_perm();
0) 0.480 us | }
0) 0.877 us | }
跟踪应用程序的系统调用
虽然 strace
就可以打印出任务的每个系统调用,但是,它所能完成的仅仅只是对每个系统调用及结果进行记录,而无法看到系统调用内部究竟发生了什么。而 ftrace 可以很好的完成这一点。当然,这需要一点点编码量。
回到最初的命题,执行 ffstrings
过程中,抛出了 EPERM
错误码。首先使用 strace
进行定位,究竟是哪个系统调用出了问题
$ strace ./ffstrings <PID>
# ... omitted output
read(4, 0x7f9ac2970000, 1024) = -1 EPERM (Operation not permitted)
# ... omitted output
修改应用程序代码,添加两个新的函数 (trace_on
, trace_off
)。
Hint: 这段代码的健壮性并不好,请各位多担待
int trace_fd = -1;
int marker_fd = -1;
char *debugfs = "/sys/kernel/debug";
void trace_on()
{
char path[256];
strcpy(path, debugfs); /* BEWARE buffer overflow */
strcat(path,"/tracing/tracing_on");
trace_fd = open(path, O_WRONLY);
if (trace_fd >= 0)
write(trace_fd, "1", 1);
strcpy(path, debugfs);
strcat(path,"/tracing/trace_marker");
marker_fd = open(path, O_WRONLY);
if (marker_fd >= 0)
write(marker_fd, "In critical area\n", 17);
}
void trace_off()
{
if (marker_fd >= 0)
write(marker_fd, "Out critical area\n", 17);
write(trace_fd, "0", 1);
close(trace_fd);
close(marker_fd);
trace_fd = -1;
marker_fd = -1;
}
在问题代码前后执行这两个新的函数
trace_on();
char c = getc(mem);
trace_off();
再去从 trace
文件中取一下执行结果(局部结果)
到此为止,就可以简单的实现对内核函数的跟踪。
不过,这仅仅只能看到内核函数调用,具体的调用关系,还是得拿到相应版本的内核代码仔细研读研读。毕竟,内核代码的改动也是相当频繁的,不同版本调用关系会出现很大的变化。
__ __
/ _| __ _ _ __ __ _ / _| ___ _ __ __ _
| |_ / _` | '_ \ / _` | |_ / _ \ '_ \ / _` |
| _| (_| | | | | (_| | _| __/ | | | (_| |
|_| \__,_|_| |_|\__, |_| \___|_| |_|\__, |
|___/ |___/