五、事件处理框架
libevent的事件处理框架是一个反应堆模型,而反应堆模型的核心就是IO复用。拿epoll来说,反应堆模型有两个核心数据结构,一个是epoll维护的内核事件表,一个是保存激活事件的事件队列。当然,值的注意的是,如果是单线程或者单进程,反应堆模型一定是IO复用+非阻塞IO,否则无法保证及时响应。下面,将分析事件注册和事件删除的具体细节。
1.event_base
Reactor模型中的Reactor组件在libevent中的对应结构就是event_base。
struct event_base {
const struct eventop *evsel; /*evsel和evbase就类比与类和静态函数之间的关系,evsel指在这里使用的复用模型*/
void *evbase;
/*这个event_base所关注的事件数目*/
int event_count; /* counts number of total events */
/*这个event_base中激活事件的数目*/
int event_count_active; /* counts number of active events */
/*判断循环退出的条件*/
int event_gotterm; /* Set to terminate loop */
int event_break; /* Set to terminate loop immediately */
/* active event management */
/*只存激活事件的链表,执行存放不同优先级事件的链,所以使用二级指针,激活事件的链表*/
struct event_list **activequeues;
/*事件链表的数目,数据是根据优先级来决定的*/
int nactivequeues;
/* signal handling info */
/*处理信号事件的单独结构体*/
struct evsignal_info sig;
/*存放所有事件的链表*/
struct event_list eventqueue;
struct timeval event_tv;
/*管理和超时事件相关的时间小顶堆*/
struct min_heap timeheap;
/*存放时间的时间缓冲*/
struct timeval tv_cache;
};
event_base在libevent是一个核心数据结构,其对应一个反应堆模型实例。如果把event_base看成一个类的话,
const struct eventop *evsel;
中的回调函数就是这个类中的方法。
struct eventop {
const char *name;//io复用的名字
void *(*init)(struct event_base *);//初始化
int (*add)(void *, struct event *);//注册事件
int (*del)(void *, struct event *);//删除事件
int (*dispatch)(struct event_base *, void *, struct timeval *);//事件分发
void (*dealloc)(struct event_base *, void *);//销毁资源
/* set if we need to reinitialize the event base */
int need_reinit;
};
在 libevent 中, 每种 I/O demultiplex 机制的实现都必须提供这五个函数接口,
来完成自身的初始化、销毁释放;对事件的注册、注销和分发。
2.初始化event_base需要做个事情
首先为 event_base 实例申请空间,然后初始化 timer mini-heap,初始化系统io,初始化事件链表,检测系统的时间设置
libevent中的五个基本接口函数
void *(*init)(struct event_base *);//初始化
int (*add)(void *, struct event *);//注册事件
int (*del)(void *, struct event *);//删除事件
int (*dispatch)(struct event_base *, void *, struct timeval *);//事件分发
void (*dealloc)(struct event_base *, void *);//销毁资源
1)注册事件event_add
/*这个函数也是库中对外接口的很重要的一个,将event添加到监听队列,(这里的监听是监听
这个套接字上的事件)*/
int event_add(struct event *ev, const struct timeval *tv)
{
struct event_base *base = ev->ev_base;//要注册到的event_base
const struct eventop *evsel = base->evsel;
void *evbase = base->evbase;
int res = 0;
event_debug((
"event_add: event: %p, %s%s%scall %p",
ev,
ev->ev_events & EV_READ ? "EV_READ " : " ",
ev->ev_events & EV_WRITE ? "EV_WRITE " : " ",
tv ? "EV_TIMEOUT " : " ",
ev->ev_callback));
assert(!(ev->ev_flags & ~EVLIST_ALL));
/*发现这个event是定时事件,那么就将此事件添加到小顶堆中,这里并没有直接添加,而是
为添加做准备,在小顶堆上申请一个空间*/
if (tv != NULL && !(ev->ev_flags & EVLIST_TIMEOUT)) {//EVLIST_TIMEOUT表示ev已经在定时器事件堆中
if (min_heap_reserve(&base->timeheap,
1 + min_heap_size(&base->timeheap)) == -1)
return (-1); /* ENOMEM == errno */
}
/*如果这个事件还没有被添加到事件队列中,并且这个事件是I/O或者信号事件,都
会在这里被添加到*/
if ((ev->ev_events & (EV_READ|EV_WRITE|EV_SIGNAL)) &&
!(ev->ev_flags & (EVLIST_INSERTED|EVLIST_ACTIVE))) {
/*这个函数是I/O复用机制中的回调函数,回调函数中的内容就是将event添加到event_base
中.........*/
/*这个函数就是将event和epoll_event集合中的某个联系起来*/
res = evsel->add(evbase, ev);
/*同时将这个时间添加到event_base中的eventqueues队列中,这个队列存放着和这个
event_base相关的所有事件*/
if (res != -1)
event_queue_insert(base, ev, EVLIST_INSERTED);
}
/*
* we should change the timout state only if the previous event
* addition succeeded.
*/
if (res != -1 && tv != NULL) {
struct timeval now;
/*
* we already reserved memory above for the case where we
* are not replacing an exisiting timeout.
*/
/*检查这个事件是否存在在队列中,如果存在,那么就删除这个时间*/
if (ev->ev_flags & EVLIST_TIMEOUT)
event_queue_remove(base, ev, EVLIST_TIMEOUT);
/* Check if it is active due to a timeout. Rescheduling
* this timeout before the callback can be executed
* removes it from the active list. */
if ((ev->ev_flags & EVLIST_ACTIVE) &&
(ev->ev_res & EV_TIMEOUT)) {
/* See if we are just active executing this
* event in a loop
*/
if (ev->ev_ncalls && ev->ev_pncalls) {
/* Abort loop */
*ev->ev_pncalls = 0;
}
event_queue_remove(base, ev, EVLIST_ACTIVE);
}
/*通过上面一些列的检测,在这里可以将这个超时事件添加到已经申请好的空间中的位置*/
gettime(base, &now);
evutil_timeradd(&now, tv, &ev->ev_timeout);
event_debug((
"event_add: timeout in %ld seconds, call %p",
tv->tv_sec, ev->ev_callback));
event_queue_insert(base, ev, EVLIST_TIMEOUT);
}
return (res);
}
注:在注册事件时,主要做两件事:
(1)将事件注册到内核事件注册表中
(2)将事件添加到对应的队列或者堆中
需要注意的是,I/O事件和信号事件会被添加到注册事件链表中,如果它们是就绪事件,则直接添加到已注册事件链表中;定时事件则被添加到管理定时事件的小顶堆中
六、统一事件源和事件循环
我们知道libevent中有三种事件:I/O事件、信号事件和定时事件。我们接下来会看看怎样把这三种事件统一起来处理,即libevent的事件主循环是如何把这三种事件集成进来的。在这之前,我们必须清楚事件主循环是如何根据系统提供的事件多路分发机制执行事件循环的。
1.libevent的事件循环
libevent的事件主循环函数是event_base_loop。该函数首先调用I/O事件多路分发器的事件监听函数,以等待事件;当有事件发生时,就依次处理。
/*event_dispatch调用了这个函数*/
int
event_base_loop(struct event_base *base, int flags)
{
const struct eventop *evsel = base->evsel;
void *evbase = base->evbase;
struct timeval tv;
struct timeval *tv_p;
int res, done;
/* clear time cache */
/*清空时间缓存*/
base->tv_cache.tv_sec = 0;
/*如果有信号事件被注册,将信号事件的event_base也指向同一个base(evsignal_base是全局变量,在处理signal时,用于指明signal所属的event_base实例)*/
if (base->sig.ev_signal_added)
evsignal_base = base;
done = 0;
while (!done) {
/* Terminate the loop if we have been asked to */
/*如果想让这个循环退出,可以调用函数改变循环是否进行的条件*/
if (base->event_gotterm) {
base->event_gotterm = 0;
break;
}
if (base->event_break) {
base->event_break = 0;
break;
}
/*校正时间,更新event_tv到tv_cache指示的时间或者当前时间(第一次)
看完下面的解释 跟踪到函数里面看这个函数的作用*/
timeout_correct(base, &tv);
tv_p = &tv;
/*flags目前没有什么作用,传递的都是0,如果当前没有被激活的事件,从小顶堆中
取出时间,作为回调epoll_wait第三个参数*/
if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) {
timeout_next(base, &tv_p);
} else {
/*
* if we have active events, we just poll new events
* without waiting.
*/
/*如果仍然有激活的事件,并不是马上处理,而是将时间便为0,让epoll_wait立刻
返回,按照以前的流程继续走*/
evutil_timerclear(&tv);
}
/* If we have no events, we just exit */
if (!event_haveevents(base)) {
event_debug(("%s: no events registered.", __func__));
return (1);
}
/*记录了两个时间,在循环前和循环后分别初始化了event_base内部时间
event_tv <---- tv_cache*/
/* update last old time */
gettime(base, &base->event_tv);
/* clear time cache */
/*清空时间缓存 ----- 时间1*/
base->tv_cache.tv_sec = 0;
/*这里调用了I/O复用的驱动器,在epoll中相当与是epoll_wait,如果这个函数返回,说明
存在事件需要处理 等待这I/O就绪*/
res = evsel->dispatch(base, evbase, tv_p);
if (res == -1)
return (-1);
/*缓存tv_cache存储了当前时间的值----时间2
tv_cache <--- now*/
gettime(base, &base->tv_cache);
/*使用小顶堆的堆顶作为循环的时间是将定时事件融合到I/O机制中的关键,
在这个函数中将合适的超时事件添加到激活队列中*/
timeout_process(base);
/*根据活跃事件的个数(event_count_active)来进行处理*/
if (base->event_count_active) {
/*处理事件的函数*/
event_process_active(base);
if (!base->event_count_active && (flags & EVLOOP_ONCE))
done = 1;
} else if (flags & EVLOOP_NONBLOCK)
done = 1;
}
/*时间event_tv只是了dispatch()上次返回,也就是I/O事件就绪时间的时间,第一次进入循环时,
由于tv_cache被清空,因此gettime执行系统调用获取当前系统时间,而后将会更新为tv_cache
指示的是
时间tv_cache在dispatch()返回后被设置为当前系统时间,因此它缓存了本次I/O事件就绪时的
时间
从代码逻辑上,event_tv取得的是tv_cache上一次的值,因此event_tv应该小于tv_cache的值
设置时间缓存的优点是每次获取时间都执行系统调用,这样在上面标注的时间2到时间点1的
这段时间,调用gettime()取得的都是tv_cache缓存的时间*/
/* clear time cache */
/*退出时也要将时间缓存清空*/
base->tv_cache.tv_sec = 0;
event_debug(("%s: asked to terminate loop.", __func__));
return (0);
}
2.如果还没有就绪事件发生,则会将超时事件小顶堆中的堆顶的事件的时间作为epoll_wait的超时参数
3.每次有超时事件发生时,会先将超时事件从小顶堆中取出放入对应的激活事件队列中(因为base中有多个不同优先级的激活事件队列)
4.事件循环每循环一次都会更新时间缓存(base中的tv_cache)
5.事件循环每循环一次都只处理优先级最高的激活事件队列中的所有激活事件,这样当不断有高优先级的事件发生时,底优先级的事件就得不到处理,这样就出现了饥饿现象。
2.统一事件源
1)统一I/O和信号事件
为什么要统一I/O和信号事件:
1.信号是一种异步事件(即信号的处理函数和程序的主循环是两条不同的执行线路)。很显然,信号处理函数需要尽可能狂的执行完毕,以确保该信号不被长时间屏蔽。2.信号事件的处理在有I/O复用机制的框架里如果特殊处理势必会影响普通事件的处理
解决问题的思路:
把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它只是简单地通知主循环程序接受到信号,并将信号值传递给主循环,主循环再根据接收到的信号值执行对应的逻辑代码。而对于libevent这样有I/O复用机制的系统,只需要在信号发生时,通知系统的I/O复用机制,然后信号处理函数返回,这样信号事件就能统一和I/O事件以及Timer一起处理了。
实现的方法:
当信号发生时,信号处理函数将已经注册的信号从信号事件队列中取出放到对应的激活事件队列中,并且通过管道来通知I/O多路分发器有信号事件发生,然后信号处理函数然返回,最后激活的信号事件等待被处理。
2)统一I/O和定时事件
Libevent 将 Timer 和 Signal 事件都统一到了系统的 I/O 的 demultiplex 机制中了。
首先将 Timer 事件融合到系统 I/O 多路复用机制中,还是相当清晰的,因为系统的 I/O机制像 select()和 epoll_wait()都允许程序制定一个最大等待时间(也称为最大超时时间)timeout,即使没有 I/O 事件发生,它们也保证能在 timeout 时间内返回。
那么根据所有 Timer 事件的最小超时时间来设置系统 I/O 的 timeout 时间;当系统 I/O返回时,再激活所有就绪的 Timer 事件就可以了,这样就能将 Timer 事件完美的融合到系统
的 I/O 机制中了。
这是在 Reactor 和 Proactor 模式(主动器模式,比如 Windows 上的 IOCP)中处理 Timer事件的经典方法了, ACE,nginx ,redis(当然redis并没有采用小顶堆来存放超时事件,而是用的无序链表,简单但效率低)采用的也是这种方法,
堆是一种经典的数据结构,向堆中插入、删除元素时间复杂度都是 O(lgN), N 为堆中元素的个数,而获取最小 key 值(小根堆)的复杂度为 O(1);因此变成了管理 Timer 事件的绝佳人选(当然是非唯一的), libevent 就是采用的堆结构。
七、统一I/O和信号事件详解
libevent中,信号处理函数是通过域套接字来通知I/O复用机制有信号事件发生的。
1.域套接字socket pair
域套接字就是供本地进程间通信的套接字,nginx中的主进程和工作进程,工作进程和工作进程间都是通过域套接字进行通信的。
创建一个 socket pair 并不是复杂的操作,可以参见下面的流程图,清晰起见,其中忽略了一些错误处理和检查。
Libevent 提供了辅助函数 evutil_socketpair()来创建一个 socket pair,可以结合上面的创建流程来分析该函数(实际上就是建立tcp连接的过程)。
2.如何通过域套接字来通知信号事件的发生
Socket pair 创建好了,可是 libevent 的事件主循环还是不知道 Signal 是否发生了啊,看来我们还差了最后一步,那就是:为 socket pair 的读 socket 在 libevent 的 event_base 实例上注册一个 persist 的读事件。
这样当向写 socket 写入数据时,读 socket 就会得到通知,触发读事件,从而 event_base就能相应的得到通知了。
前面提到过,Libevent 会在事件主循环中检查标记,来确定是否有触发的 signal,如果标记被设置就处理这些 signal,这段代码在各个具体的 I/O 机制中,以 Epoll 为例,在epoll_dispatch()函数中,代码片段如下:
/*在函数event_dispatch函数中使用的回调*/
static int
epoll_dispatch(struct event_base *base, void *arg, struct timeval *tv)
{
struct epollop *epollop = arg;
struct epoll_event *events = epollop->events;
struct evepoll *evep;
int i, res, timeout = -1;
/*tv就是传递的参数,这个值为管理时间的最小堆中堆顶元素*/
if (tv != NULL)
timeout = tv->tv_sec * 1000 + (tv->tv_usec + 999) / 1000;
if (timeout > MAX_EPOLL_TIMEOUT_MSEC) {
/* Linux kernels can wait forever if the timeout is too big;
* see comment on MAX_EPOLL_TIMEOUT_MSEC. */
timeout = MAX_EPOLL_TIMEOUT_MSEC;
}
/*传递的超时时间是从小顶堆中取出的堆顶值
@epollop->epfd 是在epoll_init函数总epoll_create函数返回的值
@events 是在epoll_init函数中动态生成的struct epoll_event描述符集合
@epollop->nevents 是所监听的描述符个数
@timeout 是每个循环的时间
函数一直阻塞,知道有事件被激活*/
res = epoll_wait(epollop->epfd, events, epollop->nevents, timeout);
/*函数返回,说明在监听的描述符集合中活跃描述副个数*/
if (res == -1) {
if (errno != EINTR) {
event_warn("epoll_wait");
return (-1);
}
/*这个处理信号时间的特殊函数,前面在event_init中提到,初始化了一对和信号相关的
套接字,其中一个套接字在事件队列中
在这个地方返回,说明这个信号事件并非用户空间已经定义的信号事件,很可能是突发事件*/
evsignal_process(base);
return (0);
} else if (base->sig.evsignal_caught) {
/*如果事件触发条件被激活,仍然需要处理信号事件,这个函数是将信号事件添加到激活队列
也就是在这里处理了信号事件,将信号时间融合到I/O事件中,因为只要将事件放到激活队列
中,在回调的dispatch函数结束后便有去执行与这个event相关的回调*/
evsignal_process(base);
}
event_debug(("%s: epoll_wait reports %d", __func__, res));
/*根据返回的值,将相应的I/O事件添加到相应的触发条件上*/
for (i = 0; i < res; i++) {
/*获取这个I/O事件的事件类型*/
int what = events[i].events;
struct event *evread = NULL, *evwrite = NULL;
int fd = events[i].data.fd;
if (fd < 0 || fd >= epollop->nfds)
continue;
/*获取触发I/O的描述符*/
evep = &epollop->fds[fd];
if (what & (EPOLLHUP|EPOLLERR)) {
evread = evep->evread;
evwrite = evep->evwrite;
} else {
if (what & EPOLLIN) {
evread = evep->evread;
}
if (what & EPOLLOUT) {
evwrite = evep->evwrite;
}
}
/*如果这个I/O事件的事件类型不是 EPOLLHUP/EPOLLERR/EPOLLIN/EPOLLOUT中的任何
一种,那么处理下一个被触发的I/O事件*/
if (!(evread||evwrite))
continue;
/*如果这个事件是可读事件,添加到激活队列中*/
if (evread != NULL)
event_active(evread, EV_READ, 1);
if (evwrite != NULL)
event_active(evwrite, EV_WRITE, 1);
}
return (0);
}
完整的处理框架如下所示:
3.evsignal_info 结构体
/*这个是为了处理信号事件单独封装的结构体*/
struct evsignal_info {
/*为socket pair 的读socket向event_base注册读事件时使用的event结构体*/
struct event ev_signal;
int ev_signal_pair[2];/*在event_init函数中介绍了这对套接字存在的价值*/
int ev_signal_added; /*记录ev_signal事件是已经注册了*/
volatile sig_atomic_t evsignal_caught; /*是否有信号发生的标记*/
struct event_list evsigevents[NSIG];/*evsigevents[i]表示注册到信号i的事件链表*/
sig_atomic_t evsigcaught[NSIG];/*evsigcaught[i]具体记录信号i触发的次数,*/
#ifdef HAVE_SIGACTION
struct sigaction **sh_old;/*记录了原来的signal处理函数指针,当信号sigo注册的event被清空
后,需要重新设置其处理函数*/
#else
ev_sighandler_t **sh_old;
#endif
int sh_old_max;
};
下面详细介绍一下个字段的含义和作用:
1)ev_signal, 为 socket pair 的读 socket 向 event_base 注册读事件时使用的 event 结构体;
2)ev_signal_pair,socket pair 对,作用见第一节的介绍;
3)ev_signal_added,记录 ev_signal 事件是否已经注册了;
4)evsignal_caught,是否有信号发生的标记;是 volatile 类型,因为它会在另外的线程中被
修改;
5)evsigvents[NSIG],数组,evsigevents[signo]表示注册到信号 signo 的事件链表;
6)evsigcaught[NSIG],具体记录每个信号触发的次数,evsigcaught[signo]是记录信号 signo
被触发的次数;
7)sh_old 记录了原来的 signal 处理函数指针,当信号 signo 注册的 event 被清空时,需要重
新设置其处理函数;
evsignal_info 的初始化包括,创建 socket pair,设置 ev_signal 事件(但并没有注册,而
是等到有信号注册时才检查并注册),并将所有标记置零,初始化信号的注册事件链表指针
等。
4. 注册、注销 signal 事件
注册 signal 事件是通过 evsignal_add(struct event *ev)函数完成的, libevent 对所有的信号注册同一个处理函数 evsignal_handler(),该函数将在下一段介绍,注册过程如下:
1 取得 ev 要注册到的信号 signo;
2 如果信号 signo 未被注册,那么就为 signo 注册信号处理函数 evsignal_handler();
3 如果事件 ev_signal 还没有注册,就注册 ev_signal 事件;
4 将事件 ev 添加到 signo 的 event 链表中;
从 signo 上注销一个已注册的 signal 事件就更简单了,直接从其已注册事件的链表中移除即可。如果事件链表已空,那么就恢复旧的处理函数;
下面的讲解都以 signal()函数为例,sigaction()函数的处理和 signal()相似。
处理函数evsignal_handler()函数做的事情很简单,就是记录信号的发生次数,并通知event_base有信号触发,需要处理:
/*这个是信号处理函数,当信号到达是,回调执行的就是这个函数,对于所有的信号都是执行这个函数
函数中最重要的就是send一个字节给evsigna_info结构体中的内置套接字,这样就可以是的epoll_wait
返回,返回以后再判断所监听的I/O事件被激活是属于哪类事件,如果是信号事件,再回调用户自定义
的回调函数,这样就可以将信号事件转化为I/O事件了*/
static void
evsignal_handler(int sig)
{
int save_errno = errno;
if (evsignal_base == NULL) {
event_warn(
"%s: received signal %d, but have no base configured",
__func__, sig);
return;
}
/*信号sig内激活的次说增加1
修改信号被捕获的标志*/
evsignal_base->sig.evsigcaught[sig]++;
evsignal_base->sig.evsignal_caught = 1;
#ifndef HAVE_SIGACTION
signal(sig, evsignal_handler);
#endif
/* Wake up our notification mechanism */
/*唤醒epoll_wait,使其返回*/
send(evsignal_base->sig.ev_signal_pair[0], "a", 1, 0);
errno = save_errno;
}
5 信号注册函数
/*添加信号的函数*/
int
evsignal_add(struct event *ev)
{
int evsignal;
struct event_base *base = ev->ev_base;
struct evsignal_info *sig = &ev->ev_base->sig;
/*如果这个时间发现是可读/可写,说明不是信号事件*/
if (ev->ev_events & (EV_READ|EV_WRITE))
event_errx(1, "%s: EV_SIGNAL incompatible use", __func__);
/*获取信号值*/
evsignal = EVENT_SIGNAL(ev);
assert(evsignal >= 0 && evsignal < NSIG);
/*如果这个信号所在的链表为空,说明此信号第一次被注册*/
if (TAILQ_EMPTY(&sig->evsigevents[evsignal])) {
event_debug(("%s: %p: changing signal handler", __func__, ev));
/*这个函数就是使用了处理信号处理函数的部分,但是信号处理函数的回调只
有一个evsigna_handler*/
if (_evsignal_set_handler(
base, evsignal, evsignal_handler) == -1)
return (-1);
/* catch signals if they happen quickly */
/*evsignal_base是全局变量*/
evsignal_base = base;
/*这里调用了event_add,但是这个event_add关注的是信号内部的socket,在这个
socket注册的回调函数是读取套接字上的一个字节,为了激活信号的
内部I/O事件*/
if (!sig->ev_signal_added) {
if (event_add(&sig->ev_signal, NULL))
return (-1);
/*ev_singal_added的值只在这里被修改,如果第一次地一个信号别添加,才会执行这里
表示判断信号存在的内部I/O被添加到反应堆中*/
sig->ev_signal_added = 1;
}
}
/* multiple events may listen to the same signal */
/*在信号链表中将这个信号event添加进去*/
TAILQ_INSERT_TAIL(&sig->evsigevents[evsignal], ev, ev_signal_next);
return (0);
}
注:对于没有就绪的事件有三种存储形式
1.I/O注册事件队列;2.信号事件队列(每个信号都有一个信号事件队列);3.存放定时事件的小顶堆
对于就绪事件,有多个优先级不同的激活事件队列,每次事件循环都只会处理优先级最高的激活事件队列中的所有事件
6 小节
本节介绍了 libevent 对 signal 事件的具体处理框架,包括事件注册、删除和 socket pair通知机制,以及是如何将 Signal 事件集成到事件主循环之中的。