Linux2.4內核進程調度的缺陷: Linux2.4 內核的進程調度採用時間片輪轉和優先級相結合的調度策略,但存在以下幾個致命缺陷: 1>調度算法時間複雜度是 O(n)。2.4 內核每次調度都要進行一次循環,耗時與當前就緒 進程數有關,因此達不到實時性的要求;時間片重算時必須給 task_struct 結構和就緒進程隊列上鎖. 2>不提供搶佔式調度, 會導致大量的競爭,使就緒隊列成爲一個明顯的瓶頸; 3>在 SMP 系統中,只有一個就緒隊列,這將導致大部分的 CPU 處於空閒狀態,從而影響 SMP 的效率; Linux2.6 內核進程調度分析 進程的調度時機與引起進程調度的原因和進程調度的方式有關。在 2.6 中,除核心應用 主動調用調度器之外, 核心還在應用不完全感知的情況下在以下三種時機中啓動調度器工作: 1>從中斷或系統調用返回到用戶態; 2>某個進程允許被搶佔 CPU; 3>主動進入休眠狀態; 調度策略: 在 Linux2.6 中,仍有三種調度策略: SCHED_OTHER、SCHED_FIFO 和 SCHED_RR。 SCHED_ORHER:普通進程,基於優先級進行調度。 SCHED_FIFO:實時進程,實現一種簡單的先進先出的調度算法。 SCHED_RR:實時進程,基於時間片的SCHED_FIFO,實時輪流調度算法。 前者是普通進程調度策略,後兩者都是實時進程調度策略。 SCHED_FIFO 與 SCHED_RR 的區別是: 當進程的調度策略爲前者時,當前實時進程將一直佔用 CPU 直至自動退出,除非有更緊迫的、 優先級更高的實時進程需要運行時,它纔會被搶佔 CPU;當進程的調度策略 爲後者時,它與其它實時進程以實時輪流算法去共同使用 CPU,用完時間片放到運行隊列尾部。
注:實時進程的優先級高於普通進程,後面介紹。
O(1)調度器是以進程的動態優先級 prio爲調度依據的,它總是選擇目前就緒隊列中優先 級最高的進程作爲候選進程 next。由於實時進程的優先級總是比普通進程的優先級高,故能 保證實時進程總是比普通進程先被調度。 Linux2.6 中,優先級 prio 的計算不再集中在調度器選擇 next 進程時,而是分散在進程 狀態改變的任何時候,這些時機有: 1>進程被創建時; 2>休眠進程被喚醒時; 3>從TASK_INTERRUPTIBLE 狀態中被喚醒的進程被調度時; 4>因時間片耗盡或時間片過長而分段被剝奪 CPU 時; 在這些情況下,內核都會調用 effective_prio()重新計算進程的動態優先級 prio並根據計算結果調整它在就緒隊列中的位置。 調度算法: O(1)調度器的重要數據結構 (1)就緒隊列 struct runqueue runqueue 的設計是 O(1)調度器的關鍵技術所在,它用於存放特定 CPU 上的就緒進程隊 列信息,其中包含每個 CPU 的調度信息。該結構在 /kernel/sched.c 中的定義如下: struct runqueue { ... prio_array_t *active, *expired, array[2]; active 是指向活動進程隊列的指針 expired 是指向過期進程隊列的指針 array[2]是實際的優先級進程隊列,其中一個是活躍的一個是過期的,過期數組存放時間片耗完的進程 ... } 在 2.6 中,每個 CPU 單獨維護一個就緒隊列,每個就緒隊列都有一個自旋鎖,從而解 決了 2.4 中因只有一個就緒隊列而造成的瓶頸。 (2)task_struct 結構 Linux2.6 內核使用 task_struct 結構來表示進程。2.6 對 task_struct 也做了較大的改動, 該結構定義在/include/linux/sched.h 中: 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 反映了該進程需要運行的緊迫性。進程休眠該值增加,如果進程當前正在運行該值減少。 是影響進程優先級最重要的元素。值越大,說明該進程越需要被調度。 ... }; (3)優先級數組 每個處理器的就緒隊列都有兩個優先級數組,它們是 prio_array 類型的結構體。Linux2.6 內核正是因爲使用了優先級數組,才實現了 O(1)調度算法。該結構定義在 kernel/sched.c 中: struct prio_array{ ... unsigned int nr_active; /**相應 runqueue 中的進程數 unsigned long bitmap[BITMAP_SIZE]; /**索引位圖,BITMAP_SIZE 默認值爲 5,5個long(32位)類型,每位代表一個優先級,可以代表160個優先級,但實際中只有140。 與下面的queue[]對應。 分佈0-99對應爲實時進程,100-140對應爲普通的進程 struct list_head queue[MAX_PRIO]; /**每個優先級的進程隊列,MAX_PRIO 是系統允許的最大優先級數,默認值爲 140,數值越小優先級越高 bitmap每一位都與 queue[i]相對應,當 queue[i]的進程隊列不爲空時,bitmap 相應位爲 1,否則就爲 0。 }
O(1)調度算法實現的簡單介紹 (1)選擇並運行候選進程 next它確定下一個應該佔有 CPU 並運行的進程, 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個進程投入運行; 詳細過程如圖 1 所示:
圖 1 圖中的網格爲 140 位優先級數組,queue[7]爲優先級爲 7 的就緒進程鏈表。 此種算法保證了調度器運行的時間上限,加速了候選進程的定位過程。 (2)時間片的計算方法與時機 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內核設計與實現》 《深入理解linux內核》 其中例子幾乎書上都有,如有錯誤歡迎指正。
|
|