Skip to content
Stack & Ink
Go back

【Linux】 ftrace 机制分析

Table of contents

Open Table of contents

1. tracefs

tracefs 是一个特殊的伪文件系统,用于给用户态提供ftrace接口,挂载在/sys/kernel/tracing目录上。

1.1 目录构成

控制选项

文件/目录作用典型理解
available_tracers查看当前内核支持哪些 tracer“有哪些跟踪器能用”
current_tracer选择当前使用的 tracer“现在用哪个跟踪器”
tracing_ontracing 总开关“开始/停止记录”
trace_options查看/修改 tracer 的各种选项“跟踪行为和输出格式怎么配置”
options/trace_options 中的选项拆成独立文件“逐项开关 option”
tracing_cpumask指定哪些 CPU 参与 tracing“跟踪哪些 CPU”
trace_clock选择 trace 时间戳使用的时钟源“时间戳从哪里来”

缓冲区管理

文件/目录作用典型理解
trace读取当前 trace buffer 内容“看现在已经记录了什么”
trace_pipe持续流式读取 trace 数据“实时追踪新事件”
per_cpu/查看各 CPU 独立的 trace buffer“每个 CPU 各记各的”
buffer_size_kb配置每个 CPU 的 buffer 大小“每个 CPU 分多大缓存”
buffer_total_size_kb查看所有 CPU buffer 总大小“总共占了多少 tracing 内存”
buffer_subbuf_size_kb配置 ring buffer 子缓冲区大小“buffer 内部分块多大”
buffer_percent设置 buffer 阈值百分比“积累到什么程度再触发相关读取/唤醒行为”
free_buffer释放 tracing buffer 内存“不用了,把缓存还给系统”

Function 过滤器

文件/目录作用典型理解
available_filter_functions列出可被 function tracer 过滤/跟踪的函数“哪些函数能拿来 trace”
available_filter_functions_addrs同上,但带函数地址“函数名 + 地址版”
set_ftrace_filter指定要跟踪哪些函数“白名单”
set_ftrace_notrace指定哪些函数不要跟踪“黑名单”
set_ftrace_pid只跟踪指定 PID 相关执行“只看这些进程”
set_ftrace_notrace_pid排除指定 PID“这些进程别看”
enabled_functions查看当前已经启用 ftrace hook 的函数“现在真正挂上的有哪些”
touched_functions查看曾被 dynamic ftrace 修改过的函数“哪些函数曾经被动过”
dyn_ftrace_total_info查看 dynamic ftrace 的总体统计信息“dynamic ftrace 当前规模/统计”

Graph 过滤器

文件/目录作用典型理解
set_graph_function指定哪些函数作为 function graph 的跟踪入口“从哪些函数开始画调用图”
set_graph_notrace排除不想进入 graph 的函数“这些函数别展开”
max_graph_depth限制最大调用深度“最多往下追几层”

Event 过滤器

文件/目录作用典型理解
events/各类 trace event 的详细控制目录“事件系统的主目录”
available_events列出当前所有可用 event“有哪些事件能跟踪”
set_event批量启用/关闭 event“一次性选哪些事件”
set_event_pid只记录指定 PID 触发的 event“只看这些进程产生的事件”
set_event_notrace_pid排除指定 PID 的 event“这些进程产生的事件别记”
show_event_filters查看当前 event filter“现在有哪些事件过滤条件”
show_event_triggers查看当前 event trigger“事件触发器配置了什么”

Dynamic_Event 过滤器

文件/目录作用典型理解
kprobe_events创建/删除基于 kprobe 的动态事件“运行时给内核函数/地址加事件”
kprobe_profile查看 kprobe event 的命中统计“这些动态 kprobe 被触发了多少次”
uprobe_events创建/删除基于 uprobe 的动态事件“运行时给用户态函数/地址加事件”
uprobe_profile查看 uprobe event 的命中统计“这些动态 uprobe 被触发了多少次”
dynamic_events统一管理多种动态事件“动态 event 的统一入口”

MISC

文件/目录作用典型理解
instances/创建多个独立 tracing 实例“开多个互不干扰的 trace 会话”
trace_marker用户态向 trace buffer 写文本标记“手动往时间线里插一句话”
trace_marker_raw用户态写入更原始的 marker 数据“更底层地注入 trace 数据”
saved_cmdlines保存 PID 对应的进程名“让 trace 里能显示 comm”
saved_cmdlines_size配置保存多少条 PID→comm 映射“进程名缓存多大”
saved_tgids保存 PID 到 TGID 的映射“辅助识别线程属于哪个进程”
printk_formats保存 trace 中 printk 格式相关信息“辅助解析格式化输出”
error_log查看 tracefs 配置/解析错误“刚才那条 trace 命令为什么失败”
timestamp_mode查看/控制部分 tracing 时间戳模式“时间戳怎么解释”

