# [Erlang]时间函数及时间校正机制详解

abv123456789 分享于 2015-06-04

os:timestamp() 获取到的时间为操作系统的时间，不做任何修正；而erlang:now()，每次获取都会确保生成了唯一的时间，就是说，erlang:now()在实现上对时间做了一个校正，每次都生成一个单调向前的唯一值。

## erlang:now()的特点：

Monotonic
erlang:now() never jumps backwards - it always moves forward
Interval correct
The interval between two erlang:now() calls is expected to correspond to the correct time in real life (as defined by an atomic clock, or better)
Absolute correctness
The erlang:now/0 value should be possible to convert to an absolute and correct date-time, corresponding to the real world date and time (the wall clock)
System correspondence
The erlang:now/0 value converted to a date-time is expected to correspond to times given by other programs on the system (or by functions like os:timestamp/0)
Unique
No two calls to erlang:now on one Erlang node should return the same value

## erlang 时间校正

### 时间校正的作用：

2. 时间平稳：   同样举个例子，说明时间不平稳问题：   比如，erlang开发中，经常都会出现一个进程call另一个进程的场景，一般是5秒超时，假如时间突然加快了5秒，就相当于没有等待操作完成，就直接超时了。当然这是很不合理的

### erlang时间校正的特点：

Monotonic
The clock should not move backwards
Intervals should be near the truth
We want the actual time (as measured by an atomic clock or an astronomer) that passes between two time stamps, T1 and T2, to be as near to T2 - T1 as possible.
Tight coupling to the wall clock
We want a timer that is to be fired when the wall clock reaches a time in the future, to fire as near to that point in time as possible

### erlang是怎么校正时间的？

erlang内部时间会和系统挂钟时间保持同步，当系统挂钟时间突然改变时，erlang会比较两个时间的差异，让内部的时间的同步值轻微变大或变小，幅度最大是1%，就是说，VM经历 1s 实际上可能就是 0.99s 或者1.01s。当系统时间改变了1分钟，erlang会花100分钟来慢慢校正，并最终和系统时间保持同步。

### 哪些函数受到时间校正影响？

erlang:now/0
The infamous erlang:now/0 function uses time correction so that differences between two "now-timestamps" will correspond to other timeouts in the system. erlang:now/0 also holds other properties, discussed later.
Timeouts on receive uses time correction to determine a stable timeout interval.
The timer module
As the timer module uses other built in functions which deliver corrected time, the timer module itself works with corrected time.
erlang:start_timer/3 and erlang:send_after/3
The timer BIF's work with corrected time, so that they will not fire prematurely or too late due to changes in the wall clock time.

## 源码剖析

erlang:now() 是 bif 实现，代码如下：（以R16B02为例） [cpp]  view plain copy
1. /*
2.  * bif.c now_0函数，实现 erlang:now/0
3.  * return a timestamp
4.  */
5. BIF_RETTYPE now_0(BIF_ALIST_0)
6. {
7.     Uint megasec, sec, microsec;
8.     Eterm* hp;
9.
10.     get_now(&megasec, &sec, &microsec); // 获取当前时间
11.     hp = HAlloc(BIF_P, 4);
12.     BIF_RET(TUPLE3(hp, make_small(megasec), make_small(sec),
13.            make_small(microsec))); // 返回{MegaSecs, Secs, MicroSecs}
14. }

1. /*
2.  * erl_time_sup.c get_now函数，获取当前时间
3.  * get a timestamp
4.  */
5. void get_now(Uint* megasec, Uint* sec, Uint* microsec)
6. {
7.     SysTimeval now;
8.
9.     erts_smp_mtx_lock(&erts_timeofday_mtx);
10.
11.     get_tolerant_timeofday(&now); // 获取当前时间值
12.     do_erts_deliver_time(&now);  // 记录当前的时间（用于VM内部读取当前时间，如timer）
13.
14.     /* 确保时间比上次获取的大 */
15.     if (then.tv_sec > now.tv_sec ||
16.     (then.tv_sec == now.tv_sec && then.tv_usec >= now.tv_usec)) {
17.     now = then;
18.     now.tv_usec++;
19.     }
20.     /* Check for carry from above + general reasonability */
21.     if (now.tv_usec >= 1000000) {
22.     now.tv_usec = 0;
23.     now.tv_sec++;
24.     }
25.     then = now;
26.
27.     erts_smp_mtx_unlock(&erts_timeofday_mtx);
28.
29.     *megasec = (Uint) (now.tv_sec / 1000000);
30.     *sec = (Uint) (now.tv_sec % 1000000);
31.     *microsec = (Uint) (now.tv_usec);
32.
33.     update_approx_time(&now);//更新「简要」时间（仅用于标记进程启动时间）
34. }

