深入理解 Linux 時間子系統

作者簡介:

程磊,一線碼農,在某手機公司擔任系統開發工程師,日常喜歡研究內核基本原理。

一、時間概念解析

我們住在空間裏,活在時間中。時間對我們來說是既熟悉又陌生。熟悉是因爲我們每天都在時間的驅動下忙碌着,陌生是因爲我們從來沒有停下來認真思考過時間是什麼。今天我們先從對時間的使用需求開始說起。

1.1 時間使用的需求

我們對使用時間有三種需求:知時、定時和計時。知時就是我們需要知道現在的時間是多少,表達方式是時分秒、年月日。定時是我們需要在某個時間點被告知,時間點可以是相對的或者絕對的,告知可以是一次性的或者是週期性的,比如每天早上 7:30 叫我起牀,是絕對時間點週期性告知,每隔 10 分鐘向我彙報一次情況,是相對時間點週期性告知。計時是我們需要知道某件事從開始到結束一共花了多少時間,比如大學運動會 1000 米賽跑,裁判在運動員起跑時按一下計時器,結束時再按一下計時器,得出某運動員跑一千米用了 3 分 50 秒。

1.2 時間體系的要素

爲了達到知時的目的,我們首先需要建立時間體系的概念。時間體系由三個要素構成,1 時間原點、2 時間基本單位、3 時間是否會暫停。我們把每天用的這個時間叫做自然時間,自然時間在計算機裏面也叫做真實時間 (Real Time),注意 Real Time 在這裏是真實時間的意思,而不是實時的意思。自然時間有時候也會被叫做牆鍾時間 (wall clock time),或者簡略爲牆上時間 (wall time),小時候家裏牆上用掛鐘來看時間的同學立馬就能明白了。對自然時間建立的時間體系並不是唯一的,可以有不同的時間原點和時間基本單位。我們現在使用的公元紀年,它的時間原點是耶穌出生的那一年的一月一號零時零分零秒。其實我們也可以使用黃帝紀年,那現在就是 5000 多年了,也可以把建國的時間當做時間原點,那現在就是 70 幾年。公元紀年的時間基本單位是秒,好在全球的秒都是一樣的,沒有出現什麼中秒、美秒、歐秒的區分,不然換算來換算去就會很麻煩。自然時間不會暫停,計算機裏面的有些時間體系可能會暫停,這個我們後面再講。我們再來總結一下,現在全世界使用的自然時間體系是公元紀年,其時間原點是耶穌誕生當年的一月一號零時零分零秒,其時間基本單位是秒,時間流逝不會暫停。這就特別好,大家都是在同一個時間體系下生活,這樣討論時間就很方便,不用來回轉換了。如果不同國家使用的時間體系都不相同,時間體系的原點不同,時間基本單位也不相同,那相互之間來回轉換時間就會非常麻煩。

1.3 時間的表示維度

接下來我們說一下時間的表示維度,注意是時間的表示維度,不是時間的維度,時間本身的維度是一維的。如果我告訴你說現在的時間是六百三十七億六千五百七十九萬多秒,你是不是會一臉懵逼,反應不過來。雖然時間的基本單位是秒,但是我們如果直接用秒來表示時間,那將非常難以理解和記憶。爲此我們建立了多層級的時間表示維度,60 秒是一分鐘,60 分鐘是一個小時,24 小時是一天,365 天是一年。然後我們說今天是某年某月某日,具體時間是幾時幾分幾秒,就非常方便了,很便於我們人類使用理解。對於人類來說時間精確到秒就足夠使用了,但是對於科學研究來說還需要更高的精度,於是我們把 1 秒的 1/1000 叫做毫秒,1 毫秒的 1/1000 叫做微秒,1 微秒的 1/1000 叫做納秒。這樣時間的表示維度就很豐富了,便於我們在不同的情況下使用。那麼計算機中的時間表示維度是多少呢?人類善於理解多維度的時間表示,但是計算機卻善於處理單維度的時間表示。但是計算機用單維度的時間表示卻有個問題,如果用秒作爲基本單位,那麼精度顯然達不到,如果用納秒作爲基本單位的話,數值又太大。所以計算機中的時間採用的是兩層表示維度,超過 1 秒的時間用秒錶示,不夠一秒的時間用納秒錶示,每 10 億納秒向前進位一秒。這樣計算機中時間處理就非常方便了。

