進程調度:
在可運行態進程之間分配有限處理器時間資源的內核子系統。
一 調度策略
1 進程類型
I/O消耗型進程:大部分時間用來提交I/O請求或是等待I/O請求,經常處於可運行狀態,但運行時間短,等待請求過程時處於阻塞狀態。如交互式程序。
處理器消耗型進程:時間大都用在執行代碼上,除非被搶占否則一直不停的運行。
綜合型:既是I/O消耗型又是處理器消耗型。
調度策略要在:進程響應迅速(響應時間短)和最大系統利用率(高吞吐量)之間尋找平衡。
2 調度概念
優先級:基於進程價值和對處理器時間需求進行進程分級的調度。
時間片:表明進程被搶占前所能持續運行的時間,規定一個默認的時間片。時間片過長導致系統交互性的響應不好,
程序並行性效果差;時間片太短增大進程切換帶來的處理器耗時。矛盾!
時間片耗盡進程運行到期,暫時不可運行狀態。直到所有進程時間片都耗盡,重新計算進程時間片。
Linux調度程序提高交互式程序優先級,提供較長時間片;實現動態調整優先級和時間片長度機制。
進程搶占:Linux系統是搶占式,始終運行優先級高的進程。
3 調度算法
可執行隊列:runqueue;給定處理器上可執行進程的鏈表,每個處理器一個。每個可執行進程都唯一歸屬於一個可執行隊列。
運行隊列是調度程序中最基本的數據結構:
struct runqueue { spinlock_t lock; /* 保護運行隊列的自旋鎖*/ unsigned long nr_running; /* 可運行任務數目*/ unsigned long nr_switches; /* 上下文切換數目*/ unsigned long expired_timestamp; /* 隊列最后被換出時間*/ unsigned long nr_uninterruptible; /* 處於不可中斷睡眠狀態的任務數目*/ unsigned long long timestamp_last_tick; /* 最后一個調度程序的節拍*/ struct task_struct *curr; /* 當前運行任務*/ struct task_struct *idle; /* 該處理器的空任務*/ struct mm_struct *prev_mm; /* 最后運行任務的mm_struct結構體*/ struct prio_array *active; /* 活動優先級隊列*/ atomic_t nr_iowait; /* 等待I/O操作的任務數目*/ …… };
提供了一組宏來獲取給定CPU的進程執行隊列:
#define cpu_rq(cpu) //返回給定處理器可執行隊列的指針
#define this_rq() //返回當前處理器的可執行隊列
#define task_rq(p) //返回給定任務所在的隊列指針
在操作處理器任務隊列時候要用鎖:
__task_rq_lock
……
__task_rq_unlock
4 schedule
系統要選定下一個執行的進程通過調用schedule函數完成。
調度時機:
l 進程狀態轉換的時刻:進程終止、進程睡眠;
l 當前進程的時間片用完時(current->counter=0);
l 設備驅動程序調用;
l 進程從中斷、異常及系統調用返回到用戶態時;
睡眠和喚醒:
休眠(被阻塞)的進程處於一個特殊的不可執行狀態。休眠有兩種進程狀態:
TASK_INTERRUPTIBLE:接收到信號就被喚醒
TASK_UNINTERRUPTIBLE:忽略信號
兩種狀態進程位於同一個等待隊列上,等待某些事件,不能夠運行。
進程休眠策略:
//q是我們希望睡眠的等待隊列 DECLARE_WAITQUEUE(wait, current); add_wait_queue(q, &wait); //condition 是我們在等待的事件
while (!condition) { //將進程狀態設為不可執行休眠狀態 or TASK_UNINTERRUPTIBLE set_current_state(TASK_INTERRUPTIBLE); if(signal_pending(current)) //調度進程 schedule(); } //進程被喚醒條件滿足 進程可執行狀態 set_current_state(TASK_RUNNING); //將進程等待隊列中移除 remove_wait_queue(q, &wait);
進程通過執行下面幾個步驟將自己加入到一個等待隊列中:
1) 調用DECLARE_WAITQUEUE()創建一個等待隊列的項。
2) 調用add_wait_queue()把自己加入到隊列中。該隊列會在進程等待的條件滿足時喚醒它。
當然我們必須在其他地方撰寫相關代碼,在事件發生時,對等待隊列執行wake_up()操作。
3) 將進程的狀態變更為 TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。
4) 如果狀態被置為TASK_INTERRUPTIBLE,則信號喚醒進程。這就是所謂的偽喚醒(喚醒不是因為事件的發生),因此檢查並處理信號。
5) 檢查條件是否為真;如果是的話,就沒必要休眠了。如果條件不為真,調用schedule()。
6) 當進程被喚醒的時候,它會再次檢查條件是否為真。如果是,它就退出循環,如果不是,它再次調用schedule()並一直重復這步操作。
7) 當條件滿足后,進程將自己設置為TASK_RUNNING並調用remove_wait_queue()把自己移出等待隊列。
二 搶占和上下文切換
進程切換schedule函數調用context_switch()函數完成以下工作:
l 調用定義在<asm/mmu_context.h>中的switch_mm(),該函數負責把虛擬內存從上一個進程映射切換到新進程中。
l 調用定義在<asm/system.h>中的switch_to(),該函數負責從上一個進程的處理器狀態切換到新進程的處理器狀態。
這包括保存、恢復棧信息和寄存器信息。在前面看到schedule函數調用有很多種情況,完全依靠用戶來調用不能達到
很好的效果。內核需要判斷什么時候調用schedule,內核提供了一個need_resched標志來表明是否需要重新執行一次調度:
l 當某個進程耗盡它的時間片時,scheduler_tick()就會設置這個標志;
l 當一個優先級高的進程進入可執行狀態的時候,try_to_wake_up()也會設置這個標志。
每個進程都包含一個need_resched標志,這是因為訪問進程描述符內的數值要比訪問一個全局變量快
(因為current宏速度很快並且描述符通常都在高速緩存中)。
1 用戶搶占
內核即將返回用戶空間時候,如果need_resched標志被設置,會導致schedule函數被調用,此時發生用戶搶占。
用戶搶占在以下情況時產生:
l 從系統調返回用戶空間。
l 從中斷處理程序返回用戶空間。
2 內核搶占
只要重新調度是安全的,那么內核就可以在任何時間搶占正在執行的任務。
什么時候重新調度才是安全的呢?只要沒有持有鎖,內核就可以進行搶占。鎖是非搶占區域的標志。由於內核是支持SMP的,
所以,如果沒有持有鎖,那么正在執行的代碼就是可重新導入的,也就是可以搶占的。
為了支持內核搶占所作的第一處變動就是為每個進程的thread_info引入了preempt_count計數器。該計數器初始值為0,
每當使用鎖的時候數值加1,釋放鎖的時候數值減1。當數值為0的時候,內核就可執行搶占。從中斷返回內核空間的時候,
內核會檢查need_resched和preempt_count的值。如果need_resched被設置,並且preempt_count為0的話,這說明
有一個更為重要的任務需要執行並且可以安全地搶占,此時,調度程序就會被調用。
內核搶占會發生在:
l 當從中斷處理程序正在執行,且返回內核空間之前。
l 當內核代碼再一次具有可搶占性的時候。
l 如果內核中的任務顯式的調用schedule()。
l 如果內核中的任務阻塞(這同樣也會導致調用schedule())。