libevent源码学习(1):日志及错误处理
目录
设置日志处理回调函数event_set_log_callback
设置错误处理回调函数event_set_fatal_callback
以下源码均基于libevent-2.0.21-stable。
日志及错误处理,虽说不是libevent的核心,甚至说是有些“简陋”,但其也是必不可少的部分。在libevent的源码中,仔细观察可以发现,很多函数中都调用了event_warn、event_err之类的函数,而这些函数就是在日志与错误处理模块中实现的,除此之外,在libevent的日志及错误处理的实现中,还使用到了反应堆中回调函数的思想,因此,个人觉得,在分析libevent的核心部分之前,先看看比较容易的日志及错误处理这一块还是有些必要的。
错误处理函数
函数声明
libevent的日志及错误处理模块在log.c和log-internal.h中。日志及错误处理函数声明位于log-internal.h中,主要包含以下内容:
这些都是错误处理函数的声明,无需多说,需要注意的是,这里的函数末尾还多了一些语句,如EV_CHECK_FMT(2,3) EV_NORETURN,很明显,这些都是宏定义,那这些宏定义有什么作用呢?
__attribute__指令
跳转到宏定义处,如下所示:
也就是说,这里的宏定义实际上是定义的__attribute__,使用了GNU C的__attribute__机制。它实际上是对编译器进行指示,对于函数相当于是一个修饰作用。比如说这里的
- #define EV_CHECK_FMT(a,b) __attribute__((format(printf, a, b)))
- #define EV_NORETURN __attribute__((noreturn))
对于event_err函数来说,参数中含有可变参数,函数由EV_CHECK_FMT(2,3) 和EV_NORETURN修饰,
其中EV_CHECK_FMT(2,3) 对应与__attribute__((format(printf, 2, 3))),提示编译器按照printf函数格式化的形式来对event_err函数进行编译,2表示第2个参数为格式化字符串,3表示格式化的可变参数从第3个参数开始。简单来说,就是提示编译器从第3个参数开始按照第2个参数字符串的格式进行格式化;
EV_NORETURN表示event_err函数没有返回值,也不能有返回值。
函数定义
先来看看上述声明的处理函数的定义,位于log.c文件中,如下所示:
- void
- event_err(int eval, const char *fmt, ...)
- {
- va_list ap;
- va_start(ap, fmt);
- _warn_helper(_EVENT_LOG_ERR, strerror(errno), fmt, ap);
- va_end(ap);
- event_exit(eval);
- }
- void
- event_warn(const char *fmt, ...)
- {
- va_list ap;
- va_start(ap, fmt);
- _warn_helper(_EVENT_LOG_WARN, strerror(errno), fmt, ap);
- va_end(ap);
- }
- void
- event_sock_err(int eval, evutil_socket_t sock, const char *fmt, ...)
- {
- va_list ap;
- int err = evutil_socket_geterror(sock); //宏定义为errno
- va_start(ap, fmt);
- _warn_helper(_EVENT_LOG_ERR, evutil_socket_error_to_string(err), fmt, ap);
- va_end(ap);
- event_exit(eval);
- }
- void
- event_sock_warn(evutil_socket_t sock, const char *fmt, ...)
- {
- va_list ap;
- int err = evutil_socket_geterror(sock);
- va_start(ap, fmt);
- _warn_helper(_EVENT_LOG_WARN, evutil_socket_error_to_string(err), fmt, ap);
- va_end(ap);
- }
- void
- event_errx(int eval, const char *fmt, ...)
- {
- va_list ap;
- va_start(ap, fmt);
- _warn_helper(_EVENT_LOG_ERR, NULL, fmt, ap);
- va_end(ap);
- event_exit(eval);
- }
- void
- event_warnx(const char *fmt, ...)
- {
- va_list ap;
- va_start(ap, fmt);
- _warn_helper(_EVENT_LOG_WARN, NULL, fmt, ap);
- va_end(ap);
- }
- void
- event_msgx(const char *fmt, ...)
- {
- va_list ap;
- va_start(ap, fmt);
- _warn_helper(_EVENT_LOG_MSG, NULL, fmt, ap);
- va_end(ap);
- }
- void
- _event_debugx(const char *fmt, ...)
- {
- va_list ap;
- va_start(ap, fmt);
- _warn_helper(_EVENT_LOG_DEBUG, NULL, fmt, ap);
- va_end(ap);
- }
以上都是错误处理函数,可以发现,这些函数都是有共同点的:它们的参数除了一个用于格式化的字符串fmt,其他都是可变参数。而在函数体内,也大致相同:一个是可变参数宏,一个是调用了_warn_helper函数,下面先来说下这两个东西。
可变参数宏
可变参数宏常用于C语言中的变参函数,所谓变参函数是指在定义函数的时候无法确定函数有多少个参数,就像你要定义一个序列求和函数,但是你并不知道这个序列有多少个元素,那么就可以使用可变参数宏。另一个例子就是printf函数,实际上printf函数就是用可变参数宏实现的。
常用的可变参数宏有以下几个:va_list、va_start、va_arg和va_end,
其中va_list是一个指向参数列表的指针类型,使用时直接用该类型定义一个变量即可,如上面的va_list ap;
va_start是用来指定最后一个非可变参数(也就相当于指明了可变参数列表的起始位置),如上面的错误处理函数最后一个非可变参数是fmt,因此调用方式为va_start(ap,fmt),其中ap就是刚刚定义的va_list ap;
va_arg用来获取下一个可变参数,由其返回值实现。它需要输入两个参数,一个是va_list变量,也就是这里的ap,另一个就是参数的类型,比如说这里当前参数fmt类型为const char *,那么就需要使用va_arg(ap,const char *);
va_end就不用说了,既然使用了va_start,那么就应当成对使用va_end。
为什么可以这样来获取可变参数呢?这是因为函数的参数都是放在栈中的,并且函数的参数是从从右至左依次入栈,第一个参数地址最低,最后一个参数地址最高,函数原型中相邻的参数在物理地址上也是相邻的,因此调用va_start先让ap指针指向最后一个非可变参数fmt,fmt的类型是const char *类型,占据的大小为sizeof(fmt),因此此时地址加上sizeof(fmt)就是第一个可变参数的地址了,因此获取下一个可变参数就要用到va_arg(ap,const char*)。
回到错误处理函数中,每个函数都调用了va_list、va_start和va_end,那va_arg呢?那就只能是通过_warn_helper函数来调用了。
_warn_helper函数
先来看看__warn_helper函数的声明,如下所示:
static void _warn_helper(int severity, const char *errstr, const char *fmt,va_list ap);
再来看看是怎么用的,以event_err为例,其调用方式为
_warn_helper(_EVENT_LOG_ERR, strerror(errno), fmt, ap);
这里传入了4个参数,第1个参数是一个宏,根据函数声明中的描述severity,意味“严重性”,这里传入的参数为_EVENT_LOG_ERR,跳转到其定义如下:
可以发现,这实际上就是用于说明出现的消息的严重性,是什么类型的:debug、message、warning or error。
再来看第2个参数,这是一个字符串类型,根据声明中的errstr和实参strerror(errno)可以知道,这实际上就是一个描述错误消息的字符串,是不需要再自行实现的。第3个参数就是调用event_err时传入的格式化字符串fmt。最后一个参数是前面定义的va_list变量ap。
从这4个参数来看,第1个参数是指明消息类型,第2个参数是系统自带的描述错误信息的,第3个和第4个参数一起用来将可变参数进行格式化。
这里需要注意的是,第2个参数错误信息是跟用户无关的,每个错误本身就对应一个描述错误信息的字符串。而第3个和第4个参数是调用者调用消息处理函数时需要格式化字符串和可变参数,如下所示:
其他处理函数的调用方式都大同小异,就不多说了。
_warn_helper函数定义如下:
- static void
- _warn_helper(int severity, const char *errstr, const char *fmt, va_list ap) //将需要格式化的字符串与报错信息合并errstr为报错信息,fmt为可变参数格式化的字符串
- {
- char buf[1024];
- size_t len;
- if (fmt != NULL) //如果fmt非空,说明可变参数需要进行格式化
- evutil_vsnprintf(buf, sizeof(buf), fmt, ap); //将可变参数格式化后的字符串写入buf中
- else
- buf[0] = '\0';
- if (errstr) { //如果errstr非空
- len = strlen(buf); //如果至少还能放下冒号、空格和一个终止符(对应“: %s”)
- if (len < sizeof(buf) - 3) {
- evutil_snprintf(buf + len, sizeof(buf) - len, ": %s", errstr); //buf+len定位到buf有效字符的末尾的后一个位置,
- // sizeof(buf)-len限制最多只能填满buf,不能越界
- //换句话说,就是在buf后面追加“: ”+errstr
- }
- }
- event_log(severity, buf);
- }
这里调用了一个evutil_vsnprintf函数,它实际上就相当于vsnprintf,evutil_vsnprintf(buf, sizeof(buf), fmt, ap);就是将通过fmt和可变参数格式化后的字符串从地址buf开始写入,毫无疑问,前面所说缺少的va_arg就是在evutil_vsnprintf进行调用的。通过这一步,就相当于将调用event_err时输入的字符串格式化后放到了buf中。
接下来判断errstr,前面说过,errstr实际上就是错误消息对应的描述性字符串,如果errstr非空,那么就试图将errstr字符串添加到buf的后面,如何实现的呢?首先通过strlen获取buf的实际长度,sizeof获取buf所占空间大小(strlen计算终止符以前的大小,sizeof计算整个buf所占的空间大小)。
这里会判断strlen(buf)是否小于sizeof(buf)-3,如果为真的话就表示buf所占的1024个字节空间至少还能再放下3个字节(包括终止符),这条判断有什么用呢?再往下面看。
这里调用了evutil_snprintf,而在evutil_snprintf函数内部调用了evutil_vsnprintf函数,因此evutil_snprintf(buf + len, sizeof(buf) - len, ": %s", errstr);一句的作用是将用errstr格式化": %s"后的字符串从地址buf+len开始写入。buf就是char buf[1024]的首地址,buf+len就相当于定位到了当前buf字符串的末尾再往后一位,从该位开始先写入": "(一个冒号加一个空格),再写入errstr。也就是说,evutil_snprintf的作用就是将调用时输入的格式化字符串fmt和错误描述字符串errstr拼接在一起,中间用": "连接,这也就解释了为什么前面需要判断剩余空间是否小于3,这3个字节空间就是用来放一个冒号、一个空格和一个终止符的。如果小于3,说明连“: ”都放不下了就直接跳出,如果不小于3,说明至少还能放下“: ”。
buf拼接好了之后会再调用一个event_log(severity, buf);函数,将severity和处理后的buf字符串传入。从函数名就能猜出来,这与日志处理相关。
日志处理
event_log日志处理入口
event_log函数定义如下:
- static void
- event_log(int severity, const char *msg)
- {
- if (log_fn) //如果日志回调函数非空,则调用回调函数
- log_fn(severity, msg);
- else { //如果未定义日志回调函数,则直接在终端输出信息:"[severity] msg"
- const char *severity_str;
- switch (severity) {
- case _EVENT_LOG_DEBUG:
- severity_str = "debug";
- break;
- case _EVENT_LOG_MSG:
- severity_str = "msg";
- break;
- case _EVENT_LOG_WARN:
- severity_str = "warn";
- break;
- case _EVENT_LOG_ERR:
- severity_str = "err";
- break;
- default:
- severity_str = "???";
- break;
- }
- (void)fprintf(stderr, "[%s] %s\n", severity_str, msg);
- }
- }
event_log函数有两个参数,第一个serverity反映消息类型,第二个参数是字符串类型,这里传入的实际上就是前面处理后描述错误信息的buf字符串。
这里首先会先判断log_fn,如果log_fn非空,则执行log_fn(severity,msg),如果log_fn为空则执行else部分。
先来看看log_fn为空的情形,这一部分很简单,每一类severity都对应了相应的severity_str,然后通过fprintf将前面处理后描述错误信息的buf字符串(在这里就是形参msg)按"[%s] %s\n"形式格式化,最终输出到标准错误输出stderr,打印到终端屏幕。
那么log_fn是什么呢?log_fn非空又对应什么呢?
日志处理回调函数指针log_fn
跳到定义查看如下:
static event_log_cb log_fn = NULL;
这里log_fn是一个event_log_cb类型,看到cb就应该联想到callback,因此这很可能就是一个log_cb日志回调函数,跳转查看其定义:
typedef void (*event_log_cb)(int severity, const char *msg);
由此可知,event_log_cb是由typedef定义的函数指针类型,且指向的函数返回值为void,参数为(int severity, const char *msg)。也就是说,static event_log_cb log_fn = NULL;一句的作用实际上是将log_fn定义为一个函数指针变量,其应当指向一个返回值为void,含两个参数int severity和const char *msg的函数,初始化为NULL。
也就是说,在一开始log_fn是为空的,那么如何让log_fn非空呢?
设置日志处理回调函数event_set_log_callback
event_set_log_callback函数的定义非常简单:
- void
- event_set_log_callback(event_log_cb cb)
- {
- log_fn = cb;
- }
可见,该函数的参数也是一个event_log_cb类型,即函数指针,该函数的作用就是将传入的函数指针赋给log_fn。
换句话说,只要这里传入的cb不为空,那么调用event_set_log_callback函数后log_fn就指向了cb所对应的函数,log_fn也就非空,那么再回到event_log函数中,判断log_fn为真,就会直接执行log_fn(severity,msg),这里就相当于以severity,msg为参数,调用了cb所对应的函数。
因此,只需要通过event_log_cb传入自定义的日志回调函数的指针(可以直接传入函数名),那么在处理日志的时候就会执行自定义的日志回调函数。
另外还需要注意的一点是,如果event_log_cb函数传入的实参为NULL,那么log_fn又会重置为Null,然后执行默认处理行为:将错误信息打印到终端屏幕上。
错误处理
event_exit错误处理入口
前面错误处理入口函数部分,提到每个入口函数都有相似的地方:可变参数宏和调用_warn_helper函数,通过这两点完成了日志处理功能,那么错误处理又是在哪里完成的呢?还是回到哪些错误处理入口函数,这次来看看它们之间的不同。
可以发现,如果是error相关的处理函数(event_err、event_sock_err和event_errx),那么在函数末尾会调用一个event_exit(eval);而其他的warn、msg一类的函数则没有调用event_exit(eval);这是符合逻辑的,出现了error程序就应当终止,因此这里的event_exit函数就应当是错误处理函数了。
跳转到event_exit函数的定义,如下所示:
- static void
- event_exit(int errcode) //
- {
- if (fatal_fn) {
- fatal_fn(errcode);
- exit(errcode); /* should never be reached */
- } else if (errcode == _EVENT_ERR_ABORT)
- abort();
- else
- exit(errcode);
- }
可以发现,这里也有一个fatal_fn,这里会先判断fatal_fn是否为空,如果为空,还会进一步判断errcode是否为_EVENT_ERR_ABORT,如果是_EVENT_ERR_ABORT,就会调用abort函数,向调用进程发送SIGABORT信号,使得进程异常退出,否则直接exit。
那么这个fatal_fn是什么东西呢?
错误处理回调函数指针fatal_fn
查看fatal_fn的相关定义,如下所示:
static event_fatal_cb fatal_fn = NULL;
其中的event_fatal_cb定义如下:
typedef void (*event_fatal_cb)(int err);
可见,这里的fatal_fn实际上和前面的log_fn是差不多的,初始化也是NULL。
设置错误处理回调函数event_set_fatal_callback
错误处理回调函数是通过event_set_fatal_callback进行设置的,其定义如下:
- void
- event_set_fatal_callback(event_fatal_cb cb) //指定错误处理回调函数
- {
- fatal_fn = cb;
- }
与event_set_log_callback类似,直接传入函数名即可设置错误处理函数,若要恢复默认处理函数,就直接传入NULL即可。
日志及错误处理流程
实际上,对于libevent库的使用者来说,日志及错误处理内部如何实现是无需关心的,但是仍有两点需要注意:
libevent默认的日志处理行为是打印在终端屏幕,这往往不符合我们真正的需求。如果我们想按照自己的方式进行日志处理,那么就可以自定义一个日志处理函数(比如说将错误或警告信息输出到文件中),再将该函数名作为参数调用event_set_log_callback即可,如果想再恢复默认的日志处理行为,那么再次调用event_set_log_callback函数传入NULL即可。
另一点是错误处理,libevent的错误处理仅在发生error的时候进行,在进行错误处理之前会先进行日志处理,默认的错误处理行为是直接abort或者exit。如果想在发生错误后,程序退出之前做一些其他处理,那么就可以自定义一个错误处理函数,并将该函数名作为参数调用event_set_fatal_callback即可,如果想再恢复默认的错误处理行为,那么再次调用event_set_fatal_callback函数传入NULL即可。
不管是调用event_set_log_callback还是调用event_set_fatal_callback,都应该在error、warn、msg、debug等发生之前调用,因为一旦发生了各种情况,那么就会自动去调用日志和错误处理函数了, 因此应当提前设置好自定义的处理函数。
libevent源码学习(1):日志及错误处理的更多相关文章
- libevent源码学习
怎么快速学习开源库比如libevent? libevent分析 - sparkliang的专栏 - 博客频道 - CSDN.NET Libevent源码分析 - luotuo44的专栏 - 博客频道 ...
- libevent源码学习(2):内存管理
目录 内存管理函数 函数声明 event-config.h 函数定义 event_mm_malloc_ event_mm_calloc_ event_mm_strdup_ event_mm_reall ...
- libevent源码学习(11):超时管理之min_heap
目录min_heap的定义向min_heap中添加eventmin_heap中event的激活以下源码均基于libevent-2.0.21-stable. 在前文中,分析了小顶堆min_h ...
- libevent源码学习(10):min_heap数据结构解析
min_heap类型定义min_heap函数构造/析构函数及初始化判断event是否在堆顶判断两个event之间超时结构体的大小关系判断堆是否为空及堆大小返回堆顶event分配堆空间堆元素的上浮堆元素 ...
- libevent源码学习(8):event_signal_map解析
目录event_signal_map结构体向event_signal_map中添加event激活event_signal_map中的event删除event_signal_map中的event以下源码 ...
- libevent源码学习(9):事件event
目录在event之前需要知道的event_baseevent结构体创建/注册一个event向event_base中添加一个event设置event的优先级激活一个event删除一个event获取指定e ...
- libevent源码学习(6):事件处理基础——event_base的创建
目录前言创建默认的event_baseevent_base的配置event_config结构体创建自定义event_base--event_base_new_with_config禁用(避免使用)某一 ...
- libevent源码学习(7):event_io_map
event_io_map 哈希表操作函数 hashcode与equals函数 哈希表初始化 哈希表元素查找 哈希表扩容 哈希表元素插入 哈希表元素替换 哈希表元素删除 自定义条件删除元素 哈希表第一个 ...
- Mybatis源码学习之日志(五)
简述 在Java开发中常用的日志框架有Log4j.Log4j2.Apache Commons Log.java.util.logging.slf4j等,这些工具对外的接口并不相同.为了统一这些工具的接 ...
随机推荐
- pytest-rerunfailures/pytest-repeat重跑插件
在测试中,我们会经常遇到这种情况,由于环境等一些原因,一条case运行5次,只有两次成功 其它三次失败,针对这种概率性成功或失败,若是我们每次都运行一次就比较耗时间,这个时候 就需要pytest提供的 ...
- Dirichlet 前缀和的几种版本
[模板]Dirichlet 前缀和 求 \[B[i] = \sum_{d|i} A[d] \] $ n \le 2\times 10^{7} $ 看代码: for( int i = 1 ; i < ...
- Codeforces 1408I - Bitwise Magic(找性质+集合幂级数)
Codeforces 题面传送门 & 洛谷题面传送门 Yet another immortal D1+D2 I %%%%%% 首先直接统计肯定是非常不容易的,不过注意到这个 \(k\) 非常小 ...
- 有限元边界 Dirichlet 条件处理
参考自百度文档,这里只考虑 Dirichlet 边界条件情况. 有限元法基本方法就是是构造线性方程组 \[\begin{equation} Au = f \end{equation}\] 进行求解.其 ...
- R 语言实战-Part 5-2笔记
R 语言实战(第二版) part 5-2 技能拓展 ----------第21章创建包-------------------------- #包是一套函数.文档和数据的合集,以一种标准的格式保存 #1 ...
- 【GS模型】全基因组选择之rrBLUP
目录 1. 理论 2. 实操 2.1 rrBLUP包简介 2.2 实操 3. 补充说明 关于模型 关于交叉验证 参考资料 1. 理论 rrBLUP是基因组选择最常用的模型之一,也是间接法模型的代表.回 ...
- mysql—将字符型数字转成数值型数字
今天写sql语句时,相对字符串类型的数字进行排序,怎么做呢? 需要先转换成数字再进行排序 1.直接用加法 字符串+0 eg: select * from orders order by (mark+0 ...
- C++栈溢出
先看一段代码 #include<iostream> using namespace std; #define n 510 void sum(int a,int b) { cout<& ...
- 搭建简单的SpringCloud项目一:注册中心和公共层
注:笔者在搭建途中其实遇见不少问题,统一放在后面的文章说明,现在的搭建是测试OK的. GitHub:https://github.com/ownzyuan/test-cloud 后续:搭建简单的Spr ...
- mysql-centos8下安装
参考文章 1.下载安装包 客服端与服务端 依赖包 2.linux下检查是否安装 rpm -qa | grep -i mysql 安装过会显示软件名称,没安装过就是空的 3.安装包传到虚拟机 先需要把安 ...