基于USDT的日志优化

嵌入式应用的日志管理

据笔者的经验,嵌入式设备端的日志管理通常比较糟;举个例子,笔者以前统计过在一些安卓设备上,每小时安卓APP生成的日志量约为4.5GB。尽管安卓系统的日志管理系统是原生的,但这么大的日志生成量确实给设备带来比较大的负载(尤其当日志需要保存到文件中时)。这固然与嵌入式软件研发管理脱不开关系,但我们仍然需要寻找相应的解决办法——尤其对于一个没有研发管理权限的开发人员(如笔者),不可能限制其他的开发人员不在代码中加入过量的、循环的日志信息。这类日通常仅仅是讯息类的,即使存放到日志文件中,也没有很重要的调试、诊断作用。一种方案是,通过编译时的宏定义来控制这些日志信息是否会生成,但这也造成了一问题:用于生产的嵌入式应用很可能因编译时的宏定义,不会生成这些冗余的调试信息,而一些软件异常只能在实际应用中发生。

笔者之前的一篇博客介绍了使用systemtap来跟踪嵌入式应用的方法;本文介绍一种,基于systemtap软件包提供的sdt.h头文件,来静态跟踪日志的方法。这种方法可以去除大量冗余的日志(即默认不写入日志文件),仅在开发人员需要去查看应用实时的产生日志时,才会读取(并给系统带来一定的负载);同时也可以方便地指定查看某个进程中指定的某个日志。

USDT头文件的提取

简单直接的方法是下载systemtap源码,编译安装后在安装路径中找sdt.h头文件。该头文件仅仅是提供创建DTRACE跟踪点的很多宏定义,它是相对独立的,没有对应的C代码文件。更简单的是,不用编译安装,将该头文件做以下修改后,即可使用:

diff --git a/includes/sys/sdt.h b/includes/sys/sdt.h
index 4075a5f..a0cf9d5 100644
--- a/includes/sys/sdt.h
+++ b/includes/sys/sdt.h
@@ -241,7 +241,9 @@ __extension__ extern unsigned long long __sdt_unsp;
    nice with code in COMDAT sections, which comes up in C++ code.
    Without that assembler support, some combinations of probe placements
    in certain kinds of C++ code may produce link-time errors.  */
-#include "sdt-config.h"
+
+#define _SDT_ASM_SECTION_AUTOGROUP_SUPPORT 0
+
 #if _SDT_ASM_SECTION_AUTOGROUP_SUPPORT
 # define _SDT_ASM_AUTOGROUP "?"
 #else

嵌入式应用的代码修改

为了达到演示的效果,笔者在之前介绍systemtap博客代码基础上进行简单修改:

diff --git a/20211107/main.c b/20211107/main.c
index b455340..c5e7265 100644
--- a/20211107/main.c
+++ b/20211107/main.c
@@ -15,6 +15,9 @@ int main(int argc, char *argv[])
        if (ptrs[3] != NULL)
                ptrs[2] = NULL;

+       my_tracelog("Pointers: %p, %p, %p, %p",
+               ptrs[0], ptrs[1], ptrs[2], ptrs[3]);
+
        my_free(ptrs[0]);
        my_free(ptrs[1]);
        my_free(ptrs[2]);
diff --git a/20211107/mymalloc.c b/20211107/mymalloc.c
index 0e9ccb1..8a97c74 100644
--- a/20211107/mymalloc.c
+++ b/20211107/mymalloc.c
@@ -1,4 +1,6 @@
 #include 
+#include 
+
 #include "mymalloc.h"
 #include 

@@ -36,3 +38,21 @@ void * my_realloc(void * ptr, size_t size)
        DTRACE_PROBE3(mymalloc, my_realloc, ptr, size, ret);
        return ret;
 }
+
+void my_tracelog(const char * fmt_, ...)
+{
+       int len;
+       va_list pva;
+       char tbuf[512];
+
+       va_start(pva, fmt_);
+       len = vsnprintf(tbuf, sizeof(tbuf) - 1, fmt_, pva);
+       va_end(pva);
+
+       if (len >= sizeof(tbuf))
+               len = sizeof(tbuf) - 1;
+       if (len > 0) {
+               tbuf[len] = '\0';
+               DTRACE_PROBE1(mymalloc, my_tracelog, tbuf);
+       }
+}
diff --git a/20211107/mymalloc.h b/20211107/mymalloc.h
index f124806..63886c7 100644
--- a/20211107/mymalloc.h
+++ b/20211107/mymalloc.h
@@ -26,6 +26,8 @@ void * my_realloc(void * ptr, size_t size);
   #define my_free free
 #endif

+void my_tracelog(const char * fmt, ...)
+       __attribute__((format (printf, 1, 2)));

 #ifdef __cplusplus
 };

如上,笔者主要增加了my_tracelog函数。该函数不会输出任何内容,仅仅是将日志格式化后传递给DTRACE_PROBE1。该宏定义会给my_tracelog函数编译中加一入个空指令NOP,这一点在之前的博客文章中已提到。与单纯的UPROBE相比,USDT跟踪点的优点就是引入了这一个空指令,使得当Linux空核开始控测时,不需要再执行复杂的操作(模拟)执行被UPROBE替换的指令。

使用bpftrace工具探测USDT

USDT的探测工具有多种,对于系统资源较少的嵌入式设备,可以尝systemtap或lttng;笔者使用的树莓派设备,之前给它编译了bpftrace调试工具,以下是日志的探测效果:

root@OpenWrt:~# bpftrace -l 'usdt:/root/test-malloc:*'
usdt:/root/test-malloc:mymalloc:my_calloc
usdt:/root/test-malloc:mymalloc:my_free
usdt:/root/test-malloc:mymalloc:my_malloc
usdt:/root/test-malloc:mymalloc:my_realloc
usdt:/root/test-malloc:mymalloc:my_tracelog
root@OpenWrt:~# cat tracelog.bt
#!/usr/bin/bpftrace
usdt:/root/test-malloc:mymalloc:my_tracelog
{
        printf("PID: %u comm: %s => %s\n", pid, comm, str(arg0));
}
root@OpenWrt:~# bpftrace tracelog.bt &
root@OpenWrt:~# Attaching 1 probe...
root@OpenWrt:~# ./test-malloc
PID: 1613 comm: test-malloc => Pointers: 0x34f8d2a0, 0x34f8d2c0, (nil), 0x34f8d330
root@OpenWrt:~# ./test-malloc
PID: 1614 comm: test-malloc => Pointers: 0x1f1162a0, 0x1f1162c0, (nil), 0x1f116330
root@OpenWrt:~# ./test-malloc
PID: 1615 comm: test-malloc => Pointers: 0x3dc962a0, 0x3dc962c0, (nil), 0x3dc96330

注意,上面编写了tracelog.bt脚本;通过脚本的编写,我们可以方便地指定我们想跟踪的应用USDT探测点,更加灵活。该方案的一个缺陷是,应用不能动态地知道应用是否正在被探测;如果能知道这一点,那可以在不被探测的情况下,跳过vsnprintf格式化操作而直接返回函数,从而进一步降低系统的负载。

这是一种非常规的、可行的日志简化方案;对于不需要保存到文件的日志内容可以考虑。如果要在实际生产中使用,建议用C语言编写一个BTF文件加载到内核中,以替代上面的tracelog.bt,也不再需要在嵌入式设备上运行占用很多系统资源的bpftrace性能分析工具了。

你可能感兴趣的:(linux,USDT,日志优化)