Linux進程及其調度策略

作者:Vamei
原文:www.cnblogs.com/vamei/p/9364382.html

進程是操作系統虛擬出來的概念,用來組織計算機中的任務。它從誕生到隨着CPU時間執行,直到最終消失。不過,進程的生命都得到了操作系統內核的關照。就好像疲於照顧幾個孩子的母親內核必須做出決定,如何在進程間分配有限的計算資源,最終讓用戶獲得最佳的使用體驗。內核中安排進程執行的模塊稱爲調度器(scheduler)。這裏將介紹調度器的工作方式。

進程狀態

調度器可以切換進程狀態(process state)。一個Linux進程從被創建到死亡,可能會經過很多種狀態,比如執行、暫停、可中斷睡眠、不可中斷睡眠、退出等。我們可以把Linux下繁多的進程狀態,歸納爲三種基本狀態。

圖1 進程的基本狀態

進程創建後,就自動變成了就緒狀態。如果內核把CPU時間分配給該進程,那麼進程就從就緒狀態變成了執行狀態。

在執行狀態下,進程執行指令,最爲活躍。正在執行的進程可以主動進入阻塞狀態,比如這個進程需要將一部分硬盤中的數據讀取到內存中。在這段讀取時間裏,進程不需要使用CPU,可以主動進入阻塞狀態,讓出CPU。

當讀取結束時,計算機硬件發出信號,進程再從阻塞狀態恢復爲就緒狀態。進程也可以被迫進入阻塞狀態,比如接收到SIGSTOP信號。

調度器是CPU時間的管理員。Linux調度器需要負責做兩件事:一件事是選擇某些就緒的進程來執行;另一件事是打斷某些執行中的進程,讓它們變回就緒狀態。不過,並不是所有的調度器都有第二個功能。

調度器在讓一個進程變回就緒時,就會立即讓另一個就緒的進程開始執行。多個進程接替使用CPU,從而最大效率地利用CPU時間。當然,如果執行中進程主動進入阻塞狀態,那麼調度器也會選擇另一個就緒進程來消費CPU時間。

所謂的上下文切換(context switch)就是指進程在CPU中切換執行的過程。內核承擔了上下文切換的任務,負責儲存和重建進程被切換掉之前的CPU狀態,從而讓進程感覺不到自己的執行被中斷。應用程序的開發者在編寫計算機程序時,就不用專門寫代碼處理上下文切換了。

進程優先級

調度器分配CPU時間的基本依據,就是進程的優先級。根據程序任務性質的不同,程序可以有不同的執行優先級。根據優先級特點,我們可以把進程分爲兩種類別。

1、實時進程(Real-Time Process):優先級高、需要儘快被執行的進程。它們一定不能被普通進程所阻擋,例如視頻播放、各種監測系統。

2、普通進程(Normal Process):優先級低、更長執行時間的進程。例如文本編譯器、批處理一段文檔、圖形渲染。

普通進程根據行爲的不同,還可以被分成互動進程(interactive process)和批處理進程(batch process)。互動進程的例子有圖形界面,它們可能處在長時間的等待狀態,例如等待用戶的輸入。一旦特定事件發生,互動進程需要儘快被激活。一般來說,圖形界面的反應時間是50到100毫秒。批處理進程沒有與用戶交互的,往往在後臺被默默地執行。

實時進程由Linux操作系統創造,普通用戶只能創建普通進程。兩種進程的優先級不同,實時進程的優先級永遠高於普通進程。進程的優先級是一個0到139的整數。數字越小,優先級越高。其中,優先級0到99留給實時進程,100到139留給普通進程。

一個普通進程的默認優先級是120。我們可以用命令nice來修改一個進程的默認優先級。例如有一個可執行程序叫app,執行命令:

$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的數。

O(n)和O(1)調度器

下面介紹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之前使用的進程調度器。

爲了解決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、110、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時間分配給進程。現代計算機都是多任務系統,調度器在多任務系統中起着頂樑柱的作用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章