Tracepoints
内核中的每个 tracepoint 提供一个钩子来调用 probe 函数。一个 tracepoint 可以打开或关闭。打开时,probe函数关联到 tracepoint;关闭时,probe函数不关联到 tracepoint。tracepoint 关闭时对kernel产生的影响很小,只是增加了极少的时间开销(一个分支条件判断),极小的空间开销(一条函数调用语句和几个数据结构)。当一个 tracepoint 打开时,用户提供的probe函数在每次这个 tracepoint 执行是都会被调用。
技术背景
当需要获取内核的debug信息时,通常你会通过以下printk的方式打印信息:
|
|
缺点:
- 内核中printk是统一控制的,各个模块的printk都会被打印,无法只打印需要关注的模块。
- 如果需要修改/新增打印信息,需要修改所有受影响的printk语句。这些printk分散在代码多处,每个地方都需要修改。
- 嵌入式系统中,如果printk信息量大,console 如果有有大量的打印输出,用户无法在console输入命令,影响人机交互。
实现原理
内核采用 插桩的方法抓取 log,“插桩”也称为 tracepoint。每种 tracepoint有一个name、一个enable开关、一系列桩函数、注册桩函数的函数、卸载桩函数的函数。桩函数功能类似于printk,不过“桩函数”并不会把信息打印到console,而是输出到内核的ring buffer(环形缓冲区),缓冲区中的信息通过debugfs对用户呈现。逻辑架构如下:
接下来说明涉及到一些内核数据结构,代码参考:
| 数据结构 | 代码路径 |
|---|---|
| DEFINE_TRACE(name) DECLARE_TRACE(name, proto, args) | include/linux/tracepoint.h |
| struct tracepoint | include/linux/tracepoint-defs.h |
tracepoint依次执行桩函数,每个桩函数实现不同的debug功能。内核通过register_trace_##name将桩函数添加到tracepoint中,通过unregister_trace_##name从tracepoint中移除- 内核通过
DEFINE_TRACE(name)定义struct tracepoint变量来描述tracepoint
|
|
内核通过 #define DECLARE_TRACE(name, proto, args) 定义 tracepoint 用到的函数,定义的函数原型如下(从代码中摘取了几个,不止以下3个):
|
|
tracepoint 提供了统一的框架,用 void * 指向任何函数,所以各个 tracepoint 取出桩函数指针后,需要转换成自己的函数指针类型, TP_PROTO(data_proto) 传递函数指针类型用于转换,具体的转换在 –>这一行
|
|
桩函数的proto的传递的例
|
|
第2行 –> 声明了桩函数原型。
|
|
至此执行到 __DECLARE_TRACE 宏,参考前面说明,提到了何时转换成桩函数指针类型。
从上面可以看出 tracepoint的机制很简单,就是把用于debug的函数指针组织在一个 struct tracepoint 变量中,然后依次执行各个函数指针。不过为了避免各个模块重复写代码,内核用了比较复杂的宏而已。
另外我们也可以发现,使用 tracepoint 必须要通过 register_trace_##name 将桩函数(也就是我们需要的debug函数)添加到 tracepoint 中,这个工作只能通过 moudule 或者修改内核代码实现,对于开发者来说,操作比较麻烦。ftrace开发者们意识到了这点,所以提供了trace event功能,开发者不需要自己去注册桩函数了,易用性较好。
参考资料
-
No backlinks found.