目录
Libevent提供的epoll后端结构体
初始化epoll_init
何时调用epoll_init
事件添加epoll_nochangelist_add
何时调用epoll_nochangelist_add
事件删除epoll_nochangelist_del
何时调用epoll_nochangelist_del
事件监听epoll_dispatch
为什么evmap_io_active的第三个参数需要或上一个EV_ET?
何时调用epoll_dispatch
epoll销毁epoll_dealloc
何时调用epoll_dealloc
以下源码均基于libevent-2.0.21-stable。
在event_base配置中提到过,libevent中封装了多种IO复用模型,当创建一个event_base的时候,libevent就会自动选定一种支持的IO复用模型作为该event_base的后端(back-end),后续真正实现事件添加、监听、删除等操作也是通过所选后端来实现的。因此,本文就主要以epoll为例,分析libevent是如何封装epoll这一IO复用模型的。
在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函数定义如下:
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是在什么时候被调用的呢?
在创建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使用的流程,大概也能想到,这个函数的作用会与epoll_ctl函数相关了。该函数定义如下:
static int
epoll_nochangelist_add(struct event_base *base, evutil_socket_t fd,
short old, short events, void *p)
{
struct event_change ch; //event_change需要告诉epoll,事件的相关信息以及执行的操作类型(添加、修改、删除)
ch.fd = fd;
ch.old_events = old; //old中保留所有相同fd下除了这里需要添加的event以外的所有event的事件类型有哪些种类
ch.read_change = ch.write_change = 0;
if (events & EV_WRITE) //如果需要添加的事件对写事件感兴趣,那么就需要在写端执行添加操作,并且记录下是否设置了ET模式
ch.write_change = EV_CHANGE_ADD |
(events & EV_ET);
if (events & EV_READ) //如果需要添加的事件对读事件感兴趣,那么就需要在读端执行添加操作,并且记录下是否设置了ET模式
ch.read_change = EV_CHANGE_ADD |
(events & EV_ET);
//ch中的read_change和write_change用来告诉epoll_ctl,在读端和写端分别需要执行什么操作,
//ch中的old_event告诉epoll_ctl,fd对应的所有event有哪些感兴趣的事件类型
return epoll_apply_one_change(base, base->evbase, &ch);
}
这里需要传入多个参数,在此之前, 可以先来想一下epoll_ctl需要哪些参数。epoll_ctl需要4个参数:epoll的文件描述符epfd、操作类型op、监听的文件描述符fd以及一个包含监听事件类型的epoll_event结构体。如果这里的epoll_nochangelist_add想添加一个监听事件,那么自然就需要通过知道这4个参数的值。
对于epoll的文件描述符和监听的文件描述符,可以通过epoll_nochangelist_add的第一个和第二个参数得到。那么如果确定操作类型以及需要监听的事件类型呢?
这里出现了一个event_change结构体,该结构体定义如下:
struct event_change { //用于告诉epoll,针对于event所需执行的操作(添加、修改、删除)以及事件的相关信息
/** The fd or signal whose events are to be changed */
evutil_socket_t fd; //io事件的fd/signal事件的sig
/* The events that were enabled on the fd before any of these changes
were made. May include EV_READ or EV_WRITE. */
short old_events; //fd/sig对应的所有感兴趣事件种类,可能含有EV_READ或者EV_WRITE
/* The changes that we want to make in reading and writing on this fd.
* If this is a signal, then read_change has EV_CHANGE_SIGNAL set,
* and write_change is unused. */
ev_uint8_t read_change; //fd读端需要执行的操作
ev_uint8_t write_change; //fd写端需要执行的操作
};
结合这个结构体再来说一下epoll_nochangelist_add中ch变量的作用:ch的fd中保存了待添加事件对应的文件描述符;ch的old_events中保存的是在当前事件未添加的情况下,fd中已添加的感兴趣事件类型;ch的read_change和write_change则分别表示在fd读端或写端需要执行的操作,读写两端需要执行的操作是根据当前传入的事件类型决定的,而old_events则保留的是在当前事件未传入之前fd对应的事件监听的状态。old_events、read_change和write_change则决定了最终调用epoll_ctl时的操作类型op。
回到epoll_nochangelist_add函数中,如果需要添加监听的事件类型为读事件,那么就会认为需要在fd的读端进行添加操作,因此设置read_change为EV_CHANGE_ADD,同理也会设置write_change。
最后还会调用epoll_apply_one_change函数,该函数定义如下:
static int
epoll_apply_one_change(struct event_base *base,
struct epollop *epollop,
const struct event_change *ch)
{
struct epoll_event epev;
int op, events = 0;
if (1) {
if ((ch->read_change & EV_CHANGE_ADD) ||
(ch->write_change & EV_CHANGE_ADD)) { //如果读端或者写端需要进行添加操作
/* If we are adding anything at all, we'll want to do
* either an ADD or a MOD. */
events = 0;
op = EPOLL_CTL_ADD; ////设置epoll_ctl的操作类型为添加
if (ch->read_change & EV_CHANGE_ADD) { //如果是读端需要进行添加操作,那么epoll监听fd的类型就应当设置EPOLLIN
events |= EPOLLIN;
} else if (ch->read_change & EV_CHANGE_DEL) { //如果是读端还需要进行删除操作,又添加又删除那么就不做任何操作
;
} else if (ch->old_events & EV_READ) { //如果fd对应的所有事件中存在读事件,那么监听fd的时候就应当设置EPOLLIN
events |= EPOLLIN;
}
if (ch->write_change & EV_CHANGE_ADD) { //如果是写端需要执行添加操作,那么epoll监听fd的类型就应当设置EPOLLOUT
events |= EPOLLOUT;
} else if (ch->write_change & EV_CHANGE_DEL) {//如果是写端还需要进行删除操作,又添加又删除那么就不做任何操作
;
} else if (ch->old_events & EV_WRITE) { //如果fd对应的事件中存在写事件,那么epoll监听fd的类型就应当设置EPOLLOUT
events |= EPOLLOUT;
}
if ((ch->read_change|ch->write_change) & EV_ET)//如果读写端都设置为ET,那么epoll监听fd的类型也设置EPOLLET
events |= EPOLLET;
if (ch->old_events) {//如果本身fd下就有读或写事件,那么就设置操作类型为修改,如果本身fd下没有读和写事件,那么操作类型就是添加
op = EPOLL_CTL_MOD; //设置epoll_ctl的操作类型为修改
}
} ......
}
可以看到,epoll_apply_one_change最终是会调用epoll_ctl函数,并且定义了一个events变量用来描述epoll需要监听的事件类型,以及一个op变量用来描述epoll_ctl的操作类型。
对于epoll监听的事件类型,libevent的策略为:如果是在fd的读端需要添加一个事件,或者在添加新的监听事件之前fd就已经设置过读事件了,那么监听的事件类型就应该包含EPOLLIN;如果是在fd的写端需要添加一个事件,或者在添加新的监听事件之前fd就已经设置过写事件了,那么监听的事件类型就应该包含EPOLLOUT。如果读端和写端都设置了ET,那么新的监听事件也应当为ET;
由此也可以看到,考虑epoll使用的第二个流程epoll_ctl,在epoll_apply_one_change函数中,就已经设置了需要监听事件的类型和对epoll的操作类型,如果想使用ET模式,那么一开始设置event的监听类型时就应该加上EV_ET,如EV_READ|EV_ET(边沿触发读事件),这样最终epoll_ctl调用时事件的监听类型中才会有EPOLLET,默认情况下是没有设置EV_ET的,即水平触发的。
对于epoll_ctl的操作类型,libevent的策略为:如果在添加新的监听事件之前fd中没有设置读写事件的监听,那么新的监听行为就认为是“添加一个监听事件”;如果在添加新的监听事件之前fd中已经设置过读写事件的监听,那么新的监听行为就认为是“修改一个监听事件”。
确定好了需要监听的事件类型和epoll_ctl的操作类型,就可以调用epoll_ctl向内核监听事件集合中添加事件了,如下所示:
static int
epoll_apply_one_change(struct event_base *base,
struct epollop *epollop,
const struct event_change *ch)
{
struct epoll_event epev;
int op, events = 0;
if (1) {
......
memset(&epev, 0, sizeof(epev));
epev.data.fd = ch->fd; //监听的fd
epev.events = events; //epoll监听fd的类型
if (epoll_ctl(epollop->epfd, op, ch->fd, &epev) == -1) {//调用epoll_ctl函数
if (op == EPOLL_CTL_MOD && errno == ENOENT) {
//如果试图对一个未添加的fd进行修改操作,那么就默认进行添加操作,
if (epoll_ctl(epollop->epfd, EPOLL_CTL_ADD, ch->fd, &epev) == -1) {
event_warn("Epoll MOD(%d) on %d retried as ADD; that failed too",
(int)epev.events, ch->fd);
return -1;
} else {
......
}
} else if (op == EPOLL_CTL_ADD && errno == EEXIST) {
//如果试图对一个已经添加的fd再执行添加操作,那么就默认对其进行修改
if (epoll_ctl(epollop->epfd, EPOLL_CTL_MOD, ch->fd, &epev) == -1) {
event_warn("Epoll ADD(%d) on %d retried as MOD; that failed too",
(int)epev.events, ch->fd);
return -1;
} else {
......
}
} else if (op == EPOLL_CTL_DEL &&
(errno == ENOENT || errno == EBADF ||
errno == EPERM)) {//如果试图对一个无效的或未添加的或不支持epoll的fd进行删除操作
event_debug(("Epoll DEL(%d) on fd %d gave %s: DEL was unnecessary.",
(int)epev.events,
ch->fd,
strerror(errno)));
} else {
event_warn("Epoll %s(%d) on fd %d failed. Old events were %d; read change was %d (%s); write change was %d (%s)",
epoll_op_to_string(op),
(int)epev.events,
ch->fd,
ch->old_events,
ch->read_change,
change_to_string(ch->read_change),
ch->write_change,
change_to_string(ch->write_change));
return -1;
}
} else {
......
}
}
return 0;
}
很显然,在调用epoll_ctl后,如果出错了之后,还会对错误原因进行相应的处理:
如果是尝试对一个未添加的fd进行监听事件修改操作导致的出错,那么就会重新以“添加”操作类型来调用epoll_ctl函数;
如果是尝试对一个已添加的fd进行监听事件添加操作导致的出错,那么就会重新以“修改”操作类型来调用epoll_ctl函数;
其他出错原因则直接报错。
在调用event_add函数进行事件添加时,event_add会将event通过evmap_io_add或evmap_signal_add添加到event_io_map或event_signal_map中,而在evmap_io_add或evmap_signal_add函数中则会调用选用的后端的add函数,如果选用的后端是epoll模型,那么就会调用epoll_nochangelist_add函数,如下所示:
int
evmap_io_add(struct event_base *base, evutil_socket_t fd, struct event *ev)
{
......
if (res) {//如果有读或写事件,就将event添加到base中
void *extra = ((char*)ctx) + sizeof(struct evmap_io);
if (evsel->add(base, ev->ev_fd,
old, (ev->ev_events & EV_ET) | res, extra) == -1) //调用对应后端的add函数
return (-1);
retval = 1;
}
......
}
int
evmap_signal_add(struct event_base *base, int sig, struct event *ev)
{
......
if (TAILQ_EMPTY(&ctx->events)) {
if (evsel->add(base, ev->ev_fd, 0, EV_SIGNAL, NULL)
== -1)
return (-1);
}
......
}
在epoll中,如果要删除一个fd,还是需要通过epoll_ctl函数,只不过操作类型需要设置为EPOLL_CTL_DEL。因此epoll_nochangelist_del最终也是需要调用epoll_ctl函数,其定义如下:
static int
epoll_nochangelist_del(struct event_base *base, evutil_socket_t fd,
short old, short events, void *p)
{
struct event_change ch;
ch.fd = fd;
ch.old_events = old;
ch.read_change = ch.write_change = 0;
if (events & EV_WRITE)//如果需要删除的事件为写事件,那么就表明在写端需要执行删除操作
ch.write_change = EV_CHANGE_DEL;
if (events & EV_READ)//如果需要删除的事件为读事件,那么就表明在读端需要执行删除操作
ch.read_change = EV_CHANGE_DEL;
return epoll_apply_one_change(base, base->evbase, &ch);
}
可见epoll_nochangelist_del函数与epoll_nochangelist_add函数是极其类似的,最后都调用了epoll_apply_one_change函数,在该函数中与之相关的代码如下所示:
static int
epoll_apply_one_change(struct event_base *base,
struct epollop *epollop,
const struct event_change *ch)
{
struct epoll_event epev;
int op, events = 0;
if (1) {
......
} else if ((ch->read_change & EV_CHANGE_DEL) ||
(ch->write_change & EV_CHANGE_DEL)) {//如果需要在读端或者写端进行删除操作
/* If we're deleting anything, we'll want to do a MOD
* or a DEL. */
op = EPOLL_CTL_DEL; //设置epoll_ctl的操作类型为删除
if (ch->read_change & EV_CHANGE_DEL) { //如果读端需要进行删除操作
if (ch->write_change & EV_CHANGE_DEL) { //如果读写端都需要进行删除操作
events = EPOLLIN|EPOLLOUT; //当op为EPOLL_CTL_DEL时,是直接删除fd,事件类型是没有影响的
} else if (ch->old_events & EV_WRITE) { //读端需要进行删除操作,但是fd对应的事件中有写事件,那么epoll_ctl依然需要对该fd进行写端的监听
events = EPOLLOUT;
op = EPOLL_CTL_MOD; // 这里就相当于把对fd的监听类型修改为EPOLLOUT(取消对EPOLLIN的监听)
} else { //如果写端不进行删除并且fd对应的事件中没有写事件,那么就直接删除fd
events = EPOLLIN;
}
} else if (ch->write_change & EV_CHANGE_DEL) { //如果读端不需要进行删除操作,但是写端需要进行删除操作
if (ch->old_events & EV_READ) { //如果fd对应的所有事件中有读事件,那么就通过epoll_ctl只监听fd的读端
events = EPOLLIN;
op = EPOLL_CTL_MOD;
} else { //如果fd对应的所有事件中没有读事件,就直接删除fd
events = EPOLLOUT;
}
}
}
......
}
这里也是需要定义一个events变量用来描述epoll需要监听的事件类型,以及一个op变量用来描述epoll_ctl的操作类型。需要注意的是,这里封装的epoll_nochangelist_del和epoll_ctl在删除情况下的操作是不同的,当操作类型是EPOLL_CTL_DEL时,epoll_ctl是会直接将epoll中监听的文件描述符fd删除掉,而这里的epoll_nochangelist_del是可以删掉文件描述符fd的读事件监听或写事件监听,而不是只能直接删除文件描述符。
对于epoll监听的事件类型,libevent的策略为:如果是当前事件指定读写两端都需要进行删除操作的话,相当于彻底删除epoll中的fd,此时事件类型的设置是没有作用的;如果是当前事件指定读端需要进行删除操作,而写端不需要进行删除操作,并且fd之前已经设置过写事件监听,那么就相当于重新修改fd的监听事件为写事件监听,因此监听事件类型改为EPOLLOUT,并且操作类型应当为“修改监听事件类型”;如果当前事件指定读端需要进行删除操作,而写端不需要进行删除操作,并且fd之前也没有设置过写事件监听,那么也就相当于直接从epoll中删除fd了。同理,如果是当前事件指定写端需要删除,而读端不需要删除,如果fd之前设置过读事件监听,那么就相当于重新修改fd的监听事件为读事件监听,因此监听事件类型改为EPOLLIN,并且操作类型为“修改监听事件类型”,其他情况则是直接删除fd即可。
然后就是调用epoll_ctl函数进行删除操作,与epoll_nochangelist_add是相同的,这里就不多说了。
与epoll_nochangelist_add类似,在调用event_del函数进行事件删除时,event_del会将event通过evmap_io_del或evmap_signal_del从event_io_map或event_signal_map中删除,而在evmap_io_del或evmap_signal_del函数中则会调用选用的后端的add函数,如果选用的后端是epoll模型,那么就会调用epoll_nochangelist_del函数,如下所示:
int
evmap_io_del(struct event_base *base, evutil_socket_t fd, struct event *ev)
{
......
if (res) {
void *extra = ((char*)ctx) + sizeof(struct evmap_io);
if (evsel->del(base, ev->ev_fd, old, res, extra) == -1) //调用后端的del函数
return (-1);
retval = 1;
}
.......
}
int
evmap_signal_del(struct event_base *base, int sig, struct event *ev)
{
......
if (TAILQ_FIRST(&ctx->events) == TAILQ_LAST(&ctx->events, event_list)) {
if (evsel->del(base, ev->ev_fd, 0, EV_SIGNAL, NULL) == -1)
return (-1);
}
......
}
前面epoll_init调用了epoll_create来创建了epoll,通过epoll_nochangelist_add和epoll_nochangelist_del来对监听事件进行添加和删除,那么剩下的,自然就是调用epoll_wait函数从内核从得到就绪的epoll_event数组了。epoll_dispatch中就实现这一功能,该函数定义如下:
#define MAX_EPOLL_TIMEOUT_MSEC (35*60*1000)
static int
epoll_dispatch(struct event_base *base, struct timeval *tv)
{
struct epollop *epollop = base->evbase;
struct epoll_event *events = epollop->events;
int i, res;
long timeout = -1;
if (tv != NULL) {
timeout = evutil_tv_to_msec(tv); //将设置的tv转换为毫秒形式
if (timeout < 0 || timeout > MAX_EPOLL_TIMEOUT_MSEC) { //在linux内核中epoll最多支持超时2147482毫秒(稍大于35分钟),这里直接设置为35分钟
/* Linux kernels can wait forever if the timeout is
* too big; see comment on MAX_EPOLL_TIMEOUT_MSEC. */
timeout = MAX_EPOLL_TIMEOUT_MSEC;
}
}
epoll_apply_changes(base);
event_changelist_remove_all(&base->changelist, base);
EVBASE_RELEASE_LOCK(base, th_base_lock);
res = epoll_wait(epollop->epfd, events, epollop->nevents, timeout); //epoll开始监听,激活事件结果保存在events中,最多接受nevents个激活事件
EVBASE_ACQUIRE_LOCK(base, th_base_lock);
if (res == -1) {
if (errno != EINTR) {
event_warn("epoll_wait");
return (-1);
}
return (0);
}
event_debug(("%s: epoll_wait reports %d", __func__, res));
EVUTIL_ASSERT(res <= epollop->nevents);
for (i = 0; i < res; i++) { //遍历触发的事件
int what = events[i].events; //激活事件的激活类型
short ev = 0;
if (what & (EPOLLHUP|EPOLLERR)) { //如果对端关闭或者发生错误
ev = EV_READ | EV_WRITE;
} else {
if (what & EPOLLIN) //如果是EPOLLIN类型,说明有数据可读
ev |= EV_READ;
if (what & EPOLLOUT) //如果是EPOLLOUT类型,说明有数据可写
ev |= EV_WRITE;
}
if (!ev)
continue;
evmap_io_active(base, events[i].data.fd, ev | EV_ET);//将所有发生了读/写事件的event插入到激活队列中
}
//如果epoll_wait返回的激活事件数量达到了设置的nevents,并且还没有达到设置的事件上限
//说明此时真正激活的事件数量可能超过nevents,那么就重新设置epollop中的nevents和events
if (res == epollop->nevents && epollop->nevents < MAX_NEVENT) {
/* We used all of the event space this time. We should
be ready for more events next time. */
int new_nevents = epollop->nevents * 2;
struct epoll_event *new_events;
new_events = mm_realloc(epollop->events,
new_nevents * sizeof(struct epoll_event));
if (new_events) {
epollop->events = new_events;
epollop->nevents = new_nevents;
}
}
return (0);
}
epoll_despatch函数需要传入一个超时结构体,这个结构体相当于epoll_wait中的第二个参数,用来指定epoll_wait阻塞多久。由于内核所支持epoll_wait最大阻塞时长为35分钟左右,因此需要先将传入的超时结构体参数进行判断来保证其不大于最大阻塞时长。然后就是调用epoll_wait在阻塞时长之后将就绪链表中的事件从内核态拷贝到用户态的epoll_event数组中,接着遍历epoll_event数组,将就绪的事件全部添加到event_base的激活队列中,等待统一处理激活队列时处理相应事件。
这里调用了evmap_io_active函数目的是将激活的事件添加到激活队列中,这里需要注意的是,evmap_io_active的第三个参数描述的是事件的激活类型(即事件是由读事件激活、写事件激活还是其他的),根据前面可以知道,这里的变量ev实际上就已经描述了事件的激活类型,那么为什么还要把ev|EV_ET作为事件的激活类型呢?
这是因为ev只能描述发生在这个fd上的事件类型,但是实际上可以有多个event对应于同一个fd,也就是说一个ev可能相应有多个event需要被激活,而这些event有些可能设置了EV_ET有些则可能没有,而最终在判断fd下所有event是否真的被激活的时候(evmap_io_active最后会调用event_active_nolock函数),是将这些event的监听事件类型和发生的事件类型进行“与”操作的,相同的才能作为这个event的激活类型,但是dispatch返回的激活类型是不可能包括EPOLLET的,如果不为ev或上一个EV_ET的话,那么那些设置了边沿触发的event最终的激活类型就不会记录EV_ET。event的激活类型,最终会作为参数传递给event的回调函数。
此外还需注意的一点是,epoll_wait函数的第三个参数nevents限制了获取的激活事件的个数,激活的事件都保存在epollop的events数组中,如果真实激活事件的个数达到了nevents,那么说明events数组不够大,可能有一些事件激活了但是没有接收到events数组中,因此还应当对该数组进行扩充。
epoll_despatch函数的调用实际上是在事件主循环中执行的,在事件主循环中,对于非超时的事件,就会调用event_base绑定的后端的dispatch方法来获取那些激活的非超时事件,如果绑定的后端刚好是epoll,那么就会调用这里的epoll_dispatch函数,如下所示:
int
event_base_loop(struct event_base *base, int flags)
{
......
res = evsel->dispatch(base, tv_p);
......
}
epoll的销毁实际上就是删除epollop结构体并且关闭epoll的文件描述符即可,如下所示:
static void
epoll_dealloc(struct event_base *base)
{
struct epollop *epollop = base->evbase;
evsig_dealloc(base);
if (epollop->events)
mm_free(epollop->events);
if (epollop->epfd >= 0)
close(epollop->epfd);
memset(epollop, 0, sizeof(struct epollop));
mm_free(epollop);
}
如果选用的后端为epoll,那么epoll会在event_base创建时同时被创建,因此如果要销毁一个epoll,那么自然就是在销毁event_base的时候进行销毁了,如下所示:
void
event_base_free(struct event_base *base)
{
......
if (base->evsel != NULL && base->evsel->dealloc != NULL)
base->evsel->dealloc(base);
......
}