内核中的每个 tracepoint 提供一个钩子来调用 probe 函数。一个 tracepoint 可以打开或关闭。打开时,probe函数关联到 tracepoint;关闭时,probe函数不关联到 tracepointtracepoint 关闭时对kernel产生的影响很小,只是增加了极少的时间开销(一个分支条件判断),极小的空间开销(一条函数调用语句和几个数据结构)。当一个 tracepoint 打开时,用户提供的probe函数在每次这个 tracepoint 执行是都会被调用。

技术背景

当需要获取内核的debug信息时,通常你会通过以下printk的方式打印信息:

1
2
3
4
5
6
void trace_func()
{
    // ...
    printk("输出信息");
    // ...
}

缺点:

  • 内核中printk是统一控制的,各个模块的printk都会被打印,无法只打印需要关注的模块。
  • 如果需要修改/新增打印信息,需要修改所有受影响的printk语句。这些printk分散在代码多处,每个地方都需要修改。
  • 嵌入式系统中,如果printk信息量大,console 如果有有大量的打印输出,用户无法在console输入命令,影响人机交互。

实现原理

内核采用 插桩的方法抓取 log,“插桩”也称为 tracepoint。每种 tracepoint有一个name、一个enable开关、一系列桩函数、注册桩函数的函数、卸载桩函数的函数。桩函数功能类似于printk,不过“桩函数”并不会把信息打印到console,而是输出到内核的ring buffer(环形缓冲区),缓冲区中的信息通过debugfs对用户呈现。逻辑架构如下:

接下来说明涉及到一些内核数据结构,代码参考:

数据结构 代码路径
DEFINE_TRACE(name) DECLARE_TRACE(name, proto, args) include/linux/tracepoint.h
struct tracepoint include/linux/tracepoint-defs.h
  • tracepoint依次执行桩函数,每个桩函数实现不同的debug功能。内核通过 register_trace_##name 将桩函数添加到 tracepoint 中,通过 unregister_trace_##nametracepoint中移除
  • 内核通过 DEFINE_TRACE(name) 定义 struct tracepoint 变量来描述 tracepoint
1
2
3
4
5
6
7
struct tracepoint {
    const char *name;            // Tracepoint name,内核中通过hash表管理所有的`tracepoint`,找到对应的hash slot后,需要通过name来识别具体的`tracepoint`
    struct static_key key;       // Tracepoint状态,1表示disable,0表示enable
    int (*regfunc)(void);        // 注册桩函数的函数
    void (*unregfunc)(void);     // 卸载桩函数的函数
    struct tracepoint_func __rcu *funcs;  // Tracepoint中所有的桩函数链表
};

内核通过 #define DECLARE_TRACE(name, proto, args) 定义 tracepoint 用到的函数,定义的函数原型如下(从代码中摘取了几个,不止以下3个):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 声明一个外部`tracepoint`变量,`static inline`部分定义了一些`tracepoint`用到的公共函数
static inline void trace_##name(proto)

register_trace_##name(void (*probe)(data_proto), void *data)

unregister_trace_##name(void (*probe)(data_proto), void *data)

