libevent源码学习----io多路复用的封装和使用

因为是非阻塞监听事件的发生,所以内部其实还是采用io多路复用函数实现的。
又因为可供选择的io函数很多,linux下有epoll, poll, select等,window下有ICOP, select等,所以libevent需要在程序启动时选择一个合适的io多路复用函数,合适的依据是

  • 系统支持,为了实现跨平台
  • io函数的效率尽量高
  • 用户是否主动设置了不想使用的io函数

为了解决跨平台,libevent对所有的io函数都进行了各自的封装
为了解决效率问题,libevent在选择时,从效率高的开始选
为了解决用户设置,libevent为每一个io函数提供一个名字,用户人为设置不想使用的io函数时也是传送io函数名字,libevent维护一个字符串队列,选择不在这个队列中的io函数


以下程序就是libevent如何初始化io多路复用函数的

/* 由event_base_new调用 */
struct event_base *
event_base_new_with_config(const struct event_config *cfg)
{
    int i;
    struct event_base *base;

    /* ... */

    /* 
     * 为了实现让base可以根据系统需要或者用户的需要调用不同的io复用函数,
     * 比如说系统可能不支持某个io复用函数,又或者是用户指明不想要使用
     * 哪个io复用函数,指明不想只要哪个函数可以通过调用带有config的函数提供
     * 创建base的配置
     * 为了解决这种情况,在全局变量中有一个eventopts数组,这个数组中存储着
     * 所有系统支持的io复用函数,每个io复用函数都是一个结构体实例化对象
     * 可以在每一个io函数的文件,比如select.c中看到
     * 而evbase存储的不是使用的io函数,而是使用的io函数对应的数据结构体
     * 其实就是存数据的,在io函数文件中也可以看到
     * 下面的for循环是为了找到第一个可用的io函数,因为数组中的函数是按效率
     * 排序的
     */
    base->evbase = NULL;



    /* 如上 */
    for (i = 0; eventops[i] && !base->evbase; i++) {
        if (cfg != NULL) {
            /* determine if this backend should be avoided */
            if (event_config_is_avoided_method(cfg,
                eventops[i]->name))
                continue;
            if ((eventops[i]->features & cfg->require_features)
                != cfg->require_features)
                continue;
        }

        base->evsel = eventops[i];
        /* 调用对应io函数的初始化函数
         * 注意:在这个函数内部同时对信号进行了初始化,其实是创建了一个socketpair
         * 目的是将信号统一成event,见evsig_init
         */
        base->evbase = base->evsel->init(base);
    }

    /* 如果没有找到可用的io函数,会出错返回,同时清除已经创建的base */
    if (base->evbase == NULL) {
        event_warnx("%s: no event mechanism available",
            __func__);
        base->evsel = NULL;
        event_base_free(base);
        return NULL;
    }

    /* ... */

    return (base);
}

全局io函数数组,以此实现跨平台,根据预编译头判断系统是否支持某个io函数,从而构造出一个存储着所有可用的io多路复用函数的数组,初始化base时,只需要筛选出用户允许的即可

/* Array of backends in order of preference. */
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
};

接下来单独看每一个io函数的封装

select的封装,由此可见每个io函数的封装都是定义一个struct eventop类型的变量,将对应io函数的接口指针存储着,这就将所有的io函数都统一起来,不需要特定io调用特定接口

const struct eventop selectops = {
    "select",
    select_init,
    select_add,
    select_del,
    select_dispatch,
    select_dealloc,
    0, /* doesn't need reinit. */
    EV_FEATURE_FDS,
    0,
};

此外libevent也对每个io函数使用的数据类型进行了封装,比如说epoll_event,pollfd以及fd_set

对select的fd_set进行的封装,为什么read/write都有两份,可以参考几种服务器模型以及io多路复用函数中的select部分

struct selectop {
    int event_fds;      /* Highest fd in fd set */
    int event_fdsz;
    int resize_out_sets;
    fd_set *event_readset_in;
    fd_set *event_writeset_in;
    fd_set *event_readset_out;
    fd_set *event_writeset_out;
};

