特点
|
说明
|
单调向前
|
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 ,写得很详细。