在开始描述之前,我们先介绍下linux tracepoint是什么?以及为什么需要tracepoint?
TRACE_EVENT系列宏定义的展开基本上是笔者当前见过的最复杂的宏定义了,没有之一。
《linux tracepoint系列宏定义(TRACE_EVENT,DEFINE_TRACE等)展开过程》是一系列文章,其中的宏定义的替换展开以及#unset很#define对宏定义的重定义等一些列操作是很乏味的,但是如果你确实对tracepoint很感兴趣,那么需要多点耐心,毕竟将tracepoint理解清楚后对linux进行性能调优也很重要。如果没有内核中的这些tracepoint静态桩,linux内核性能分析将成为空中楼阁、
1、tracepoint概念
跟踪点(tracepoint)是放置在内核代码中较重要位置的硬编码检测点。例如,在系统调用、调度程序事件、文件系统操作和磁盘I/O的开始和结束处都有跟踪点。跟踪点基础设施由Mathieu Desnoyers开发,并于2009年在Linux 2.6.32版本中首次提供。跟踪点是一个稳定的接口,数量有限。跟踪点(tracepoint)是性能分析的重要资源,为高级跟踪工具提供了强大的支持,提供了对内核行为的深入了解。虽然基于功能的跟踪可以提供类似的功能(例如kprobes),但只有跟踪点提供稳定的接口,允许开发健壮的工具。
Linux tracepoint 是基于静态插桩的内核追踪机制,其核心原理可概括为以下要点:
1.1、静态插桩机制
预定义探测点:Tracepoint在内核源码编译阶段通过宏(如TRACE_EVENT)静态嵌入关键代码路径,形成不可移动的探测点。这些探测点类似于代码中的“标记”,例如系统调用入口、文件系统操作等关键位置。触发条件判断:每个tracepoint包含一个state标志位,执行到插桩点时内核会先检查该标志。若未启用(state=0),仅执行一个条件判断分支,性能损耗极低(约一个 CPU 周期);启用时(state=1)才会触发后续处理逻辑。
1.2、动态回调机制
Probe函数绑定:Tracepoint通过注册函数(如 register_trace_xxx)绑定一个或多个用户定义的probe函数。这些函数可实现数据采集、事件记录等功能,且支持运行时动态挂载或卸载。参数传递与数据处理:通过TP_PROTO和TP_ARGS宏定义参数传递协议,TP_STRUCT__entry和TP_fast_assign宏构建数据结构并填充数据,最终由TP_printk格式化输出到环形缓冲区(ring buffer)。
1.3、性能优化设计
零开销停用:未启用时仅保留一个条件判断语句,无内存占用或函数调用开销。多核安全处理:Tracepoint 数据结构通过aligned(32)确保缓存行对齐,避免多核环境下的伪共享问题。
2、tracepoint在展开过程中使用的技巧
本节的内容对于后面理解TRACE_EVENT等一系列宏定义的展开过程很关键。
看下面一段示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>#define PARAMS(args...) args#define fun(a,b,c,d,e,f,g) \
{ \printf("%d ",a); \printf("%d ",b); \printf("%d ",c); \printf("%s \n",d); \
}int main()
{fun(1,3,5,"char",TP_PRINT,TP_ASSIGN,TP_STR);printf("%d %d %d %s %s \r\n",PARAMS(1024,2048,4096,"hello","world"));return 0;
}
编译通过后,执行输出结果为:
1 5 3 char
1024 2048 4096 hello world
FUN这个宏定义中传入7个参数,但实际使用了4个参数,未使用的参数传递什么内容并不会影响程序的编译和执行且未使用的参数会被忽略。
在tracepoint.h中 TRACE_EVENT首次展开时就是如示例代码这样操作的,所以编译能够通过。
/*TRACE_EVENT宏定义,共计6个参数,在tracepoint.h中定义的TRACE_EVENT只使用了前三个参数*/
#define TRACE_EVENT(name, proto, args, struct, assign, print) \
DECLARE_TRACE(name, PARAMS(proto), PARAMS(args))
这个技巧在tracepoint的各种宏定义展开时频繁使用,也是tracepoint分阶段展开处理各个阶段参数的关键。
再看下面一段示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#define FUNC1(a,b,c) /*定义FUNC2时,FUNC1为空,展开后FUNC2是否应该为空呢?这里的关键点在于宏替换是在预处理阶段按顺序处理的。当FUNC2被定义时,它引用了当前的FUNC1。但随后FUNC1被取消定义并重新定义,所以当后续代码中调用FUNC2时,会使用最新的FUNC1定义。FUNC2 的展开结果取决于调用时 FUNC1 的当前定义。
*/
#define FUNC2(a,b,c) \FUNC1(a,b,c)#undef FUNC1
#define FUNC1(a,b,c) \printf("a=%d,b=%d,c=%d \r\n",a,b,c); int main(int argc,char *argv[])
{int a=1,b=2,c=3;FUNC2(a,b,c); return 0;
}
编译通过后,执行输出结果为:
a=1,b=2,c=3
FUNC2 的展开结果取决于调用时 FUNC1 的当前定义。这个技巧在tracepoint的各种宏定义展开时频繁使用。
3、static_key
static_key使得在tracepoint未开始时的开销非常小,就是执行一条nop执行。具体实现查看include/linux/jump_lable.h文件,后面计划对static_key推出文章进行讲解。
static __always_inline bool static_key_false(struct static_key *key){/*编译时设置branch参数为false,1:标签处为nop指令,运行时默认返回false。.pushsection和.popsection是编译时有效的汇编指令,用于控制代码和数据的节(Section)布局。所以运行时下面函数不会往__jump_table段中添加jump_entry数据。*/return arch_static_branch(key, false);}static __always_inline bool static_key_true(struct static_key *key){/*编译时设置branch参数为true,1:标签处为nop指令,运行时默认返回false,由于取反(!),本函数最终返回true。.pushsection和.popsection是编译时有效的汇编指令,用于控制代码和数据的节(Section)布局。所以运行时下面函数不会往__jump_table段中添加jump_entry数据。返回值取反。*/return !arch_static_branch(key, true);}
4、以trace-events-sample为例来分析tracepoint探测点实现原理
内核在sample/trace_events/trace-events-sample.c/h文件来展示tracepoint的使用。
后面会根据trace-events-sample.h文件来描述TRACE_EVENT系列宏定义在每个阶段展开的流程,在本系列文章的最后,会将race-events-sample.h文件经过预编译宏替换后的全部内容张贴出来,来佐证我们的描述是正确的。
