使用TRACE_EVENT宏添加Tracepoint(1/3部分)

Posted X-Solomon-X

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用TRACE_EVENT宏添加Tracepoint(1/3部分)相关的知识,希望对你有一定的参考价值。

前言:

”TRACE_EVENT()宏专门用于允许开发人员将tracepoint(追踪点)添加到他们的子系统上,并且Ftrace可以自动跟踪它们。开发人员不需要了解Ftrace是如何开展工作的,他们只需要使用TRACE_EVENT()宏创建他们的跟踪点。另外,他们需要遵循一些关于如何创建头文件的指南,他们将获得对Frace跟踪器的完全访问权限。TRACE_EVENT()宏的另外一个设计目标是不将其耦合到Frace或者任何其他的跟踪器。它与使用它的跟踪器无关,现在,很明显TRACE_EVENT()宏也被perf,LTTng和System Tap使用。“

TRACE_EVENT()宏剖析:

要实现自动化追踪点功能必须满足以下要求:

  • 它必须创建一个可以放置在内核代码中的追踪点。
  • 它必须创建一个可以挂载到此追踪点的回调函数。
  • 回调函数必须能够以最快的方式它的数据记录到追踪器的环形缓冲区中。
  • 它必须创建一个函数来解析记录到环形缓冲区的数据,并且将其转换为追踪器可以向用户显示的ASCII码格式。

为了实现这一点,TRACE_EVENT()宏将分为六个组件,它们对应于宏的参数:

TRACE_EVENT(name, proto, args, struct, assign, print)

  • name - 要创建的追踪点的名称。
  • prototype - 追踪点回调函数的参数原型。
  • args - 与参数原型匹配的参数。
  • struct - 追踪器可以使用(但不是必须)来存储传递到追踪点的数据结构。
  • assign - 将args参数的数据赋值给struct结构。
  • print - 以人类可读的ASCII格式输出。

以下是追踪点定义的很好的例子,下面将使用该定义来描述TRACE_EVENT()宏的每个部分。

TRACE_EVENT(sched_switch,

	TP_PROTO(struct rq *rq, struct task_struct *prev,
		 struct task_struct *next),

	TP_ARGS(rq, prev, next),

	TP_STRUCT__entry(
		__array(	char,	prev_comm,	TASK_COMM_LEN	)
		__field(	pid_t,	prev_pid			)
		__field(	int,	prev_prio			)
		__field(	long,	prev_state			)
		__array(	char,	next_comm,	TASK_COMM_LEN	)
		__field(	pid_t,	next_pid			)
		__field(	int,	next_prio			)
	),

	TP_fast_assign(
		memcpy(__entry->next_comm, next->comm, TASK_COMM_LEN);
		__entry->prev_pid	= prev->pid;
		__entry->prev_prio	= prev->prio;
		__entry->prev_state	= prev->state;
		memcpy(__entry->prev_comm, prev->comm, TASK_COMM_LEN);
		__entry->next_pid	= next->pid;
		__entry->next_prio	= next->prio;
	),

	TP_printk("prev_comm=%s prev_pid=%d prev_prio=%d prev_state=%s ==> next_comm=%s next_pid=%d next_prio=%d",
		__entry->prev_comm, __entry->prev_pid, __entry->prev_prio,
		__entry->prev_state ?
		  __print_flags(__entry->prev_state, "|",
				 1, "S" ,  2, "D" ,  4, "T" ,  8, "t" ,
				 16, "Z" ,  32, "X" ,  64, "x" ,
				 128, "W" ) : "R",
		__entry->next_comm, __entry->next_pid, __entry->next_prio)
   );

除了第一个name参数以外,所有的参数都封装在另外一个之中(TP_PROTO、TP_ARGS、TP_STRUCT__entry、TP_fast_assign 和 TP_printk)。这些宏提供了更多的处理控制,还允许在TRACE_EVENT()宏之中使用逗号。

