Libevent详解与实践(一)

一、设置libevent库

Libevent具有一些在整个进制中共享的会影响整个库的全局设置。

在调用Libevent库的任何其他部分之前,必须对这些设置进行任何更改。 如果您不这样做,Libevent可能会处于不一致状态。

1. 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);

要覆盖Libevent的日志记录行为,请编写与event_log_cb参数匹配的自己的函数,并将其作为参数传递给event_set_log_callback()。 只要Libevent想要记录一条消息,它将把它传递给该函数。 可以通过以NULL作为参数再次调用event_set_log_callback()来使Libevent恢复其默认行为。

示例

#include 
#include 

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

注意

在用户提供的event_log_cb回调中调用Libevent函数是不安全的! 例如,如果您尝试编写一个使用bufferevents向网络套接字发送警告消息的日志回调,则很可能会遇到奇怪且难以诊断的错误。 在将来的Libevent版本中,某些功能可能会取消此限制。

通常,调试日志不会启用,并且不会发送到日志回调。 如果Libevent要支持它们,则可以手动将其打开。

接口

#define EVENT_DBG_NONE 0
#define EVENT_DBG_ALL 0xffffffffu

void event_enable_debug_logging(ev_uint32_t which);

调试日志很冗长,在大多数情况下不一定有用。 使用EVENT_DBG_NONE调用event_enable_debug_logging()将获得默认行为。 使用EVENT_DBG_ALL调用它会打开所有受支持的调试日志。 将来的版本中可能会支持更多细致的选项。

这些函数在中声明。 它们首先出现在Libevent 1.0c中,除event_enable_debug_logging()首次出现在Libevent 2.1.1-alpha中。

兼容性说明

在Libevent 2.0.19-稳定版本之前,EVENT_LOG_ *宏的名称以下划线开头:_EVENT_LOG_DEBUG,_EVENT_LOG_MSG,_EVENT_LOG_WARN和_EVENT_LOG_ERR。 这些较早的名称已被弃用,仅应用于与Libevent 2.0.18-stable和更早版本的向后兼容。 在将来的Libevent版本中可能会删除它们。

2. 处理致命错误

当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函数。

这些函数在中声明。首先出现在Libevent 2.0.3-alpha中。

3. 内存管理

默认情况下,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 
#include 
#include 

