libevent源码解读——日志

用户定义日志回调函数

Libevent在默认情况下,会将日志信息输出到终端上,这样不方便查看。可以编写日志回调函数,在回调函数中把信息输出到一个文件上,方便日后的查看。
当有日志时,Libevent库就会调用这个日志回调函数。

回调函数和日志定制函数的格式如下所示:

typedef void (*event_log_cb)(int severity, const char *msg);

void event_set_log_callback(event_log_cb cb);

回调函数第一个参数severity是级别类型,使用宏定义扩展

#define EVENT_LOG_DEBUG 0
#define EVENT_LOG_MSG   1
#define EVENT_LOG_WARN  2
#define EVENT_LOG_ERR   3
 
/* Obsolete names: these are deprecated, but older programs might use them.
	过时的名称:这些名称已弃用,但较旧的程序可能会使用它们
 * They violate the reserved-identifier namespace. */
#define _EVENT_LOG_DEBUG EVENT_LOG_DEBUG
#define _EVENT_LOG_MSG EVENT_LOG_MSG
#define _EVENT_LOG_WARN EVENT_LOG_WARN
#define _EVENT_LOG_ERR EVENT_LOG_ERR

值得注意的是,不能在你的日志回调函数里面调用任何Libevent提供的API函数,否则将发生未定义行为。

Libevent是通过定义一个全局函数指针变量来指向用户定义的日志回调函数。

static event_log_cb log_fn = NULL;	//log_fn默认情况下为NULL

void
event_set_log_callback(event_log_cb cb)
{
      log_fn = cb;
}

从event_set_log_callback的实现代码可以看到,该函数并没有对这个参数cb做任何检查。


日志API及日志消息处理流程

调用libevent提供的日志有两个步骤:

1、使用者确定要记录的日志的级别和错误类型
2、调用对应的日志函数

有下面这些可供调用选择的日志函数

void event_err(int eval, const char *fmt, ...);
void event_warn(const char *fmt, ...);
void event_sock_err(int eval, evutil_socket_t sock, const char *fmt, ...);
void event_sock_warn(evutil_socket_t sock, const char *fmt, ...);
 
void event_errx(int eval, const char *fmt, ...);
void event_warnx(const char *fmt, ...);
void event_msgx(const char *fmt, ...);
void _event_debugx(const char *fmt, ...);

这些函数内部实现都差不多。
下面是其中几个实现(在log.c中):

void
event_warn(const char *fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    event_logv_(EVENT_LOG_WARN, strerror(errno), fmt, ap);
    va_end(ap);
}

void
event_warnx(const char *fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    event_logv_(EVENT_LOG_WARN, NULL, fmt, ap);
    va_end(ap);
}

void
event_sock_warn(evutil_socket_t sock, const char *fmt, ...)
{
    va_list ap;
    int err = evutil_socket_geterror(sock);

    va_start(ap, fmt);
    event_logv_(EVENT_LOG_WARN, evutil_socket_error_to_string(err), fmt, ap);
    va_end(ap);
}

事实上从源码中我们可以看出event_err、event_warn等API最终是通过调用event_logv_实现的。

其中,event_warn和event_warnx的区别是:
event_logv_函数的第二个参数分别是strerror(errno)和NULL。

下面是event_logv_()的实现:

void
event_logv_(int severity, const char *errstr, const char *fmt, va_list ap)
{
    char buf[1024];
    size_t len;

    if (severity == EVENT_LOG_DEBUG && !event_debug_get_logging_mask_())
        return;
        
//如果有可变参数,就把可变参数格式化打印到一个缓存区buf中
    if (fmt != NULL)
        evutil_vsnprintf(buf, sizeof(buf), fmt, ap);
    else
        buf[0] = '\0';

//如果有额外的信息描述,把这些信息追加到可变参数的后面
    if (errstr) {
        len = strlen(buf);
        //-3是因为还有另外三个字符,冒号、空格和\0。
        if (len < sizeof(buf) - 3) {
            evutil_snprintf(buf + len, sizeof(buf) - len, ": %s", errstr);
        }
    }
    
//把缓存区的数据作为一条日志记录(用户操作可变参数传进来的)
    event_log(severity, buf);
}

  • 如果日志级别是EVENT_LOG_DEBUG并且没有开启日志debug,则不会有日志的输出。
  • 如果fmt不为NULL,则按照fmt给的输出格式进行对于的格式输出。
  • 如果errstr不为NULL的话,会附带上错误信息。
  • 最终,调用的是event_log

若用户不定义自己的日志回调函数,Libevent会调用自己的默认日志处理函数event_log()
下面是event_log()函数的源码:

static void
event_log(int severity, const char *msg)
{
    if (log_fn)	//如果log_fn不为NULL,调用用户的日志回调函数
        log_fn(severity, msg);
    else {
        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函数中可以看到,当函数指针log_fn不为NULL时,就调用log_fn指向的函数,否则就直接向stderr输出日志信息。
所以,设置自己的日志回调函数后,如果想恢复Libevent默认的日志回调函数,只需再次调用event_set_log_callback函数,参数设置为NULL即可。

这些日志函数的声明在log-internal.h中,如果是GNU的编译器,将会检查是否匹配。
其声明如下:

#ifdef __GNUC__
#define EV_CHECK_FMT(a,b) __attribute__((format(printf, a, b)))
#define EV_NORETURN __attribute__((noreturn))
#else
#define EV_CHECK_FMT(a,b)
#define EV_NORETURN
#endif
 
#define _EVENT_ERR_ABORT ((int)0xdeaddead)
 
void event_err(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3) EV_NORETURN;
void event_warn(const char *fmt, ...) EV_CHECK_FMT(1,2);
void event_sock_err(int eval, evutil_socket_t sock, const char *fmt, ...) EV_CHECK_FMT(3,4) EV_NORETURN;
void event_sock_warn(evutil_socket_t sock, const char *fmt, ...) EV_CHECK_FMT(2,3);
void event_errx(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3) EV_NORETURN;
void event_warnx(const char *fmt, ...) EV_CHECK_FMT(1,2);
void event_msgx(const char *fmt, ...) EV_CHECK_FMT(1,2);
void _event_debugx(const char *fmt, ...) EV_CHECK_FMT(1,2);

attribute format属性可以给被声明的函数加上类似printf或者scanf的特征。
它可以使编译器检查函数声明和函数实际调用参数之间的格式化字符串是否匹配。format属性告诉编译器,按照printf、scanf等标准C函数参数格式规则对该函数的参数进行检查。这在我们自己封装调试信息的接口时非常的有用。

format的语法格式为:

format (archetype, string-index, first-to-check)

“archetype”指定是哪种风格;
“string-index”指定传入函数的第几个参数是格式化字符串;
“first-to-check”指定从函数的第几个参数开始按上述规则进行检查。

具体使用如下:

__attribute__((format(printf, a, b)))
__attribute__((format(scanf, a, b)))

其中参数a与b的含义为:
a:第几个参数为格式化字符串(format string)
b:参数集合中的第一个,即参数“…”里的第一个参数在函数参数总数排在第几位

例子:

void event_sock_err(int eval, evutil_socket_t sock, const char 
*fmt, ...) EV_CHECK_FMT(3,4) EV_NORETURN;

在event_sock_err的参数列表里,格式化字符串char *fmt是第三个参数,”…”排在四位,所以从第四个参数开始检查,EV_CHECK_FMT(3,4)

下面直接用代码验证:

#include <stdio.h>
#include <stdarg.h>

#if 1
#define CHECK_FMT(a, b) __attribute__((format(printf, a, b)))
#else
#define CHECK_FMT(a, b)
#endif

void TRACE(const char *fmt, ...) CHECK_FMT(1, 2);

void TRACE(const char *fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);

    //printf()有返回值,强制类型转换
    (void)printf(fmt, ap);

    va_end(ap);
}

int main(void)
{
    TRACE("iValue = %d\n", 6);
    TRACE("iValue = %d\n", "test");
    TRACE("iValue = %d %d %s\n", 12, "test");

    return 0;
}

注意:需要打开警告信息即(-Wall)。
在这里插入图片描述
如果不使用__attribute__ format则不会有警告。
注意,默认情况下,编译器是能识别类似printf的“标准”库函数。


错误处理

同样的与大部分日志库都提供的功能类似,libevent也提供相应的错误处理。
如果发生了某个致命的错误,那么我们希望的处理是保留日志消息并调用相应的错误处理又或者终止程序。
libevent中的错误处理是:在输出对应的日志消息后,如果应用程序提供相应的错误处理,那么就会运行该错误处理,否则终止程序。

错误处理函数和回调函数的格式如下:

typedef void (*event_fatal_cb)(int err);
 
void event_set_fatal_callback(event_fatal_cb cb);
static event_fatal_cb fatal_fn = NULL;//全局变量

void
event_set_fatal_callback(event_fatal_cb cb)
{
    fatal_fn = cb;
}

源码如下:

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);
}

实现原理和前面的日志处理函数一样,都是通过一个全局函数指针变量存储用户的错误处理函数。

在默认情况下,Libevent处理这些致命错误时会粗暴地杀死程序,但大多数情况下,Libevent在调用这个致命处理函数前都会调用前面的日志记录函数,其级别是_EVENT_LOG_ERR。此时,虽然程序突然死了,但还是可以在日志中找到一些信息。

如果要编写自己的日志回调函数和错误回调函数,那么应该放在程序的开始位置。


参考链接:
https://blog.csdn.net/luotuo44/article/details/38317797
https://blog.csdn.net/huangjh2017/article/details/76944564

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值