上一篇
上篇博文探讨了信号处理与libevent框架的结合,详细学习了struct event_base,struct event结构体及构成两个结构体的基本数据结构,见过了巧妙的函数回调机制与信号事件的唤醒机制,这对以后设计一个网络框架具有及其重要的意义。这篇文章介绍time-test.c这个例子,其通过libevent提供的定时事件来实现间隔固定时间打印的功能。
文章目录
一、从time-test.c 示例代码开始
int lasttime;
static void timeout_cb(int fd, short event, void *arg){}
int main (int argc, char **argv)
{
struct event timeout;
struct timeval tv;
/* Initalize the event library */
event_init();
/* Initalize one event */
evtimer_set(&timeout, timeout_cb, &timeout);
evutil_timerclear(&tv);
tv.tv_sec = 2;
event_add(&timeout, &tv);
lasttime = time(NULL);
event_dispatch();
return (0);
}
此处先贴出struct timeval的结构体定义,而且我们发现此处未直接定义event_base,那意味着我们不使用吗,且看下面event_init()程序在言。
struct timeval { //libevent-patches-1.4\compat\sys\_libevent_time.h
long tv_sec; /* seconds */
long tv_usec; /* and microseconds */
};
二、event_init函数为何物
struct event_base *current_base = NULL; //108line
struct event_base *event_init(void) //163line
{//libevent-patches-1.4\event.c
struct event_base *base = event_base_new();
if (base != NULL)
current_base = base;
return (base);
}
一看便知原来其在本质上还是调用上篇已经剖析过的event_base_new()函数生成一个struct event_base对象,并且为全局变量current_base赋值,这样可以用于在各种场景下调用base结构,之后继续调用evtimer_set等函数用于初始化定时事件!
三、初始化定时事件
首先调用evtimer_set(&timeout, timeout_cb, &timeout);
,然我们发现它是一个巧妙的宏,展开之后为event_set这个熟悉的函数,函数初始化timeout事件,并为其设置回调函数。
#define evtimer_set(ev, cb, arg) event_set(ev, -1, 0, cb, arg)
void event_set(struct event *ev, int fd, short events,
void (*callback)(int, short, void *), void *arg)
{
/* Take the current base - caller needs to set the real base later */
ev->ev_base = current_base;
ev->ev_callback = callback;
ev->ev_arg = arg;
ev->ev_fd = fd;
ev->ev_events = events;
ev->ev_res = 0;
ev->ev_flags = EVLIST_INIT;
ev->ev_ncalls = 0;
ev->ev_pncalls = NULL;
min_heap_elem_init(ev);
/* by default, we put new events into the middle priority */
if(current_base)
ev->ev_pri = current_base->nactivequeues/2;
}
其上先重点关注timeout->ev_base = current_base,timeout->ev_callback = timeout_cb,timeout->ev_arg = &timeout这几个成员即可。
接着调用evutil_timerclear(&tv)这个宏,这个宏作用显而易见,清空struct timeval tv值。 最后函数会设置时间间隔为2s.
#define evutil_timerclear(tvp) timerclear(tvp)
#define timerclear(tvp) (tvp)->tv_sec = (tvp)->tv_usec = 0
接着就要调用event_add函数将该定时事件加入到监控中去。
四、瞧一瞧event_add如何添加定时事件
int event_add(struct event *ev, const struct timeval *tv)
{
...
if (tv != NULL && !(ev->ev_flags & EVLIST_TIMEOUT)) {
if (min_heap_reserve(&base->timeheap,1 + min_heap_size(&base->timeheap)) == -1)
//此处主要用于为current_base->timeheap设置相应的值
return (-1); /* ENOMEM == errno */
}
.../*省略部分为将事件加入到poll/epoll监听机制里面的函数逻辑,显然定时器不需要使用他们*/
if (res != -1 && tv != NULL) {
struct timeval now;
...
gettime(base, &now);//获取现在时间
evutil_timeradd(&now, tv, &ev->ev_timeout); //设置定时事件的超时时间
。。。
event_queue_insert(base, ev, EVLIST_TIMEOUT);
}
return (res);
}
故上面的函数首先调用min_heap_reserve用于为current_base->timeheap设置相应的值,然后设置定时事件的超时时间即ev->ev_timeout值。最后调用event_queue_insert(base, ev, EVLIST_TIMEOUT)函数,其主要做了些下面的事,其中min_heap_push函数则是将该要监听的超时事件添加到base->timeheap这个小根堆上以便后面取出该事件使用!
void event_queue_insert(struct event_base *base, struct event *ev, int queue)
{ ...
if (~ev->ev_flags & EVLIST_INTERNAL)
base->event_count++;
ev->ev_flags |= queue;
...
case EVLIST_TIMEOUT: {
min_heap_push(&base->timeheap, ev);
break;
}
五、最后调用event_dispatch让定时事件跑起来
event_dispatch()调用event_loop(0),其继续调用event_base_loop(current_base, 0),在event_base_loop中最重要的就是一个while循环我们来看一看在定时事件上其如何发挥作用。
done = 0;
while (!done) {
/* Terminate the loop if we have been asked to */
......
timeout_correct(base, &tv);
/*简而言之,此处当为更新超时时间*/
tv_p = &tv;
if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) {
timeout_next(base, &tv_p);
} else {
evutil_timerclear(&tv);}
...
/* update last old time */
gettime(base, &base->event_tv);
/* clear time cache */
base->tv_cache.tv_sec = 0;
/*对于超时事件来说,dispatch不做任何事,超时后立即返回*/
res = evsel->dispatch(base, evbase, tv_p);
gettime(base, &base->tv_cache);
/*timeout_process若有超时事件发生则处理之*/
timeout_process(base);
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;
}
显然由上面的代码可知,若有超时事件发生时首先调用timeout_process(base),
void
timeout_process(struct event_base *base)
{
...
while ((ev = min_heap_top(&base->timeheap))) {
if (evutil_timercmp(&ev->ev_timeout, &now, >))
break;
/* delete this event from the I/O queues */
event_del(ev);
...
event_active(ev, EV_TIMEOUT, 1);
}
}
该函数首先判断确实有超时事件发生,然后调用event_active(ev, EV_TIMEOUT, 1)函数处理该超时事件ev,把该函数外壳拔掉则为event_queue_insert(ev->ev_base, ev, EVLIST_ACTIVE);函数
void event_queue_insert(struct event_base *base, struct event *ev, int queue)
{
。。。
if (~ev->ev_flags & EVLIST_INTERNAL)
base->event_count++;
。。。
case EVLIST_ACTIVE:
base->event_count_active++;
TAILQ_INSERT_TAIL(base->activequeues[ev->ev_pri],
ev,ev_active_next);
break;
。。。
}
面对超时事件他会调用TAILQ_INSERT_TAIL宏将该事件加入到base->activequeues[0]这个待处理队列中去,至此终于将超时事件处理完毕加入等待队列,接下来继续回到base->activequeues中去执行下一个函数去处理它。即执行event_process_active(base)函数,这是一个熟悉的函数,在上一章节中刚讲过它的具体处理逻辑,现在把他再贴出来:
static void
event_process_active(struct event_base *base)
{
.../*获取处理事件头*/
for (i = 0; i < base->nactivequeues; ++i) {
if (TAILQ_FIRST(base->activequeues[i]) != NULL) {
activeq = base->activequeues[i];
break;
}
}
/*for循环内依次处理每一个待处理事件*/
for (ev = TAILQ_FIRST(activeq); ev; ev = TAILQ_FIRST(activeq)) {
if (ev->ev_events & EV_PERSIST)
event_queue_remove(base, ev, EVLIST_ACTIVE);
else
event_del(ev);
/* Allows deletes to work */
ncalls = ev->ev_ncalls;
ev->ev_pncalls = &ncalls;
while (ncalls) {
ncalls--;
ev->ev_ncalls = ncalls;
(*ev->ev_callback)((int)ev->ev_fd, ev->ev_res, ev->ev_arg);
if (event_gotsig || base->event_break)
return;
}
}
}
最终会调用触发事件的回调函数,本事件应该出发头部的timeout_cb函数。
六、总结
总之,通过两篇文章两个例子熟悉了libevent的基本执行流程,从事件源的构造与初始化、事件多路分发机制的设计、Reactor框架的设计及事件处理程序等多个方面为我们展示了高性能网络框架的奥秘与设计思路。印象最深的莫过于双向链表的使用与维护,完备的事件侦测与校验机制以及可怕的事件回调机制方式,妙!
下一篇