libevent源码学习(1):日志及错误处理

目录

错误处理函数

函数声明

__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.  
  6. va_start(ap, fmt);
  7. _warn_helper(_EVENT_LOG_ERR, strerror(errno), fmt, ap);
  8. va_end(ap);
  9. event_exit(eval);
  10. }
  11.  
  12. void
  13. event_warn(const char *fmt, ...)
  14. {
  15. va_list ap;
  16.  
  17. va_start(ap, fmt);
  18. _warn_helper(_EVENT_LOG_WARN, strerror(errno), fmt, ap);
  19. va_end(ap);
  20. }
  21.  
  22. void
  23. event_sock_err(int eval, evutil_socket_t sock, const char *fmt, ...)
  24. {
  25. va_list ap;
  26. int err = evutil_socket_geterror(sock); //宏定义为errno
  27.  
  28. va_start(ap, fmt);
  29. _warn_helper(_EVENT_LOG_ERR, evutil_socket_error_to_string(err), fmt, ap);
  30. va_end(ap);
  31. event_exit(eval);
  32. }
  33.  
  34. void
  35. event_sock_warn(evutil_socket_t sock, const char *fmt, ...)
  36. {
  37. va_list ap;
  38. int err = evutil_socket_geterror(sock);
  39.  
  40. va_start(ap, fmt);
  41. _warn_helper(_EVENT_LOG_WARN, evutil_socket_error_to_string(err), fmt, ap);
  42. va_end(ap);
  43. }
  44.  
  45. void
  46. event_errx(int eval, const char *fmt, ...)
  47. {
  48. va_list ap;
  49.  
  50. va_start(ap, fmt);
  51. _warn_helper(_EVENT_LOG_ERR, NULL, fmt, ap);
  52. va_end(ap);
  53. event_exit(eval);
  54. }
  55.  
  56. void
  57. event_warnx(const char *fmt, ...)
  58. {
  59. va_list ap;
  60.  
  61. va_start(ap, fmt);
  62. _warn_helper(_EVENT_LOG_WARN, NULL, fmt, ap);
  63. va_end(ap);
  64. }
  65.  
  66. void
  67. event_msgx(const char *fmt, ...)
  68. {
  69. va_list ap;
  70.  
  71. va_start(ap, fmt);
  72. _warn_helper(_EVENT_LOG_MSG, NULL, fmt, ap);
  73. va_end(ap);
  74. }
  75.  
  76. void
  77. _event_debugx(const char *fmt, ...)
  78. {
  79. va_list ap;
  80.  
  81. va_start(ap, fmt);
  82. _warn_helper(_EVENT_LOG_DEBUG, NULL, fmt, ap);
  83. va_end(ap);
  84. }

         以上都是错误处理函数,可以发现,这些函数都是有共同点的:它们的参数除了一个用于格式化的字符串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. {
  5. char buf[1024];
  6. size_t len;
  7.  
  8. if (fmt != NULL) //如果fmt非空,说明可变参数需要进行格式化
  9. evutil_vsnprintf(buf, sizeof(buf), fmt, ap); //将可变参数格式化后的字符串写入buf中
  10. else
  11. buf[0] = '';
  12.  
  13. if (errstr) { //如果errstr非空
  14. len = strlen(buf); //如果至少还能放下冒号、空格和一个终止符(对应“: %s”)
  15. if (len < sizeof(buf) - 3) {
  16. evutil_snprintf(buf + len, sizeof(buf) - len, ": %s", errstr); //buf+len定位到buf有效字符的末尾的后一个位置,
  17. // sizeof(buf)-len限制最多只能填满buf,不能越界
  18. //换句话说,就是在buf后面追加“: ”+errstr
  19. }
  20. }
  21.  
  22. event_log(severity, buf);
  23. }

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

  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 ", 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 "形式格式化,最终输出到标准错误输出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函数的定义非常简单:

  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,然后执行默认处理行为:将错误信息打印到终端屏幕上。

错误处理

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函数的定义,如下所示:

  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

         查看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进行设置的,其定义如下:

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

原文地址:https://www.cnblogs.com/cnhk19/p/14419623.html