设置libevent

libevent有一些全局的设置会影响整个libevent库。你必须在调用库的任何函数之前进行设置,否则会导致libevent处于不一致的状态。

记录日志

libevent可以记录内部的错误和警告,如果编译时支持日志记录,它还会记录调试信息。默认情况下,这些信息被写入stderr,你可以提供自己的日志函数来覆盖这些行为。

#define EVENT_LOG_DEBUG 0
#define EVENT_LOG_MSG   1
#define EVENT_LOG_WARN  2
#define EVENT_LOG_ERR   3

/* Deprecated; see note at the end of this section */
#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

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

void event_set_log_callback(event_log_cb cb);

你可以写一个符合event_log_cb签名的函数,然后传递给event_set_log_callback来重写libevent默认的日志行为。当libevent需要写日志的时候,它会传递给你提供的函数。你可以调用event_set_log_callback,并且传递NULL参数来恢复libevent默认的日志行为。

#include <event2/event.h>
#include <stdio.h>

static void discard_cb(int severity, const char *msg)
{
    /* This callback does nothing. */
}

static FILE *logfile = NULL;
static void write_to_file_cb(int severity, const char *msg)
{
    const char *s;
    if (!logfile)
        return;
    switch (severity) {
        case _EVENT_LOG_DEBUG: s = "debug"; break;
        case _EVENT_LOG_MSG:   s = "msg";   break;
        case _EVENT_LOG_WARN:  s = "warn";  break;
        case _EVENT_LOG_ERR:   s = "error"; break;
        default:               s = "?";     break; /* never reached */
    }
    fprintf(logfile, "[%s] %s\n", s, msg);
}

/* Turn off all logging from Libevent. */
void suppress_logging(void)
{
    event_set_log_callback(discard_cb);
}

/* Redirect all Libevent log messages to the C stdio file 'f'. */
void set_logfile(FILE *f)
{
    logfile = f;
    event_set_log_callback(write_to_file_cb);
}

注意:

在自己的日志函数里面,再次调用libevent的函数并不安全!举个例子,如果你想在日志回调里面用bufferevents把消息通过网络发送出去,你这样会进入奇怪的、难以调试的bug里面。这些限制可能会在未来版本的libevent中移除。

一般的,调试日志没有开启,所以不会发送至日志回调,如果libevent在编译的时候支持的话,你可以手动开启。

#define EVENT_DBG_NONE 0
#define EVENT_DBG_ALL 0xffffffffu

void event_enable_debug_logging(ev_uint32_t which);

调试日志是冗长的,而且在多数场景下并没有用。给event_enable_debug_logging传递EVENT_DBG_NONE参数为默认的日志记录行为,传递EVENT_DBG_ALL会开启所有调试日志,未来可能会提供更细粒度的控制。

这些函数声明在<event2/event.h>头文件中,他们首次出现在1.0c版本中,event_enable_debug_logging首次出现在2.1.1-alpha中。

处理致命错误

当Libevent检测到不可恢复的内部错误(例如损坏的数据结构)时,其默认行为是调用exit()abort()以退出当前正在运行的进程。这些错误几乎总是意味着某处存在bug:要么在您的代码中,要么在Libevent本身中。

如果您希望应用程序更优雅地处理致命错误,您可以提供一个Libevent应该调用的函数来代替退出,覆盖 Libevent的默认行为。

typedef void (*event_fatal_cb)(int err);
void event_set_fatal_callback(event_fatal_cb cb);

首先你需要定义一个函数,来供libevent遇到致命错误的时候调用。然后将这个函数传递给event_set_fatal_callback。如果libevent遇到致命错误的时候,它会调用你提供的函数。

你的函数不应该再返回至libevent,这样做的话会导致未定义的行为,libevent为避免崩溃会退出。如果你的函数一旦被调用,你不应该再调用其它的libevent的函数了。

这些函数声明在<event2/event.h>头文件中,首次出现在2.0.3-alpha中。

内存管理

默认情况下,Libevent使用C库的内存管理函数从堆中分配内存。您可以通过为malloc、realloc和free提供您自己的替代品,让Libevent使用另一个内存管理器。如果您希望Libevent使用更高效的分配器,或者您希望Libevent使用检测的分配器来查找内存泄漏,则您可能想要这样做。

void event_set_mem_functions(void *(*malloc_fn)(size_t sz),
                             void *(*realloc_fn)(void *ptr, size_t sz),
                             void (*free_fn)(void *ptr));

下面是一个简单的小例子,替换了libevent的分配函数,然后统计总共分配了多少字节内存,在实际使用中,你应该加锁来防止libevent在多线程中使用。

#include <event2/event.h>
#include <sys/types.h>
#include <stdlib.h>

