- 前言
在計算機系統中,cpu的主要任務是執行程序,其核心步驟包括取指、譯碼和執行。然而,若無程序需要執行,cpu如何處理這一情況呢?有人可能會認為直接停止運行即可,但實際上,決定何時停止以及如何停止需要在復雜的軟硬件環境中仔細考慮。
讓我們轉向Linux內核,Linux系統中的CPU被兩種程序所占用:一類是進程(或線程),即進程上下文;另一類是中斷和異常的處理程序,即中斷上下文。
進程負責處理事務,例如讀取用戶輸入并在屏幕上顯示。當事務處理完畢,如用戶不再輸入且無新內容需顯示時,進程可以釋放CPU,但隨時準備重新占用(如用戶突然按鍵)。同樣,若系統無中斷或異常事件,CPU不會在中斷上下文中花費時間。
在Linux內核中,CPU這種無所事事的狀態被稱為idle狀態,而cpuidle框架正是管理這種狀態的工具。
注:cpuidle框架系列文章將以ARM64為例平臺。由于ARM64發布時間較短,早期版本的內核中沒有相關代碼,因此我們選擇了最新的3.18-rc4版本的內核。
- 功能概述
曾經,Linux內核的cpu idle框架非常簡單,簡單到driver工程師只需在“includeasm-armarch-xxxsystem.h”中定義一個名為arch_idle的內聯函數,并在該函數中調用內核提供的cpu_do_idle接口即可,其余的實現內核會幫我們完成,如下:
static inline void arch_idle(void) { cpu_do_idle(); }
盡管簡單,但這包含了idle處理的兩個關鍵點:
1)idle進程
idle進程的存在是為了解決“何時idle”的問題。
我們知道,Linux系統運行的基礎是進程調度。當所有進程都不再運行時,即為CPU idle狀態。內核通過一個簡單的方法來判斷這種狀態:在init進程(系統的第一個進程)完成初始化任務后,將其轉變為idle進程。由于idle進程的優先級最低,當其被調度時,說明系統中其他進程已停止運行,即CPU已idle。最終,idle進程會調用idle指令(如WFI),使CPU進入idle狀態。
“ARM WFI和WFE指令”中提到,WFI Wakeup events會將CPU從WFI狀態喚醒,這些事件通常是一些中斷事件。因此,CPU喚醒后會執行中斷處理程序,處理程序中會喚醒某些進程。當處理程序返回時,進行調度,如果沒有其他進程需要執行,調度器會恢復idle進程的運行,當然,idle進程不會做任何事情,繼續進入idle狀態,等待下一次喚醒。
2)WFI
WFI用于解決“如何idle”的問題。
通常情況下,ARM CPU在idle時可以使用WFI指令,將CPU置于等待中斷狀態。在這種狀態下,至少會關閉ARM核的時鐘,以節省功耗(具體實現取決于ARM核的設計,可參考“ARM WFI和WFE指令”)。
也許您會覺得,上述過程已經足夠好,為什么還要開發cpuidle框架?我的理解是:
- 軟件架構
在Linux內核中,cpuidle框架位于“drivers/cpuidle”文件夾中,包含cpuidle核心、cpuidle調控器和cpuidle驅動三個模塊,再結合位于內核調度中的cpuidle入口,共同完成CPU的idle管理。軟件架構如下圖:
1)內核調度模塊
位于kernelschedidle.c中,負責實現idle線程的通用入口(cpuidle入口)邏輯,包括idle模式的選擇和idle的進入等。
2)cpuidle核心
cpuidle核心負責實現cpuidle框架的整體結構,主要功能包括:
cpuidle核心的代碼主要包括:cpuidle.c、driver.c、governor.c、sysfs.c。
3)cpuidle驅動
不同的架構和CPU核會有不同的cpuidle驅動,平臺驅動開發者可以在cpuidle核心提供的框架下開發自己的cpuidle驅動。代碼主要包括:cpuidle-xxx.c。
4)cpuidle調控器
Linux內核的框架有兩種比較固定的抽象模式:
模式2的解釋可能有些抽象,但在cpuidle的場景中容易理解:
前面提到,許多CPU提供了多種idle級別(即所謂的“方案”),這些idle級別的主要區別在于“idle時的功耗”和“退出時的延遲”。cpuidle驅動(機制)負責定義這些idle狀態(每個狀態的功耗和延遲分別是多少),并實現進入和退出的相關操作。最終,cpuidle驅動會將這些信息傳遞給調控器,由調控器根據具體的應用場景決定選擇哪種idle狀態(策略)。
內核中的cpuidle調控器都位于governors/目錄下。
- 軟件流程
在閱讀本章之前,請先閱讀以下三篇文章:
Linux cpuidle framework(2)_cpuidle核心
Linux cpuidle framework(3)_ARM64通用CPU idle驅動
Linux cpuidle framework(4)_menu調控器
前面提到過,內核會在系統啟動完成后,在init進程(或線程)中處理cpuidle相關事務。大致過程如下(內核啟動相關的分析將在其他文章中詳細介紹):
cpu_startup_entry流程:
具體代碼比較簡單,不再分析,但有一點需要特別說明:
使用cpuidle框架進入idle狀態時,本地irq處于關閉狀態,因此從idle返回時,只能繼續執行,直到irq被打開,才能執行相應的中斷處理程序,這與傳統的cpuidle不同。同時,這也間接驗證了“Linux cpuidle framework(4)_menu調控器”中提到的,為什么menu調控器在reflect接口中只是簡單地設置一個標志。因為reflect是在關閉中斷時被調用的,需要盡快返回,以便處理中斷事件。