1.4 時鐘與走時

想要實現知時的目的我們就需要有工具,這個工具就叫做時鐘 (clock),有了時鐘我們就能夠快速準確地知道自然時間。下面我們來給時鐘下一個定義。時鐘,包括硬件的、軟件的、機械的、電子的,都是用來追蹤和記錄自然時間流逝的工具。下面我們再來說一個動詞,走時,大家一聽這個詞可能會不知道是啥意思。我再來說一句話,這個表走時非常精準,大家立馬就明白了是啥意思。我們再給走時下個定義,走時,是時鐘追蹤和記錄時間流逝的動作。爲什麼在這裏要說個走時的概念呢,因爲有了走時的概念,後面的很多東西都能很輕鬆地講清楚。

1.5 時間需求之間的關係

我們再來看一下知時、計時、定時三者之間的關係。先說知時和計時,其實兩者之間是可以相互轉化的。知時可以轉化爲計時,我們在事情開始的時候記錄一下時間,在事情結束的時候記錄一下時間,兩者之間的時間差值就是計時。計時也可以轉化爲知時,把計時的起點設置爲某一個時間體系的時間原點,那麼計時的結果就是知時的結果。計時是時間原點不特定的知時,知時是時間原點特定的計時。知時的結果是一個時間點,它是當前時間點到時間原點的一個時間段。計時的結果是時間段,它是相對於計時原點的時間點。明白了知時和計時之間的關係對於我們理解後面計算機的具體做法有很大的幫助。

下面我們再來看一下定時和知時、計時之間的關係。由於知時、計時可以相互轉換,所以它們可以放在一起討論同定時的關係。定時是需要知時、計時的支持的,如果沒有知時、計時,那麼就沒法定時。絕對定時用知時作爲基礎時間比較方便,相對定時用計時作爲基礎時間比較方便。當然反過來也是可以的,因爲知時計時是可以相互轉化的。還有一點就是定時可以用來作爲時鐘實現走時的方法,這個在計算機時間管理的實現中就有所體現。

二、時間子系統的硬件基礎

在生活中我們有各種各樣的時鐘來滿足我們對時間的需求。比如以前家裏常用的座鐘、掛鐘,個人也會戴個機械手錶或者電子手錶,這些時鐘既能知時也能定時 (有鬧鐘功能),知時本身也能轉化爲計時。所以一個時鐘就能滿足我們對時間的所有需求。在有些場合比如大學運動會時,會有專門的計時器,在比賽開始之前把計時器清零,比賽開始的時候按下開始,計時器開始走時,然後每當有一個人達到終點的時候按一下計時,計時器就會把當時的時間記下來,當所有人都跑完的時候按下結束,計時器停止走時。然後回看計時器就可以看到每個人跑完一千米的用時了。這種專用的計時器用來計時就非常方便。

現在家裏有座鐘、掛鐘的人已經非常少了,戴手錶的人也非常少了,大家基本都是用手機來看時間。手機不僅桌面上有時間顯示,裏面還有個時鐘 App,它和以前的時鐘功能差不多,而且更強大。時鐘 App 裏面不僅能看時間 (知時),還能定鬧鐘 (絕對時間定時),裏面還有一個計時器功能,實際上是倒計時,倒計時的本質是相對時間定時。裏面還有一個秒錶的功能,和我們前面說的運動會計時器的功能是一樣的,所以秒錶是個專業的計時器。所以手機上的時鐘 App 完美得實現了我們對時間的所有需求。

手機實際上就是個計算機系統,而且安卓手機用的還是 Linux 內核。時鐘 App 所實現的功能需要 Linux 內核的支持,內核時間子系統的實現需要有硬件的支持。

2.1 時鐘硬件類型

計算機裏面一共有三類時鐘硬件,分別是真時鐘 RTC(Real Time Clock)、定時器 Timer、計時器 Counter。RTC 相當於是手錶、座鐘,定時器相當於是鬧鐘,計時器相當於是運動會中的計時器。注意是三類時鐘硬件,而不是三個,某一類時鐘可能有多個不同的硬件,某一個時鐘硬件也可能實現多種不同的時鐘類型。

