[Linux內核設計與實現]Linux進程調度

進程調度可以看作在可運行態進程之間分配有限處理器時間資源的內核子系統。最大限度利用處理器時間的原則是,只要有可以執行的進程,那麼總會有進程在運行。但是,只要系統中可運行狀態的進程數量大於處理器個數,就會有進程不能運行,這些進程在等待運行。在一組處於可運行狀態的進程中選擇一個來執行,是調度程序所需要完成的基本任務。多任務系統可以分爲兩類:非搶佔式多任務(cooperative multitasking)和搶佔式多任務(preemptive multitasking)。Linux提供搶佔式多任務模式。


策略

策略決定調度程序在何時讓什麼進程運行。調度器的策略往往決定系統的總體印象,並負責優化使用處理器時間。


I/O消耗型和處理器消耗型進程

進程可以分爲I/O消耗型和處理器消耗型。前者指進程的大部分時間用來提交I/O請求或是等待I/O請求。因此這樣的進程經常處於可運行狀態,但通常都是運行短短的一會,因爲它在等等更多的I/O請求時最後總會阻塞(所有I/O操作,比如鍵盤活動,磁盤I/O等)。處理器消耗型進程把大量時間用在執行代碼上,對這類進程,調度策略是儘量降低它們的運行頻率,對他們而言,延長其運行時間會更合適。調度策略通常要在兩個矛盾的目標中間尋求平衡:進程響應迅速(響應時間短)和最大系統利用率(吞吐量)。Linux爲了保證交互式應用,更傾向於優先調度I/O消耗型進程。


進程優先級

調度算法最基本的一類就是基於優先級的調度。這是一種根據進程的價值和對處理器時間的需求來對進程分級的方法。優先級高的進程先運行,低的後運行,相同優先級的進程按論轉方式進程調度。在包括Linux在內的系統中,優先級高的進程使用的時間片(timeslice)也較長。調度程序總是選擇時間片未用完而且優先級高的進程來運行。Linux系統實現了一種基於動態優先級的調度方法。Linux內核提供了兩組獨立的優先級範圍。一種是nice值,範圍從-20到19,默認值是0。nice的值越大優先級越低。第二種是實時優先級,其值是可配置的,默認情況下它的變化範圍是從0到99。任何實時進程的優先級都高於普通進程。Linux提供對POSIX實時優先級的支持。


時間片與內核搶佔

時間片表明進程在被搶佔之前所能持續運行的時間。調度策略必須規定一個默認的時間片,但這並不是一件簡單的事情:時間片過長會導致系統交互的響應欠佳,讓人覺得系統無法併發執行應用程序;時間片過短會明顯增大進程切換帶來的處理器耗時,因爲肯定會有相當一部分系統時間用於進程切換上,而這些進程能夠用來運行的時間片卻很短。Linux系統提高交互式程序的優先級,讓它們運行的更頻繁,於是調度程序提供較長的默認時間片給交互式程序。此外,Linux調度程序還能根據進程的優先級動態調整分配給它的時間片。從而保證優先級高的進程,假定也是重要性高的進程,執行的頻率高,執行時間長。進程並不一定非要一次就消耗完它所有的時間片。當一個進程時間片耗盡時,就認爲進程到期了。沒有時間片的進程不會再投入運行,除非等到其它所有的進程都耗盡了它們的時間片。這時,所有進程的時間片會被重新計算。另外,當進程運行時,如果有其它進程進入TASK_RUNNING狀態,內核就會檢查它的優先級是否高於當前正在執行的進程。如果高於,調度程序就會被喚醒,搶佔當前正在執行的進程並運行新的可運行進程。


Linux調度算法

Linux的調度程序定義在kernel/sched.c中。2.6內核的調度算法實現了以下目標:

充分實現O(1)調度。

全面實現SMP的可擴展性。每個處理器擁有自己的鎖和自己的可執行隊列。

強化SMP親和力。儘量將相關一組的任務分配給一個cpu進行連續的執行。只有在需要平衡任務隊列的大小時才能在cpu之間移動進程。

保證公平。在合理設定的時間範圍內,沒有進程會處於飢餓狀態。同樣的,也沒有進程能夠顯失公平的得到大量時間片。

雖然最常見的優化情況是系統只有1~2個可運行進程,但是優化也完全有能力擴展到具有多處理器且每個處理器上運行多個進程的系統中。


可執行隊列

調度程序中最基本的數據結構是運行隊列(runqueue)。可執行隊列定義於kernel/sched.c中,由結構runqueue表示。可執行隊列是給定處理器上的可執行進程的鏈表,每個處理器一個。每個可投入運行的進程都惟一的歸屬於一個可執行隊列。此外,可執行隊列中還包含每個處理器的調度信息。

