C 语言用户态函数可观测性 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
monkeyNik
V2EX    C

C 语言用户态函数可观测性

  •  
  •   monkeyNik 2024-01-23 12:08:57 +08:00 1807 次点击
    这是一个创建于 679 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本文不是介绍 eBPF 相关的用户态 Probe 的内容,而是如何利用开源 C 语言库Melon的函数模板来轻松实现函数的可观测性需求,例如:测量耗时等。

    本文主要介绍的是 Melon 库中的func模块,之所以没有给这个模块起名叫可观测性或者span,原因是这是一个更为通用的模块,不仅限于可观测性的需求。

    func模块实现的功能与 GCC 的 constructor 和 destructor 特性十分相似,就是在 C 语言函数的入口和出口增加用户自定义回调函数,在调用函数时自行调用这些函数。

    我们先看一个简单的例子:

    // a.c #include "mln_func.h" MLN_FUNC(int, abc, (int a, int b), (a, b), { printf("in %s\n", __FUNCTION__); return a + b; }) MLN_FUNC(static int, bcd, (int a, int b), (a, b), { printf("in %s\n", __FUNCTION__); return abc(a, b) + abc(a, b); }) static void my_entry(const char *file, const char *func, int line) { printf("entry %s %s %d\n", file, func, line); } static void my_exit(const char *file, const char *func, int line) { printf("exit %s %s %d\n", file, func, line); } int main(void) { mln_func_entry_callback_set(my_entry); mln_func_exit_callback_set(my_exit); printf("%d\n", bcd(1, 2)); return 0; } 

    这段代码中,使用MLN_FUNC定义了两个函数,分别为abcbcd,且在bcd中会调用abc。其实这个模板宏相对比较容易理解,其宏函数参数顺序如下:

    • 返回值类型(涵盖函数作用域,如static
    • 函数名
    • 函数形参列表(需要用()扩住)
    • 函数实参列表(需要用()扩住)
    • 函数体

    这里唯一有些困惑的是实参列表,这与宏的实现有关。我们以abc为例,简述一下实现原理。

    原理:这个宏会定义两个函数,一个名为abc,一个名为__abc。函数体其实对应的是__abc,也就是说__abc才是真正我们期望调用的那个函数,而abc是对__abc的一个封装,会在__abc的调用前后调用自定义回调函数。

    而实参列表就是在函数abc中调用__abc时需要给__abc传递的参数,所以这个参数列表其实就是形参列表去掉类型之后的名字和顺序。

    这个实参列表无法忽略,是因为__abc不能省略,而__abc不能省略是因为函数体中可能包含 return 语句,因此我们无法完全隐式地在 return 前,甚至是在 return 的表达式计算后真正的返回前调用回调函数。所以必须单独定义成一个函数也就是__abc

    下面我们来编译这个程序:

    cc -o a a.c -I /path/to/melon/include -L /path/to/melon/lib -lmelon 

    其中/path/to/melon的部分是 Melon 的安装路径,默认一般是/usr/local/melon

    然后运行一下

    ./a in bcd in abc in abc 6 

    你会发现回调函数完全没被调用。这不是我们的代码有问题,而是我们并未启用模板功能。模板启用需要编译时存在MLN_FUNC_FLAG的宏定义,我们既可以将它定义在源文件中,也可以在编译时作为命令行参数给出。下面我以后者为例展示:

    cc -o a a.c -I /path/to/melon/include -L /path/to/melon/lib -lmelon -DMLN_FUNC_FLAG 

    再次运行

    ./a entry a.c bcd 10 in __bcd entry a.c abc 5 in __abc exit a.c abc 5 entry a.c abc 5 in __abc exit a.c abc 5 exit a.c bcd 10 6 

    可以看到,回调函数都被正常调用了。

    利用这个开关宏,我们可以在不修改任何代码的情况下,轻松切换是否需要开启这项功能。

    综合示例

    前面给出的例子比较简单,那么下面就来看一个实现测量函数调用耗时的例子吧。

    这里我将给出三个文件:

    • span.h:这是为测量耗时所定义的数据结构和函数声明等内容。
    • span.c:这是为测量耗时定义的相关函数。
    • a.c:这是我们自定义的一些函数以及在main函数中调用这些函数。

    其中,span.hspan.c可以随意复制粘贴使用,这是一个独立的模块,当然,你还需要先安装好 Melon 库。

    span.h

    #include <sys/time.h> #include "mln_array.h" typedef struct mln_span_s { struct timeval begin; struct timeval end; const char *file; const char *func; int line; mln_array_t subspans; struct mln_span_s *parent; } mln_span_t; extern int mln_span_start(void); extern void mln_span_stop(void); extern void mln_span_dump(void); extern void mln_span_release(void); 

    这里定义了一个数据结构mln_span_t,用来存放函数调用的起始和结束时的时间戳,以及函数所在源文件的信息。还包含了这个函数中调用的其他函数的调用时长信息,以及一个指向上一级调用(也就是调用当前函数的函数)信息的指针。

    也就是说,当我们的函数执行完毕后,我们遍历这个结构就能拿到完整的调用关系及其调用细节。

    span.c

    #include <stdlib.h> #include <string.h> #include "span.h" #include "mln_stack.h" #include "mln_func.h" static mln_stack_t *callstack = NULL; static mln_span_t *root = NULL; static void mln_span_entry(const char *file, const char *func, int line); static void mln_span_exit(const char *file, const char *func, int line); static mln_span_t *mln_span_new(mln_span_t *parent, const char *file, const char *func, int line); static void mln_span_free(mln_span_t *s); static mln_span_t *mln_span_new(mln_span_t *parent, const char *file, const char *func, int line) { mln_span_t *s; struct mln_array_attr attr; if (parent != NULL) { s = (mln_span_t *)mln_array_push(&parent->subspans); } else { s = (mln_span_t *)malloc(sizeof(mln_span_t)); } if (s == NULL) return NULL; memset(&s->begin, 0, sizeof(struct timeval)); memset(&s->end, 0, sizeof(struct timeval)); s->file = file; s->func = func; s->line = line; attr.pool = NULL; attr.pool_alloc = NULL; attr.pool_free = NULL; attr.free = (array_free)mln_span_free; attr.size = sizeof(mln_span_t); attr.nalloc = 7; if (mln_array_init(&s->subspans, &attr) < 0) { if (parent == NULL) free(s); return NULL; } s->parent = parent; return s; } static void mln_span_free(mln_span_t *s) { if (s == NULL) return; mln_array_destroy(&s->subspans); if (s->parent == NULL) free(s); } int mln_span_start(void) { struct mln_stack_attr sattr; mln_func_entry_callback_set(mln_span_entry); mln_func_exit_callback_set(mln_span_exit); sattr.free_handler = NULL; sattr.copy_handler = NULL; if ((callstack = mln_stack_init(&sattr)) == NULL) return -1; return 0; } void mln_span_stop(void) { mln_func_entry_callback_set(NULL); mln_func_exit_callback_set(NULL); mln_stack_destroy(callstack); } void mln_span_release(void) { mln_span_free(root); } static void mln_span_format_dump(mln_span_t *span, int blanks) { int i; mln_span_t *sub; for (i = 0; i < blanks; ++i) printf(" "); printf("| %s at %s:%d takes %lu (us)\n", \ span->func, span->file, span->line, \ (span->end.tv_sec * 1000000 + span->end.tv_usec) - (span->begin.tv_sec * 1000000 + span->begin.tv_usec)); for (i = 0; i < mln_array_nelts(&(span->subspans)); ++i) { sub = ((mln_span_t *)mln_array_elts(&(span->subspans))) + i; mln_span_format_dump(sub, blanks + 2); } } void mln_span_dump(void) { if (root != NULL) mln_span_format_dump(root, 0); } static void mln_span_entry(const char *file, const char *func, int line) { mln_span_t *span; if ((span = mln_span_new(mln_stack_top(callstack), file, func, line)) == NULL) { fprintf(stderr, "new span failed\n"); exit(1); } if (mln_stack_push(callstack, span) < 0) { fprintf(stderr, "push span failed\n"); exit(1); } if (root == NULL) root = span; gettimeofday(&span->begin, NULL); } static void mln_span_exit(const char *file, const char *func, int line) { mln_span_t *span = mln_stack_pop(callstack); if (span == NULL) { fprintf(stderr, "call stack crashed\n"); exit(1); } gettimeofday(&span->end, NULL); } 

    这里就是耗时统计所需要的所有函数定义。利用一个栈数据结构来保证函数的调用关系,然后在函数的入口回调处创建mln_span_t结点记录起始时间和函数信息并入栈,在出口回调处记录结束时间并出栈。

    a.c

    #include "span.h" #include "mln_func.h" MLN_FUNC(int, abc, (int a, int b), (a, b), { return a + b; }) MLN_FUNC(static int, bcd, (int a, int b), (a, b), { return abc(a, b) + abc(a, b); }) int main(void) { mln_span_start(); bcd(1, 2); mln_span_stop(); mln_span_dump(); mln_span_release(); return 0; } 

    这里还是那个配方,就是调用bcd,然后bcd调用abc。我们这次在main函数中使用span.h中声明的函数。

    一起来简单编译一下:

    cc -o a span.c a.c -I /usr/local/melon/include -L /usr/local/melon/lib -lmelon -DMLN_FUNC_FLAG 

    然后运行一下:

    ./a | bcd at a.c:8 takes 2 (us) | abc at a.c:4 takes 0 (us) | abc at a.c:4 takes 0 (us) 

    小结

    Melon的函数模板其实设计之初也是为了可观测性,因为 GCC 仅支持了 constructor 和 destructor 。如果显式地在代码中加入各种跟踪函数调用,就会让整个函数定义看着非常不连贯和杂乱。因此选择了当前的这个使用方式,但也不可避免的引入了看似没什么用途的实参部分。

    另外,Melon 库支持模块选择性编译,因此函数模版模块可以单独编译成库,换言之,这个模块是完全无操作系统依赖的,单片机的小伙伴们可以随意取用。

    感谢阅读!

    tool2d
        1
    tool2d  
       2024-01-23 13:01:50 +08:00
    用 logpoint 也可以达到类似的功能。

    一般来说 gdb 需要用 breakpoint 来断点,输出变量内容。而 logpoint 顾名思义,就是不中断程序的前提下,直接用 log 输出指定变量/函数运行的日志信息。
    zzz22333
        2
    zzz22333  
       2024-01-23 15:08:20 +08:00
    把获取时间的函数注册进去,不一定有 gettimeofday 这个函数的
    monkeyNik
        3
    monkeyNik  
    OP
       2024-01-23 15:18:37 +08:00
    @zzz22333 是的,不过 span 不是 Melon 中的模块,只是我这里给出的一个演示示例,所以从简没考虑可移植性问题。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     940 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 22:33 PVG 06:33 LAX 14:33 JFK 17:33
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86