对于一个服务器来说,它对时间的要求并不是特别高,因此可以在程序中维护一个时间,然后在某些条件触发时对这个时间进行更新,程序中的时间均使用这个维护的时间量,这样做能避免每次需要时间的时候进行一次耗时的系统调用,提升程序性能。在Haproxy中,作者正是这样做的。
数据结构
[src/time.c]
unsigned int curr_sec_ms; /* millisecond of current second (0..999) */
unsigned int curr_sec_ms_scaled; /* millisecond of current second (0..2^32-1) */
unsigned int now_ms; /* internal date in milliseconds (may wrap) */
struct timeval now; /* internal date is a monotonic function of real clock */
struct timeval date; /* the real current date */
struct timeval start_date; /* the process's start date */
时间更新
时间部分最主要的就是时间更新函数tv_update_date了,这个函数在某些条件触发的时候调用以更新程序中维护的时间量。在前一部分中可看到,在每次epoll_wait函数调用完成后均会调用此函数更新时间。
[src/time.c] tv_update_date()
/* tv_udpate_date: sets <date> to system time, and sets <now> to something as
* close as possible to real time, following a monotonic function. The main
* principle consists in detecting backwards and forwards time jumps and adjust
* an offset to correct them. This function should be called once after each
* poll, and never farther apart than MAX_DELAY_MS*2. The poll's timeout should
* be passed in <max_wait>, and the return value in <interrupted> (a non-zero
* value means that we have not expired the timeout). Calling it with (-1,*)
* sets both <date> and <now> to current date, and calling it with (0,1) simply
* updates the values.
*/
作者说明,每次poll调用后都会调用此函数进行时间更新,从前一节的分析可知,第一个参数是epoll_wait的超时时间,第二个参数是epoll_wait的返回值,当其返回0时表示超时返回,那么表示epoll_wait等待了max_wait的时间才返回,其在等待期间并没有被中断掉。其他两种情况都表示等待期间时间还没超时就被中断了。
[src/time.c] tv_update_date()
REGPRM2 void tv_update_date(int max_wait, int interrupted)
{
static struct timeval tv_offset; /* warning: signed offset! */
struct timeval adjusted, deadline;
gettimeofday(&date, NULL);
if (unlikely(max_wait < 0)) {
tv_zero(&tv_offset);
adjusted = date;
goto to_ms;
}
作者的注释说明当max_wait为负数时,会将date和now设置为当前时间。这是对应于-1的调用。
[src/time.c] tv_update_date()
__tv_add(&adjusted, &date, &tv_offset);
if (unlikely(__tv_islt(&adjusted, &now))) {
goto fixup; /* jump in the past */
}
/* OK we did not jump backwards, let's see if we have jumped too far
* forwards. The poll value was in <max_wait>, we accept that plus
* MAX_DELAY_MS to cover additional time.
*/
_tv_ms_add(&deadline, &now, max_wait + MAX_DELAY_MS);
if (likely(__tv_islt(&adjusted, &deadline)))
goto to_ms; /* OK time is within expected range */
如果max_wait不小于0,那么将静态变量中记录的上次时间误差与date相加作为调整值存入adjusted,如果调整后的值不小于之前设置的now,那么调整后的值下限符合要求,然后对其上限进行检查,上限是之前设置的now加上max_wait再加上系统允许的最大延迟偏移,如果adjusted符合要求,那么就直接设置now为调整后的值。否则的话,不管是上限还是下限哪一边不满足要求,则进入下面的fixup进行调整。
[src/time.c] tv_update_date()
fixup:
/* Large jump. If the poll was interrupted, we consider that the date
* has not changed (immediate wake-up), otherwise we add the poll
* time-out to the previous date. The new offset is recomputed.
*/
_tv_ms_add(&adjusted, &now, interrupted ? 0 : max_wait);
tv_offset.tv_sec = adjusted.tv_sec - date.tv_sec;
tv_offset.tv_usec = adjusted.tv_usec - date.tv_usec;
if (tv_offset.tv_usec < 0) {
tv_offset.tv_usec += 1000000;
tv_offset.tv_sec--;
}
作者说明,如果poll函数(epoll_wait)被中断了,那么就认为时间没有改变,否则时间就过了给予它的超时时间。根据是否被中断,决定将之前设置的now加上max_wait或者0放入adjusted。将adjusted与date的差值保存进tv_offset中。
此处作者将adjusted与date的差值保存到tv_offset用于下次的时间调整,我的理解是, gettimeofday系统调用在获取到时间之后并准备从内核返回时有可能导致调度而形成一个时间窗口,因此需要对它自己做一个调整。这与系统的调度相关,虽然说可能每次系统调用从进入到回来的耗时可能不一样,但是记录这一次的差值,然后在下一次调用的时候加上这个差值,大多数时候是能够降低系统的实际时间和获取到的时间之间的误差范围的。作者将调整后符合要求的时间赋予now,因为前面的注释中,作者说明now是尽量接近系统实时时间的一个量。
还有就是epoll_wait也是系统调用,并且其调用可能在中间被中断,那么它消耗了多少时间并不明确,因此,根据前一节的情况来看,这个调整应该还包括对epoll_wait中间被中断时消耗的时间对实际时间的影响,因为fixup中计算偏移值的时候只是根据是否被中断而使用不同的极值而已。
[src/time.c] tv_update_date()
to_ms:
now = adjusted;
curr_sec_ms = now.tv_usec / 1000; /* ms of current second */
curr_sec_ms_scaled = curr_sec_ms * 4294971; /* ms * 2^32 / 1000 */
now_ms = now.tv_sec * 1000 + curr_sec_ms;
return;
}
对于curr_sec_ms_scaled的作用在event_accept中会说明。