关于原理,主要转载自这一篇文章:
linux trace point机制1—原理描述(linux5.1.6)
当需要获取内核的debug信息时,通常你会通过以下printk的方式打印信息:
void trace_func()
{
//……
printk("输出信息");
//……
}
缺点:
内核采用“插桩”的方法抓取log,“插桩”也称为trace point。每种trace point有一个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 |
struct tracepoint {
const char *name; /* Tracepoint name */
struct static_key key;
int (*regfunc)(void);
void (*unregfunc)(void);
struct tracepoint_func __rcu *funcs;
};
@ name* trace point的名字,内核中通过hash表管理所有的trace point,找到对应的hash slot后,需要通过name来识别具体的trace point。
@key trace point状态,1表示disable,0表示enable。
@regfunc 添加桩函数的函数
@unregfunc 卸载桩函数的函数
@funcs trace point中所有的桩函数链表
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) \
{ \
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); \
}
第2行声明一个外部trace point变量。"static inline"部分定义了一些trace point用到的公共函数。
第5行判断trace point是否disable,如果没有disable,那么调用__DO_TRACE遍历执行trace point中的桩函数(通过“函数指针”来实现执行桩函数)。
trace point提供了统一的框架,用void *指向任何函数,所以各个trace point取出桩函数指针后,需要转换成自己的函数指针类型, TP_PROTO(data_proto)传递函数指针类型用于转换,具体的转换在:(–> 这一行)
#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)
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行(–>)声明了桩函数原型。
#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宏,参考前面说明,提到了何时转换成桩函数指针类型。
从上面可以看出trace point的机制很简单,就是把用于debug的函数指针组织在一个struct trace point变量中,然后依次执行各个函数指针。不过为了避免各个模块重复写代码,内核用了比较复杂的宏而已。
另外我们也可以发现,使用trace point必须要通过register_trace_##name将桩函数(也就是我们需要的debug函数)添加到trace point中,这个工作只能通过moudule或者修改内核代码实现,对于开发者来说,操作比较麻烦。ftrace开发者们意识到了这点,所以提供了trace event功能,开发者不需要自己去注册桩函数了,易用性较好,后面文章会谈到trace event是如何实现的以及如何使用。