前言:最近在摸索Node.js调试诊断的内容,因为Node.js提供的能力有时候可能解决不了问题,比如堆内存不行变化,但rss不断上升。因此,我们需要更深入地挖掘以了解更多解决问题的方法。而这些方向往往会涉及到底层的东西,自然要了解内核提供的一些技术和能力。经过多年的发展,可谓是百花齐放,十分复杂。本文简单分享一下内核静态跟踪技术的实现。跟踪,其实就是在代码执行的时候收集一些信息,以辅助故障排除。1TracepointsTracepoints是一种静态检测技术。虽然实现起来很复杂,但概念相对简单。比如我们在登录的时候,类似这种情况。在业务代码中,我们写了很多日志来记录进程运行时的信息。Tracepoints是内核提供的一种基于钩子的检测技术。但是,与日志记录不同的是,我们可以在任何地方添加相应的代码,而Tracepoints几乎取决于内核来决定在何处插入存根,几乎是因为我们也可以编写内核模块并将其注册到内核中。通知检测点。下面通过一个例子来看看Tracepoint的使用和实现(例子来自内核文档tracepoints.rst)。在分析之前,我们先来看看两个非常重要的宏。第一个是DECLARE_TRACE。#defineDECLARE_TRACE(name,proto,args)\__DECLARE_TRACE(name,PARAMS(proto),PARAMS(args),\cpu_online(raw_smp_processor_id()),\PARAMS(void*__data,proto),\PARAMS(__data,args))我们只需要关注主体的实现,不用关注参数,继续扩展。#define__DECLARE_TRACE(name,proto,args,cond,data_proto,data_args)\externstructtracepoint__tracepoint_##name;\//执行钩子函数staticinlinevoidtrace_##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);\}\//注册钩子函数staticinlineint\register_trace_##name(void(*probe)(data_proto),void*data)\{\returntracepoint_probe_register(&__tracepoint_##name,\(void*)probe,data);\}\//注销钩子函数staticinlineint\unregister_trace_##name(void(*probe)(data_proto),void*data)\{\returntracepoint_probe_unregister(&__tracepoint_##name,\(void*)probe,data);\}\staticinlinebool\trace_##name##_enabled(void)\{\returnstatic_key_false(&__tracepoint_##name.key);\}__DECLARE_TRACE主要实现了几个函数,我们只需要关注注册钩子和执行钩子函数即可(格式为register_trace_${yourname}和trace_${yourame})。接下来看第二个宏DEFINE_TRACE。#defineDEFINE_TRACE_FN(名称,reg,unreg)\structtracepoint__tracepoint_##name#defineDEFINE_TRACE(名称)\DEFINE_TRACE_FN(名称,NULL,NULL);我省略了一些代码,DEFINE_TRACE主要定义了一个tracepoint结构体。现在您知道了这两个宏,让我们看看如何使用Tracepoint。1.1使用include/trace/events/subsys.h#includeDECLARE_TRACE(subsys_eventname,TP_PROTO(intfirstarg,structtask_struct*p),TP_ARGS(firstarg,p));首先通过头文件中的DECLARE_TRACE宏定义一系列函数。subsys/file.c#includeDEFINE_TRACE(subsys_eventname);voidsomefct(void){...trace_subsys_eventname(arg,task);...}//实现自己的钩子函数并注册到内核voidcallback(...){}register_trace_subsys_eventname(callback);然后在实现文件中通过DEFINE_TRACE定义一个tracepoint结构。然后调用register_trace_subsys_eventname函数将自定义的hook函数注册到内核,然后在需要收集信息的地方调用处理hook的函数trace_subsys_eventname。1.2实现了解了使用之后,我们来看一下实现。先看注册钩子函数。inttracepoint_probe_register(structtracepoint*tp,void*probe,void*data){returntracepoint_probe_register_prio(tp,probe,data,TRACEPOINT_DEFAULT_PRIO);}inttracepoint_probe_register_prio(structtracepoint*tp,void*probe,void*data,intpriolock){mecttracet_funte(cuttracet_funtemut_ex;&trace);tp_func.func=probe;tp_func.data=data;tp_func.prio=prio;ret=tracepoint_add_func(tp,&tp_func,prio);mutex_unlock(&tracepoints_mutex);returnret;}tracepoint_probe_register_prio定义了一个结构体,用于tracepoint_fun表示hook信息,以及然后调用tracepoint_add_func,其中tp是刚刚自定义的tracepoint结构体。staticinttracepoint_add_func(structtracepoint_func*func,intprio){structtracepoint_func*old,*tp_funcs;intret;//获取钩子列表(&tp_funcs,func,prio);rcu_assign_pointer(tp->funcs,tp_funcs);return0;}staticstructtracepoint_func*func_add(structtracepoint_func**funcs,structtracepoint_func*tp_func,intprio){structbetracepoint=nurc_pro*=-1;/*+2:onefornewprobe,oneforNULLfunc*/new=allocate_probes(nr_probes+2);pos=0;new[pos]=*tp_func;new[nr_probes+1].func=NULL;*funcs=new;}注册函数逻辑其实就是往自定义结构的队列中插入一个新节点。接下来我们看一下处理hooks的逻辑。#define__DO_TRACE(tp,proto,args,cond,rcuidle)\do{\structtracepoint_func*it_func_ptr;\void*it_func;\void*__data;\int__maybe_unused__idx=0;\//获取队列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)在逻辑上类似于我们的应用层。在执行hook的时候,也就是我们的回调,我们可以通过内核接口向ringbuffer写入信息,然后应用层可以通过debugfs获取这些信息。2、trace事件有了Tracepoint机制后,我们就可以编写模块加载到内核中,实现自己的存根点。但是内核也为我们提供了很多内置的插入点。具体是通过trace事件来实现的。让我们看一个例子。#defineTRACE_EVENT(name,proto,args,struct,assign,print)\DECLARE_TRACE(name,PARAMS(proto),PARAMS(args))TRACE_EVENT(consume_skb,TP_PROTO(structsk_buff*skb),TP_ARGS(skb),TP_STRUCT__entry(__field(void*,skbaddr)),TP_fast_assign(__entry->skbaddr=skb;),TP_printk("skbaddr=%p",__entry->skbaddr));上面定义了一个宏TRACE_EVENT,本质上是对DECLARE_TRACE的封装,所以这里定义了一系列函数(注册钩子,处理钩子)。然后在consume_skb函数中处理注册的钩子。voidconsume_skb(structsk_buff*skb){trace_consume_skb(skb);__kfree_skb(skb);}3.总结内核提供了非常丰富但也非常复杂的机制,让用户可以通过内核的能力获取底层数据,进行问题排查和性能优化。我们可以看到存根插入机制是一个静态机制。我们通常需要依赖当前版本内核支持的存根来获取相应的信息。不过内核也提供了动态跟踪能力,可以实现热插拔获取信息的能力。总的来说,Linux下有各种跟踪技术。虽然它们很复杂,但是上层也提供了各种更方便的工具。这些功能是我们进行深入故障排除的强大工具。