多級反饋隊列(Multi-level Feedback Queue,簡稱MLFQ)需要解決兩方面的問題。首先它要優化周轉時間,這可以通過優先執行較短的工作來實現。然而,操作系統常常不知道工作要運行多久,而這又是SJF等算法所必需的。其次,MLFQ希望給用戶提供較好的交互體驗,因此需要降低響應時間。然而,輪轉調度雖然降低了響應時間,周轉時間卻很差。所以這里的問題是:通常我們對進程一無所知,應該如何構建調度程序來實現這些目標?調度程序如何在運行過程中學習進程的特征,從而做出更好的調度決策?
多級反饋隊列:基本規則
為了構建這樣的調度程序,本章將介紹多級消息隊列背后的基本算法。MLFQ中有許多獨立的隊列,每個隊列有不同的優先級。任何時刻,一個工作只能存在於一個隊列中。MLFQ總是優先執行較高優先級的工作(即那些在較高級隊列中的工作)。每個隊列中可能會有多個工作,它們具有同樣的優先級。在這種情況下,我們就對這些工作采用輪轉調度。至此,我們得到了MLFQ的兩條基本規則:
規則1:如果A的優先級大於B的優先級,運行A不運行B
規則2:如果A的優先級等於B的優先級,輪轉運行A和B
上圖中,最高優先級有兩個工作(A和B),工作C位於中等優先級,而D的優先級最低。按剛才介紹的基本規則,由於A和B有最高優先級,調度程序將交替的調度他們,而C和D永遠都沒有機會運行,除非A和B已經完成。
如何改變優先級
顯然,在一個工作的生命周期中,MLFQ必須按照一定的策略改變其優先級。例如,如果一個工作不斷放棄CPU去等待鍵盤輸入,這是交互型進程的可能行為,MLFQ因此會讓它保持高優先級。相反,如果一個工作長時間地占用CPU,MLFQ會降低其優先級。通過這種方式,MLFQ在進程運行過程中學習其行為,從而利用工作的歷史來預測它未來的行為。下面是我們第一次嘗試優先級調整算法。
規則3:工作進入系統時,放在最高優先級(最上層)隊列
規則4a:工作用完整個時間片后,降低其優先級(移入低一級隊列)
規則4b:如果工作在其時間片以內主動釋放CPU,則優先級不變
單個長工作
假如系統中有一個需要長時間運行的工作,我們看看使用當前的MLFQ調度會發生什么。下圖展示了在一個有3個隊列的調度程序中,隨着時間的推移,這個工作的運行情況。
從這個例子可以看出,該工作首先進入最高優先級(Q2)。執行一個10ms的時間片后,調度程序將工作的優先級減1,因此進入Q1。在Q1執行一個時間片后,最終降低優先級進入系統的最低優先級(Q0),一直留在那里。
來了一個短工作
再看一個較復雜的例子,看看MLFQ如何近似SJF。在這個例子中,有兩個工作:A是一個長時間運行的CPU密集型工作,B是一個運行時間很短的交互型工作。假設A執行一段時間后B到達。會發生什么呢?
上圖展示了這種場景的結果。A(用黑色表示)在最低優先級隊列執行(長時間運行的CPU密集型工作都這樣)。B(用灰色表示)在時間為100ms時到達,並被加入最高優先級隊列。由於它的運行時間很短(只有20ms),經過兩個時間片,在被移入最低優先級隊列之前,B執行完畢。然后A繼續運行(在低優先級)。
通過這個例子,我們可以體會到這個算法的一個主要目標:如果不知道工作是短工作還是長工作,那么就在開始的時候假設其是短工作,並賦予最高優先級。如果確實是短工作,則很快會執行完畢,否則將被慢慢移入低優先級隊列,而這時該工作也被認為是長工作了。通過這種方式,MLFQ近似於SJF。
結合I/O
根據上述規則4b,如果進程在時間片用完之前主動放棄CPU,則保持它的優先級不變。這條規則的意圖很簡單:假設交互型工作中有大量的I/O操作(比如等待用戶的鍵盤或鼠標輸入),它會在時間片用完之前放棄CPU。在這種情況下,我們不想處罰它,只是保持它的優先級不變。
下圖展示了這個運行過程,交互型工作B(用灰色表示)每執行1ms便需要進行I/O操作,它與長時間運行的工作A(用黑色表示)競爭CPU。MLFQ算法保持B在最高優先級,因為B總是讓出CPU。如果B是交互型工作,MLFQ就進一步實現了它的目標,讓交互型工作快速運行。
當前MLQF的一些問題
至此,我們有了基本的MLFQ。它看起來似乎相當不錯,長工作之間可以公平地分享CPU,又能給短工作或交互型工作很好的響應時間。然而,這種算法有一些非常嚴重的缺點。首先,會有飢餓問題。如果系統有“太多”交互型工作,就會不斷占用CPU,導致長工作永遠無法得到CPU。即使在這種情況下,我們也希望這些長工作也能有所進展。其次,某些用戶會用一些手段欺騙調度程序,讓它給予進程遠超公平的資源。例如,上述算法對如下的攻擊束手無策:進程在時間片用完之前,調用一個I/O操作(比如訪問一個無關的文件),從而主動釋放CPU。如此便可以保持在高優先級,占用更多的CPU時間。做得好時(比如,每運行99%的時間片時間就主動放棄一次CPU),工作可以幾乎獨占CPU。最后,一個程序可能在不同時間表現不同。一個計算密集的進程可能在某段時間表現為一個交互型的進程。用目前的方法,它不會享受系統中其他交互型工作的待遇。
提升優先級
我們首先來嘗試避免飢餓問題。要讓CPU密集型工作也能局的一些進展,一個簡單的思路是周期性地提升所有工作地優先級,最簡單的實現就是將所有工作一股腦兒地扔到最高優先級隊列。於是,我們有了以下規則。
規則5:每經過一段時間,就將系統中所有工作重新加入最高優先級隊列
新規則一下解決了兩個問題。首先,進程不會餓死——在最高優先級隊列中,它會以輪轉的方式,與其他高優先級工作分享CPU,從而最終獲得執行。其次,如果一個CPU密集型工作變成了交互型,當它優先級提升時,調度程序會正確對待它。
在這種場景下,我們展示長工作與兩個交互型短工作競爭CPU時的行為。下圖左邊沒有優先級提升,長工作在兩個短工作到達后被餓死。右邊每50ms就有一次優先級提升(這里只是舉例,這個值可能過小),因此至少保證長工作會有一些進展,每過50ms就被提升到最高優先級,從而定期獲得執行。
顯然,添加時間段引入了新的問題:時間段的值該如何設定?如果設置得太高,長工作就會飢餓;如果設置得太低,交互型工作又得不到合適的CPU時間比例。
更好的計時方式
為了防止用戶欺騙調度程序,讓它給予進程遠超公平的資源,MLQF為每層隊列提供更為完善的CPU計時方式。調度程序記錄一個進程在某一層中消耗的總時間,而不是在調度時重新計時。只要進程用完了自己的配額,就將它降到低一級隊列中去。不論它是一次用完的,還是拆成很多次用完。
規則4:一旦工作用完了其在某一層中的時間配額(無論中間主動放棄了多少次CPU),就降低其優先級(移入低一級隊列)
上圖對比了在規則4a、4b的策略下(左),以及在新的規則4(右)的策略下,同樣試圖欺騙調度程序的進程的表現。沒有規則4的保護時,進程可以在每個時間片結束前發起一次I/O操作,從而壟斷CPU時間。有了這樣的保護后,不論進程的I/O行為如何,都會慢慢地降低優先級,因而無法獲得超過公平的CPU時間比例。
其他問題
關於MLFQ調度算法還有一些問題。其中一個大問題是如何配置一個調度程序,例如,配置多少隊列?每一層隊列的時間片配置多大?為了避免飢餓問題以及進程行為改變,應該多久提升一次進程的優先級?這些問題都沒有顯而易見的答案,因此只有利用對工作負載的經驗,以及后續對調度程序的調優,才會導致令人滿意的平衡。例如,大多數的MLFQ變體都支持不同隊列可變的時間片長度。高優先級隊列通常只有較短的時間片(比如10ms或者更少),因而這一層的交互工作可以更快地切換。相反,低優先級隊列中更多的是CPU密集型工作,配置更長的時間片會取得更好的效果。
本章包含了一組優化的MLFQ規則。為了方便查閱,我們重新列在這里。
規則1:如果A的優先級大於B的優先級,運行A不運行B
規則2:如果A的優先級等於B的優先級,輪轉運行A和B
規則3:工作進入系統時,放在最高優先級(最上層)隊列
規則4:一旦工作用完了其在某一層中的時間配額(無論中間主動放棄了多少次CPU),就降低其優先級(移入低一級隊列)
規則5:每經過一段時間,就將系統中所有工作重新加入最高優先級隊列