#define __DECLARE_TRACE(name, proto, args, cond, data_proto, data_args) //\
	extern struct tracepoint __tracepoint_##name;			\
	static inline void trace_##name(proto)				\
	{								\
// 判断`tracepoint`是否disable,如果没有disable,那么调用__DO_TRACE遍历执行`tracepoint`中的桩函数(通过“函数指针”来实现执行桩函数)
		if (static_key_false(&__tracepoint_##name.key))		\
			__DO_TRACE(&__tracepoint_##name,		\
				TP_PROTO(data_proto),			\
				TP_ARGS(data_args),			\
				TP_CONDITION(cond), 0);			\
		if (IS_ENABLED(CONFIG_LOCKDEP) && (cond)) {		\
			rcu_read_lock_sched_notrace();			\
			rcu_dereference_sched(__tracepoint_##name.funcs);\
			rcu_read_unlock_sched_notrace();		\
		}							\
	}								\
	__DECLARE_TRACE_RCU(name, PARAMS(proto), PARAMS(args),		\
		PARAMS(cond), PARAMS(data_proto), PARAMS(data_args))	\
	static inline int						\
	register_trace_##name(void (*probe)(data_proto), void *data)	\
	{								\
		return tracepoint_probe_register(&__tracepoint_##name,	\
						(void *)probe, data);	\
	}								\
	static inline int						\
	register_trace_prio_##name(void (*probe)(data_proto), void *data,\
				   int prio)				\
	{								\
		return tracepoint_probe_register_prio(&__tracepoint_##name, \
					      (void *)probe, data, prio); \
	}								\
	static inline int						\
	unregister_trace_##name(void (*probe)(data_proto), void *data)	\
	{								\
		return tracepoint_probe_unregister(&__tracepoint_##name,\
						(void *)probe, data);	\
	}								\
	static inline void						\
	check_trace_callback_type_##name(void (*cb)(data_proto))	\
	{								\
	}								\
	static inline bool						\
	trace_##name##_enabled(void)					\
	{								\
		return static_key_false(&__tracepoint_##name.key);	\
	}

tracepoint 提供了统一的框架,用 void * 指向任何函数,所以各个 tracepoint 取出桩函数指针后,需要转换成自己的函数指针类型, TP_PROTO(data_proto) 传递函数指针类型用于转换,具体的转换在 –>这一行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#define __DO_TRACE(tp, proto, args, cond, rcuidle)			//\
	do {								\
		struct tracepoint_func *it_func_ptr;			\
		void *it_func;						\
		void *__data;						\
		//.........................
		it_func_ptr = rcu_dereference_raw((tp)->funcs);		\
									\
		if (it_func_ptr) {					\
			do {						\
				it_func = (it_func_ptr)->func;		\
				__data = (it_func_ptr)->data;		\
	-->		((void(*)(proto))(it_func))(args);	\
			} while ((++it_func_ptr)->func);		\
		}							\
		//.........................
	} while (0)

桩函数的proto的传递的例

1
2
3
4
5
DEFINE_EVENT_CONDITION(f2fs__submit_page_bio, f2fs_submit_page_write,
-->      TP_PROTO(struct page *page, struct f2fs_io_info *fio),
         TP_ARGS(page, fio),
         TP_CONDITION(page->mapping)
);

第2行 –> 声明了桩函数原型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#define DEFINE_EVENT_CONDITION(template, name, proto, args, cond)
        DEFINE_EVENT(template, name, PARAMS(proto), PARAMS(args))

#define DEFINE_EVENT(template, name, proto, args)
        DECLARE_TRACE(name, PARAMS(proto), PARAMS(args))

#define DECLARE_TRACE(name, proto, args)
        __DECLARE_TRACE(name, PARAMS(proto), PARAMS(args),           
              cpu_online(raw_smp_processor_id()),           
              PARAMS(void *__data, proto),                 
              PARAMS(__data, args))

至此执行到 __DECLARE_TRACE 宏,参考前面说明,提到了何时转换成桩函数指针类型。

从上面可以看出 tracepoint的机制很简单,就是把用于debug的函数指针组织在一个 struct tracepoint 变量中,然后依次执行各个函数指针。不过为了避免各个模块重复写代码,内核用了比较复杂的宏而已。

另外我们也可以发现,使用 tracepoint 必须要通过 register_trace_##name 将桩函数(也就是我们需要的debug函数)添加到 tracepoint 中,这个工作只能通过 moudule 或者修改内核代码实现,对于开发者来说,操作比较麻烦。ftrace开发者们意识到了这点,所以提供了trace event功能,开发者不需要自己去注册桩函数了,易用性较好。

参考资料