工作原理

TracePoint(以下称为TP点)静态写入到内核代码中,其入口函数形如trace_[name],称其为钩子函数(hook),钩子函数可以认为是TP点入口函数,它再调用探针函数( probe ),探针函数中对参数进行处理。TP点可设置开关,默认关闭,内核经过特殊优化基本没有开销(这里不再分析,相关技术关键字:jump_lablestatic_callretpolinemeltdownspectre),打开时最终通过钩子函数调用我们放入的探针函数。

Event则是操作系统提供控制TP点的入口,通过挂载debugfs文件系统,可以看到许多events,通过文件系统的操作最终调用内核处理函数,实现用户态与内核态的交互。

整个交互过程如下:

kernel-tracepoint_0

  • 用户通过文件 /sys/kernel/debug/tracing/events/xxx/enable与内核交互,写入1则开启TP点。
  • 内核运行过程中若TP点关闭,则绕过其hook函数,若开启则执行trace_[name]函数,进入hook函数中执行probe函数。
  • probe函数将内容输出到ring buffer中,用户通过/sys/kernel/debug/tracing/trace 读取其内容。

实现剖析

tracepoint 的添加

// 核心宏 tracepoint.h 中的 TRACE_EVENT_FN
// 最终会调用 __DECLARE_TRACE
// 定义出一些必要的数据结构
extern int __traceiter_##name(data_proto);
static inline void trace_##name(proto);

核心部分在于定义函数trace_##name(proto),这个函数就是插入到代码的TP点,开启时就会去调用它,分析其实现:

static inline void trace_##name(proto) {
    // 判断是否开启,使用来 jump_label
    if (static_key_false(&__tracepoint_##name.key))
        // 核心函数
        __DO_TRACE(name,                
        TP_ARGS(args),                
        TP_CONDITION(cond), 0);            
}
// 展开后最核心的部分为
__DO_TRACE_CALL(name, TP_ARGS(args));
// 继续展开
DECLARE_STATIC_CALL(tp_func_##name, __traceiter_##name);
static_call(tp_func_##name)(__data, args);
// 这里相当于利用 static_call 调用 __traceiter_##name
// 继续暂开宏
int __traceiter_##_name(void *__data, proto) {
    struct tracepoint_func *it_func_ptr;
    it_func_ptr =                        \
            rcu_dereference_raw((&__tracepoint_##_name)->funcs);
    // 拿到tp点中的funcs直接执行
    if (it_func_ptr) {                    \
            do {                        \
                it_func = READ_ONCE((it_func_ptr)->func); \
                __data = (it_func_ptr)->data;        \
                ((void(*)(void *, proto))(it_func))(__data, args); \
            } while ((++it_func_ptr)->func);        \
        }                            \
        return 0;    
}

总结:TracePoint的宏定义一个TP点,默认关闭。开启时调用trace_[name]函数,继续调用__traceiter_[name]函数,根据TP点中的funcs调用挂载的函数。挂载函数过程见下文。

event 的添加

整个调用栈如下:

start_kernel        // 内核流程开始
trace_init
trace_event_init
event_trace_enable  // 初始化

其核心数据结构为__start_ftrace_events,这是在编译时就已经写好的段,遍历整个数组,其类型为trace_event_call,初始化后放在全局变量ftrace_trace_arrays中。

event的定义使用头文件trace_events.h中的宏,这里和tracepoint.h中的宏复用,分为多个stage进行结构体的定义,定义如下结构体:

struct trace_event_raw_<call>;
struct trace_event_data_offsets_<call>;
enum print_line_t trace_raw_output_<call>(struct trace_iterator *iter, int flags);
static struct trace_event_call event_<call>;
static void trace_event_raw_event_<call>(void *__data, proto); 
static struct trace_event ftrace_event_type_<call>;
static char print_fmt_<call>[] = <TP_printk>;
static struct trace_event_class __used event_class_<template>;

会定义非常多的结构体,挑选关键的进行分析:

// 每一个event的实例,关联tp点
static struct trace_event_call event_<call>;
// 每一个event的probe入口,system名字等信息
static struct trace_event_class __used event_class_<template>;
// probe点
static void trace_event_raw_event_<call>(void *__data, proto);

其核心就是probe如何函数trace_event_raw_event_<call>(void *__data, proto);,继续分析:

static notrace void    
trace_event_raw_event_##call(void *__data, proto){
    // ringbuffer的处理
    entry = trace_event_buffer_reserve(&fbuffer, trace_file,
                 sizeof(*entry) + __data_size);        
}

总结:event中定义的结构体与tracepoint关联,定义好处理函数。下文分析如何将event中的probe激活到TP点的执行过程中。

event 的激活

event的激活通过debugfs,首先是events的创建,调用栈如下:

tracer_init_tracefs                    // 内核异步任务进行初始化
tracer_init_tracefs_work_func
event_trace_init
early_event_add_tracer
__trace_early_add_event_dirs
event_create_dir
trace_create_file                   // 这里可以看到 enable 的操作

ftrace_enable_fops定义了enable的操作:

event_enable_write
ftrace_event_enable_disable
__ftrace_event_enable_disable
trace_event_reg
tracepoint_probe_register        
tracepoint_probe_register_prio
tracepoint_add_func

TP点的激活通过tracepoint_probe_register函数,将trace_event_class_[name]中的probe函数传入,并开启TP跟踪开关,最终TP点会调用到trace_event_raw_event_[name]中。

实战演练

目标:统计 brk 系统调用发生时的参数 (该效果通过已有的syscall也能实现)。

TracePoint 定义

include/trace/events创建brk.h中添加eventtp点。该头文件为上述分析的linux/tracepoint.htrace/define_trace.h中文件定义的宏定义出一些必要tp结构体。

/* SPDX-License-Identifier: GPL-2.0 */
// 定义显示 event 的子系统名字
// 创建 /sys/kernel/debug/tracing/events/TRACE_SYSTEM 文件夹
#undef TRACE_SYSTEM
#define TRACE_SYSTEM brk

#if !defined(_TRACE_BRK_H) || defined(TRACE_HEADER_MULTI_READ)
#define _TRACE_BRK_H

// 包含 tracepoint.h 以创建 TP 点
#include <linux/tracepoint.h>

TRACE_EVENT(syscall_brk,
        // 定义 trace_syscall_brk 函数参数
        TP_PROTO(unsigned long origbrk, unsigned long brk),
        TP_ARGS(origbrk, brk),
        // 定义 trace_syscall_bak 函数内部需要的变量
        TP_STRUCT__entry(
            __field(unsigned long, origbrk)
            __field(unsigned long, brk)
        ),
        // trace_syscall_bak 函数内部变量进行赋值
        TP_fast_assign(
            __entry->origbrk = origbrk;
            __entry->brk = brk
        ),
        // 定义输出格式
        TP_printk("origbrk=0x%lx brk=0x%lx", __entry->origbrk, __entry->brk)
)

#endif

// 通过 define_trace.h 中的宏创建 event
/* This part must be outside protection */
#include <trace/define_trace.h>

TracePoint 插桩

把头文件引入到brk系统调用函数中mmap.c

// mmap.c: 54 引入创建的 tp 点
#define CREATE_TRACE_POINTS
#include <trace/events/mmap.h>
#include <trace/events/brk.h>
// mmap.c: 184
// ... 引入 TP 点
    origbrk = mm->brk;
    trace_syscall_brk(origbrk, brk);

TracePoint 启用

echo 1 >> /sys/kernel/debug/tracing/events/brk/syscall_brk/enable

观察TP点触发时,写入ring buffer的内容:

kernel-tracepoint_1

最后修改:2023 年 01 月 05 日
如果觉得我的文章对你有用,请随意赞赏