/* This union's purpose is to be as big as the largest of all the
 * types it contains. */
union alignment {
    size_t sz;
    void *ptr;
    double dbl;
};
/* We need to make sure that everything we return is on the right
   alignment to hold anything, including a double. */
#define ALIGNMENT sizeof(union alignment)

/* We need to do this cast-to-char* trick on our pointers to adjust
   them; doing arithmetic on a void* is not standard. */
#define OUTPTR(ptr) (((char*)ptr)+ALIGNMENT)
#define INPTR(ptr) (((char*)ptr)-ALIGNMENT)

static size_t total_allocated = 0;
static void *replacement_malloc(size_t sz)
{
    void *chunk = malloc(sz + ALIGNMENT);
    if (!chunk) return chunk;
    total_allocated += sz;
    *(size_t*)chunk = sz;
    return OUTPTR(chunk);
}
static void *replacement_realloc(void *ptr, size_t sz)
{
    size_t old_size = 0;
    if (ptr) {
        ptr = INPTR(ptr);
        old_size = *(size_t*)ptr;
    }
    ptr = realloc(ptr, sz + ALIGNMENT);
    if (!ptr)
        return NULL;
    *(size_t*)ptr = sz;
    total_allocated = total_allocated - old_size + sz;
    return OUTPTR(ptr);
}
static void replacement_free(void *ptr)
{
    ptr = INPTR(ptr);
    total_allocated -= *(size_t*)ptr;
    free(ptr);
}
void start_counting_bytes(void)
{
    event_set_mem_functions(replacement_malloc,
                            replacement_realloc,
                            replacement_free);
}

注意

  • 替代内存管理函数会影响libevent以后的所有内存相关函数调用,allocateresizefree,所以你必须确保在调用任何libevent的函数之前替代这些函数。否则,libevent将使用你的版本的free来释放C版本的malloc返回的内存。
  • mallocrealloc函数需要返回与C库具有相同对齐方式的内存块。
  • 您的realloc函数需要正确处理realloc(NULL, sz) (即,将其视为malloc(sz))。
  • 您的realloc函数需要正确处理realloc(ptr, 0)(即,将其视为free(ptr))。
  • 您的free函数不需要处理free(NULL)
  • 您的malloc函数不需要处理malloc(0)
  • 如果您从多个线程使用Libevent,则替换的内存管理函数需要是线程安全的。
  • Libevent 将使用这些函数来分配它返回给您的内存。因此,如果您想释放由 Libevent 函数分配和返回的内存,并且您已经替换了mallocrealloc函数,那么您可能必须使用替换的free函数来释放它。

event_set_mem_functions函数声明在<event2/event.h>中,首次出现在2.0.1-alpha中。

Libevent 可以在禁用event_set_mem_functions()的情况下构建。如果是,则使用event_set_mem_functions的程序将不会编译或链接。在 Libevent 2.0.2-alpha 及更高版本中,您可以通过检查是否定义了EVENT_SET_MEM_FUNCTIONS_IMPLEMENTED宏来检测event_set_mem_functions()的存在。

锁与线程

正如你所知道的那样,当你写多线程的程序的时候,多个线程同时访问一块数据并不总是安全的。

libevent结构通常可以通过多线程以三种方式工作。

  • 某些结构是单线程的:从多线程并发访问总是不安全的。
  • 某些结构是可选加锁的:可以为每个对象告诉libevent是否需要同时从多个线程使用它。
  • 某些结构总是加锁的:如果libevent以支持锁的方式运行,在多线程访问总是安全的。

想在libevent中使用锁,你必须要在libevent分配任何在多线程间共同使用的结构体之前,告诉libevent要使用哪个锁函数。

如果您正在使用pthreads库或本机Windows线程代码,那么您很幸运。有一些预定义的函数可以将Libevent设置为使用正确的pthread或Windows函数。

#ifdef WIN32
int evthread_use_windows_threads(void);
#define EVTHREAD_USE_WINDOWS_THREADS_IMPLEMENTED
#endif
#ifdef _EVENT_HAVE_PTHREADS
int evthread_use_pthreads(void);
#define EVTHREAD_USE_PTHREADS_IMPLEMENTED
#endif

这两个函数成功时返回0,失败时返回-1.

如果你使用一个不同的线程库,你有更多的工作需要做,需要使用你的库来定义如下函数

  • Locks
  • locking
  • unlocking
  • lock allocation
  • lock destruction
  • Conditions
  • condition variable creation
  • condition variable destruction
  • waiting on a condition variable
  • signaling/broadcasting to a condition variable
  • Threads
  • thread ID detection

然后你需要使用evthread_set_lock_callbacksevthread_set_id_callback告诉libevent这些函数。

