第四章 進程調度
4.1 多任務
1、多任務操作系統就是能同時並發的交互執行多個進程的操作系統。
2、多任務操作系統使多個進程處於堵塞或者睡眠狀態,實際不被投入執行,這些任務盡管位於內存,但是並不處於可運行狀態。
3、多任務系統分類:
(1)非搶占式多任務
(2)搶占式多任務
4、Linux提供了搶占式的多任務模式。在此模式下,由調度程序來決定什么時候停止一個進程的運行,以便其他進程能夠得到執行機會。這個強制的掛起動作叫做搶占。進程被搶占之前能夠運行的時間是預先設置好的,叫進程的時間片。時間片實際上就是分配給每個可運行進程的處理器時間段。
5、在非搶占式多任務模式下,除非進程自己主動停止運行,否則它會一直執行。進程主動掛起自己的操作稱為讓步。但這種機制有很多缺點:調度程序無法躲每個進程該執行多長時間作出統一規定,所以進程獨占的處理器時間可能會超過用戶的預料:更糟的是,一個絕不做出讓步的懸掛進程就能使系統崩潰。
4.2Linux的進程調度
1、O(1)調度器擁有數以十計的多處理器的環境,但缺少交互進程。
2、反轉樓梯最后期限調度算法(RSDL),吸取了隊列理論,公平調度。又被稱為完美公平調度算法(CFS)。
4.3策略
1、決定調度程序在何時讓進程運行。
4.3.1 I/O消耗型和處理器消耗型的進程
1、進程可以分為:
(1)I/O消耗型:進程的大部分時間用來提交I/O請求或者等待I/O請求,經常處於可運行狀態但是運行時間很短,等待更多的請求時最后總會阻塞。
(2)處理器消耗型:把時間大多用在執行代碼上,除非被搶占,否則通常都會不停地運行。因為它們沒有太多的I/O需求。不屬於I/O驅動類型。
2、調度策略:盡量降低它們的調度頻率,延長其運行時間。
3、調度策略通常要在兩個矛盾的目標中間尋找平衡:
(1)進程調度迅速(響應時間短)
(2)最大系統利用率(高吞吐量)
4、Linux傾向於優先調度I/O消耗型進程。
4.3.2 進程優先級
1、調度算法中最基本的一類就是基於優先級的調度,這是一種根據進程的價值和其對處理器時間的需求來對進程分級的想法。
2、調度程序總是選擇時間片未用盡而且優先級最高的進程運行。
3、 Linux采用了兩種不同的優先級范圍:
(1)nice
范圍[-20,19],默認值為0;nice值越大,優先級越低;
Linux系統中nice值代表時間片的比例,可以通過ps-el命令查看系統中進程列表,結果中標記NI的一列及時進程對應的nice值。
(2)實時優先級
其值可以配置,默認變化范圍是[0,99];值越高優先級越高;
4、任何實時進程的優先級都高於普通的進程,也就是說實時優先級和nice優先級處於互不相交的兩個范疇。
5、通過命令ps-eo state,uid,pid,ppid,rtprio,time,comm.查看系統中的進程列表以及對應的實時優先級(位於RTPRIO列下),其中如果有進程對應列顯示“-”則說明它不是實時進程。
4.3.3 時間片
1、時間片表示進程在被搶占前所能持續運行的時間。
2、I/O消耗型進程不需要很長的時間片,而處理器消耗型進程希望時間片越長越好。
3、Linux的CFS調度器沒有直接分配時間片到進程,而是將處理器的使用比划分給進程。這樣一來,進程所獲得的處理器時間和系統負載密切相關。這個比例受nice值影響,nice值作為權重來調整進程所使用的處理器時間使用比。
4、Linux系統是搶占式的,是否要將一個進程立刻投入運行(也就是搶占當前進程),是完全由進程的優先級和是否有時間片來決定。
5、CFS調度器:搶占時機取決於新的可執行程序消耗了多少處理器使用比,如果消耗的使用比當前進程小:新程序立刻投入運行,搶占當前進程,否則推遲。
4.3.4 調度策略的活動
1、文字編輯程序顯然是 1/0 消耗型的,因為它大部分時間都在等待用戶的鍵盤輸入(無論用戶的輸入速度有多快,都不可能趕上處理的速度)用戶總是希望按下鍵系統就能馬上響應。
2、視頻編碼程序是處理器消耗型的。
3、CFS總是會毫不猶豫地讓文本編輯器在需要時被投入運行,而讓視頻處理程序只能在剩下的時刻運行。
4.4 Linux調度算法
4.4.1 調度器類
1、Linux調度器是以模塊方式提供,目的是允許不同類型的進程可以有針對性地選擇調度算法。這種模塊化結構被稱為調度器類,它允許多種不同的可動態添加的調度算法並存,調度屬於自己范疇的進程。
2、基礎的調度器代碼定義在kernel/sched.c文件中。
3、每個調度器有一個優先級,會按照優先級順序遍歷調度類,選擇優先級最高的調度器類。
4、完全公平調度CFS是一個針對普通進程的調度類。
4.4.2 Unix系統中的進程調度
1、Unix使用的調度算法是分配絕對的時間片,這樣就會引發固定的切換頻率,不利於公平性。而Linux采用的CFS完全摒棄了時間片,分配給進程一個處理器使用比重,保證恆定的公平性和變動的切換頻率。
4.4.3 公平調度
1、CFS的做怯是允許每個進程運行一段時間、循環輪轉、選擇運行最少的進程作為下一個運行進程,而不再采用分配給每個進程時間片的做法了,在所有可運行進程總數基礎上計算出一個進程應該運行多久。而不是依靠nice 值來計算時間片。
2、nice 值在 CFS 中被作為進程獲得的處理器運行比的權重:越高的nice 值(越低的優先級)進程獲得更低的處理器使用權重。
3、目標延遲:無限小調度周期的近似值
4、最小粒度:每個進程獲得的時間片底線,默認為1ms。
5、任何進程所獲得的處理器時間是由它自己和其他所有可運行進程nice 值的相對差值決定的。
4.5 Linux調度的實現
1、CFS調度算法的實現:
四個組成部分:
(1)時間記賬
(2)進程選擇
(3)調度器入口
(4)睡眠和喚醒
4.5.1 時間記賬
1、所有的調度器都必須對進程運行時間做記賬。
2、CFS 使用調度器實體煩結構(定義在文件<linux/sched.h>的 struct_sched _entity 中)來追蹤進程運行記賬。
3、CFS 使用 vruntime 變量來記錄一個程序到底運行了多長時間以及它還應該再運行多久。
4、定義在kemeVsched_fair.c 文件中的 update_curr()函數實現了該記賬功能。
6、update_ currO 計算了當前進程的執行時間,並且將其存放在變量delta_exec 中。update_ curr()是由系統定時器周期性調用。
4.5.2 進程選擇
1、CFS調度算法的核心:選擇具有最小vruntime的任務。
2、CFS使用紅黑樹來組織可運行進程隊列,並利用其迅速找到最小vruntime值的進程。
3、Linux中,紅黑樹被稱為rbtree,是一個自平衡二叉搜索樹,是一種以樹節點形式存儲的數據,這些數據會對應一個鍵值,可以通過這些鍵值來快速檢索節點上的數據,(重要的是,通過鍵值檢索到對應節點的速度與整個樹的節點規模成指數比關系)。
(1)挑選下一個任務
CFS的進程選擇算法簡單總結為“運行rbtree樹種最左邊葉子節點所代表的那個進程”。實現這一過程的函數是__pick_next_entity()。
__pick_next_entity()函數本身不會遍歷樹找到最左葉子節點,該值緩存在rb_leftmost字段中,函數返回值就是CFS選擇的下一個運行進程。如果返回NULL,表示樹空,沒有可運行進程,這時選擇idle任務運行。
(2)向樹中加入進程
發生在進程被喚醒或者通過fork()調用第一次創建進程時。
函數enqueue_entity():更新運行時間和其他一些統計數據,然后調用__enqueue_entity()。進行繁重的插入工作,把數據項真正插入到紅黑樹中:
link為null時循環終止,退出。
在父節點上調用rb_link_node(),使新插入的進程成為其子節點。
函數rb_insert_color()更新樹的自平衡相關特性。
(3)從樹中刪除進程
刪除動作發生在進程堵塞或終止時。
相關函數是dequeue_entity()和__dequeue_entity():
rb_erase()函數刪除進程
更新rb_leftmost緩存
如果刪除的是最左節點,還要調用rb_next()按順序遍歷,找到新的最左節點。
4.5.3 調度器入口
1、進程調度的主要入口點函數是schedule()。它定義在文件kemel/sched.c中。
它會調用pick_next_task();pick_next_task()會以優先級為序,從高到低依次檢查每一個調度類,並且從最高優先級的調度類中選擇最高優先級的進程。pick_next_task()會返回指向下一個可運行進程的指針,沒有時返回NULL。
2、pick_next_task()函數實現會調用pick_next_entity(),而該函數會調用__pick_next_entity()函數。
4.5.4 睡眠和喚醒
1、休眠(被阻塞)的進程處於一個特殊的不可執行狀態。
2、進程休眠有很多原因,但肯定都是為了等待一些事件。
3、休眠的一個常見的原因就是文件I/O——如進程對一個文件執行了read()操作,而這需要從磁盤里讀取。
4、內核的操作都相同:進程把自己標記成休眠狀態,從可執行紅黑樹中移出,放入等待序列,然后調用schedule()選擇和執行一個其他進程
5、喚醒的過程:進程被設置為可執行狀態,然后再從等待隊列中移到可執行紅黑樹中。
6、休眠有兩種相關的進程狀態:
(1)TASK_INTERRUPTIBLE
(2)TASK_UNINTERRUPTIBLE
唯一區別是處於TASK_UNINTERRUPTIBLE的進程會忽略信號,而處於TASK_INTERRUPTIBLE狀態的進程如果接收到一個信號,會被提前喚醒並響應該信號
7、等待隊列
等待隊列是由等待某些事件發生的進程組成的簡單鏈表
休眠通過等待隊列進行處理。
內核用wake_queue_head_t來表示等待隊列。
等待隊列可以通過DECLARE_WAITQUEUE()靜態創建,也可以由init_waitqueue_head()動態創建。
8、進程通過執行以下幾個步驟將自己加入到一個等待隊列中:
1)調用宏DEFINE_WAIT()創建一個等待隊列的選項。
2)調用add_wait_queue()把自己加入到隊列中。
3)調用prepare_to_wait()方法將進程的狀態變更為TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。
4)如果狀態被設置成TASK_INTERRUPTIBLE,則信號喚醒進程。
5)當進程被喚醒的時候,會再次檢查條件是否為真,真則退出循環,否則再次調用schedule()並且一直重復這步動作。
6)當條件滿足后,進程將自己設置為TASK_RUNNING並調用finish_wait()方法把自己移出等待序列。
函數inotify_read():負責從通知文件描述符中讀取信息。
9、喚醒操作通過函數wake_up()進行,它會喚醒指定的等待隊列上的所有進程。它調用try_to_wake_up(),該函數負責將進程設置成TASK_RUNNING狀態,調用enqueue_task()將此進程放入紅黑樹中,如果被喚醒的進程優先級比正在執行的進程優先級高,設置need_resched標志。通常哪段代碼促成等待條件達成,它就負責隨后調用wake_up()函數。
10、關於休眠有一點需要注意,存在虛假的喚醒。有時候進程被喚醒並不是因為它所等待的條件達成了才需要用一個循環處理來保證它等待的條件真正達成。
4.6 搶占和上下文切換
1、上下文切換,也就是從一個可執行進程切換到另一個可執行進程,由定義在 kernel/ sched.c 中的 context_switch()函數負責處理。
2、每當一個新的進程被選出來准備投入運行的時候, schedule()就會調用該函數。它完成了兩項基本的工作:
(1)調用聲明在 <asm/mmu_ context.h>中的 switch_mm(),該函數負責把虛擬內存從上一個進程映射到新進程中。
(2)調用聲明在 <asm/system.h> 中的 switch_to(),該函數負責從上一個進程的處理器狀態切換到新進程的處理器狀態。這包括保存、恢復棧信息和寄存器信息,還有其他任何與體系結構相關的狀態信息,都必須以每個進程為對象進行管理和保存。
3、內核提供了一個need_resched標志來表明是否需要重新執行一次調度。當某個進程應該被搶占時,scheduler_tick()會設置這個標志:當一個優先級高的進程進入可執行狀態時,try_to_wake_up()會設置這個標志。內核檢查這個標志確認其被設置,調用schedule()來切換到一個新的進程。該標志對於內核來說是一個信息,表示youqitajinc應當被運行了,要盡快調用調度程序。再返回用戶空間以及從中斷返回時,內核也會檢查標志。每個進程都包含一個need_resched標志,因為訪問進程描述符里的數值比訪問一個全局變量要快。
4.6.1 用戶搶占
1、內核即將返回用戶空間的時候,如果need_resched標志被設置,會導致schedule()被調用,此時會發生用戶搶占。
2、用戶搶占在以下情況時產生:
(1)從系統調返回用戶空間時;
(2)從中斷處理程序返回用戶空間時;
4.6.2 內核搶占
1、Linux完整地支持內核搶占。
2、只要重新調度是安全的,內核就可以在任何時間搶占正在執行的任務。
3、內核搶占會發生在:
(1)中斷處理程序正在執行,且返回內核空間之前
(2)內核代碼再一次具有可搶占性的時候。
(3)如果內核中的任務顯式地調用 schedule()
(4)如果內核中的任務阻塞(這同樣也會導致調用schedule())。
4.7 實時調度策略
1、Linux提供了兩種實時調度策略:SCHED_FIFO和 SCHED_RR。而普通的、非實時的調度策略是SCHED_NORMAL。
2、SCHED_FIFO 實現了一種簡單的、先入先出的調度算法。
3、SCHED_RR 是帶有時鬧片的 SCHED_FIFO,一種實時輪流調度算法。
4、這兩種實時算法實現的都是靜態優先級。內核不為實時進程計算動態優先級,這能保證給定優先級別的實時進程總能搶占優先級比它低的進程。
5、軟實時:內核調度進程,盡力使進程在它的限定時間到來前進行,但內核不保證總能滿足這些進程的要求。
6、硬實時:系統保證在一定條件下,可以滿足任何調度的要求。
7、優先級范圍
(1)實時:
范圍:0~[MAX_RT_PRIO-1]。
默認MAX_RT_PRIO=100,所以默認實時優先級范圍為[0,99]
(2)SCHED_NORMAL:
范圍: [MAX_RT_PRIO]~[MAX_RT_PRIO+40]
默認情況下,nice值從-20到+19對應的是從100到139的實時優先級范圍。
4.8 與調度相關的系統調用
4.8.1 與調度策略和優先級相關的系統調用
1、sched_setparam()和sched__getparam()分別用於設置和獲取進程的實時優先級
2、nice()函數可以將給定進程的靜態優先級增加一個給定的量,只有超級用戶才能在調用它時使用負值,從而提高進程的優先級。
3、nice()函數會調用內核的 set_ user_ nice()函數,這個函數會設置進程的 task_struct 的 static_prio 和prio 值。
4.8.2 與處理器綁定有關的系統調用
1、Linux調度程序提供強制的處理器綁定機制。也就是說,雖然它盡力通過一種軟的親和性試圖使進程盡量在同一個處理器上運行,但它也允許用戶強制指定“這個進程無論如何都必須在這些處理器上運行”。 這種強制的親和性保存在進程task_struct的cpus_allowed這個位掩碼標志中。
2、進程只運行在指定處理器上,對處理器的指定是由該進程描述符的 cpus_allowed 域設置的。
4.8.3 放棄處理器時間
1、Linux通過 sched_yieldO 系統調用,提供了一種讓進程顯式地將處理器時間讓給其他等待執行進程的機制。
2、內核代碼為了方便,可以直接調用yield(),先要確定給定進程確實處於可執行狀態,然后再調用 sched__yield()。
3、用戶空間的應用程序直接使用 sched__yield()系統調用就可以了。