V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
monkeyNik
V2EX  ›  C

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

  •  
  •   monkeyNik · 95 天前 · 1130 次点击
    这是一个创建于 95 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本文不是介绍 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  
       95 天前
    用 logpoint 也可以达到类似的功能。

    一般来说 gdb 需要用 breakpoint 来断点,输出变量内容。而 logpoint 顾名思义,就是不中断程序的前提下,直接用 log 输出指定变量/函数运行的日志信息。
    zzz22333
        2
    zzz22333  
       95 天前
    把获取时间的函数注册进去,不一定有 gettimeofday 这个函数的
    monkeyNik
        3
    monkeyNik  
    OP
       95 天前
    @zzz22333 是的,不过 span 不是 Melon 中的模块,只是我这里给出的一个演示示例,所以从简没考虑可移植性问题。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3006 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 13:32 · PVG 21:32 · LAX 06:32 · JFK 09:32
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.