--- tags: Linux Kernel Internals, 作業系統 --- # Linux 核心設計: CPUIdle(1): 子系統架構 ## Overview 如果系統中的邏輯 CPU (獲取和執行指令的實體,例如 hardware thread 或處理器核心) 在中斷或喚醒事件後處於 idle 狀態時,這意味著沒有任何任務需要 CPU 資源來運行。此時,Linux 系統中的排程器會安排特殊的 idle task 給該 CPU。這個 idle task 可以讓邏輯 CPU 停止從記憶體中獲取指令,並將特定的處理器功能單元置於低功耗狀態。因此,系統整體將可以減少消耗的功率。 極端來看,如果每次由於 idle 就直接將 CPU 關閉,則下次有新的任務要處理時,要復原 CPU 運作將耗費大量時間。因此在實際場景上,可能有多種不同的 idle 狀態。從核心角度需要找到最合適的一個 idle 狀態,那就是核心中 CPU idle time management subsystem 存在的作用,該機制又稱為 CPUIdle。 ![image](https://hackmd.io/_uploads/SyJdfWbk0.png =400x) > [CPU 进入 IDLE 都做了啥?](https://www.eefocus.com/article/511091.html) 子系統的架構可以簡單以上圖概括。主要可分為三個功能單元:負責選擇合適 idle 狀態並要求處理器進入的 governor、將 governor 的決策反映給硬體的 driver,以及為提供通用框架的 core。 ## Logical CPU CPUIdle 子系統對於 CPU 的觀點與 CPU Scheduler 一致,其對 CPU 的視點是從邏輯單元的角度出發,也就是邏輯 CPU(logical CPU)。更具體的說,相異的邏輯 CPU 不必是各自單獨的實體,也可能只是在軟體中顯示為獨立介面的處理器核心。下面我們用三種案例來探討。 其一,如果是單核心處理器,核心一次只能執行一個指令序列/程式。在此情況下,整個處理器可視同是一個邏輯 CPU。此時如果要求硬體進入 idle 狀態,這將應用於整個處理器。 其二,如果處理器是多核心的,而其中的每個核心一次能夠一個指令序列/程式。這些核心不需要完全彼此獨立(例如可以共享 cache),但大多數時候它們在物理上平行的工作。在這種情況下,每個核心都是一個邏輯 CPU。每個核心可以各自被要求進入 idle 狀態,但 idle 狀態的改變也可能適用於更大的單元(如 CPU cluster)。 最後一種情況是多核心處理器,其中的每個核心能夠同時執行多個程式。在這種案例下,每個核心存在多個技術上稱為 hardware thread 的單元(最為著名的案例是 Intel 的 [Hyper-threading 技術](https://en.wikipedia.org/wiki/Hyper-threading)) ,每個 hardware thread 都可以執行一個指令序列/程式。 則從 CPUIdle 的角度來看,每個 hardware thread 都是一個邏輯 CPU,如果對核心中其中一個 hardware thread 要求進入 idle 狀態,其運行將停止。但只有在同一核心內的所有 hardware thread 都被要求進入 idle 後,核心才可以單獨置於 idle 狀態。 ## Idle task [`start_kernel()`](https://elixir.bootlin.com/linux/latest/source/init/main.c#L874) 是啟動 Linux Kernel arch-independent 程式邏輯的進入點,這裡會初始化核心各子系統和基礎建設。 通過路徑 `start_kernel` -> `arch_call_rest_init` -> `rest_init` -> `cpu_startup_entry`,初始化核心的任務最後成為一個 idle task。其行為上是在一個不終止的迴圈中反覆運行 `do_idle`,如下。 ```cpp void cpu_startup_entry(enum cpuhp_state state) { arch_cpu_idle_prepare(); cpuhp_online_idle(state); while (1) do_idle(); } ``` 在 `do_idle` 中會判斷除 idle task 外,是否需要讓出 CPU 資源給其他的 task。若否,`cpuidle_idle_call` 就會運用 CPUIdle framework 來將 CPU 設置到適當的 idle 模式。 ## Governor CPUIdle governor 的作用是提供選擇 idle 狀態的策略。基於模組化的想法,每一種 CPUIdle governor 都可以在 Linux 核心可以運行的任何硬體平台上使用。Governor 的行為不會依賴任何硬體架構或平台設計細節。 描述 CPUIdle governor 的資料結構是 `cpuidle_governor`: ```cpp struct cpuidle_governor { char name[CPUIDLE_NAME_LEN]; struct list_head governor_list; unsigned int rating; int (*enable) (struct cpuidle_driver *drv, struct cpuidle_device *dev); void (*disable) (struct cpuidle_driver *drv, struct cpuidle_device *dev); int (*select) (struct cpuidle_driver *drv, struct cpuidle_device *dev, bool *stop_tick); void (*reflect) (struct cpuidle_device *dev, int index); }; ``` CPUIdle governor 由 `cpuidle_register_governor()` 註冊到 CPUIdle core。如果一切順利,governor 將新增至可用的清單中。如果它是清單中唯一的一個 governor,或其 `rating` 值大於目前使用的 governor 之 `rating`,或新 governor 的名稱在 `cpuidle.governor=` cmdline 的參數值中被傳遞給 kernel,其將在註冊當下被選用。此外,userspace 也可以透過 sysfs 選擇 CPUIdle governor。 ```cpp int (*enable) (struct cpuidle_driver *drv, struct cpuidle_device *dev); ``` `enable` callback 的作用是讓 governor 做好準備處理由 `dev` 表示的(邏輯)CPU 的前置。`drv` 則表示要與對應該 CPU 的 CPUIdle driver,後者應具有一個 `struct cpuidle_state` 的列表,可以代表給定的 CPU 能夠進入的不同 idle 模式。 這個 callback 可能會失敗,在這種情況下會傳回一個負錯誤碼。則 core 將認為該 CPU 為異常,於是捨棄 CPUIdle 而改用特定於 arch 的預設程式碼來處理之。這種狀況將持續直到下次 `->enable()` 再次對該 CPU 呼叫,重新判斷是否應用 CPUIdle framework 於該 CPU。 ```cpp void (*disable) (struct cpuidle_driver *drv, struct cpuidle_device *dev); ``` `disable` 使 governor 停止管理由 `dev` 表示的(邏輯)CPU。作為 `enable` 的對應,預期會撤銷上次為目標 CPU 呼叫 `->enable()` 回呼時所做的任何行為,例如釋放分配的所有記憶體等等。 ```cpp int (*select) (struct cpuidle_driver *drv, struct cpuidle_device *dev, bool *stop_tick); ``` 呼叫 `select` 將為 `dev` 表示的(邏輯)CPU 選擇 idle 模式。 idle 狀態的清單由 `struct cpuidle_state` 的陣列表示,後者可由 drv 存取到(`drv->states`)。此 callback 的回傳值將被解釋為對該陣列的 index,或者是負值的錯誤代碼。 `stop_tick` 用於指示在要求處理器進入選定的空閒狀態之前,是否必要停止 scheduler tick。該變數之值預設為 true,如果 callback 將其清除為 false,處理器將被要求進入選定的 idle 狀態時,不停止給定 CPU 上的 scheduler tick(但如果 tick 在此之前已在該 CPU 上停止,不會重新啟動之)。 該 callback 在任何 governor 上都不可為 NULL。 ```cpp void (*reflect) (struct cpuidle_device *dev, int index); ``` reflect 用以使 governor 評估上次 `->select()` 所選擇的 idle 狀態是否準確,並可能使用該反饋來提高下次對 idle 狀態選擇的準確性。 此外,CPUIdle governor 在選擇 idle 狀態時需要考慮處理器喚醒延遲的限制。CPUIdle governor 會將 CPU 編號傳遞給 `cpuidle_governor_latency_req()` 並得到對應的延遲限制。則任何 idle 狀態的 `exit_latency` 值若大於該值,`->select()` 將不得傳回那些 idle 狀態對應之 index 值。 ## Driver CPUIdle driver 提供與硬體關聯的 CPUIdle 介面。 描述 CPUIdle driver 的資料結構是 `struct cpuidle_driver`。而 driver 首先必須填入包含在該結構中的 `struct cpuidle_state` 陣列。這個陣列表示對應(邏輯)CPU 可以被設置的 idle 狀態的選項。 陣列中會按 `struct cpuidle_state` 下的 `target_residency` 欄位的值升序排序(即 index 0 對應於 `target_residency` 最小的 idle 狀態。由於 `target_residency` 表示 CPU 需保持 idle 多長時間才能進入對應狀態,通常其數值越大,就反映它對應的 idle 狀態更深。 更全面來看,有幾個設定與 `cpuidle_state` 的選擇息息相關: * `target_residency`: 包括進入該狀態的時間,在此 idle 狀態下所需停留的最短時間(以微秒為單位) * 如果進入並停留的時間短於此時間,則用相同時間停留在較淺的 idle 狀態可能消耗更少功耗,因為狀態切換可能需額外的成本 * `exit_latency`: 在此 idle 狀態下被喚醒後到開始執行第一道指令所需的最長時間(以微秒為單位) * `flags`: 描述 idle 狀態的特性。如使用 `CPUIDLE_FLAG_POLLING`,表示對應狀態不代表真實的 idle 狀態。處理器不會進入任何 idle 狀態,而是單純用軟體 loop Linux 核心是以甚麼方法利用 `target_residency` 與 `exit_latency` 來判斷狀態的選擇呢? 預期需要將 CPU 退出 idle 的情形可分為兩類。一類是定時的發生的事件,因為是kernel 自己設置了計時器,因此能確切地知道它們何時觸發,因此就能知道 CPU 可處於 idle 的最長可能時間。 另一種則是可能在任意未知時間發生的事件。對於這種情況,governor 只能知道從過去 CPU 被喚醒後實際上 idle 了多久("idle duration")。則使用該資訊並結合特定演算法,governor 可以基於各自的假設做出預測。這也是在 CPUIdle 子系統中擁有多個 governor 的主要原因。 每個 `cpuidle_state` 還包含了 `enter` callback,它不能是 NULL。用來要求處理器進入這個指定的 idle 狀態。其 prototype 如下: ```cpp void (*enter) (struct cpuidle_device *dev, struct cpuidle_driver *drv, int index); ``` 前兩個參數分別代表執行此 callback 的邏輯 CPU `dev` 和代表對應 driver 的 `drv`。最後一個參數代表 driver 的 `states` 陣列中的 index,表示要進入的 idle 狀態之編號。 另一個類似的 callback `enter_s2idle()` 用來實作與 suspend to idle(S2I) 相關的行為。其與 `enter()` 的區別在於該 callback 有不得在任何時候(即使是暫時的)重新啟用 interrupt,或嘗試更改任何 clock event device 狀態的限制。而 `enter()` 則不受此限。 在 `cpuidle_driver` 建立好 `states` 後,也需同時更新 `state_count` 為對應的正確數量。 此外,如果 `states` 陣列中的存在 entry 是 "coupled" idle state(只有多個相關邏輯 CPU idle 時才能進入的空閒狀態),則 `cpuidle_driver` 中的 `safe_state_index` 欄位需要被設置成 `states` 中任一非 coupled 的 idle 狀態。 如果給定的 CPUIdle driver 僅用來處理系統中邏輯 CPU 的部分子集,則可以設置 `cpuidle_driver` 中的 `cpumask` 來標示之。 CPUIdle driver 必須在註冊後才能使用。註冊的方法又可分為兩種: * 如果 `states` 中都沒有 coupled idle state,則可以呼叫 `cpuidle_register_driver()` 來註冊 * 否則應使用 `cpuidle_register()` 來進行 然而若選擇使用 `cpuidle_register_driver()`,driver 需要額外藉助 `cpuidle_register_device()` 來為 driver 負責的所有邏輯 CPU 註冊 `struct cpuidle_device`。因此通常建議在所有情況下都使用 `cpuidle_register()` 來註冊 CPUIdle driver。 一旦完成了 `struct cpuidle_device` 的註冊,CPUIdle 的 sysfs 介面就被建立,並為其代表的邏輯 CPU 呼叫 governor 的 `->enable()` callback。 當不再需要 CPUIdle driver 時,可以取消註冊之,從而釋放與其關聯的一些資源。順序上要先藉`cpuidle_unregister_device()` 取消註冊目標 CPUIdle driver 處理的 CPU 對應之所有 `struct cpuidle_device` 物件,接著再 `cpuidle_unregister_driver()`。或者也可以選擇直接用 `cpuidle_unregister()` 來完成上述一系列步驟。 CPUIdle driver 可以動態調整處理器 idle states 的清單(例如,當系統電源從 AC 電源切換到電池電源或相反時,可能會發生)。收到變更的要求後,CPUIdle drvier 應呼叫 `cpuidle_pause_and_lock()` 暫時禁用 CPUIdle,然後對所有受該變更影響的 `cpuidle_device` 呼叫 `cpuidle_disable_device()`。改變 idle state 配置後,再為所有相關的 `cpuidle_device` 呼叫 `cpuidle_enable_device()`,並由 `cpuidle_resume_and_unlock()` 再次啟用 CPUIdle。 ## Scheduler Tick 為了讓系統有分時多工的特徵,排程器會需要週期性發生的 timer tick,即 scheduler tick。簡單解釋的話,即每個任務都會獲得一段 time slice 以取得 CPU 資源來運行,當將 time slice 用完時,CPU 應切換到其他 runnable 的任務。藉此方式,從使用者的角度上可以有每個任務皆平行運作的效果,即使任務數量遠超硬體的 CPU 數目。然而,目前正在運行的任務可能不想主動放棄 CPU。因此為使切換發生,需要週期性的定時器中斷來觸發排程器的執行。 然而 scheduler tick 與 CPUIdle 彼此是存在衝突的。因為 tick 會定期且相對頻繁地觸發,那麼如果允許在 idle CPU 上維持 scheduler tick,則就無法要求硬體進入 `target_residency` 時間高於 tick 週期的 idle 狀態了,否則將無法取得節省功耗的目的。 幸好實際上在 idle CPU 上保持 tick 並非必要。根據定義,能進入 idle 的 CPU 理因除了特殊的 idle task 之外,沒有其他可運行的任務。因此上述需要 scheduler tick 在 idle 狀況下不復存在。因此,從理論上可以完全停止在 idle CPU 上的定時器中斷。 實際上在 CPU 進入 idle 時停止 tick 是否有意義,取決於governor 的運作方式。首先,如果在下個 schduler tick 的時間範圍內,已知有另一個定時器的中斷會觸發,那麼停止 scheduler tick 反而是多餘的動作。其次,governor 可能期望在 tick 範圍內進行非定時器的喚醒事件,停止 scheduler tick 同樣不必要(留意到關閉或是重啟 scheduler tick 的成本是可觀的)。這也意味著在較淺的 idle 狀態我們不一定要關閉 scheduler tick,否則則可能反而造成額外的功耗而無法取得任何效益。 這種允許在 idle 狀態下關閉 tick 的系統名為 tickless kernel,一般而言將比起無法停止 tick 的系統更節能。對於 tickless kernel,預設情況下它將使用 menu governor;若否,預設的 CPUIdle governor 是 ladder governor。對於各個 governor 的演算法會在後續章節細說。 ## Reference * [CPU Idle Time Management(driver-api)](https://docs.kernel.org/driver-api/pm/cpuidle.html) * [CPU Idle Time Management(guide)](https://www.kernel.org/doc/html/latest/admin-guide/pm/cpuidle.html) * [Cpuidle from user space](https://www.fsl.cs.sunysb.edu/docs/cpuidle/cpuidle-from-userspace.pdf) * [Linux cpuidle framework(1)_概述和软件架构](http://www.wowotech.net/pm_subsystem/cpuidle_overview.html) * [System Analysis and Tuning Guide - Power management](https://documentation.suse.com/zh-tw/sles/15-SP3/html/SLES-all/cha-tuning-power.html)