libevent源码学习(1):日志及错误处理
阅读原文时间:2023年07月08日阅读:2

目录

错误处理函数

函数声明

__attribute__指令

函数定义

可变参数宏

_warn_helper函数

日志处理

event_log日志处理入口

日志处理回调函数指针log_fn

设置日志处理回调函数event_set_log_callback

错误处理

event_exit错误处理入口

错误处理回调函数指针fatal_fn

设置错误处理回调函数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__机制。它实际上是对编译器进行指示,对于函数相当于是一个修饰作用。比如说这里的

  1. #define EV_CHECK_FMT(a,b) __attribute__((format(printf, a, b)))

  2. #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文件中,如下所示:

  1. void

  2. event_err(int eval, const char *fmt, …)

  3. {

  4. va_list ap;

  5. va_start(ap, fmt);

  6. _warn_helper(_EVENT_LOG_ERR, strerror(errno), fmt, ap);

  7. va_end(ap);

  8. event_exit(eval);

  9. }

  10. void

  11. event_warn(const char *fmt, …)

  12. {

  13. va_list ap;

  14. va_start(ap, fmt);

  15. _warn_helper(_EVENT_LOG_WARN, strerror(errno), fmt, ap);

  16. va_end(ap);

  17. }

  18. void

  19. event_sock_err(int eval, evutil_socket_t sock, const char *fmt, …)

  20. {

  21. va_list ap;

  22. int err = evutil_socket_geterror(sock); //宏定义为errno

  23. va_start(ap, fmt);

  24. _warn_helper(_EVENT_LOG_ERR, evutil_socket_error_to_string(err), fmt, ap);

  25. va_end(ap);

  26. event_exit(eval);

  27. }

  28. void

  29. event_sock_warn(evutil_socket_t sock, const char *fmt, …)

  30. {

  31. va_list ap;

  32. int err = evutil_socket_geterror(sock);

  33. va_start(ap, fmt);

  34. _warn_helper(_EVENT_LOG_WARN, evutil_socket_error_to_string(err), fmt, ap);

  35. va_end(ap);

  36. }

  37. void

  38. event_errx(int eval, const char *fmt, …)

  39. {

  40. va_list ap;

  41. va_start(ap, fmt);

  42. _warn_helper(_EVENT_LOG_ERR, NULL, fmt, ap);

  43. va_end(ap);

  44. event_exit(eval);

  45. }

  46. void

  47. event_warnx(const char *fmt, …)

  48. {

  49. va_list ap;

  50. va_start(ap, fmt);

  51. _warn_helper(_EVENT_LOG_WARN, NULL, fmt, ap);

  52. va_end(ap);

  53. }

  54. void

  55. event_msgx(const char *fmt, …)

  56. {

  57. va_list ap;

  58. va_start(ap, fmt);

  59. _warn_helper(_EVENT_LOG_MSG, NULL, fmt, ap);

  60. va_end(ap);

  61. }

  62. void

  63. _event_debugx(const char *fmt, …)

  64. {

  65. va_list ap;

  66. va_start(ap, fmt);

  67. _warn_helper(_EVENT_LOG_DEBUG, NULL, fmt, ap);

  68. va_end(ap);

  69. }

以上都是错误处理函数,可以发现,这些函数都是有共同点的:它们的参数除了一个用于格式化的字符串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函数定义如下:

  1. static void

  2. _warn_helper(int severity, const char *errstr, const char *fmt, va_list ap) //将需要格式化的字符串与报错信息合并errstr为报错信息,fmt为可变参数格式化的字符串

  3. {

  4. char buf[1024];

  5. size_t len;

  6. if (fmt != NULL) //如果fmt非空,说明可变参数需要进行格式化

  7. evutil_vsnprintf(buf, sizeof(buf), fmt, ap); //将可变参数格式化后的字符串写入buf中

  8. else

  9. buf[0] = '\0';

  10. if (errstr) { //如果errstr非空

  11. len = strlen(buf); //如果至少还能放下冒号、空格和一个终止符(对应“: %s”)

  12. if (len < sizeof(buf) - 3) {

  13. evutil_snprintf(buf + len, sizeof(buf) - len, ": %s", errstr); //buf+len定位到buf有效字符的末尾的后一个位置,

  14. // sizeof(buf)-len限制最多只能填满buf,不能越界

  15. //换句话说,就是在buf后面追加“: ”+errstr

  16. }

  17. }

  18. event_log(severity, buf);

  19. }

这里调用了一个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函数定义如下:

  1. static void

  2. event_log(int severity, const char *msg)

  3. {

  4. if (log_fn) //如果日志回调函数非空,则调用回调函数

  5. log_fn(severity, msg);

  6. else { //如果未定义日志回调函数,则直接在终端输出信息:"[severity] msg"

  7. const char *severity_str;

  8. switch (severity) {

  9. case _EVENT_LOG_DEBUG:

  10. severity_str = "debug";

  11. break;

  12. case _EVENT_LOG_MSG:

  13. severity_str = "msg";

  14. break;

  15. case _EVENT_LOG_WARN:

  16. severity_str = "warn";

  17. break;

  18. case _EVENT_LOG_ERR:

  19. severity_str = "err";

  20. break;

  21. default:

  22. severity_str = "???";

  23. break;

  24. }

  25. (void)fprintf(stderr, "[%s] %s\n", severity_str, msg);

  26. }

  27. }

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非空又对应什么呢?

跳到定义查看如下:

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函数的定义非常简单:

  1. void

  2. event_set_log_callback(event_log_cb cb)

  3. {

  4. log_fn = cb;

  5. }

可见,该函数的参数也是一个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,然后执行默认处理行为:将错误信息打印到终端屏幕上。

错误处理

前面错误处理入口函数部分,提到每个入口函数都有相似的地方:可变参数宏和调用_warn_helper函数,通过这两点完成了日志处理功能,那么错误处理又是在哪里完成的呢?还是回到哪些错误处理入口函数,这次来看看它们之间的不同。

可以发现,如果是error相关的处理函数(event_err、event_sock_err和event_errx),那么在函数末尾会调用一个event_exit(eval);而其他的warn、msg一类的函数则没有调用event_exit(eval);这是符合逻辑的,出现了error程序就应当终止,因此这里的event_exit函数就应当是错误处理函数了。

跳转到event_exit函数的定义,如下所示:

  1. static void

  2. event_exit(int errcode) //

  3. {

  4. if (fatal_fn) {

  5. fatal_fn(errcode);

  6. exit(errcode); /* should never be reached */

  7. } else if (errcode == _EVENT_ERR_ABORT)

  8. abort();

  9. else

  10. exit(errcode);

  11. }

可以发现,这里也有一个fatal_fn,这里会先判断fatal_fn是否为空,如果为空,还会进一步判断errcode是否为_EVENT_ERR_ABORT,如果是_EVENT_ERR_ABORT,就会调用abort函数,向调用进程发送SIGABORT信号,使得进程异常退出,否则直接exit。

那么这个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进行设置的,其定义如下:

  1. void

  2. event_set_fatal_callback(event_fatal_cb cb) //指定错误处理回调函数

  3. {

  4. fatal_fn = cb;

  5. }

与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等发生之前调用,因为一旦发生了各种情况,那么就会自动去调用日志和错误处理函数了, 因此应当提前设置好自定义的处理函数。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章