erlang:now()的特點:
特點 | 說明 |
單調向前 | erlang:now() 獲取的時間是單調向前,就算系統時間倒退了,也不會影響這個函數的使用。(時間依舊是向前的,較之前幾乎沒有偏差) |
唯一性 | erlang:now() 獲取的值都是唯一的,不會重複出現2個相同的值。 |
間隔修正 | 兩次 erlang:now() 調用的間隔都可以被利用來修正erlang時間。 |
erlang 時間校正
時間校正的作用:
erlang時間校正的特點:
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(),我也準備了一點資料,放在拓展閱讀分享吧。拓展閱讀
gethrtime()
#define sys_gethrtime() gethrtime()
關於 gethrtime() 可以看下unix官方文檔說明man page for gethrtime ,寫得很詳細。 也就是這兩個特點: