# Linux 核心設計: CPUFreq(1): 子系統架構 ## Overview 在考慮電源管理的場景下,CPU 做為系統最主要的工作單元,需具有完善的機制以在功耗和效能間取得平衡。舉例來說,在 [Linux 核心設計: CPUIdle](https://hackmd.io/@RinHizakura/SyPwWWZkC) 系列文章談到: CPU 有在空閒(Idle)時切換至更為省電模式之能力。 除此之外,大多數現代 CPU 還能夠在多種不同的時脈頻率和電壓配置下運行,這些設定通常稱為 Operating Performance Points 或 P state(ACPI 術語)。一般而言,頻率越高、電壓越高,CPU 在單位時間內可以執行的指令就越多,但同時功耗就越大。在 CPU 的效率和功耗之間找到權衡是必須的。 在某些情況下,可能需要盡可能讓應用程式快速的運行,此時必須使用最高的頻率。但也有其他一些情況不需要快速執行,此時,保持在最高頻率則可能導致電源浪費。此外,由於散熱等原因,也有可能在物理上保持 CPU 高頻運行是不可行。為此,現代硬體設計上通常允許 CPU 在不同的頻率/電壓設定之間切換,或者使其進入不同的 P state(以 ACPI 術語來說)。 在 Linux 系統上,這是由 CPUFreq framework 管理。搭配特定的演算法,系統可估算所需的 CPU capacity,進而決定將 CPU 置於哪些 P state/時脈頻率。由於系統的使用率通常會隨時間變化,因此必須週期性的估計並調頻。這個機制通常稱為 CPU performance scaling 或者 CPU DVFS(Dynamic Voltage/Frequency Scaling)。 ![image](https://hackmd.io/_uploads/H1nCuwEYyx.png) > [Evaluation framework for energy-aware multiprocessor scheduling in real-Time systems](https://cs.gmu.edu/~aydin/jsa19.pdf) CPUFreq framework 可由上圖大致概括,對上,**scaling Governor** 負責根據給定的策略(policy),結合對系統負載等追蹤結果決定 CPU 應調整至的頻率,將結果轉達至 CPUFreq 子系統核心(**CPUFreq core**)。對下,子系統將 Governor 決定的 CPU 頻率透過 notifier 通知給對應 **scaling driver**,後者再設定硬體以滿足對應的頻率請求。而 CPUFreq Framework 也以 sysfs 的形式,向 userspace 提供 CPU 頻率的查詢和控制等介面。 原則上,所有 Governor 都可以與任意 CPUFreq 搭配使用。這個設計是基於以下觀察:多數情況下,調頻演算法用來選擇 P state 的資訊可以以與獨立於平台的抽象形式表示。 然而,有些硬體本身可以自行蒐集資訊,並自行決定效能優化的演算法,此時上述觀察就不成立。因為這些資訊通常特定於其來源的硬體介面,難以以特定的 abstract 描述。因此,CPUFreq 實際上允許 scaling driver 繞過 Governor 並實現自己的調頻演算法。例如 [intel_pstate](https://www.kernel.org/doc/html/latest/admin-guide/pm/intel_pstate.html) 驅動程式為此特例。 ## 相關術語與資料結構 ### `struct cpufreq_policy` `struct cpufreq_policy` 是描述 CPUFreq 設定的關鍵資料結構。平台設計上,CPU 常有群組(cluster)的概念。此時多個 CPU 會共用硬體對低功耗狀態的控制。例如,同一個暫存器(或暫存器組)用於同時控制多個 CPU 的 P-state,寫入暫存器會同時影響同群組的 CPU。 對於這些共享狀態的 CPU 集合,CPUFreq 框架藉由 `struct cpufreq_policy` 來表示之。為了保持一致性,即使集合中只有一個 CPU 時,也會獨自使用 `struct cpufreq_policy` 結構。 更詳細的說,CPUFreq 為系統中的每個 CPU(包括目前 offline 的 CPU)維護一個指向 `struct cpufreq_policy` 的指標。共用相同 CPU 狀態控制介面的 CPU 群,它們對應的指標都將指向同一個 `struct cpufreq_policy`。 ### CPUFreq Notifier Notifier 是 Linux 核心中一種對於事件的通知機制。簡單來說,就是子系統可以建立對應事件的 notifier,而其他子系統如果對該事件感興趣,則可以對其註冊。則當該事件在系統中發生時,曾經註冊的子系統就可以得到通知,以進行對應的行為。以 CPUFreq 為例,可以透過 [`cpufreq_register_notifier`](https://github.com/torvalds/linux/blob/master/drivers/cpufreq/cpufreq.c#L2077) 來註冊 notifier 並以 [`cpufreq_unregister_notifier`](https://github.com/torvalds/linux/blob/master/drivers/cpufreq/cpufreq.c#L2121) 來取消。在 [`include/linux/notifier.h`](https://github.com/torvalds/linux/blob/master/include/linux/notifier.h) 可以找到詳細的介面與說明。 Notifier 又可以分為 policy notifiers 和 transition notifiers 兩種。 #### CPUFreq policy notifiers 當新的 policy 被建立或移除,註冊 [`CPUFREQ_POLICY_NOTIFIER`](https://github.com/torvalds/linux/blob/master/include/linux/cpufreq.h#L510) 的驅動會獲得通知。而 notifier 可以再根據對應 callback 的第二個參數是 `CPUFREQ_CREATE_POLICY` 或者 `CPUFREQ_REMOVE_POLICY` 辨別是兩個事件中的何者。第三個參數是一個 [`struct cpufreq_policy`](https://github.com/torvalds/linux/blob/master/include/linux/cpufreq.h#L55),後者包含新策略的最低頻率 `min`、最高頻率 `max`(以 kHz 為單位)以及需滿足此策略的 CPU 集合(set)等資訊。 #### CPUFreq transition notifiers 如果註冊了 [`CPUFREQ_TRANSITION_NOTIFIER`](https://github.com/torvalds/linux/blob/master/include/linux/cpufreq.h#L509C8-L509C36),當 CPUfreq framework 要切換 CPU 的頻率時會收到通知。頻率的切換會分成兩次通知,根據第二個參數分別是`CPUFREQ_PRECHANGE` 或 `CPUFREQ_POSTCHANGE`。第三個參數是一個 [`struct cpufreq_freqs`](https://github.com/torvalds/linux/blob/master/include/linux/cpufreq.h#L184C1-L189C3): ```cpp struct cpufreq_freqs { struct cpufreq_policy *policy; unsigned int old; unsigned int new; u8 flags; /* flags of cpufreq_driver, see below. */ }; ``` policy 指向一個 `struct cpufreq_policy`,而 `old` 和 `new` 分別就是切換前與切換後的頻率數值。`flags` 則是標註特殊的頻率切換狀況。 ### CPU Frequency Table #### `cpufreq_frequency_table` 由於大多數 CPU 只允許設定在幾個特定的離散頻率,因此就需要一個包含一些可選頻率的「頻率表」幫助 CPUFreq 完成工作。這個頻率表具體是一個 [`struct cpufreq_frequency_table`](https://elixir.bootlin.com/linux/v6.13.1/source/include/linux/cpufreq.h#L693) 結構組成的陣列。 ```cpp struct cpufreq_frequency_table { unsigned int flags; unsigned int driver_data; /* driver specific data, not used by core */ unsigned int frequency; /* kHz - doesn't need to be in ascending order */ }; ``` * `flags`: 特殊設定的標示 * `driver_data`: CPUFreq driver 需要用到的特定資料 * `frequency`: 該 entry 對應的頻率(kHz) 陣列的末尾需要是一個 `frequency` 設為 `CPUFREQ_TABLE_END` 的 `cpufreq_frequency_table` entry。 如果想跳過頻率表中的某個項目,可以將頻率設為 `CPUFREQ_ENTRY_INVALID`。 陣列的排列不需按特定順序,但如果有序的話可能可以幫助 CPUFreq core 更快地匹配,進而更有效率的執行 DVFS。 #### CPU Frequency Table Helpers 核心中提供一些 helper functions,可以用來便於操作頻率表。 * `cpufreq_frequency_table_verify` 確保表中至少有一個有效頻率在 `policy->min` 和 `policy->max` 範圍內等其他條件。 * `cpufreq_frequency_table_target` 可以通過給定的 index 查詢頻率表,並回傳對應頻率的 entry。 * `cpufreq_for_each_entry` 可以用來 iterate 整個頻率表 * `cpufreq_for_each_valid_entry` 類似 `cpufreq_for_each_entry`,但跳過 frequency 是 `CPUFREQ_ENTRY_INVALID` 的 entry * 如果需要以 index 去 iterate 而非 entry 本身,可以改用 `cpufreq_for_each_entry_idx()` / `cpufreq_for_each_valid_entry_idx()` #### OPP Library 對於 CPU 的可選頻率,可以通過 [Operating Performance Points (OPP) Library](https://docs.kernel.org/power/opp.html) 來建表。[`dev_pm_opp_init_cpufreq_table`](https://elixir.bootlin.com/linux/v6.13.1/source/drivers/opp/cpu.c#L43) 提供了介面可將 OPP 層有關可用頻率的內部資訊轉換為 CPUFreq 中的格式(`struct cpufreq_frequency_table`)。而 [`dev_pm_opp_free_cpufreq_table`](https://elixir.bootlin.com/linux/v6.13.1/source/drivers/opp/cpu.c#L96) 則可以釋放該頻率表。 ## CPUFreq Initialization CPUFreq 的運作仰賴 scaling driver。由於一次只能註冊一個 scaling driver,因此 driver 需有能力處理系統中的所有 CPU。 Scaling driver 的註冊可以在 CPU 註冊之前或之後。如果 CPU 是事先註冊的,則 driver 會在自己註冊期間透過 CPUFreq core 介面來加入所有已註冊的 CPU;如果在 Scaling driver 註冊之後才註冊了 CPU,則 CPUFreq core 會在這些 CPU 註冊時將他們加入到 driver 的管理中。總而言之,一旦發現尚未登記的 CPU,CPUFreq core 就會啟動來記錄之。 ### 詳細流程 每一個新 CPU 引發 CPUFreq core 的啟動時,就會檢查給定 CPU 是否已對應到任一 `cpufreq_policy`。如果已對應,跳過建立 `cpufreq_policy` 的階段;否則的話,建立一個新的 `cpufreq_policy`、初始化之。後者也會造成在 sysfs 中建立一個新的 `policy` 目錄,並將給定 CPU 對應的 `cpufreq_policy` 設定為之。 關於 `cpufreq_policy` 初始化的細節,此時 scaling driver 的 `->init()` 會被呼叫,並將要被初始化的 `cpufreq_policy` 作為參數傳遞。在 `->init()` 中,需要初始化新 CPU 調頻相關的硬體,也會設定相關參數(例如支援的最小/最大頻率、frequency table 等)。這裡也需設定 policy 之 cpumask,將與此新 CPU 共用硬體調頻的所有 CPU(包括 online/offline CPU)之對應 bits 設置起來。如此一來,CPUFreq core 將能使用這個 mask 來將其他所有 CPU 也對應到此 `cpufreq_policy`。 下一個主要初始化步驟是為 `cpufreq_policy` 附加一個 Governor。初始時,預設的 Governor 可能由 kernel 的 bootargs 或 config 決定,但之後可以透過 sysfs 變更。然後就可以透過 governor 的 `->start()` 來啟動該 governor。 `->start()` 會向 CPU 排程器註冊所有屬給定 policy 的 online CPU 之 CPU utilization 更新 callback,由此一來,排程器就可以在可能影響 CPU utilization 的重要事件發生時(例如 task 的 enquee/dequeue、每個 scheduler ticks)呼叫此 callback,重新估計當前的 CPU utilization 以選定適合的 P-state/CPU 頻率,並藉由 scaling driver 對硬體做相應變更。根據設定,Scaling driver 可以直接在 scheduler context 運行,也可以非同步(例如 kernel thread 或 workqueue)式的運作。 在 [Overview](#Overview) 一節提到了像 `intel_pstate` 繞過了 CPUFreq Governor 由硬體自身選擇 P-state 的特殊情況。在此狀況下,governor 將不會被附加到 scaling driver 上。取而代之,scaling driver 的 `->setpolicy()` 會被使用來為每個 policy 註冊每個 CPU utilization 更新的 callback,這些 callback 在排程器中被以與有附加 governor 的相同的方式使用,但在 intel_pstate 下同時會確定要使用的 P-state,並從 scheduler context 中一次性更改相應的硬體設定。 其他更細節的流程可參考 [CPU Initialization](https://www.kernel.org/doc/html/latest/admin-guide/pm/cpufreq.html#cpu-initialization) 一節。 :::info 上述談及的 CPU 都是指 logical CPU ::: ## sysfs 介面 在 kernel 初始化時,CPUFreq core 會在 `/sys/devices/system/cpu/` 下建立一個 `cpufreq` 的 sysfs 目錄。在此目錄下包含數個名為 `policyX` 的子目錄(X 是 policy 的整數編號),對應於 CPUFreq 核心維護的每個 `cpufreq_policy`。 對於每個 CPU `/sys/devices/system/cpu/cpuY/`(Y 是 CPU 的編號),都會連結到一個 `policyX`,表示 CPU 與該給定 policy 關聯。 而在每個 `policyX` 下包含其特定的屬性,可以用來控制 CPUFreq 對於該 policy 即與其關聯的所有 CPU 的行為。有些屬性是通用的,不特定於目前使用的 scaling driver 以及 governor,但 scaling driver 也可以會在 sysfs 中制定特定屬性,以控制 policy 相關的行為。 以下對通用屬性進行說明: ## Generic Scaling Governors ## 如何實作 CPUFreq Driver? > [How to Implement a new CPUFreq Processor Driver](https://docs.kernel.org/cpu-freq/cpu-drivers.html) 如前文所述,在 CPUFreq framework 中最重要的兩個部分是 Governor 和 Driver。對於 Governor,對應的是與硬體無關的調頻策略,因此通常是直接使用在 Linux 中提供的 Governor(Ondemand、Performance、Powersave 等)。而 Driver 部分涉及實際的調頻,因此各平台廠家就必須針對各自的datasheet 設計 Driver。假設今天我們打造了新的 CPU/晶片組,應該怎麼進行 CPUFreq Driver 的實作呢? ### `struct cpufreq_driver` 對於 CPUFreq Driver,我們需要定義特定的 [`struct cpufreq_driver`](https://elixir.bootlin.com/linux/v6.13.1/source/include/linux/cpufreq.h#L336),並藉由 `cpufreq_register_driver()` 將其註冊到核心中。 ```cpp struct cpufreq_driver { char name[CPUFREQ_NAME_LEN]; u16 flags; void *driver_data; /* needed by all drivers */ int (*init)(struct cpufreq_policy *policy); int (*verify)(struct cpufreq_policy_data *policy); ``` * `name`: driver 的命名 * `flags`: 可選,用來為 CPUFreq core 提供 driver 的相關特性 * `driver_data`: 可選,儲存 driver 特定資料的指標 * `init`: 每一個 policy 被初始化時的實作 * `verify`: 嘗試調整 policy 時(例如 policy, governor, min, max),藉此實作以驗證合法性 ```cpp /* define one out of two */ int (*setpolicy)(struct cpufreq_policy *policy); int (*target)(struct cpufreq_policy *policy, unsigned int target_freq, unsigned int relation); /* Deprecated */ int (*target_index)(struct cpufreq_policy *policy, unsigned int index); unsigned int (*fast_switch)(struct cpufreq_policy *policy, unsigned int target_freq); ``` 上述的四個 callbacks 中,由於大多數 CPUfreq 驅動程序只支援將 CPU 頻率設定為預先定義的固定值。這種情況下,可以使用 `target()`、`target_index()` 或 `fast_switch()`。 一些特定的處理器能夠支援在特定範圍間切換頻率。這時則應該使用 `setpolicy()`。 ### `init` 當新的 CPU 註冊到核心中,或 CPUFreq driver 註冊自己後,如果存在沒有分配到 policy 的 CPU,則 `init()` callback 就會被呼叫。 :::info 留意到 `init()` 和 `exit()` 只會針對各個 policy 呼叫一次,而不是針對該 policy 管理的每個 CPU 各自一次。 ::: `init()` 中包含的參數是 `struct cpufreq_policy *policy`,驅動程式需要在其中填入必要的數值。 | Cotent | Description | |:----------------------------------------------------------- |:---------------------------------------------- | | policy->cpuinfo.min_freq policy->cpuinfo.max_freq | 此 policy 下最大/最小可能的 CPU 頻率 | | policy->cpuinfo.transition_latency | CPU 在兩個頻率間切換所需的時間(ns) | | policy->cur | 當前 CPU 的指定頻率 | | policy->min, policy->max, policy->policy, policy->governor | policy 初始化時的預設設定 | | policy->cpus | 在此 policy 中一起執行 DVFS 的 CPU 群組之 mask | 此外,上表中一些值的內容(`cpuinfo.min[max]_freq`、`policy->min[max]`)可以藉由設定 frequency table 方式設定,具體是在初始化時藉 [`cpufreq_table_validate_and_sort`](https://elixir.bootlin.com/linux/v6.13.1/source/drivers/cpufreq/freq_table.c#L354) 去設定。 ### `verify` 當使用者嘗試設定新的 policy 時(例如 `governor`/`min`/`max`)時,必須驗證設定是否正確,此時 `verify()` callback 會被使用。 實作可以透過 [`cpufreq_verify_within_limits()`](https://elixir.bootlin.com/linux/v6.13.1/source/include/linux/cpufreq.h#L481) 來協助檢驗正確性。 ### `target` / `target_index` / `setpolicy` / `fast_switch` 大多數的 CPU 頻率調節機制只能將頻率設定為預先定義的數個固定值。對於這些情況,可以使用 `target()`、`target_index()` 或 `fast_switch()` callbacks。 一些支援 CPUFreq 的處理器可以在特定範圍內切換頻率。對於這種類型,應該使用 `setpolicy()` callback。 `target_index()` 包含 `struct cpufreq_policy *` 的 `policy` 和 `unsigned int` 的 `index` 兩個參數,`index` 會指向 frequency table 的其中一個 entry,表示指定的設定頻率。 `target()` 是棄用的 callback,包含 `struct cpufreq_policy *policy`、`unsigned int target_freq` 和 `unsigned int relationship` 三個參數。實際頻率的設定使用以下規則來決定: * 盡量接近 `target_freq` * 新頻率設定值必須介於 `policy->min` 和 `policy->max` 之間 * 如果 `relationship` 為 `CPUFREQ_REL_L`,則嘗試選擇大於或等於 `target_freq` 的新頻率值 * 如果 `relationship` 為 `CPUFREQ_REL_H`,則嘗試選擇一個小於或等於 `target_freq` 的新頻率值 ### `fast_switch` `fast_switch` 用於在排程器的 context 下切換頻率。因為是在 atomic context 中,在此 callback 中 sleep 是不允許的,因此並非所有驅動程式都需要實作此函數。此 callback 必須經過高度最佳化才能盡可能快速地進行切換。 ### `setpolicy` `setpolicy` 根據給定的 [`struct cpufreq_policy`](https://github.com/torvalds/linux/blob/master/include/linux/cpufreq.h#L55),設定動態頻率切換的下限 `policy->min`、上限 `policy->max`,或者效能取向(`CPUFREQ_POLICY_PERFORMANCE`) 或節能取向(`CPUFREQ_POLICY_POWERSAVE`)的定頻策略。 ### `get_intermediate` / `target_intermediate` 只有實作 `target_index()` 且未設定 `CPUFREQ_ASYNC_NOTIFICATION` 的 `cpufreq_driver` 有可能需要實作這兩個 callbacks。`get_intermediate` 用來得到平台的中間頻率,而 `target_intermediate()` 則是在跳到 `index` 對應的頻率前使用,先將 CPU 設定為該中間頻率。 所謂中間頻率,是指在 CPU 頻率之間轉換時需要先進入的臨時頻率,目的是確保平穩的頻率變化。具體來說,比如當指示 CPU 從最低頻率切換到最高頻率時,需要先切換到穩定的中間頻率,然後透過 `get_intermediate()` 確認其正確性後,再進一步提升至最終的目標頻率。主要是針對某些較舊的驅動程式或複雜的硬體,為避免錯誤需要有此機制。 如果不希望在某個目標頻率下切換到中間頻率,則可以讓 `get_intermediate()` 傳回 `0`。在此情況下,CPUFreq core 將直接呼叫 `->target_index()` 進行調頻。 ## Reference * [CPU Performance Scaling](https://www.kernel.org/doc/html/latest/admin-guide/pm/cpufreq.html) * [CPUFreq](https://docs.kernel.org/cpu-freq/index.html) * [linux cpufreq framework(1)_概述](http://www.wowotech.net/pm_subsystem/cpufreq_overview.html) * [Improvements in CPU frequency management](https://lwn.net/Articles/682391/) * [3.2. CPUfreq(Redhat Doc)](https://docs.redhat.com/zh-cn/documentation/red_hat_enterprise_linux/7/html/power_management_guide/cpufreq_governors#intel_p_state_governor_types)