文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Linux 内核静态追踪技术的实现

2024-12-02 17:51

关注

前言:最近在探索 Node.js 调试和诊断方向的内容,因为 Node.js 提供的能力有时候可能无法解决问题,比如堆内存没有变化,但是 rss 一直上涨。所以需要深入一点去了解更多的排查问题方式。而这些方向往往都涉及到底层的东西,所以就自然需要去了解内核提供的一些技术,内核提供的能力,经过多年的发展,可谓是百花齐放,而且非常复杂。本文简单分享一下内核的静态追踪技术的实现。追踪,其实就是收集代码在执行时的一些信息,以便协助排查问题。

1 Tracepoint

Tracepoints 是一种静态插桩的技术,实现虽然复杂,但是概念上比较简单。比如我们打日志的时候,就类似这种情况,我们在业务代码里,写了很多 log 用来记录进程在运行时的信息。Tracepoints 则是内核提供的一种基于钩子的插桩技术。不过和打日志不一样的是,我们想在哪里打就在哪里加对应的代码,而 Tracepoints 则几乎是依赖于内核决定哪里可以插桩,说几乎是因为我们也可以写内核模块注册到内核来通知插桩点。下面来通过一个例子看一下 Tracepoint 的使用和实现(例子来自内核文档 tracepoints.rst)。分析之前先看一下两个非常重要的宏。第一个是 DECLARE_TRACE。

  1. #define DECLARE_TRACE(name, proto, args)                \ 
  2.     __DECLARE_TRACE(name, PARAMS(proto), PARAMS(args),      \ 
  3.             cpu_online(raw_smp_processor_id()),     \ 
  4.             PARAMS(void *__data, proto),            \ 
  5.             PARAMS(__data, args)) 

