前言
在本小节中,我们将展开对定时事件的研究。首先还是和研究信号事件部分一样,先看看它是如何集成到多路I/O中的(或者说是如何与event_base联系起来的)。
如何将定时事件集成到主循环中
由于seletc、poll、epoll这类多路I/O机制支持定时,所以将定时事件集成到主循环中比起信号事件容易的多。我们只需要将定时事件注册到小根堆上,然后根据堆顶(最短超时事件)来计算多路I/O机制需要等待的最大超时时间,这样超时之后,就可以处理就绪的定时事件了。
在主循环中,有这样一段代码:
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.
*/
evutil_timerclear(&tv);
}
...
res = evsel->dispatch(base, evbase, tv_p);
...
timeout_process(base);
timeout_next
用于计算主循环最大的等待时间,这里将tv_p
赋值为等待的时间。
然后evsel->dispatch
调用具体的多路I/O机制的等待函数,比如epoll的epoll_wait
,最后一个参数设置的定时时间就是timeout_next
计算得出的时间。这样的话,就不用来一个定时事件就调用epoll_wait
等待,而是将定时事件都集中在一起处理。
在这段代码中,有三个核心函数timeout_next
、dispatch
、timeout_process
。接下来我们会介绍第一个和最后一个,dispatch
放在关于多路I/O机制集成部分的小节讲。
timeout_next
static int
timeout_next(struct event_base *base, struct timeval **tv_p)
{
struct timeval now;
struct event *ev;
struct timeval *tv = *tv_p;
/* min_heap_top返回处于小根堆的堆顶的定时事件 */
if ((ev = min_heap_top(&base->timeheap)) == NULL) {
/* if no time-based events are active wait for I/O */
//没有定时时间,讲等待时间赋为null,返回
*tv_p = NULL;
return (0);
}
//获取当前时间
if (gettime(base, &now) == -1)
return (-1);
/*
* 比较当前最快超时的时间与当前时间
* 小与等与:不用等待了,直接返回
*/
if (evutil_timercmp(&ev->ev_timeout, &now, <=)) {
evutil_timerclear(tv);
return (0);
}
//计算需要等待的时间,即超时时间-当前时间,并将结果赋给tv
evutil_timersub(&ev->ev_timeout, &now, tv);
assert(tv->tv_sec >= 0);
assert(tv->tv_usec >= 0);
event_debug(("timeout_next: in %ld seconds", tv->tv_sec));
return (0);
}
该函数的作用就是计算需要等待的时间,步骤如下:
- 取得最快超时的时间
- 将其与当前时间比较
- 如若小于等与当前时间,证明已经超时了,将
tv
置成0,返回之后,dispatch
会很快将其激活(等待时间都赋成0了) - 如若大于当前时间,证明还有段时间需要等待,调用
evutil_timersub
函数做个减法,将等待时间赋给tv
tv
指向tv_p
,而tv_p
用于给dispatch
指定超时时间。
timeout_process
void
timeout_process(struct event_base *base)
{
struct timeval now;
struct event *ev;
if (min_heap_empty(&base->timeheap))
return;
gettime(base, &now);
//取得小根堆顶的事件,即最有可能已经超时的事件。
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_debug(("timeout_process: call %p",
ev->ev_callback));
//激活
event_active(ev, EV_TIMEOUT, 1);
}
}
这个函数的逻辑也很简单:
- 把小根堆顶的事件的超时时间拿来和当前时间比较
- 如果大于,则证明时间还没到,那么小根堆其余的事件也不可能超时了
- 如果小于,证明该事件已经满足激活条件了(超时),将其激活
- 重复查看新的小根堆顶是否满足条件,直至不满足条件或者堆中已经没有事件了。
小结
在本小节中,我们知道了定时事件是如何集成到一起让主循环去处理的,并且仔细看了timeout_next
以及timeout_process
这两个重要的函数,在下一小节中,我们将看到关于时间管理部分的函数,即timeout_correct
还有时间的加减、获取等。