当前位置: 首页 > news >正文

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

然后运行一下

./ain 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

再次运行

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

感谢阅读!

相关文章:

  • 前后台分离跨域交互
  • 【GitHub项目推荐--不错的Flutter项目】【转载】
  • part3 jdk17新特性详解
  • 数据结构排序算详解(动态图+代码描述)
  • 向设备树中添加pinctrl节点与gpio节点的方法
  • ReactHooks 官网文档翻译
  • React进阶 - 14(说一说”虚拟DOM“中的”Diff算法“)
  • 马尔可夫预测(Python)
  • 【日常聊聊】自然语言处理的发展
  • 央视推荐的护眼灯是哪款?护眼灯品牌推荐
  • LeetCode 每日一题 Day 51 - 53
  • spring(一):基于XML获取Bean对象以及各种依赖注入方式
  • 【iOS ARKit】BlendShapes
  • Golang 通过开源库 go-redis 操作 NoSQL 缓存服务器
  • 256:vue+openlayers利用高德逆地理编码,点击地图,弹出某点坐标和地址信息
  • 3.7、@ResponseBody 和 @RestController
  • 5、React组件事件详解
  • android图片蒙层
  • javascript 总结(常用工具类的封装)
  • LeetCode541. Reverse String II -- 按步长反转字符串
  • Octave 入门
  • Redis在Web项目中的应用与实践
  • 关于字符编码你应该知道的事情
  • 机器人定位导航技术 激光SLAM与视觉SLAM谁更胜一筹?
  • 记录一下第一次使用npm
  • 马上搞懂 GeoJSON
  • 前端代码风格自动化系列(二)之Commitlint
  • 如何合理的规划jvm性能调优
  • 入门级的git使用指北
  • 《TCP IP 详解卷1:协议》阅读笔记 - 第六章
  • 机器人开始自主学习,是人类福祉,还是定时炸弹? ...
  • ​520就是要宠粉,你的心头书我买单
  • ​LeetCode解法汇总2182. 构造限制重复的字符串
  • (1)STL算法之遍历容器
  • (4)logging(日志模块)
  • (day 2)JavaScript学习笔记(基础之变量、常量和注释)
  • (ISPRS,2023)深度语义-视觉对齐用于zero-shot遥感图像场景分类
  • (MATLAB)第五章-矩阵运算
  • (WSI分类)WSI分类文献小综述 2024
  • (安全基本功)磁盘MBR,分区表,活动分区,引导扇区。。。详解与区别
  • (附源码)SSM环卫人员管理平台 计算机毕设36412
  • (附源码)基于ssm的模具配件账单管理系统 毕业设计 081848
  • (附源码)计算机毕业设计ssm基于Internet快递柜管理系统
  • (免费领源码)Java#Springboot#mysql农产品销售管理系统47627-计算机毕业设计项目选题推荐
  • (企业 / 公司项目)前端使用pingyin-pro将汉字转成拼音
  • (原創) 未来三学期想要修的课 (日記)
  • (转)大型网站架构演变和知识体系
  • .NET CORE 2.0发布后没有 VIEWS视图页面文件
  • .NET Framework 4.6.2改进了WPF和安全性
  • .NET Micro Framework初体验(二)
  • .NET 程序如何获取图片的宽高(框架自带多种方法的不同性能)
  • .NET 中创建支持集合初始化器的类型
  • .net对接阿里云CSB服务
  • @RequestBody详解:用于获取请求体中的Json格式参数
  • [51nod1610]路径计数