Libevent 源码阅读笔记(三)、日志系统的实现

Libevent 如何定义自己的日志输出格式

Libevent 的日志系统中定义了四个日志的输出等级,默认情况下会将日志信息输出到终端。不过 Libevent 允许用户设置自定义的日志输出回调函数以及异常日志输出函数,以提供日志信息的进一步解释和输出重定向。回调函数的指针定义如下:

//event.h
//日志输出等级
#define EVENT_LOG_DEBUG 0   
#define EVENT_LOG_MSG   1
#define EVENT_LOG_WARN  2
#define EVENT_LOG_ERR   3
//用户自定义的日志输出回调函数,其中 severity 代表日志输出等级
typedef void (*event_log_cb)(int severity, const char *msg);
typedef void (*event_fatal_cb)(int err);

Libevent 默认将这两个回调函数置为 NULL,用户可以通过函数event_set_log_callbackevent_set_fatal_callback进行设置

//log.c
static event_log_cb log_fn = NULL;
void event_set_log_callback(event_log_cb cb)
{
    log_fn = cb;
}
static event_fatal_cb fatal_fn = NULL;
void event_set_fatal_callback(event_fatal_cb cb)
{
    fatal_fn = cb;
}

若用户没有自定义日志输出函数,那么 Libevent 会根据 severity 的值调用相应的默认日志输出函数,并向 stderr 输出日志信息。具体的实现如下:

//log.c
static void event_log(int severity, const char *msg)
{
    if (log_fn)
        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)的作用是减少编译器因为“未被使用的返回值”而产生的警告信息。
        (void)fprintf(stderr, "[%s] %s\n", severity_str, msg);
    }
}

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 默认的日志输出函数。在源码当中,日志系统相关的文件主要有两个: log.c 和 log-internal.h。我们先来看 log-internal.h 文件

// log-internal.h 
#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

//EVENT_DEBUG_LOGGING_ENABLED 为调试模式的开关
#ifdef EVENT_DEBUG_LOGGING_ENABLED
EVENT2_CORE_EXPORT_SYMBOL extern ev_uint32_t event_debug_logging_mask_;
#define event_debug_get_logging_mask_() (event_debug_logging_mask_)
#else
#define event_debug_get_logging_mask_() (0)
#endif

//EVENT_ERR_ABORT_ 代表终止程序时的错误码
#define EVENT_ERR_ABORT_ ((int)0xdeaddead)

/* EVENT2_EXPORT_SYMBOL是定义在 include/event2/visibility.h 下的一个宏
 * define EVENT2_EXPORT_SYMBOL __attribute__ ((visibility("default")))
 */
EVENT2_EXPORT_SYMBOL
void event_err(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3) EV_NORETURN;
EVENT2_EXPORT_SYMBOL
void event_warn(const char *fmt, ...) EV_CHECK_FMT(1,2);
EVENT2_EXPORT_SYMBOL
void event_sock_err(int eval, evutil_socket_t sock, const char *fmt, ...) EV_CHECK_FMT(3,4) EV_NORETURN;
EVENT2_EXPORT_SYMBOL
void event_sock_warn(evutil_socket_t sock, const char *fmt, ...) EV_CHECK_FMT(2,3);
EVENT2_EXPORT_SYMBOL
void event_errx(int eval, const char *fmt, ...) EV_CHECK_FMT(2,3) EV_NORETURN;
EVENT2_EXPORT_SYMBOL
void event_warnx(const char *fmt, ...) EV_CHECK_FMT(1,2);
EVENT2_EXPORT_SYMBOL
void event_msgx(const char *fmt, ...) EV_CHECK_FMT(1,2);
EVENT2_EXPORT_SYMBOL
void event_debugx_(const char *fmt, ...) EV_CHECK_FMT(1,2);

EVENT2_EXPORT_SYMBOL
void event_logv_(int severity, const char *errstr, const char *fmt, va_list ap)
    EV_CHECK_FMT(3,0);

在 log-internal.h 中,我们可以看到 Libevent 中的日志系统其实运用到了编译器属性 __attribute__ 的一些技巧。

__attribute__ 是 GNU C 下的一种机制,可以用来设置函数属性、变量属性以及类型属性,它的语法格式如下

__attribute__((attribute-list))

其中 attribute-list 是用逗号分隔开的列表。在 Libevent 的日志系统中,主要应用到的有以下三种 __attribute__ 属性:

1. __attribute__((format(archetype, string-index, first-to-check)))