/* 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分配,调整大小或释放内存的调用。因此,在调用任何其他Libevent函数之前,需要确保已替换函数。否则,Libevent将使用你的free版本释放从C库的malloc版本分配的内存。

  • 你的malloc和realloc函数需要返回与C库相同的内存块。

  • 你的realloc函数需要正确处理realloc(NULL,sz)(即,将其视为malloc(sz))。

  • 你的realloc函数需要正确处理realloc(ptr,0)(也就是说,将其视为free(ptr))。

  • 你的free函数不需要处理free(NULL)。

  • 你的malloc函数不需要处理malloc(0)。

  • 如果从多个线程中使用Libevent,则替换后的内存管理功能必须是线程安全的。

  • Libevent将使用这些函数来分配返回给您的内存。因此,如果要释放由Libevent函数分配和返回的内存,并且已经替换了malloc和realloc函数,则可能必须使用替换free函数来释放它。

中声明了event_set_mem_functions()函数。它首先出现在Libevent 2.0.1-alpha中。

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

4. 锁和线程

编写多线程程序,在多个线程同时访问相同的数据并不总是安全的。

Libevent结构通常可以在多线程中以三种方式工作。

  • 有些结构本质上是单线程的:从多个线程同时使用它们永远是不安全的。

  • 某些结构是有可选的锁:可以告诉每个对象Libevent是否需要在多线程使用每个对象。

  • 某些结构始终是锁定的:如果Libevent在锁定支持下运行,则始终可以安全地多个线程使用它们。

为获取锁,在调用分配需要在多个线程间共享的结构体的 libevent 函数之前,必须告知 libevent 使用哪个锁函数。

如果使用 pthreads 库,或者使用 Windows 本地线程代码,那么已经有设置 libevent 使用正确的 pthreads 或者 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。

如果使用不同的线程库,则需要一些额外的工作,必须使用你的线程库来定义函数去实现:

  • 锁定

  • 解锁

  • 分配锁

  • 析构锁

  • 条件变量

  • 创建条件变量

  • 析构条件变量

  • 等待条件变量

  • 触发/广播某条件变量

  • 线程

  • 线程 ID 检测

使用 evthread_set_lock_callbacks 和 evthread_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_VERSION 。 必 须 设 置

supported_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 参数必须是一个函数,它返回一个无符号长整数,标识调用此函数的线程。对于相同线程,这个函数应该总是返回同样的值;而对于同时调用该函数的不同线程,必须返回不同的值。evthread_condition_callbacks 结构体描述了与条件变量相关的回调函数。对于上述版本,condition_api_version 字 段 必 须 设 置 为EVTHREAD_CONDITION_API_VERSION 。alloc_condition 函数必须返回到新条件变量的指针。它接受0作为其参数。free_condition 函数必须释放条件变量持有的存储器和资源。wait_condition 函数要求三个参数:一个由alloc_condition 分配的条件变量,一个由你提供的 evthread_lock_callbacks.alloc 函数分配的锁,以及一个可选的超时值。调用本函数时,必须已经持有参数指定的锁;本函数应该释放指定的锁,等待条件变量成为授信状态,或者直到指定的超时时间已经流逝(可选 )。wait_condition 应该在错误时返回-1,条件变量授信时返回0,超时时返回1。返回之前,函数应该确定其再次持有锁。最后,signal_condition 函数应该唤醒等待该条件变量的某个线程(broadcast 参数为 false 时),或者唤醒等待条件变量的所有线程(broadcast 参数为 true 时)。只有在持有与条件变量相关的锁的时候,才能够进行这些操作。

关于条件变量的更多信息,请查看 pthreads 的 pthread_cond_*函数文档,或者 Windows的 CONDITION_VARIABLE(Windows Vista 新引入的)函数文档。

示例

关于使用这些函数的示例,请查看 Libevent 源代码发布版本中的 evthread_pthread.c 和 evthread_win32.c 文件。

这些函数在中声明,其中大多数在 2.0.4-alpha 版本中首次出现。2.0.1-alpha 到2.0.3-alpha 使用较老版本的锁函数。event_use_pthreads 函数要求程序链接event_pthreads 库。

条件变量函数是2.0.7-rc 版本新引入的,用于解决某些棘手的死锁问题。

可以创建禁止锁支持的 libevent。这时候已创建的使用上述线程相关函数的程序将不能运行。

5. 调试锁的使用

为帮助调试锁的使用,libevent 有一个可选的“锁调试”特征。这个特征包装了锁调用,以便捕获典型的锁错误,包括:

  • 解锁并没有持有的锁

  • 重新锁定一个非递归锁

如果发生这些错误中的某一个,libevent 将给出断言失败并且退出。

接口

void evthread_enable_lock_debugging(void);
#define evthread_enable_lock_debuging() evthread_enable_lock_debugging()

注意

必须在创建或者使用任何锁之前调用这个函数。为安全起见,请在设置完线程函数后立即调用这个函数。

此功能是Libevent 2.0.4-alpha中的新增功能,拼写错误的名称为“ evthread_enable_lock_debuging()”。 拼写固定为2.1.2-alpha中的evthread_enable_lock_debugging(); 目前都支持这两个名称。

6. 调试事件的使用

libevent 可以检测使用事件时的一些常见错误并且进行报告。这些错误包括:

  • 将未初始化的 event 结构体当作已经初始化的

  • 试图重新初始化未决的 event 结构体

跟踪哪些事件已经初始化需要使用额外的内存和处理器时间,所以只应该在真正调试程序的时候才启用调试模式。

接口

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

没有启用调试的时候调用 event_debug_unassign 没有效果。

示例

#include 
#include 

#include 

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编译的任何程序都将输出非常详细的日志,详细说明后端的低级活动。 这些日志包括但不限于以下内容:

  • 活动添加

  • 事件删除

  • 平台特定的事件通知信息

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

这些调试功能已添加到Libevent 2.0.4-alpha中。

7.检测libevent的版本

新版本的 libevent 会添加特征,移除 bug。有时候需要检测 libevent 的版本,以便:

  • 检测已安装的 libevent 版本是否可用于创建你的程序

  • 为调试显示 libevent 的版本

  • 检测 libevent 的版本,以便向用户警告 bug,或者提示要做的工作

接口

#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,这两个版本可能不同。

可以获取两种格式的 libevent 版本:用于显示给用户的字符串版本,或者用于数值比较的4字节整数版本。整数格式使用高字节表示主版本,低字节表示副版本,第三字节表示修正版本,最低字节表示发布状态: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 

#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
}

示例:运行时检测

#include 
#include 

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

本节描述的宏和函数定义在中。event_get_version 函数首次出现在1.0c版本;其他的首次出现在2.0.1-alpha 版本。

8. 释放全局的Libevent结构

即使释放了使用Libevent分配的所有对象,也将剩下一些全局分配的结构。通常这不是问题:退出流程后,无论如何都将对其进行清理。但是拥有这些结构可能会使某些调试工具误以为Libevent正在泄漏资源。如果需要确保Libevent已发布所有内部库全局数据结构,则可以调用:

接口

void libevent_global_shutdown(void);

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

调用libevent_global_shutdown()将使其他Libevent函数的行为无法预期。除了作为您的程序调用的最后一个Libevent函数外,不要调用它。一个例外是libevent_global_shutdown()是幂等(idempotent)的:可以调用它,即使它已经被调用也可以。

此函数在中声明。它是在Libevent 2.1.1-alpha中引入的。

欢迎关注我的公众号.png

你可能感兴趣的:(Libevent详解与实践(一))