CPU 空閑時在幹嘛?
- 2021 年 4 月 20 日
- 筆記
人在空閑時會發獃會無聊,計算機呢?
假設你正在用計算機瀏覽網頁,當網頁加載完成後你開始閱讀,此時你沒有移動鼠標,沒有敲擊鍵盤,也沒有網絡通信,那麼你的計算機此時在幹嘛?
有的同學可能會覺得這個問題很簡單,但實際上,這個問題涉及從硬件到軟件、從 CPU 到操作系統等一系列環節,理解了這個問題你就能明白操作系統是如何工作的了。
你的計算機 CPU 使用率是多少?
如果此時你正在計算機旁,並且安裝有 Windows 或者 Linux ,你可以立刻看到自己的計算機 CPU 使用率是多少。
這是博主的一台安裝有 Win10 的筆記本:

可以看到大部分情況下 CPU 利用率很低,也就在 8% 左右,而且開啟了 283 個進程,這麼多進程基本上無所事事,都在等待某個特定事件來喚醒自己,就好比你寫了一個打印用戶輸入的程序,如果用戶一直不按鍵盤,那麼你的進程就處於這種狀態。
有的同學可能會想也就你的比較空閑吧,實際上大部分個人計算機 CPU 使用率都差不多這樣(排除掉看電影、玩遊戲等場景),如果你的使用率總是很高,風扇一直在嗡嗡的轉,那麼不是軟件 bug 就有可能是病毒。。。
那麼有的同學可能會問,剩下的 CPU 時間都去哪裡了?
剩下的 CPU 時間去哪裡了?
這個問題也很簡單,還是以 Win10 為例,打開任務管理器,找到 「詳細信息」 這一欄,你會發現有一個 「系統空閑進程」,其 CPU 使用率達到了 99%,正是這個進程消耗了幾乎所有的 CPU 時間。

那麼為什麼存在這樣一個進程呢?以及這個進程什麼時候開始運行呢?
在正式講解之前在這裡給大家分享一份刷題資料,認真過上一遍,國內BAT大廠面試算法大部分題目都能做出來:Github瘋傳!阿里P8大佬寫的Leetcode刷題筆記,秒殺80%的算法題
接下來我們從操作系統講起。
代碼、進程與操作系統
當你用最喜歡的代碼編輯器編寫代碼時,這時的代碼不過就是磁盤上的普通文件,此時的程序和操作系統沒有半毛錢關係,操作系統也不認知這種文本文件。

程序員寫完代碼後開始編譯,這時編譯器將普通的文本文件翻譯成二進制可執行文件,此時的程序依然是保存在磁盤上的文件,和普通沒有本質區別。

)
但此時不一樣的是,該文件是可執行文件,也就是說操作系統開始 「懂得」 這種文件,所謂 「懂得」 是指操作系統可以識別、解析、加載,因此必定有某種類似協議的規範,這樣編譯器按照這種協議生成可執行文件,操作系統就能加載了。
在 Linux 下可執行文件格式為 ELF ,在 Windows 下是 EXE 。
此時雖然操作系統可以識別可執行程序,但如果你不去雙擊一下(或者在Linux下運行相應命令)的依然和操作系統沒有半毛錢關係。
但是當你運行可執行程序時魔法就出現了。
此時操作系統開始將可執行文件加載到內存,解析出代碼段、數據段等,並為這個程序創建運行時需要的堆區棧區等內存區域,此時這個程序在內存中就是這樣了:

最後,根據可執行文件的內容,操作系統知道該程序應該執行的第一條機器指令是什麼,並將其告訴 CPU ,CPU 從該程序的第一條指令開始執行,程序就這樣運行起來了。
一個在內存中運行起來的程序顯然和保存在磁盤上的二進制文件是不一樣的,總的有個名字吧,根據「弄不懂原則」,這個名字就叫進程,英文名叫做Process。
我們把一個運行起來的程序叫做進程,這就是進程的由來。
此時操作系統開始掌管進程,現在進程已經有了,那麼操作系統是怎麼管理進程的呢?
調度器與進程管理
銀行想必大家都去過,實際上如果你仔細觀察的話銀行的辦事大廳就能體現出操作系統最核心的進程管理與調度。
首先大家去銀行都要排隊,類似的,進程在操作系統中也是通過隊列來管理的。
同時銀行還按照客戶的重要程度劃分了優先級,大部分都是普通客戶;但當你在這家銀行存上幾個億時就能升級為 VIP 客戶,優先級最高,每次去銀行都不用排隊,優先辦理你的業務。
類似的,操作系統也會為進程劃分優先級,操作系統會根據進程優先級將其放到相應的隊列中供調度器調度。