計算機中還有其它的時鐘類型,比如晶振時鐘,是驅動 CPU 運行的週期信號,用來觸發和同步 CPU 內部的操作,我們常說某 CPU 是多少 GHz,就是說這個時鐘晶振每秒向 CPU 發送多少信號 (大概如此,實際上比較複雜,還有倍頻什麼的,這裏就不討論了)。晶振時鐘一般在 CPU 內部,有些嵌入式 CPU 的晶振在外部。時鐘晶振在軟件層不可見。還有一些設備也有自己的時鐘,還有相應的驅動可以控制它。由於這些時鐘都和時間子系統沒有關係,所以本文中就不討論它們了。

不同平臺的時鐘硬件各有不同,下面我們就來分別說說。

2.2 x86 平臺上的時鐘

真時鐘 RTC,在 x86 上的硬件實現也叫做 RTC,和 CMOS(計算機中有很多叫做 CMOS 的東西,但是是不同的概念,此處的 CMOS 是指 BIOS 設置保存數據的地方) 是放在一起的。由於在關機後都需要供電,所以兩者放在了一起,由一個紐扣電池供電。所以有時候也會被人叫做 CMOS 時鐘。

定時器 Timer,在 UP 時代是 PIT(Programmable Interval Timer),它以固定時間間隔向 CPU 發送中斷信號。PIT 可以在系統啓動時設置每秒產生多少個定時器中斷,一般設置是 100,250,300,1000,這個值叫做 HZ。到了 SMP 時代,PIT 就不適用了,此時有多種不同的定時器。有一個叫做 Local APIC Timer 的定時器,它是和中斷系統相關的。中斷系統有一個全局的 IO APIC,有 NR_CPU 個 Local APIC,一個 Local APIC 對應一個 CPU。所以在每個 Local APIC 都安裝一個定時器,專門給自己對應的 CPU 發送定時器中斷,就很方便。還有一個定時器叫做 HPET(High Precision Event Timer),它是 Intel 和微軟共同研發的。它不僅是個定時器,而且還有計時器的功能。HPET 不和特定的 CPU 綁定,所以它可以給任意一個 CPU 發中斷,這點和 Local APIC Timer 不同。

計時器 Counter,RTC 或者定時器雖然也可以實現計時器的目的,但是由於精度太差,所以系統都有專門的計時器硬件。計時器一般都是一個整數寄存器,以特定的時間間隔增長,比如說 1 納秒增加 1,這樣兩次讀它的值就可以算出其中的時間差,而且精度很高。x86 上最常用的計時器叫做 TSC(Time Stamp Counter),是個 64 位整數寄存器。還有一個計時器叫做 ACPI PMT(ACPI Power Management Timer),但是它是一個設備寄存器,需要通過 IO 端口來讀取。而 TSC 是 CPU 寄存器,可以直接讀取,讀取速度就非常快。

2.3 ARM 平臺上的時鐘

暫略

三. 時間子系統的軟件架構

當我們知道了我們明白什麼、我們有什麼、我們想要什麼的時候,我們就會知道我們應該怎麼做。

從第一章我們明白了時間的基本概念,從第二章我們知道了我們有 RTC、計時器、定時器三類底層硬件,從第三章和第四章我們知道了我們需要什麼,那麼我們就會很容易的分析出我們應該怎麼做。

3.1 系統時鐘的設計

在用戶空間和內核空間都有知時的需求,而底層又有 RTC 硬件,這樣看來知時的需求很好實現啊,直接訪問 RTC 硬件就可以了。這麼做行嗎?我們來分析一下。首先 RTC 是個外設,訪問 RTC 要走 IO 端口,而這相對來說是個很慢的操作。其次 RTC 的精度不夠,有的 RTC 精度是秒,有的是毫秒,這顯然是不夠用的。最後系統要實現很多時間體系,直接訪問 RTC 靈活性也不夠。所以直接訪問 RTC 是一個很差的設計,那麼該怎麼實現知時的需求呢?

我們先來回憶一下時鐘和走時的定義。

我們用機械手錶來解釋一個這個概念。手錶裏面有發條,發條的變化是在追蹤時間的流逝,然後發條通過齒輪把時間的變化記錄在錶盤的時針、分針、秒針上,這樣我們就可以看到現在的時間是多少了。