1. /*
2.  * erl_time_sup.c get_tolerant_timeofday函数，获取当前时间
3.  * 根据系统API不同有两种实现，这里取其中一种做说明
4.  */
5. static void get_tolerant_timeofday(SysTimeval *tv)
6. {
7.     SysHrTime diff_time, curr;
8.
9.     if (erts_disable_tolerant_timeofday) {// 时间校正功能被禁用，直接返回系统时间
10.     sys_gettimeofday(tv);
11.     return;
12.     }
13.     *tv = inittv; // 取VM启动时间
14.
15.     // 计算从VM启动到现在经过的内部时间（正值，单位微秒）
16.     diff_time = ((curr = sys_gethrtime()) + hr_correction - hr_init_time) / 1000;
17.
18.     if (curr < hr_init_time) {
19.     erl_exit(1,"Unexpected behaviour from operating system high "
20.          "resolution timer");
21.     }
22.
23.     // 检查是否刚校正过（两次校正最小间隔 1s）
24.     if ((curr - hr_last_correction_check) / 1000 > 1000000) {
25.     /* Check the correction need */
26.     SysHrTime tv_diff, diffdiff;
27.     SysTimeval tmp;
28.     int done = 0;
29.
30.     // 计算从VM启动到现在经过的实际时间（如果系统时间被调整过，可能是负值，单位微秒）
31.     sys_gettimeofday(&tmp);
32.     tv_diff = ((SysHrTime) tmp.tv_sec) * 1000000 + tmp.tv_usec;
33.     tv_diff -= ((SysHrTime) inittv.tv_sec) * 1000000 + inittv.tv_usec;
34.     diffdiff = diff_time - tv_diff;// 实际时间与内部时间的差值（缩短这个时间差以赶上实际时间）
35.     if (diffdiff > 10000) { // 内部时间比外部时间快 0.01s 以上
36.         SysHrTime corr = (curr - hr_last_time) / 100; //  两次调用经过的实际时间 * 1%
37.         if (corr / 1000 >= diffdiff) {
38.             ++done;
39.             hr_correction -= ((SysHrTime)diffdiff) * 1000;
40.             /* 超过diffdiff*1000 * 100，只修正 diffdiff*1000，
41.              * 就是1s需要花100s修正，同时标记本次修正完成
42.              * 什么情况下会走到这里：就是这个函数很久没调用，超过了时间偏差的100倍
43.              * 然后标记修正完成，至此，就没有时间偏差了
44.              */
45.         } else {
46.         hr_correction -= corr; // 修正值为两次调用经过的实际时间 * 1%
47.         }
48.         // 重算与VM启动时间的间隔
49.         diff_time = (curr + hr_correction - hr_init_time) / 1000;
50.     } else if (diffdiff < -10000) { // 内部时间比外部时间慢 0.01s 以上
51.         SysHrTime corr = (curr - hr_last_time) / 100;
52.         if (corr / 1000 >= -diffdiff) {
53.         ++done;
54.         hr_correction -= ((SysHrTime)diffdiff) * 1000;
55.         } else {
56.         hr_correction += corr;
57.         }
58.         diff_time = (curr + hr_correction - hr_init_time) / 1000;
59.     } else {
60.         /* 内部时间与外部时间偏差在0.01s 内，标记完成，等1s后修正剩下的时间
61.          * 这段代码目的是，如果时间偏差在0.01s内，VM特意等1s后修正这个时间
62.          * 另外，如果时间没出差错，就都走到这里，减少时间函数调用开销
63.          */
64.         ++done;
65.     }
66.     if (done) {
67.         hr_last_correction_check = curr;
68.     }
69.     }
70.     tv->tv_sec += (int) (diff_time / ((SysHrTime) 1000000));
71.     tv->tv_usec += (int) (diff_time % ((SysHrTime) 1000000));
72.     if (tv->tv_usec >= 1000000) {
73.     tv->tv_usec -= 1000000;
74.     tv->tv_sec += 1;
75.     }
76.     hr_last_time = curr;
77. }

## 拓展阅读

### gethrtime()

[cpp]  view plain copy
1. #define sys_gethrtime() gethrtime()

## 结束语

R18之后，erlang提供了更多时间校正相关的API，对用户暴露底层时间的相关信息。这里暂时就不说明了。 链接地址

2015/5/5  修正标题（时间校正体系 => 时间校正机制）

×
• 登录
• 注册

×