Linux Cpuidle 介紹

一、引入背景

先來看一個抖音場景下面的功耗表現,這個是功耗分解板拆解出來的某一款手機 cpu 的核壓和功率,會發現它們它們的數值不是一直保持在高位,有些時間會下降到很低。

這個時候有人會說下降是因爲 cpu 上面沒有任務在執行了,所以 cpu 就不需要工作造成功耗損失,那麼就引出來一件事情,當 cpu 上面沒有任務執行的時候,系統是如何進行 cpu 管理的,如果在沒有任務的時候只是單純的關閉 cpu,那麼下一次來一個事件的時候系統又如何兼顧此時的性能呢。在 Linux kernel 中,當 cpu 中沒有任務在執行,也沒有任何中斷、異常信號過來的時候,我們稱爲處於 idle 狀態,針對這種狀態 Linux 設計了一套 cpuidle framework 框架,專門用於 cpuidle 的管理。

二、Idle 狀態

1. Idle 狀態,在代碼裏面是如何判斷的呢

在 Linux 系統啓動的時候,會在每個 cpu 上創建對應的 idle 進程,start_kernel() 函數初始化內核需要的所有數據結構,並創建一個名爲 init 的進程(pid=1),當 init 進程創建完後,cpu 的 idle 進程處於 cpu_idle_loop() 無限循環中,當沒有其他進程處於 TASK_RUNNING 狀態時候,調度器纔會執行 cpu idle 線程,讓 cpu 進入 idle 模式. 其函數調用關係簡要概括如下:

start_kernel –> rest_init –> cpu_startup_entry,在 cpu_startup_entry 這個函數中,最終程序會進入無限循環 do_idle loop 中

在 do_idle() 中,代碼會不斷地輪詢,判斷當前系統是否需要調度,如果系統當前不需要調度,則進入到 idle 狀態.

do_idle()->cpuidle_idle_call()->cpuidle_select()

在 cpuidle_select 函數里就是真正在進行 cpu idle 的選擇操作。

2. 多級 idle 狀態的產生

關閉一些核可以節省功耗,但關閉之後對時延 (性能) 必會造成一定的影響,如果在關閉之後很短的時間內就被喚醒,那麼就會造成功耗 / 性能雙方都不討好,在進入退出 idle 的過程中也是會有功耗的損失的,如果在 idle 狀態下面節省的功耗還無法彌補進入退出該 idle 的功耗,那麼反而會得不償失。根據性能 / 功耗的的這種矛盾,很多廠家會制定多個層級的 idle 狀態,在每個層級下面的功耗、進入退出 idle 的功耗損失、以及進入退出的延遲都會是不同的數值,而 cpuidle framework 會根據不同的場景來進行仲裁選擇使用何種的 idle 狀態。

三、cpuidle framework 軟件架構

在 kernel 中 cpuidle framework 主體包含三個模塊,分別爲 cpuidle core、cpuidle governors 和 cpuidle drivers,架構圖如下:

cpu idle core: 負責整體框架,同時負責和 sched 模塊對接,當調度器發現沒有任務在執行時候,就切換到 idle 進程,通知到 cpuidle framework 的 cpuidle core 模塊要做接下來的 idle 操作。向 cpuidle driver/governors 模塊提供統一的 driver 和 governors 註冊和管理接口,向用戶空間程序提供 governor 選擇的接口。

cpuidle driver: 負責具體 idle 機制的實現(不同等級下面 idle 的指標也是在這個模塊進行填充),不同的平臺會有不同的 drivers 實現。

cpudile governors: 在這個模塊進行 cpuidle 的選擇,選擇的算法主要是基於切換的功耗代價和系統的延遲容忍度,電源管理的目標就是在保證延遲在系統可以接受的範圍內儘可能的節省功耗。

下面分別介紹下這幾個模塊的主體功能。

cpuidle core

cpuidle core 是 cpuidle framework 的核心模塊,負責抽象出 cpuidle device、cpuidle driver 和 cpuidle governor 三個實體。主體數據結構如下:

cpuidle_state:

上面也提到了現在很多廠家都會制定多個層級的 idle 狀態,Linux kernel 使用 struct cpuidle_state 結構抽象 idle 級別,主要成員的含義如下:

lname、desc:名稱和簡介;

lexit_latency:CPU 從該 idle state 下返回運行狀態的延遲