我们只需要关注主体的实现,而不需要关注参数,继续展开。

  1. #define __DECLARE_TRACE(name, proto, args, cond, data_proto, data_args) \ 
  2.     extern struct tracepoint __tracepoint_##name;           \ 
  3.     // 执行钩子函数 
  4.     static inline void trace_##name(proto)              \ 
  5.     {                               \ 
  6.         if (static_key_false(&__tracepoint_##name.key))     \ 
  7.             __DO_TRACE(&__tracepoint_##name,        \ 
  8.                 TP_PROTO(data_proto),           \ 
  9.                 TP_ARGS(data_args),         \ 
  10.                 TP_CONDITION(cond), 0);         \ 
  11.     }                               \ 
  12.     // 注册钩子函数 
  13.     static inline int                       \ 
  14.     register_trace_##name(void (*probe)(data_proto), void *data)    \ 
  15.     {                               \ 
  16.         return tracepoint_probe_register(&__tracepoint_##name,  \ 
  17.                         (void *)probe, data);   \ 
  18.     }                               \    
  19.     // 注销钩子函数                    
  20.     static inline int                       \ 
  21.     unregister_trace_##name(void (*probe)(data_proto), void *data)  \ 
  22.     {                               \ 
  23.         return tracepoint_probe_unregister(&__tracepoint_##name,\ 
  24.                         (void *)probe, data);   \ 
  25.     }                               \ 
  26.     static inline bool                      \ 
  27.     trace_##name##_enabled(void)                    \ 
  28.     {                               \ 
  29.         return static_key_false(&__tracepoint_##name.key);  \ 
  30.     } 

__DECLARE_TRACE 主要是实现了几个函数,我们只需要关注注册钩子和执行钩子函数(格式是 register_trace_${yourname} 和 trace_${yourame})。接下来看第二个宏 DEFINE_TRACE。

  1. #define DEFINE_TRACE_FN(name, reg, unreg)                \ 
  2.     struct tracepoint __tracepoint_##name#define DEFINE_TRACE(name)                      \ 
  3.     DEFINE_TRACE_FN(nameNULLNULL); 

我省略了一些代码,DEFINE_TRACE 主要是定义了一个 tracepoint 结构体。了解了两个宏之后,来看一下如何使用 Tracepoint。

1.1 使用

include/trace/events/subsys.h

  1. #include DECLARE_TRACE(subsys_eventname, 
  2.     TP_PROTO(int firstarg, struct task_struct *p), 
  3.     TP_ARGS(firstarg, p)); 

首先在头文件里通过 DECLARE_TRACE 宏定义了一系列函数。subsys/file.c

  1. #include  
  2.  
  3.  
  4.  
  5. DEFINE_TRACE(subsys_eventname);void somefct(void){ 
  6.  
  7.     ... 
  8.     trace_subsys_eventname(arg, task); 
  9.     ... 
  10.  
  11.  
  12.  
  13.  
  14. // 实现自己的钩子函数并注册到内核 
  15.  
  16. void callback(...) {} 
  17.  
  18. register_trace_subsys_eventname(callback); 

然后在实现文件里通过 DEFINE_TRACE 定义一个 tracepoint 结构体。接着调用 register_trace_subsys_eventname 函数把自定义的钩子函数注册到内核,然后在需要收集信息的地方调用处理钩子的函数 trace_subsys_eventname。

1.2 实现

了解了使用之后,接下来看看实现。首先看一下注册钩子函数。

  1. int tracepoint_probe_register(struct tracepoint *tp, void *probe, void *data){ 
  2.     return tracepoint_probe_register_prio(tp, probe, data, TRACEPOINT_DEFAULT_PRIO); 
  3.  
  4.  
  5.  
  6.  
  7. int tracepoint_probe_register_prio(struct tracepoint *tp, void *probe, 
  8.  
  9.                    void *data, int prio){ 
  10.     struct tracepoint_func tp_func; 
  11.     int ret; 
  12.  
  13.     mutex_lock(&tracepoints_mutex); 
  14.     tp_func.func = probe; 
  15.     tp_func.data = data; 
  16.     tp_func.prio = prio; 
  17.     ret = tracepoint_add_func(tp, &tp_func, prio); 
  18.     mutex_unlock(&tracepoints_mutex); 
  19.     return ret; 
  20.  

tracepoint_probe_register_prio 中定义了一个 tracepoint_func 结构体用于表示钩子信息,然后调用 tracepoint_add_func,其中 tp 就刚才自定义的 tracepoint 结构体。

  1. static int tracepoint_add_func(struct tracepoint *tp, struct tracepoint_func *func, int prio){ 
  2.     struct tracepoint_func *old, *tp_funcs; 
  3.     int ret; 
  4.     // 拿到钩子列表 
  5.     tp_funcs = rcu_dereference_protected(tp->funcs, lockdep_is_held(&tracepoints_mutex)); 
  6.     // 插入新的钩子到列表 
  7.     old = func_add(&tp_funcs, func, prio); 
  8.     rcu_assign_pointer(tp->funcs, tp_funcs); 
  9.     return 0;}static struct tracepoint_func * func_add(struct tracepoint_func **funcs, struct tracepoint_func *tp_func, 
  10.      int prio){ 
  11.     struct tracepoint_func *new; 
  12.     int nr_probes = 0; 
  13.     int pos = -1; 
  14.      
  15.     new = allocate_probes(nr_probes + 2); 
  16.     pos = 0; 
  17.     new[pos] = *tp_func; 
  18.     new[nr_probes + 1].func = NULL
  19.     *funcs = new; 
  20.  

注册函数的逻辑其实就是往自定义的结构体的队列里插入一个新的节点。接下来再看一下处理钩子的逻辑。

  1. #define __DO_TRACE(tp, proto, args, cond, rcuidle)          \ 
  2.     do {                                \ 
  3.         struct tracepoint_func *it_func_ptr;            \ 
  4.         void *it_func;                      \ 
  5.         void *__data;                       \ 
  6.         int __maybe_unused __idx = 0;               \ 
  7.         // 拿到队列 
  8.         it_func_ptr = rcu_dereference_raw((tp)->funcs);     \ 
  9.         // 非空则执行里面的节点的回调 
  10.         if (it_func_ptr) {                  \ 
  11.             do {                        \ 
  12.                 it_func = (it_func_ptr)->func;      \ 
  13.                 __data = (it_func_ptr)->data;       \ 
  14.                 ((void(*)(proto))(it_func))(args);  \ 
  15.             } while ((++it_func_ptr)->func);        \ 
  16.         }                           \ 
  17.     } while (0) 

逻辑上和我们在应用层的类似。在执行钩子,也就是我们的回调时,我们可以通过内核接口把信息写到 ring buffer,然后应用层可以通过 debugfs 获取到这个信息。

2 trace event

有了 Tracepoint 机制后,我们就可以写模块加载到内核中实现自己的插桩点。但是内核也为我们内置提供了非常多的插桩点。具体是通过 trace event 来实现的。下面看一个例子。

  1. #define TRACE_EVENT(name, proto, args, struct, assign, print)   \ 
  2.     DECLARE_TRACE(name, PARAMS(proto), PARAMS(args))TRACE_EVENT(consume_skb, 
  3.  
  4.     TP_PROTO(struct sk_buff *skb), 
  5.  
  6.     TP_ARGS(skb), 
  7.  
  8.     TP_STRUCT__entry( 
  9.         __field(    void *, skbaddr ) 
  10.     ), 
  11.  
  12.     TP_fast_assign( 
  13.         __entry->skbaddr = skb; 
  14.     ), 
  15.  
  16.     TP_printk("skbaddr=%p", __entry->skbaddr)); 

上面定义了一个宏 TRACE_EVENT,它本质上是对 DECLARE_TRACE 的封装,所以这里是定义了一系列的函数(注册钩子、处理钩子)。然后在 consume_skb 函数中处理了注册的钩子。

  1. void consume_skb(struct sk_buff *skb){ 
  2.     trace_consume_skb(skb); 
  3.     __kfree_skb(skb); 
  4.  

3. 总结

内核提供了非常丰富但是也非常复杂的机制,从而用户可以通过内核的能力获取到更底层的数据,用以排查问题和做性能优化。我们可以看到插桩的这种机制是一种静态的机制,我们通常需要依赖当前版本的内核所支持的桩,从而获得对应的信息,但其实内核也提供了动态追踪的能力,可以实现热插拔获取信息的能力。总的来说,Linux 下的追踪技术多种多样,虽然非常复杂,但是上层也提供了各种更方便的工具,这些能力是我们深入排查问题的利器。

 

来源:编程杂技内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