目录
为什么evmap_io_active的第三个参数需要或上一个EV_ET?
以下源码均基于libevent-2.0.21-stable。
在event_base配置中提到过,libevent中封装了多种IO复用模型,当创建一个event_base的时候,libevent就会自动选定一种支持的IO复用模型作为该event_base的后端(back-end),后续真正实现事件添加、监听、删除等操作也是通过所选后端来实现的。因此,本文就主要以epoll为例,分析libevent是如何封装epoll这一IO复用模型的。
Libevent提供的epoll后端结构体
在libevent中,提供了多种IO复用模型,可以从全局数组eventops中看到,如下所示:
static const struct eventop *eventops[] = { //包含了各种可使用的backups的函数结构体
#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
};
在linux下,如果创建event_base的时候没有什么特殊的cfg配置要求,那么libevent就会自动分配epoll作为event_base的后端,其对应于这里的epollops,这也是一个结构体,其中包含了相关的函数,如下所示:
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的使用,主要分为3步:1.通过epoll_create创建一个epoll实例;2.通过epoll_ctl向前面创建的epoll实例中添加、修改或删除需要监听的事件;3.通过epoll_wait来返回epoll的就绪事件链表。libevent中的epoll后端主要通过5个函数,来实现对epoll的封装,下面就来分析一下这5个函数:
初始化epoll_init
epoll_init函数定义如下:
static void *
epoll_init(struct event_base *base)
{
int epfd;
struct epollop *epollop;
/* Initialize the kernel queue. (The size field is ignored since
* 2.6.8.) */
if ((epfd = epoll_create(32000)) == -1) { //创建一个可以监听32000个文件描述符的epoll,其中包含1个epoll_create返回的文件描述符
if (errno != ENOSYS)
event_warn("epoll_create");
return (NULL);
}
evutil_make_socket_closeonexec(epfd); //在调用exec时关闭epfd
if (!(epollop = mm_calloc(1, sizeof(struct epollop)))) {
close(epfd);
return (NULL);
}
epollop->epfd = epfd; //epoll base的文件描述符
/* Initialize fields */
epollop->events = mm_calloc(INITIAL_NEVENT, sizeof(struct epoll_event));//这里实际上开辟的是epoll_event的数组,数组元素为epoll_event类型,元素个数初始化为32
if (epollop->events == NULL) {
mm_free(epollop);
close(epfd);
return (NULL);
}
epollop->nevents = INITIAL_NEVENT; //events数组大小
if ((base->flags & EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST) != 0 ||
((base->flags & EVENT_BASE_FLAG_IGNORE_ENV) == 0 &&
evutil_getenv("EVENT_EPOLL_USE_CHANGELIST") != NULL))
base->evsel = &epollops_changelist;
evsig_init(base);
return (epollop);
}
在epoll_init函数中,创建了一个可监听30000个文件描述符的epoll对象,并且把epoll对象的文件描述符保存在了epfd中。然后创建了一个epollop结构体,该结构体定义如下:
struct epollop {
struct epoll_event *events; //epoll_event数组,epoll_wait监听激活的epoll_event都会放到这里
int nevents; //events数组长度,通过限定events数组的长度来限制epoll_wait最多可以接受多少激活的事件
int epfd; //epoll的文件描述符
};
也就是说,每一个epoll对象都会对应这样一个epollop结构体,其中含有一个events成员,通过epoll_init对于该成员的内存分配可以知道,events实际上是一个epoll_event数组,由此大概也能猜到,这个events应当就是用来存储最终epoll激活的那些epoll_event。而nevents则是这个数组的长度。
另一个还需注意的地方是,epoll_init函数的返回值就是这个epollop,从上面的分析也可以知道,epollop结构体记录了epoll的文件描述符,以及未来用来存放就绪epoll_event的数组及其大小,那么epoll_init是在什么时候被调用的呢?
何时调用epoll_init
在创建event_base的时候,event_base_new_with_config内部会调用epoll_init函数,如下所示:
struct event_base *
event_base_new_with_config(const struct event_config *cfg)
{
......
for (i = 0; eventops[i] && !base->evbase; i++) { //找到合适的后端
......
base->evsel = eventops[i]; //保存后端对应的后端结构体
base->evbase = base->evsel->init(base); // 用选定的backup的初始化函数来初始化base中的evbase
}
......
}
在event_base_new_with_config函数中,当选定了一个符合条件的后端IO模型后,就会直接将这个后端的函数结构体保存到event_base的evsel中,并且调用所选后端的init函数(比如说这里的epoll_init函数),返回一个epollop结构体保存到event_base的evbase成员中。这样,当我们创建一个event_base之后,实际上就是创建了一个epoll对象,这个epoll对象的必要信息和相关函数就都放在了event_base的evsel和evbase成员中。
事件添加epoll_nochangelist_add
参考epoll使用的流程,大概也能想到,这个函数的作用会与epoll_ctl函数相关了。该函数定义如下:
static int
epoll_nochangelist_add(struct event_base *base,