1.1. Linux調度時機
Linux進程調度分爲主動調度和被動調度兩種方式:
自願的調度隨時都可以進行,內核裏可以通過schedule()啓動一次調度,當然也可以將進程狀態設置爲TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE,暫時放棄運行而進入睡眠;用戶空間可以通過pause()達到同樣的目的;如果爲這種暫時的睡眠放棄加上時間限制,內核態有schedule_timeout,用戶態有nanosleep()用於此目的;注意內核中這種主動放棄是不可見的,隱藏在每一個可能受阻的系統調用中,如open()、read()、select()等。
被動調度發生在系統調用返回的前夕、中斷異常處理返回前、用戶態處理軟中斷返回前。
自從Linux 2.6內核後,linux實現了搶佔式內核,即處於內核態的進程也可能被調度出去。比如一個進程正在內核態運行,此時一箇中斷髮生使另一個高權值進程就緒,在中斷處理程序結束之後,linux2.6內核之前的版本會恢復原進程的運行,直到該進程退出內核態纔會引發調度程序;而linux2.6搶佔式內核,在處理完中斷後,會立即引發調度,切換到高權值進程。爲支持內核代碼可搶佔,在2.6版內核中通過採用禁止搶佔的自旋鎖來保護臨界區。在釋放自旋鎖時(spin_unlock_mutex),同樣會引發調度檢查。而對那些長期持鎖或禁止搶佔的代碼片段插入了搶佔點,此時檢查調度需求,以避免不合理的延遲發生。而在檢查過程中,調度進程很可能就會中止當前的進程來讓另外一個進程運行,只要新的進程不需要持有該鎖。
1.2. Linux任務狀態轉換
1.3. Linux進程調度原理
1.3.1. 進程調度的一般原理
進程調度在近幾個版本中都進行了重要的修改。我們以2.6.9版爲例過行分析。在進行具體的代碼分析之前。我們先學習一下關於進程調度的原理。
1:進程類型
在linux調度算法中,將進程分爲兩種類型,即:I/O消耗型和CPU消耗型。例如文本處理程序與正在執行的Make的程序。文本處理程序大部份時間都在等待I/O設備的輸入,而make程序大部份時間都在CPU的處理上。因此爲了提高響應速度,I/O消耗程序應該有較高的優先級,才能提高它的交互性。相反的,Make程序相比之下就不那麼重要了,只要它能處理完就行了。因此,基於這樣的原理,linux有一套交互程序的判斷機制。
在task_struct結構中新增了一個成員:sleep_avg此值初始值爲100。進程在CPU上執行時,此值減少。當進程在等待時,此值增加。最後,在調度的時候。根據sleep_avg的值重新計算優先級。
2:進程優先級
正如我們在上面所說的:交互性強的需要高優先級,交互性弱的需要低優先級。在linux系統中,有兩種優先級:普通優先級和實時優先級。我們在這裏主要分析的是普通優先級,實時優先級部份可自行了解。
3:運行時間片
進程的時間片是指進程在搶佔前可以持續運行的時間。在linux中,時間片長短可根據優先級來調整。進程不一定要一次運行完所有的時間片。可以在運時的中途被切換出去。
4:進程搶佔
當一個進程被設爲TASK_RUNING狀態時,它會判斷它的優先級是否高於正在運行的進程,如果是,則設置調度標誌位,調用schedule()執行進程的調度。當一個進程的時間片爲0時,也會執行進程搶佔。
1.3.2. Linux O(1)調度
Linux2.6實現O(1)調度,每個CPU都有兩個進程隊列,採用優先級爲基礎的調度策略。內核爲每個進程計算出一個反映其運行“資格”的權值,然後挑選權值最高的進程投入運行。在運行過程中,當前進程的資格隨時間而遞減,從而在下一次調度的時候原來資格較低的進程可能就有資格運行了。到所有進程的資格都爲零時,就重新計算。
調度程序運行時,要在所有可運行的進程中選擇最值得運行的進程。選擇進程的依據主要有進程的調度策略(policy)、靜態優先級(priority)、動態優先級(counter)、以及實時優先級(rt-priority)四個部分。首先,Linux從整體上區分爲實時進程和普通進程,二者調度算法不同,實時進程優先於普通進程運行。進程依照優先級的高低被依次調用,實時優先級級別最高。
從某種意義上講,所有位於當前隊列的任務都將被執行並且都將被移到“過期”隊列之中(實時進程則例外,交互性強的進程也可能例外)。當這種事情發生時,情況就會有所變化,隊列就會被進行切換,原來的“過期”隊列成爲當前隊列,而空的當前隊列也就變成了過期隊列。
schedule()函數是完成進程調度的主要函數,並完成進程切換的工作。schedule()用於確定最高優先級進程的代碼非常快捷高效,其性能的好壞對系統性能有着直接影響,它在/kernel/sched.c 中的定義如下:
{
...
int idx;
...
preempt_disable();
...
idx = sched_find_first_bit( array -> bitmap);
queue = array -> queue + idx;
next = list_entry( queue -> next, task_t, run_list);
...
prev = context_switch( rq, prev, next);
...
}
其中,sched_find_first_bit()能快速定位優先級最高的非空就緒進程鏈表,運行時間和就緒隊列中的進程數無關,是實現 O(1)調度算法的一個關鍵所在。schedule()的執行流程:
首先,調用 pre_empt_disable(),關閉內核搶佔,因爲此時要對內核的一些重要數據結構進行操作,所以必須將內核搶佔關閉;其次,調用 sched_find_first_bit()找到位圖中的第1個置1的位,該位正好對應於就緒隊列中的最高優先級進程鏈表;再者,調用context_switch()執行進程切換,選擇在最高優先級鏈表中的第1個進程投入運行;詳細過程如圖所示:
圖中的網格爲140位優先級數組,queue[7]爲優先級爲7的就緒進程鏈表。此種算法保證了調度器運行的時間上限,加速了候選進程的定位過程。
時間片的計算方法與時機:
Linux2.4 調度系統在所有就緒進程的時間片都耗完以後在調度器中一次性重新計算,其中重算是用for循環相當耗時。
Linux2.6爲每個CPU保留 active和expired兩個優先級數組,active 數組中包含了有剩餘時間片的任務,expired數組中包含了所有用完時間片的任務。當一個任務的時間片用完了就會重新計算其時間片,並插入到expired隊列中,當 active隊列中所有進程用完時間片時,只需交換指向active和expired隊列的指針即可。此交換是實現O(1)算法的核心,由schedule()中以下程序來實現:
array = rq ->active;
if (unlikely(!array->nr_active)) {
rq -> active = rq -> expired;
rq -> expired = array;
array = rq ->active;
...
}
Linux進程有140個優先級,前100個分配給實時進程,後40個給普通進程使用。
在 Linux2.6 中,仍有三種調度策略:SCHED_OTHER、SCHED_FIFO 和 SCHED_RR。
1.3.3. 普通進程
SCHED_ORHER:普通進程,基於動態優先級進行調度,其動態優先級可以理解爲調度器爲每個進程根據多種因素計算出的權值。
Linux2.6中,優先級prio的計算不再集中在調度器選擇next進程時,而是分散在進程狀態改變的任何時候,這些時機有:
進程被創建時;
休眠進程被喚醒時;
從TASK_INTERRUPTIBLE 狀態中被喚醒的進程被調度時;
因時間片耗盡或時間片過長而分段被剝奪 CPU 時;
在這些情況下,內核都會調用 effective_prio()重新計算進程的動態優先prio並根據計算結果調整它在就緒隊列中的位置。
struct task_struct{
...
int prio,static_prio;
prio 是動態優先級,static_prio 是靜態優先級(與最初nice相關)
...
prio_array_t *array;
記錄當前 CPU 的活躍就緒隊列
unsigned long sleep_avg;
進程的平均等待時間,取值範圍[0,MAX_SLEEP_AVG],初值爲0。
sleep_avg反映了該進程需要運行的緊迫性。進程休眠該值增加,如果進程當前正在運行該值減少。是影響進程優先級最重要的元素。值越大,說明該進程越需要被調度。
...
};
1.3.4. 實時進程
SCHED_FIFO:實時進程,實現一種簡單的先進先出的調度算法。
SCHED_RR:實時進程,基於時間片的SCHED_FIFO,實時輪流調度算法。
SCHED_FIFO與SCHED_RR的區別是:當進程的調度策略爲前者時,當前實時進程將一直佔用CPU直至自動退出,除非有更緊迫的、優先級更高的實時進程需要運行時,它纔會被搶佔CPU;當進程的調度策略爲後者時,它與其它優先級相同的實時進程以實時輪流算法去共同使用CPU,用完時間片放到運行隊列尾部,注意實時進程並不會放入過期隊列中。
雖然在一個CPU內,實時進程的調度方式可以認爲是嚴格優先級的,但是對於SMP系統,每個CPU都有自己的運行隊列,實時進程被分配到各CPU隊列,高優先級的實時進程並不一定比低優先級的先運行。
1.4. 實時性
Linux2.6內核本身就是可搶佔的,具有一定的實時性;而一些實時補丁的出現,更加增強了linux的實時性,達到軟實時的標準,這其中著名的是Ingo's RT patch。
該補丁把中斷(IRQ)和軟中斷(softIRQ)全部線程化並賦予不同的優先級,實時任務可以有比中斷線程更高的優先級;它使用Mutex替代spinlock來使得自旋鎖完全可搶佔;另外分解了內核中鎖的粒度,增加了內核搶佔點,進一步降低了延時。由於中斷已經線程化了,很多中斷關閉就沒必要了,因而消除了很多中斷關閉區域。
爲了能併入主流內核,Ingo Molnar的實時補丁也採用了非常靈活的策略,它支持四種搶佔模式:
1.No Forced Preemption (Server),這種模式等同於沒有使能搶佔選項的標準內核,主要適用於科學計算等服務器環境。
2.Voluntary Kernel Preemption (Desktop),這種模式使能了自願搶佔,但仍然失效搶佔內核選項,它通過增加搶佔點縮減了搶佔延遲,因此適用於一些需要較好的響應性的環境,如桌面環境,當然這種好的響應性是以犧牲一些吞吐率爲代價的。
3.Preemptible Kernel (Low-Latency Desktop),這種模式既包含了自願搶佔,又使能了可搶佔內核選項,因此有很好的響應延遲,實際上在一定程度上已經達到了軟實時性。它主要適用於桌面和一些嵌入式系統,但是吞吐率比模式2更低。
4.Complete Preemption (Real-Time),這種模式使能了所有實時功能,因此完全能夠滿足軟實時需求,它適用於延遲要求爲幾十微秒或稍低的實時系統。