• libevent源码深度剖析十一


    libevent源码深度剖析十一

    ——时间管理
    张亮

         为了支持定时器,Libevent必须和系统时间打交道,这一部分的内容也比较简单,主要涉及到时间的加减辅助函数、时间缓存、时间校正和定时器堆的时间值调整等。下面就结合源代码来分析一下。

    1 初始化检测

        Libevent在初始化时会检测系统时间的类型,通过调用函数detect_monotonic()完成,它通过调用clock_gettime()来检测系统是否支持monotonic时钟类型:

    1. static void detect_monotonic(void)  
    2. {  
    3. #if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)  
    4.     struct timespec    ts;  
    5.     if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0)  
    6.         use_monotonic = 1; // 系统支持monotonic时间  
    7. #endif  
    8. }  


         Monotonic时间指示的是系统从boot后到现在所经过的时间,如果系统支持Monotonic时间就将全局变量use_monotonic设置为1,设置use_monotonic到底有什么用,这个在后面说到时间校正时就能看出来了。

    2 时间缓存

         结构体event_base中的tv_cache,用来记录时间缓存。这个还要从函数gettime()说起,先来看看该函数的代码:

    1. static int gettime(struct event_base *base, struct timeval *tp)  
    2. {  
    3.     // 如果tv_cache时间缓存已设置,就直接使用  
    4.     if (base->tv_cache.tv_sec) {  
    5.         *tp = base->tv_cache;  
    6.         return (0);  
    7.     }  
    8.     // 如果支持monotonic,就用clock_gettime获取monotonic时间  
    9. #if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)  
    10.     if (use_monotonic) {  
    11.         struct timespec    ts;  
    12.         if (clock_gettime(CLOCK_MONOTONIC, &ts) == -1)  
    13.             return (-1);  
    14.         tp->tv_sec = ts.tv_sec;  
    15.         tp->tv_usec = ts.tv_nsec / 1000;  
    16.         return (0);  
    17.     }  
    18. #endif  
    19.     // 否则只能取得系统当前时间  
    20.     return (evutil_gettimeofday(tp, NULL));  
    21. }  



         如果tv_cache已经设置,那么就直接使用缓存的时间;否则需要再次执行系统调用获取系统时间。
         函数evutil_gettimeofday()用来获取当前系统时间,在Linux下其实就是系统调用gettimeofday();Windows没有提供函数gettimeofday,而是通过调用_ftime()来完成的。
         在每次系统事件循环中,时间缓存tv_cache将会被相应的清空和设置,再次来看看下面event_base_loop的主要代码逻辑:

    1. int event_base_loop(struct event_base *base, int flags)  
    2. {  
    3.     // 清空时间缓存  
    4.     base->tv_cache.tv_sec = 0;  
    5.     while(!done){  
    6.         timeout_correct(base, &tv); // 时间校正  
    7.         // 更新event_tv到tv_cache指示的时间或者当前时间(第一次)  
    8.          // event_tv <--- tv_cache  
    9.         gettime(base, &base->event_tv);  
    10.         // 清空时间缓存-- 时间点1  
    11.         base->tv_cache.tv_sec = 0;  
    12.         // 等待I/O事件就绪  
    13.         res = evsel->dispatch(base, evbase, tv_p);  
    14.         // 缓存tv_cache存储了当前时间的值-- 时间点2  
    15.          // tv_cache <--- now  
    16.         gettime(base, &base->tv_cache);  
    17.         // .. 处理就绪事件  
    18.     }  
    19.     // 退出时也要清空时间缓存  
    20.     base->tv_cache.tv_sec = 0;  
    21.     return (0);  
    22. }  


         时间event_tv指示了dispatch()上次返回,也就是I/O事件就绪时的时间,第一次进入循环时,由于tv_cache被清空,因此gettime()执行系统调用获取当前系统时间;而后将会更新为tv_cache指示的时间。
         时间tv_cache在dispatch()返回后被设置为当前系统时间,因此它缓存了本次I/O事件就绪时的时间(event_tv)。
    从代码逻辑里可以看出event_tv取得的是tv_cache上一次的值,因此event_tv应该小于tv_cache的值。
         设置时间缓存的优点是不必每次获取时间都执行系统调用,这是个相对费时的操作;在上面标注的时间点2到时间点1的这段时间(处理就绪事件时),调用gettime()取得的都是tv_cache缓存的时间。

    3 时间校正

         如果系统支持monotonic时间,该时间是系统从boot后到现在所经过的时间,因此不需要执行校正。
    根据前面的代码逻辑,如果系统不支持monotonic时间,用户可能会手动的调整时间,如果时间被向前调整了(MS前面第7部分讲成了向后调整,要改 正),比如从5点调整到了3点,那么在时间点2取得的值可能会小于上次的时间,这就需要调整了,下面来看看校正的具体代码,由函数 timeout_correct()完成:

    1. static void timeout_correct(struct event_base *base, struct timeval *tv)  
    2. {  
    3.     struct event **pev;  
    4.     unsigned int size;  
    5.     struct timeval off;  
    6.     if (use_monotonic) // monotonic时间就直接返回,无需调整  
    7.         return;  
    8.     gettime(base, tv); // tv <---tv_cache  
    9.     // 根据前面的分析可以知道event_tv应该小于tv_cache  
    10.     // 如果tv < event_tv表明用户向前调整时间了,需要校正时间  
    11.     if (evutil_timercmp(tv, &base->event_tv, >=)) {  
    12.         base->event_tv = *tv;  
    13.         return;  
    14.     }  
    15.     // 计算时间差值  
    16.     evutil_timersub(&base->event_tv, tv, &off);  
    17.     // 调整定时事件小根堆  
    18.     pev = base->timeheap.p;  
    19.     size = base->timeheap.n;  
    20.     for (; size-- > 0; ++pev) {  
    21.         struct timeval *ev_tv = &(**pev).ev_timeout;  
    22.         evutil_timersub(ev_tv, &off, ev_tv);  
    23.     }  
    24.     base->event_tv = *tv; // 更新event_tv为tv_cache  
    25. }  


         在调整小根堆时,因为所有定时事件的时间值都会被减去相同的值,因此虽然堆中元素的时间键值改变了,但是相对关系并没有改变,不会改变堆的整体结构。因此只需要遍历堆中的所有元素,将每个元素的时间键值减去相同的值即可完成调整,不需要重新调整堆的结构。
    当然调整完后,要将event_tv值重新设置为tv_cache值了。

    4 小节

         主要分析了一下libevent对系统时间的处理,时间缓存、时间校正和定时堆的时间值调整等,逻辑还是很简单的,时间的加减、设置等辅助函数则非常简单,主要在头文件evutil.h中,就不再多说了。

  • 相关阅读:
    模拟退火、禁忌搜索、迭代局部搜索求解TSP问题Python代码分享
    多起点的局部搜索算法(multi-start local search)解决TSP问题(附Java代码及注释)
    爬取一定范围内的地图兴趣点并生成地点分布图
    Tabu Search求解作业车间调度问题(Job Shop Scheduling)-附Java代码
    Python爬虫系列
    干货 | 蚁群算法求解带时间窗的车辆路径规划问题详解(附Java代码)
    10分钟教你Python爬虫(下)--爬虫的基本模块与简单的实战
    vs code 打开文件时,取消文件目录的自动定位跟踪
    eclipse自动补全导致变量会跟上String后缀的问题解决
    16. nested exception is com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "auditUnitName"
  • 原文地址:https://www.cnblogs.com/breg/p/3725770.html
Copyright © 2020-2023  润新知