我們再來回憶一下知時和計時之間的關係。知時是原點特定的計時,計時是原點不特定的知時,知時和計時可以相互轉化。知時相減就是計時,給計時一個特定的原點就是知時。計算機上既有 RTC 也有計時器,RTC 雖然又慢精度又低,但是計時器又快精度又高啊。計時器的精度可以達到 1 納秒或者幾納秒,而且計時器大部分都是通過寄存器訪問的,速度非常快的。給計時器的起點一個確定的時間點,它就是 RTC 了啊。於是乎方案就出來了:Linux 提出了系統時鐘的概念,它是一個軟件時鐘,相應的把 RTC 叫做硬件時鐘。系統時鐘是用一個變量 xtime 記錄現在的時間點,xtime 的初始值用 RTC 來初始化,這樣就只用訪問 RTC 一次就可以了,然後 xtime 的值隨着計時器的增長而增長。xtime 的值的更新有兩種情況,一種是調度器 tick 的時候從計時器更新一下,一種是讀 xtime 的時候從計時器更新一下。對於這個時鐘,計時器就相當於是發條,調度器 tick 就相當於是齒輪,xtime 就相當於是時針、分針、秒針,一個軟件時鐘就這麼設計好了。

Linux 中用來實現系統時鐘的軟件體系叫做 The Linux Timekeeping Architecture。如果我們把 Timekeeping 翻譯成 “時間維護”,感覺意思好像不到位。好在我們前面講了“走時” 的概念,把 Timekeeping 翻譯成 “走時” 的話,一下子就覺得意思到了。後面我們就用 “Linux 走時框架” 這個詞了。在 Linux 走時框架中有三個基本概念:1. 走時器(struct timekeeper),用來記錄一些基本數據,包括系統時鐘的當前時間值和其它全局時間體系的一些數據;2. 時鐘源(struct clocksouce),是對計時器硬件的一種抽象;3. 時鐘事件設備(struct clock_event_device),是對定時器硬件的一種抽象。這三個對象相互配合共同構成了系統時鐘。

系統可能會有很多計時器硬件和定時器硬件。在系統啓動時每個硬件都會初始化並註冊自己。註冊完之後系統會選擇一個最佳的時鐘源作爲走時器的時鐘源,選擇一個最佳的時鐘事件設備作爲更新系統時鐘的設備。系統啓動時會去讀取 RTC 的值來初始化系統時鐘的值,然後時鐘事件設備不斷產生週期性的定時器事件,在定時器事件處理函數中會讀取時鐘源的值,再減去上一次讀到的值,得到時間差,這個時間差就是系統時鐘應該前進的時間值,把這個值更新到走時器中,並相應更新其它時間體系的值。系統時鐘就是按照這種方式不斷地在走時。系統時鐘除了在啓動時和休眠喚醒時會去讀取 RTC 的值,其它時間都不會和 RTC 交換,兩者各自獨立地走時,互不影響。

用戶空間 API 讀取和設置的時間是系統時鐘,和硬件時鐘 RTC 沒有關係。如果要讀寫 RTC 的話,需要用 ioctl RTC_SET_TIME 對 / dev/rtc 進行操作。stime、settimeofday 設置的系統時鐘,不會更改到 RTC 上,系統重啓後更改就消失了。通過 / dev/rtc 修改的硬件時間也不會更改到系統時間上,只有系統重啓後纔會反映到系統時鐘上。對此有一個系統命令 hwclock,它不僅可以修改 RTC,也可以在兩者之間進行同步。hwclock --hctosys 把硬件時鐘同步到系統時鐘,hwclock --systohc 把系統時鐘同步到硬件時鐘。事實上我們發現用 settimeofday 修改的系統時鐘在系統重啓後生效了,並沒有丟失,這是爲什麼呢?是因爲系統默認的關機腳本里面會執行 hwclock --systohc,把系統時鐘同步到硬件時鐘,所以我們修改的系統時鐘纔不會丟失。

3.2 系統時鐘的實現

暫略

推薦閱讀:

http://www.wowotech.net/timer_subsystem/time-subsyste-architecture.html

http://www.wowotech.net/timer_subsystem/timekeeping.html

http://www.wowotech.net/timer_subsystem/clocksource.html

http://www.wowotech.net/timer_subsystem/clock-event.html

3.3 動態 tick 與定時器

