時鐘源、時鐘事件、sched_clock() 和延遲定時器

本文件試圖簡要解釋一些基本的核心計時抽象概念。它部分與通常在核心樹的 drivers/clocksource 中找到的驅動程式有關,但程式碼可能分佈在整個核心中。

如果您在核心原始碼中進行 grep 搜尋,您會發現許多架構特定的時鐘源、時鐘事件的實現,以及幾個同樣架構特定的 sched_clock() 函式和一些延遲定時器的覆蓋。

為了為您的平臺提供計時,時鐘源提供基本的時間線,而時鐘事件在此時間線的某些點上觸發中斷,提供諸如高解析度定時器之類的設施。 sched_clock() 用於排程和時間戳,延遲定時器使用硬體計數器提供準確的延遲源。

時鐘源

時鐘源的目的是為系統提供時間線,告訴您現在的時間。例如,在 Linux 系統上發出命令“date”最終將讀取時鐘源以確定準確的時間。

通常,時鐘源是單調的原子計數器,它將提供 n 位,這些位從 0 計數到 (2^n)-1,然後迴繞到 0 並重新開始。理想情況下,只要系統正在執行,它就永遠不會停止滴答。它可能會在系統掛起期間停止。

時鐘源應具有儘可能高的解析度,並且與真實世界的掛鐘相比,頻率應儘可能穩定和正確。它不應在時間上不可預測地來回移動,也不應遺漏一些週期。

它必須能夠防止硬體中發生的各種影響,例如,在總線上分兩個階段讀取計數器暫存器,首先是最低 16 位,然後在第二個匯流排週期中讀取高 16 位,計數器位可能會在此期間更新,從而導致來自計數器的非常奇怪的值的風險。

當時鍾源的掛鐘精度不能令人滿意時,計時程式碼中有各種怪癖和層,例如,將使用者可見的時間同步到系統中的 RTC 時鐘或使用 NTP 與聯網的時間伺服器同步,但它們基本上所做的只是更新時鐘源的偏移量,時鐘源為系統提供基本的時間線。這些措施本身不會影響時鐘源,它們只會使系統適應它的缺點。

時鐘源結構應提供將提供的計數器轉換為納秒值的方法,該納秒值是無符號長整型(unsigned 64 位)數字。由於此操作可能會非常頻繁地呼叫,因此嚴格地以數學方式執行此操作是不可取的:相反,僅使用算術運算乘法和移位,將該數字儘可能接近納秒值,因此在 clocksource_cyc2ns() 中您會發現

ns ~= (clocksource * mult) >> shift

您會發現時鐘原始碼中有許多輔助函式,旨在幫助提供這些 mult 和 shift 值,例如 clocksource_khz2mult()、clocksource_hz2mult(),它們有助於從固定移位確定 mult 因子,以及 clocksource_register_hz() 和 clocksource_register_khz(),它們將幫助使用時鐘源的頻率作為唯一的輸入來分配 shift 和 mult 因子。

對於從單個 I/O 記憶體位置訪問的真正簡單的時鐘源,現在甚至有 clocksource_mmio_init(),它將採用記憶體位置、位寬、一個引數,指示暫存器中的計數器是向上計數還是向下計數,以及定時器時鐘速率,然後生成所有必要的引數。

由於 100 MHz 的 32 位計數器在大約 43 秒後將回繞到零,因此處理時鐘源的程式碼必須對此進行補償。這就是為什麼時鐘源結構還包含一個“mask”成員,用於指示源的多少位有效。這樣,計時程式碼就知道計數器何時迴繞,並且可以在迴繞點的兩側插入必要的補償程式碼,以便系統時間線保持單調。

時鐘事件

時鐘事件在概念上是時鐘源的逆向:它們採用所需的時間規格值,並計算出要插入硬體定時器暫存器的值。

