天天看點

系統解讀CPU 隔離:Full Dynticks 深探

作者 | Frederic Weisbecker

策劃 | 闫園園

SUSE Labs 團隊探索了 Kernel CPU 隔離及其核心元件之一:Full Dynticks(或 Nohz Full),并撰寫了本系列文章:

CPU 隔離 – 簡介

CPU 隔離 – Full Dynticks 深探

CPU 隔離 – Nohz_full

CPU 隔離 – 管理和權衡

CPU 隔離 – 實踐

本文是第二篇。

我們之前介紹了 linux kernel 定時器中斷,以及它在 kernel 内部狀态和服務維護中扮演的角色。抖動敏感型工作負載的使用者可能希望消除這種高速率事件導緻的 CPU 周期盜用和 CPU 緩存清除。

然而,停止定時器中斷并非易事,因為許多 kernel 元件依賴周期性事件,主要是定時器、定時和排程程式。但有一個例外:當 CPU 空閑時,不需要這種 100~1000 Hz 頻率的中斷。事實上,當 CPU 無需工作時,就沒有任務排程器需要維護,沒有定時器排隊,也沒有定時使用者。

是以,“節能”自然成為打破周期性定時器的第一個誘因,因為它是專門針對 CPU 空閑狀态的優化。CONFIG_NO_HZ_IDLE(https://lwn.net/Articles/223185/)核心選項帶來了一種停止周期性中斷的機制,并在 CPU 空閑狀态時實作。這一重大進展為滿足抖動敏感型工作負載的需求鋪平了道路,并提供了一個動态中斷的基礎架構。接下來就是擴充這個功能,以便在 CPU 忙碌的時候,也可以停止時鐘中斷。然而,目前的技術水準并不能達到人們預期的目标,下一節中介紹的每一個問題都花了幾年時間來解決。

時鐘中斷服務的替代方案

如前文所述,定時的一次性事件(計時器回調)或周期性事件(排程程式、計時、RCU 等)的幾個子系統需要時鐘中斷 。是以,如果我們想在 CPU 運作實際任務時停止時鐘中斷,則不能忽略那些請求事件。我們必須使用替代方案為它們提供服務,或者在最壞的情況下限制我們的服務。也就是說,對于這些子系統對周期性時鐘中斷的依賴性,我們必須從以下各種方式中選擇哪些是可能且相關的:

綁定到另一個 CPU

有些工作碰巧在目前 CPU Tick 時執行,但它也可以在另一個 CPU 上執行,而不會出現任何問題。未綁定的計時器就是這樣的情況,即未固定到任何 CPU 的計時器。這也間接适用于未綁定的延遲工作隊列(https://www.kernel.org/doc/html/v4.10/core-api/workqueue.html),因為它們依賴未綁定的計時器。這些計時器很容易綁定到其他地方,但這是以運作這些未綁定工作的 CPU 投入一些額外開銷為代價的。

負載到另一個 CPU

Some tick work related to the current CPU is not initially designed to be executed on another CPU but we can manage to do it, usually at some cost. This is the case for RCU callbacks processing and regular scheduler tick. 有些與目前 CPU 相關的時鐘中斷,其最初設計并非是在另一個 CPU 上執行的,但我們可以設法做到這一點,這通常需要付出一定的成本。RCU 回調處理和正常排程程式就是這種情況。

RCU 回調處理

RCU(https://lwn.net/Articles/262464/)是一種無鎖同步機制,一旦保證所有 CPU 都能看到指定的更新,寫入程式就可以執行回調。這些回調通常在其排隊的 CPU 上執行,即可以來自 softirq 上下文,也可以來自名為“rcuc”的固定核心線程。跟蹤和執行這些回調需要時鐘中斷以輪詢它們的隊列和内部狀态。

為了解決這個問題,一項名為 RCU NOCB 的功能,通過配置核心參數 CONFIG_RCU_NOCB_CPU=y 得以實作。它允許将整個工作從依賴始終中斷轉移到一組名為“rcuog, rcuop or rcuos”的未綁定的 CPU 的核心線程。

這當然會給運作 rcuo[gps] kthreads 和鎖定競争的 CPU 帶來特定的開銷。

排程程式時的時鐘中斷

排程器需要持續收集關于本地和全局任務負載的多項統計資訊,進而使其内部狀态保持最新。在相當長的時間内,忙碌的 CPU 在進入完全 nohz 模式之前可能有殘餘的 1 Hz Tick。最終,這些殘餘的 1 Hz Tick 會轉移到未綁定的工作隊列中。

這也會給運作這些工作隊列的 CPU 帶來更多開銷。

用上下文更改事件替換輪詢事件

計時器中斷從中斷的上下文和頻率推導資訊。這是“CPU 記賬”和“RCU 靜态狀态報告”兩個重要元件的基礎。為了在沒有中斷的情況下處理這些特性,我們需要從上下文變化和時間戳(通常需要一定代價)中推導出這些資訊。這讀起來可能很抽象,是以,最好在實踐中多了解一下。

Cputime 記賬

當在 procfs 檔案系統中檢查指定程序的 stat 檔案時(/proc/$PID/stat : https://man7.org/linux/man-pages/man5/procfs.5.html),可以檢索多個上下文的 cputime 統計資訊,例如線程在使用者空間、核心空間、客戶機等中花費的時間。

這些數字由排程程式 cputime 記賬功能來維護。Tick 會觸發并檢查它中斷了哪個上下文。如果中斷了使用者上下文,則一個 jiffy(兩次 Tick 之間的時間)将計入使用者時間。如果中斷了核心上下文,則 jiffy 将被計入核心時間。這種行為如下圖所示:

系統解讀CPU 隔離:Full Dynticks 深探

圖 3:Dynticks- 空閑 Cputime 記賬

在上例中,我們記錄了 2 次使用者 Tick 和 6 次核心 Tick。對于 1000 Hz 的 Tick,一個 jiffy 等于 1 毫秒。是以,使用者時間記錄為 2ms,核心時間記錄為 6ms。最終結果總是與在每種環境中的實際時間相近,但通常已經足夠好了。Tick 頻率越高,cputime 越精确。

現在檢查一下閑置記賬。這種方式不同,因為空閑時間内沒有 Tick,是以,我們所能做的就是計算退出空閑狀态和進入空閑狀态的時間戳之間的差。

為了能夠在運作非空閑任務并且 Tick 停止時對使用者和核心 CPU 使用時間進行記賬,我們必須将空閑記賬邏輯擴充到使用者 / 核心記賬中。如下所示:

系統解讀CPU 隔離:Full Dynticks 深探

圖 4:Full dynticks Cputime 記賬

在這裡,核心時間可以通過使用者進入空閑狀态的時間戳減去提出空閑狀态的時間戳來檢索。同時,在 idle_enter 之前和 user_exit 之後發生的任何事情都要加在其中。使用者時間是使用者進入和使用者退出空閑狀态之間的差,它的計算非常簡單,甚至比基于 Tick 的記賬更加精确。

但這帶來了一個問題:為什麼不在 Tick 運作時一直使用這種解決方案呢?

因為每次在我們跨越使用者 / 核心邊界時,需要讀取精确但可能提取很慢的硬體時鐘。通用工作負載經常遇到這種情況,進而産生性能損失。是以,這種無 Tick 的記賬必須保留給将其條目在核心的工作負載。

RCU 靜止狀态報告

當 RCU 寫者程式釋出更新并将回調排隊等待執行時,它必須等待所有 CPU 報告新的“RCU 靜止狀态”。這意味着 CPU 已經通過了不屬于受保護 RCU 讀者程式的代碼,稱為“RCU 讀取端臨界區”。

實際上,rcu_read_lock() 和 rcu_read_unlock() 之間的任何代碼(或任何不可搶占的代碼)都是“RCU 讀取端臨界區”;其他剩下的都是“RCU 靜止狀态”。

要追蹤靜止狀态,RCU 依賴 Tick 并檢查它中斷了哪個上下文。如果中斷了不在 rcu_read_lock()/rcu_read_unlock() 對保護部分内的代碼,它報告靜止狀态。如果中斷了使用者空間,它也被認為是靜止狀态,因為使用者空間不能使用核心 RCU 子系統。

系統解讀CPU 隔離:Full Dynticks 深探

圖 5:Dynticks- 空閑 RCU 靜止狀态報告

上圖還說明了 tick-deprived idle 任務如何從特殊處理方式中再次受益。空閑 CPU 不是主動報告靜止狀态,而是通過進入“RCU 擴充靜止狀态”被動報告。它在進入和退出空閑狀态時遞增一個具有完整記憶體屏障的原子變量。

然後,等待所有 CPU 報告靜态狀态的 RCU 最終會掃描未響應的 CPU,以找出擴充的靜态狀态,并代表這些 CPU 報告靜态狀态。

這種模式之是以有效,是因為我們知道空閑上下文不使用 RCU。我們知道使用者空間具有相同的屬性,是以,當運作非空閑任務的時候停止 Tick 時,這種被動報告方案可以擴充到使用者空間中:

系統解讀CPU 隔離:Full Dynticks 深探

圖 6:Full-dynticks RCU 靜止狀态報告

由于 CPU 很少在核心中花費太多時間,是以,上述提議将取代基于 Tick 的靜止狀态報告。RCU 擴充的靜止狀态要麼在其間出現短暫的延遲,要麼就持續很長時間。

與 cputime 記賬類似,這同樣有一個問題:為什麼即使在 Tick 運作時也不采用這種模式?

因為這将在每個使用者 / 核心往返過程中産生一個代價高昂的原子操作,并且會有一個完整的記憶體屏障。此外,報告靜态狀态的責任最終由其他 CPU 承擔。

如果沒有其他選擇,則繼續使用 Tick

如果沒有周期性事件或者頻繁事件,有些情況根本無法解決。例如,排程程式任務搶占就是如此。為了保證本地公平性,排程程式必須能夠在多個任務之間共享 CPU,并定期檢查是否需要搶占。是以,在 CPU 上運作單個任務是在空閑上下文中進一步停止 Tick 的要求。其他子系統也可能會請求定期 Tick,進而在某些情況下保持運作:posix cpu 計時器、perf 事件等。我們将進一步探讨這些細節。

您可以看到,在運作實際任務時,完全停止 Tick 是可能的,但會出現很多陷阱,使用者必須準備做好一些權衡。我們将在下一篇文章中詳細解釋。

繼續閱讀