目录
设置日志处理回调函数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__,使用了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函数的声明,如下所示:
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函数定义如下:
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非空又对应什么呢?
跳到定义查看如下:
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函数的定义非常简单:
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,然后执行默认处理行为:将错误信息打印到终端屏幕上。
前面错误处理入口函数部分,提到每个入口函数都有相似的地方:可变参数宏和调用_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的相关定义,如下所示:
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进行设置的,其定义如下:
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等发生之前调用,因为一旦发生了各种情况,那么就会自动去调用日志和错误处理函数了, 因此应当提前设置好自定义的处理函数。
手机扫一扫
移动阅读更方便
你可能感兴趣的文章