struct runqueue {
        spinlock_t          lock;   /* spin lock that protects this runqueue */
        unsigned long       nr_running;         /* number of runnable tasks */
        unsigned long       nr_switches;        /* context switch count */
        unsigned long       expired_timestamp;    /* time of last array swap */
        unsigned long       nr_uninterruptible;   /* uninterruptible tasks */
        unsigned long long  timestamp_last_tick;  /* last scheduler tick */
        struct task_struct  *curr;                /* currently running task */
        struct task_struct  *idle;           /* this processor's idle task */
        struct mm_struct    *prev_mm;        /* mm_struct of last ran task */
        struct prio_array   *active;         /* active priority array */
        struct prio_array   *expired;        /* the expired priority array */
        struct prio_array   arrays[2];       /* the actual priority arrays */
        struct task_struct  *migration_thread; /* migration thread */
        struct list_head    migration_queue;   /* migration queue*/
        atomic_t            nr_iowait; /* number of tasks waiting on I/O */
};


對可運行隊列進程操作時,要首先鎖定隊列,然後操作隊列,最後釋放鎖。當對多個運行隊列進行操作時要注意鎖的順序,否則可能會造成死鎖。

優先級數組


每個運行隊列都有兩個優先級數組,一個活躍的,一個過期的。優先級數組在kernel/sched.c中定義,它是prio_array類型的結構體。優先級數組是一種能夠提供O(1)級算法複雜度的數據結構體。優先級數組使可運行處理器的每一種優先級都包含一個相應的隊列,而這些隊列包含對應優先級上的可執行進程鏈表。優先級數組還擁有一個優先級位圖,當需要查找當前系統內擁有最高優先級的可執行進程時,它可以幫助提高效率。

struct prio_array {
        int               nr_active;         /* number of tasks in the queues */
        unsigned long     bitmap[BITMAP_SIZE];  /* priority bitmap */
        struct list_head  queue[MAX_PRIO];      /* priority queues */
};

MAX_PRIO定義了系統擁有的優先級個數,默認值是140。每個優先級數組都包含一個位圖成員,每個優先級準備一位。一開始所有的位都被置爲0,當某個擁有一定優先級的進程開始準備執行時(進程狀態變爲TASK_RUNNING),位圖中相應的位就會被置爲1。每個優先級數組還包含一個struct list_head的隊列,其中每個元素都是一個struct list_head類型的鏈表。每個鏈表與一個給定的優先級對應,包含該處理器隊列上相應優先級的全部可運行進程。

重新計算時間片


在決定新的時間片長短時會用到進程的優先級和其它一些屬性。這種實現有一些缺點:

  • 可能會耗費相當長的時間。最壞情況下,有n個進程的系統複雜度可能達到O(n)。
  • 重算時必須靠鎖的形式來保護運行隊列和每個進程描述符。這樣做可能加劇對鎖的爭用。
  • 重新計算時間片時間是不確定的,這會給時間確定性要求很高的實時進程帶來麻煩。
  • 實現很粗糙

Linux調度程序減少對循環的依賴,它爲每個處理器維護兩個優先級數組:活動數組和過期數組。活動數組內的可執行隊列上的進程都有時間片剩餘;過期數組內的可執行隊列上的進程都耗盡了時間片。當一個進程的時間片耗盡時,它會被移到過期數字中。重新計算時間片就發生在進程在活動數組和過期數組之間切換時。


進程時間片大小是根據進程優先級來決定的。進程優先級包括兩部分:靜態優先級(nice值)和動態優先級(根據是I/O消耗型還是處理器消耗型)。計算時間片時,以靜態優先級爲基礎,通過計算進程動態優先級來最終決定進程時間片大小。舉例來說,如果一個進程的初始時間片大小爲100ms,當該進程交互性很強時,進程就會獲得時間獎勵,最終獲得的時間片就會大於100ms。反之,如果進程交互性很弱,那麼就會得到懲罰,進程最終獲得的時間片會小於100ms。

調度程序還提供了另外一種機制來支持交互進程:如果一個進程的交互性非常強,那麼當它的時間片用完之後,它會被再次放置到活動數組而不是過期數組中。這樣可以避免交互性強的進程處於過期數組,當它需要交互時,卻不能執行。對於處於飢餓狀態的進程,它也會被放置在活動數組中,避免其進一步飢餓。


睡眠和喚醒

休眠(被阻塞)狀態的進程處於一個特殊的不可執行狀態。休眠通過等待隊列進程處理。等待隊列是由等待某些事件發生的進程組成的簡單鏈表。

Figure 4.3. Sleeping and waking up.

負載平衡程序

負載平衡程序由kernel/sched.c中的函數load_balance()來實現。它有兩種調用方法:在schedule()執行的時候,只要當前的可執行隊列爲空,它就會被調用;定時器調用,系統空閒1ms時調用一次或者其它情況下每隔200ms調用一次。單處理器系統中該函數不會被調用,因爲系統只有一個可執行隊列。

