惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Privacy International News Feed
Martin Fowler
Martin Fowler
The GitHub Blog
The GitHub Blog
罗磊的独立博客
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
宝玉的分享
宝玉的分享
Vercel News
Vercel News
酷 壳 – CoolShell
酷 壳 – CoolShell
爱范儿
爱范儿
I
InfoQ
Y
Y Combinator Blog
月光博客
月光博客
小众软件
小众软件
有赞技术团队
有赞技术团队
A
About on SuperTechFans
U
Unit 42
C
CXSECURITY Database RSS Feed - CXSecurity.com
Know Your Adversary
Know Your Adversary
NISL@THU
NISL@THU
P
Proofpoint News Feed
V
Vulnerabilities – Threatpost
G
Google Developers Blog
V
V2EX
V
V2EX - 技术
Forbes - Security
Forbes - Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
GbyAI
GbyAI
The Cloudflare Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tailwind CSS Blog
人人都是产品经理
人人都是产品经理
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
S
Security @ Cisco Blogs
T
Threat Research - Cisco Blogs
M
MIT News - Artificial intelligence
量子位
Microsoft Security Blog
Microsoft Security Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园_首页
Recorded Future
Recorded Future
F
Full Disclosure
Hacker News - Newest:
Hacker News - Newest: "LLM"
Cyberwarzone
Cyberwarzone
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
PCI Perspectives
PCI Perspectives
H
Hacker News: Front Page
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
Check Point Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org

C

各位大佬, C 語言該怎麼練習啊 分享一个代码优化导致的死循环 人再笨还能写不出内存安全的 C? 想念 C C11 的 _Generic,现实当中用得多不多?(尤其是公司项目) 坑爹的 GBK:大家都应该去用 UTF-8 分享一个用 AI 学习 C 语言的例子 一个简单的 C 程序,但是不明白区别在哪里 我这段 C 代码可以在编译时候输出结构体的大小,你们还有什么好点子, show me the code! 这段话是否正确?「取余这个运算,只有 Python 是对的。当初 C 这个老师教错了,那么一大票学生也就只敢跟着老师错。只有 Python 敢于站出来坚持正确答案。」 C 中可变参数如何直接传递到 printf() C 的内存打印实现 函数能否实现透传不定长度参数,最终由 printf 打印 请大佬帮忙修改一份 elf 文件里的数值 Linux 上 C 的程序遇到个异常退出问题,局部变量大小有限制?? C 语言新手求助:如何在 vscode 中使用第三方库? 将资源嵌入到可执行文件中并保持目录结构 一个简单实用的 C 工程示例, 附简洁的 Makefile 关于 C 语言的相关问题 轮子更新: C/C++ 跨平台小工具库 为什么下列程序进行的是无符号乘法? 用 riscv64- Linux -gnu-gcc 编译的 c 文件为啥能在 x8664 下运行? char *s = "0123"和 char s[] = "0123"的区别 有人能完整地解释一下 int (*daytab) [13]和 int *daytab[13]吗 在 c 语言中, int a;是 declaration 还是 definition 一个简单(奇怪)的 C 语言问题 c 语言中打印指针的值打印的是 OS 分配的虚拟地址的值吗?要怎么知道 OS 给这个 c 程序进程分配的虚拟地址的大小呢? gcc 是怎么找到 system 函数的定义(实现)的? 用 C 实现轻量级表达式完成策略定制化和模板内容生成 c 语言是如何给汉字编码的? 还是不太理解 C 静态库和动态库? 开源 C 库 bfdev: MPI 大数运算 C 语言中的面向切面编程(AOP) 网络语言污染/扭曲专业术语名词——《为什么 c 语言这么抽象?》 C 语言函数资源开销可观测性 在 C 中,如何正确拷贝字符串 c 语言中自定义 section 段问题请教 C 语言的源码过滤功能的工具 求助 libcurl 的 curl_easy_pause() 使用方法 各位有什么深入了解 C 语言的书嘛? 不熟悉 cmake,请教一下多模块项目的 cmake 写法 大家有没有值得推荐的 c 语言的开源项目,用来学习或者贡献的。 求帮忙看一下这个 C 代码为什么会段错误! int 型返回值所能表达的内容极限 刚刚突然感受到了 C 语言指针的神奇之处 开源 C 语言库 Melon 之模块选择性编译 这年头搞 c 还有前途吗 看看我的 leetcode 第 151 题目 反转字符串 百变开源 C 语言库日志模块 最好用的 C 语言 JSON 解析器
C 语言用户态函数可观测性
monkeyNik · 2024-01-23 · via C

本文不是介绍 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 库支持模块选择性编译,因此函数模版模块可以单独编译成库,换言之,这个模块是完全无操作系统依赖的,单片机的小伙伴们可以随意取用。

感谢阅读!