如何使用 TRACE_EVENT() 宏来创建跟踪点

一、简介

   在Linux的整个历史中,人们一直希望在内核中添加静态跟踪点(在内核中的特定位置记录数据以便稍后检索的函数)。由于担心跟踪点会牺牲性能,这些努力并不是很成功。与Ftrace函数跟踪程序不同,跟踪点可以记录的不仅仅是所输入的函数。跟踪点可以记录函数的局部变量。随着时间的推移,已经尝试了各种添加跟踪点的策略,并取得了不同的成功,TRACE_EVENT()宏是添加内核跟踪点的最新方法。

   

1. TRACE_EVENT() 宏的剖析

自动跟踪点具有各种必须满足的要求:

  • 它必须创建一个可以放在内核代码中的跟踪点。
  • 它必须创建一个可以挂接到此跟踪点的回调函数。
  • 回调函数必须能够以最快的方式将传递给它的数据记录到跟踪环缓冲区中。
  • 它必须创建一个函数,可以解析记录到环形缓冲区的数据,并将其转换为跟踪程序可以显示给用户的可读格式。

为此,TRACE_EVENT() 宏分为六个组件,它们对应于宏的参数:

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

  • name - 要创建的跟踪点的名称。
  • prototype - 跟踪点回调函数的原型。
  • args - 与函数原型匹配的参数。
  • struct - 跟踪点可以使用(并不是必须的)这个结构来存储传入到跟踪点的数据。
  • assign - 将数据分配给结构的类似于C的方法。
  • print - 以人类可读的ASCII格式输出结构的方法。

2. 举例

   这里可以找到 sched_switch 跟踪点定义的一个很好的例子。下面将使用该定义来描述 TRACE_EVENT() 宏的每个部分。

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

(1) Name

第一个参数是名称。
TRACE_EVENT(sched_switch,
这是用于调用此跟踪点的名称。实际使用的跟踪点的名称前面有trace_前缀(即trace_sched_switch)。

(2) 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);
它用作添加到内核代码的跟踪点和回调函数的原型。请记住,跟踪点调用回调函数,就好像在跟踪点的位置调用回调函数一样。

(3) Arguments

第三个参数是原型使用的参数。
TP_ARGS(rq, prev, next),
这可能看起来很奇怪,但它不仅是TRACE_EVENT() 宏所必需的,而且也是下面的跟踪点基础结构所必需的。跟踪点代码在激活时将调用回调函数(可以将多个回调分配给给定的跟踪点)。创建跟踪点的宏必须同时访问原型和参数。下面是跟踪点宏完成此操作所需的示例:

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

(4) 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 tracepoint使用的宏有:

  • __field(type, name) - 它定义了一个普通的结构元素,比如int var;其中type是int,name是var。
  • __array(type, name, len) - 这将定义一个数组项,相当于int name[len];其中type是int,数组的名称是array,数组中的项数是len。

还有其他元素宏将在后面的文章中介绍。来自sched_switch tracepoint的定义将生成一个如下所示的结构:

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;
};

Note that the spacing used in the TP_STRUCT__entry definition breaks the rules outlined by checkpatch.pl. That is done because these macros are not function-like but, instead, are used to define a structure. The spacing follows the rules of structure spacing and not of function spacing, so that the names line up in the structure declaration. Needless to say, checkpatch.pl fails horribly when processing changes to TRACE_EVENT() definitions.

(5) Assignment

第五个参数定义了将参数中的数据保存到环形缓冲区的方式。

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() 定义的参数的变量名来将适当的数据分配到该输入结构中。

(6) Print

最后一个参数定义如何使用 printk() 从 TP_STRUCT_uentry 结构中打印字段。

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)

变量 __entry 再次被用来引用包含数据的结构体的指针。Format string 就像其他 printf 的 format 一样。__print_flags() 是 TRACE_EVENT() 附带的一组帮助函数的一部分,将在另一篇文章中讨论。不要创建新的跟踪点特定的帮助程序,因为这会混淆用户空间工具,这些工具知道TRACE_EVENT() helper宏,但不知道如何处理为单个跟踪点创建的宏。

