Linux內(nèi)核中的形形色色的“鐘表”,你了解多少?
如果Linux也是一個普通人的話,那么她的手腕上應該有十幾塊手表,包括:CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_PROCESS_CPUTIME_ID、CLOCK_THREAD_CPUTIME_ID、CLOCK_MONOTONIC_RAW、CLOCK_REALTIME_COARSE、CLOCK_MONOTONIC_COARSE、CLOCK_BOOTTIME、CLOCK_REALTIME_ALARM、CLOCK_BOOTTIME_ALARM、CLOCK_TAI。本文主要就是介紹Linux內(nèi)核中的形形色色的“鐘表”。
二、理解Linux中各種clock分類的基礎
既然本文講Linux中的計時工具,那么我們首先面對的就是“什么是時間?”,這個問題實在是太難回答了,因此我們這里就不正面回答了,我們只是從幾個側(cè)面來窺探時間的特性,而時間的本質(zhì)就留給物理學家和哲學家思考吧。
1、如何度量時間
時間往往是和變化相關,因此人們往往喜歡使用有固定周期變化規(guī)律的運動行為來定義時間,于是人們把地球圍自轉(zhuǎn)一周的時間分成24份,每一份定義為一個小時,而一個小時被平均分成3600份,每一份就是1秒。然而,地球的運動周期不是那么穩(wěn)定,怎么辦?多測量幾個,平均一下嘛。
雖然通過天體的運動定義了秒這樣的基本的時間度量單位,但是,要想精確的表示時間,我們依賴一種有穩(wěn)定的周期變化的現(xiàn)象。上一節(jié)我們說過了:地球圍繞太陽運轉(zhuǎn)不是一個穩(wěn)定的周期現(xiàn)象,因此每次觀察到的周期不是固定的(當然都大約是24小時的樣子),用它來定義秒多少顯得不是那么精準??茖W家們發(fā)現(xiàn)銫133原子在能量躍遷時候輻射的電磁波的振蕩頻率非常的穩(wěn)定(不要問我這是什么原理,我也不知道),因此被用來定義時間的基本單位:秒(或者稱之為原子秒)。
2、Epoch
定義了時間單位,等于時間軸上有了刻度,雖然這條代表時間的直線我們不知道從何開始,最終去向何方,我們終歸是可以把一個時間點映射到這條直線上了。甚至如果定義了原點,那么我們可以用一個數(shù)字(到原點的距離)來表示時間。
如果說定義時間的度量單位是技術活,那么定義時間軸的原點則完全是一個習慣問題。拿出你的手表,上面可以讀出2017年5月10,23時17分28秒07毫秒……作為一個地球人,你選擇了耶穌誕辰日做原點,講真,這弱爆了。作為linuxer,你應該擁有這樣的一塊手表,從這個手表上只能看到一個從當前時間點到linux epoch的秒數(shù)和毫秒數(shù)。Linux epoch定義為1970-01-01 00:00:00 +0000 (UTC),后面的這個UTC非常非常重要,我們后面會描述。
除了wall time,linux系統(tǒng)中也需要了解系統(tǒng)自啟動以來過去了多少的時間,這時候,我們可以把鐘表的epoch調(diào)整成系統(tǒng)的啟動時間點,這時候獲取系統(tǒng)啟動時間就很容易了,直接看這塊鐘表的讀數(shù)即可。
3、時間調(diào)整
記得小的時候,每隔一段時間,老爸的手表總會慢上一分鐘左右的時間,也是他總是在7點鐘,新聞聯(lián)播之前等待那校時的最后一響。一聽到“剛才最后一響是北京時間7點整”中那最后“滴”的一聲,老爸也把自己的手表調(diào)整成為7點整。對于linux系統(tǒng),這個操作類似clock_set接口函數(shù)。
類似老爸機械表的時間調(diào)整,linux的時間也需要調(diào)整,機械表的發(fā)條和齒輪結(jié)構沒有那么精準,計算機的晶振亦然。前面講了,UTC的計時是基于原子鐘的,但是來到Linux內(nèi)核這個場景,我們難道要為我們的計算機安裝一個原子鐘來計時嗎?當然可以,如果你足夠有錢的話。我們一般人的計算機還是基于系統(tǒng)中的本地振蕩器來計時的,雖然精度不理想,但是短時間內(nèi)你也不會有太多的感覺。當然,人們往往是向往更精確的計時(有些場合也需要),因此就有了時間同步的概念(例如NTP(Network Time Protocol))。
所謂時間同步其實就是用一個精準的時間來調(diào)整本地的時間,具體的調(diào)整方式有兩種,一種就是直接設定當前時間值,另外一種是采用了潤物細無聲的形式,對本地振蕩器的輸出進行矯正。第一種方法會導致時間軸上的時間會向前或者向后的跳躍,無法保證時間的連續(xù)性和單調(diào)性。第二種方法是對時間軸緩慢的調(diào)整(而不是直接設定),從而保證了連續(xù)性和單調(diào)性。
4、閏秒(leap second)
通過原子秒延展出來的時間軸就是TAI(International Atomic Time)clock。這塊“表”不管日出、日落,機械的按照ce原子定義的那個秒在推進時間。冷冰冰的TAI clock雖然精準,但是對人類而言是不友好的,畢竟人還是生活在這顆藍色星球上。而那些基于地球自轉(zhuǎn),公轉(zhuǎn)周期的時間(例如GMT)雖然符合人類習慣,但是又不夠精確。在這樣的背景下,UTC(Coordinated Universal Time)被提出來了,它是TAI clock的基因(使用原子秒),但是又會適當?shù)恼{(diào)整(leap second),滿足人類生產(chǎn)和生活的需要。
OK,至此,我們了解了TAI和UTC兩塊表的情況,這兩塊表的發(fā)條是一樣的,按照同樣的時間滴答(tick,精準的根據(jù)原子頻率定義的那個秒)來推動鐘表的秒針的轉(zhuǎn)動,唯一不同的是,UTC clock有一個調(diào)節(jié)器,在適當?shù)臅r間,可以把秒針向前或者向后調(diào)整一秒。
TAI clock和UTC clock在1972年進行了對準(相差10秒),此后就各自獨立運行了。在大部分的時間里,UTC clock跟隨TAI clock,除了在適當?shù)臅r間點,realtime clock會進行l(wèi)eap second的補償。從1972年到2017年,已經(jīng)有了27次leap second,因此TAI clock的讀數(shù)已經(jīng)比realtime clock(UTC時間)快了37秒。換句話說,TAI和UTC兩塊表其實可以抽象成一個時間軸,只不過它們之間有一個固定的偏移。在1972年,它們之間的offset是10秒,經(jīng)過多年的運轉(zhuǎn),到了2017年,offset累計到37秒,讓我靜靜等待下一個leap second到了的時刻吧。
5、計時范圍
有一類特殊的clock稱作秒表,啟動后開始計時,中間可以暫停,可以恢復。我們可以通過這樣的秒表來記錄一個人睡眠的時間,當進入睡眠狀態(tài)的時候,按下start按鍵開始計時,一旦醒來則按下stop,暫停計時。linux中也有這樣的計時工具,用來計算一個進程或者線程的執(zhí)行時間。
6、時間精度
時間是連續(xù)的嗎?你眼中的世界是連續(xù)的嗎?看到窗外清風吹拂的樹葉的時候,你感覺每一個樹葉的形態(tài)都被你捕捉到了。然而,未必,你看急速前進的汽車的輪胎的時候,感覺車輪是倒轉(zhuǎn)的。為什么?其實這僅僅是因為我們的眼睛大約是每秒15~20幀的速度在采樣這個世界,你看到的世界是離散的。算了,扯遠了,我們姑且認為時間的連續(xù)的,但是Linux中的時間記錄卻不是連續(xù)的,我們可以用下面的圖片表示:
系統(tǒng)在每個tick到來的時候都會更新系統(tǒng)時間(到linux epoch的秒以及納秒值記錄),當然,也有其他場景進行系統(tǒng)時間的更新,這里就不贅述了。因此,對于linux的時間而言,它是一些離散值,是一些時間采樣點的值而已。當用戶請求時間服務的時候,例如獲取當前時間(上圖中的紅線),那么最近的那個Tick對應的時間采樣點值再加上一個當前時間點到上一個tick的delta值就精準的定位了當前時間。不過,有些場合下,時間精度沒有那么重要,直接獲取上一個tick的時間值也基本是OK的,不需要校準那個delta也能滿足需求。而且粗粒度的clock會帶來performance的優(yōu)勢。
7、睡覺的時候時間會停止運作嗎?
在現(xiàn)實世界提出這個問題會稍顯可笑,魯迅同學有一句名言:時間永是流逝,街市依舊太平。但是對于Linux系統(tǒng)中的clock,這個就有現(xiàn)實的意義了。比如說clock的一個重要的派生功能是創(chuàng)建timer(也就是說timer總是基于一個特定的clock運作)。在一個5秒的timer超期之前,系統(tǒng)先進入了suspend或者關機狀態(tài),這時候,5秒時間到達的時候,一般的timer都不會觸發(fā),因為底層的clock可能是基于一個free running counter的,在suspend或者關機狀態(tài)的時候,這個HW counter都不再運作了,你如何期盼它能喚醒系統(tǒng),來執(zhí)行timer expired handler?但是用戶還是有這方面的實際需求的,最簡單的就是關機鬧鈴。怎么辦?這就需要一個特別的clock,能夠在suspend或者關機的時候,仍然可以運作,推動timer到期觸發(fā)。
三、Linux下的各種clock總結(jié)
在linux系統(tǒng)中定義了如下的clock id:
#define CLOCK_REALTIME 0
#define CLOCK_MONOTONIC 1
#define CLOCK_PROCESS_CPUTIME_ID 2
#define CLOCK_THREAD_CPUTIME_ID 3
#define CLOCK_MONOTONIC_RAW 4
#define CLOCK_REALTIME_COARSE 5
#define CLOCK_MONOTONIC_COARSE 6
#define CLOCK_BOOTTIME 7
#define CLOCK_REALTIME_ALARM 8
#define CLOCK_BOOTTIME_ALARM 9
#define CLOCK_SGI_CYCLE 10 /* Hardware specific */
#define CLOCK_TAI 11
CLOCK_PROCESS_CPUTIME_ID和CLOCK_THREAD_CPUTIME_ID這兩個clock是專門用來計算進程或者線程的執(zhí)行時間的(用于性能剖析),一旦進程(線程)被切換出去,那么該進程(線程)的clock就會停下來。因此,這兩種的clock都是per-process或者per-thread的,而其他的clock都是系統(tǒng)級別的。
leap second? | clock set? | clock tunning? | original point | resolution | active in suspend? | |
realtime | yes | yes | yes | Linux epoch | ns | no |
monotonic | yes | no | yes | Linux epoch | ns | no |
monotonic raw | yes | no | no | Linux epoch | ns | no |
realtime coarse | yes | yes | yes | Linux epoch | tick | no |
monotonic coarse | yes | no | yes | Linux epoch | tick | no |
boot time | yes | no | yes | machine start | ns | no |
realtime alarm | yes | yes | yes | Linux epoch | ns | yes |
boottime alarm | yes | no | yes | machine start | ns | yes |
tai | no | no | no | Linux epoch | ns | no |