Name

TRACE_EVENT()宏的第一个参数是name。

TRACE_EVENT(sched_switch,

这是用于调用此追踪点的名称,实际使用的追踪点在name前加上trace_前缀,即trace_sched_switch,比如在进程切换前想插入一个追踪点,追踪进程切换的信息:

#kernel/sched/core.c
static void __sched notrace __schedule(bool preempt)

        struct task_struct *prev, *next;
        unsigned long *switch_count;
        struct rq_flags rf;
        struct rq *rq;
        int cpu;

        cpu = smp_processor_id();

        ...

        
        #到这里插入一个追踪点,每次发生进程切换,就打印信息。

                rq = context_switch(rq, prev, next, &rf);
         else 
                rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
                rq_unlock_irq(rq, &rf);
        

        balance_callback(rq);

在每次进行进程的上下问切换的时候,插入trace_sched_switch追踪点,记录每次进程切换的信息。

#kernel/sched/core.c
static void __sched notrace __schedule(bool preempt)

        struct task_struct *prev, *next;
        unsigned long *switch_count;
        struct rq_flags rf;
        struct rq *rq;
        int cpu;

        cpu = smp_processor_id();

        ...

        
                ++*switch_count;

                trace_sched_switch(preempt, prev, next);

                rq = context_switch(rq, prev, next, &rf);
         else 
                rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
                rq_unlock_irq(rq, &rf);
        

        balance_callback(rq);

Prototype

TRACE_EVENT()宏的第二个参数是prototype,编写prototype其实就是申明追踪点的形参,

TP_PROTO(struct rq *rq, struct task_struct *prev, struct task_struct *next),

其效果类似于:

trace_sched_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next);

Arguments

TRACE_EVENT()宏的第三个参数是arguments,arguments是prototype使用的参数。

    TP_ARGS(rq, prev, next),

