Libevent的IO复用技术和定时事件原理

  Libevent 是一个用C语言编写的、轻量级的开源高性能网络库,主要有以下几个亮点:事件驱动( event-driven),高性能;轻量级,专注于网络,不如 ACE 那么臃肿庞大;源代码相当精炼、易读;跨平台,支持 Windows、 Linux、 *BSD 和 Mac Os;支持多种 I/O 多路复用技术, epoll、 poll、 dev/poll、 select 和 kqueue 等;支持 I/O,定时器和信号等事件;注册事件优先级。

1 Libevent中的epoll

  Libevent重要的底层技术之一就是IO复用函数,比如Linux的epoll、windows下的select。Libevent的epoll相关的函数在epoll.c文件中,为了方便使用epoll对事件的操作,定义了一个epollop结构体。

struct epollop {
    struct epoll_event *events;
    int nevents;
    int epfd;
};

  其中,events指针用于存放就绪的事件,也就是内核会拷贝就绪的事件到这个events指向的内存中;nevents表示events指向的内存为多大,也就是可以存放多少个epoll_event类型的数据;epfd也就是调用epoll_create()返回的内核事件表对应的描述符。

  Libevent为了封装IO复用技术,定义了一个统一的事件操作结构体eventop:

/** Structure to define the backend of a given event_base. */
struct eventop {
    /* 后端IO复用技术的名称 */
    const char *name;
    
    void *(*init)(struct event_base *);
    int (*add)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
    int (*del)(struct event_base *, evutil_socket_t fd, short old, short events, void *fdinfo);
    int (*dispatch)(struct event_base *, struct timeval *);
    
    /* 释放IO复用机制使用的资源 */
    void (*dealloc)(struct event_base *);
    
    int need_reinit;
    
    /* IO复用机制支持的一些特性,可选如下3个值的按位或:EV_FEATURE_ET(支持边沿触发事件EV_ET)、
     * EV_FEATURE_O1(事件监测算法的复杂度为O(1))和EV_FEATURE_FDS(不仅能监听socket上的事件,还能
     * 监听其他类型的文件描述符上的事件) */
    enum event_method_feature features;
    
    /* 有些IO复用机制需要为每个IO事件队列和信号事件队列分配额外的内存,以避免同一个文件描述符被重复
     * 插入IO复用机制的事件表中。evmap_io_add(或evmap_io_del)函数最亲爱调用eventop的add(或del)方法时,
     * 将这段内存的起始地址作为第5个参数传递给add(或del)方法。fdinfo_len指定了该段内存的长度 */ 
    size_t fdinfo_len;
};

  对于epoll来说,封装的事件操作为:

const struct eventop epollops = {
    "epoll",
    epoll_init,
    epoll_nochangelist_add,
    epoll_nochangelist_del,
    epoll_dispatch,
    epoll_dealloc,
    1, /* need reinit */
    EV_FEATURE_ET|EV_FEATURE_O1,
    0
};

  结构体中的函数都是在epoll.c中定义好的,并且都是static的,但是只需要并且也只能通过epollops变量来调用这些函数了,epoll相关的函数就不在赘述,详情可以参考源代码。那么Libevent是什么时候来获取这个变量的值呢?秘密就在event.c文件中,其中定义的eventops数组包含了支持的所有IO复用技术,当然包括我们讲的epoll了。

/* Libevent通过遍历eventops数组来选择其后端IO复用技术,遍历的顺序是从数组的第一个元素开始,
 * 到最后一个元组结束。Linux系统下,默认选择的后端IO复用技术是epoll。*/
static const struct eventop *eventops[] = {
#ifdef _EVENT_HAVE_EVENT_PORTS
    &evportops,
#endif
#ifdef _EVENT_HAVE_WORKING_KQUEUE
    &kqops,
#endif
#ifdef _EVENT_HAVE_EPOLL
    &epollops,
#endif
#ifdef _EVENT_HAVE_DEVPOLL
    &devpollops,
#endif
#ifdef _EVENT_HAVE_POLL
    &pollops,
#endif
#ifdef _EVENT_HAVE_SELECT
    &selectops,
#endif
#ifdef WIN32
    &win32ops,
#endif
    NULL
};

  在event_base的初始化函数event_base_new_with_config中,会遍历eventops数组,选择其中符合要求的IO复用机制,然后退出遍历过程,这样event_base就选择了一个后端的IO复用机制,比如Libevent在Linux下默认是使用epoll的。

