1. 程式人生 > >每天3分鐘作業系統修煉祕籍(25):程序排程演算法圖解說明

每天3分鐘作業系統修煉祕籍(25):程序排程演算法圖解說明

點我檢視祕籍連載

程序排程

在這裡簡單介紹一些程序排程相關的演算法策略,雖然瞭解這些對於使用Linux來說不會有很大幫助,但是卻能幫助我們瞭解程序排程追求的是什麼,它和生活中的很多案例都類似。

程序排程的兩個關鍵性指標是:響應時間和週轉時間。

  • 響應時間:程序未執行到下次被選中執行的時間間隔。例如程序剛被建立到第一次排程到它的時間間隔,再例如從該程序切換走後到下次排程到該程序的時間間隔。響應時間體現了互動性,響應時間越短,互動性越好。例如從鍵盤敲下一個字元,如果需要等待幾秒鐘才出現到螢幕,這個互動性是非常差的。
  • 週轉時間:程序從啟動開始到執行完成所花費的時間。週轉時間體現的效能,週轉時間越短,說明程序從開始到完成等待的時間越短。

效能和公平在排程演算法中往往是矛盾的,優化效能必然會降低公平,追求公平必然會丟失效能,排程演算法就在這兩者之間進行權衡。

為了一步步推進排程演算法,先做幾個不現實的假設,在後面會不斷的放寬這些假設:

  • 假設1.每個程序執行的總時間相等(即獲取到的總CPU時間長度相等)。
  • 假設2.所有程序同時啟動(當然,肯定會有先後順序,只不過在排程這些程序前,它們全已經啟動好了)。
  • 假設3.一旦被排程選中,將一直執行直到任務完成。
  • 假設4.所有程序只消耗CPU,沒有任何IO操作。
  • 假設5.每個程序的工作時長是已知的(即事先知道每個程序需要佔用多長時間的CPU)。

先進先出(FIFO)排程演算法

假設現在有A、B、C三個程序同時啟動,每個程序總共執行10s。假設首先排程到程序A,再排程到程序B,最後排程到程序C,示意圖如圖(左)。

可輕鬆計算出:

平均週轉時間 = (10+20+30)/3=20s
平均響應時間 = (0+10+10)/3=6.67s

現在放寬假設1,不再認為每個程序的執行時長是相等的。假設,程序A執行時長為100秒,程序B和程序C執行時長仍為10秒,示意圖如圖(右)。那麼:

平均週轉時間 = (100+110+120)/3=110s
平均響應時間 = (0+100+110)/3=70s

FIFO的缺點已經出現了,如果耗時較少的程序被放在耗時更長的程序之後,那麼短程序將做出無謂的等待。

這個現象在生活中很常見,例如超市收銀臺結賬時,如果我們只是買了一件商品,但發現隊伍前面有個人的購物車裡堆滿了商品,我們在心裡肯定想讓收銀員先為我們結賬,因為在我們看來,我們的結賬速度是非常快的。所以,超市裡這種結賬方式的效率是非常低的,如果超市裡開放一個零散的結賬通道,只給那些購物數量少的使用者結賬,那麼就可以提高收銀效率,但超市肯定是不會設定這種通道的,因為超市裡無法對使用者選擇哪個收銀通道做出限制,使用者也不會遵守這種限制。

不過零散結賬通道的模式,正是下一種要介紹的排程演算法:最短任務優先。

最短任務優先(SJF)排程演算法

見名知意,最短任務優先(shortest job first)表示最短的程序先執行。

例如,上面的示例,程序A執行100秒,程序B和程序C執行10秒,A最長,所以最後執行。那麼SJF排程演算法的示意圖如圖(左):

那麼:

平均週轉時間 = (10+20+120)/3=50s
平均響應時間 = (0+10+20)/3=10s

在目前的假設前提下,這是很好的結果。

但是,如果繼續放寬假設2,程序不再同時啟動,而是隨時啟動,那麼最糟糕的情況是程序A先啟動然後被排程選中,然後再啟動程序B和C(假設在10秒的時候啟動),這時又回到了最糟糕的狀態,程序B和C必須等待耗時最長的程序A執行完成,如圖(右)。那麼:

