進程是操做系統虛擬出來的概念,用來組織計算機中的任務。但隨着進程被賦予愈來愈多的任務,進程好像有了真實的生命,它從誕生就隨着CPU時間執行,直到最終消失。不過,進程的生命都獲得了操做系統內核的關照。就好像疲於照顧幾個孩子的母親內核必須作出決定,如何在進程間分配有限的計算資源,最終讓用戶得到最佳的使用體驗。內核中安排進程執行的模塊稱爲調度器(scheduler)。這裏將介紹調度器的工做方式。html
調度器能夠切換進程狀態(process state)。一個Linux進程從被建立到死亡,可能會通過不少種狀態,好比執行、暫停、可中斷睡眠、不可中斷睡眠、退出等。咱們能夠把Linux下繁多的進程狀態,概括爲三種基本狀態。算法
圖1 進程的基本狀態網絡
進程建立後,就自動變成了就緒狀態。若是內核把CPU時間分配給該進程,那麼進程就從就緒狀態變成了執行狀態。在執行狀態下,進程執行指令,最爲活躍。正在執行的進程能夠主動進入阻塞狀態,好比這個進程須要將一部分硬盤中的數據讀取到內存中。在這段讀取時間裏,進程不須要使用CPU,能夠主動進入阻塞狀態,讓出CPU。當讀取結束時,計算機硬件發出信號,進程再從阻塞狀態恢復爲就緒狀態。進程也能夠被迫進入阻塞狀態,好比接收到SIGSTOP信號。數據結構
調度器是CPU時間的管理員。Linux調度器須要負責作兩件事:一件事是選擇某些就緒的進程來執行;另外一件事是打斷某些執行中的進程,讓它們變回就緒狀態。不過,並非全部的調度器都有第二個功能。有的調度器的狀態切換是單向的,只能讓就緒進程變成執行狀態,不能把正在執行中的進程變回就緒狀態。支持雙向狀態切換的調度器被稱爲搶佔式(pre-emptive)調度器。app
調度器在讓一個進程變回就緒時,就會當即讓另外一個就緒的進程開始執行。多個進程接替使用CPU,從而最大效率地利用CPU時間。固然,若是執行中進程主動進入阻塞狀態,那麼調度器也會選擇另外一個就緒進程來消費CPU時間。所謂的上下文切換(context switch)就是指進程在CPU中切換執行的過程。內核承擔了上下文切換的任務,負責儲存和重建進程被切換掉以前的CPU狀態,從而讓進程感受不到本身的執行被中斷。應用程序的開發者在編寫計算機程序時,就不用專門寫代碼處理上下文切換了。 性能
調度器分配CPU時間的基本依據,就是進程的優先級。根據程序任務性質的不一樣,程序能夠有不一樣的執行優先級。根據優先級特色,咱們能夠把進程分爲兩種類別。spa
普通進程根據行爲的不一樣,還能夠被分紅互動進程(interactive process)和批處理進程(batch process)。互動進程的例子有圖形界面,它們可能處在長時間的等待狀態,例如等待用戶的輸入。一旦特定事件發生,互動進程須要儘快被激活。通常來講,圖形界面的反應時間是50到100毫秒。批處理進程沒有與用戶交互的,每每在後臺被默默地執行。操作系統
實時進程由Linux操做系統創造,普通用戶只能建立普通進程。兩種進程的優先級不一樣,實時進程的優先級永遠高於普通進程。進程的優先級是一個0到139的整數。數字越小,優先級越高。其中,優先級0到99留給實時進程,100到139留給普通進程。設計
一個普通進程的默認優先級是120。咱們能夠用命令nice來修改一個進程的默認優先級。例若有一個可執行程序叫app,執行命令:3d
$nice -n -20 ./app
命令中的-20指的是從默認優先級上減去20。經過這個命令執行app程序,內核會將app進程的默認優先級設置成100,也就是普通進程的最高優先級。命令中的-20能夠被換成-20至19中任何一個整數,包括-20 和 19。默認優先級將會變成執行時的靜態優先級(static priority)。調度器最終使用的優先級根據的是進程的動態優先級:
動態優先級 = 靜態優先級 – Bonus + 5
若是這個公式的計算結果小於100或大於139,將會取100到139範圍內最接近計算結果的數字做爲實際的動態優先級。公式中的Bonus是一個估計值,這個數字越大,表明着它可能越須要被優先執行。若是內核發現這個進程須要常常跟用戶交互,將會把Bonus值設置成大於5的數字。若是進程不常常跟用戶交互,內核將會把進程的Bonus設置成小於5的數。
下面介紹Linux的調度策略。最原始的調度策略是按照優先級排列好進程,等到一個進程運行完了再運行優先級較低的一個,但這種策略徹底沒法發揮多任務系統的優點。所以,隨着時間推移,操做系統的調度器也屢次進化。
先來看Linux 2.4內核推出的O(n)調度器。O(n)這個名字,來源於算法複雜度的大O表示法。大O符號表明這個算法在最壞狀況下的複雜度。字母n在這裏表明操做系統中的活躍進程數量。O(n)表示這個調度器的時間複雜度和活躍進程的數量成正比。
O(n)調度器把時間分紅大量的微小時間片(Epoch)。在每一個時間片開始的時候,調度器會檢查全部處在就緒狀態的進程。調度器計算每一個進程的優先級,而後選擇優先級最高的進程來執行。一旦被調度器切換到執行,進程能夠不被打擾地用盡這個時間片。若是進程沒有用盡時間片,那麼該時間片的剩餘時間會增長到下一個時間片中。
O(n)調度器在每次使用時間片前都要檢查全部就緒進程的優先級。這個檢查時間和進程中進程數目n成正比,這也正是該調度器複雜度爲O(n)的緣由。當計算機中有大量進程在運行時,這個調度器的性能將會被大大下降。也就是說,O(n)調度器沒有很好的可拓展性。O(n)調度器是Linux 2.6以前使用的進程調度器。當Java語言逐漸流行後,因爲Java虛擬機會建立大量進程,調度器的性能問題變得更加明顯。
爲了解決O(n)調度器的性能問題,O(1)調度器被髮明瞭出來,並從Linux 2.6內核開始使用。顧名思義,O(1)調度器是指調度器每次選擇要執行的進程的時間都是1個單位的常數,和系統中的進程數量無關。這樣,就算系統中有大量的進程,調度器的性能也不會降低。O(1)調度器的創新之處在於,它會把進程按照優先級排好,放入特定的數據結構中。在選擇下一個要執行的進程時,調度器不用遍歷進程,就能夠直接選擇優先級最高的進程。
和O(n)調度器相似,O(1)也是把時間片分配給進程。優先級爲120如下的進程時間片爲:
(140–priority)×20毫秒
優先級120及以上的進程時間片爲:
(140–priority)×5 毫秒
O(1)調度器會用兩個隊列來存放進程。一個隊列稱爲活躍隊列,用於存儲那些待分配時間片的進程。另外一個隊列稱爲過時隊列,用於存儲那些已經享用過期間片的進程。O(1)調度器把時間片從活躍隊列中調出一個進程。這個進程用盡時間片,就會轉移到過時隊列。當活躍隊列的全部進程都被執行事後,調度器就會把活躍隊列和過時隊列對調,用一樣的方式繼續執行這些進程。
上面的描述沒有考慮優先級。加入優先級後,狀況會變得複雜一些。操做系統會建立140個活躍隊列和過時隊列,對應優先級0到139的進程。一開始,全部進程都會放在活躍隊列中。而後操做系統會從優先級最高的活躍隊列開始依次選擇進程來執行,若是兩個進程的優先級相同,他們有相同的機率被選中。執行一次後,這個進程會被從活躍隊列中剔除。若是這個進程在此次時間片中沒有完全完成,它會被加入優先級相同的過時隊列中。當140個活躍隊列的全部進程都被執行完後,過時隊列中將會有不少進程。調度器將對調優先級相同的活躍隊列和過時隊列繼續執行下去。過時隊列和活躍隊列,如圖2所示。
圖2 過時隊列和活躍隊列(須要替換)
咱們下面看一個例子,有五個進程,如表1所示。
表1 進程
Linux操做系統中的進程隊列(run queue),如表2所示。
表2 進程隊列
那麼在一個執行週期,被選中的進程依次是先A,而後B和C,隨後是D,最後是E。
注意,普通進程的執行策略並無保證優先級爲100的進程會先被執行完進入結束狀態,再執行優先級爲101的進程,而是在每一個對調活躍和過時隊列的週期中都有機會被執行,這種設計是爲了不進程飢餓(starvation)。所謂的進程飢餓,就是優先級低的進程好久都沒有機會被執行。
咱們看到,O(1)調度器在挑選下一個要執行的進程時很簡單,不須要遍歷全部進程。可是它依然有一些缺點。進程的運行順序和時間片長度極度依賴於優先級。好比,計算優先級爲100、1十、120、130和139這幾個進程的時間片長度,如表3所示。
表3 進程的時間片長度
從表格中你會發現,優先級爲110和120的進程的時間片長度差距比120和130之間的大了10倍。也就是說,進程時間片長度的計算存在很大的隨機性。O(1)調度器會根據平均休眠時間來調整進程優先級。該調度器假設那些休眠時間長的進程是在等待用戶互動。這些互動類的進程應該得到更高的優先級,以便給用戶更好的體驗。一旦這個假設不成立,O(1)調度器對CPU的調配就會出現問題。
從2007年發佈的Linux 2.6.23版本起,徹底公平調度器(CFS,Completely Fair Scheduler)取代了O(1)調度器。CFS調度器不對進程進行任何形式的估計和猜想。這一點和O(1)區分互動和非互動進程的作法徹底不一樣。
CFS調度器增長了一個虛擬運行時(virtual runtime)的概念。每次一個進程在CPU中被執行了一段時間,就會增長它虛擬運行時的記錄。在每次選擇要執行的進程時,不是選擇優先級最高的進程,而是選擇虛擬運行時最少的進程。徹底公平調度器用一種叫紅黑樹的數據結構取代了O(1)調度器的140個隊列。紅黑樹能夠高效地找到虛擬運行最小的進程。
咱們先經過例子來看CFS調度器。假如一臺運行的計算機中原本擁有A、B、C、D四個進程。內核記錄着每一個進程的虛擬運行時,如表4所示。
表4 每一個進程的虛擬運行時
系統增長一個新的進程E。新建立進程的虛擬運行時不會被設置成0,而會被設置成當前全部進程最小的虛擬運行時。這能保證該進程被較快地執行。在原來的進程中,最小虛擬運行時是進程A的1 000納秒,所以E的初始虛擬運行時會被設置爲1 000納秒。新的進程列表如表5所示。
表5 新的進程列表
假如調度器須要選擇下一個執行的進程,進程A會被選中執行。進程A會執行一個調度器決定的時間片。假如進程A運行了250納秒,那它的虛擬運行時增長。而其餘的進程沒有運行,因此虛擬運行時不變。在A消耗完時間片後,更新後的進程列表,如表6所示。
表6 更新後的進程列表
能夠看到,進程A的排序降低到了第三位,下一個將要被執行的進程是進程E。從本質上看,虛擬運行時表明了該進程已經消耗了多少CPU時間。若是它消耗得少,那麼理應優先得到計算資源。
按照上述的基本設計理念,CFS調度器能讓全部進程公平地使用CPU。聽起來,這讓進程的優先級變得毫無心義。CFS調度器也考慮到了這一點。CFS調度器會根據進程的優先級來計算一個時間片因子。一樣是增長250納秒的虛擬運行時,優先級低的進程實際得到的可能只有200納秒,而優先級高的進程實際得到可能有300納秒。這樣,優先級高的進程就得到了更多的計算資源。
以上就是調度器的基本原理,以及Linux用過的幾種調度策略。調度器能夠更加合理地把CPU時間分配給進程。現代計算機都是多任務系統,調度器在多任務系統中起着頂樑柱的做用。
歡迎閱讀「騎着企鵝採樹莓」系列文章