load_balance()函數執行過程如下:

  • 首先,load_balance()調用find_busiest_queue(),找到最繁忙的可執行隊列。
  • 其次,load_balance()從最繁忙的運行隊列中選擇一些優先級數組以便抽取進程。最好是過期數組,因爲這些進程已經有一段時間沒有運行。
  • 接着,load_balance()尋找到含有進程並且優先級最高的鏈表,因爲把優先級高的進程平均分散開來纔是最重要的。
  • 分析找到的所有這些優先級相同的進程,選擇一個不是正在,執行,也不會因爲處理器相關性而不可移動,並且不在高速緩存中的進程。
  • 只要執行隊列之間仍然不均衡,就重複上面步驟,繼續從繁忙隊列中抽取進程到當前隊列。


搶佔和上下文切換

上下文切換,也就是從一個可執行進程切換到另外一個可執行進程。由定義在kernel/sched.c中的context_switch()函數負責處理。每當一個新的進程被選出來準備投入運行的時候,schedule()就會調用該函數。它完成了兩項基本的工作:

  • 調用定義在<asm/mmu_context.h>中的switch_mm(),該函數負責把虛擬內存從上一個進程映射切換到新進程中。
  • 調用定義在<asm/system.h>中的switch_to(),該函數負責從上一個進程的處理器狀態切換到新進程的處理器狀態。這包括保存、恢復桟信息和寄存器信息。

用戶搶佔

內核即將返回用戶空間的時候,如果need_resched標誌,會導致schedule()被調用,此時就會發生用戶搶佔。在內核返回用戶空間的時候,它知道自己是安全的,因爲既然它可以繼續去執行當前的進程,那麼它當然可以再選擇一個新的進程去執行。所以,內核無論是在從中斷處理程序還是在系統調用後返回,都會檢查need_resched標誌。如果它被設置了,那麼內核會選擇一個其它進程投入運行。

用戶搶佔在以下情況時發生:

  • 從系統調用返回用戶空間。
  • 從中斷處理程序返回用戶空間。

內核搶佔

Linux完整的支持內核搶佔。如果沒有持有鎖,內涵就可以進行搶佔(重新調度是安安的)。爲了支持內核搶佔所做的第一處變動就是爲每個進程的thread_info引入preempt_count計數器。該計數器初始值爲0,每當使用鎖的時候數值加1,釋放鎖的時候數值減1。如果need_resched被設置,並且preempt_count爲0,這說明有一個更爲重要的任務需要執行並可以安全的搶佔。如果preempt_count不爲0,說明當前任務持有鎖,所以搶佔是不安全的。如果內核中的進程被阻塞了,或它顯式的調用了schedule(),內核搶佔也會顯式的發生。

內核搶佔會發生在:

  • 當從中斷處理程序正在執行,且返回內核空間之前。
  • 當內核代碼再一次具有可搶佔性的時候。
  • 如果內核中的任務顯式的調用了schedule()。
  • 如果內核中的任務阻塞。

實時


Linux提供了兩種實時調度策略:SCHED_FIFO和SCHED_RR。而普通的、非實時的調度策略是SCHED_NORMAL。SCHED_FIFO是一種簡單的、先入先出的調度算法,它不使用時間片。SCHED_FIFO級的進程會比任何SCHED_NORMAL級的進程都先得到調度。一旦一個SCHED_FIFO級進程處於可執行狀態,它就會一直執行下去,直到它自己受阻塞或顯式的釋放處理器爲止。只有較高優先級的SCHED_FIFO或者SCHED_RR任務才能搶佔SCHED_FIFO任務。如果有兩個或者更多SCHED_FIFO級進程,它們會輪流執行。只要有SCHED_FIFO級進程在執行,其它級別較低的進程就只能等待它結束之後纔會有機會執行。

SCHED_RR與SCHED_FIFO大體相同,只是SCHED_RR級的進程在耗盡事先分配給它的時間之後就不能再繼續執行。SCHED_RR是帶有時間片的SCHED_FIFO--一種實時輪流調度算法。對於SCHED_FIFO進程,高優先級的進程總是立即搶佔低優先級進程,但低優先級進程決不能搶佔SCHED_RR任務。


Linux的實時調度算法提供了一種軟實時工作方式。軟實時的含義是,內核調度進程,盡力使進程在它的限定時間到來前運行,但內核不保證總能滿足這些進程的要求。相反,硬實時系統保證在一定條件下,可以滿足任何調度的要求。Linux對實時任務的調度不做任何保證。雖然不能保證硬實時工作方式,但Linux的實時調度算法的性能還是很不錯的。


參考資料:

《Linux內核設計與實現》,第二版



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