平均週轉時間 = (100+(110-10)+(120-10))/3=103.3s
平均響應時間 = (0+(100-10)+(110-10))/3=63.3s

最短完成時間優先(STCF)排程演算法

於是假設,如果程序是可以搶佔的,那麼短任務就可以在啟動的時候直接執行,而不需要等待耗時長的程序執行完,這是最短完成時間優先(shortest time-to-completion first,STCF)排程演算法。

要保證STCF排程演算法生效,必須放寬假設3,即不再要求程序一旦被排程選中就必須執行完,而是可以中斷,然後由作業系統決定排程哪個程序,對於該演算法來說,顯然是排程完成時間最短的程序。

所以,FIFO和SJF都是非搶佔式的排程演算法,而STCF是搶佔式的排程演算法。

回到STCF排程演算法,當執行程序A的時候,在第10秒的時候程序B和程序C啟動,STCF排程示意圖如圖所示。

於是計算:

平均週轉時間 = (120+(20-10)+(30-10))/3=50s
平均響應時間 = (0+0+10)/3=3.3s

目前為止,該演算法已經對兩個指標都達到了最佳效果,特別是響應時間大幅減小。這主要歸功於搶佔式,只要是搶佔式的,那麼平均響應時間就不會比非搶佔的差。

Round-Robin排程演算法

雖然搶佔式的最短完成時間優先排程演算法已經不錯,但對於它的響應時間,其實還能更優化,只需在最短完成時間有限的排程演算法上新增一個功能:指定每個程序最多隻執行一段固定的時間,排程時輪詢每一個程序。也就是輪詢(Round-Robin,RR)排程演算法。

這段固定的時間長度,就是時間片。要使用時間片的功能,必然需要時鐘中斷,且時間片的長度必須為時鐘週期的倍數,例如時鐘中斷是每10秒中斷一次,那麼時間片可以是10s、20s、100s等。

仍然接前面最短完成時間優先排程的例子,但現在新增2s的時間片並假設A、B、C這3個程序同時啟動,那麼採用RR排程演算法的示意圖如圖。

計算:

平均週轉時間 = (120+(30-2)+30)/3=59.3s
平均響應時間 = (0+2+4)/3=3s

時間片的長度對於RR排程演算法是至關重要的。時間片越短,RR在響應時間上表現越好,但是時間片太短也是有問題的,因為上下文切換頻繁意味著它的代價越高。時間片也不能太長,時間片越長,RR在響應時間上表現越差,互動性越差。所以需要權衡時間片的長短(當然,這是核心開發人員需要考慮的,我們使用者直接享受成果),在允許的條件下讓時間片足夠長。

提示:增加時間片長度可降低成本

增加時間片長度,可以攤銷上下文切換的成本。例如,時間片長度為10ms,上下文切換需要1ms,那麼大概會花費10%的時間用在上下文切換上,但如果將時間片設定為100ms,則只會花費大約1%的時間用在上下文切換上,於是時間片帶來的上下文切換成本就被攤銷了。

如果只關注平均響應時間,那麼RR演算法非常好,因為它在乎的時間片這段間隔時長,時間片越短,(不考慮上下文切換成本)RR越優秀,換句話說,每一個程序都能有機會快速被選中,它的互動性非常好。

但是,RR會在每個程序執行過程中間穿插其它程序,從而延伸每個程序,導致RR演算法在平均週轉時間上表現很差,甚至可以說RR在平均週轉時間指標上是最差的演算法。

考慮IO問題

在前面的排程演算法中,一直都假設程序不會執行IO,這是不現實的。所以,這裡將IO問題考慮到排程演算法中。

站在我們角度上考慮,這其實也不是問題,我們早已經知道解決方案是在IO等待時將CPU分配給其它程序。但是,站在排程器的角度上考慮,在排程程序時必須需要將這個因素考慮進去。所以,這裡仍然花一點筆墨簡單描述下這個問題。

一方面,排程程式需要在程序開始IO時做出決定,因為在IO期間程序是不消耗CPU的,它將一直阻塞等待直到IO完成,這時排程器需要排程另外一個程序去使用CPU。

另一方面,排程程式需要在IO完成時做出決定,因為IO完成時會發送中斷,從而回到作業系統,作業系統處理中斷時會將IO完成對應的那個程序放回到就緒佇列中,然後排程器排程下一個要執行的程序,當然也可能會直接排程到該程序。

