很多人会注意到这个问题,erlang提供了2个时间函数,erlang:now() 和 os:timestamp()。用法一样,都是返回当前的时间。具体时间是从1970年1月1日零时算起,到现在经过的时间,结果为{MegaSecs, Secs, MicroSecs}。
这两个函数有什么区别?
os:timestamp() 获取到的时间为操作系统的时间,不做任何修正;而erlang:now(),每次获取都会确保生成了唯一的时间,就是说,erlang:now()在实现上对时间做了一个校正,每次都生成一个单调向前的唯一值。
erlang:now()的特点:
主要是这3个特点:
特点 |
说明 |
单调向前 |
erlang:now() 获取的时间是单调向前,就算系统时间倒退了,也不会影响这个函数的使用。(时间依旧是向前的,较之前几乎没有偏差) |
唯一性 |
erlang:now() 获取的值都是唯一的,不会重复出现2个相同的值。 |
间隔修正 |
两次 erlang:now() 调用的间隔都可以被利用来修正erlang时间。 |
到这里,可以看出 erlang 内部实现了一套时间校正的机制,当系统时间出错的时候,就会做修正。(关于这块内容,可以看Erlang相关文档 time correction)
erlang 时间校正
时间校正的作用:
在开始这段内容前,讲讲时间校正的作用
1. 时间单调向前:
举个例子,说明时间倒退问题:
比如,游戏中会统计今天和昨天杀怪的总数量,跨零点时要把今天杀怪字段的数量写到昨天的字段,然后将今天的置0。跨零点后,如果时间倒退了几秒钟,然后就会重复跨零点。那么,今天的数量会覆盖昨天的数量,导致昨天的数量被清零。
2. 时间平稳:
同样举个例子,说明时间不平稳问题:
比如,erlang开发中,经常都会出现一个进程call另一个进程的场景,一般是5秒超时,假如时间突然加快了5秒,就相当于没有等待操作完成,就直接超时了。当然这是很不合理的
erlang时间校正的特点:
假如操作系统时间出现了改变,erlang不会立刻改变内部时间为系统时间,而是将时间轻微加快或减慢,最终和系统时间保持一致。就算系统时间突然倒退到以前的某个时间,但时间总是向前这点是不会改变的,所以,erlang只是预期在将来某个时间和系统时间达成一致,而不会倒退时间。
erlang是怎么校正时间的?
erlang内部时间会和系统挂钟时间保持同步,当系统挂钟时间突然改变时,erlang会比较两个时间的差异,让内部的时间的同步值轻微变大或变小,幅度最大是1%,就是说,当系统时间改变了1分钟,erlang会花100分钟来慢慢校正,并最终和系统时间保持同步。
哪些函数受到时间校正影响?
源码剖析
erlang:now() 是 bif 实现,代码如下:(以R16B02为例)
/* * bif.c now_0函数,实现 erlang:now/0 * return a timestamp */ BIF_RETTYPE now_0(BIF_ALIST_0) { Uint megasec, sec, microsec; Eterm* hp; get_now(&megasec, &sec, μsec); // 获取当前时间 hp = HAlloc(BIF_P, 4); BIF_RET(TUPLE3(hp, make_small(megasec), make_small(sec), make_small(microsec))); // 返回{MegaSecs, Secs, MicroSecs} }
再来看下 get_now() 函数。
/* * erl_time_sup.c get_now函数,获取当前时间 * get a timestamp */ void get_now(Uint* megasec, Uint* sec, Uint* microsec) { SysTimeval now; erts_smp_mtx_lock(&erts_timeofday_mtx); get_tolerant_timeofday(&now); // 获取当前时间值 do_erts_deliver_time(&now); // 记录当前的时间(用于VM内部读取当前时间,如timer) /* 确保时间比上次获取的大 */ if (then.tv_sec > now.tv_sec || (then.tv_sec == now.tv_sec && then.tv_usec >= now.tv_usec)) { now = then; now.tv_usec++; } /* Check for carry from above + general reasonability */ if (now.tv_usec >= 1000000) { now.tv_usec = 0; now.tv_sec++; } then = now; erts_smp_mtx_unlock(&erts_timeofday_mtx); *megasec = (Uint) (now.tv_sec / 1000000); *sec = (Uint) (now.tv_sec % 1000000); *microsec = (Uint) (now.tv_usec); update_approx_time(&now);//更新「简要」时间(仅用于标记进程启动时间) }
这里重点看下get_tolerant_timeofday(),实现了时间校正功能。
/* * erl_time_sup.c get_tolerant_timeofday函数,获取当前时间 * 根据系统API不同有两种实现,这里取其中一种做说明 */ static void get_tolerant_timeofday(SysTimeval *tv) { SysHrTime diff_time, curr; if (erts_disable_tolerant_timeofday) {// 时间校正功能被禁用,直接返回系统时间 sys_gettimeofday(tv); return; } *tv = inittv; // 取VM启动时间 // 计算从VM启动到现在经过的内部时间(正值,单位微秒) diff_time = ((curr = sys_gethrtime()) + hr_correction - hr_init_time) / 1000; if (curr < hr_init_time) { erl_exit(1,"Unexpected behaviour from operating system high " "resolution timer"); } // 检查是否刚校正过(两次校正最小间隔 1s) if ((curr - hr_last_correction_check) / 1000 > 1000000) { /* Check the correction need */ SysHrTime tv_diff, diffdiff; SysTimeval tmp; int done = 0; // 计算从VM启动到现在经过的实际时间(如果系统时间被调整过,可能是负值,单位微秒) sys_gettimeofday(&tmp); tv_diff = ((SysHrTime) tmp.tv_sec) * 1000000 + tmp.tv_usec; tv_diff -= ((SysHrTime) inittv.tv_sec) * 1000000 + inittv.tv_usec; diffdiff = diff_time - tv_diff;// 实际时间与内部时间的差值(缩短这个时间差以赶上实际时间) if (diffdiff > 10000) { // 内部时间比外部时间快 0.01s 以上 SysHrTime corr = (curr - hr_last_time) / 100; // 两次调用经过的实际时间 * 1% if (corr / 1000 >= diffdiff) { ++done; hr_correction -= ((SysHrTime)diffdiff) * 1000; /* 超过diffdiff*1000 * 100,只修正 diffdiff*1000, * 就是1s需要花100s修正,同时标记本次修正完成 * 什么情况下会走到这里:就是这个函数很久没调用,超过了时间偏差的100倍 * 然后标记修正完成,至此,就没有时间偏差了 */ } else { hr_correction -= corr; // 修正值为两次调用经过的实际时间 * 1% } // 重算与VM启动时间的间隔 diff_time = (curr + hr_correction - hr_init_time) / 1000; } else if (diffdiff < -10000) { // 内部时间比外部时间慢 0.01s 以上 SysHrTime corr = (curr - hr_last_time) / 100; if (corr / 1000 >= -diffdiff) { ++done; hr_correction -= ((SysHrTime)diffdiff) * 1000; } else { hr_correction += corr; } diff_time = (curr + hr_correction - hr_init_time) / 1000; } else { /* 内部时间与外部时间偏差在0.01s 内,标记完成,等1s后修正剩下的时间 * 这段代码目的是,如果时间偏差在0.01s内,VM特意等1s后修正这个时间 * 另外,如果时间没出差错,就都走到这里,减少时间函数调用开销 */ ++done; } if (done) { hr_last_correction_check = curr; } } tv->tv_sec += (int) (diff_time / ((SysHrTime) 1000000)); tv->tv_usec += (int) (diff_time % ((SysHrTime) 1000000)); if (tv->tv_usec >= 1000000) { tv->tv_usec -= 1000000; tv->tv_sec += 1; } hr_last_time = curr; }
这里,erlang利用一个单调递增的时间函数 sys_gethrtime(),作为参照物来判断VM实际经历的真实时间,然后再轻微的向系统挂钟时间倾斜,以致最终和系统挂钟时间保持同步。至于sys_gethrtime(),我也准备了一点资料,放在拓展阅读分享吧。
拓展阅读
gethrtime()
前面提到的sys_gethrtime(),实际上是一个宏(暂时只讨论linux下的实现,win下类似)
#define sys_gethrtime() gethrtime()
关于 gethrtime() 可以看下unix官方文档说明man page for gethrtime ,写得很详细。
另外,这里参考了 C ++参考指南 C++ Reference Guide - High Resolution Timers
也就是这两个特点:
1. 单调向前,不会倒退
2. 线性增长,不会变快或变慢
所以,Erlang VM利用基于硬件的单调递增时间,取两个时刻的差值来计算VM运行的时间,然后取操作系统的挂钟时间做比较,通过逐渐缩小这个时间差来实现VM时间始终单调向前,从而平稳地把VM时间纠正到用户的挂钟时间。这里的平稳指的是VM的时间校正机制会将时间"轻微"加快或减慢,但最大幅度是1%,也就是说,VM经历 1s 实际上可能就是 0.99s 或者1.01s
时间同步
这里看到《并行与分布仿真系统》的作者写的相关文章Synchronizing Clocks?(Wallclock Time),也很有参考价值
结束语
最后,说下时间校正的副作用。
erlang实现时间校正有计算开销的,而且这个内部校正值是全局变量,不只是所有erlang进程,还是VM所有调度线程都会读写这个时间,所以就要有锁来保证数据。为此,erlang内部设定好了 erlang:now/0 调用频率不会超过1微妙1次。
当然,如果获取时间只是用于测试目的,或者打印错误日志时间,完全可以用 os:timestamp/0 来代替。对于一些有大规模进程的项目,还可以设立一些时间管理进程,用于同步时间,而每个进程只要读取自己的进程字典就好。
如果不想使用,还可以禁用这个功能。
Time correction is enabled or disabled by passing the +c [true|false] command line argument to erl.
R18之后,erlang提供了更多时间校正相关的API,对用户暴露底层时间的校正值,和实际的时间差。这里暂时就不说明了。链接地址
参考:http://blog.csdn.net/mycwq/article/details/45346411