#define EVTHREAD_WRITE  0x04
#define EVTHREAD_READ   0x08
#define EVTHREAD_TRY    0x10

#define EVTHREAD_LOCKTYPE_RECURSIVE 1
#define EVTHREAD_LOCKTYPE_READWRITE 2

#define EVTHREAD_LOCK_API_VERSION 1

struct evthread_lock_callbacks {
       int lock_api_version;
       unsigned supported_locktypes;
       void *(*alloc)(unsigned locktype);
       void (*free)(void *lock, unsigned locktype);
       int (*lock)(unsigned mode, void *lock);
       int (*unlock)(unsigned mode, void *lock);
};

int evthread_set_lock_callbacks(const struct evthread_lock_callbacks *);

void evthread_set_id_callback(unsigned long (*id_fn)(void));

struct evthread_condition_callbacks {
        int condition_api_version;
        void *(*alloc_condition)(unsigned condtype);
        void (*free_condition)(void *cond);
        int (*signal_condition)(void *cond, int broadcast);
        int (*wait_condition)(void *cond, void *lock,
            const struct timeval *timeout);
};

int evthread_set_condition_callbacks(
        const struct evthread_condition_callbacks *);

evthread_lock_callbacks结构描述了您的锁定回调及其功能。对于上述版本,lock_api_version字段必须设置为EVTHREAD_LOCK_API_VERSIONsupported_locktypes字段必须设置为EVTHREAD_LOCKTYPE_*常量的位掩码,以描述您可以支持哪些锁类型。(从2.0.4-alpha开始,EVTHREAD_LOCK_RECURSIVE是强制性的,EVTHREAD_LOCK_READWRITE未使用。) alloc函数必须返回指定类型的新锁。free函数必须释放指定类型的锁持有的所有资源。lock函数必须尝试以指定的模式获取锁,成功时返回 0,失败时返回非零。unlock函数必须尝试解锁锁,成功时返回 0,失败时返回非零。

锁类型如下:

  • 0

    一个常规的,非递归的锁

  • EVTHREAD_LOCKTYPE_RECURSIVE

    不会阻止已经持有它的线程再次需要它的锁。一旦持有它的线程解锁它的次数与它最初被锁定的次数一样多,其他线程就可以获得该锁。

  • EVTHREAD_LOCKTYPE_READWRITE

    允许多个线程同时读,写的时候只允许一个线程。写排斥其它所有的读。

锁模式如下:

  • EVTHREAD_READ

    仅对读写锁有效,为了读而获取或释放锁。

  • EVTHREAD_WRITE

    仅对读写锁有效,为了写而获取或释放锁。

  • EVTHREAD_TRY

    尝试加锁,仅当立刻可以获取锁的时候才加锁。

id_fn参数必须是一个函数,返回一个unsigned long标识哪个线程正在调用该函数。它必须始终为同一个线程返回相同的数字,并且如果两个不同的线程同时执行,则不能为它们返回相同的数字。

调试event用法

在使用libevent可以检测和报告的事件时,有一些常见的错误。它们包括:

  • 将未初始化的event结构视为已初始化。
  • 尝试重新初始化挂起的event结构。

跟踪libevent初始化的事件结构需要额外的内存和cpu,所以不到万不得已,不要开始调试模式。

void event_enable_debug_mode(void);

此函数需要在event_base被创建之前调用。

使用调试模式时,如果您的程序使用大量由event_assign()[而不是event_new()] 创建的事件,您可能会耗尽内存。发生这种情况是因为Libevent无法判断何时不再使用使用event_assign()创建的事件。(当您对其调用 event_free()时,它可以告诉您event_new()事件已变为无效。)如果您想避免在调试时耗尽内存,您可以明确告诉 Libevent 此类事件不再被视为分配:

void event_debug_unassign(struct event *ev);

当调试模式没有启用的时候调用此函数没有任何影响。

#include <event2/event.h>
#include <event2/event_struct.h>

#include <stdlib.h>

void cb(evutil_socket_t fd, short what, void *ptr)
{
    /* We pass 'NULL' as the callback pointer for the heap allocated
     * event, and we pass the event itself as the callback pointer
     * for the stack-allocated event. */
    struct event *ev = ptr;

    if (ev)
        event_debug_unassign(ev);
}

/* Here's a simple mainloop that waits until fd1 and fd2 are both
 * ready to read. */