其效果类似于:

    #define TRACE_POINT(name, proto, args) \\
       void trace_##name(proto)            \\
                                          \\
               if (trace_##name##_active)  \\
                       callback(args);     \\
       

Structure

TRACE_EVENT()宏的第四个参数是structure,此参数描述将存储在追踪点环形缓冲区中的数据结构布局。数据结构的每一个元素都由其他的宏定义。这些宏用于自动创建数据结构,每个宏之间没有被任何的分隔符所分隔。

    TP_STRUCT__entry(
		__array(	char,	prev_comm,	TASK_COMM_LEN	)
		__field(	pid_t,	prev_pid			)
		__field(	int,	prev_prio			)
		__field(	long,	prev_state			)
		__array(	char,	next_comm,	TASK_COMM_LEN	)
		__field(	pid_t,	next_pid			)
		__field(	int,	next_prio			)
    ),

sched_switch追踪点只用了两种类型的宏:

  • __field(type, name) - 定义了一个名为name的type类型的变量。
  • __array(type, name, len) - 定义了一个名为name的数组,type类型,长度为len。

其效果类似于:

    struct 
	      char   prev_comm[TASK_COMM_LEN];
	      pid_t  prev_pid;
	      int    prev_prio;
	      long   prev_state;
	      char   next_comm[TASK_COMM_LEN];
	      pid_t  next_pid;
	      int    next_prio;
    ;

注:TP_STRUCT__entry宏间距违反了checkpatch.pl脚本的规则,因为TP_STRUCT__entry宏遵循的是结构间距的规则,而不是函数间距的规则。

Assignment

TRACE_EVENT()宏的第五个参数是Assignment,定义了将argument参数中的数据赋值到structure追踪点环形缓冲区数据结构中的方法。

    TP_fast_assign(
		memcpy(__entry->next_comm, next->comm, TASK_COMM_LEN);
		__entry->prev_pid	= prev->pid;
		__entry->prev_prio	= prev->prio;
		__entry->prev_state	= prev->state;
		memcpy(__entry->prev_comm, prev->comm, TASK_COMM_LEN);
		__entry->next_pid	= next->pid;
		__entry->next_prio	= next->prio;
    ),

TP_fast_assign宏中的代码是普通的C语言代码。__entry这个特殊的变量指向了TP_STRUCT__entry定义的数据结构的指针,并直接指向环形缓冲区。TP_fast_assign用于填充在TP_STRUCT_entry创建的数据结构中的所有字段。可以使用由TP_PROTO和TP_ARGS定义的参数的变量名称将数据赋值到__entry变量中。

Print

TRACE_EVENT()宏的最后一个参数是print,print定义了如何将structure环形缓冲区的内容以格式化的形式打印出来。

TP_printk("prev_comm=%s prev_pid=%d prev_prio=%d prev_state=%s ==> " \\
 		  "next_comm=%s next_pid=%d next_prio=%d",
		__entry->prev_comm, __entry->prev_pid, __entry->prev_prio,
		__entry->prev_state ?
		  __print_flags(__entry->prev_state, "|",
				 1, "S" ,  2, "D" ,  4, "T" ,  8, "t" ,
				 16, "Z" ,  32, "X" ,  64, "x" ,
				 128, "W" ) : "R",
		__entry->next_comm, __entry->next_pid, __entry->next_prio)

经过了assignment宏对__entry所指向的数据结构的每个字段进行赋值后,print宏利用__entry指向的数据以TP_printk定义的格式进行输出,如同任何其他printf格式一样。__print_flags()宏是TRACE_EVENT()宏附带的一组辅助函数的一部分,将在另外一篇文章中介绍。

Format file

这个利用TRACE_EVENT()宏创建的sched_switch追踪点产生如下格式文件于:/sys/kernel/debug/tracing/events/sched/sched_switch/format

   name: sched_switch
   ID: 33
   format:
	field:unsigned short common_type;	offset:0;	size:2;
	field:unsigned char common_flags;	offset:2;	size:1;
	field:unsigned char common_preempt_count;	offset:3;	size:1;
	field:int common_pid;	offset:4;	size:4;
	field:int common_lock_depth;	offset:8;	size:4;

	field:char prev_comm[TASK_COMM_LEN];	offset:12;	size:16;
	field:pid_t prev_pid;	offset:28;	size:4;
	field:int prev_prio;	offset:32;	size:4;
	field:long prev_state;	offset:40;	size:8;
	field:char next_comm[TASK_COMM_LEN];	offset:48;	size:16;
	field:pid_t next_pid;	offset:64;	size:4;
	field:int next_prio;	offset:68;	size:4;

   print fmt: "task %s:%d [%d] (%s) ==> %s:%d [%d]", REC->prev_comm, REC->prev_pid,
   REC->prev_prio, REC->prev_state ? __print_flags(REC->prev_state, "|",  1, "S" ,
    2, "D" ,  4, "T" ,  8, "t" ,  16, "Z" ,  32, "X" ,  64, "x" ,  128,
   "W" ) : "R", REC->next_comm, REC->next_pid, REC->next_prio

注意:__entry在输出格式中被替换成了REC,并且,第一组字段(common_*)不是来自TRACE_EVENT()宏,是由创建此"format"格式文件的Ftrace工具添加到所有的追踪点中,其他的追踪器(如perf)可以添加不同的字段。格式文件提供了为用户空间工具提供解析sched_switch追踪点输出二进制的信息。

TRACE_EVENT宏在哪个头文件下进行定义

参考这个包含sched_switch追踪点的头文件定义:

https://github.com/torvalds/linux/blob/master/include/trace/events/sched.hhttps://github.com/torvalds/linux/blob/master/include/trace/events/sched.h包含TRACE_EVENT()宏的头文件必须位于include/trace/events目录中,并且头文件的首行必须申明:

   #undef TRACE_SYSTEM
   #define TRACE_SYSTEM sched

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

其中,"#define TRACE_SYSTEM sched"起到了这样的效果,TRACE_SYSTEM定义了头文件中的TRACE_EVENT()宏属于哪个组,同时也表明了TRACE_EVENT()宏中定义的追逐点属于"/sys/kernel/debug/tracing/events/"目录下的哪个子目录,这种分组决定了用户按组启用或者禁用追踪点。

[root@VM-centos tkernel4]# perf list "sched_switch"

List of pre-defined events (to be used in -e):


  sched:sched_switch                                 [Tracepoint event]


Metric Groups:


[root@VM-centos tkernel4]# ls /sys/kernel/debug/tracing/events/sched/
enable  sched_switch     

可以看到sched_switch追踪点前面有个前缀"sched"。以及相应的"debug/tracing/events/"目录下多了一个子目录"sched",TRACE_SYSTEM必须在#if的保护之外。其他的追踪点的头文件必须使用除"sched"和"_TRACE_SCHED_H"之外,其他的内容。TRACE_HEADER_MULTI_READ测试允许多次包含此文件。

随后,将TRACE_EVENT()宏的内容所需要的任何头文件包含进来:

#include <linux/sched/numa_balancing.h>
#include <linux/tracepoint.h>

现在可以使用TRACE_EVENT()宏定义的追踪点,并且,在TRACE_EVENT()宏定义上方做好注释。

最后,以下面的宏结束整个头文件的定义。

   #endif /* _TRACE_SCHED_H */

   /* This part must be outside protection */
   #include <trace/define_trace.h>

其中,"define_trace.h"文件是创建追踪点的函数所在,关于该文件如何展开工作的解释留在下一篇文章,这个文件必须包含在#endif的保护之外,并且位于头文件的底部。

TRACE_EVENT宏创建的追踪点如何使用

要在C文件中插入追踪点,必须包含追踪头文件(trace/events/sched.h),但C文件(并且只有一个)还必须在追踪头文件之前定义CREATE_TRACE_POINTS,这将导致define_trace.h创建跟踪点所必须的必要函数。在 kernel/sched.c 中定义了以下内容:

   #define CREATE_TRACE_POINTS
   #include <trace/events/sched.h>

如果在另外一个文件中也需要插入相同的追踪点,那么只需要包含这个追踪头文件(trace/events/sched.h)就可以了,不需要定义 CREATE_TRACE_POINTS。 为同一个头文件多次定义 CREATE_TRACE_POINTS会在编译时导致链接器错误。

最后,跟踪点在代码中的使用就像它在 TRACE_EVENT() 宏中定义的那样:

   static inline void
   context_switch(struct rq *rq, struct task_struct *prev,
	          struct task_struct *next)
   
	   struct mm_struct *mm, *oldmm;

	   prepare_task_switch(rq, prev, next);
	   trace_sched_switch(rq, prev, next);
	   mm = next->mm;
	   oldmm = prev->active_mm;

参考资料:

Using the TRACE_EVENT() macro (Part 1) [LWN.net]https://lwn.net/Articles/379903/Using the TRACE_EVENT() macro (Part 2) [LWN.net]https://lwn.net/Articles/381064/Using the TRACE_EVENT() macro (Part 3) [LWN.net]https://lwn.net/Articles/383362/https://github.com/torvalds/linux/tree/master/include/trace/eventshttps://github.com/torvalds/linux/tree/master/include/trace/events

以上是关于使用TRACE_EVENT宏添加Tracepoint(1/3部分)的主要内容,如果未能解决你的问题,请参考以下文章

使用TRACE_EVENT宏添加Tracepoint(1/3部分)

使用TRACE_EVENT宏添加Tracepoint(1/3部分)

宏中未使用的参数会怎样?

使用trace_event跟踪进程的一生

吸引住妹子的trace_event技术

keil5里面需要添加宏定义么