進程調度程序是在可運行態進程之間分配有限的處理器時間資源的內核子系統。
調度程序完成的基本工作是,在一組處於可運行狀態的進程中選擇一個來執行。
1、多任務
多任務操作系統是能同時併發地交互執行多個進程的操作系統。
多任務系統分爲兩類:非搶佔式多任務和搶佔式多任務。
搶佔式多任務模式下,由調度程序來決定什麼時候停止一個進程的運行,以便其他進程能夠得到執行機會。這個強制的掛起動作叫做搶佔。
進程在被搶佔之前能夠運行的時間是預先設置好的,叫做進程的時間片。
非搶佔式多任務模式下,除非進程自己主動停止運行,否則它會一直執行。進程主動掛起自己的操作稱爲讓步。
缺點:調度程序無法對每個進程執行多長時間做出統一規定;不做出讓步的懸掛進程能使系統崩潰。
2、Linux的進程調度
在Linux的2.4內核之前,調度程序相當簡陋。
2.5內核採用O(1)調度程序的新調度程序,包括靜態時間片算法和針對每一處理器的運行隊列。但是該算法對於交互進程(響應時間敏感的程序)表現不佳。
2.6內核採用完全公平調度算法(CFS)。其中包含“反轉樓梯最後期限調度算法(RSDL)”,吸取了隊列理論。
3、策略
策略決定調度程序在何時讓什麼進程運行。
3.1 I/O消耗型和處理器消耗型的進程
進程可以被分爲I/O消耗型和處理器消耗型。
I/O消耗型進程的大部分時間用來提交或等待I/O請求,經常處於可運行狀態,但運行時間短。
處理器消耗型進程把時間大多用在執行代碼上,除非被搶佔。調度策略儘量降低它們的調度頻率,延長運行時間。
進程可以同時表現爲兩種類型。
調度策略要在進程響應迅速和最大系統利用率中間尋找平衡。
Unix的調度程序更傾向於I/O消耗型程序,以提供更好的程序響應速度。Linux縮短響應時間,更傾向於優先調度I/O消耗型進程。
3.2 進程優先級
Linux採用了兩種不同的優先級範圍。
nice值
範圍爲從-20到+19,默認值爲0。越大的nice值意味着更低的優先級,第nice值的進程可以獲得更多的處理器時間。Linux系統中,nice值代表時間片的比例。
實時優先級
默認情況下變化範圍是從0到99,越高的數值意味着進程優先級越高。任何實時進程的優先級都高於普通進程。使用命令ps-eo state,uid,pid,ppid,rtprio,time,comm.
查看進程列表以及對應的實時優先級。如果進程對應列顯示-
,說明不是實時進程。
3.3 時間片
時間片是一個數值,表明進程在被搶佔前能持續運行的時間。時間片過長會導致系統對交互的響應表現欠佳,太短會明顯增大進程切換帶來的處理器消耗。
Linux的CFS調度器將處理器的使用比例劃分給了進程。
關於搶佔時機,如果新進程消耗的處理器使用比比當前進程小,則立刻投入運行,搶佔當前進程;否則推遲其運行。
4、調度算法
4.1 調度器類
Linux調度器是以模塊方式提供的,允許不同類型的進程可以有針對性地選擇調度算法。這種模塊化結構稱爲調度器類,允許多種不同的可動態添加的算法並存,調度屬於自己範疇的進程。
每個調度器都有一個優先級,定義在kernel/sched.c文件中,按照優先級遍歷調度類,擁有可執行進程的最高優先級的調度器類勝出。
CFS是針對普通進程的調度類,定義在kernel/sched_fair.c文件中。
4.2 Unix進程調度
Unix系統的nice值問題。
- 若要將nice值映射到時間片,需要將nice單位值對應到處理器的絕對時間。進程切換無法最優化進行。
- 相對nice值。nice值減小1帶來的效果不同。
- 時間片必須是定時器節拍(10ms或1ms)的整倍數。
- 使給定進程打破公平原則,獲得更多處理器時間。
分配絕對的時間片引發的固定的切換頻率,給公平性造成了很大變數。
4.3 公平調度
CFS允許每個進程運行一段時間、循環輪轉、選擇運行最少的進程作爲下一個運行進程,而不再採用分配給每個進程時間片的做法。CFS在所有可運行進程總數上計算出一個進程應該運行多久。
nice值在CFS中被作爲進程獲得的處理器運行比的權重。
CFS爲完美多任務中的無限小調度週期的近似值設立了一個目標,稱作目標延遲。
CFS引入每個進程獲得的時間片底線,稱爲最小粒度。默認值爲1秒。
進程的處理器時間是由它自己和其他所有可運行進程nice值的相對差值決定的。nice值對應的絕對時間不再是一個絕對值,而是處理器的使用比。
5、Linux調度的實現
5.1 時間記賬
Unix系統分配時間片給進程,當系統時鐘節拍發生時,時間片減少一個節拍週期,時間片減少到0時被其他可運行進程搶佔。
5.1.1 調度器實體結構
CFS使用調度器實體結構追蹤進程運行記賬:
struct sched_entity {
struct load_weight load;
struct rb_node run_node;
struct list_head group_node;
unsigned int on_rg;
u64 exec_start;
u64 sum_exec_runtime;
u64 vruntime;
u64 prev_sum_exec_runtime;
u64 last_wakeup;
u64 avg_overlap;
u64 nr_migrations;
u64 start_runtime;
u64 avg_wakeup;
}
在進程描述符中是名爲se的成員變量
5.1.2 虛擬實時
vruntime變量存放進程的虛擬運行時間,單位爲ns。CFS使用vruntime變量記錄一個程序運行了多長時間以及它還應該再運行多久。
kernel/sched_fair.c中的update_curr()函數實現記賬功能。
update_curr()計算當前進程的執行時間,存放在變量delta_exec中,傳遞給__update_curr(),根據當前可運行進程總數對運行時間進行加權計算。最終將權重值與當前運行進程的vruntime相加。
update_curr()由系統定時器週期性調用,所以vruntime可以測量給定進程的運行時間,和下一個被運行的進程。
5.2 進程選擇
CFS調度算法核心:選擇具有最小vruntime的任務。CFS使用紅黑樹組織可運行進程隊列,並利用其迅速找到最小vruntime值的進程。
5.2.1 挑選下一個任務
CFS算法運行rbtree樹中最左邊葉子節點所代表的進程。這一過程通過函數__pick_next_entity()實現。
5.2.2 向樹中加入進程
在進程變爲可運行狀態或者fork()第一次創建進程時,CFS將進程加入rbtree,通過函數enqueue_entity()實現。
enqueue_entity()函數更新運行時間和其他一些統計數據,然後調用__enqueue_entity()進行插入操作。
5.2.3 從樹中刪除進程
刪除動作發生在進程堵塞(變爲不可運行態)或者終止時。通過函數dequeue_entity()實現,調用函數__dequeue_entity()。
5.3 調度器入口
進程調度的主要入口點是函數schedule(),它會調用pick_next_task()依次檢查每一個調度器類,從最高優先級的調度類中選擇最高優先級的進程。
5.4 睡眠和喚醒
休眠(被阻塞)的進程處於一個特殊的不可執行狀態,進程把自己標記成休眠狀態,從可執行紅黑樹中移出,放入等待隊列,然後調用schedule()選擇和執行其他進程。喚醒時,進程被設置爲可執行狀態,從等待隊列中移到可執行紅黑樹中。
5.4.1 等待隊列
等待隊列是由等待某些事件發生的進程組成的簡單鏈表。內核用wake_queue_head_t來代表等待隊列,可以靜態創建也可以動態創建。
加入等待隊列的步驟:
- 調用宏DEFINE_WAIT()創建一個等待隊列的項;
- 調用add_wait_queue()把自己加入到隊列中。事件發生時,對等待隊列執行wake_up()操作;
- 調用prapare_to_wait()將進程狀態變更爲TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE;
- 檢查並處理信號。信號喚醒進程是僞喚醒,不是因爲事件的發生喚醒;
- 進程被喚醒時,再次檢查條件是否爲真,如果不是,再次調用schedule()並重復這步操作;
- 條件滿足後,設置爲TASK_RUNNING並調用finish_wait()將自己移出等待隊列。
inotify_read()函數負責從通知文件描述符中讀取信息。
5.4.2 喚醒
函數wake_up()喚醒指定的等待隊列上的所有進程。調用函數try_to_wake_up()將進程設置爲TASK_RUNNING,調用enqueue_task()將進程放入紅黑樹,被喚醒的進程比當前進程優先級高還要設置need_resched標誌。
6、搶佔和上下文切換
上下文切換是從一個可執行進程切換到另一個可執行進程,由context_switch()函數處理,該函數由schedule()函數調用,完成工作:
- 調用switch_mm(),把虛擬內存從上一個進程映射切換到新進程中。
- 調用switch_to(),從上一個進程的處理器狀態切換到新進程的處理器狀態。包括保存、恢復棧信息和寄存器信息,還有其他任何與體系結構相關的狀態信息。
內核提供need_resched標誌表明是否需要重新執行一次調度。內核檢查該表示確認其被設置(比如返回用戶空間以及從中斷返回時),調用schedule()切換到新的進程。
每個進程都包含一個need_resched標誌。
6.1 用戶搶佔
內核即將返回用戶空間時,如果need_resched標誌被設置,會導致schedule()被調用。此時發生用戶搶佔。
用戶搶佔發生在從系統調用返回用戶空間時,或從中斷處理程序返回用戶空間時。
6.2 內核搶佔
不支持內核搶佔的系統,內核代碼一直執行到完成或明顯的阻塞爲止。
如果沒有持有鎖,正在執行的代碼就是可重新導入的,也就是可以搶佔的。
每個進程的thread_info引入preempt_count計數器。使用鎖的時候加1,釋放鎖的時候減1。當數值爲0時,內核可執行搶佔。從中斷返回內核空間時,內核會檢查need_resched和preempt_count的值。
內核搶佔發生在:
- 中斷處理程序正在執行,且返回內核空間之前;
- 內核代碼再一次具有可搶佔性的時候;
- 內核中的任務顯式地調用schedule();
- 內核中的任務被阻塞,調用schedule();
7、實時調度策略
Linux提供兩種實時調度策略:SCHED_FIFO和SCHED_RR。普通的非實時調度策略是SCHED_NORMAL。
實時策略不被完全公平調度器管理,而是被一個特殊的實時調度器管理。
7.1 SCHED_FIFO
SCHED_FIFO實現了一種簡單的、先入先出的調度算法,不使用時間片。
- 處於可運行狀態的SCHED_FIFO進程比SCHED_NORMAL進程先得到調度。
- 不基於時間片。SCHED_FIFO進程處於可執行狀態,會一直執行,直到自己受阻塞或顯式地釋放處理器。
- 只有優先級更高的SCHED_FIFO或者SCHED_RR任務才能搶佔SCHED_FIFO任務。
- 多個同優先級的SCHED_FIFO進程會輪流執行。
- 其他級別較低的進程只能等待SCHED_FIFO進程變爲不可運行態後纔有機會執行。
7.2 SCHED_RR
SCHED_RR是帶有時間片的SCHED_FIFO,是一種實時輪流調度算法,在耗盡實現分配給它的時間後就不能再繼續執行。
這兩種實時算法實現的是靜態優先級,內核不爲實時進程計算動態優先級。
7.3 軟實時
Linux的實時調度算法提供軟實時工作方式:內核調度進程,盡力使進程在它的限定時間到來前運行,但內核不保證總能滿足這些進程的要求。硬實時系統保證在一定條件下,可以滿足任何調度的要求。
實時優先級範圍從0到MAX_RT_PRIO減1,默認MAX_RT_PRIO爲100。
SCHED_NORMAL進程的nice值範圍從MAX_RT_PRIO到(MAX_RT_PRIO+40)。nice值從-20到+19對應實時優先級從100到139。
8、與調度相關的系統調用
與調度相關的系統調用
系統調用 | 描述 |
---|---|
nice() | 設置進程的nice值 |
sched_setscheduler() | 設置進程的調度策略 |
sched_getscheduler() | 獲取進程的調度策略 |
sched_setparam() | 設置進程的實時優先級 |
sched_getparam() | 獲取進程的實時優先級 |
sched_get_priority_max() | 獲取實時優先級的最大值 |
sched_get_priority_min() | 獲取實時優先級的最小值 |
sched_rr_get_interval() | 獲取進程的時間片值 |
sched_setaffinity() | 設置進程的處理器的親和力 |
sched_getaffinity() | 獲取進程的處理器的親和力 |
sched_yield() | 暫時讓出處理器 |
8.1 與調度策略和優先級相關的系統調用
- sched_setscheduler()和sched_getscheduler(),讀取或改寫進程tast_struct的policy和rt_priority的值。
- sched_setparam()和sched_getparam(),獲取封裝在sched_param結構體中的rt_priority。
- sched_get_priority_max()和sched_get_priority_min()。
- nice(),只有超級用戶才能使用負值,設置進程task_struct的static_prio和prio的值。
8.2 和處理器綁定有關的系統調用
Linux調度程序提供強制的處理器綁定機制。它盡力通過一種軟的親和性使進程儘量在同一個處理器上運行,但也允許強制指定進程運行的處理器。
強制的親和性保存在進程task_struct的cpus_allowed的位掩碼標誌中,每一位對應一個處理器。默認所有位都被設置。
使用sched_setaffinity()和sched_getaffinity()函數可以設置和獲取位掩碼。
進程第一次創建時繼承父進程的掩碼,和父進程運行在相同處理器上。當處理器綁定關係改變時,內核採用移植線程把任務對到合法處理器上。最後,加載平衡器把任務拉倒允許的處理器上。
8.3 放棄處理器時間
通過sched_yield()讓進程顯式地將處理器時間讓給其他等待執行的進程。將進程從活動隊列移到過期隊列中,在一段時間內它不會再被執行。
實時進程例外,不會過期,只是被移動到其優先級隊列的最後面。
內核代碼調用yield()確定給定進程處於可執行狀態,然後在調用sched_yield()。用戶空間程序直接調用sched_yield()。