(十三)时间管理

前言

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

该函数逻辑大致如下:

  1. 如果有时间缓存,就可以直接获取该缓存并返回
  2. 否则便检测当前系统是否支持用clock_gettime将monotonic时间类型转换为struct timespec
  3. 如果支持,则调用该函数,然后给tp赋值并返回
  4. 如果不支持,则直接使用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

整理一下:

  1. event_tv主要存储的是dispatch上次返回的时间,也就是事件就绪的时间。
  2. base->tv_cache其实是给event_tv提供缓存的,可以看到在dispatch调用了之后,base->tv_cache便设置成了当前系统的值,而下次循环的时候,gettime便会将base->tv_cache的值赋给base->event_ev(不过第一次进循环的时候情况特殊,因为base->tv_cache为0,所以第一次进循环时,base->event_ev的值为当前系统的值)
  3. 由于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;
}

我们整理一下:

  1. 首先如果支持monotonic,则无需校正,因为是系统在引导开始的时间.比起gettimeofday可能造成的时间回退问题(gettimeofdaytime 都不应该用来衡量经过任意时间触发事件或其他),它更加的安全(这就是为什么gettime需要大费周折的想知道系统到底支不支持monotonic)
  2. 如果不支持,则使用gettime给tv赋值,并将其与base->event_tv比较
  3. 如果大于,则是正常的,将tv的值赋给base->event_tv然后返回
  4. 如果小于,则代表需要校正,将两者差值作为校正值,给小根堆上每一个定时事件的时间都进行校正
  5. 最后将tv的值赋给base->event_tv并返回

小结

讲到这里,我相信你对该部分已经有一定的理解了。如果还有不清楚的地方,可以反复多读几次,tv_cache的意义可能有点难以理解,需要陪着循环的进行来理解。接下来我们就对那么多种多路I/O机制如何集成到libevent中的进行分析。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值