对于epoll也是如此,libevent内部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
};

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

可以发现,每个io多路复用函数的封装都是遵循struct eventop类型的,所以base中只需要存储着eventop类型的指针evsel,在初始化它之后只需要调用struct eventop提供的接口函数,就可以直接调用io多路复用函数的接口函数,实现了统一

而对于每个io多路复用函数的数据类型,libevent没有进行统一的封装,因为也没有必要。在初始化base中

/* 
 * evbase存储的就是对应的数据结构,它是个void*指针,所以可以存储任意类型的结构,比如
 * 对于select而言是struct selectop,
 * 对于epoll而言是struct epollop
 * /
base->evbase = base->evsel->init(base);

libevent中io多路复用的使用体现在

  • 新建event注册到base中,此时会把监听的fd和事件添加到io复用中,本质上就是调用epoll_ctl,FD_SET等
  • 删除event从base中,会把监听的fd从io复用中删除,本质上调用epoll_ctl等
  • 开启事件驱动循环,监听事件的发生,本质上调用各种wait函数如epoll_wait,select,poll等
    比如添加event
//函数将event添加到base的io map和io复用函数的监听事件中
int
evmap_io_add(struct event_base *base, evutil_socket_t fd, struct event *ev)
{
    /* ... */

    if (evsel->add(base, ev->ev_fd,
        old, (ev->ev_events & EV_ET) | res, extra) == -1)
        return (-1);

    /* ... */
}

//将event从io map中删除,由event_del_internal调用
int
evmap_io_del(struct event_base *base, evutil_socket_t fd, struct event *ev)
{
    /* ... */

    if (evsel->del(base, ev->ev_fd, old, res, extra) == -1)
        return (-1);

    /* ... */
}
/*
 * 实际的事件驱动循环,其实就是一个while循环,每次调用io复用函数进行事件监听
 * 监听返回之前将活跃的event都按优先级添加到base的激活队列中
 * 回到循环后对base的激活队列中的event按照优先级顺序调用回调函数
 * 再根据是否是永久event决定要不要从base的所有队列中删除event
 * 对于具有超时时间的event则需要特殊处理,见timeout_process
 */
int
event_base_loop(struct event_base *base, int flags)
{
    const struct eventop *evsel = base->evsel;
    struct timeval tv;
    struct timeval *tv_p;
    int res, done, retval = 0;

    /* ... */

    done = 0;
    while (!done) {

        /* ... */

        /*
         * 调用Io复用函数的监听函数,开始阻塞/非阻塞的监听
         * 超时时间设置为最小堆中堆顶event的超时时间,原因如下
         * 
         * 此时监听的有三种event
         * 第一种是没有设置超时时间的,包括信号,所以什么时候返回都不影响
         * 第二种是取得最小超时时间的堆顶event,此时可以满足在超时时间返回
         * 第三种是最小堆中的其他event,这些event的超时时间在堆顶event之后,因为超时时间是绝对时间
         *      也就是说如果堆顶event没有超时,那么其它的event将不可能超时
         *      而当最小超时时间后返回处理超时之后重新开始监听,
         *      因为是绝对时间,所以不会影响最小堆的其他event的超时
         *
         * 在返回之间,将活跃的event添加到base的激活队列中
         * 
         * 注意:不处理具有超时时间的event,因为这些event根本就没有添加到io函数中
         * 处理这些是在timeout_process函数中
         */
        res = evsel->dispatch(base, tv_p);


         /* ... */
    }

     /* ... */
    return (retval);
}

其实都是间接调用每一个io接口


总结
这部分主要学习到libevent是如何实现跨平台的io多路复用函数的选择的,所谓跨平台,就是将所有可能的平台都考虑到。同时看到libevent是如何把所有io函数都进行统一的,这一点很值得学习
题外话,其实对io的封装就是基类纯虚函数加各种派生类,用基类指针实现多态….

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值