Systemtap中内核trace事件的实现

2020-02-18
5分钟阅读时长

概述

内核中定义了一系列的trace point,这些trace point在特定的内核函数中被触发调用时被记录,而对应到systemtap中就是kernel.trace类型的probe事件,可以使用命令来查看系统所有的trace point:

$ sudo stap -L 'kernel.trace("*")' | more
kernel.trace("9p:9p_client_req") $clnt:struct p9_client* $type:int8_t $tag:int
kernel.trace("9p:9p_client_res") $clnt:struct p9_client* $type:int8_t $tag:int $err:int
kernel.trace("9p:9p_protocol_dump") $clnt:struct p9_client* $pdu:struct p9_fcall*

换言之,通过systemtap能够对这些已经静态注册的内核调用记录点进行监控、跟踪。

以下来解释trace point在内核的实现以及与systemtap相关的内容。

数据结构

内核通过DECLARE_TRACE来声明一个trace point:

DECLARE_TRACE(subsys_eventname,
	TP_PROTO(int firstarg, struct task_struct *p),
	TP_ARGS(firstarg, p));

在这里:

  • subsys_eventname是定义trace事件的唯一字符串,又能拆解成两部分:subsys就是子系统的名称,而eventname是事件名称。比如下面将作为实例的softirq_entry,就定义了一个在softirq子系统中的entry事件。
  • TP_PROTO(int firstarg, struct task_struct *p):定义了传入trace函数的参数原型。
  • TP_ARGS(firstarg, p):定义了参数名称,其类型与TP_PROTO中的类型一一对应。

这个宏的定义如下:

// include/linux/tracepoint.h
#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定义如下:

#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);	\
	}

可以看到,这个宏做了如下的事情:

  • 声明了一个类型为tracepoint的结构体变量__tracepoint_##name
  • 定义了几个相关的函数,分别用于处理trace event、注册、注销等。其中需要重点关注的是宏trace_##name,这里定义了对对应的traceevent进行跟踪的函数。

其中,宏里面一个字符串跟着##name表示这个字符串与name的连接形成的字符串。

这里的结构体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;
};

该结构体中分别定义了:

  • traceevent名称。
  • 注册、注销、被触发时的处理函数。

以上只是声明了tracepoint结构体变量,而具体定义变量的宏是DEFINE_TRACE

// include/linux/tracepoint.h
#define DEFINE_TRACE(name)						\
	DEFINE_TRACE_FN(name, NULL, NULL);

#define DEFINE_TRACE_FN(name, reg, unreg)				 \
	static const char __tpstrtab_##name[]				 \
	__attribute__((section("__tracepoints_strings"))) = #name;	 \
	struct tracepoint __tracepoint_##name				 \
	__attribute__((section("__tracepoints"))) =			 \
		{ __tpstrtab_##name, STATIC_KEY_INIT_FALSE, reg, unreg, NULL };\
	static struct tracepoint * const __tracepoint_ptr_##name __used	 \
	__attribute__((section("__tracepoints_ptrs"))) =		 \
		&__tracepoint_##name;

因此,DEFINE_TRACE的作用就是:

  • __tracepoints_stringssection中定义了字符串数组变量__tpstrtab_##name,其值为name。
  • __tracepointssection中定义了结构体tracepoint变量__tracepoint_##name

以上解释了trace point相关的数据结构、宏、变量等,下面以一个实例来展开说明。

实例

这里以软中断被调用时的入口trace event为例,其定义如下:

DEFINE_EVENT(softirq, softirq_entry,

	TP_PROTO(unsigned int vec_nr),

	TP_ARGS(vec_nr)
);

这里的宏DEFINE_EVENT不过是前面DECLARE_TRACE宏的一个包装:

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

从上面的讨论可以知道,这里声明了一个名为__tracepoint_softirq_entrytracepoint类型结构体。而根据我们前面对宏的展开分析,trace_##name也就是这里展开的trace_softirq_entry是对这个trace event进行调用的入口,果然在__do_softirq函数中看到了它的身影:

// kernel/softirq.c
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
  // ...
  trace_softirq_entry(vec_nr);
  // ...
}

systemtap相关

这里需要注意的另一个问题是,每个systemtap中的kernel.trace当时可以知道的参数,除了trace event本身的参数之外,还有当时所在嵌入函数内部的变量,比如这里的softirq_entry这个probe,在systemtap对应的tapset中是这样的:

probe softirq.entry = kernel.trace("irq_softirq_entry") !,
     		      kernel.trace("softirq_entry") ?
{
	# kernels < 2.6.37
	h = @choose_defined($h, 0)
	vec = @choose_defined($vec, 0)
	action = (@defined($h) ? @cast($h,"softirq_action","kernel<linux/interrupt.h>")->action : 0)
	# kernels >= 2.6.37
	vec_nr = @choose_defined($vec_nr, 0)
}

这里可以的变量h类型是在内核中的头文件<linux/interrupt.h>中定义的softirq_action,因为这个变量就是在上面的函数__do_softirq中定义的:

// kernel/softirq.c
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
  // ...
  struct softirq_action *h;

  // ...
  trace_softirq_entry(vec_nr);
  // ...
}

所以,要看一个systemtap的kernel.trace能引用哪些变量,除了看其自身,还包括看其所嵌入函数的上下文中的变量,最好直接到对应的tapset的说明,因为-L只能打印出这个kernel.trace自身定义的变量:

$ sudo stap -L 'kernel.trace("softirq_entry")'
kernel.trace("irq:softirq_entry") $vec_nr:unsigned int

总结:

  • 内核中的trace事件以trace_*来命名。
  • 看到systemtap中的'kernel.trace("xx")',其对应的内核代码可以使用trace_xx来搜索,通过阅读这个trace事件所嵌入的代码也可以或者这个probe事件能打印的变量。

参考资料