libevent - note 之 时间管理 - 时间缓存与校正
上一文章中,知道在libevent中,用了小根堆对Timer事件进行管理,其key值对应事件
的超时时间,从小根堆中可以取出最小的超时时间
对于Timer,libevnet有一个时间管理机制,即下面要说到的等待时间管理,时间缓存校正等
1.等待时间处理
- timeout_next()函数 – 计算等待时间
对于Timer事件,什么时候执行需要根据对应事件的超时时间来定,所以要知道相应的最短的等待时间可以取出小根堆里最小的超时时间进行计算
static int
timeout_next(struct event_base *base, struct timeval **tv_p)
{
struct timeval now;
struct event *ev;
struct timeval *tv = *tv_p;
int res = 0;
//堆的首元素具有最小的超时值
ev = min_heap_top(&base->timeheap);
if (ev == NULL) {
/* if no time-based events are active wait for I/O */
*tv_p = NULL;//如果没有定时事件,将等待时间设置为NULL,表示一直阻塞直到有I/O事件发生
...
}
if (gettime(base, &now) == -1) {
res = -1;
}
//如果超时时间<=当前值,不能等待,需要立即返回
if (evutil_timercmp(&ev->ev_timeout, &now, <=)) {
evutil_timerclear(tv);
...
}
// 计算等待的时间=当前时间-最小的超时时间
evutil_timersub(&ev->ev_timeout, &now, tv);
return (res);
}
- event_base_loop()循环的处理
得到Timer事件的最小 超时时间有什么用呢?得到最小超时时间后,根据它来设置系统I/O的timeout时间,当系统I/O返回时,再激活就绪的定时事件。
(这里,系统I/O机制包括select()或epoll_wait()根据此制定一个最大等待时间 )
...
//活跃事件总数为0,或flags标志为0,或EVLOOP阻塞,进行等待时间计算
if (!base->event_count_active && !(flags & EVLOOP_NONBLOCK)) {
timeout_next(base, &tv_p);// 根据Timer事件计算evsel->dispatch的最大等待时间
} else {
evutil_timerclear(&tv);//如果还有活动事件,就不要等待,让evsel->dispatch立即返回
}
res = evsel->dispatch(base, tv_p);
timeout_process(base);//处理超时事件,将超时事件插入到激活链表中
...
由上可以看出,libevent对Timer事件的处理方法就是先计算小根堆最小等待时间是否超时,若超时,将超时的Timer事件插入到激活链表中
2.时间校正与调整
对于libevent来讲,由于超时操作依赖于时间的准确度,所以每次的操作都要进行时间管理
其主要包括 如何获取时间,当系统时间被更改时如何校正调整,小根堆的调整
2.1 时间的获取
使用时间缓存及更新时间缓存的操作
作用:不必每次获取时间都执行系统调用这个相对费时的操作;
/*********gettime 函数里,获取缓存时间,如果没有则系统调用************/
static int gettime(struct event_base *base, struct timeval *tp)
{
//如果tv_cache时间缓存已设置,就直接使用
if (base->tv_cache.tv_sec) {
*tp = base->tv_cache;
return (0);
}
...
}
在主循环中
int event_base_loop(struct event_base *base, int flags)
{
base->tv_cache.tv_sec = 0;//清空时间缓存
while(!done){
...
gettime(base, &base->event_tv);// 获取时间到event_tv
base->tv_cache.tv_sec = 0;// 清空时间缓存,只能系统调用-- 时间点1
res = evsel->dispatch(base, evbase, tv_p);// 等待I/O事件就绪
gettime(base, &base->tv_cache);// 获取时间到tv_cache,-- 时间点2
... 处理就绪事件
}
// 退出时也要清空时间缓存
base->tv_cache.tv_sec = 0;
return (0);
}
在标注的时间点 2 到时间点 1 的这段时间(处理就绪事件时),调用 gettime()取得的都是
tv_cache 缓存的时间
由此使用时间缓存,避免了频繁调用系统时间获取
2.2 时间校正 与 小根堆的调整
Monotonic时间类似于uptime,即从开机后算起到当前的时间
函数detect_monotonic()中,检测到系统支持Monotonic时间
就将全局变量 use_monotonic 设置为 1
如果系统支持monotonic 时间,即所谓的Uptime时间,一般不会被更改,则不需要执行校正
要校正的情况:
非monotonic时间,时间被向前调整了
这会造成一个情况,当前获取的新时间会小于之前获取的时间,即
tv_cache(新cache)反而小于event_tv的时间(上次的cache)
调整函数 timeout_correct()完成:
/********evutil.h***/
static void timeout_correct(struct event_base *base, struct timeval *tv)
{
struct event **pev;
unsigned int size;
struct timeval off;
if (use_monotonic) //monotonic时间就直接返回,无需调整
return;
gettime(base, tv); // tv<---tv_cache
if (evutil_timercmp(tv, &base->event_tv, >=)) {
base->event_tv = *tv;
return;
}
evutil_timersub(&base->event_tv, tv, &off);// 计算时间差值
//调整定时事件小根堆
pev = base->timeheap.p;
size = base->timeheap.n;
for (; size-- > 0; ++pev) {
struct timeval *ev_tv = &(**pev).ev_timeout;
evutil_timersub(ev_tv, &off, ev_tv); //ev_tv = ev_tv -&off
}
base->event_tv = *tv; // 更新event_tv为tv_cache
}
在调整小根堆时,因为所有定时事件的时间值都会被减去相同的值,因此虽然堆中元
素的时间键值改变了,但是相对关系并没有改变,不会改变堆的整体结构。因此只需要遍历
堆中的所有元素,将每个元素的时间键值减去相同的值即可完成调整,不需要重新调整堆的
结构。
当然调整完后,要将 event_tv 值重新设置为 tv_cache 值了
参考:libevent源码分析