for (i = 0; eventops[i] && !base->evbase; i++) {
    // ...

    /* also obey the environment variables */
    if (should_check_environment &&
        event_is_method_disabled(eventops[i]->name))
        continue;

    /* base->evsel记录后端IO复用机制 */
    base->evsel = eventops[i];

    /* 指向IO复用机制真正存储的数据,它通过evsel成员的init函数来进行初始化 */
    /* 比如epoll时,evbase指向的是epollop */
    base->evbase = base->evsel->init(base);
}

   到这里为止,Libevent已经初始化好了一种后台IO复用机制技术,这里以epoll为例,其他IO复用技术流程也类似。

 

2 Libevent的定时事件原理

  Libevent的定时事件也是要"加入"到Libevent中的IO复用框架中的,比如我们需要定时5秒钟,那么等到5秒钟之后就可以执行对应设置的回调函数了。以下是使用Libevent实现一个简单的定时器应用:

#include 

#include <event.h>
#include 

using namespace std;

// Time callback function
void onTime(int sock, short event, void *arg)
{
    static int cnt = 0;
    cout << "Game Over! " << cnt++ << endl;

    struct timeval tv;
    tv.tv_sec = 1;
    tv.tv_usec = 0;
    if (cnt < 5) {
        // Add timer event
        event_add((struct event *) arg, &tv);
    }
    else {
        cout << "onTime is over" << endl;
    }
}

int main(int argc, char **argv)
{
    cout << event_get_version() << endl;

    struct event_base *base = event_init();
    struct event ev;

    evtimer_set(&ev, onTime, &ev);

    struct timeval timeevent;
    timeevent.tv_sec = 1;
    timeevent.tv_usec = 0;

    event_add(&ev, &timeevent);

    // Start event loop
    event_base_dispatch(base);
    event_base_free(base);

    return 0;
}

  定时器事件会被加入到一个时间堆(小堆结构)中,每次执行事件等待函数时,对于epoll来说就是epoll_wait函数了,把时间堆上最小节点的时间值赋值给该函数,这样如果有事件来临或者是时间超时了,都会返回。然后判断当前时间和调用事件等待函数之前的时间差是否大于或等于时间堆上最小节点的时间值,如果条件成立就执行对应的时间回调函数,这样就完成了一个定时事件。下面代码就是在事件监听循环中的部分代码。

while (!done) {
    base->event_continue = 0;

    /* Terminate the loop if we have been asked to */
    if (base->event_gotterm) {
        break;
    }

    if (base->event_break) {
        break;
    }

    timeout_correct(base, &tv);    /* 校准系统时间 */

    tv_p = &tv;
    if (!N_ACTIVE_CALLBACKS(base) && !(flags & EVLOOP_NONBLOCK)) {
        /* 获取时间堆上堆顶元素的超时值,即IO复用系统调用本次应该设置的超时值 */
        timeout_next(base, &tv_p);
    } else {
        /*
         * if we have active events, we just poll new events
         * without waiting.
         */
        /* 如果有就绪事件尚未处理,则将IO复用系统调用的超时时间置0
         * 这样IO复用系统调用就直接返回,程序也就可以直接处理就绪事件了 */
        evutil_timerclear(&tv);
    }

    /* If we have no events, we just exit */
    /* 如果event_base中没有注册任何事件,则直接退出事件循环 */
    if (!event_haveevents(base) && !N_ACTIVE_CALLBACKS(base)) {
        event_debug(("%s: no events registered.", __func__));
        retval = 1;
        goto done;
    }

    /* update last old time */
    gettime(base, &base->event_tv); /* 更新系统时间 */

    clear_time_cache(base);

    /* 调用事件多路分发器的dispatch方法等待事件 */
    res = evsel->dispatch(base, tv_p);

    // 超时时间返回值为0
    if (res == -1) {
        event_debug(("%s: dispatch returned unsuccessfully.",
            __func__));
        retval = -1;
        goto done;
    }

    update_time_cache(base);    /* 将系统缓存更新为当前系统时间 */

    timeout_process(base);    /* 检查时间堆上的到期事件并以此执行之 */

    if (N_ACTIVE_CALLBACKS(base)) {
        /* 调用event_process_active函数依次处理就绪的信号事件和IO事件 */
        /* 这里也可能有定时事件 */
        int n = event_process_active(base);
        if ((flags & EVLOOP_ONCE)
            && N_ACTIVE_CALLBACKS(base) == 0
            && n != 0)
            done = 1;
    } else if (flags & EVLOOP_NONBLOCK)
        done = 1;
}

 

参考:

  1、Libevent初探

  2、Libevent源码

你可能感兴趣的:(Libevent的IO复用技术和定时事件原理)