在上一小节,我们主要介绍了定时事件相关的函数。在本小节中,为了加强这部分的理解,我们将探讨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函数
该函数逻辑大致如下:
如果有时间缓存,就可以直接获取该缓存并返回否则便检测当前系统是否支持用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每次都调用系统调用来获取时间了,而是直接取缓存(要知道系统调用是很耗时的,涉及到从用户态到内核态的切换)。
我们整理一下:
首先如果支持monotonic,则无需校正,因为是系统在引导开始的时间.比起gettimeofday可能造成的时间回退问题(gettimeofday和time 都不应该用来衡量经过任意时间触发事件或其他),它更加的安全(这就是为什么gettime需要大费周折的想知道系统到底支不支持monotonic)如果不支持,则使用gettime给tv赋值,并将其与base->event_tv比较如果大于,则是正常的,将tv的值赋给base->event_tv然后返回如果小于,则代表需要校正,将两者差值作为校正值,给小根堆上每一个定时事件的时间都进行校正最后将tv的值赋给base->event_tv并返回讲到这里,我相信你对该部分已经有一定的理解了。如果还有不清楚的地方,可以反复多读几次,tv_cache的意义可能有点难以理解,需要陪着循环的进行来理解。接下来我们就对那么多种多路I/O机制如何集成到libevent中的进行分析。