1.2 使用示例

function / graph 追踪:

echo > trace 		# 清空buffer
echo function_graph > current_tracer # 选择tracer
echo schedule > set_graph_function	 # 设置要追踪的函数
echo 1 > tracing_on	# 开启追踪
echo 0 > tracing_on # 关闭追踪

cat trace			# 查看追踪情况
cat trace_pipe		# 从管道中实时读取

event 追踪:

echo > trace
echo nop > current_tracer
echo 1 > events/sched/sched_switch/enable
echo 1 > tracing_on
echo 0 > tracing_on

probe 追踪:

echo > trace
echo nop > current_tracer
echo 'p:myopen do_sys_openat2' > kprobe_events
echo 1 > events/kprobes/myopen/enable
echo 1 > tracing_on
echo 0 > tracing_on

2. fentry

2.1 初始化

内核开启了配置选项后,会在编译选项中添加:

ifeq ($(CONFIG_DYNAMIC_FTRACE_WITH_CALL_OPS),y)
  KBUILD_CPPFLAGS += -DCC_USING_PATCHABLE_FUNCTION_ENTRY
  CC_FLAGS_FTRACE := -fpatchable-function-entry=4,2		# 一共4条nop,两条放在函数入口前
else ifeq ($(CONFIG_DYNAMIC_FTRACE_WITH_ARGS),y)
  KBUILD_CPPFLAGS += -DCC_USING_PATCHABLE_FUNCTION_ENTRY
  CC_FLAGS_FTRACE := -fpatchable-function-entry=2		# 2条nop,0条放在函数入口前
endif

查看一下汇编,可以看到 4 条 nop:

mzz@vivobook ~/P/l/b/kernel (main)> aarch64-unknown-linux-gnu-objdump -d vmlinux | \
                                        grep -B4 -A12 '<do_sys_openat2>:'
ffff80008030feec:       17ffffa4        b       ffff80008030fd7c <build_open_flags+0x9c>
ffff80008030fef0:       d503201f        nop
ffff80008030fef4:       d503201f        nop