低精度定時器是內核在早期就有的定時器接口,它的實現是靠調度器 tick 來驅動的。高精度定時器是隨着硬件和軟件的發展而產生的。調度器 tick 的 HZ(每秒 tick 多少次) 是可以配置,它的配置選項有 4 個,100,、250、300、1000,也即是說每次 tick 的間隔是 10ms、4ms、3.3ms、1ms。所以用調度器 tick 來驅動低精度定時器是很合適的,tick 的精度能滿足低精度定時器的精度。但是用調度器 tick 來驅動高精度定時器就不合適了,因爲這樣高精度定時器的精度最多是 1ms,達不到納秒的級別,這樣就算不上是高精度定時器了。所以對於高精度定時器來說,情況就正好反了過來,高精度定時器直接用硬件實現,然後創建一個軟件高精度定時器來模擬調度器 tick。也就是說,對於只有低精度定時器的系統來說,是調度器 tick 驅動低精度定時器;對於有高精度定時器的系統來說,是高精度定時器驅動調度器 tick,這個調度器 tick 再去驅動低精度定時器。

內核的低精度定時器接口和高精度定時器接口都是一次性的,不是週期性的。通過一次性的定時器可以實現週期性的定時器,方法是在每次定時器到期時再設置下一次的定時器,一直這樣就形成了週期性的。這裏說的是定時器接口的一次性和週期性,而不是定時器硬件。下面我們再來看看定時器硬件是一次性的還是週期性的。定時器硬件本身可以是一次性的也可以是週期性的,也可以兩種模式都存在,由內核選擇使用哪一種。對於低精度定時器來說,它的定時器硬件可以是一次性的也可以是週期性的,由於調度器 tick 是週期性的,所以它的底層硬件就是週期性的。低精度定時器的精度最多是 1ms,也就是定時器中斷做多一秒有 1000 次,這對於系統來說是可以承受的。但是對於高精度定時器來說,理論上它的定時器硬件也可以是週期性的。但是如果它的定時器硬件是週期性的,由於它的精度最多可以達到 1 納秒,也就是說 1 納秒要發生一次定時器中斷,每秒發生 10 億次。這對於系統來說是不可承受的,而且並不是每納秒都有定時器事件要處理,所以大部分定時器中斷是沒有用的。如果我們把 1 納秒 1 次中斷改爲 1 微妙,1 微妙 1 次中斷不就可以大大減少中斷的數量嘛,但是這樣定時器的精度就是 1 微妙,達不到 1 納秒的要求了。所以對於高精度定時器,底層的定時器硬件就只能是一次性的了。每次定時器事件到來的時候再去查看一下下一個最近的定時器事件什麼時候到期,然後再去設置一下定時器硬件。這樣高精度定時器就可以一直運行下去了。但是我們的調度器 tick 也需要定時器中斷,而且是週期性的,怎麼辦?好辦,創建一個到期時間爲 1ms 的高精度定時器,每次到期的時候再設置一下繼續觸發,這樣就形成了一個 1000HZ 週期性的定時器事件,就可以驅動調度器 tick。

下面我們講一下定時器和調度器 tick 的初始化過程,以 x86 爲例。系統啓動時會先初始化 timekeeping。然後 hpet 註冊自己,hpet 既有定時器也有計時器,hpet 定時器會成爲系統定時器,hpet 計時器會成爲 timekeeper 的時鐘源。後面 tsc 計時器也會註冊自己,併成爲最終的時鐘源。Local APIC Timer 定時器也會註冊自己,併成爲最終的 per CPU tick device。hpet 最終只能做 broadcast 定時器了。系統在每次 run local timer 的時候都會檢測一下,如果不支持高精度定時器,就嘗試切換到動態 tick 模式,如果支持高精度定時器就切換到高精度定時器模式,此模式下會嘗試切換到動態 tick 模式。當高精度定時器和動態 tick 設置成功之後,Local APIC Timer 會運行在一次性模式,調度器 tick 是由一個叫做 sched_timer 的高精度定時器驅動的。每次定時器到期時都會 reprogram next event。

3.4 用戶空間 API 的實現

用戶空間 API 的實現文件如下表所示,具體實現細節就不再展開解釋了,大家搜索 SYSCALL_DEFINE 可以快速找到函數實現的地方。

四. 總結回顧

通過前面的介紹,我們瞭解了時間的基本概念,知道了計算機中實現時間子系統的基礎硬件,學會了時間的用戶空間 API 和內核接口,明白了時間子系統的設計原理。下面我們畫個圖總結一下:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RaWdSnpgWr0-G6fqXP36Fg