從erlang時間函數說到時間校正機制

很多人會注意到這個問題,erlang提供了2個時間函數,erlang:now() 和 os:timestamp()。用法一樣,都是返回當前的時間。具體時間是從1970年1月1日零時算起,到現在經過的時間,結果爲{MegaSecs, Secs, MicroSecs}。

這兩個函數有什麼區別?

os:timestamp() 獲取到的時間爲操作系統的時間,不做任何修正;而erlang:now(),每次獲取都會確保生成了唯一的時間,就是說,erlang:now()在實現上對時間做了一個校正,每次都生成一個單調向前的唯一值。

erlang:now()的特點:

Monotonicerlang:now() never jumps backwards - it always moves forwardInterval correctThe 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 correctnessThe 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 correspondenceThe 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)UniqueNo two calls to erlang:now on one Erlang node should return the same value

主要是這3個特點:
特點
說明
單調向前
erlang:now() 獲取的時間是單調向前,就算系統時間倒退了,也不會影響這個函數的使用。(時間依舊是向前的,較之前幾乎沒有偏差)
唯一性
erlang:now() 獲取的值都是唯一的,不會重複出現2個相同的值。
間隔修正
兩次 erlang:now() 調用的間隔都可以被利用來修正erlang時間。
到這裏,可以看出 erlang 內部實現了一套時間校正的機制,當系統時間出錯的時候,就會做修正。(關於這塊內容,可以看Erlang相關文檔 time correction

erlang 時間校正

時間校正的作用:

在開始這段內容前,講講時間校正的作用
1. 時間單調向前:
  舉個例子,說明時間倒退問題:
  比如,遊戲中會統計今天和昨天殺怪的總數量,跨零點時要把今天殺怪字段的數量寫到昨天的字段,然後將今天的置0。跨零點後,如果時間倒退了幾秒鐘,然後就會重複跨零點。那麼,今天的數量會覆蓋昨天的數量,導致昨天的數量被清零。

2. 時間平穩:
  同樣舉個例子,說明時間不平穩問題:
  比如,erlang開發中,經常都會出現一個進程call另一個進程的場景,一般是5秒超時,假如時間突然加快了5秒,就相當於沒有等待操作完成,就直接超時了。當然這是很不合理的

erlang時間校正的特點:

MonotonicThe clock should not move backwardsIntervals should be near the truthWe 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 clockWe 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是怎麼校正時間的?

erlang內部時間會和系統掛鐘時間保持同步,當系統掛鐘時間突然改變時,erlang會比較兩個時間的差異,讓內部的時間的同步值輕微變大或變小,幅度最大是1%,就是說,VM經歷 1s 實際上可能就是 0.99s 或者1.01s。當系統時間改變了1分鐘,erlang會花100分鐘來慢慢校正,並最終和系統時間保持同步。

哪些函數受到時間校正影響?

erlang:now/0The 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.receive ... afterTimeouts on receive uses time correction to determine a stable timeout interval.The timer moduleAs 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/3The 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() ,以上幾個功能都有賴於時間校正的實現。比如 erlang:send_after/3 , 就算系統時間改變了,這個函數發出的消息也會按預定時間期限送達。

源碼剖析

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, &microsec); // 獲取當前時間
    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 ,寫得很詳細。 
gethrtime() 作用是用於實時獲取當前時間,精度非常高,以納秒爲單位,但是,兩次足夠緊連的調用有可能返回相同的結果,畢竟精度單位是納秒,可以保證的是時間不會倒退。
另外,這裏參考了 C ++參考指南 C++ Reference Guide - High Resolution Timers 

也就是這兩個特點:
1. 單調向前,不會倒退;
2. 線性增長,不會變快或變慢。每個時間刻度經歷的時間都是一樣的。

所以,Erlang VM利用基於硬件的單調遞增時間,取兩個時刻的差值來計算VM運行的時間,然後取操作系統的掛鐘時間做比較,通過逐漸縮小這個時間差來實現VM時間始終單調向前,從而平穩地把VM時間糾正到用戶的掛鐘時間。

時間同步

這裏看到《並行與分佈仿真系統》的作者寫的相關文章Synchronizing Clocks
(Wallclock Time),也很有參考價值

假設時間提前了10毫秒,就每隔30毫秒產生一次中斷,每次中斷的增量時間爲29毫秒,花10次完成修正。

結束語

最後,說下時間校正的副作用。
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,對用戶暴露底層時間的相關信息。這裏暫時就不說明了。鏈接地址

2015/5/5  修正標題(時間校正體系 => 時間校正機制)
參考:http://blog.csdn.net/mycwq/article/details/45346411
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章