源码阅读 libevent - 时间管理

为了支持定时器,libevent 必须和系统时间打交道,z 在 event_add 时,用户制定了一个超时时间,这个超时时间是一个相对时间,但在 event_add 函数内部,libevent 将其转换为了绝对时间,这就带来了一个问题:在系统时间改变时,libevent 需要修改 event 的绝对时间,为此 libevent 内部有以下几种解决方案:

  1. 使用单调时间 monotonic time
  2. 使用系统时间生成 fake monotonic time
  3. 使用系统时间(已弃用)

使用 monotonic 时间

什么是 monotonic 时间

monotonic time 字面意思是单调时间,实际上它指的是系统启动以后流逝的时间,这是由变量 jiffies 来记录的。

系统每次启动时 jiffies 初始化为 0,每来一个 timer interruptjiffies1,也就是说它代表系统启动后流逝的 tick

是否使用 monotonic 时间

决定 libevent 是否使用 monotonic 时间取决于系统是否支持 monotonic 时间,libevent 对其有两处判断,一处在编译时判断,一处在运行时判断:

编译时判断

通过 Cmake 判断系统环境,来确定获取 monotonic 时间的函数。

// CMakeLists.txt:
CHECK_FUNCTION_EXISTS_EX(clock_gettime EVENT__HAVE_CLOCK_GETTIME)
CHECK_FUNCTION_EXISTS_EX(mach_absolute_time EVENT__HAVE_MACH_ABSOLUTE_TIME)

// time-internal.h:
#if defined(EVENT__HAVE_CLOCK_GETTIME) && defined(CLOCK_MONOTONIC)
#define HAVE_POSIX_MONOTONIC
#elif defined(EVENT__HAVE_MACH_ABSOLUTE_TIME)
#define HAVE_MACH_MONOTONIC
#elif defined(_WIN32)
#define HAVE_WIN32_MONOTONIC
#else
#define HAVE_FALLBACK_MONOTONIC
#endif

根据 time-internal.h 中的四个不同的宏定义,libevent 定义了四个 evutil_configure_monotonic_time_ 函数,本文主要分析 HAVE_POSIX_MONOTONIC 宏定义下的 evutil_configure_monotonic_time_ 函数。

运行时判断

libevent 会在 event_base 创建时对 monotonic 时间的支持进行判断,判断方式为:调用不同平台下的 monotonic 时间获取函数,如果能正确返回,则代表该平台支持 monotonic 时间,libevent 后续使用 gettime 获取时间均会使用 monotonic 时间。

/* The POSIX clock_gettime() interface provides a few ways to get at a monotonic clock.
   CLOCK_MONOTONIC is most widely supported.
   Linux also provides a CLOCK_MONOTONIC_COARSE with accuracy of about 1-4 msec.
   On all platforms I'm aware of, CLOCK_MONOTONIC really is monotonic.
   Platforms don't agree about whether it should jump on a sleep/resume. */
int evutil_configure_monotonic_time_(struct evutil_monotonic_timer *base, int flags) {
    /* CLOCK_MONOTONIC exists on FreeBSD, Linux, and Solaris.  You need to check for it at runtime,
     * because some older kernel versions won't have it working. */
    const int fallback = flags & EV_MONOT_FALLBACK;
    struct timespec ts;

    if (!fallback && clock_gettime(CLOCK_MONOTONIC, &ts) == 0) {
        base->monotonic_clock = CLOCK_MONOTONIC;
        return 0;
    }
    if (CLOCK_MONOTONIC < 0) event_errx(1,"I didn't expect CLOCK_MONOTONIC to be < 0");
    base->monotonic_clock = -1;
    return 0;
}

如果 libevent 所在的系统支持 monotonic 时间,那么根本就不用考虑用户手动修改系统时间这坑爹的事情。但如果所在的系统没有支持 monotonic 时间,那么 libevent 就只能使用 evutil_gettimeofday 获取一个用户能修改的时间。

使用 fake monotonic 时间

从上述分析中可以看到,在平台不支持 base->monotonic_clock 会被 libevent 置为 -1,此时调用 gettime 就会调用 evutil_gettimeofday 来获取系统时间,并通过 adjust_monotonic_time 将获取到的系统时间调整为一个单调递增的时间。

int evutil_gettime_monotonic_(struct evutil_monotonic_timer *base, struct timeval *tp) {
    struct timespec ts;
    if (base->monotonic_clock < 0) {
        if (evutil_gettimeofday(tp, NULL) < 0) return -1; // evutil_gettimeofday 调用 gettimeofday 函数
        adjust_monotonic_time(base, tp);
        return 0;
    }

    if (clock_gettime(base->monotonic_clock, &ts) == -1) return -1;
    tp->tv_sec = ts.tv_sec;
    tp->tv_usec = ts.tv_nsec / 1000;
    return 0;
}

static int gettime(struct event_base *base, struct timeval *tp) {
    if (base->tv_cache.tv_sec) {
        *tp = base->tv_cache;
        return 0;
    }
    if (evutil_gettime_monotonic_(&base->monotonic_timer, tp) == -1) return -1;
    ... ...
    return 0;
}

系统时间到 fake monotonic 时间的转化

libevent 获取到系统时间后,会调用 adjust_monotonic_time 生成 fake monotonic 时间。

/* This function assumes it's called repeatedly with a not-actually-so-monotonic time source whose outputs
   are in 'tv'. It implements a trivial ratcheting mechanism so that the values never go backwards. */
static void adjust_monotonic_time(struct evutil_monotonic_timer *base, struct timeval *tv) {
    /* tv 输入的是 real time,输出为 fake monotonic time */
    evutil_timeradd(tv, &base->adjust_monotonic_clock, tv); // evutil_timeradd(a,b,c) <--> c = a + b
    // 如果 tv < last_time 表明用户向前调整时间了,需要校正时间
    if (evutil_timercmp(tv, &base->last_time, <)) { // evutil_timercmp(a,b,op) <--> a op b
        struct timeval adjust; // 保存差值
        evutil_timersub(&base->last_time, tv, &adjust); // evutil_timersub(a,b,c) <--> c = a - b
        evutil_timeradd(&adjust, &base->adjust_monotonic_clock, &base->adjust_monotonic_clock);
        *tv = base->last_time;
    }
    base->last_time = *tv;
}

从上面代码中可以看出,如果用户没有向前调整时间,则 adjust_monotonic_time 函数返回的就是真实的时间,如果用户向前调整了时间,libevent 会将用户向前调整的时间累计到 base->adjust_monotonic_clock 参数中,再之后获取的时间就都会被 adjust_monotonic_time 函数加上 base->adjust_monotonic_clock 再返回出来,从而实现时间的单调递增,即 fake monotonic 时间

使用系统时间

该部分内容已被 libevent2012 年弃用,如读者有兴趣,可以查看 参考资料[2] 中的分析。

Revision: f5e4eb05e5bdf61b0a1b11f99effaf835a8796ce
Author: Nick Mathewson <nickm@torproject.org>
Date: 2012/4/21 1:14:10
Message:
Refactor monotonic timer handling into a new type and set of functions; add a gettimeofday-based ratcheting implementation
Now, event.c can always assume that we have a monotonic timer; thismakes event.c easier to write.
----
Modified: event-internal.h
Modified: event.c
Modified: evutil_time.c
Modified: time-internal.h

参考资料