二、头文件

TRACE_EVENT()宏不能仅仅放置在期望它能与Ftrace或任何其他跟踪程序一起工作的任何位置。包含TRACE_EVENT()宏的头文件必须遵循特定格式。这些头文件通常位于include/trace/events目录中,但不需要。如果它们不在此目录中,则需要其他配置。

TRACE_EVENT()头中的第一行不是普通的ifdef_TRACE_SCHED_H,而是:

#undef TRACE_SYSTEM
#define TRACE_SYSTEM sched

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

此示例用于调度程序跟踪事件,其他事件头将使用除sched和trace_sched_H之外的其他内容。trace_HEADER_MULTI_READ test允许此文件包含多次;这对于trace_event()宏的处理非常重要。还必须为文件定义跟踪系统,并且跟踪系统必须在if的保护范围之外。TRACE_系统定义文件中TRACE_EVENT()宏所属的组。这也是事件将在debugfs tracing/events目录中分组的目录名。此分组对Ftrace很重要,因为它允许用户按组启用或禁用事件。

然后,该文件包含TRACE_EVENT()宏内容所需的任何头。(例如,include<linux/sched.h>)。tracepoint.h文件是必需的。

#include <linux/tracepoint.h>

现在可以使用trace_EVENT()宏定义所有跟踪事件。请在TRACE_EVENT()宏上方包含描述跟踪点的注释。以include/trace/events/sched.h为例。文件结尾为:

#endif /* _TRACE_SCHED_H */

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

define_trace.h是创建跟踪点的所有魔力所在。关于这个文件如何工作的解释将留给另一篇文章。现在,只需知道这个文件必须包含在跟踪头文件的底部,而不受endif的保护就足够了。

三、使用 tracepoint

如果跟踪点不在任何地方使用,那么定义它就没有意义。要使用跟踪点,必须包含跟踪头,但在包含跟踪之前,还必须定义一个C文件(并且只有一个)的创建跟踪点。这将导致define_trace.h创建生成跟踪事件所需的必要函数。在kernel/sched.c中定义了以下内容:

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

如果另一个文件需要使用跟踪文件中定义的跟踪点,则它只需要包含跟踪文件,而不需要定义创建跟踪点。为同一头文件定义多次将在生成时导致链接器错误。例如,在kernel/fork.c中,只包含头文件:

#include <trace/events/sched.h>

最后,在代码中使用跟踪点,就像在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;

四、实际使用

1. module_traceevent.h

#undef TRACE_SYSTEM
#define TRACE_SYSTEM xhr_test

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

#include <linux/tracepoint.h>

TRACE_EVENT(te_test,
    TP_PROTO(int num),
    TP_ARGS(num),
    TP_STRUCT__entry(
        __field(int, output)
        __field(int, count)
    ),
    TP_fast_assign(
        __entry->count++;
        __entry->output = num;
    ),
    TP_printk("count=%d output=%d",
        __entry->count, __entry->output)
);

#endif /* _TRACE_TE_TEST_H */
    
/* This part must be outside protection */
#undef TRACE_INCLUDE_PATH
#define TRACE_INCLUDE_PATH .
#define TRACE_INCLUDE_FILE module_traceevent

#include <trace/define_trace.h>

2. module_entry.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>

#define CREATE_TRACE_POINTS
#include "module_traceevent.h"

static int xhr_thread(void *arg)
{
    static unsigned long count;

    while (!kthread_should_stop())
    {
        set_current_state(TASK_INTERRUPTIBLE);
        schedule_timeout(HZ);
        trace_te_test(count);
        count++;
    }

    return 0;
}

static struct task_struct *xhr_task;

