主宰操作系統的經典算法

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此篇文章帶你梳理一下操作系統中都出現過哪些算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6c/6c7284804bc6555c12e88ff11ebba7ad.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"進程和線程管理中的算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進程和線程在調度時候出現過很多算法,這些算法的設計背景是"},{"type":"text","marks":[{"type":"strong"}],"text":"當一個計算機是多道程序設計系統時,會頻繁的有很多進程或者線程來同時競爭 CPU 時間片"},{"type":"text","text":"。 那麼如何選擇合適的進程/線程運行是一項藝術。當兩個或兩個以上的進程/線程處於就緒狀態時,就會發生這種情況。如果只有一個 CPU 可用,那麼必須選擇接下來哪個進程/線程可以運行。操作系統中有一個叫做 "},{"type":"codeinline","content":[{"type":"text","text":"調度程序(scheduler)"}]},{"type":"text","text":" 的角色存在,它就是做這件事兒的,調度程序使用的算法叫做 "},{"type":"codeinline","content":[{"type":"text","text":"調度算法(scheduling algorithm)"}]},{"type":"text","text":" 。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/da/dac11508122e2366db10daa5232c7533.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"調度算法分類"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對不同的操作系統環境,也有不同的算法分類,操作系統主要分爲下面這幾種"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"批處理操作系統"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"交互式操作系統"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實時操作系統"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們分別來看一下這些操作系統中的算法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"批處理操作系統中的算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"設計目標"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"批處理系統"}]},{"type":"text","text":"廣泛應用於商業領域,比如用來處理工資單、存貨清單、賬目收入、賬目支出、利息計算、索賠處理和其他週期性作業。在批處理系統中,一般會選擇使用"},{"type":"codeinline","content":[{"type":"text","text":"非搶佔式算法"}]},{"type":"text","text":"或者"},{"type":"codeinline","content":[{"type":"text","text":"週期性比較長"}]},{"type":"text","text":"的"},{"type":"codeinline","content":[{"type":"text","text":"搶佔式算法"}]},{"type":"text","text":"。這種方法可以減少線程切換因此能夠提升性能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在"},{"type":"codeinline","content":[{"type":"text","text":"交互式用戶環境"}]},{"type":"text","text":"中,因爲爲了用戶體驗,所以會避免長時間佔用進程,所以需要"},{"type":"codeinline","content":[{"type":"text","text":"搶佔式算法"}]},{"type":"text","text":"。由於某個進程出現錯誤也有可能無限期的排斥其他所有進程。爲了避免這種情況,搶佔式也是必須的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在"},{"type":"codeinline","content":[{"type":"text","text":"實時系統"}]},{"type":"text","text":"中,搶佔式不是必須的,因爲進程知道自己可能運行不了很長時間,通常很快的做完自己的工作並掛起。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"關鍵指標"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常有"},{"type":"codeinline","content":[{"type":"text","text":"三個指標"}]},{"type":"text","text":"來衡量系統工作狀態:"},{"type":"text","marks":[{"type":"strong"}],"text":"吞吐量、週轉時間和 CPU 利用率"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"吞吐量(throughout)"}]},{"type":"text","text":" 是系統每小時完成的作業數量。綜合考慮,每小時完成 50 個工作要比每小時完成 40 個工作好。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"週轉時間(Turnaround time)"}]},{"type":"text","text":" 是一種平均時間,它指的是從一個批處理提交開始直到作業完成時刻爲止的平均時間。該數據度量了用戶要得到輸出所需的平均等待時間。週轉時間越小越好。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"CPU 利用率(CPU utilization)"}]},{"type":"text","text":" 通常作爲批處理系統上的指標。即使如此,CPU 利用率也不是一個好的度量指標,真正有價值的衡量指標是系統每小時可以完成多少作業(吞吐量),以及完成作業需要多長時間(週轉時間)。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們就來認識一下批處理中的算法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"先來先服務"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很像是先到先得。。。它是一種非搶佔式的算法。此算法將按照請求順序爲進程分配 CPU。最基本的,會有一個就緒進程的等待隊列。當第一個任務從外部進入系統時,將會立即啓動並允許運行任意長的時間。它不會因爲運行時間太長而中斷。當其他作業進入時,它們排到就緒隊列尾部。當正在運行的進程阻塞,處於等待隊列的第一個進程就開始運行。當一個阻塞的進程重新處於就緒態時,它會像一個新到達的任務,會排在隊列的末尾,即排在所有進程最後。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/5e/5e3dd00020c84a03b3df34f0a21c94bb.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個算法的強大之處在於"},{"type":"codeinline","content":[{"type":"text","text":"易於理解"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"編程"}]},{"type":"text","text":",在這個算法中,一個單鏈表記錄了所有就緒進程。要選取一個進程運行,只要從該隊列的頭部移走一個進程即可;要添加一個新的作業或者阻塞一個進程,只要把這個作業或進程附加在隊列的末尾即可。這是很簡單的一種實現。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不過,先來先服務也是有缺點的,那就是沒有優先級的關係,試想一下,如果有 100 個 I/O 進程正在排隊,第 101 個是一個 CPU 密集型進程,那豈不是需要等 100 個 I/O 進程運行完畢纔會等到一個 CPU 密集型進程運行,這在實際情況下根本不可能,所以需要優先級或者搶佔式進程的出現來優先選擇重要的進程運行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"最短作業優先"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"批處理中的第二種調度算法是 "},{"type":"codeinline","content":[{"type":"text","text":"最短作業優先(Shortest Job First)"}]},{"type":"text","text":",我們假設運行時間已知。例如,一家保險公司,因爲每天要做類似的工作,所以人們可以相當精確地預測處理 1000 個索賠的一批作業需要多長時間。當輸入隊列中有若干個同等重要的作業被啓動時,調度程序應使用最短優先作業算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/53/53fa5b52eca66017313a46fcbe627445.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上圖 a 所示,這裏有 4 個作業 A、B、C、D ,運行時間分別爲 8、4、4、4 分鐘。若按圖中的次序運行,則 A 的週轉時間爲 8 分鐘,B 爲 12 分鐘,C 爲 16 分鐘,D 爲 20 分鐘,平均時間內爲 14 分鐘。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在考慮使用最短作業優先算法運行 4 個作業,如上圖 b 所示,目前的週轉時間分別爲 4、8、12、20,平均爲 11 分鐘,可以證明最短作業優先是最優的。考慮有 4 個作業的情況,其運行時間分別爲 a、b、c、d。第一個作業在時間 a 結束,第二個在時間 a + b 結束,以此類推。平均週轉時間爲 (4a + 3b + 2c + d) / 4 。顯然 a 對平均值的影響最大,所以 a 應該是最短優先作業,其次是 b,然後是 c ,最後是 d 它就只能影響自己的週轉時間了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">需要注意的是,在所有的進程都可以運行的情況下,最短作業優先的算法纔是最優的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"最短剩餘時間優先"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最短作業優先的搶佔式版本被稱作爲 "},{"type":"codeinline","content":[{"type":"text","text":"最短剩餘時間優先(Shortest Remaining Time Next)"}]},{"type":"text","text":" 算法。使用這個算法,調度程序總是選擇剩餘運行時間最短的那個進程運行。當一個新作業到達時,其整個時間同當前進程的剩餘時間做比較。如果新的進程比當前運行進程需要更少的時間,當前進程就被掛起,而運行新的進程。這種方式能夠使短期作業獲得良好的服務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"交互式系統中的調度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"交互式系統中在個人計算機、服務器和其他系統中都是很常用的,所以有必要來探討一下交互式調度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"輪詢調度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一種最古老、最簡單、最公平並且最廣泛使用的算法就是 "},{"type":"codeinline","content":[{"type":"text","text":"輪詢算法(round-robin)"}]},{"type":"text","text":"。每個進程都會被分配一個時間段,稱爲"},{"type":"codeinline","content":[{"type":"text","text":"時間片(quantum)"}]},{"type":"text","text":",在這個時間片內允許進程運行。如果進程在時間片結束前阻塞或結束,則 CPU 立即進行切換。輪詢算法比較容易實現。調度程序所做的就是維護一個可運行進程的列表,就像下圖中的 a,當一個進程用完時間片後就被移到隊列的末尾,就像下圖的 b。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e4/e443b575d0d92b46af190b4cff37a2b2.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"時間片輪詢調度中唯一有意思的一點就是時間片的長度。從一個進程切換到另一個進程需要一定的時間進行管理處理,包括保存寄存器的值和內存映射、更新不同的表格和列表、清除和重新調入內存高速緩存等。這種切換稱作 "},{"type":"codeinline","content":[{"type":"text","text":"進程間切換(process switch)"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"上下文切換(context switch)"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"優先級調度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"輪詢調度假設了所有的進程是同等重要的。但事實情況可能不是這樣。例如,在一所大學中的等級制度,首先是院長,然後是教授、祕書、後勤人員,最後是學生。這種將外部情況考慮在內就實現了"},{"type":"codeinline","content":[{"type":"text","text":"優先級調度(priority scheduling)"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2a/2ae4e11a9c22d17925aefdbd2b9d6a7b.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它的基本思想很明確,每個進程都被賦予一個優先級,優先級高的進程優先運行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是也不意味着高優先級的進程能夠永遠一直運行下去,調度程序會在每個時鐘中斷期間降低當前運行進程的優先級。如果此操作導致其優先級降低到下一個最高進程的優先級以下,則會發生進程切換。或者,可以爲每個進程分配允許運行的最大時間間隔。當時間間隔用完後,下一個高優先級的進程會得到運行的機會。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以很方便的將一組進程按優先級分成若干類,並且在各個類之間採用優先級調度,而在各類進程的內部採用輪轉調度。下面展示了一個四個優先級類的系統"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f2/f2391615e129497d18e6069ff120db7b.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它的調度算法主要描述如下:上面存在優先級爲 4 類的可運行進程,首先會按照輪轉法爲每個進程運行一個時間片,此時不理會較低優先級的進程。若第 4 類進程爲空,則按照輪詢的方式運行第三類進程。若第 4 類和第 3 類進程都爲空,則按照輪轉法運行第 2 類進程。如果不對優先級進行調整,則低優先級的進程很容易產生飢餓現象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"最短進程優先"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於批處理系統而言,由於最短作業優先常常伴隨着最短響應時間,所以如果能夠把它用於交互式進程,那將是非常好的。交互式進程通常遵循下列模式:等待命令、執行命令、等待命令、執行命令。。。如果我們把每個命令的執行都看作一個分離的作業,那麼我們可以通過首先運行最短的作業來使響應時間最短。這裏唯一的問題是如何從當前可運行進程中找出最短的那一個進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一種方式是根據進程過去的行爲進行推測,並執行估計運行時間最短的那一個。假設每個終端上每條命令的預估運行時間爲 "},{"type":"codeinline","content":[{"type":"text","text":"T0"}]},{"type":"text","text":",現在假設測量到其下一次運行時間爲 "},{"type":"codeinline","content":[{"type":"text","text":"T1"}]},{"type":"text","text":",可以用兩個值的加權來改進估計時間,即"},{"type":"codeinline","content":[{"type":"text","text":"aT0+ (1- 1)T1"}]},{"type":"text","text":"。通過選擇 a 的值,可以決定是儘快忘掉老的運行時間,還是在一段長時間內始終記住它們。當 a = 1/2 時,可以得到下面這個序列"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f4/f4d82e9928952ca77f2cac5bd2b04e12.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到,在三輪過後,T0 在新的估計值中所佔比重下降至 1/8。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有時把這種通過當前測量值和先前估計值進行加權平均從而得到下一個估計值的技術稱作 "},{"type":"codeinline","content":[{"type":"text","text":"老化(aging)"}]},{"type":"text","text":"。這種方法會使用很多預測值基於當前值的情況。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"保證調度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一種完全不同的調度方法是對用戶做出明確的性能保證。一種實際而且容易實現的保證是:若用戶工作時有 n 個用戶登錄,則每個用戶將獲得 CPU 處理能力的 1/n。類似地,在一個有 n 個進程運行的單用戶系統中,若所有的進程都等價,則每個進程將獲得 1/n 的 CPU 時間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"彩票調度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對用戶進行承諾並在隨後兌現承諾是一件好事,不過很難實現。但是有一種既可以給出預測結果而又有一種比較簡單的實現方式的算法,就是 "},{"type":"codeinline","content":[{"type":"text","text":"彩票調度(lottery scheduling)"}]},{"type":"text","text":"算法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其基本思想是爲進程提供各種系統資源(例如 CPU 時間)的彩票。當做出一個調度決策的時候,就隨機抽出一張彩票,擁有彩票的進程將獲得該資源。在應用到 CPU 調度時,系統可以每秒持有 50 次抽獎,每個中獎者將獲得比如 20 毫秒的 CPU 時間作爲獎勵。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果希望進程之間協作的話可以交換它們之間的票據。例如,客戶端進程給服務器進程發送了一條消息後阻塞,客戶端進程可能會把自己所有的票據都交給服務器,來增加下一次服務器運行的機會。當服務完成後,它會把彩票還給客戶端讓其有機會再次運行。事實上,如果沒有客戶機,服務器也根本不需要彩票。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">可以把彩票理解爲 buff,這個 buff 有 15% 的機率能讓你產生 "},{"type":"codeinline","content":[{"type":"text","text":"速度之靴"}]},{"type":"text","text":" 的效果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"公平分享調度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到目前爲止,我們假設被調度的都是各個進程自身,而不用考慮該進程的擁有者是誰。結果是,如果用戶 1 啓動了 9 個進程,而用戶 2 啓動了一個進程,使用輪轉或相同優先級調度算法,那麼用戶 1 將得到 90 % 的 CPU 時間,而用戶 2 將之得到 10 % 的 CPU 時間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了阻止這種情況的出現,一些系統在調度前會把進程的擁有者考慮在內。在這種模型下,每個用戶都會分配一些CPU 時間,而調度程序會選擇進程並強制執行。因此如果兩個用戶每個都會有 50% 的 CPU 時間片保證,那麼無論一個用戶有多少個進程,都將獲得相同的 CPU 份額。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b1/b15e8d804d25a81cebb67750ae20af4a.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"實時系統中的調度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"實時系統(real-time)"}]},{"type":"text","text":" 對於時間有要求的系統。實時系統可以分爲兩類,"},{"type":"codeinline","content":[{"type":"text","text":"硬實時(hard real time)"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"軟實時(soft real time)"}]},{"type":"text","text":" 系統,前者意味着必須要滿足絕對的截止時間;後者的含義是雖然不希望偶爾錯失截止時間,但是可以容忍。在這兩種情形中,實時都是通過把程序劃分爲一組進程而實現的,其中每個進程的行爲是可預測和提前可知的。這些進程一般壽命較短,並且極快的運行完成。在檢測到一個外部信號時,調度程序的任務就是按照滿足所有截止時間的要求調度進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實時系統中的事件可以按照響應方式進一步分類爲"},{"type":"codeinline","content":[{"type":"text","text":"週期性(以規則的時間間隔發生)"}]},{"type":"text","text":"事件或 "},{"type":"codeinline","content":[{"type":"text","text":"非週期性(發生時間不可預知)"}]},{"type":"text","text":"事件。一個系統可能要響應多個週期性事件流,根據每個事件處理所需的時間,可能甚至無法處理所有事件。例如,如果有 m 個週期事件,事件 i 以週期 Pi 發生,並需要 Ci 秒 CPU 時間處理一個事件,那麼可以處理負載的條件是 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/eb/eb2486ac313de94c11639c5c55789d7f.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"只有滿足這個條件的實時系統稱爲"},{"type":"codeinline","content":[{"type":"text","text":"可調度的"}]},{"type":"text","text":",這意味着它實際上能夠被實現。一個不滿足此檢驗標準的進程不能被調度,因爲這些進程共同需要的 CPU 時間總和大於 CPU 能提供的時間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實時系統的調度算法可以是靜態的或動態的。前者在系統開始運行之前做出調度決策;後者在運行過程中進行調度決策。只有在可以提前掌握所完成的工作以及必須滿足的截止時間等信息時,靜態調度才能工作,而動態調度不需要這些限制。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"調度策略和機制"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"到目前爲止,我們隱含的假設系統中所有進程屬於不同的分組用戶並且進程間存在相互競爭 CPU 的情況。通常情況下確實如此,但有時也會發生一個進程會有很多子進程並在其控制下運行的情況。例如,一個數據庫管理系統進程會有很多子進程。每一個子進程可能處理不同的請求,或者每個子進程實現不同的功能(如請求分析、磁盤訪問等)。主進程完全可能掌握哪一個子進程最重要(或最緊迫),而哪一個最不重要。但是,以上討論的調度算法中沒有一個算法從用戶進程接收有關的調度決策信息,這就導致了調度程序很少能夠做出最優的選擇。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解決問題的辦法是將 "},{"type":"codeinline","content":[{"type":"text","text":"調度機制(scheduling mechanism)"}]},{"type":"text","text":" 和 "},{"type":"codeinline","content":[{"type":"text","text":"調度策略(scheduling policy)"}]},{"type":"text","text":" 分開,這是長期一貫的原則。這也就意味着調度算法在某種方式下被參數化了,但是參數可以被用戶進程填寫。讓我們首先考慮數據庫的例子。假設內核使用優先級調度算法,並提供了一條可供進程設置優先級的系統調用。這樣,儘管父進程本身並不參與調度,但它可以控制如何調度子進程的細節。調度機制位於內核,而調度策略由用戶進程決定,調度策略和機制分離是一種關鍵性思路。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"內存管理中的算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"操作系統在內存管理上也出現過許多算法,這些算法的目標的最終目的都是爲了合理分配內存。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"操作系統有兩種內存管理方式,一種是"},{"type":"codeinline","content":[{"type":"text","text":"位圖"}]},{"type":"text","text":",一種是 "},{"type":"codeinline","content":[{"type":"text","text":"鏈表"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在使用鏈表管理內存時,有幾種方法的變體"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當按照地址順序在鏈表中存放進程和空閒區時,有幾種算法可以爲創建的進程(或者從磁盤中換入的進程)分配內存。我們先假設內存管理器知道應該分配多少內存,最簡單的算法是使用 "},{"type":"codeinline","content":[{"type":"text","text":"首次適配(first fit)"}]},{"type":"text","text":"。內存管理器會沿着段列表進行掃描,直到找個一個足夠大的空閒區爲止。除非空閒區大小和要分配的空間大小一樣,否則將空閒區分爲兩部分,一部分供進程使用;一部分生成新的空閒區。首次適配算法是一種速度很快的算法,因爲它會盡可能的搜索鏈表。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首次適配的一個小的變體是 "},{"type":"codeinline","content":[{"type":"text","text":"下次適配(next fit)"}]},{"type":"text","text":"。它和首次匹配的工作方式相同,只有一個不同之處那就是下次適配在每次找到合適的空閒區時就會記錄當時的位置,以便下次尋找空閒區時從上次結束的地方開始搜索,而不是像首次匹配算法那樣每次都會從頭開始搜索。"},{"type":"codeinline","content":[{"type":"text","text":"Bays(1997)"}]},{"type":"text","text":" 證明了"},{"type":"text","marks":[{"type":"strong"}],"text":"下次適配算法的性能略低於首次匹配算法"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外一個著名的並且廣泛使用的算法是 "},{"type":"codeinline","content":[{"type":"text","text":"最佳適配(best fit)"}]},{"type":"text","text":"。最佳適配會從頭到尾尋找整個鏈表,找出能夠容納進程的最小空閒區。最佳適配算法會試圖找出最接近實際需要的空閒區,以最好的匹配請求和可用空閒區,而不是先一次拆分一個以後可能會用到的大的空閒區。比如現在我們需要一個大小爲 2 的塊,那麼首次匹配算法會把這個塊分配在位置 5 的空閒區,而最佳適配算法會把該塊分配在位置爲 18 的空閒區,如下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c4/c4d14bda413c214db26f2de62f4fc22c.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼最佳適配算法的性能如何呢?最佳適配會遍歷整個鏈表,所以最佳適配算法的性能要比首次匹配算法差。但是令人想不到的是,最佳適配算法要比首次匹配和下次匹配算法浪費更多的內存,因爲它會產生大量無用的小緩衝區,首次匹配算法生成的空閒區會更大一些。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最佳適配的空閒區會分裂出很多非常小的緩衝區,爲了避免這一問題,可以考慮使用 "},{"type":"codeinline","content":[{"type":"text","text":"最差適配(worst fit)"}]},{"type":"text","text":" 算法。即總是分配最大的內存區域(所以你現在明白爲什麼最佳適配算法會分裂出很多小緩衝區了吧),使新分配的空閒區比較大從而可以繼續使用。仿真程序表明最差適配算法也不是一個好主意。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果爲進程和空閒區維護各自獨立的鏈表,那麼這四個算法的速度都能得到提高。這樣,這四種算法的目標都是爲了檢查空閒區而不是進程。但這種分配速度的提高的一個不可避免的代價是增加複雜度和減慢內存釋放速度,因爲必須將一個回收的段從進程鏈表中刪除並插入空閒鏈表區。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果進程和空閒區使用不同的鏈表,那麼可以按照大小對空閒區鏈表排序,以便提高最佳適配算法的速度。在使用最佳適配算法搜索由小到大排列的空閒區鏈表時,只要找到一個合適的空閒區,則這個空閒區就是能容納這個作業的最小空閒區,因此是最佳匹配。因爲空閒區鏈表以單鏈表形式組織,所以不需要進一步搜索。空閒區鏈表按大小排序時,首次適配算法與最佳適配算法一樣快,而下次適配算法在這裏毫無意義。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一種分配算法是 "},{"type":"codeinline","content":[{"type":"text","text":"快速適配(quick fit)"}]},{"type":"text","text":" 算法,它爲那些常用大小的空閒區維護單獨的鏈表。例如,有一個 n 項的表,該表的第一項是指向大小爲 4 KB 的空閒區鏈表表頭指針,第二項是指向大小爲 8 KB 的空閒區鏈表表頭指針,第三項是指向大小爲 12 KB 的空閒區鏈表表頭指針,以此類推。比如 21 KB 這樣的空閒區既可以放在 20 KB 的鏈表中,也可以放在一個專門存放大小比較特別的空閒區鏈表中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"快速匹配算法尋找一個指定代銷的空閒區也是十分快速的,但它和所有將空閒區按大小排序的方案一樣,都有一個共同的缺點,即在一個進程終止或被換出時,尋找它的相鄰塊並查看是否可以合併的過程都是非常耗時的。如果不進行合併,內存將會很快分裂出大量進程無法利用的小空閒區。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"頁面置換算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"頁面置換有非常多的算法,下面一起來認識一下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當發生缺頁異常時,操作系統會選擇一個頁面進行換出從而爲新進來的頁面騰出空間。如果要換出的頁面在內存中"},{"type":"codeinline","content":[{"type":"text","text":"已經被修改"}]},{"type":"text","text":",那麼必須將其寫到磁盤中以使磁盤副本"},{"type":"codeinline","content":[{"type":"text","text":"保持最新狀態"}]},{"type":"text","text":"。如果頁面沒有被修改過,並且磁盤中的副本也已經是最新的,那麼就"},{"type":"codeinline","content":[{"type":"text","text":"不需要"}]},{"type":"text","text":"進行重寫。那麼就直接使用調入的頁面覆蓋需要移除的頁面就可以了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當發生缺頁中斷時,雖然可以隨機的選擇一個頁面進行置換,但是如果每次都選擇一個不常用的頁面會提升系統的性能。如果一個經常使用的頁面被換出,那麼這個頁面在短時間內又可能被重複使用,那麼就可能會造成額外的性能開銷。在關於頁面的主題上有很多"},{"type":"codeinline","content":[{"type":"text","text":"頁面置換算法(page replacement algorithms)"}]},{"type":"text","text":",這些已經從理論上和實踐上得到了證明。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面我們就來探討一下有哪些頁面置換算法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"最優頁面置換算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最優的頁面置換算法很容易描述,但在實際情況下很難實現。它的工作流程如下:在缺頁中斷髮生時,這些頁面之一將在下一條指令(包含該指令的頁面)上被引用。其他頁面則可能要到 10、100 或者 1000 條指令後纔會被訪問。每個頁面都可以用在該頁首次被訪問前所要執行的指令數作爲標記。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最優化的頁面算法表明應該標記最大的頁面。如果一個頁面在 800 萬條指令內不會被使用,另外一個頁面在 600 萬條指令內不會被使用,則置換前一個頁面,從而把需要調入這個頁面而發生的缺頁中斷推遲。計算機也像人類一樣,會把不願意做的事情儘可能的往後拖。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個算法最大的問題時無法實現。當缺頁中斷髮生時,操作系統無法知道各個頁面的下一次將在什麼時候被訪問。這種算法在實際過程中根本不會使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"最近未使用頁面置換算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了能夠讓操作系統收集頁面使用信息,大部分使用虛擬地址的計算機都有兩個狀態位,R 和 M,來和每個頁面進行關聯。"},{"type":"text","marks":[{"type":"strong"}],"text":"每當引用頁面(讀入或寫入)時都設置 R,寫入(即修改)頁面時設置 M"},{"type":"text","text":",這些位包含在每個頁表項中,就像下面所示"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/54/54809dd7fadf3cd6ac1ffae2d7219c9a.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲每次訪問時都會更新這些位,因此由"},{"type":"codeinline","content":[{"type":"text","text":"硬件"}]},{"type":"text","text":"來設置它們非常重要。一旦某個位被設置爲 1,就會一直保持 1 直到操作系統下次來修改此位。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果硬件沒有這些位,那麼可以使用操作系統的"},{"type":"codeinline","content":[{"type":"text","text":"缺頁中斷"}]},{"type":"text","text":"和"},{"type":"codeinline","content":[{"type":"text","text":"時鐘中斷"}]},{"type":"text","text":"機制來進行模擬。當啓動一個進程時,將其所有的頁面都標記爲"},{"type":"codeinline","content":[{"type":"text","text":"不在內存"}]},{"type":"text","text":";一旦訪問任何一個頁面就會引發一次缺頁中斷,此時操作系統就可以設置 "},{"type":"codeinline","content":[{"type":"text","text":"R 位(在它的內部表中)"}]},{"type":"text","text":",修改頁表項使其指向正確的頁面,並設置爲 "},{"type":"codeinline","content":[{"type":"text","text":"READ ONLY"}]},{"type":"text","text":" 模式,然後重新啓動引起缺頁中斷的指令。如果頁面隨後被修改,就會發生另一個缺頁異常。從而允許操作系統設置 M 位並把頁面的模式設置爲 "},{"type":"codeinline","content":[{"type":"text","text":"READ/WRITE"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以用 R 位和 M 位來構造一個簡單的頁面置換算法:當啓動一個進程時,操作系統將其所有頁面的兩個位都設置爲 0。R 位定期的被清零(在每個時鐘中斷)。用來將最近未引用的頁面和已引用的頁面分開。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當出現缺頁中斷後,操作系統會檢查所有的頁面,並根據它們的 R 位和 M 位將當前值分爲四類:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第 0 類:沒有引用 R,沒有修改 M"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第 1 類:沒有引用 R,已修改 M"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第 2 類:引用 R ,沒有修改 M"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第 3 類:已被訪問 R,已被修改 M"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘管看起來好像無法實現第一類頁面,但是當第三類頁面的 R 位被時鐘中斷清除時,它們就會發生。時鐘中斷不會清除 M 位,因爲需要這個信息才能知道是否寫回磁盤中。清除 R 但不清除 M 會導致出現一類頁面。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"NRU(Not Recently Used)"}]},{"type":"text","text":" 算法從編號最小的非空類中隨機刪除一個頁面。此算法隱含的思想是,在一個時鐘內(約 20 ms)淘汰一個已修改但是沒有被訪問的頁面要比一個大量引用的未修改頁面好,NRU 的主要優點是"},{"type":"text","marks":[{"type":"strong"}],"text":"易於理解並且能夠有效的實現"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"先進先出頁面置換算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另一種開銷較小的方式是使用 "},{"type":"codeinline","content":[{"type":"text","text":"FIFO(First-In,First-Out)"}]},{"type":"text","text":" 算法,這種類型的數據結構也適用在頁面置換算法中。由操作系統維護一個所有在當前內存中的頁面的鏈表,最早進入的放在表頭,最新進入的頁面放在表尾。在發生缺頁異常時,會把頭部的頁移除並且把新的頁添加到表尾。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先進先出頁面可能是最簡單的頁面替換算法了。在這種算法中,操作系統會跟蹤鏈表中內存中的所有頁。下面我們舉個例子看一下(這個算法我剛開始看的時候有點懵逼,後來纔看懂,我還是很菜)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/86/864c0f0c0e59573e9a259b5700b8ffb8.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"初始化的時候,沒有任何頁面,所以第一次的時候會檢查頁面 1 是否位於鏈表中,沒有在鏈表中,那麼就是 "},{"type":"codeinline","content":[{"type":"text","text":"MISS"}]},{"type":"text","text":",頁面1 進入鏈表,鏈表的先進先出的方向如圖所示。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類似的,第二次會先檢查頁面 2 是否位於鏈表中,沒有在鏈表中,那麼頁面 2 進入鏈表,狀態爲 "},{"type":"codeinline","content":[{"type":"text","text":"MISS"}]},{"type":"text","text":",依次類推。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們來看第四次,此時的鏈表爲 "},{"type":"codeinline","content":[{"type":"text","text":"1 2 3 "}]},{"type":"text","text":",第四次會檢查頁面 "},{"type":"codeinline","content":[{"type":"text","text":"2"}]},{"type":"text","text":" 是否位於鏈表中,經過檢索後,發現 2 在鏈表中,那麼狀態就是 "},{"type":"codeinline","content":[{"type":"text","text":"HIT"}]},{"type":"text","text":",並不會再進行入隊和出隊操作,第五次也是一樣的。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面來看第六次,此時的鏈表還是 "},{"type":"codeinline","content":[{"type":"text","text":"1 2 3"}]},{"type":"text","text":",因爲之前沒有執行進入鏈表操作,頁面 "},{"type":"codeinline","content":[{"type":"text","text":"5"}]},{"type":"text","text":" 會首先進行檢查,發現鏈表中沒有頁面 5 ,則執行頁面 5 的進入鏈表操作,頁面 2 執行出鏈表的操作,執行完成後的鏈表順序爲 "},{"type":"codeinline","content":[{"type":"text","text":"2 3 5"}]},{"type":"text","text":"。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"第二次機會頁面置換算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們上面學到的 FIFO 鏈表頁面有個"},{"type":"codeinline","content":[{"type":"text","text":"缺陷"}]},{"type":"text","text":",那就是出鏈和入鏈並不會進行 check "},{"type":"codeinline","content":[{"type":"text","text":"檢查"}]},{"type":"text","text":",這樣就會容易把經常使用的頁面置換出去,爲了避免這一問題,我們對該算法做一個簡單的修改:我們檢查最老頁面的 "},{"type":"codeinline","content":[{"type":"text","text":"R 位"}]},{"type":"text","text":",如果是 0 ,那麼這個頁面就是最老的而且沒有被使用,那麼這個頁面就會被立刻換出。如果 R 位是 1,那麼就清除此位,此頁面會被放在鏈表的尾部,修改它的裝入時間就像剛放進來的一樣。然後繼續搜索。 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種算法叫做 "},{"type":"codeinline","content":[{"type":"text","text":"第二次機會(second chance)"}]},{"type":"text","text":"算法,就像下面這樣,我們看到頁面 A 到 H 保留在鏈表中,並按到達內存的時間排序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/55/55bf2e4c67c490cf2dff46c601d83686.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"a)按照先進先出的方法排列的頁面;b)在時刻 20 處發生缺頁異常中斷並且 A 的 R 位已經設置時的頁面鏈表。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設缺頁異常發生在時刻 20 處,這時最老的頁面是 A ,它是在 0 時刻到達的。如果 A 的 R 位是 0,那麼它將被淘汰出內存,或者把它寫回磁盤(如果它已經被修改過),或者只是簡單的放棄(如果它是未被修改過)。另一方面,如果它的 R 位已經設置了,則將 A 放到鏈表的尾部並且重新設置"},{"type":"codeinline","content":[{"type":"text","text":"裝入時間"}]},{"type":"text","text":"爲當前時刻(20 處),然後清除 R 位。然後從 B 頁面開始繼續搜索合適的頁面。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"尋找第二次機會的是在最近的時鐘間隔中未被訪問過的頁面。如果所有的頁面都被訪問過,該算法就會被簡化爲單純的 "},{"type":"codeinline","content":[{"type":"text","text":"FIFO 算法"}]},{"type":"text","text":"。具體來說,假設圖 a 中所有頁面都設置了 R 位。操作系統將頁面依次移到鏈表末尾,每次都在添加到末尾時清除 R 位。最後,算法又會回到頁面 A,此時的 R 位已經被清除,那麼頁面 A 就會被執行出鏈處理,因此算法能夠正常結束。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"時鐘頁面置換算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"即使上面提到的第二次頁面置換算法也是一種比較合理的算法,但它經常要在鏈表中移動頁面,既降低了效率,而且這種算法也不是必須的。一種比較好的方式是把所有的頁面都保存在一個類似鐘面的環形鏈表中,一個錶針指向最老的頁面。如下圖所示"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/f8/f80b4405eb70ff4737dd1a01008ea43a.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當缺頁錯誤出現時,算法首先檢查錶針指向的頁面,如果它的 R 位是 0 就淘汰該頁面,並把新的頁面插入到這個位置,然後把錶針向前移動一位;如果 R 位是 1 就清除 R 位並把錶針前移一個位置。重複這個過程直到找到了一個 R 位爲 0 的頁面位置。瞭解這個算法的工作方式,就明白爲什麼它被稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"時鐘(clokc)"}]},{"type":"text","text":"算法了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"最近最少使用頁面置換算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最近最少使用頁面置換算法的一個解釋會是下面這樣:在前面幾條指令中頻繁使用的頁面和可能在後面的幾條指令中被使用。反過來說,已經很久沒有使用的頁面有可能在未來一段時間內仍不會被使用。這個思想揭示了一個可以實現的算法:在缺頁中斷時,置換未使用時間最長的頁面。這個策略稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"LRU(Least Recently Used)"}]},{"type":"text","text":" ,最近最少使用頁面置換算法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然 LRU 在理論上是可以實現的,但是從長遠看來代價比較高。爲了完全實現 LRU,會在內存中維護一個所有頁面的鏈表,最頻繁使用的頁位於表頭,最近最少使用的頁位於表尾。困難的是在每次內存引用時更新整個鏈表。在鏈表中找到一個頁面,刪除它,然後把它移動到表頭是一個非常耗時的操作,即使使用"},{"type":"codeinline","content":[{"type":"text","text":"硬件"}]},{"type":"text","text":"來實現也是一樣的費時。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然而,還有其他方法可以通過硬件實現 LRU。讓我們首先考慮最簡單的方式。這個方法要求硬件有一個 64 位的計數器,它在每條指令執行完成後自動加 1,每個頁表必須有一個足夠容納這個計數器值的域。在每次訪問內存後,將當前的值保存到被訪問頁面的頁表項中。一旦發生缺頁異常,操作系統就檢查所有頁表項中計數器的值,找到值最小的一個頁面,這個頁面就是最少使用的頁面。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"用軟件模擬 LRU"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘管上面的 LRU 算法在原則上是可以實現的,"},{"type":"text","marks":[{"type":"strong"}],"text":"但是很少有機器能夠擁有那些特殊的硬件"},{"type":"text","text":"。上面是硬件的實現方式,那麼現在考慮要用"},{"type":"codeinline","content":[{"type":"text","text":"軟件"}]},{"type":"text","text":"來實現 LRU 。一種可以實現的方案是 "},{"type":"codeinline","content":[{"type":"text","text":"NFU(Not Frequently Used,最不常用)"}]},{"type":"text","text":"算法。它需要一個軟件計數器來和每個頁面關聯,初始化的時候是 0 。在每個時鐘中斷時,操作系統會瀏覽內存中的所有頁,會將每個頁面的 R 位(0 或 1)加到它的計數器上。這個計數器大體上跟蹤了各個頁面訪問的頻繁程度。當缺頁異常出現時,則置換計數器值最小的頁面。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"NFU 最主要的問題是它不會忘記任何東西,想一下是不是這樣?例如,在一個多次(掃描)的編譯器中,在第一遍掃描中頻繁使用的頁面會在後續的掃描中也有較高的計數。事實上,如果第一次掃描的執行時間恰好是各次掃描中最長的,那麼後續遍歷的頁面的統計次數總會比第一次頁面的統計次數"},{"type":"codeinline","content":[{"type":"text","text":"小"}]},{"type":"text","text":"。結果是操作系統將置換有用的頁面而不是不再使用的頁面。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"幸運的是隻需要對 NFU 做一個簡單的修改就可以讓它模擬 LRU,這個修改有兩個步驟"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,在 R 位被添加進來之前先把計數器右移一位;"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二步,R 位被添加到最左邊的位而不是最右邊的位。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"修改以後的算法稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"老化(aging)"}]},{"type":"text","text":" 算法,下圖解釋了老化算法是如何工作的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/43/43dde2f08b4a4d8ef3d118285648f365.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們假設在第一個時鐘週期內頁面 0 - 5 的 R 位依次是 1,0,1,0,1,1,(也就是頁面 0 是 1,頁面 1 是 0,頁面 2 是 1 這樣類推)。也就是說,"},{"type":"text","marks":[{"type":"strong"}],"text":"在 0 個時鐘週期到 1 個時鐘週期之間,0,2,4,5 都被引用了"},{"type":"text","text":",從而把它們的 R 位設置爲 1,剩下的設置爲 0 。在相關的六個計數器被右移之後 R 位被添加到 "},{"type":"codeinline","content":[{"type":"text","text":"左側"}]},{"type":"text","text":" ,就像上圖中的 a。剩下的四列顯示了接下來的四個時鐘週期內的六個計數器變化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當缺頁異常出現時,將"},{"type":"codeinline","content":[{"type":"text","text":"置換(就是移除)"}]},{"type":"text","text":"計數器值最小的頁面。如果一個頁面在前面 4 個時鐘週期內都沒有被訪問過,那麼它的計數器應該會有四個連續的 0 ,因此它的值肯定要比前面 3 個時鐘週期內都沒有被訪問過的頁面的計數器小。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個算法與 LRU 算法有兩個重要的區別:看一下上圖中的 "},{"type":"codeinline","content":[{"type":"text","text":"e"}]},{"type":"text","text":",第三列和第五列"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d3/d3cf4dd08f52c53cadda0d8cd264aecf.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"它們在兩個時鐘週期內都沒有被訪問過,在此之前的時鐘週期內都引用了兩個頁面。根據 LRU 算法,如果需要置換的話,那麼應該在這兩個頁面中選擇一個。那麼問題來了,我萌應該選擇哪個?現在的問題是我們不知道時鐘週期 1 到時鐘週期 2 內它們中哪個頁面是後被訪問到的。因爲在每個時鐘週期內只記錄了一位,所以無法區分在一個時鐘週期內哪個頁面最早被引用,哪個頁面是最後被引用的。因此,我們能做的就是置換"},{"type":"codeinline","content":[{"type":"text","text":"頁面3"}]},{"type":"text","text":","},{"type":"text","marks":[{"type":"strong"}],"text":"因爲頁面 3 在週期 0 - 1 內都沒有被訪問過,而頁面 5 卻被引用過"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LRU 與老化之前的第 2 個區別是,在老化期間,計數器具有有限數量的位(這個例子中是 8 位),這就限制了以往的訪問記錄。如果兩個頁面的計數器都是 0 ,那麼我們可以隨便選擇一個進行置換。實際上,有可能其中一個頁面的訪問次數實在 9 個時鐘週期以前,而另外一個頁面是在 1000 個時鐘週期之前,但是我們卻無法看到這些。在實際過程中,如果時鐘週期是 20 ms,8 位一般是夠用的。所以我們經常拿 20 ms 來舉例。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"工作集頁面置換算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在最單純的分頁系統中,剛啓動進程時,在內存中並沒有頁面。此時如果 CPU 嘗試匹配第一條指令,就會得到一個缺頁異常,使操作系統裝入含有第一條指令的頁面。其他的錯誤比如 "},{"type":"codeinline","content":[{"type":"text","text":"全局變量"}]},{"type":"text","text":"和 "},{"type":"codeinline","content":[{"type":"text","text":"堆棧"}]},{"type":"text","text":" 引起的缺頁異常通常會緊接着發生。一段時間以後,進程需要的大部分頁面都在內存中了,此時進程開始在較少的缺頁異常環境中運行。這個策略稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"請求調頁(demand paging)"}]},{"type":"text","text":",因爲頁面是根據需要被調入的,而不是預先調入的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在一個大的地址空間中系統的讀所有的頁面,將會造成很多缺頁異常,因此會導致沒有足夠的內存來容納這些頁面。不過幸運的是,大部分進程不是這樣工作的,它們都會以"},{"type":"codeinline","content":[{"type":"text","text":"局部性方式(locality of reference)"}]},{"type":"text","text":" 來訪問,這意味着在執行的任何階段,程序只引用其中的一小部分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個進程當前正在使用的頁面的集合稱爲它的 "},{"type":"codeinline","content":[{"type":"text","text":"工作集(working set)"}]},{"type":"text","text":",如果整個工作集都在內存中,那麼進程在運行到下一運行階段(例如,編譯器的下一遍掃面)之前,不會產生很多缺頁中斷。"},{"type":"text","marks":[{"type":"strong"}],"text":"如果內存太小從而無法容納整個工作集,那麼進程的運行過程中會產生大量的缺頁中斷,會導致運行速度也會變得緩慢"},{"type":"text","text":"。因爲通常只需要幾納秒就能執行一條指令,而通常需要十毫秒才能從磁盤上讀入一個頁面。如果一個程序每 10 ms 只能執行一到兩條指令,那麼它將需要很長時間才能運行完。如果只是執行幾條指令就會產生中斷,那麼就稱作這個程序產生了 "},{"type":"codeinline","content":[{"type":"text","text":"顛簸(thrashing)"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在多道程序的系統中,通常會把進程移到磁盤上(即從內存中移走所有的頁面),這樣可以讓其他進程有機會佔用 CPU 。有一個問題是,當進程想要再次把之前調回磁盤的頁面調回內存怎麼辦?從技術的角度上來講,並不需要做什麼,此進程會一直產生缺頁中斷直到它的"},{"type":"codeinline","content":[{"type":"text","text":"工作集"}]},{"type":"text","text":" 被調回內存。然後,每次裝入一個進程需要 20、100 甚至 1000 次缺頁中斷,速度顯然太慢了,並且由於 CPU 需要幾毫秒時間處理一個缺頁中斷,因此由相當多的 CPU 時間也被浪費了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,不少分頁系統中都會設法跟蹤進程的工作集,確保這些工作集在進程運行時被調入內存。這個方法叫做 "},{"type":"codeinline","content":[{"type":"text","text":"工作集模式(working set model)"}]},{"type":"text","text":"。它被設計用來減少缺頁中斷的次數的。在進程運行前首先裝入工作集頁面的這一個過程被稱爲 "},{"type":"codeinline","content":[{"type":"text","text":"預先調頁(prepaging)"}]},{"type":"text","text":",工作集是隨着時間來變化的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據研究表明,大多數程序並不是均勻的訪問地址空間的,而訪問往往是集中於一小部分頁面。一次內存訪問可能會取出一條指令,也可能會取出數據,或者是存儲數據。在任一時刻 t,都存在一個集合,它包含所喲歐最近 k 次內存訪問所訪問過的頁面。這個集合 "},{"type":"codeinline","content":[{"type":"text","text":"w(k,t)"}]},{"type":"text","text":" 就是工作集。因爲最近 k = 1次訪問肯定會訪問最近 k > 1 次訪問所訪問過的頁面,所以 "},{"type":"codeinline","content":[{"type":"text","text":"w(k,t)"}]},{"type":"text","text":" 是 k 的單調遞減函數。隨着 k 的增大,"},{"type":"codeinline","content":[{"type":"text","text":"w(k,t)"}]},{"type":"text","text":" 是不會無限變大的,因爲程序不可能訪問比所能容納頁面數量上限還多的頁面。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/7a/7ad5c0090250bc31225802b7fdfa339a.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事實上大多數應用程序只會任意訪問一小部分頁面集合,但是這個集合會隨着時間而緩慢變化,所以爲什麼一開始曲線會快速上升而 k 較大時上升緩慢。爲了實現工作集模型,操作系統必須跟蹤"},{"type":"text","marks":[{"type":"strong"}],"text":"哪些頁面在工作集中"},{"type":"text","text":"。一個進程從它開始執行到當前所實際使用的 CPU 時間總數通常稱作 "},{"type":"codeinline","content":[{"type":"text","text":"當前實際運行時間"}]},{"type":"text","text":"。進程的工作集可以被稱爲在過去的 t 秒實際運行時間中它所訪問過的頁面集合。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面來簡單描述一下工作集的頁面置換算法,基本思路就是找出一個不在工作集中的頁面並淘汰它。下面是一部分機器頁表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/77/772edb91cb031e0cb34faa22a714c9fe.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因爲只有那些在內存中的頁面纔可以作爲候選者被淘汰,所以該算法忽略了那些不在內存中的頁面。每個表項至少包含兩條信息:上次使用該頁面的近似時間和 R(訪問)位。空白的矩形表示該算法不需要其他字段,例如頁框數量、保護位、修改位。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"算法的工作流程如下,假設硬件要設置 R 和 M 位。同樣的,在每個時鐘週期內,一個週期性的時鐘中斷會使軟件清除 "},{"type":"codeinline","content":[{"type":"text","text":"Referenced(引用)"}]},{"type":"text","text":"位。在每個缺頁異常,頁表會被掃描以找出一個合適的頁面把它置換。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隨着每個頁表項的處理,都需要檢查 R 位。如果 R 位是 1,那麼就會將當前時間寫入頁表項的 "},{"type":"codeinline","content":[{"type":"text","text":"上次使用時間"}]},{"type":"text","text":"域,表示的意思就是缺頁異常發生時頁面正在被使用。因爲頁面在當前時鐘週期內被訪問過,那麼它應該出現在工作集中而不是被刪除(假設 t 是橫跨了多個時鐘週期)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果 R 位是 0 ,那麼在當前的時鐘週期內這個頁面沒有被訪問過,應該作爲被刪除的對象。爲了查看是否應該將其刪除,會計算其使用期限(當前虛擬時間 - 上次使用時間),來用這個時間和 t 進行對比。如果使用期限大於 t,那麼這個頁面就不再工作集中,而使用新的頁面來替換它。然後繼續掃描更新剩下的表項。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然而,如果 R 位是 0 但是使用期限小於等於 t,那麼此頁應該在工作集中。此時就會把頁面臨時保存起來,但是會記"},{"type":"codeinline","content":[{"type":"text","text":"生存時間最長(即上次使用時間的最小值)"}]},{"type":"text","text":"的頁面。如果掃描完整個頁表卻沒有找到適合被置換的頁面,也就意味着所有的頁面都在工作集中。在這種情況下,如果找到了一個或者多個 R = 0 的頁面,就淘汰生存時間最長的頁面。最壞的情況下是,在當前時鐘週期內,所有的頁面都被訪問過了(也就是都有 R = 1),因此就隨機選擇一個頁面淘汰,如果有的話最好選一個未被訪問的頁面,也就是乾淨的頁面。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"工作集時鐘頁面置換算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當缺頁異常發生後,需要掃描整個頁表才能確定被淘汰的頁面,因此基本工作集算法還是比較浪費時間的。一個對基本工作集算法的提升是基於時鐘算法但是卻使用工作集的信息,這種算法稱爲"},{"type":"codeinline","content":[{"type":"text","text":"WSClock(工作集時鐘)"}]},{"type":"text","text":"。由於它的實現簡單並且具有高性能,因此在實踐中被廣泛應用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"與時鐘算法一樣,所需的數據結構是一個以頁框爲元素的循環列表,就像下面這樣"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a9/a98d52be8523db87d323b24092eb7bd3.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最初的時候,該表是空的。當裝入第一個頁面後,把它加載到該表中。隨着更多的頁面的加入,它們形成一個環形結構。每個表項包含來自基本工作集算法的上次使用時間,以及 R 位(已標明)和 M 位(未標明)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"與時鐘算法一樣,在每個缺頁異常時,首先檢查指針指向的頁面。如果 R 位被是設置爲 1,該頁面在當前時鐘週期內就被使用過,那麼該頁面就不適合被淘汰。然後把該頁面的 R 位置爲 0,指針指向下一個頁面,並重復該算法。該事件序列化後的狀態參見圖 b。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在考慮指針指向的頁面 R = 0 時會發生什麼,參見圖 c,如果頁面的使用期限大於 t 並且頁面爲被訪問過,那麼這個頁面就不會在工作集中,並且在磁盤上會有一個此頁面的副本。申請重新調入一個新的頁面,並把新的頁面放在其中,如圖 d 所示。另一方面,如果頁面被修改過,就不能重新申請頁面,因爲這個頁面在磁盤上沒有有效的副本。爲了避免由於調度寫磁盤操作引起的進程切換,指針繼續向前走,算法繼續對下一個頁面進行操作。畢竟,有可能存在一個老的,沒有被修改過的頁面可以立即使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原則上來說,所有的頁面都有可能因爲"},{"type":"codeinline","content":[{"type":"text","text":"磁盤I/O"}]},{"type":"text","text":" 在某個時鐘週期內被調度。爲了降低磁盤阻塞,需要設置一個限制,即最大隻允許寫回 n 個頁面。一旦達到該限制,就不允許調度新的寫操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼就有個問題,指針會繞一圈回到原點的,如果回到原點,它的起始點會發生什麼?這裏有兩種情況:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"至少調度了一次寫操作"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"沒有調度過寫操作"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在第一種情況中,指針僅僅是不停的移動,尋找一個未被修改過的頁面。由於已經調度了一個或者多個寫操作,最終會有某個寫操作完成,它的頁面會被標記爲未修改。置換遇到的第一個未被修改過的頁面,這個頁面不一定是第一個被調度寫操作的頁面,因爲硬盤驅動程序爲了優化性能可能會把寫操作重排序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於第二種情況,所有的頁面都在工作集中,否則將至少調度了一個寫操作。由於缺乏額外的信息,最簡單的方法就是置換一個未被修改的頁面來使用,掃描中需要記錄未被修改的頁面的位置,如果不存在未被修改的頁面,就選定當前頁面並把它寫回磁盤。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"頁面置換算法小結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們到現在已經研究了各種頁面置換算法,現在我們來一個簡單的總結,算法的總結歸納如下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| 算法 | 註釋 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| :-------------------- | :----------------------- |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| 最優算法 | 不可實現,但可以用作基準 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| NRU(最近未使用) 算法 | 和 LRU 算法很相似 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| FIFO(先進先出) 算法 | 有可能會拋棄重要的頁面 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| 第二次機會算法 | 比 FIFO 有較大的改善 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| 時鐘算法 | 實際使用 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| LRU(最近最少)算法 | 比較優秀,但是很難實現 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| NFU(最不經常食用)算法 | 和 LRU 很類似 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| 老化算法 | 近似 LRU 的高效算法 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| 工作集算法 | 實施起來開銷很大 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"| 工作集時鐘算法 | 比較有效的算法 |"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"最優算法"}]},{"type":"text","text":"在當前頁面中置換最後要訪問的頁面。不幸的是,沒有辦法來判定哪個頁面是最後一個要訪問的,"},{"type":"codeinline","content":[{"type":"text","text":"因此實際上該算法不能使用"}]},{"type":"text","text":"。然而,它可以作爲衡量其他算法的標準。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"NRU"}]},{"type":"text","text":" 算法根據 R 位和 M 位的狀態將頁面氛圍四類。從編號最小的類別中隨機選擇一個頁面。NRU 算法易於實現,但是性能不是很好。存在更好的算法。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"FIFO"}]},{"type":"text","text":" 會跟蹤頁面加載進入內存中的順序,並把頁面放入一個鏈表中。有可能刪除存在時間最長但是還在使用的頁面,因此這個算法也不是一個很好的選擇。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"第二次機會"}]},{"type":"text","text":"算法是對 FIFO 的一個修改,它會在刪除頁面之前檢查這個頁面是否仍在使用。如果頁面正在使用,就會進行保留。這個改進大大提高了性能。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"時鐘"}]},{"type":"text","text":" 算法是第二次機會算法的另外一種實現形式,時鐘算法和第二次算法的性能差不多,但是會花費更少的時間來執行算法。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"LRU"}]},{"type":"text","text":" 算法是一個非常優秀的算法,但是沒有"},{"type":"codeinline","content":[{"type":"text","text":"特殊的硬件(TLB)"}]},{"type":"text","text":"很難實現。如果沒有硬件,就不能使用 LRU 算法。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"NFU"}]},{"type":"text","text":" 算法是一種近似於 LRU 的算法,它的性能不是非常好。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"老化"}]},{"type":"text","text":" 算法是一種更接近 LRU 算法的實現,並且可以更好的實現,因此是一個很好的選擇"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後兩種算法都使用了工作集算法。工作集算法提供了合理的性能開銷,但是它的實現比較複雜。"},{"type":"codeinline","content":[{"type":"text","text":"WSClock"}]},{"type":"text","text":" 是另外一種變體,它不僅能夠提供良好的性能,而且可以高效地實現。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總之,"},{"type":"text","marks":[{"type":"strong"}],"text":"最好的算法是老化算法和WSClock算法"},{"type":"text","text":"。他們分別是基於 LRU 和工作集算法。他們都具有良好的性能並且能夠被有效的實現。還存在其他一些好的算法,但實際上這兩個可能是最重要的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"文件系統中的算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"文件系統在備份的過程中會使用到算法,文件備份分爲"},{"type":"text","marks":[{"type":"strong"}],"text":"邏輯轉儲和物理轉儲"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"物理轉儲和邏輯轉儲"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"物理轉儲的主要優點是簡單、極爲快速(基本上是以磁盤的速度運行),缺點是"},{"type":"codeinline","content":[{"type":"text","text":"全量備份"}]},{"type":"text","text":",不能跳過指定目錄,也不能增量轉儲,也不能恢復個人文件的請求。因此句"},{"type":"text","marks":[{"type":"strong"}],"text":"大多數情況下不會使用物理轉儲,而使用邏輯轉儲"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"邏輯轉儲(logical dump)"}]},{"type":"text","text":"從一個或幾個指定的目錄開始,遞歸轉儲自指定日期開始後更改的文件和目錄。因此,在邏輯轉儲中,轉儲磁盤上有一系列經過仔細識別的目錄和文件,這使得根據請求輕鬆還原特定文件或目錄。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"既然邏輯轉儲是最常用的方式,那麼下面就讓我們研究一下邏輯轉儲的通用算法。此算法在 UNIX 系統上廣爲使用,如下圖所示"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/9c/9c99f04b057be311b55d071f1c441b1e.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"待轉儲的文件系統,其中方框代表"},{"type":"codeinline","content":[{"type":"text","text":"目錄"}]},{"type":"text","text":",圓圈代表"},{"type":"codeinline","content":[{"type":"text","text":"文件"}]},{"type":"text","text":"。黃色的項目表是自上次轉儲以來修改過。每個目錄和文件都被標上其 inode 號。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"此算法會轉儲位於修改文件或目錄路徑上的所有目錄(也包括未修改的目錄),原因有兩個。第一是能夠在不同電腦的文件系統中恢復轉儲的文件。通過這種方式,轉儲和重新存儲的程序能夠用來在兩個電腦之間"},{"type":"codeinline","content":[{"type":"text","text":"傳輸整個文件系統"}]},{"type":"text","text":"。第二個原因是能夠對單個文件進行"},{"type":"codeinline","content":[{"type":"text","text":"增量恢復"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"邏輯轉儲算法需要維持一個 inode 爲索引的"},{"type":"codeinline","content":[{"type":"text","text":"位圖(bitmap)"}]},{"type":"text","text":",每個 inode 包含了幾位。隨着算法的進行,位圖中的這些位會被設置或清除。算法的執行分成四個階段。第一階段從"},{"type":"codeinline","content":[{"type":"text","text":"起始目錄(本例爲根目錄)"}]},{"type":"text","text":"開始檢查其中所有的目錄項。對每一個修改過的文件,該算法將在位圖中標記其 inode。算法還會標記並遞歸檢查每一個目錄(不管是否修改過)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在第一階段結束時,所有修改過的文件和全部目錄都在位圖中標記了,如下圖所示"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/28/2870e19dece94eb3d433aacd5d569b7f.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"理論上來說,第二階段再次遞歸遍歷目錄樹,並去掉目錄樹中任何不包含被修改過的文件或目錄的標記。本階段執行的結果如下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/48/4816a4e0f4086bba8ecc005f92266aa3.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意,inode 編號爲 10、11、14、27、29 和 30 的目錄已經被去掉了標記,因爲它們所包含的內容"},{"type":"codeinline","content":[{"type":"text","text":"沒有修改"}]},{"type":"text","text":"。它們也不會轉儲。相反,inode 編號爲 5 和 6 的目錄本身儘管沒有被修改過也要被轉儲,因爲在新的機器上恢復當日的修改時需要這些信息。爲了提高算法效率,可以將這兩階段的目錄樹遍歷合二爲一。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在已經知道了哪些目錄和文件必須被轉儲了,這就是上圖 b 中標記的內容,第三階段算法將以節點號爲序,掃描這些 inode 並轉儲所有標記爲需轉儲的目錄,如下圖所示"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e7/e7a0ae1edcf596b5473ef3b1ee34f76b.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了進行恢復,每個被轉儲的目錄都用目錄的屬性(所有者、時間)作爲前綴。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e6/e647f5408a601ab11ca9caa8475cd8bd.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,在第四階段,上圖中被標記的文件也被轉儲,同樣,由其文件屬性作爲前綴。至此,轉儲結束。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從轉儲磁盤上還原文件系統非常簡單。一開始,需要在磁盤上創建空文件系統。然後恢復最近一次的完整轉儲。由於磁帶上最先出現目錄,所以首先恢復目錄,給出文件系統的"},{"type":"codeinline","content":[{"type":"text","text":"框架(skeleton)"}]},{"type":"text","text":",然後恢復文件系統本身。在完整存儲之後是第一次增量存儲,然後是第二次重複這一過程,以此類推。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘管邏輯存儲十分簡單,但是也會有一些棘手的問題。首先,既然空閒塊列表並不是一個文件,那麼在所有被轉儲的文件恢復完畢之後,就需要從零開始重新構造。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外一個問題是關於"},{"type":"codeinline","content":[{"type":"text","text":"鏈接"}]},{"type":"text","text":"。如果文件鏈接了兩個或者多個目錄,而文件只能還原一次,那麼並且所有指向該文件的目錄都必須還原。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還有一個問題是,UNIX 文件實際上包含了許多 "},{"type":"codeinline","content":[{"type":"text","text":"空洞(holes)"}]},{"type":"text","text":"。打開文件,寫幾個字節,然後找到文件中偏移了一定距離的地址,又寫入更多的字節,這麼做是合法的。但兩者之間的這些塊並不屬於文件本身,從而也不應該在其上進行文件轉儲和恢復。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後,無論屬於哪一個目錄,"},{"type":"text","marks":[{"type":"strong"}],"text":"特殊文件,命名管道以及類似的文件"},{"type":"text","text":"都不應該被轉儲。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"I/O 中的算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 I/O 的磁盤調度中也出現過很多算法,關於尋址和磁盤臂的轉動都會對算法產生影響,下面我們就來一起看下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般情況下,影響磁盤快讀寫的時間由下面幾個因素決定"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"尋道時間 - 尋道時間指的就是將磁盤臂移動到需要讀取磁盤塊上的時間"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"旋轉延遲 - 等待合適的扇區旋轉到磁頭下所需的時間"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"實際數據的讀取或者寫入時間"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這三種時間參數也是磁盤尋道的過程。一般情況下,尋道時間對總時間的影響最大,所以,有效的降低尋道時間能夠提高磁盤的讀取速度。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果磁盤驅動程序每次接收一個請求並按照接收順序完成請求,這種處理方式也就是 "},{"type":"codeinline","content":[{"type":"text","text":"先來先服務(First-Come, First-served, FCFS)"}]},{"type":"text","text":" ,這種方式很難優化尋道時間。因爲每次都會按照順序處理,不管順序如何,有可能這次讀完後需要等待一個磁盤旋轉一週才能繼續讀取,而其他柱面能夠馬上進行讀取,這種情況下每次請求也會排隊。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常情況下,磁盤在進行尋道時,其他進程會產生其他的磁盤請求。磁盤驅動程序會維護一張表,表中會記錄着柱面號當作索引,每個柱面未完成的請求會形成鏈表,鏈表頭存放在表的相應表項中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一種對先來先服務的算法改良的方案是使用 "},{"type":"codeinline","content":[{"type":"text","text":"最短路徑優先(SSF)"}]},{"type":"text","text":" 算法,下面描述了這個算法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假如我們在對磁道 6 號進行尋址時,同時發生了對 11 , 2 , 4, 14, 8, 15, 3 的請求,如果採用先來先服務的原則,如下圖所示"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2b/2b8281dfc6c57977b8e5700560ad62cd.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以計算一下磁盤臂所跨越的磁盤數量爲 5 + 9 + 2 + 10 + 6 + 7 + 12 = 51,相當於是跨越了 51 次盤面,如果使用最短路徑優先,我們來計算一下跨越的盤面"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/c4/c479e607dccd78e0090b7e3a3fd05877.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"跨越的磁盤數量爲 4 + 1 + 1 + 4 + 3 + 3 + 1 = 17 ,相比 51 足足省了兩倍的時間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,最短路徑優先的算法也不是完美無缺的,這種算法照樣存在問題,那就是"},{"type":"codeinline","content":[{"type":"text","text":"優先級"}]},{"type":"text","text":" 問題,"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏有一個原型可以參考就是我們日常生活中的電梯,電梯使用一種"},{"type":"codeinline","content":[{"type":"text","text":"電梯算法(elevator algorithm)"}]},{"type":"text","text":" 來進行調度,從而滿足協調效率和公平性這兩個相互衝突的目標。電梯一般會保持向一個方向移動,直到在那個方向上沒有請求爲止,然後改變方向。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"電梯算法需要維護一個"},{"type":"codeinline","content":[{"type":"text","text":"二進制位"}]},{"type":"text","text":",也就是當前的方向位:"},{"type":"codeinline","content":[{"type":"text","text":"UP(向上)"}]},{"type":"text","text":"或者是 "},{"type":"codeinline","content":[{"type":"text","text":"DOWN(向下)"}]},{"type":"text","text":"。當一個請求處理完成後,磁盤或電梯的驅動程序會檢查該位,如果此位是 UP 位,磁盤臂或者電梯倉移到下一個更高跌未完成的請求。如果高位沒有未完成的請求,則取相反方向。當方向位是 "},{"type":"codeinline","content":[{"type":"text","text":"DOWN "}]},{"type":"text","text":"時,同時存在一個低位的請求,磁盤臂會轉向該點。如果不存在的話,那麼它只是停止並等待。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們舉個例子來描述一下電梯算法,比如各個柱面得到服務的順序是 4,7,10,14,9,6,3,1 ,那麼它的流程圖如下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/9e/9ecdc9d1076993fa866675966e9b7204.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以電梯算法需要跨越的盤面數量是 3 + 3 + 4 + 5 + 3 + 3 + 1 = 22"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"電梯算法通常情況下不如 SSF 算法。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一些磁盤控制器爲軟件提供了一種檢查磁頭下方當前扇區號的方法,使用這樣的控制器,能夠進行另一種優化。如果對一個相同的柱面有兩個或者多個請求正等待處理,驅動程序可以發出請求讀寫下一次要通過磁頭的扇區。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">這裏需要注意一點,當一個柱面有多條磁道時,相繼的請求可能針對不同的磁道,這種選擇沒有代價,因爲選擇磁頭不需要移動磁盤臂也沒有旋轉延遲。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於磁盤來說,最影響性能的就是尋道時間和旋轉延遲,所以一次只讀取一個或兩個扇區的效率是非常低的。出於這個原因,許多磁盤控制器總是讀出多個扇區並進行高速緩存,即使只請求一個扇區時也是這樣。一般情況下讀取一個扇區的同時會讀取該扇區所在的磁道或者是所有剩餘的扇區被讀出,讀出扇區的數量取決於控制器的高速緩存中有多少可用的空間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"磁盤控制器的高速緩存和操作系統的高速緩存有一些不同,磁盤控制器的高速緩存用於緩存沒有實際被請求的塊,而操作系統維護的高速緩存由顯示地讀出的塊組成,並且操作系統會認爲這些塊在近期仍然會頻繁使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當同一個控制器上有多個驅動器時,操作系統應該爲每個驅動器都單獨的維護一個未完成的請求表。一旦有某個驅動器閒置時,就應該發出一個尋道請求來將磁盤臂移到下一個被請求的柱面。如果下一個尋道請求到來時恰好沒有磁盤臂處於正確的位置,那麼驅動程序會在剛剛完成傳輸的驅動器上發出一個新的尋道命令並等待,等待下一次中斷到來時檢查哪個驅動器處於閒置狀態。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"死鎖中的算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在死鎖的處理策略中,其中一點是忽略死鎖帶來的影響(驚呆了),出現過一個叫做"},{"type":"codeinline","content":[{"type":"text","text":"鴕鳥算法"}]},{"type":"text","text":"的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最簡單的解決辦法就是使用"},{"type":"codeinline","content":[{"type":"text","text":"鴕鳥算法(ostrich algorithm)"}]},{"type":"text","text":",把頭埋在沙子裏,假裝問題根本沒有發生。每個人看待這個問題的反應都不同。數學家認爲死鎖是不可接受的,必須通過有效的策略來防止死鎖的產生。工程師想要知道問題發生的頻次,系統因爲其他原因崩潰的次數和死鎖帶來的嚴重後果。如果死鎖發生的頻次很低,而經常會由於硬件故障、編譯器錯誤等其他操作系統問題導致系統崩潰,那麼大多數工程師不會修復死鎖。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在死鎖的檢測中出現過一些算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"每種類型多個資源的死鎖檢測方式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果有多種相同的資源存在,就需要採用另一種方法來檢測死鎖。可以通過構造一個矩陣來檢測從 P1 -> Pn 這 n 個進程中的死鎖。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在我們提供一種基於矩陣的算法來檢測從 P1 到 Pn 這 n 個進程中的死鎖。假設資源類型爲 m,E1 代表資源類型1,E2 表示資源類型 2 ,Ei 代表資源類型 i (1 <= i <= m)。E 表示的是 "},{"type":"codeinline","content":[{"type":"text","text":"現有資源向量(existing resource vector)"}]},{"type":"text","text":",代表每種已存在的資源總數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在我們就需要構造兩個數組:C 表示的是"},{"type":"codeinline","content":[{"type":"text","text":"當前分配矩陣(current allocation matrix)"}]},{"type":"text","text":" ,R 表示的是 "},{"type":"codeinline","content":[{"type":"text","text":"請求矩陣(request matrix)"}]},{"type":"text","text":"。Ci 表示的是 Pi 持有每一種類型資源的資源數。所以,Cij 表示 Pi 持有資源 j 的數量。Rij 表示 Pi 所需要獲得的資源 j 的數量"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3c/3c7ccdaea1c3b1b5b332f40b69e019f4.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般來說,已分配資源 j 的數量加起來再和所有可供使用的資源數相加 = 該類資源的總數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"死鎖的檢測就是基於向量的比較。每個進程起初都是沒有被標記過的,算法會開始對進程做標記,進程被標記後說明進程被執行了,不會進入死鎖,當算法結束時,任何沒有被標記過的進程都會被判定爲死鎖進程。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面我們探討了兩種檢測死鎖的方式,那麼現在你知道怎麼檢測後,你何時去做死鎖檢測呢?一般來說,有兩個考量標準:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每當有資源請求時就去檢測,這種方式會佔用昂貴的 CPU 時間。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"每隔 k 分鐘檢測一次,或者當 CPU 使用率降低到某個標準下去檢測。考慮到 CPU 效率的原因,如果死鎖進程達到一定數量,就沒有多少進程可以運行,所以 CPU 會經常空閒。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"還有死鎖避免的算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"銀行家算法"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"銀行家算法是 Dijkstra 在 1965 年提出的一種調度算法,它本身是一種死鎖的調度算法。它的模型是基於一個城鎮中的銀行家,銀行家向城鎮中的客戶承諾了一定數量的貸款額度。算法要做的就是判斷請求是否會進入一種不安全的狀態。如果是,就拒絕請求,如果請求後系統是安全的,就接受該請求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"比如下面的例子,銀行家一共爲所有城鎮居民提供了 15 單位個貸款額度,一個單位表示 1k 美元,如下所示"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/96/9691098c51edbb3ffd45be807f32ab18.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"城鎮居民都喜歡做生意,所以就會涉及到貸款,每個人能貸款的最大額度不一樣,在某一時刻,A/B/C/D 的貸款金額如下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/dd/dd0c055a4fbde8955bd3d0e1ddf8030e.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面每個人的貸款總額加起來是 13,馬上接近 15,銀行家只能給 A 和 C 進行放貸,可以拖着 B 和 D、所以,可以讓 A 和 C 首先完成,釋放貸款額度,以此來滿足其他居民的貸款。這是一種"},{"type":"codeinline","content":[{"type":"text","text":"安全"}]},{"type":"text","text":"的狀態。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果每個人的請求導致總額會超過甚至接近 15 ,就會處於一種不安全的狀態,如下所示"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/74/7476825d8447879e940c33e975afe1e5.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣,每個人還能貸款至少 2 個單位的額度,如果其中有一個人發起最大額度的貸款請求,就會使系統處於一種死鎖狀態。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":">這裏注意一點:不安全狀態並不一定引起死鎖,由於客戶不一定需要其最大的貸款額度,但是銀行家不敢抱着這種僥倖心理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"銀行家算法就是對每個請求進行檢查,檢查是否請求會引起不安全狀態,如果不會引起,那麼就接受該請求;如果會引起,那麼就推遲該請求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"類似的,還有多個資源的銀行家算法,讀者可以自行了解。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我自己一年肝了四本 PDF,下面是我的 PDF 預覽,讀者可以掃描我的二維碼回覆對應的關鍵字下載 PDF"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/76/76b9255f308717b79eb3d406cd4fb712.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/47/470adc44d5623c01023b1cd70afa7290.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章