ffff80008030fef8 <do_sys_openat2>:
ffff80008030fef8:       d503201f        nop
ffff80008030fefc:       d503201f        nop
ffff80008030ff00:       a9bb7bfd        stp     x29, x30, [sp, #-80]!
ffff80008030ff04:       910003fd        mov     x29, sp
ffff80008030ff08:       a9025bf5        stp     x21, x22, [sp, #32]
ffff80008030ff0c:       2a0003f6        mov     w22, w0
ffff80008030ff10:       9100f3e0        add     x0, sp, #0x3c
ffff80008030ff14:       a90153f3        stp     x19, x20, [sp, #16]
ffff80008030ff18:       aa0103f5        mov     x21, x1
ffff80008030ff1c:       aa0203f4        mov     x20, x2
ffff80008030ff20:       a9007c1f        stp     xzr, xzr, [x0]
ffff80008030ff24:       aa0003e1        mov     x1, x0

除了添加nop,编译器还会将所有被patch的函数入口添加到__patchable_function_entries段,最终被加入到.init.data段中,使用__start_mcount_loc / __stop_mcount_loc 进行标记:

aarch64-unknown-linux-gnu-nm -n vmlinux | grep mcount_loc
ffff8000809ad3b0 D __start_mcount_loc
ffff8000809cc310 D __stop_mcount_loc

start_kernel 中会调用ftrace_init,最终调用下面的函数,将每个 fentry 保存到一个 dyn_ftrace 结构体中:

static int ftrace_process_locs(struct module *mod,
                               unsigned long *start,
                               unsigned long *end)
{
    struct ftrace_page *start_pg;
    struct ftrace_page *pg;
    struct dyn_ftrace *rec;
    unsigned long *p;
    unsigned long count;

    count = end - start;
    if (!count)
        return 0;

    /* 1. 按地址排序所有 patchable entry */
    if (!IS_ENABLED(CONFIG_BUILDTIME_MCOUNT_SORT) || mod)
        sort(start, count, sizeof(*start), ftrace_cmp_ips, NULL);

    /* 2. 为 dyn_ftrace records 分配页 */
    start_pg = ftrace_allocate_pages(count, NULL);
    if (!start_pg)
        return -ENOMEM;

    /* 3. 挂到全局 ftrace_pages */
    if (!mod)
        ftrace_pages = ftrace_pages_start = start_pg;

    /* 4. 每个有效 patchable entry -> 一个 dyn_ftrace */
    p = start;
    pg = start_pg;

    while (p < end) {
        unsigned long addr = *p++;

        if (!addr)
            continue;

        if (!mod &&
            !(is_kernel_text(addr) || is_kernel_inittext(addr)))
            continue;

        /* 调整到实际要 patch 的 callsite,一般来说是将addr指向第3条nop */
        addr = ftrace_call_adjust(addr);

        /* 省略换页逻辑 */

        rec = &pg->records[pg->index++];
        rec->ip = addr;
    }

    /* 5. 初始化每个 record / 必要时处理代码 */
    ftrace_update_code(mod, start_pg);

    return 0;
}

2.2 注册 function tracer

tracefs中进行如下操作时会触发 register_ftrace_function 回调,接收用户态传入的一个ftrace_ops,然后会调用 ftrace_ops_init 进行初始化:

echo function > current_tracer
int ftrace_startup(struct ftrace_ops *ops, int command)
{
    int ret;

    /* 1. 把这个 ftrace_ops 注册进 ftrace core */
    ret = __register_ftrace_function(ops);
    if (ret)
        return ret;

    /* 2. 标记这个 ops 已进入启用流程 */
    ops->flags |= FTRACE_OPS_FL_ENABLED | FTRACE_OPS_FL_ADDING;

    /*
     * 3. 根据 ops->func_hash 找到匹配的 dyn_ftrace,
     *    更新这些 record 的引用计数/flags。
     *
     *    如果有函数从“不需要 tracing”变成“需要 tracing”,
     *    就需要真正修改函数入口代码。
     */
    if (ftrace_hash_rec_enable(ops))
        command |= FTRACE_UPDATE_CALLS;

    /*
     * 4. 根据 command 更新实际代码:
     *
     *    NOP
     *    NOP
     *
     *      ↓
     *
     *    MOV X9, LR
     *    BL  ftrace trampoline
     */
    ftrace_startup_enable(command);

    /* 启用流程结束 */
    ops->flags &= ~FTRACE_OPS_FL_ADDING;

    return 0;
}

现在被 trace 的函数入口的后两条nop被patch成了:

mov x9, lr
bl  ftrace_caller

当该函数入口只有一个ftrace_ops 订阅时,前两个 nop 会被 patch 成一个 8B 的指针,用于指向该 ftrace_ops

ftrace_caller 是一段汇编,核心就是调用 ops->func,如果只有一个ops,那么就直接调用 ops 中注册的回调,否则会调用 ftrace_ops_list_func 进行分发。

最后来看一下通过 tracefs 注册的 ops->func 的回调到底是什么:

void trace_function(...)
{
    struct trace_buffer *buffer = tr->array_buffer.buffer;
    struct ring_buffer_event *event;
    struct ftrace_entry *entry;

    /* 1. 从 ring buffer 预留一条 TRACE_FN 事件 */
    event = __trace_buffer_lock_reserve(buffer, TRACE_FN,
                                        sizeof(*entry),
                                        trace_ctx);
    if (!event)
        return;

    /* 2. 拿到这条事件的数据区 */
    entry = ring_buffer_event_data(event);

    /* 3. 填 function trace 的核心字段 */
    entry->ip        = ip;
    entry->parent_ip = parent_ip;

    /* 4. 提交到 ring buffer */
    __buffer_unlock_commit(buffer, event);
}

2.3 注册 graph tracer

核心机制与function tracer 一样,不过 graph 会在ops->func上挂载一个自己的回调,这个回调会修改 LR 跳转到自己的 return trampoline 上,等到函数运行完成返回后就会来到这里,执行用户注册的回调, 然后将原来的 LR 改回去,让函数正确返回。

2.4 注册 event tracer

无论是静态 event tracer 注册给tracepoint,还是动态 event tracer 注册给 kprobe / uprobe,tracefs 都只是做了一层简单的封装,将自己的 event probe callback 回调挂载到对应的探测点上,等待回调执行向 ring buffer 中写入数据。

3. fprobe

fprobe 是对 register_ftrace_function 的上层封装,使得用户无需自己管理 ftrace_ops。、

其工作流程与function tracer 几乎完全一致:

register_fprobe()

根据 symbol / glob / address 生成目标集合

更新 fp->ops 的 filter_hash

register_ftrace_function(&fp->ops)

ftrace_hash_rec_enable()

只给匹配的 dyn_ftrace 增加引用计数并 patch

4. ring buffer

TODO


Share this post:

Previous Post
【Linux】 perf 机制分析
Next Post
【Linux】 uprobe 源码分析