void mainloop(evutil_socket_t fd1, evutil_socket_t fd2, int debug_mode)
{
    struct event_base *base;
    struct event event_on_stack, *event_on_heap;

    if (debug_mode)
       event_enable_debug_mode();

    base = event_base_new();

    event_on_heap = event_new(base, fd1, EV_READ, cb, NULL);
    event_assign(&event_on_stack, base, fd2, EV_READ, cb, &event_on_stack);

    event_add(event_on_heap, NULL);
    event_add(&event_on_stack, NULL);

    event_base_dispatch(base);

    event_free(event_on_heap);
    event_base_free(base);
}

详细的事件调试功能只能在编译时使用CFLAGS环境变量-DUSE_DEBUG启用。启用此标志后,任何针对Libevent编译的程序都将输出一个非常详细的日志,详细说明后端的低级活动。这些日志包括但不限于以下内容:

  • 添加event
  • 删除event
  • 平台特定事件通知信息

此功能无法通过 API 调用启用或禁用,因此只能在开发人员构建中使用。

检测libevent的版本

新版本的libevent可能会添加新特性或者修复bug,所以你可能需要检测libevent的版本:

  • 检测已安装的 Libevent 版本是否足以构建您的程序。
  • 显示用于调试的 Libevent 版本。
  • 检测 Libevent 的版本,以便您可以警告用户有关错误或解决它们。
#define LIBEVENT_VERSION_NUMBER 0x02000300
#define LIBEVENT_VERSION "2.0.3-alpha"
const char *event_get_version(void);
ev_uint32_t event_get_version_number(void);

宏返回libevent编译时版本;函数返回运行时版本。注意,如果您已将程序动态链接到Libevent,这些版本可能会有所不同。

您可以获得两种格式的 Libevent 版本:适合向用户显示的字符串,或适合数字比较的 4 字节整数。整数格式使用高字节表示主要版本,第二个字节表示次要版本,第三个字节表示补丁版本,低字节表示发布状态(0 表示发布,非0表示给定发布后的开发系列)。

因此,已发布的 Libevent 2.0.1-alpha 的版本号为 [02 00 01 00] 或 0x02000100。 2.0.1-alpha 和 2.0.2-alpha 之间的开发版本可能具有 [02 00 01 08] 或 0x02000108 的版本号。

#include <event2/event.h>

#if !defined(LIBEVENT_VERSION_NUMBER) || LIBEVENT_VERSION_NUMBER < 0x02000100
#error "This version of Libevent is not supported; Get 2.0.1-alpha or later."
#endif

int
make_sandwich(void)
{
        /* Let's suppose that Libevent 6.0.5 introduces a make-me-a
           sandwich function. */
#if LIBEVENT_VERSION_NUMBER >= 0x06000500
        evutil_make_me_a_sandwich();
        return 0;
#else
        return -1;
#endif
}
Example: Run-time checks
#include <event2/event.h>
#include <string.h>

int
check_for_old_version(void)
{
    const char *v = event_get_version();
    /* This is a dumb way to do it, but it is the only thing that works
       before Libevent 2.0. */
    if (!strncmp(v, "0.", 2) ||
        !strncmp(v, "1.1", 3) ||
        !strncmp(v, "1.2", 3) ||
        !strncmp(v, "1.3", 3)) {

        printf("Your version of Libevent is very old.  If you run into bugs,"
               " consider upgrading.\n");
        return -1;
    } else {
        printf("Running with Libevent version %s\n", v);
        return 0;
    }
}

int
check_version_match(void)
{
    ev_uint32_t v_compile, v_run;
    v_compile = LIBEVENT_VERSION_NUMBER;
    v_run = event_get_version_number();
    if ((v_compile & 0xffff0000) != (v_run & 0xffff0000)) {
        printf("Running with a Libevent version (%s) very different from the "
               "one we were built with (%s).\n", event_get_version(),
               LIBEVENT_VERSION);
        return -1;
    }
    return 0;
}

本节中的宏和函数在<event2/event.h>中定义。event_get_version()函数最早出现在Libevent 1.0c中;其他的首先出现在Libevent 2.0.1-alpha中。

释放全局的libevent结构体

即使您已经释放了使用 Libevent 分配的所有对象,也会留下一些全局分配的结构。这通常不是问题:一旦进程退出,无论如何它们都会被清理。但是拥有这些结构可能会使一些调试工具混淆,认为 Libevent 正在泄漏资源。如果需要确保 Libevent 已经发布了所有内部库全局数据结构,可以调用:

void libevent_global_shutdown(void);

此函数不会释放 Libevent 函数返回给您的任何结构。如果您想在退出前释放所有内容,则需要自己释放所有eventevent_basesbufferevents等。

调用libevent_global_shutdown函数会导致其它libevent函数出现未定义的行为,所以确保在最后再调用此函数。

该函数在<event2/event.h>中声明。它是在Libevent 2.1.1-alpha中引入的。

标签: libevent

添加新评论