文章目录
ftrace 原理分析
1 简介
ftrace 是一个内核内部跟踪程序,用于帮助系统开发者和设计者观察内核内部正在发生的事情。它可以用于调试和分析用户空间之外的延迟和性能问题。
ftrace 的本意是指function tracer,这是因为它最开始主要是为了跟踪与采集内核运行时,函数的调用与执行情况。经过不断发展,ftrace逐渐提供了各类跟踪功能。例如,查看进程上下文切换的相关信息、系统中断被禁用的时间以及高优先级进程从被唤醒到被系统调度执行期间的最大延迟时间等等。这些功能可以很好的辅助内核开发和研究人员对内核进行调试, 并及时发现内核中的各类问题。另外,ftrace也具有很好的拓展性, 它提供了一些简洁易用的接口来允许开发者以插件的形式添加和使用更多种类的跟踪器(tracer) , 正因如此, 其逐渐发展成为了一个内核跟踪框架。一些常见的ftrace跟踪器如下:
| 跟踪器 | 相关功能描述 |
|---|---|
| function | 函数调用跟踪程序来跟踪所有内核函数。 |
| function_graph | 与函数跟踪程序相似,不同之处在于函数跟踪程序在函数的入口探测函数,而函数图跟踪程序在函数的入口和出口都进行跟踪。然后,它提供了绘制函数调用图的能力,类似于C源代码。 |
| blk | 阻塞式输入输出跟踪器, 跟踪记录Block I/O 相关信息 |
| irqsoff | 跟踪禁用中断的区域,并以最长的最大延迟保存跟踪。看tracing_max_latency文件。当记录新的max时,它将替换旧的跟踪值。最好在启用"latency-format"选项的情况下查看此跟踪,这在选择跟踪程序时自动设置。 |
| preemptoff | 类似于irqsoff,但是跟踪和记录抢占被禁用的时间量 |
| preemptirqsoff | 类似于irqsoff和preemptoff,但是跟踪和记录irqs和/或抢占被禁用的最大时间。 |
| wakeup | 跟踪和记录在最高优先级任务被唤醒后实际调度它所需要的最大延迟。按照一般开发人员的预期跟踪所有任务。 |
| wakeup_rt | 跟踪和记录RT任务所需要的最大延迟。这对于那些对RT任务的唤醒时间感兴趣的人很有用。 |
| wakeup_dl | 跟踪和记录唤醒SCHED_DEADLINE任务所需的最大延迟(与“wakeup”和“wakeup_rt”一样)。 |
| mmiotrace | 一种用于跟踪二进制模块的特殊跟踪程序。它将跟踪一个模块对硬件的所有调用。它也从I/O中写入和读取所有内容。 |
| branch | 这个跟踪程序可以在跟踪内核中可likely/unlikley的调用时配置。它将跟踪何时命中可能和不可能的分支,以及它的预测是否正确。 |
同样的,Ftrace还有个最常见的用途是 event 跟踪。内核有数百个静态事件点,可以通过tracefs启用这些事件点,以便查看内核的某些部分正在发生什么。
由于 ftrace 是一个大的框架,这里基于 function trace 进行分析。
2 ftrace 的编译器支持
2.1 HAVE_FUNCTION_TRACER 选项对 ftrace 的支持
首先对应 arch 要使用 ftrace 需要满足下面两个基础特性:
- STACKTRACE_SUPPORT - 实现
save_stack_trace() - TRACE_IRQFLAGS_SUPPORT - 实现
include/asm/irqflags.h
当 arch 支持 ftrace 时,需要在 Kconfig 中选中 HAVE_FUNCTION_TRACER 选项,该选项将会激活 ftrace 功能。
该功能将会在编译阶段为代码添加 -pg选项,该选项将会为每个函数开头生成mcount/__mcount函数,这对于用户空间不是难事,libc 库定义了 mcount函数来完成相关工作。而对于内核则需要架构代码去实现mcount和ftrace_stub函数。不同架构可能生成的函数名不是 mcount而是_mcount/__mcount,这取决于具体架构。
如下:
// x86,有 -pg
// echo 'main(){}' | gcc -x c -S -o - - -pg
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
1: call *mcount@GOTPCREL(%rip) // 调用 mcount
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
...
// x86,没有 -pg
// echo 'main(){}' | gcc -x c -S -o - -
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6 // 没有 mcount 相关函数生成调用
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
...
// aarch64,有 -pg
// echo 'main(){}' | aarch64-linux-gnu-gcc -x c -S -o - - -pg
.text
.global _mcount
.align 2
.global main
.type main, %function
main:
.LFB0:
.cfi_startproc
stp x29, x30, [sp, -16]!
.cfi_def_cfa_offset 16
.cfi_offset 29, -16
.cfi_offset 30, -8
mov x29, sp
mov x0, x30
mov x30, x0
hint 7 // xpaclri
mov x0, x30
bl _mcount // 生成了 _mcount
mov w0, 0
ldp x29, x30, [sp], 16
.cfi_restore 30
.cfi_restore 29
.cfi_def_cfa_offset 0
ret
.cfi_endproc
.LFE0:
ftrace 对于 mcount 的内部调用进行了,以便满足 ftrace 的要求。下面描述的是一个正确的mcount 应该做的事:
mcount应该检查函数指针ftrace_trace_function,看看它是否被设置为 ftrace_stub,如果是,那么无事可做,直接返回,如果不是,则进行调用ftrace_trace_function,第一个参数是 frompc,第二个参数时 selfpc。例如:foo() 调用 bar(),当 bar() 调用 mcount() 时:
frompc- 地址bar()将被用来返回到foo()selfpc- 地址bar()(mcount()大小校正)
// 理论流程
void ftrace_stub(void)
{
return;
}
void mcount(void)
{
/* save any bare state needed in order to do initial checking */
extern void (*ftrace_trace_function)(unsigned long, unsigned long);
if (ftrace_trace_function != ftrace_stub)
goto do_trace;
/* restore any bare state */
return;
do_trace:
/* save all state needed by the ABI (see paragraph above) */
unsigned long frompc = ...;
unsigned long selfpc = <return address> - MCOUNT_INSN_SIZE;
ftrace_trace_function(frompc, selfpc);
/* restore all state needed by the ABI */
}
// arm64 实现
ENTRY(_mcount)
mcount_enter
ldr_l x2, ftrace_trace_function
adr x0, ftrace_stub
cmp x0, x2 // if (ftrace_trace_function
b.eq skip_ftrace_call // != ftrace_stub) {
mcount_get_pc x0 // function's pc
mcount_get_lr x1 // function's lr (= parent's pc)
blr x2 // (*ftrace_trace_function)(pc, lr);
skip_ftrace_call: // }
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
ldr_l x2, ftrace_graph_return
cmp x0, x2 // if ((ftrace_graph_return
b.ne ftrace_graph_caller // != ftrace_stub)
ldr_l x2, ftrace_graph_entry // || (ftrace_graph_entry
adr_l x0, ftrace_graph_entry_stub // != ftrace_graph_entry_stub))
cmp x0, x2
b.ne ftrace_graph_caller // ftrace_graph_caller();
#endif /* CONFIG_FUNCTION_GRAPH_TRACER */
mcount_exit
ENDPROC(_mcount)
EXPORT_SYMBOL(_mcount)
NOKPROBE(_mcount)
基于HAVE_FUNCTION_TRACER我们还可以选择HAVE_FUNCTION_GRAPH_TRACER它会开启上述的CONFIG_FUNCTION_GRAPH_TRACER选项,以便于可以根据栈绘制调用表关系图(未仔细研究该部分)。
同样的,在 CONFIG_FUNCTION_GRAPH_TRACER下检查 ftrace_graph_return函数指针如果没有设置特定于ftrace_graph_entry和ftrace_graph_entry_stub函数,则调用特定于架构的ftrace_graph_caller,该函数反过来调用特定于架构的 prepare_ftrace_return,prepare_ftrace_return参数与 ftrace_trace_function 略有不同,第二个参数selfpc是相同的,但第一个参数应该是指向frompc的指针。
通常它位于堆栈上。这允许函数暂时劫持返回地址,使其指向特定于 arch 的函数return_to_handler。
下面是更新后的 mcount 伪代码:
void mcount(void)
{
...
if (ftrace_trace_function != ftrace_stub)
goto do_trace;
+#ifdef CONFIG_FUNCTION_GRAPH_TRACER
+ extern void (*ftrace_graph_return)(...);
+ extern void (*ftrace_graph_entry)(...);
+ if (ftrace_graph_return != ftrace_stub ||
+ ftrace_graph_entry != ftrace_graph_entry_stub)
+ ftrace_graph_caller();
+#endif
/* restore any bare state */
...
下面是新的 ftrace_graph_caller 汇编函数的伪代码:
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
void ftrace_graph_caller(void)
{
/* save all state needed by the ABI */
unsigned long *frompc = &...;
unsigned long selfpc = <return address> - MCOUNT_INSN_SIZE;
/* passing frame pointer up is optional -- see below */
prepare_ftrace_return(frompc, selfpc, frame_pointer);
/* restore all state needed by the ABI */
}
#endif
下面是新的 return_to_handler 汇编函数的伪代码。
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
void return_to_handler(void)
{

本文对 ftrace 进行原理分析。ftrace 是内核内部跟踪程序,可用于调试和分析内核性能问题,有多种跟踪功能且具扩展性。文中介绍了其编译器支持,包括 HAVE_FUNCTION_TRACER 和 HAVE_DYNAMIC_FTRACE 选项,还阐述了初始化和 function trace 流程,最终形成庞大内核调试框架。
最低0.47元/天 解锁文章
64

被折叠的 条评论
为什么被折叠?