假設有A、B兩個程序,各自都需要50ms的CPU時間,但是A每執行10ms就執行一次10ms的IO,而B沒有IO操作,並且排程程式要求先執行完A再執行B。如圖。

無需計算,也知道這是非常不理想的排程方式。因為在進行IO的時候,CPU完全閒置了,僅只是在那裡空轉。

所以,更好的方式是在程序A進行IO的時候,同時將CPU分配給程序B,如圖。

多級反饋佇列

到目前為止,非搶佔式的SJF和STCF排程演算法都是優化週轉時間,通過先執行短任務首先,而搶佔式的RR則是優化響應時間,通過劃分執行時間片實現。它們都遵守了一個假設:每個程序的執行時長是已知的,但這是最不現實的假設,因為我們根本不可能會提前知道一個程序要做的工作需要耗費多少時間。那麼如何合理地排程程序?在計算機領域裡,這種優化未知問題的場景有時候非常關鍵,例如CPU的分支預測、快取演算法以及程序排程等等。解決這類問題的思路就是觀察歷史,從歷史資料中推測未來。既然是預測,自然會有預測失敗的時候,而預測失敗的代價可能會比正常情況下更大,所以需要不斷優化預測問題的方式,並提供預測失敗時的挽救手段,儘量減小代價。

下面將介紹一種在程序執行時長未知情況下的排程演算法:多級反饋佇列(Multi-level Feedback Queue,MLFQ)。

多級反饋佇列通過使用不同優先順序的多個佇列來實現,它的基本規則是:每個程序只能存在於一個佇列中,而一個佇列中可以包含多個程序;同一佇列中的程序具有相同的優先順序;排程器優先排程最高優先順序佇列中的程序,並對該佇列中的程序採用RR排程演算法輪詢排程每個程序。

下圖是一個多級反饋佇列的簡單示意圖。

MLFQ最大的問題是如何設定每個佇列的優先順序。因為每個程序的情況是未知的,所以需要根據一些排程指標去觀察歷史從而預測未來。

例如,對於使用者坐在計算機前等待的程序(如等待鍵盤輸入),很可能是一個互動性的程序,應當將它作為高優先順序的程序,儘快響應給使用者。再例如,對於長時間佔用CPU工作的程序,很可能是後臺服務類程序,應該降低它的優先順序以便為更重要的互動式程序讓路。

此外,還必須保證高優先順序的程序在執行一段時間後能夠降低它的優先順序,否則在高優先順序佇列中的程序完全執行完之前,低優先順序佇列中的程序將沒有機會被選中。正如上圖中最高優先順序的A和B程序,如果不降低它的優先順序,C和D永遠無法執行,換句話說,A和B程序壟斷了CPU的使用權。

還要考慮IO問題,對於因執行IO而主動放棄CPU的程序,應當讓其優先順序不變,否則在IO完成時它很可能已經被擠到低優先順序的位置,從而得不到較好的響應時間。

提示:CPU密集型和IO密集型程序

根據這裡的描述,大概可以做出一個推斷:1.IO密集型任務很可能是互動型程序,應該給它高優先順序;2.CPU密集型任務很可能是服務類程序,應該給它低優先順序。

看上去似乎有悖常理,CPU密集型任務不應該多給它一些CPU時間嗎?一方面,在IO過程中(IO速度非常慢),CPU已經為CPU密集型程序工作很長時間了,臨時去處理一下完成IO的程序對CPU密集型程序來說影響並不大,畢竟完成IO後的程序在那裡嗷嗷待哺。另一方面,IO密集型任務在IO時已經將CPU交給其它程序並工作了足夠長的時間,那麼在IO完成的時候,於情於理也應該再次拿回CPU。

其實,CPU密集和IO密集中密集的含義並不是它們亟需CPU或IO,而是描述這類程序需要長時間消耗CPU或IO。此外,大多數優先順序排程演算法都能夠為不同優先順序佇列分配不同長度的時間片,那麼可以給高優先順序(IO密集型)佇列分配短時間片,給低優先順序(CPU密集型)佇列分配長時間片。Windows和Solaris正是這麼做的,但Linux正好相反,給低優先順序任務分配更短時間片,給高優先順序任務分配更長時間片。

