前言
在上一小节,我们主要介绍了定时事件相关的函数。在本小节中,为了加强这部分的理解,我们将探讨libevent有关时间管理的部分,比如我们之前在event_base_loop
中看到的时间缓存,时间校正这些。
初始化
在event_base_new
函数中有这样一段代码:
detect_monotonic();
gettime(base, &base->event_tv);
min_heap_ctor(&base->timeheap);
detect_monotonic
:检测系统是否支持monotonic
时钟类型(monotonic时间自系统开机后就一直单调递增,但是不计算系统休眠时间)
gettime
:将base->event_tv
设置成当前时间
min_heap_ctor
:将min_heap_t
的成员赋成0
这里我们首先来看detect_monotonic
函数:
static void
detect_monotonic(void)
{
#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
struct timespec ts;
if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0)
use_monotonic = 1;
#endif
}
很简短的一个函数,它首先利用条件编译判断当前系统是否支持monotonic以及clock_gettime函数。然后使用clock_gettime
系统调用取得当前系统时间的struct timespec
形式(该结构体成员可以精确到纳秒级),最后将use_monotonic
置为1。
接着便是gettime
函数
gettime
static int
gettime(struct event_base *base, struct timeval *tp)
{
//判断有无时间缓存
if (base->tv_cache.tv_sec) {
*tp = base->tv_cache;
return (0);
}
#if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
//支持使用monotonic时间类型
if (use_monotonic) {
struct timespec ts;
//取得当前系统时间
if (clock_gettime(CLOCK_MONOTONIC, &ts) == -1)
return (-1);
//赋值
tp->tv_sec = ts.tv_sec;
tp->tv_usec = ts.tv_nsec / 1000;
return (0);
}
#endif
//如果不支持monotonic就只好直接调用evutil_gettimeofday取得当前系统时间了
return (evutil_gettimeofday(tp, NULL));
}
该函数逻辑大致如下:
- 如果有时间缓存,就可以直接获取该缓存并返回
- 否则便检测当前系统是否支持用
clock_gettime
将monotonic时间类型转换为struct timespec
- 如果支持,则调用该函数,然后给tp赋值并返回
- 如果不支持,则直接使用
evutil_gettimeofday
函数取得系统当前时间。
这里需要有2点注意:
1. struct timeval
支持ms级,而struct timespec
支持ns级,所以在转换的时候需要换算一下
2. evutil_gettimeofday
只是做了一层简单的封装,为了应对不同的平台。linux下直接使用系统调用gettimeofday()
,windows下使用_ftime()
。
你可能会想时间缓存到底用来干什么,它代表什么以及为什么我们不一开始就使用evutil_gettimeofday
来获取时间,而要大费周折的来判断是否能用monotonic。
接下来我们就依次来解决这些问题。
首先缓存肯定是为了节约时间,在gettime
函数中,如果无缓存,则会调用函数去获取当前系统时间,如果有缓存,则赋给tp
直接返回。这至少可以看出,时间缓存缓存的是当前的系统时间,不过不完全对。我们需要再看看其他使用缓存的地方,比如event_base_loop
中(只列举了部分代码)。
...
/* clear time cache */
//主循环一开始便将时间缓存赋为0
base->tv_cache.tv_sec = 0;
...
done = 0;
while (!done) {
...
/*
* 校正时间,这个函数我们等下再分析
* 它的作用就是防止gettimeofday由于NTR(Network Time Protocol)发生的时间回退问题,将时间加以校正
*/
timeout_correct(base, &tv);
...
/* update last old time */
//将base->event_tv的时间更新成缓存时间或者当前时间
gettime(base, &base->event_tv);
/* clear time cache */
//清除缓存
base->tv_cache.tv_sec = 0;
//等待事件被触发
res = evsel->dispatch(base, evbase, tv_p);
if (res == -1)
return (-1);
//现在缓存的值是当前时间(因为前面进行了清0操作,所以无时间缓存,顺序进行到后面获取系统时间)
gettime(base, &base->tv_cache);
timeout_process(base);
...
}
/* clear time cache */
//最后再清除一次
base->tv_cache.tv_sec = 0;
...
关于为什么需要校正时间,其根本原因这里有详细介绍:
gettimeofday() should never be used to measure time
整理一下:
event_tv
主要存储的是dispatch
上次返回的时间,也就是事件就绪的时间。base->tv_cache
其实是给event_tv
提供缓存的,可以看到在dispatch
调用了之后,base->tv_cache
便设置成了当前系统的值,而下次循环的时候,gettime
便会将base->tv_cache
的值赋给base->event_ev
(不过第一次进循环的时候情况特殊,因为base->tv_cache
为0,所以第一次进循环时,base->event_ev
的值为当前系统的值)- 由于
base->event_tv
取的值都是上一次循环中base->tv_cache
的值,所以在未给base->event_tv
赋成最新的base->tv_cache
之前,base->event_tv
的值是小于base->tv_cache
的值的,这点需要理解,因为在校正时间的时候就利用了这一点。
最后总结一下,时间缓存主要是缓存的是从调用了dispatch
之后,再到调用dispatch
之前的这一段的时间,所以每次不必用gettime
每次都调用系统调用来获取时间了,而是直接取缓存(要知道系统调用是很耗时的,涉及到从用户态到内核态的切换)。
timeout_correct
static void
timeout_correct(struct event_base *base, struct timeval *tv)
{
struct event **pev;
unsigned int size;
struct timeval off;
//如果是使用的monotonic,不需要校正,直接返回
if (use_monotonic)
return;
/* Check if time is running backwards */
/* 接下来便检测时间是否回退 */
/* 获取时间
* tv可能有两种赋值,一种是tv_cache,另一种是系统时间
*/
gettime(base, tv);
/* 比较tv和base->event_tv的大小
* (你可能会想第一次进循环的时候,base->event_tv还没被赋值呢
* 别忘了,在event_base_new中已经将base->event_tv初始化了)
* 如果tv >= base->event_tv(正常情况下应如此,上面讲过理由),则不需要校正
* 如果tv < base->event_tv,则代表时间需要校正
*/
if (evutil_timercmp(tv, &base->event_tv, >=)) {
//不需要校正,赋值并返回
base->event_tv = *tv;
return;
}
/* 下面这一段都是进行校正操作 */
event_debug(("%s: time is running backwards, corrected",
__func__));
//base->event_tv - tv,并将结果赋给off(off就是需要调整的时间差)
evutil_timersub(&base->event_tv, tv, &off);
/*
* We can modify the key element of the node without destroying
* the key, beause we apply it to all in the right order.
*/
//将小根堆上所有的定时事件的定时值都减去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);
}
/* Now remember what the new time turned out to be. */
//最后将tv_cache赋值给event_tv
base->event_tv = *tv;
}
我们整理一下:
- 首先如果支持monotonic,则无需校正,因为是系统在引导开始的时间.比起
gettimeofday
可能造成的时间回退问题(gettimeofday
和time
都不应该用来衡量经过任意时间触发事件或其他),它更加的安全(这就是为什么gettime
需要大费周折的想知道系统到底支不支持monotonic) - 如果不支持,则使用
gettime
给tv赋值,并将其与base->event_tv
比较 - 如果大于,则是正常的,将tv的值赋给
base->event_tv
然后返回 - 如果小于,则代表需要校正,将两者差值作为校正值,给小根堆上每一个定时事件的时间都进行校正
- 最后将tv的值赋给
base->event_tv
并返回
小结
讲到这里,我相信你对该部分已经有一定的理解了。如果还有不清楚的地方,可以反复多读几次,tv_cache的意义可能有点难以理解,需要陪着循环的进行来理解。接下来我们就对那么多种多路I/O机制如何集成到libevent中的进行分析。