(十三)时间管理

xiaoxiao2021-02-28  115

前言

在上一小节,我们主要介绍了定时事件相关的函数。在本小节中,为了加强这部分的理解,我们将探讨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中的进行分析。

转载请注明原文地址: https://www.6miu.com/read-58036.html

最新回复(0)