上一篇咱們提到過進程狀態,而進程調度主要是針對TASK_RUNNING運行狀態進行調度,由於其餘狀態是不可執行好比睡眠,不須要調度。html
進程調度程序,簡稱調度程序,它是確保進程能有效工做的一個內核子系統。調度程序負責決定哪一個進程投入運行,什麼時候運行以及運行多長時間。算法
多任務操做系統是指能同時併發執行多個進程的操做系統。併發
多任務系統劃分爲兩類:非搶佔式多任務(cooperative multitasking)和搶佔式多任務(preemptive multitasking)。負載均衡
非搶佔式是一種協做的方式,一個進程一直執行直到任務結束或者主動退出才切換到下一個進程。模塊化
搶佔式是大部分操做系統採用的方式,是指給每一個進程分配一個時間片(time slice),當時運行時間達到規定的時間時則會切換到下一個進程。函數
上面提到的時間片策略是比較傳統的方式,後面Linux系統進行了屢次改進,好比O(1)算法、電梯算法和CFS等。那麼改進的動機和依據是什麼呢,咱們來看看。oop
進程根據資源使用能夠分爲這兩大類。spa
I/O 消耗型:進程的大部分時間用來進行 I/O 的請求或者等待,好比鍵盤。這種類型的進程常常處於能夠運行的狀態,可是都只是運行一點點時間,絕大多數的時間都在處於阻塞(睡眠)的狀態。操作系統
CPU 消耗型:進程的大部分時間用在執行代碼上即CPU運算,好比開啓 Matlab 作一個大型的運算。除非被搶佔,不然它們能夠一直運行,因此它們沒有太多的 I/O 需求。調度策略每每是儘可能下降他們的調度頻率,而延長其運行時間。.net
固然這種劃分不是絕對的,通常的應用程序同時包含兩種行爲。
因此調度策略一般須要在兩個矛盾的目標中尋求平衡:進程響應迅速(響應時間短)和最大系統利用率(高吞吐量)。
Linux 系統爲了提高響應的速度,傾向於優先調度 I/O 消耗型。
調度算法中最基本的一類就是基於優先級的調度,根據進程的價值(重要性)和對處理器時間的需求來對進程分級的想法。簡單的說是優先級高的先運行,低的後運行。
Linux採用了兩種不一樣的優先級範圍。
(1)nice值
它的範圍從-20到+19,默認值0。越大的nice值優先級越低,19優先級最低,-20優先級最高。ps -ef命令中,NI標記就是進程對應的nice值。
這是普通進程的優先級。
(2)實時優先級
範圍是 0~99,與 nice 值相反,值越大優先級越高。
這是實時進程的優先級,相對普通進程的,因此任何實時進程的優先級都高於普通進程的優先級。
時間片是一個數值,它代表在搶佔前所能持續運行的時間。調度策略必須規定一個默認的時間片,這並不是易事。由於時間片過長I/O消耗型的線程得不到及時響應,而過短CPU消耗型的須要頻繁被切換,吞吐量會降低。而最新的Linux調度策略CFS不採用固定的時間片,而是採用了處理器的使用比。咱們接下來詳細介紹。
Linux調度器是以分類(模塊化)的方式提供的,即對不一樣類型的進程進行分組而且分別選擇相應的算法。
這種調度結構被稱爲調度器類(scheduler classes),它容許不一樣的可動態添加的調度算法並存,調度屬於本身範疇的進程。
以下圖Linux調度器包含了多種調度器類。
這些調度器類的優先級順序爲: Stop_Task > Real_Time > Fair > Idle_Task。
開發者能夠根據己的設計需求把所屬的Task配置到不一樣的scheduler classes中。其中的Real_Time和Fair是最經常使用的,也對應了咱們上面提到的實時進程和普通進程。
Fair調度使用的徹底公平調度器(Completely Fair Scheduler,CFS)。
這是一個針對普通進程的調度類,在Linux中稱爲SCHED_NORMAL(在POSIX中稱爲SCHED_OTHER)。
傳統的時間片方式是每一個進程固定一個時間,那麼當進程個數變化時,整個調度週期順延。時間片還會跟着系統定時器節拍隨時改變,那麼整個週期再次跟着變化。那麼優先級低的進程可能遲遲得不到調度。
而CFS把整個調度週期的時間固定,該週期叫目標延遲(target latency),也再也不採用時間片,而是根據每一個進程的nice值獲得的權重再計算獲得處理器比例,進而獲得進程本身的時間。該時間和節拍沒有任何關係,也能夠精確到ns。例如「目標延遲」設置爲20ms,2個進程各10毫秒,若是4個進程則是各5毫秒。若是100個進程呢,是否是就是0.2毫秒呢?
不必定,CFS引入了一個關鍵特性:最小粒度。即每一個進程得到時間片的最小值,默認是1毫秒。
爲了公平起見,CFS老是選擇運行最少(vruntime)的進程做爲下一個運行進程。因此這樣照顧了I/O消耗型短期處理的需求,也將更多時間留給了CPU消耗型的程序。確實解決了多進程環境下因延遲帶來的不公平性。
在 CFS 中,給每個進程安排了一個虛擬時鐘vruntime(virtual runtime),這個變量並不是直接等於他的絕對運行時間,而是根據運行時間放大或者縮小一個比例,CFS使用這個vruntime 來表明一個進程的運行時間。若是一個進程得以執行,那麼他的vruntime將不斷增大,直到它沒有執行。沒有執行的進程的vruntime不變。調度器爲了體現絕對的徹底公平的調度原則,老是選擇vruntime最小的進程,讓其投入執行。他們被維護到一個以vruntime爲順序的紅黑樹rbtree中,每次去取最小的vruntime的進程(最左側的葉子節點)來投入運行。實際運行時間到vruntime的計算公式爲:
[ vruntime = 實際運行時間 * 1024 / 進程權重 ]
這裏的1024表明nice值爲0的進程權重。全部的進程都以nice爲0的權重1024做爲基準,計算本身的vruntime。
挑選的進程進行運行了,它運行多久?進程運行的時間是根據進程的權重進行分配。
[ 分配給進程的運行時間 = 調度週期 *(進程權重 / 全部進程權重之和) ]
虛擬運行時間是經過進程的實際運行時間和進程權重(weight)計算出來的。在CFS調度器中,將進程優先級這個概念弱化,而是強調進程的權重。一個進程的權重越大,則說明這個進程更須要運行,所以它的虛擬運行時間就越小,這樣被調度的機會就越大。
關於nice和進程權重以及vruntime之間的計算方式很是複雜。有興趣的能夠在網上搜索或者看源碼。
總之,nice對時間片的做用再也不是算數加權,而是幾何加權。
實時調度策略分爲兩種:SCHED_FIFO 和 SCHED_RR。
這兩種實時進程都比任何普通進程的優先級更高(SCHED_NORMAL),都會比他們更先獲得調度。
SCHED_FIFO:一個這種類型的進程出於可執行的狀態,就會一直執行,直到它本身被阻塞或者主動放棄 CPU;它不基於時間片,能夠一直執行下去,只有更高優先級的SCHED_FIFO或者SCHED_RR才能搶佔它的任務,若是有兩個一樣優先級的SCHED_FIFO任務,它們會輪流執行,其餘低優先級的只有等它們變爲不可執行狀態,纔有機會執行。
SCHED_RR:與SCHED_FIFO大體相同,只是SCHED_RR級的進程在耗盡事先分配給它的時間後就不能再執行了。因此SCHED_RR是帶有時間片的SCHED_FIFO:一種實時輪流調度(Realtime Robin)算法。
上述兩種實時算法實現的都是靜態優先級。內核不爲實時進程計算動態優先級,保證給定的優先級的實時進程總可以搶佔比他優先級低的進程。
進程調度的主要入口點是函數schedule(),即實現進程切換的功能:選擇哪一個進程能夠運行,什麼時候投入運行。
該函數的核心是for()循環,它以優先級爲序,從最高的優先級調度類開始,遍歷全部的調度類。
進程狀態能夠分爲可執行和不可執行,分別放入不一樣的結構中。可執行的進程放在紅黑樹中,而不可執行的放在等待隊列。
一個進程可能在兩種結構中不斷移動。
好比讀文件操做,在執行工做時,處在紅黑樹中,當讀完時可能須要等待磁盤,這時會把本身標記成休眠狀態,從紅黑樹中移出,放入等待隊列,而後調用schedule()選擇和執行一個其餘進程。而當磁盤做業完成時,又會被喚醒,進程再次設置爲可執行狀態,而後從等待隊列中移到紅黑樹中。
上下文切換,就是從一個可執行進程切換到另外一個可執行進程,由context_switch()函數處理。每個新的進程被選出來準備投入運行的時候,schedule()就會調用該函數。
自願切換意味着進程須要等待某種資源,強制切換則與搶佔(Preemption)有關。
搶佔(Preemption)是指內核強行切換正在CPU上運行的進程,在搶佔的過程當中並不須要獲得進程的配合,在隨後的某個時刻被搶佔的進程還能夠恢復運行。發生搶佔的緣由主要有:進程的時間片用完了,或者優先級更高的進程來爭奪CPU了。
搶佔的過程分兩步,第一步觸發搶佔,第二步執行搶佔,這兩步中間不必定是連續的,有些特殊狀況下甚至會間隔至關長的時間:
搶佔只在某些特定的時機發生,這是內核的代碼決定的。
每一個進程都包含一個TIF_NEED_RESCHED標誌,內核根據這個標誌判斷該進程是否應該被搶佔,設置TIF_NEED_RESCHED標誌就意味着觸發搶佔。
直接設置TIF_NEED_RESCHED標誌的函數是set_tsk_need_resched();
觸發搶佔的函數是resched_task()。
TIF_NEED_RESCHED標誌何時被設置呢?在如下時刻:
週期性的時鐘中斷
時鐘中斷處理函數會調用scheduler_tick(),這是調度器核心層(scheduler core)的函數,它經過調度類(scheduling class)的task_tick方法檢查進程的時間片是否耗盡,若是耗盡則觸發搶佔。
喚醒進程的時候
當進程被喚醒的時候,若是優先級高於CPU上的當前進程,就會觸發搶佔。相應的內核代碼中,try_to_wake_up()最終經過check_preempt_curr()檢查是否觸發搶佔。
新進程建立的時候
若是新進程的優先級高於CPU上的當前進程,會觸發搶佔。相應的調度器核心層代碼是sched_fork(),它再經過調度類的task_fork方法觸發搶佔。
進程修改nice值的時候
若是進程修改nice值致使優先級高於CPU上的當前進程,也會觸發搶佔。內核代碼參見 set_user_nice()。
進行負載均衡的時候
在多CPU的系統上,進程調度器儘可能使各個CPU之間的負載保持均衡,而負載均衡操做可能會須要觸發搶佔。
不一樣的調度類有不一樣的負載均衡算法,涉及的核心代碼也不同,好比CFS類在load_balance()中觸發搶佔;RT類的負載均衡基於overload,若是當前運行隊列中的RT進程超過一個,就調用push_rt_task()把進程推給別的CPU,在這裏會觸發搶佔。
(2)執行搶佔的時機
觸發搶佔經過設置進程的TIF_NEED_RESCHED標誌告訴調度器須要進行搶佔操做了,可是真正執行搶佔還要等內核代碼發現這個標誌才行,而內核代碼只在設定的幾個點上檢查TIF_NEED_RESCHED標誌,這也就是執行搶佔的時機。
搶佔若是發生在進程處於用戶態的時候,稱爲User Preemption(用戶態搶佔);若是發生在進程處於內核態的時候,則稱爲Kernel Preemption(內核態搶佔)。
執行User Preemption(用戶態搶佔)的時機
執行Kernel Preemption(內核態搶佔)的時機
Linux在2.6版本以後就支持內核搶佔了,可是請注意,具體取決於內核編譯時的選項:
CONFIG_PREEMPT_NONE=y
不容許內核搶佔。這是SLES的默認選項。
CONFIG_PREEMPT_VOLUNTARY=y
在一些耗時較長的內核代碼中主動調用cond_resched()讓出CPU。這是RHEL的默認選項。
CONFIG_PREEMPT=y
容許徹底內核搶佔。
在 CONFIG_PREEMPT=y 的前提下,內核態搶佔的時機是:
「搶佔」這一部分來自網上,條理比書上更清晰,可是和書上也稍有差異,大致一致,不影響總體理解。
參考資料:
《Linux內核設計與實現》原書第三版