static __init int __module_init(void)
{
    printk("Hello, %s.\n", __func__);

    xhr_task = kthread_run(xhr_thread, NULL, "xhr-thread");
    if (IS_ERR(xhr_task))
        return -1;

    return 0;
}
static __exit void __module_exit(void)
{
    kthread_stop(xhr_task);
    printk("Hello, %s.\n", __func__);
    return;
}

module_init(__module_init);
module_exit(__module_exit);
MODULE_LICENSE("GPL");

3. Makefile

INCLUDES = -I. -I$(KDIR)/include
MODULE := xhr
obj-m := $(MODULE).o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

CFLAGS_module_entry.o = -I$(src)

.PHONY: all
all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

.PHONY: clean
clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

$(MODULE)-objs += module_entry.o

4. user space

sudo -s
insmod xhr.ko
cd /sys/kernel/debug/tracing/
echo 1 > tracing_on
echo 1 > events/xhr_test/te_test/enable
cat trace

output:
# entries-in-buffer/entries-written: 8/8   #P:8
#
#                              _-----=> irqs-off
#                             / _----=> need-resched
#                            | / _---=> hardirq/softirq
#                            || / _--=> preempt-depth
#                            ||| /     delay
#           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
#              | |       |   ||||       |         |
      xhr-thread-3732  [000] ....  1739.034184: te_test: count=1 output=48
      xhr-thread-3732  [000] ....  1740.058510: te_test: count=1 output=49
      xhr-thread-3732  [000] ....  1741.081934: te_test: count=1 output=50
      xhr-thread-3732  [000] ....  1742.106095: te_test: count=1 output=51

参考链接

Using the TRACE_EVENT() macro (Part 1)
Using the TRACE_EVENT() macro (Part 3)

  • 7
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是使用`__FILE__`和`__LINE__`宏来记录调用日志的代码位置的示例代码: ```c++ #include <iostream> #include <spdlog/spdlog.h> #include <spdlog/sinks/basic_file_sink.h> #include <spdlog/sinks/stdout_color_sinks.h> int main() { // 创建控制台和文件输出的日志记录器 auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>(); auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("example.log", true); // 设置日志颜色 console_sink->set_color(spdlog::level::trace, spdlog::color_type::white); console_sink->set_color(spdlog::level::debug, spdlog::color_type::blue); console_sink->set_color(spdlog::level::info, spdlog::color_type::green); console_sink->set_color(spdlog::level::warn, spdlog::color_type::yellow); console_sink->set_color(spdlog::level::err, spdlog::color_type::red); console_sink->set_color(spdlog::level::critical, spdlog::color_type::bold_red); // 创建多个日志记录器,每个记录器都可以设置不同的日志过滤级别和输出目标 auto logger1 = std::make_shared<spdlog::logger>("example", spdlog::sinks_init_list{console_sink, file_sink}); logger1->set_level(spdlog::level::trace); logger1->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [thread %t] %v"); auto logger2 = std::make_shared<spdlog::logger>("example2", spdlog::sinks_init_list{console_sink}); logger2->set_level(spdlog::level::info); logger2->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] %v"); // 记录日志并包含代码位置信息 logger1->trace("{}:{} trace message from logger1", __FILE__, __LINE__); logger1->debug("{}:{} debug message from logger1", __FILE__, __LINE__); logger1->info("{}:{} info message from logger1", __FILE__, __LINE__); logger1->warn("{}:{} warn message from logger1", __FILE__, __LINE__); logger1->error("{}:{} error message from logger1", __FILE__, __LINE__); logger1->critical("{}:{} critical message from logger1", __FILE__, __LINE__); logger2->info("{}:{} info message from logger2", __FILE__, __LINE__); // 关闭日志记录器前,需要刷新所有的日志输出 spdlog::shutdown(); return 0; } ``` 这个示例代码在记录日志时使用了`__FILE__`和`__LINE__`宏来获取代码位置信息,并将位置信息作为格式化字符串的一部分。这样,当日志被记录时,日志中就会包含调用日志的代码位置信息。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值