這就是操作系統需要實現的最核心功能。
現在準備工作已經就緒。
接下來的問題就是操作系統如何確定是否還有進程需要運行。
隊列判空:一個更好的設計
從上一節我們知道,實際上操作系統是用隊列來管理進程的,那麼很顯然,如果隊列已經為空,那麼說明此時操作系統內部沒有進程需要運行,這是 CPU 就空閑下來了,此時,我們需要做點什麼,就像這樣:
if (queue.empty()) {
do_someting();
}
這些編寫內核代碼雖然簡單,但內核中到處充斥着 if 這種異常處理的語句,這會讓代碼看起來一團糟,因此更好的設計是沒有異常,那麼怎樣才能沒有異常呢?
很簡單,那就是讓隊列永遠不會空,這樣調度器永遠能從隊列中找到一個可供運行的進程。
而這也是為什麼鏈表中通常會有哨兵節點的原因,就是為了避免各種判空,這樣既容易出錯也會讓代碼一團糟。

說到鏈表,在這裡給大家分享一份刷題資料,認真過上一遍,國內BAT大廠面試算法大部分題目都能做出來:Github瘋傳!阿里P8大佬寫的Leetcode刷題筆記,秒殺80%的算法題!
就這樣,內核設計者創建了一個叫做空閑任務的進程,這個進程就是Windows 下的我們最開始看到的「系統空閑進程」,在 Linux 下就是第 0號進程。
當其它進程都處於不可運行狀態時,調度器就從隊列中取出空閑進程運行,顯然,空閑進程永遠處於就緒狀態,且優先級最低。
既然我們已經知道了,當系統無所事事後開始運行空閑進程,那麼這個空閑進程到底在幹嘛呢?
這就需要硬件來幫忙了。
一切都要歸結到硬件
在計算機系統中,一切最終都要靠 CPU 來驅動,CPU 才是那個真正幹活的。

原來,CPU 設計者早就考慮到系統會存在空閑的可能,因此設計了一條機器指令,這個機器指令就是 halt 指令,停止的意思。
這條指令會讓部分CPU進入休眠狀態,從而極大減少對電力的消耗,通常這條指令也被放到循環中執行,原因也很簡單,就是要維持這種休眠狀態。
值得注意的是,halt 指令是特權指令,也就是說只有在內核態下 CPU 才可以執行這條指令,程序員寫的應用都運行在用戶態,因此你沒有辦法在用戶態讓 CPU 去執行這條指令。
此外,不要把進程掛起和 halt 指令混淆,當我們調用 sleep 之類函數時,暫停運行的只是進程,此時如果還有其它進程可以運行那麼 CPU 是不會空閑下來的,當 CPU 開始執行halt指令時就意味着系統中所有進程都已經暫停運行。
軟件硬件結合
現在我們有了 halt 機器指令,同時有一個循環來不停的執行 halt 指令,這樣空閑任務進程的實際上就已經實現了,其本質上就是這個不斷執行 halt 指令的循環,大功告成。
這樣,當調度器在沒有其它進程可供調度時就開始運行空間進程,也就是在循環中不斷的執行 halt 指令,此時 CPU 開始進入低功耗狀態。

在 Linux 內核中,這段代碼是這樣寫的:
while (1) {
while(!need_resched()) {
cpuidle_idle_call();
}
}
其中 cpuidle_idle_call 最終會執行 halt 指令,注意,這裡刪掉了大量代碼,實際 Linux 內核在實現空閑進程時還要考慮很多很多,不同類型的 CPU 可能會有深睡眠淺睡眠之類,操作系統必須要預測出系統可能的空閑時長並以此判斷要進入哪種休眠等等,但這並不是我們關注的重點。
總的來說,這就是計算機系統空閑時 CPU 在幹嘛,就是在執行這一段代碼,本質上就是 CPU 在執行 halt 指令。
實際上,對於個人計算機來說,halt 可能是 CPU 執行最多的一條指令,全世界的 CPU 大部分時間都用在這條指令上了,是不是很奇怪。
更奇怪的來了,有的同學可能已經注意到了,上面的循環可以是一個while(1) 死循環,而且這個循環里沒有break語句,也沒有return,那麼操作系統是怎樣跳出這個循環的呢?點擊這裡你就知道答案啦。
總結
CPU 空閑時執行特定的 halt 指令,這看上去是一個很簡單的問題,但實際上由於 halt 是特權指令,只有操作系統才可以去執行,因此 CPU 空閑時執行 halt 指令就變成了軟件和硬件相結合的問題。
操作系統必須判斷什麼情況下系統是空閑的,這涉及到進程管理和進程調度,同時,halt 指令其實是放到了一個 while 死循環中,操作系統必須有辦法能跳出循環,所以,CPU 空閑時執行 halt 指令並沒有看上去那麼簡單。
希望這篇文章對大家理解 CPU 和操作系統有所幫助。