学习完了ftrace的function的基本功能,其作用主要是用来跟踪特定内核函数调用的频次,对于内核,特别是初学者,对于函数的调用关系不清晰,并且内核中有很多函数指针,会把我们弄的摸不着头脑,那么我们就需要分析内核函数的调用的子过程,即本函数调用了哪些子函数,处理的流程如何,那么我们就需要用到function_graph,本章主要是学习如下
本文采用的linux内核版本为Linux 5.15
首先我们来看看最新的ARM64代码是否是基于pg方式来做的,首先我们来看看CC_FLAGS_FTRACE这个宏
对于这个宏在arch/arm64/Makefile中被重新定义为-fpatchable-function-entry=2,我们首先要去看看对于Makefile中:=
的含义,这个是覆盖之前的值,那么CC_FLAGS_FTRACE就不是等于-pg了
大致的意思是如果编译时指定 -fpatchable-function-entry=N[,M]
,会在函数入口后,函数第一个指令之前插入 N 个 nop,但是要留 M 个放在函数入口之前,同时通过一个特殊的 __patchable_function_entries
段来记录所有的函数入口。
链接后的 vmlinux 可以通过 __{start,stop}_mcount_loc
符号获取到所有函数入口,同时每个函数入口都会有 nop,用来做指令修改。
我们查看下各个文件的编译选项,发现已经没有使用-pg,而是使用的-fpatchable-function-entry=2
观察编译后的 blk-core.o 中的函数入口是否是2 个 nop,以及所有的函数入口记录在哪里?
其基本的原理跟_mcount类似,编译时,在所有内核函数入口前插入 2个 nop 指令,并创建 __patchable_function_entries
段用来记录所有的函数入口,链接时,把所有的函数入口归档到 __{start,stop}_mcount_loc
,启动时,由 ftrace_init
把所有函数入口维护在 start_pg
(ftrace_pages_start
)链表中。就可以针对某个特定的函数,修改其 nop 指令,让其被调用时,跳转到自定义的指令中,比如跳转到 _mcount
处,继而执行 ftrace_trace_function
函数指针,来执行特定功能的 tracer(例如:function,function_graph)
对于function_grap也是满足三步法,设置tracer类型,设置tracer参数,使能tracer
oot@rlk:/sys/kernel/tracing# echo function_graph > current_tracer
root@rlk:/sys/kernel/tracing# echo dev_attr_show > set_graph_function
root@rlk:/sys/kernel/tracing# echo 1 > tracing_on
root@rlk:/sys/kernel/tracing# echo 0 > tracing_on
通过简单的例子,看到了 function/function_graph
tracer 的基本用法,但是在实际应用中会有比较棘手的问题和需求。比如:
这个就要用到ftrace的过滤控制相关文件,详细的使用方式,可以参考这个文件Ftrace 进阶用法
文件名 | 功能 |
---|---|
set_ftrace_filter | function tracer 只跟踪某个函数 |
set_ftrace_notrace | function tracer 不跟踪某个函数 |
set_graph_function | function_graph tracer 只跟踪某个函数 |
set_graph_notrace | function_graph tracer 不跟踪某个函数 |
set_event_pid | trace event 只跟踪某个进程 |
set_ftrace_pid | function/function_graph tracer 只跟踪某个进程 |
options | 这是一个目录,其中包含每个可用跟踪选项的文件(也在 trace_options 中)。 也可以通过将“1”或“0”分别写入具有选项名称的相应文件来设置或清除选项。 |
function_profile_enabled | 设置后,它将使用函数跟踪器或函数图跟踪器(如果已配置)启用所有功能。 它将保留被调用函数数量的直方图,如果配置了函数图跟踪器,它还将跟踪在这些函数中花费的时间。 |
funcgraph-overrun | 设置后,在跟踪每个函数后显示图形堆栈的“overrun”。 溢出是当调用的堆栈深度大于为每个任务保留的深度时。 每个任务都有一个固定的函数数组,可以在调用图中进行跟踪。 如 果调用的深度超过该深度,则不会跟踪该函数。 溢出是由于超出此数组而错过的函数数。 |
funcgraph-cpu | 设置后,将显示发生跟踪的 CPU 的 CPU 编号。 |
funcgraph-overhead | 设置后,如果函数花费的时间超过一定量,则显示延迟标记。 请参阅上面标题描述下的“delay” |
funcgraph-proc | 与其他跟踪器不同,默认情况下不显示进程的命令行,而是仅在上下文切换期间跟踪进出任务时才显示。 启用此选项会在每一行显示每个进程的命令。 |
funcgraph-duration | 在每个函数(返回)结束时,函数中的持续时间以微秒为单位显示。 |
funcgraph-irqs | 禁用时,将不会跟踪中断内发生的函数。 |
funcgraph-tail | 设置后,返回事件将包括它所代表的函数。 默认情况下这是关闭的,并且只显示一个结束的大括号“}”来返回函数 |
graph-time | 使用函数图跟踪器运行函数探查器时,包括调用嵌套函数的时间。 如果未设置,则为函数报告的时间将仅包括函数本身执行的时间,而不包括它调用的函数的时间。 |
首先tracefs 初始化 set_ftrace_filter
文件,记录 global_ops
在 inode->i_private
,注册 ftrace_filter_fops
在 inode->i_fop
,相关代码如下:
对于global_ops是一个全局结构体,主要用于记录跟踪函数ftrace_stub和记录目标函数的hash表func_hash
我们会通过写入set_ftrace_filter节点,而ftrace_filter_fops
中定义打开文件,读写文件,关闭文件的函数指针,每个函数的作用和调用的关键函数介绍如下:
ftrace_filter_open:在文件打开时执行,初始化 iter
结构体实例,并存放在 file->private_data
中,关键函数:ftrace_regex_open
ftrace_filter_write:在对文件写入数据时执行,把目标函数更新到 iter->hash
中,关键函数:filter_parse_regex
, enter_record
ftrace_regex_release:在文件关闭时执行,遍历函数入口表,根据目标函数在新 hash 与旧 hash 中的存在状态,更新对应的 rec->flags
,再次遍历函数入口表,根据 rec->flags
按需替换目标函数入口
关键函数:ftrace_hash_move
, ftrace_run_update_code
ftrace_filter_open
此函数在打开文件时执行,分配 iter
结构体实例,为其初始化相关成员,需要特别关注的成员是:
parser
用于存放用户输入,当前操作流程中为 “vfs_read” 字符串hash
用于记录目标函数(vfs-read)对应的 struct dyn_ftrace * rec
,当前操作流程中,hash 是 ops->func_hash->filter_hash
的一份拷贝最后,写操作模式下,iter
记录在 file->private_data
中。 ftrace_filter_write函数对文件写时执行,解析用户态字符串ubuf,存放到parser->buffer,遍历函数入口表 ftrace_pages_start
,找到其匹配项 rec
,并通过 enter_record()
函数将 rec
记录到 iter->hash
,关键代码如下:
最值得关注的是,do_for_each_ftrace_rec宏表示遍历函数入口表,其实是在遍历 ftrace_pages_start
,所有的函数入口记录在 &pg->records
指针数组中,以 pg->index
为检索范围来找对应的 rec
。
而 rec
是一个 struct dyn_ftrace *
类型,此结构体中 ip
即函数入口,flags
用来控制对当前函数的跟踪,比如:是否开启跟踪,函数跟踪时是否保留寄存器,当前的引用计数等。在后续代码中,ftrace 是否要执行指令修改,就是通过 rec->flags
来判断。
ftrace_regex_release
此函数在文件关闭时执行,把当前的iter->hash移动到旧的hash(ops->func_hash->filter_hash),在移动过程中,对比两个hash中的目标函数,按需更新每个函数的rec->flags的计数以及相关功能flags,后以FTRACE_UPDATE_CALLS
命令执行 ftrace_run_update_code()
,此函数会检测每个函数 rec->flags
的状态,执行 FTRACE_UPDATE_MAKE_CALL
操作,将当前函数入口替换为 ftrace_caller
。代码执行流程如下:
ftrace_hash_move_and_update_ops()
函数执行 ftrace_hash_move()
,如果前者正常返回表示有函数入口需要替换,则执行 ftrace_ops_update_code()
。
ftrace_hash_move()
通过新旧两个 hash 对比来更新函数入口表中的 rec->flags
,并最终把 new_hash 更新到 old_hash。
ftrace_ops_update_code()
函数,通过指定 FTRACE_UPDATE_CALLS
调用 ftrace_run_update_code()
函数,用来执行函数入口的指令替换:
此函数在接受命令后经过一连串的调用最终执行到 ftrace_modify_all_code()
,这里执行 FTRACE_UPDATE_CALLS
命令,遍历函数入口表,执行 __ftrace_replace_code()
。
arch_ftrace_update_code这个函数首先保证安全,如用stop_machine机制,然后直接调用了ftrace_modify_all_code(int command);
command命令主要有三种:
FTRACE_UPDATE_CALLS 表示开启函数的调用链,至于那些函数需要开启,只有通过全局变量来传递;
FTRACE_DISABLE_CALLS 表示关闭函数的调用链,至于那些函数需要关闭,只有通过全局变量来传递;
FTRACE_UPDATE_TRACE_FUNC 表示修改这个调用链;
对于function最终会调用ftrace_replace_code,会选择对应的跳转目标,这里是 ftrace_caller
,通过 ftrace_update_record()
判断目标函数的修改方式,这里是 FTRACE_UPDATE_MAKE_CALL
,表示当前函数入口要从 nop 替换为对 ftrace_caller
的调用,继而执行 ftrace_make_call()
。具体代码如下:
这个就是遍历函数入口表,其实是在遍历 ftrace_pages_start
,这个之前已经讲过,所有的函数都在这个表中记录
检查函数是否被跟踪,即该地址处的原内容是否为nop,如果是从nop替换为bl tracer,则返回FTRACE_UPDATE_MAKE_CALL;如果原本该地址已经被其他tracer跟踪,则返回FTRACE_UPDATE_MODIFY_CALL
最终函数调用到了ftrace_caller函数,对于ARM64这个函数定义在arch/arm64/kernel/entry-ftrace.S文件里面。
所以本小姐设置 function tracer 的前提下,echo vfs_read >> set_ftrace_filter
命令在内核的执行过程,我们以 set_ftrace_filter
文件的相关操作函数为切入点开始分析,分别分析 {open,write,release}
三个接口的实现:
ftrace_filter_open()
,在文件打开时执行,初始化 iter
结构体实例,并存放在 file->private_data
中ftrace_filter_write()
,在对文件写入数据时执行,把目标函数更新到 iter->hash
,其中对 do_for_each_ftrace_rec
宏进行展开分析ftrace_regex_release()
,在文件关闭时执行,遍历函数入口表,根据目标函数是否在新旧 hash 中,更新对应的 rec->flags
,再次遍历函数入口表,根据 rec->flags
按需替换目标函数入口在 ftrace 执行完内核函数入口的指令替换后,函数执行时会跳转到 ftrace_caller
调用跟踪函数,而此函数就是 function
tracer 所注册的跟踪函数 function_trace_call()
。并且在观察 ftrace_caller
执行的过程中,我们留了一个问题:ftrace_call
是如何被替换为对 function_trace_call()
的调用。既然 function_trace_call()
是 function
tracer 的跟踪函数,那么我们就从 echo function > current_tracer
命令是如何工作的角度进行分析,与替换类似,同样以 current_tracer
文件的相关操作函数为切入点,分析过程中重点关注跟踪函数如何变化以及最终如何实现对 ftrace_call
的替换。
对于init_tracer_tracefs(&global_trace, NULL)会tracefs 初始化 current_tracer
文件记录 global_trace
在 inode->i_private
,注册 set_tracer_fops
在 inode->i_fop
,global_trace
是一个 struct trace_array
结构体实例,用来表示 /sys/kernel/debug/tracing/
顶级目录下的相关 tracing 配置
对于替换的路径基本跟上面的类似,set_ftrace_fops
中分别定义了对 current_tracer
文件打开、读取、写入的相关操作:
tracing_open_generic()
在文件打开时执行,设置 global_trace
到 filp->private_data
tracing_set_trace_read
在文件读取时执行,简单地把当前 tracer 的名字 tr->current_trace->name
拷贝到用户态tracing_set_trace_write()
在文件写入时执行,匹配到对应的 tracer 并执行对应的初始化工作,并最终执行对 ftrace_call
的指令替换我们重点关注写的接口tracing_set_trace_write
此函数从用户态拷贝输入的字符串,调用 tracing_set_tracer()
函数在全局 tracer 表 trace_types
中匹配对应的 tracer,然后执行 tracer 的初始化,并将当前 tracer 记录到 tr->current_trace
trace_init()
函数会调用 t->init()
,t->init()
对应到的是 function
tracer 通过 register_tracer()
函数注册的 struct tracer function_trace
的 .init
定义,即 function_trace_init()
。function_trace
代码如下kernel/trace/trace_functions.c
function_trace_init()
函数执行 select_ftrace_function()
选择 function tracer 的跟踪函数,默认通过 TRACE_FUNC_NO_OPTS
选择到 function_trace_call()
函数,并将其设置到 ops->func
(global_ops->func
),之后执行 register_ftrace_function()
注册跟踪函数。
register_ftrace_function()
函数执行如下内容,详细的可以自己看代码
__register_ftrace_function()
函数,添加当前 ops
(global_ops
) 到全局 ops 链表 ftrace_ops_list
,并设置全局跟踪函数 ftrace_trace_funcion()()
为 ops->func
ftrace_hash_ipmodify_enbale()
函数,根据 ops->func_hash->filter_hash
更新函数入口表中每个函数记录 rec
的 ipmodify 标志位ftrace_hash_rec_enable()
函数,判断是否有函数入口需要更新,如果需要更新则为 command
设置 FTRACE_UPDATE_CALLS
标志ftrace_startup_enable()
函数,判断保存的跟踪函数 saved_ftrace_func
与当前跟踪函数 ftrace_trace_function()
是否相同,如果不同,则表示需要更新跟踪函数,为 command
设置 FTRACE_UPDATE_TRACE_FUNC
,之后执行 ftrace_run_update_code(command)
对于该函数,首先设置此时为function_trace_function,其次判断函数入口是否需要更新,最终会以最后会以 FTRACE_UPDATE_CALLS | FTRACE_UPDATE_TRACE_FUNC
为命令执行 ftrace_run_update_code()
来进行函数入口和跟踪函数的替换,对于这个函数在替换的时候已经介绍过,暂时不重复说明。
所以本小节从function
tracer 使能的工作过程,观察 ftrace_caller
中的 ftrace_call
是如何被替换成 function
tracer 的跟踪函数,以及 ftrace_caller
如何为跟踪函数设置上下文
ftrace_caller
为跟踪函数设置上下文,并调用跟踪函数ftrace_caller
函数图跟踪机制,最初只是作为 function_graph
tracer 的一部分出现在内核中,用来跟踪某个内核函数运行时的子函数调用及其时间消耗,并输出一个函数调用图,后来作为 ftrace 跟踪内核函数入口和返回的核心机制,独立到 kernel/trace/fgraph.c
文件中。对于这个功能,也可以从我们上面通过设置set_graph_function和current_tracer的角度去学习代码,其基本的流程与function基本类似,我们也不去看了。我们重点关注与替换跟踪函数,跟踪函数(入口/返回跟踪函数)的标准注册接口 register_ftrace_graph()
将其赋值为当前注册者指定的图跟踪函数。
与 function
tracer 一致,function_graph
使能时会调用此 tracer 注册时指定的 .init
函数 - graph_trace_init
,直接调用 register_ftrace_graph()
设置图跟踪函数。关键代码如下:
register_ftrace_graph()以
struct fgraph_ops *为入参,只注册
graph_ops
register_ftrace_graph()
函数执行如下步骤:
ftrace_graph_return()
为 gops->retfunc
__ftrace_graph_entry()
为 gops->entryfunc
,并把全局入口跟踪函数 ftrace_graph_entry()
设置为 ftrace_graph_entry_test()
,之后执行 update_function_graph_func()
重新设置全局入口跟踪函数ftrace_startup()
,注册全局图操作结构 graph_ops
到 ftrace_ops_list
,并执行 FTRACE_START_FUNC_RET
指令替换命令ftrace_graph_entry_test()
跟踪函数会先判断当前函数是否在 global_ops->func_hash
中,再执行 __ftrace_graph_entry()
。
在 update_function_graph_func()
函数中,遍历 ftrace_ops_list
,如果存在不为 global_ops
或 graph_ops
的 ops,则继续采用 ftrace_graph_entry_test()
进行有判断的函数跟踪,避免不属于 global_ops
或者 graph_ops
需要跟踪的内核函数调用到 tracer 指定的入口跟踪函数。关键代码如下:
ftrace_startup()
函数也会被 register_ftrace_function()
函数调用,而在注册图跟踪函数的过程中,涉及到以下变化:
__register_ftrace_function()
函数将 graph_ops
添加到全局链表中,并将全局跟踪函数设置为 graph_ops->func
- ftrace_stub
,全局入口跟踪函数设置为 gops->entryfunc
ftrace_modify_all_code()
函数处理 FTRACE_START_FUNC_RET
命令,执行 ftrace_enable_ftrace_graph_caller()
,对 ftrace_graph_call
进行指令替换,其代码为arch/arm64/kernel/ftrace.c对于ARM64其实现arch/arm64/kernel/entry-ftrace.S
用户通过 register_ftrace_graph()
函数更新全局入口与返回跟踪函数,并以 prepare_ftrace_return()
函数替换 ftrace_graph_call
。在内核执行时,内核函数入口会跳转到 ftrace_caller
,继而执行 prepare_ftrace_return()
,调用全局入口跟踪函数,并以 return_to_handler
覆盖函数返回地址,使得 return_to_handler
在函数返回时执行,调用全局返回跟踪函数,之后跳转到原函数的返回地址。
首先,我们来看看return_to_hander,其实现为arch/arm64/kernel/entry-ftrace.S:
所以通过在函数的调用开始及调用结束分别调用了 prepare_ftrace_return 及 ftrace_return_to_handler 来进行 LR 的修改与恢复。这样可以统计到每一个函数的调用关系与具体执行时间(在开始与结束时分别记录了时间)。该功能可以帮助读者在性能调试的时候识别到性能瓶颈,以便于后期的进一步性能优化调优。
我们了解了 ftrace 的两个核心机制 – 动态函数跟踪、动态函数图跟踪,二者分别向用户提供 register_ftrace_function()
、register_ftrace_graph()
接口来注册对函数进行跟踪以及对函数入口和返回进行跟踪的跟踪函数。最后我们以一张 ftrace 的架构图来结束本文。
https://gitee.com/tinylab/riscv-linux/blob/master/articles/20220928-ftrace-impl-5-fgraph.md