ltarget_residency: 期望的停留時間,進入退出 idle 狀態是需要額外的功耗的,如果 cpu 很快進入退出 idle 狀態,那麼它的額外的功耗損失可能還彌補不了處於 idle 狀態的功耗收益,從上面的圖形中比較容易理解。所以就有了這樣的一個指標,當 cpu 在 idle 狀態下面停留超過一個的時間,纔不會有功耗的損失,而這個臨界點就是這裏面提到的期望的停留時間,不同的平臺和不同的 idle 等級下面這個值大小是不同的,Cpuidle Governor 也會根據該數值來進行 idle level 的選擇。

lpower_usage:cpu 在該 idle state 下的功耗,單位 nw

lenter:該 state 的回調函數

cpuidle_deivce

在現在的 SMP 系統中,每個 cpu core 都會有一個對應的 cpuidle device,內核是通過使用 struct cpuidle_device 抽象 cpuidle device, 該結構體主要成員含義如下:

lenabled:設備是否已經使能

lcpu: 該 device 對應的 cpu number

llast_residency: 該設備上一次停留在 idle 狀態的時間

lstates_usage: 記錄了該設備的每個 idle state 的統計信息

cpuidle_driver:

主要成員含義如下:

lstates、state_count:該 driver 支持的 cpuidle state 及其個數

cpuidle driver 的主要工作是定義所支持的 cpuidle state,以及 state 的 enter 接口,如下面所示,cpudile driver 就要負責將平臺定義的 idle-state 信息填充到這個結構體中

cpuidle_governor:

內核通過使用 struct cpuidle_governor 結構體來抽象 cpuidle governor,主要成員含義如下:

lgovernor_list:將該 gover 添加到一個全局的鏈表中

lrating:governor 的級別

lenable/disable:governor 的回調函數,主要進行一些初始化的操作

lselect:決策一個最佳的 idle state

lreflect:告知 governor 上一次所處的 idle state 是哪一個

cpuidle governor 是 cpuidle 框架進行 idle state 決策的核心,下面我們重點對該模塊展開講解。

四、cpuidle governor

在當前的內核中,有兩種主流的 governor 策略:ladder 和 menu,選擇哪一種取決於內核的配置,ladder 在 periodic timer tick system 中使用,menu 在 tickless system 中使用

Ladder,從字面上理解是階梯式的策略,即要到更高的層級必須從低層級 step by step 上去,在 ladder 策略中,ladder governor 會首先進入最淺的 idle state,然後如果待的時間足夠長,則會進入到更深一級的 idle state,以此類推,直到到達最深的 idle state,被喚醒時,會盡可能快地重新啓動 CPU;等到下次空閒,則又會從 idle state1 開始進入。在這種策略中,系統可能長時間都不進入最深的 idle state 中,造成功耗低一些損失。

Menu, 從字面上理解是菜單式的策略,即只要具備進入更深層次 idle state 的條件,系統就可以選擇進入到該 idle state 中,不需要從淺到深逐層遞進。

由於主流系統中常採用 tickless system,所以接下來重點介紹 menu governor

在上面的章節中有提到,governor 的主要職責是決策一個最佳的 idle state(在 kernel 的術語中也成爲 c-state),主要的考量是基於切換的功耗代價和系統的延遲容忍度。

切換代價:

在講 cpuidle core 的時候有提到 cpuidle_state 中有個成員變量,名爲 target_residency(期望的停留時間),這個可以認爲是在一次切換過程中滿足功耗不損失的 min 時間值,所以這裏切換的代價落到實處就是需要在選擇 idle state 的時候需要 cpu 在 c state 的停留時間要超過 target_residency,這裏的停留時間系統有一個術語來進行表徵:預測時間(predicted_us)

系統的延遲容忍度:

系統延遲容忍度主要是考慮對性能的影響,系統延遲容忍度在前面的文章《Linux pm_qos 介紹》中有提到,kernel 會通過 pm qos 來獲取當前系統對延遲的容忍度(latency_req),接下來 governors 所要做的事情就是在備選的幾個 c-state 中,在所有 exit_latency 小於 latency_req 的 state 中,選取功耗(power_usage)最小的那個即可。

落到實處,這兩個關鍵的考量點是如何來滿足的呢:1、即預測時間如何來判斷更準確,2、系統延遲容忍度如何更準確知道系統對延遲的要求。下面我們結合 menu governor 的主要結構和函數來展開。

