您的位置:首页 > 其它

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

2017-10-12 15:50 507 查看
因为是非阻塞监听事件的发生,所以内部其实还是采用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的封装就是基类纯虚函数加各种派生类,用基类指针实现多态….
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息