特点
|
说明
|
单调向前
|
erlang:now() 获取的时间是单调向前,就算系统时间倒退了,也不会影响这个函数的使用。(时间依旧是向前的,较之前几乎没有偏差)
|
唯一性
|
erlang:now() 获取的值都是唯一的,不会重复出现2个相同的值。
|
间隔修正
|
两次 erlang:now() 调用的间隔都可以被利用来修正erlang时间。
|
/*
* 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(),我也准备了一点资料,放在拓展阅读分享吧。
#define sys_gethrtime() gethrtime()
关于 gethrtime() 可以看下unix官方文档说明 man page for gethrtime,写得很详细。