主要數據結構:

Struct menu_governor:

在初始化的時候會調用 cpuidle_register_governor 來註冊 menu_governor,主要是提供了 enable、select、reflect 三個接口

struct menu_device:

lCorrection_factor: 保存校正因子,一共有 BUCKETS 個(一般爲 12 個),後面會提到

lBucket: 當前使用的矯正因子

lNext_timer_ns:距離下一個 tick 來臨的時間,在後面的代碼中會提到。

主體函數接口:

menu_enable_device 接口

該函數比較簡單,主要任務就是初始化在結構體 stuct menu_device 中保存的校準因子 correction_factor

menu_select 接口

總結下來就是幾個過程:

1、計算使用哪一類校正因子以及 predicted_us,因爲預測時間不是很準確的,期間隨時都可能產生除 next timer event 之外的其它 wakeup event。爲了使預測更準確,有必要加入一個校正因子(correction factor),該校正因子基於過去的實際 predicted_us 和 next_timer_us 之間的比率,爲了更精確,menu 使用動態平均的 factor。另外對於不同的 next_timers_us,校正因子的影響程度是不一樣的;對於不同的 io wait 場景,系統對校正因子也有着不同的敏感程度 。隨後嘗試通過最近的 8 個過去的停留時間來查找重複間隔,如果他們的標準差小於一定閾值,則將這 8 個時間的平均值作爲 predicted_us。最後取以上兩個流程中的最小值

2、計算延遲容忍度:根據 predicted_us 和系統負荷 (iowaiters) 計算此時的延遲容忍度,計算公式爲 predicted_us / (1 +10 * iowaiters),這個公式反映的是退出延遲和預期停留時間之間的比例,iowaiters 越大,對退出延遲的要求就越高,最後 latency_req 的值取上面這個估值和 pm_qos 中 lantency 中的最小值作爲最後的延遲容忍度

3、最後根據前面計算出來的兩個因素來選取具體的 idle state,將計算出的 predicted_us 與所有 idle 狀態的停留時間進行比較,選擇特定 idle 狀態的條件是相應的停留時間應小於 predicted_us。另外,將狀態的 exit_latency 與系統的交互性要求進行比較。基於兩個等待時間因素,選擇適當的空閒狀態。

在 cpu 退出 idle 狀態後,menu governor 會將將上一輪的進入 idle 狀態的數據更新到 menu driver 中,作爲下一次 select 的參數,具體見 menu_reflect 函數接口:

接下來下一次進入選擇流程時,會先觸發更新需求,在 menu_update 函數接口中進行操作

以上是 menu governor 的核心流程,最終結果是在延遲容忍度滿足的前提下選擇一個最佳的 idle state.

五、cpuidle 案例呈現

在系統健康的情況下,cpuidle framwork 會選擇一個最適合性能功耗的 c-state,但是在實際工作中,會遇到一些代碼編寫不規範的情況,導致最後的 c-state 處於一個我們不希望停留的狀態。如下的一個案例:

在桌面 idle 場景,實際的 trace 表現:

而我們預期的表現應該是這樣的

分析 trace 發現在 idle 的時候系統最深是進入到 state=2 狀態,而目前主流的幾個平臺一般會存在 c0~c4 幾個狀態,idx 爲 2 是沒有進入到此時預期的最深 idle 狀態(此時爲桌面待機場景,無其他操作,理應進入最深的 idle 狀態)

添加 debug 信息發現 request latency 的時候有模塊在 request  cpu_dma_latency=400us,而查看平臺數據時發現該 cpu 有四個 idle-state 等級,exit-latency 時間分別爲:100us/250us/1200us/1400us,從而導致系統無法進入到更深層次的 idle 狀態

六、總結

以上內容從背景、idle 狀態、cpuidle framework 軟件架構、cpuidle goveror 四個維度進行 cpuidle 的介紹,並且以一個實際的案例來說明在工作中可能遇到的問題點,希望可以幫助到大家來學習 linux cpuidle 模塊

參考文獻:

1、http://www.wowotech.net/tag/cpuidle

2、https://www.kernel.org/doc/html/v5.0/admin-guide/pm/cpuidle.html

3、https://zhuanlan.zhihu.com/p/97996145

4、https://blog.csdn.net/feelabclihu/article/details/106866457

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