時鐘事件與時鐘源正交。同一硬體和暫存器範圍可以用於時鐘事件,但它本質上是不同的東西。驅動時鐘事件的硬體必須能夠觸發中斷,以便在系統時間線上觸發事件。在 SMP 系統上,理想情況下(並且通常)每個 CPU 核心都有一個這樣的事件驅動定時器,以便每個核心可以獨立於任何其他核心觸發事件。

您會注意到,時鐘事件裝置程式碼基於相同的基本思想,即使用乘法和移位演算法將計數器轉換為納秒,並且您會再次找到相同的輔助函式系列來分配這些值。但是,時鐘事件驅動程式不需要“mask”屬性:系統不會嘗試計劃超出時鐘事件時間範圍的事件。

sched_clock()

除了時鐘源和時鐘事件之外,核心中還有一個特殊的弱函式叫做 sched_clock()。此函式應返回自系統啟動以來的納秒數。架構可以自行提供或不提供 sched_clock() 的實現。如果沒有提供本地實現,系統 jiffy 計數器將用作 sched_clock()。

顧名思義,sched_clock() 用於排程系統,例如,確定 CFS 排程程式中某個程序的絕對時間片。當您選擇在 printk 中包含時間資訊時,它也用於 printk 時間戳,例如用於啟動圖。

與時鐘源相比,sched_clock() 必須非常快:它被呼叫的頻率更高,尤其是被排程程式呼叫。如果您必須在準確性與時鐘源之間進行權衡,則可以在 sched_clock() 中犧牲準確性以換取速度。但是,它需要與時鐘源相同的一些基本特徵,即它應該是單調的。

sched_clock() 函式只能在無符號長整型邊界上回繞,即在 64 位之後。由於這是一個納秒值,這意味著它將在大約 585 年後迴繞。(對於大多數實際系統來說,這意味著“永遠不會”。)

如果架構不提供此函式的自己的實現,它將回退到使用 jiffies,使其最大解析度為架構 jiffy 頻率的 1/HZ。這將影響排程準確性,並且可能會在系統基準測試中顯示出來。

驅動 sched_clock() 的時鐘可能會在系統掛起/睡眠期間停止或重置為零。這對它在系統上排程事件的作用無關緊要。但是,它可能會導致 printk() 中出現有趣的時間戳。

sched_clock() 函式應該可以在任何上下文中呼叫,並且是 IRQ 和 NMI 安全的,並且在任何上下文中都返回一個合理的數值。

某些架構可能只有有限的時間源,並且缺少一個很好的計數器來匯出 64 位納秒值,因此例如在 ARM 架構上,已經建立了特殊的輔助函式來從 16 位或 32 位計數器提供 sched_clock() 納秒基數。有時,也用作時鐘源的同一個計數器用於此目的。

在 SMP 系統上,至關重要的是 sched_clock() 可以在每個 CPU 上獨立呼叫,而不會有任何同步效能損失。某些硬體(例如 x86 TSC)將導致系統上 CPU 之間的 sched_clock() 函式漂移。核心可以透過啟用 CONFIG_HAVE_UNSTABLE_SCHED_CLOCK 選項來解決此問題。這是使 sched_clock() 與普通時鐘源不同的另一個方面。

延遲定時器(僅限某些架構)

在 CPU 頻率可變的系統上,各種核心 delay() 函式有時會表現得很奇怪。基本上,這些延遲通常使用一個硬迴圈來延遲一定數量的 jiffy 分數,使用一個在啟動時校準的“lpj”(每個 jiffy 的迴圈數)值。

讓我們希望您的系統在校準此值時以最大頻率執行:當頻率降低到完整頻率的一半時,任何 delay() 都將是兩倍的時間。通常這沒有壞處,因為您通常要求該數量的延遲或更多。但基本上,這種語義在這樣的系統上是相當不可預測的。

輸入基於定時器的延遲。使用這些延遲,可以使用定時器讀取來代替硬編碼迴圈來提供所需的延遲。

這是透過宣告一個 struct delay_timer 併為此延遲定時器分配適當的函式指標和速率設定來完成的。

這在某些架構上可用,如 OpenRISC 或 ARM。