该属性可以为函数添加上类似于 print、scanf、strftime 以及 strfmon 风格的格式化字符串,从而可以帮助我们完成对格式化字符串的类型检查。其中,archetype 代表参数的格式化风格,string-index 代表格式化字符串位于形参列表中的哪个位置,first-to-check 代表可变参数列表在形参列表中的哪个位置。结合一个 event_warn 的例子:

void event_warn(const char *fmt, ...) __attribute__((format(printf, 1, 2)));

__attribute__((format(printf, 1, 2))) 代表按照 printf 的风格检查格式化字符串,其中参数列表的第 1 个参数代表格式化字符串,第 2 个参数代表可变参数列表。

2. __attribute__((noreturn))

noreturn 是一个函数属性,用来通知编译器该函数在执行后不会将控制权返回给它的调用者。以函数 event_err为例,当程序使用该日志函数时,说明程序已然产生了严重的错误,不能再继续执行下去,除了抛出异常,或者调用如exit()abort()函数终止程序以外,可以利用 noreturn 来处理。当函数执行 event_err 时,主程序会将控制权交给 event_err 函数,当 event_err 执行完毕后,控制流将不会返回主程序,而是停留在此处。

3. __attribute__ ((visibility("default")))

visibility属性可以将从动态库中导出可见符号,default 代表可见,而 hidden 代表隐藏。新版本的 Linux 内核限制了动态库的符号可见性,因为这样做可以提高程序的模块性,同时动态库装载和识别的符号越少,程序启动和运行的速度也就越快。Libevent 中被 EVENT2_EXPORT_SYMBOL 修饰的函数符号都将会被导出,因此使用该动态库程序都可以使用到这些函数。

对于event_errevent_sock_errevent_debugx_等函数,它们的实现大同小异,区别仅是部分参数的不同,以下选取部分函数进行讨论

void event_err(int eval, const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    event_logv_(EVENT_LOG_ERR, strerror(errno), fmt, ap);
    va_end(ap);
    event_exit(eval);
}

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

    va_start(ap, fmt);
    event_logv_(EVENT_LOG_ERR, evutil_socket_error_to_string(err), fmt, ap);
    va_end(ap);
    event_exit(eval);
}

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

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

void event_logv_(int severity, const char *errstr, const char *fmt, va_list ap)
{
    char buf[1024];
    size_t len;
    //当 event_debug_get_logging_mask_() == 0 时关闭调试日志的输出
    if (severity == EVENT_LOG_DEBUG && !event_debug_get_logging_mask_())
        return;

    if (fmt != NULL)
        //将日志信息按照格式字符串进行格式化,并存入 buf 中
        evutil_vsnprintf(buf, sizeof(buf), fmt, ap);
    else
        buf[0] = '\0';
    //若有额外的错误信息,则将其一起放入 buf 
    if (errstr) {
        len = strlen(buf);
        if (len < sizeof(buf) - 3) {
            evutil_snprintf(buf + len, sizeof(buf) - len, ": %s", errstr);
        }
    }

    event_log(severity, buf);
}

上述的日志函数大体的结构是相同的,都采用了可变参数列表的形式实现,最终通过event_logv_以及event_log两个函数将日志输出。

一些语法糖

在 log-internal.h 当中还定义了一个 event_debug 的宏,可以用于在调试期间打印出调试信息。利用这个宏来代替event_debugx_函数,一方面可以使得调试语义更加清晰,另一方面也可以非常方便地实现调试模式的开关。它的定义如下:

#ifdef EVENT_DEBUG_LOGGING_ENABLED
#define event_debug(x) do {         \
    if (event_debug_get_logging_mask_()) {  \
        event_debugx_ x;        \
    }                   \
    } while (0)
#else
#define event_debug(x) ((void)0)
#endif

总结

  1. Libevent 的日志系统为用户提供了两个接口event_log_cbevent_fatal_cb,以便用户实现自定义的日志处理功能,在需要的时候可以使用event_set_log_callbackevent_set_fatal_callback进行设置
  2. 如果用户没有定义自己的日志处理回调函数,则采用 Libevent 默认的日志处理系统,它将日志输出等级定义为四个,并会将日志输出到终端。
  3. 在日志系统的开发过程当中,利用好 GNU C 的一些机制,如 __attribute__ 等,可以极大简化一些繁琐的检查过程,不仅有利于调试,帮助找到一些难以发现的错误,还有助于编译器进行优化。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350