最後,還要考慮新啟動的程序應放在哪個佇列中的問題。因為不知道新程序的長短,所以假設它是短任務比較好,這樣能快速執行完,所以將新程序放在最高佇列中。

至此,MLFQ有了以下幾個規則:

  • 1.如果優先順序A大於B,則排程A
  • 2.如果優先順序A等於B,則RR排程A和B
  • 3.程序剛啟動時,放入最高優先順序佇列
  • 4(a).程序耗盡時間片後,降低該程序的優先順序(移入下一級佇列)
  • 4(b).如果在時間片內進行IO而主動放棄CPU,則保持其優先順序不變

根據這些規則,一步步地體會優先順序對排程方式的影響。

首先,假設只有單個長時間執行的程序A,如果時間片為10ms,那麼,該程序的排程方式如圖。在這裡,程序A具體要執行多長時間(而且這是未知的)不重要,只是關注它經過排程後優先順序的變化方式。

程序A啟動時在最高優先順序佇列Q2中,執行10ms後降低優先順序到Q1佇列,再10ms之後進入最低優先順序佇列,之後它將一直在此佇列中被排程。

假設現在程序A執行一段時間後,來了一個只需20ms即退出的短程序B(同樣的,具體時長不重要,只要它在程序A完成前退出即可),因為執行時間短,所以這個程序很可能是一個互動性的程序。那麼排程方式如圖。

再假設,如果這個短程序B是需要執行IO的,每執行1ms就需要IO一段時間。根據規則,主動讓出CPU的程序優先順序不變,那麼排程方式如圖。

在這裡,程序B可能IO密集型的程序,因為它多次IO,而程序A可能是CPU密集型的程序,因為它需要長時間執行。

但是這種排程方式有幾個非常嚴重的問題:

  1. 程序飢餓問題。如果有多個類似於程序B的IO密集型程序(或者互動式程序),那麼這些程序將霸佔大量的CPU,導致CPU密集型程序沒有機會獲取到CPU,也就是CPU密集型程序被餓死。
  2. 通過某些手段可以欺騙排程器,某個程序可以不斷地在時間片終止之前(比如時間片耗費了99%的時候)發出一個短IO(例如開啟一個無關檔案),這樣該程序就能一直處於最高優先順序佇列中,從而基本上壟斷CPU的使用權。
  3. 某個程序可能是CPU密集型和IO密集型的混合型別,例如一開始是CPU密集型的,等它降低到最低優先順序後,進入IO密集型,那麼它將無法獲取到IO密集型該有的待遇。

基於此,需要修改MLFQ的規則,主要改變的是:保證任何一個程序都不會一直保持某個優先順序。修改後的規則為:

  1. 如果優先順序A大於B,則排程A。
  2. 如果優先順序A等於B,則RR排程A和B。
  3. 程序剛啟動時,放入最高優先順序佇列。
  4. 為程序分配時間總額,一旦程序耗盡了分配給程序的時間總額,就降低它的優先順序。
  5. 每隔一段時間S,就將所有程序重新加入最高優先順序佇列。

上面添加了規則5,並將原來的規則4(a)和4(b)合併成規則4。

首先是規則5,在規則5的要求下,程序不可能會被餓死,因為每隔一段時間後所有程序的優先順序都提到了最高且相同。

再是規則4,原來規則4(a)和4(b)最大的問題在於每次排程時都重新計算了時間片,使得可能出現欺騙排程器的行為。但改寫成了規則4後,每個程序在某個優先順序下都是有時間限制的,不管它是一次性用完還是分成多次用完,只要時間到了就降低優先順序。

例如,下圖中顯示的高優先順序程序在隨著時間的流逝,它的優先順序也不斷下降,直到最低級別。根據規則5,等一段時間過後,所有的程序又都回到最高級別,全都被公平對待。

從上面對多級反饋佇列排程演算法的描述,不難發現優先順序是該排程演算法決定排程哪個程序的唯一標準。雖然在Linux下使用的排程演算法是CFS而不是MLFQ,但是CFS也使用優先順序的概念,改變優先順序也能達到類似的目標