主宰作業系統的經典演算法

程式設計師cxuan發表於2020-07-24

此篇文章帶你梳理一下作業系統中都出現過哪些演算法

程式和執行緒管理中的演算法

程式和執行緒在排程時候出現過很多演算法,這些演算法的設計背景是當一個計算機是多道程式設計系統時,會頻繁的有很多程式或者執行緒來同時競爭 CPU 時間片。 那麼如何選擇合適的程式/執行緒執行是一項藝術。當兩個或兩個以上的程式/執行緒處於就緒狀態時,就會發生這種情況。如果只有一個 CPU 可用,那麼必須選擇接下來哪個程式/執行緒可以執行。作業系統中有一個叫做 排程程式(scheduler) 的角色存在,它就是做這件事兒的,排程程式使用的演算法叫做 排程演算法(scheduling algorithm)

排程演算法分類

針對不同的作業系統環境,也有不同的演算法分類,作業系統主要分為下面這幾種

  • 批處理作業系統
  • 互動式作業系統
  • 實時作業系統

下面我們分別來看一下這些作業系統中的演算法。

批處理作業系統中的演算法

設計目標

批處理系統廣泛應用於商業領域,比如用來處理工資單、存貨清單、賬目收入、賬目支出、利息計算、索賠處理和其他週期性作業。在批處理系統中,一般會選擇使用非搶佔式演算法或者週期性比較長搶佔式演算法。這種方法可以減少執行緒切換因此能夠提升效能。

互動式使用者環境中,因為為了使用者體驗,所以會避免長時間佔用程式,所以需要搶佔式演算法。由於某個程式出現錯誤也有可能無限期的排斥其他所有程式。為了避免這種情況,搶佔式也是必須的。

實時系統中,搶佔式不是必須的,因為程式知道自己可能執行不了很長時間,通常很快的做完自己的工作並掛起。

關鍵指標

通常有三個指標來衡量系統工作狀態:吞吐量、週轉時間和 CPU 利用率

  • 吞吐量(throughout) 是系統每小時完成的作業數量。綜合考慮,每小時完成 50 個工作要比每小時完成 40 個工作好。
  • 週轉時間(Turnaround time) 是一種平均時間,它指的是從一個批處理提交開始直到作業完成時刻為止的平均時間。該資料度量了使用者要得到輸出所需的平均等待時間。週轉時間越小越好。
  • CPU 利用率(CPU utilization) 通常作為批處理系統上的指標。即使如此,CPU 利用率也不是一個好的度量指標,真正有價值的衡量指標是系統每小時可以完成多少作業(吞吐量),以及完成作業需要多長時間(週轉時間)。

下面我們就來認識一下批處理中的演算法。

先來先服務

很像是先到先得。。。它是一種非搶佔式的演算法。此演算法將按照請求順序為程式分配 CPU。最基本的,會有一個就緒程式的等待佇列。當第一個任務從外部進入系統時,將會立即啟動並允許執行任意長的時間。它不會因為執行時間太長而中斷。當其他作業進入時,它們排到就緒佇列尾部。當正在執行的程式阻塞,處於等待佇列的第一個程式就開始執行。當一個阻塞的程式重新處於就緒態時,它會像一個新到達的任務,會排在佇列的末尾,即排在所有程式最後。

這個演算法的強大之處在於易於理解程式設計,在這個演算法中,一個單連結串列記錄了所有就緒程式。要選取一個程式執行,只要從該佇列的頭部移走一個程式即可;要新增一個新的作業或者阻塞一個程式,只要把這個作業或程式附加在佇列的末尾即可。這是很簡單的一種實現。

不過,先來先服務也是有缺點的,那就是沒有優先順序的關係,試想一下,如果有 100 個 I/O 程式正在排隊,第 101 個是一個 CPU 密集型程式,那豈不是需要等 100 個 I/O 程式執行完畢才會等到一個 CPU 密集型程式執行,這在實際情況下根本不可能,所以需要優先順序或者搶佔式程式的出現來優先選擇重要的程式執行。

最短作業優先

批處理中的第二種排程演算法是 最短作業優先(Shortest Job First),我們假設執行時間已知。例如,一家保險公司,因為每天要做類似的工作,所以人們可以相當精確地預測處理 1000 個索賠的一批作業需要多長時間。當輸入佇列中有若干個同等重要的作業被啟動時,排程程式應使用最短優先作業演算法

如上圖 a 所示,這裡有 4 個作業 A、B、C、D ,執行時間分別為 8、4、4、4 分鐘。若按圖中的次序執行,則 A 的週轉時間為 8 分鐘,B 為 12 分鐘,C 為 16 分鐘,D 為 20 分鐘,平均時間內為 14 分鐘。

現在考慮使用最短作業優先演算法執行 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 它就只能影響自己的週轉時間了。

需要注意的是,在所有的程式都可以執行的情況下,最短作業優先的演算法才是最優的。

最短剩餘時間優先

最短作業優先的搶佔式版本被稱作為 最短剩餘時間優先(Shortest Remaining Time Next) 演算法。使用這個演算法,排程程式總是選擇剩餘執行時間最短的那個程式執行。當一個新作業到達時,其整個時間同當前程式的剩餘時間做比較。如果新的程式比當前執行程式需要更少的時間,當前程式就被掛起,而執行新的程式。這種方式能夠使短期作業獲得良好的服務。

互動式系統中的排程

互動式系統中在個人計算機、伺服器和其他系統中都是很常用的,所以有必要來探討一下互動式排程

輪詢排程

一種最古老、最簡單、最公平並且最廣泛使用的演算法就是 輪詢演算法(round-robin)。每個程式都會被分配一個時間段,稱為時間片(quantum),在這個時間片內允許程式執行。如果程式在時間片結束前阻塞或結束,則 CPU 立即進行切換。輪詢演算法比較容易實現。排程程式所做的就是維護一個可執行程式的列表,就像下圖中的 a,當一個程式用完時間片後就被移到佇列的末尾,就像下圖的 b。

時間片輪詢排程中唯一有意思的一點就是時間片的長度。從一個程式切換到另一個程式需要一定的時間進行管理處理,包括儲存暫存器的值和記憶體對映、更新不同的表格和列表、清除和重新調入記憶體快取記憶體等。這種切換稱作 程式間切換(process switch)上下文切換(context switch)

優先順序排程

輪詢排程假設了所有的程式是同等重要的。但事實情況可能不是這樣。例如,在一所大學中的等級制度,首先是院長,然後是教授、祕書、後勤人員,最後是學生。這種將外部情況考慮在內就實現了優先順序排程(priority scheduling)

它的基本思想很明確,每個程式都被賦予一個優先順序,優先順序高的程式優先執行。

但是也不意味著高優先順序的程式能夠永遠一直執行下去,排程程式會在每個時鐘中斷期間降低當前執行程式的優先順序。如果此操作導致其優先順序降低到下一個最高程式的優先順序以下,則會發生程式切換。或者,可以為每個程式分配允許執行的最大時間間隔。當時間間隔用完後,下一個高優先順序的程式會得到執行的機會。

可以很方便的將一組程式按優先順序分成若干類,並且在各個類之間採用優先順序排程,而在各類程式的內部採用輪轉排程。下面展示了一個四個優先順序類的系統

它的排程演算法主要描述如下:上面存在優先順序為 4 類的可執行程式,首先會按照輪轉法為每個程式執行一個時間片,此時不理會較低優先順序的程式。若第 4 類程式為空,則按照輪詢的方式執行第三類程式。若第 4 類和第 3 類程式都為空,則按照輪轉法執行第 2 類程式。如果不對優先順序進行調整,則低優先順序的程式很容易產生飢餓現象。

最短程式優先

對於批處理系統而言,由於最短作業優先常常伴隨著最短響應時間,所以如果能夠把它用於互動式程式,那將是非常好的。互動式程式通常遵循下列模式:等待命令、執行命令、等待命令、執行命令。。。如果我們把每個命令的執行都看作一個分離的作業,那麼我們可以通過首先執行最短的作業來使響應時間最短。這裡唯一的問題是如何從當前可執行程式中找出最短的那一個程式。

一種方式是根據程式過去的行為進行推測,並執行估計執行時間最短的那一個。假設每個終端上每條命令的預估執行時間為 T0,現在假設測量到其下一次執行時間為 T1,可以用兩個值的加權來改進估計時間,即aT0+ (1- 1)T1。通過選擇 a 的值,可以決定是儘快忘掉老的執行時間,還是在一段長時間內始終記住它們。當 a = 1/2 時,可以得到下面這個序列

可以看到,在三輪過後,T0 在新的估計值中所佔比重下降至 1/8。

有時把這種通過當前測量值和先前估計值進行加權平均從而得到下一個估計值的技術稱作 老化(aging)。這種方法會使用很多預測值基於當前值的情況。

保證排程

一種完全不同的排程方法是對使用者做出明確的效能保證。一種實際而且容易實現的保證是:若使用者工作時有 n 個使用者登入,則每個使用者將獲得 CPU 處理能力的 1/n。類似地,在一個有 n 個程式執行的單使用者系統中,若所有的程式都等價,則每個程式將獲得 1/n 的 CPU 時間。

彩票排程

對使用者進行承諾並在隨後兌現承諾是一件好事,不過很難實現。但是有一種既可以給出預測結果而又有一種比較簡單的實現方式的演算法,就是 彩票排程(lottery scheduling)演算法。

其基本思想是為程式提供各種系統資源(例如 CPU 時間)的彩票。當做出一個排程決策的時候,就隨機抽出一張彩票,擁有彩票的程式將獲得該資源。在應用到 CPU 排程時,系統可以每秒持有 50 次抽獎,每個中獎者將獲得比如 20 毫秒的 CPU 時間作為獎勵。

如果希望程式之間協作的話可以交換它們之間的票據。例如,客戶端程式給伺服器程式傳送了一條訊息後阻塞,客戶端程式可能會把自己所有的票據都交給伺服器,來增加下一次伺服器執行的機會。當服務完成後,它會把彩票還給客戶端讓其有機會再次執行。事實上,如果沒有客戶機,伺服器也根本不需要彩票。

可以把彩票理解為 buff,這個 buff 有 15% 的機率能讓你產生 速度之靴 的效果。

公平分享排程

到目前為止,我們假設被排程的都是各個程式自身,而不用考慮該程式的擁有者是誰。結果是,如果使用者 1 啟動了 9 個程式,而使用者 2 啟動了一個程式,使用輪轉或相同優先順序排程演算法,那麼使用者 1 將得到 90 % 的 CPU 時間,而使用者 2 將之得到 10 % 的 CPU 時間。

為了阻止這種情況的出現,一些系統在排程前會把程式的擁有者考慮在內。在這種模型下,每個使用者都會分配一些CPU 時間,而排程程式會選擇程式並強制執行。因此如果兩個使用者每個都會有 50% 的 CPU 時間片保證,那麼無論一個使用者有多少個程式,都將獲得相同的 CPU 份額。

實時系統中的排程

實時系統(real-time) 對於時間有要求的系統。實時系統可以分為兩類,硬實時(hard real time)軟實時(soft real time) 系統,前者意味著必須要滿足絕對的截止時間;後者的含義是雖然不希望偶爾錯失截止時間,但是可以容忍。在這兩種情形中,實時都是通過把程式劃分為一組程式而實現的,其中每個程式的行為是可預測和提前可知的。這些程式一般壽命較短,並且極快的執行完成。在檢測到一個外部訊號時,排程程式的任務就是按照滿足所有截止時間的要求排程程式。

實時系統中的事件可以按照響應方式進一步分類為週期性(以規則的時間間隔發生)事件或 非週期性(發生時間不可預知)事件。一個系統可能要響應多個週期性事件流,根據每個事件處理所需的時間,可能甚至無法處理所有事件。例如,如果有 m 個週期事件,事件 i 以週期 Pi 發生,並需要 Ci 秒 CPU 時間處理一個事件,那麼可以處理負載的條件是

只有滿足這個條件的實時系統稱為可排程的,這意味著它實際上能夠被實現。一個不滿足此檢驗標準的程式不能被排程,因為這些程式共同需要的 CPU 時間總和大於 CPU 能提供的時間。

實時系統的排程演算法可以是靜態的或動態的。前者在系統開始執行之前做出排程決策;後者在執行過程中進行排程決策。只有在可以提前掌握所完成的工作以及必須滿足的截止時間等資訊時,靜態排程才能工作,而動態排程不需要這些限制。

排程策略和機制

到目前為止,我們隱含的假設系統中所有程式屬於不同的分組使用者並且程式間存在相互競爭 CPU 的情況。通常情況下確實如此,但有時也會發生一個程式會有很多子程式並在其控制下執行的情況。例如,一個資料庫管理系統程式會有很多子程式。每一個子程式可能處理不同的請求,或者每個子程式實現不同的功能(如請求分析、磁碟訪問等)。主程式完全可能掌握哪一個子程式最重要(或最緊迫),而哪一個最不重要。但是,以上討論的排程演算法中沒有一個演算法從使用者程式接收有關的排程決策資訊,這就導致了排程程式很少能夠做出最優的選擇。

解決問題的辦法是將 排程機制(scheduling mechanism)排程策略(scheduling policy) 分開,這是長期一貫的原則。這也就意味著排程演算法在某種方式下被引數化了,但是引數可以被使用者程式填寫。讓我們首先考慮資料庫的例子。假設核心使用優先順序排程演算法,並提供了一條可供程式設定優先順序的系統呼叫。這樣,儘管父程式本身並不參與排程,但它可以控制如何排程子程式的細節。排程機制位於核心,而排程策略由使用者程式決定,排程策略和機制分離是一種關鍵性思路。

記憶體管理中的演算法

作業系統在記憶體管理上也出現過許多演算法,這些演算法的目標的最終目的都是為了合理分配記憶體。

作業系統有兩種記憶體管理方式,一種是點陣圖,一種是 連結串列

在使用連結串列管理記憶體時,有幾種方法的變體

當按照地址順序在連結串列中存放程式和空閒區時,有幾種演算法可以為建立的程式(或者從磁碟中換入的程式)分配記憶體。我們先假設記憶體管理器知道應該分配多少記憶體,最簡單的演算法是使用 首次適配(first fit)。記憶體管理器會沿著段列表進行掃描,直到找個一個足夠大的空閒區為止。除非空閒區大小和要分配的空間大小一樣,否則將空閒區分為兩部分,一部分供程式使用;一部分生成新的空閒區。首次適配演算法是一種速度很快的演算法,因為它會盡可能的搜尋連結串列。

首次適配的一個小的變體是 下次適配(next fit)。它和首次匹配的工作方式相同,只有一個不同之處那就是下次適配在每次找到合適的空閒區時就會記錄當時的位置,以便下次尋找空閒區時從上次結束的地方開始搜尋,而不是像首次匹配演算法那樣每次都會從頭開始搜尋。Bays(1997) 證明了下次適配演算法的效能略低於首次匹配演算法

另外一個著名的並且廣泛使用的演算法是 最佳適配(best fit)。最佳適配會從頭到尾尋找整個連結串列,找出能夠容納程式的最小空閒區。最佳適配演算法會試圖找出最接近實際需要的空閒區,以最好的匹配請求和可用空閒區,而不是先一次拆分一個以後可能會用到的大的空閒區。比如現在我們需要一個大小為 2 的塊,那麼首次匹配演算法會把這個塊分配在位置 5 的空閒區,而最佳適配演算法會把該塊分配在位置為 18 的空閒區,如下

那麼最佳適配演算法的效能如何呢?最佳適配會遍歷整個連結串列,所以最佳適配演算法的效能要比首次匹配演算法差。但是令人想不到的是,最佳適配演算法要比首次匹配和下次匹配演算法浪費更多的記憶體,因為它會產生大量無用的小緩衝區,首次匹配演算法生成的空閒區會更大一些。

最佳適配的空閒區會分裂出很多非常小的緩衝區,為了避免這一問題,可以考慮使用 最差適配(worst fit) 演算法。即總是分配最大的記憶體區域(所以你現在明白為什麼最佳適配演算法會分裂出很多小緩衝區了吧),使新分配的空閒區比較大從而可以繼續使用。模擬程式表明最差適配演算法也不是一個好主意。

如果為程式和空閒區維護各自獨立的連結串列,那麼這四個演算法的速度都能得到提高。這樣,這四種演算法的目標都是為了檢查空閒區而不是程式。但這種分配速度的提高的一個不可避免的代價是增加複雜度和減慢記憶體釋放速度,因為必須將一個回收的段從程式連結串列中刪除並插入空閒連結串列區。

如果程式和空閒區使用不同的連結串列,那麼可以按照大小對空閒區連結串列排序,以便提高最佳適配演算法的速度。在使用最佳適配演算法搜尋由小到大排列的空閒區連結串列時,只要找到一個合適的空閒區,則這個空閒區就是能容納這個作業的最小空閒區,因此是最佳匹配。因為空閒區連結串列以單連結串列形式組織,所以不需要進一步搜尋。空閒區連結串列按大小排序時,首次適配演算法與最佳適配演算法一樣快,而下次適配演算法在這裡毫無意義。

另一種分配演算法是 快速適配(quick fit) 演算法,它為那些常用大小的空閒區維護單獨的連結串列。例如,有一個 n 項的表,該表的第一項是指向大小為 4 KB 的空閒區連結串列表頭指標,第二項是指向大小為 8 KB 的空閒區連結串列表頭指標,第三項是指向大小為 12 KB 的空閒區連結串列表頭指標,以此類推。比如 21 KB 這樣的空閒區既可以放在 20 KB 的連結串列中,也可以放在一個專門存放大小比較特別的空閒區連結串列中。

快速匹配演算法尋找一個指定代銷的空閒區也是十分快速的,但它和所有將空閒區按大小排序的方案一樣,都有一個共同的缺點,即在一個程式終止或被換出時,尋找它的相鄰塊並檢視是否可以合併的過程都是非常耗時的。如果不進行合併,記憶體將會很快分裂出大量程式無法利用的小空閒區。

頁面置換演算法

頁面置換有非常多的演算法,下面一起來認識一下

當發生缺頁異常時,作業系統會選擇一個頁面進行換出從而為新進來的頁面騰出空間。如果要換出的頁面在記憶體中已經被修改,那麼必須將其寫到磁碟中以使磁碟副本保持最新狀態。如果頁面沒有被修改過,並且磁碟中的副本也已經是最新的,那麼就不需要進行重寫。那麼就直接使用調入的頁面覆蓋需要移除的頁面就可以了。

當發生缺頁中斷時,雖然可以隨機的選擇一個頁面進行置換,但是如果每次都選擇一個不常用的頁面會提升系統的效能。如果一個經常使用的頁面被換出,那麼這個頁面在短時間內又可能被重複使用,那麼就可能會造成額外的效能開銷。在關於頁面的主題上有很多頁面置換演算法(page replacement algorithms),這些已經從理論上和實踐上得到了證明。

下面我們就來探討一下有哪些頁面置換演算法。

最優頁面置換演算法

最優的頁面置換演算法很容易描述,但在實際情況下很難實現。它的工作流程如下:在缺頁中斷髮生時,這些頁面之一將在下一條指令(包含該指令的頁面)上被引用。其他頁面則可能要到 10、100 或者 1000 條指令後才會被訪問。每個頁面都可以用在該頁首次被訪問前所要執行的指令數作為標記。

最優化的頁面演算法表明應該標記最大的頁面。如果一個頁面在 800 萬條指令內不會被使用,另外一個頁面在 600 萬條指令內不會被使用,則置換前一個頁面,從而把需要調入這個頁面而發生的缺頁中斷推遲。計算機也像人類一樣,會把不願意做的事情儘可能的往後拖。

這個演算法最大的問題時無法實現。當缺頁中斷髮生時,作業系統無法知道各個頁面的下一次將在什麼時候被訪問。這種演算法在實際過程中根本不會使用。

最近未使用頁面置換演算法

為了能夠讓作業系統收集頁面使用資訊,大部分使用虛擬地址的計算機都有兩個狀態位,R 和 M,來和每個頁面進行關聯。每當引用頁面(讀入或寫入)時都設定 R,寫入(即修改)頁面時設定 M,這些位包含在每個頁表項中,就像下面所示

因為每次訪問時都會更新這些位,因此由硬體來設定它們非常重要。一旦某個位被設定為 1,就會一直保持 1 直到作業系統下次來修改此位。

如果硬體沒有這些位,那麼可以使用作業系統的缺頁中斷時鐘中斷機制來進行模擬。當啟動一個程式時,將其所有的頁面都標記為不在記憶體;一旦訪問任何一個頁面就會引發一次缺頁中斷,此時作業系統就可以設定 R 位(在它的內部表中),修改頁表項使其指向正確的頁面,並設定為 READ ONLY 模式,然後重新啟動引起缺頁中斷的指令。如果頁面隨後被修改,就會發生另一個缺頁異常。從而允許作業系統設定 M 位並把頁面的模式設定為 READ/WRITE

可以用 R 位和 M 位來構造一個簡單的頁面置換演算法:當啟動一個程式時,作業系統將其所有頁面的兩個位都設定為 0。R 位定期的被清零(在每個時鐘中斷)。用來將最近未引用的頁面和已引用的頁面分開。

當出現缺頁中斷後,作業系統會檢查所有的頁面,並根據它們的 R 位和 M 位將當前值分為四類:

  • 第 0 類:沒有引用 R,沒有修改 M
  • 第 1 類:沒有引用 R,已修改 M
  • 第 2 類:引用 R ,沒有修改 M
  • 第 3 類:已被訪問 R,已被修改 M

儘管看起來好像無法實現第一類頁面,但是當第三類頁面的 R 位被時鐘中斷清除時,它們就會發生。時鐘中斷不會清除 M 位,因為需要這個資訊才能知道是否寫回磁碟中。清除 R 但不清除 M 會導致出現一類頁面。

NRU(Not Recently Used) 演算法從編號最小的非空類中隨機刪除一個頁面。此演算法隱含的思想是,在一個時鐘內(約 20 ms)淘汰一個已修改但是沒有被訪問的頁面要比一個大量引用的未修改頁面好,NRU 的主要優點是易於理解並且能夠有效的實現

先進先出頁面置換演算法

另一種開銷較小的方式是使用 FIFO(First-In,First-Out) 演算法,這種型別的資料結構也適用在頁面置換演算法中。由作業系統維護一個所有在當前記憶體中的頁面的連結串列,最早進入的放在表頭,最新進入的頁面放在表尾。在發生缺頁異常時,會把頭部的頁移除並且把新的頁新增到表尾。

先進先出頁面可能是最簡單的頁面替換演算法了。在這種演算法中,作業系統會跟蹤連結串列中記憶體中的所有頁。下面我們舉個例子看一下(這個演算法我剛開始看的時候有點懵逼,後來才看懂,我還是很菜)

  • 初始化的時候,沒有任何頁面,所以第一次的時候會檢查頁面 1 是否位於連結串列中,沒有在連結串列中,那麼就是 MISS,頁面1 進入連結串列,連結串列的先進先出的方向如圖所示。
  • 類似的,第二次會先檢查頁面 2 是否位於連結串列中,沒有在連結串列中,那麼頁面 2 進入連結串列,狀態為 MISS,依次類推。
  • 我們來看第四次,此時的連結串列為 1 2 3 ,第四次會檢查頁面 2 是否位於連結串列中,經過檢索後,發現 2 在連結串列中,那麼狀態就是 HIT,並不會再進行入隊和出隊操作,第五次也是一樣的。
  • 下面來看第六次,此時的連結串列還是 1 2 3,因為之前沒有執行進入連結串列操作,頁面 5 會首先進行檢查,發現連結串列中沒有頁面 5 ,則執行頁面 5 的進入連結串列操作,頁面 2 執行出連結串列的操作,執行完成後的連結串列順序為 2 3 5

第二次機會頁面置換演算法

我們上面學到的 FIFO 連結串列頁面有個缺陷,那就是出鏈和入鏈並不會進行 check 檢查,這樣就會容易把經常使用的頁面置換出去,為了避免這一問題,我們對該演算法做一個簡單的修改:我們檢查最老頁面的 R 位,如果是 0 ,那麼這個頁面就是最老的而且沒有被使用,那麼這個頁面就會被立刻換出。如果 R 位是 1,那麼就清除此位,此頁面會被放在連結串列的尾部,修改它的裝入時間就像剛放進來的一樣。然後繼續搜尋。

這種演算法叫做 第二次機會(second chance)演算法,就像下面這樣,我們看到頁面 A 到 H 保留在連結串列中,並按到達記憶體的時間排序。

a)按照先進先出的方法排列的頁面;b)在時刻 20 處發生缺頁異常中斷並且 A 的 R 位已經設定時的頁面連結串列。

假設缺頁異常發生在時刻 20 處,這時最老的頁面是 A ,它是在 0 時刻到達的。如果 A 的 R 位是 0,那麼它將被淘汰出記憶體,或者把它寫回磁碟(如果它已經被修改過),或者只是簡單的放棄(如果它是未被修改過)。另一方面,如果它的 R 位已經設定了,則將 A 放到連結串列的尾部並且重新設定裝入時間為當前時刻(20 處),然後清除 R 位。然後從 B 頁面開始繼續搜尋合適的頁面。

尋找第二次機會的是在最近的時鐘間隔中未被訪問過的頁面。如果所有的頁面都被訪問過,該演算法就會被簡化為單純的 FIFO 演算法。具體來說,假設圖 a 中所有頁面都設定了 R 位。作業系統將頁面依次移到連結串列末尾,每次都在新增到末尾時清除 R 位。最後,演算法又會回到頁面 A,此時的 R 位已經被清除,那麼頁面 A 就會被執行出鏈處理,因此演算法能夠正常結束。

時鐘頁面置換演算法

即使上面提到的第二次頁面置換演算法也是一種比較合理的演算法,但它經常要在連結串列中移動頁面,既降低了效率,而且這種演算法也不是必須的。一種比較好的方式是把所有的頁面都儲存在一個類似鐘面的環形連結串列中,一個錶針指向最老的頁面。如下圖所示

當缺頁錯誤出現時,演算法首先檢查錶針指向的頁面,如果它的 R 位是 0 就淘汰該頁面,並把新的頁面插入到這個位置,然後把錶針向前移動一位;如果 R 位是 1 就清除 R 位並把錶針前移一個位置。重複這個過程直到找到了一個 R 位為 0 的頁面位置。瞭解這個演算法的工作方式,就明白為什麼它被稱為 時鐘(clokc)演算法了。

最近最少使用頁面置換演算法

最近最少使用頁面置換演算法的一個解釋會是下面這樣:在前面幾條指令中頻繁使用的頁面和可能在後面的幾條指令中被使用。反過來說,已經很久沒有使用的頁面有可能在未來一段時間內仍不會被使用。這個思想揭示了一個可以實現的演算法:在缺頁中斷時,置換未使用時間最長的頁面。這個策略稱為 LRU(Least Recently Used) ,最近最少使用頁面置換演算法。

雖然 LRU 在理論上是可以實現的,但是從長遠看來代價比較高。為了完全實現 LRU,會在記憶體中維護一個所有頁面的連結串列,最頻繁使用的頁位於表頭,最近最少使用的頁位於表尾。困難的是在每次記憶體引用時更新整個連結串列。在連結串列中找到一個頁面,刪除它,然後把它移動到表頭是一個非常耗時的操作,即使使用硬體來實現也是一樣的費時。

然而,還有其他方法可以通過硬體實現 LRU。讓我們首先考慮最簡單的方式。這個方法要求硬體有一個 64 位的計數器,它在每條指令執行完成後自動加 1,每個頁表必須有一個足夠容納這個計數器值的域。在每次訪問記憶體後,將當前的值儲存到被訪問頁面的頁表項中。一旦發生缺頁異常,作業系統就檢查所有頁表項中計數器的值,找到值最小的一個頁面,這個頁面就是最少使用的頁面。

用軟體模擬 LRU

儘管上面的 LRU 演算法在原則上是可以實現的,但是很少有機器能夠擁有那些特殊的硬體。上面是硬體的實現方式,那麼現在考慮要用軟體來實現 LRU 。一種可以實現的方案是 NFU(Not Frequently Used,最不常用)演算法。它需要一個軟體計數器來和每個頁面關聯,初始化的時候是 0 。在每個時鐘中斷時,作業系統會瀏覽記憶體中的所有頁,會將每個頁面的 R 位(0 或 1)加到它的計數器上。這個計數器大體上跟蹤了各個頁面訪問的頻繁程度。當缺頁異常出現時,則置換計數器值最小的頁面。

NFU 最主要的問題是它不會忘記任何東西,想一下是不是這樣?例如,在一個多次(掃描)的編譯器中,在第一遍掃描中頻繁使用的頁面會在後續的掃描中也有較高的計數。事實上,如果第一次掃描的執行時間恰好是各次掃描中最長的,那麼後續遍歷的頁面的統計次數總會比第一次頁面的統計次數。結果是作業系統將置換有用的頁面而不是不再使用的頁面。

幸運的是隻需要對 NFU 做一個簡單的修改就可以讓它模擬 LRU,這個修改有兩個步驟

  • 首先,在 R 位被新增進來之前先把計數器右移一位;
  • 第二步,R 位被新增到最左邊的位而不是最右邊的位。

修改以後的演算法稱為 老化(aging) 演算法,下圖解釋了老化演算法是如何工作的。

我們假設在第一個時鐘週期內頁面 0 - 5 的 R 位依次是 1,0,1,0,1,1,(也就是頁面 0 是 1,頁面 1 是 0,頁面 2 是 1 這樣類推)。也就是說,在 0 個時鐘週期到 1 個時鐘週期之間,0,2,4,5 都被引用了,從而把它們的 R 位設定為 1,剩下的設定為 0 。在相關的六個計數器被右移之後 R 位被新增到 左側 ,就像上圖中的 a。剩下的四列顯示了接下來的四個時鐘週期內的六個計數器變化。

當缺頁異常出現時,將置換(就是移除)計數器值最小的頁面。如果一個頁面在前面 4 個時鐘週期內都沒有被訪問過,那麼它的計數器應該會有四個連續的 0 ,因此它的值肯定要比前面 3 個時鐘週期內都沒有被訪問過的頁面的計數器小。

這個演算法與 LRU 演算法有兩個重要的區別:看一下上圖中的 e,第三列和第五列

它們在兩個時鐘週期內都沒有被訪問過,在此之前的時鐘週期內都引用了兩個頁面。根據 LRU 演算法,如果需要置換的話,那麼應該在這兩個頁面中選擇一個。那麼問題來了,我萌應該選擇哪個?現在的問題是我們不知道時鐘週期 1 到時鐘週期 2 內它們中哪個頁面是後被訪問到的。因為在每個時鐘週期內只記錄了一位,所以無法區分在一個時鐘週期內哪個頁面最早被引用,哪個頁面是最後被引用的。因此,我們能做的就是置換頁面3因為頁面 3 在週期 0 - 1 內都沒有被訪問過,而頁面 5 卻被引用過

LRU 與老化之前的第 2 個區別是,在老化期間,計數器具有有限數量的位(這個例子中是 8 位),這就限制了以往的訪問記錄。如果兩個頁面的計數器都是 0 ,那麼我們可以隨便選擇一個進行置換。實際上,有可能其中一個頁面的訪問次數實在 9 個時鐘週期以前,而另外一個頁面是在 1000 個時鐘週期之前,但是我們卻無法看到這些。在實際過程中,如果時鐘週期是 20 ms,8 位一般是夠用的。所以我們經常拿 20 ms 來舉例。

工作集頁面置換演算法

在最單純的分頁系統中,剛啟動程式時,在記憶體中並沒有頁面。此時如果 CPU 嘗試匹配第一條指令,就會得到一個缺頁異常,使作業系統裝入含有第一條指令的頁面。其他的錯誤比如 全域性變數堆疊 引起的缺頁異常通常會緊接著發生。一段時間以後,程式需要的大部分頁面都在記憶體中了,此時程式開始在較少的缺頁異常環境中執行。這個策略稱為 請求調頁(demand paging),因為頁面是根據需要被調入的,而不是預先調入的。

在一個大的地址空間中系統的讀所有的頁面,將會造成很多缺頁異常,因此會導致沒有足夠的記憶體來容納這些頁面。不過幸運的是,大部分程式不是這樣工作的,它們都會以區域性性方式(locality of reference) 來訪問,這意味著在執行的任何階段,程式只引用其中的一小部分。

一個程式當前正在使用的頁面的集合稱為它的 工作集(working set),如果整個工作集都在記憶體中,那麼程式在執行到下一執行階段(例如,編譯器的下一遍掃面)之前,不會產生很多缺頁中斷。如果記憶體太小從而無法容納整個工作集,那麼程式的執行過程中會產生大量的缺頁中斷,會導致執行速度也會變得緩慢。因為通常只需要幾納秒就能執行一條指令,而通常需要十毫秒才能從磁碟上讀入一個頁面。如果一個程式每 10 ms 只能執行一到兩條指令,那麼它將需要很長時間才能執行完。如果只是執行幾條指令就會產生中斷,那麼就稱作這個程式產生了 顛簸(thrashing)

在多道程式的系統中,通常會把程式移到磁碟上(即從記憶體中移走所有的頁面),這樣可以讓其他程式有機會佔用 CPU 。有一個問題是,當程式想要再次把之前調回磁碟的頁面調回記憶體怎麼辦?從技術的角度上來講,並不需要做什麼,此程式會一直產生缺頁中斷直到它的工作集 被調回記憶體。然後,每次裝入一個程式需要 20、100 甚至 1000 次缺頁中斷,速度顯然太慢了,並且由於 CPU 需要幾毫秒時間處理一個缺頁中斷,因此由相當多的 CPU 時間也被浪費了。

因此,不少分頁系統中都會設法跟蹤程式的工作集,確保這些工作集在程式執行時被調入記憶體。這個方法叫做 工作集模式(working set model)。它被設計用來減少缺頁中斷的次數的。在程式執行前首先裝入工作集頁面的這一個過程被稱為 預先調頁(prepaging),工作集是隨著時間來變化的。

根據研究表明,大多數程式並不是均勻的訪問地址空間的,而訪問往往是集中於一小部分頁面。一次記憶體訪問可能會取出一條指令,也可能會取出資料,或者是儲存資料。在任一時刻 t,都存在一個集合,它包含所喲歐最近 k 次記憶體訪問所訪問過的頁面。這個集合 w(k,t) 就是工作集。因為最近 k = 1次訪問肯定會訪問最近 k > 1 次訪問所訪問過的頁面,所以 w(k,t) 是 k 的單調遞減函式。隨著 k 的增大,w(k,t) 是不會無限變大的,因為程式不可能訪問比所能容納頁面數量上限還多的頁面。

事實上大多數應用程式只會任意訪問一小部分頁面集合,但是這個集合會隨著時間而緩慢變化,所以為什麼一開始曲線會快速上升而 k 較大時上升緩慢。為了實現工作集模型,作業系統必須跟蹤哪些頁面在工作集中。一個程式從它開始執行到當前所實際使用的 CPU 時間總數通常稱作 當前實際執行時間。程式的工作集可以被稱為在過去的 t 秒實際執行時間中它所訪問過的頁面集合。

下面來簡單描述一下工作集的頁面置換演算法,基本思路就是找出一個不在工作集中的頁面並淘汰它。下面是一部分機器頁表

因為只有那些在記憶體中的頁面才可以作為候選者被淘汰,所以該演算法忽略了那些不在記憶體中的頁面。每個表項至少包含兩條資訊:上次使用該頁面的近似時間和 R(訪問)位。空白的矩形表示該演算法不需要其他欄位,例如頁框數量、保護位、修改位。

演算法的工作流程如下,假設硬體要設定 R 和 M 位。同樣的,在每個時鐘週期內,一個週期性的時鐘中斷會使軟體清除 Referenced(引用)位。在每個缺頁異常,頁表會被掃描以找出一個合適的頁面把它置換。

隨著每個頁表項的處理,都需要檢查 R 位。如果 R 位是 1,那麼就會將當前時間寫入頁表項的 上次使用時間域,表示的意思就是缺頁異常發生時頁面正在被使用。因為頁面在當前時鐘週期內被訪問過,那麼它應該出現在工作集中而不是被刪除(假設 t 是橫跨了多個時鐘週期)。

如果 R 位是 0 ,那麼在當前的時鐘週期內這個頁面沒有被訪問過,應該作為被刪除的物件。為了檢視是否應該將其刪除,會計算其使用期限(當前虛擬時間 - 上次使用時間),來用這個時間和 t 進行對比。如果使用期限大於 t,那麼這個頁面就不再工作集中,而使用新的頁面來替換它。然後繼續掃描更新剩下的表項。

然而,如果 R 位是 0 但是使用期限小於等於 t,那麼此頁應該在工作集中。此時就會把頁面臨時儲存起來,但是會記生存時間最長(即上次使用時間的最小值)的頁面。如果掃描完整個頁表卻沒有找到適合被置換的頁面,也就意味著所有的頁面都在工作集中。在這種情況下,如果找到了一個或者多個 R = 0 的頁面,就淘汰生存時間最長的頁面。最壞的情況下是,在當前時鐘週期內,所有的頁面都被訪問過了(也就是都有 R = 1),因此就隨機選擇一個頁面淘汰,如果有的話最好選一個未被訪問的頁面,也就是乾淨的頁面。

工作集時鐘頁面置換演算法

當缺頁異常發生後,需要掃描整個頁表才能確定被淘汰的頁面,因此基本工作集演算法還是比較浪費時間的。一個對基本工作集演算法的提升是基於時鐘演算法但是卻使用工作集的資訊,這種演算法稱為WSClock(工作集時鐘)。由於它的實現簡單並且具有高效能,因此在實踐中被廣泛應用。

與時鐘演算法一樣,所需的資料結構是一個以頁框為元素的迴圈列表,就像下面這樣

最初的時候,該表是空的。當裝入第一個頁面後,把它載入到該表中。隨著更多的頁面的加入,它們形成一個環形結構。每個表項包含來自基本工作集演算法的上次使用時間,以及 R 位(已標明)和 M 位(未標明)。

與時鐘演算法一樣,在每個缺頁異常時,首先檢查指標指向的頁面。如果 R 位被是設定為 1,該頁面在當前時鐘週期內就被使用過,那麼該頁面就不適合被淘汰。然後把該頁面的 R 位置為 0,指標指向下一個頁面,並重復該演算法。該事件序列化後的狀態參見圖 b。

現在考慮指標指向的頁面 R = 0 時會發生什麼,參見圖 c,如果頁面的使用期限大於 t 並且頁面為被訪問過,那麼這個頁面就不會在工作集中,並且在磁碟上會有一個此頁面的副本。申請重新調入一個新的頁面,並把新的頁面放在其中,如圖 d 所示。另一方面,如果頁面被修改過,就不能重新申請頁面,因為這個頁面在磁碟上沒有有效的副本。為了避免由於排程寫磁碟操作引起的程式切換,指標繼續向前走,演算法繼續對下一個頁面進行操作。畢竟,有可能存在一個老的,沒有被修改過的頁面可以立即使用。

原則上來說,所有的頁面都有可能因為磁碟I/O 在某個時鐘週期內被排程。為了降低磁碟阻塞,需要設定一個限制,即最大隻允許寫回 n 個頁面。一旦達到該限制,就不允許排程新的寫操作。

那麼就有個問題,指標會繞一圈回到原點的,如果回到原點,它的起始點會發生什麼?這裡有兩種情況:

  • 至少排程了一次寫操作
  • 沒有排程過寫操作

在第一種情況中,指標僅僅是不停的移動,尋找一個未被修改過的頁面。由於已經排程了一個或者多個寫操作,最終會有某個寫操作完成,它的頁面會被標記為未修改。置換遇到的第一個未被修改過的頁面,這個頁面不一定是第一個被排程寫操作的頁面,因為硬碟驅動程式為了優化效能可能會把寫操作重排序。

對於第二種情況,所有的頁面都在工作集中,否則將至少排程了一個寫操作。由於缺乏額外的資訊,最簡單的方法就是置換一個未被修改的頁面來使用,掃描中需要記錄未被修改的頁面的位置,如果不存在未被修改的頁面,就選定當前頁面並把它寫回磁碟。

頁面置換演算法小結

我們到現在已經研究了各種頁面置換演算法,現在我們來一個簡單的總結,演算法的總結歸納如下

演算法 註釋
最優演算法 不可實現,但可以用作基準
NRU(最近未使用) 演算法 和 LRU 演算法很相似
FIFO(先進先出) 演算法 有可能會拋棄重要的頁面
第二次機會演算法 比 FIFO 有較大的改善
時鐘演算法 實際使用
LRU(最近最少)演算法 比較優秀,但是很難實現
NFU(最不經常食用)演算法 和 LRU 很類似
老化演算法 近似 LRU 的高效演算法
工作集演算法 實施起來開銷很大
工作集時鐘演算法 比較有效的演算法
  • 最優演算法在當前頁面中置換最後要訪問的頁面。不幸的是,沒有辦法來判定哪個頁面是最後一個要訪問的,因此實際上該演算法不能使用。然而,它可以作為衡量其他演算法的標準。

  • NRU 演算法根據 R 位和 M 位的狀態將頁面氛圍四類。從編號最小的類別中隨機選擇一個頁面。NRU 演算法易於實現,但是效能不是很好。存在更好的演算法。

  • FIFO 會跟蹤頁面載入進入記憶體中的順序,並把頁面放入一個連結串列中。有可能刪除存在時間最長但是還在使用的頁面,因此這個演算法也不是一個很好的選擇。

  • 第二次機會演算法是對 FIFO 的一個修改,它會在刪除頁面之前檢查這個頁面是否仍在使用。如果頁面正在使用,就會進行保留。這個改進大大提高了效能。

  • 時鐘 演算法是第二次機會演算法的另外一種實現形式,時鐘演算法和第二次演算法的效能差不多,但是會花費更少的時間來執行演算法。

  • LRU 演算法是一個非常優秀的演算法,但是沒有特殊的硬體(TLB)很難實現。如果沒有硬體,就不能使用 LRU 演算法。

  • NFU 演算法是一種近似於 LRU 的演算法,它的效能不是非常好。

  • 老化 演算法是一種更接近 LRU 演算法的實現,並且可以更好的實現,因此是一個很好的選擇

  • 最後兩種演算法都使用了工作集演算法。工作集演算法提供了合理的效能開銷,但是它的實現比較複雜。WSClock 是另外一種變體,它不僅能夠提供良好的效能,而且可以高效地實現。

總之,最好的演算法是老化演算法和WSClock演算法。他們分別是基於 LRU 和工作集演算法。他們都具有良好的效能並且能夠被有效的實現。還存在其他一些好的演算法,但實際上這兩個可能是最重要的。

檔案系統中的演算法

檔案系統在備份的過程中會使用到演算法,檔案備份分為邏輯轉儲和物理轉儲

物理轉儲和邏輯轉儲

物理轉儲的主要優點是簡單、極為快速(基本上是以磁碟的速度執行),缺點是全量備份,不能跳過指定目錄,也不能增量轉儲,也不能恢復個人檔案的請求。因此句大多數情況下不會使用物理轉儲,而使用邏輯轉儲

邏輯轉儲(logical dump)從一個或幾個指定的目錄開始,遞迴轉儲自指定日期開始後更改的檔案和目錄。因此,在邏輯轉儲中,轉儲磁碟上有一系列經過仔細識別的目錄和檔案,這使得根據請求輕鬆還原特定檔案或目錄。

既然邏輯轉儲是最常用的方式,那麼下面就讓我們研究一下邏輯轉儲的通用演算法。此演算法在 UNIX 系統上廣為使用,如下圖所示

待轉儲的檔案系統,其中方框代表目錄,圓圈代表檔案。黃色的專案表是自上次轉儲以來修改過。每個目錄和檔案都被標上其 inode 號。

此演算法會轉儲位於修改檔案或目錄路徑上的所有目錄(也包括未修改的目錄),原因有兩個。第一是能夠在不同電腦的檔案系統中恢復轉儲的檔案。通過這種方式,轉儲和重新儲存的程式能夠用來在兩個電腦之間傳輸整個檔案系統。第二個原因是能夠對單個檔案進行增量恢復

邏輯轉儲演算法需要維持一個 inode 為索引的點陣圖(bitmap),每個 inode 包含了幾位。隨著演算法的進行,點陣圖中的這些位會被設定或清除。演算法的執行分成四個階段。第一階段從起始目錄(本例為根目錄)開始檢查其中所有的目錄項。對每一個修改過的檔案,該演算法將在點陣圖中標記其 inode。演算法還會標記並遞迴檢查每一個目錄(不管是否修改過)。

在第一階段結束時,所有修改過的檔案和全部目錄都在點陣圖中標記了,如下圖所示

理論上來說,第二階段再次遞迴遍歷目錄樹,並去掉目錄樹中任何不包含被修改過的檔案或目錄的標記。本階段執行的結果如下

注意,inode 編號為 10、11、14、27、29 和 30 的目錄已經被去掉了標記,因為它們所包含的內容沒有修改。它們也不會轉儲。相反,inode 編號為 5 和 6 的目錄本身儘管沒有被修改過也要被轉儲,因為在新的機器上恢復當日的修改時需要這些資訊。為了提高演算法效率,可以將這兩階段的目錄樹遍歷合二為一。

現在已經知道了哪些目錄和檔案必須被轉儲了,這就是上圖 b 中標記的內容,第三階段演算法將以節點號為序,掃描這些 inode 並轉儲所有標記為需轉儲的目錄,如下圖所示

為了進行恢復,每個被轉儲的目錄都用目錄的屬性(所有者、時間)作為字首。

最後,在第四階段,上圖中被標記的檔案也被轉儲,同樣,由其檔案屬性作為字首。至此,轉儲結束。

從轉儲磁碟上還原檔案系統非常簡單。一開始,需要在磁碟上建立空檔案系統。然後恢復最近一次的完整轉儲。由於磁帶上最先出現目錄,所以首先恢復目錄,給出檔案系統的框架(skeleton),然後恢復檔案系統本身。在完整儲存之後是第一次增量儲存,然後是第二次重複這一過程,以此類推。

儘管邏輯儲存十分簡單,但是也會有一些棘手的問題。首先,既然空閒塊列表並不是一個檔案,那麼在所有被轉儲的檔案恢復完畢之後,就需要從零開始重新構造。

另外一個問題是關於連結。如果檔案連結了兩個或者多個目錄,而檔案只能還原一次,那麼並且所有指向該檔案的目錄都必須還原。

還有一個問題是,UNIX 檔案實際上包含了許多 空洞(holes)。開啟檔案,寫幾個位元組,然後找到檔案中偏移了一定距離的地址,又寫入更多的位元組,這麼做是合法的。但兩者之間的這些塊並不屬於檔案本身,從而也不應該在其上進行檔案轉儲和恢復。

最後,無論屬於哪一個目錄,特殊檔案,命名管道以及類似的檔案都不應該被轉儲。

I/O 中的演算法

在 I/O 的磁碟排程中也出現過很多演算法,關於定址和磁碟臂的轉動都會對演算法產生影響,下面我們就來一起看下

一般情況下,影響磁碟快讀寫的時間由下面幾個因素決定

  • 尋道時間 - 尋道時間指的就是將磁碟臂移動到需要讀取磁碟塊上的時間
  • 旋轉延遲 - 等待合適的扇區旋轉到磁頭下所需的時間
  • 實際資料的讀取或者寫入時間

這三種時間引數也是磁碟尋道的過程。一般情況下,尋道時間對總時間的影響最大,所以,有效的降低尋道時間能夠提高磁碟的讀取速度。

如果磁碟驅動程式每次接收一個請求並按照接收順序完成請求,這種處理方式也就是 先來先服務(First-Come, First-served, FCFS) ,這種方式很難優化尋道時間。因為每次都會按照順序處理,不管順序如何,有可能這次讀完後需要等待一個磁碟旋轉一週才能繼續讀取,而其他柱面能夠馬上進行讀取,這種情況下每次請求也會排隊。

通常情況下,磁碟在進行尋道時,其他程式會產生其他的磁碟請求。磁碟驅動程式會維護一張表,表中會記錄著柱面號當作索引,每個柱面未完成的請求會形成連結串列,連結串列頭存放在表的相應表項中。

一種對先來先服務的演算法改良的方案是使用 最短路徑優先(SSF) 演算法,下面描述了這個演算法。

假如我們在對磁軌 6 號進行定址時,同時發生了對 11 , 2 , 4, 14, 8, 15, 3 的請求,如果採用先來先服務的原則,如下圖所示

我們可以計算一下磁碟臂所跨越的磁碟數量為 5 + 9 + 2 + 10 + 6 + 7 + 12 = 51,相當於是跨越了 51 次盤面,如果使用最短路徑優先,我們來計算一下跨越的盤面

跨越的磁碟數量為 4 + 1 + 1 + 4 + 3 + 3 + 1 = 17 ,相比 51 足足省了兩倍的時間。

但是,最短路徑優先的演算法也不是完美無缺的,這種演算法照樣存在問題,那就是優先順序 問題,

這裡有一個原型可以參考就是我們日常生活中的電梯,電梯使用一種電梯演算法(elevator algorithm) 來進行排程,從而滿足協調效率和公平性這兩個相互衝突的目標。電梯一般會保持向一個方向移動,直到在那個方向上沒有請求為止,然後改變方向。

電梯演算法需要維護一個二進位制位,也就是當前的方向位:UP(向上)或者是 DOWN(向下)。當一個請求處理完成後,磁碟或電梯的驅動程式會檢查該位,如果此位是 UP 位,磁碟臂或者電梯倉移到下一個更高跌未完成的請求。如果高位沒有未完成的請求,則取相反方向。當方向位是 DOWN 時,同時存在一個低位的請求,磁碟臂會轉向該點。如果不存在的話,那麼它只是停止並等待。

我們舉個例子來描述一下電梯演算法,比如各個柱面得到服務的順序是 4,7,10,14,9,6,3,1 ,那麼它的流程圖如下

所以電梯演算法需要跨越的盤面數量是 3 + 3 + 4 + 5 + 3 + 3 + 1 = 22

電梯演算法通常情況下不如 SSF 演算法。

一些磁碟控制器為軟體提供了一種檢查磁頭下方當前扇區號的方法,使用這樣的控制器,能夠進行另一種優化。如果對一個相同的柱面有兩個或者多個請求正等待處理,驅動程式可以發出請求讀寫下一次要通過磁頭的扇區。

這裡需要注意一點,當一個柱面有多條磁軌時,相繼的請求可能針對不同的磁軌,這種選擇沒有代價,因為選擇磁頭不需要移動磁碟臂也沒有旋轉延遲。

對於磁碟來說,最影響效能的就是尋道時間和旋轉延遲,所以一次只讀取一個或兩個扇區的效率是非常低的。出於這個原因,許多磁碟控制器總是讀出多個扇區並進行快取記憶體,即使只請求一個扇區時也是這樣。一般情況下讀取一個扇區的同時會讀取該扇區所在的磁軌或者是所有剩餘的扇區被讀出,讀出扇區的數量取決於控制器的快取記憶體中有多少可用的空間。

磁碟控制器的快取記憶體和作業系統的快取記憶體有一些不同,磁碟控制器的快取記憶體用於快取沒有實際被請求的塊,而作業系統維護的快取記憶體由顯示地讀出的塊組成,並且作業系統會認為這些塊在近期仍然會頻繁使用。

當同一個控制器上有多個驅動器時,作業系統應該為每個驅動器都單獨的維護一個未完成的請求表。一旦有某個驅動器閒置時,就應該發出一個尋道請求來將磁碟臂移到下一個被請求的柱面。如果下一個尋道請求到來時恰好沒有磁碟臂處於正確的位置,那麼驅動程式會在剛剛完成傳輸的驅動器上發出一個新的尋道命令並等待,等待下一次中斷到來時檢查哪個驅動器處於閒置狀態。

死鎖中的演算法

在死鎖的處理策略中,其中一點是忽略死鎖帶來的影響(驚呆了),出現過一個叫做鴕鳥演算法

最簡單的解決辦法就是使用鴕鳥演算法(ostrich algorithm),把頭埋在沙子裡,假裝問題根本沒有發生。每個人看待這個問題的反應都不同。數學家認為死鎖是不可接受的,必須通過有效的策略來防止死鎖的產生。工程師想要知道問題發生的頻次,系統因為其他原因崩潰的次數和死鎖帶來的嚴重後果。如果死鎖發生的頻次很低,而經常會由於硬體故障、編譯器錯誤等其他作業系統問題導致系統崩潰,那麼大多數工程師不會修復死鎖。

在死鎖的檢測中出現過一些演算法

每種型別多個資源的死鎖檢測方式

如果有多種相同的資源存在,就需要採用另一種方法來檢測死鎖。可以通過構造一個矩陣來檢測從 P1 -> Pn 這 n 個程式中的死鎖。

現在我們提供一種基於矩陣的演算法來檢測從 P1 到 Pn 這 n 個程式中的死鎖。假設資源型別為 m,E1 代表資源型別1,E2 表示資源型別 2 ,Ei 代表資源型別 i (1 <= i <= m)。E 表示的是 現有資源向量(existing resource vector),代表每種已存在的資源總數。

現在我們就需要構造兩個陣列:C 表示的是當前分配矩陣(current allocation matrix) ,R 表示的是 請求矩陣(request matrix)。Ci 表示的是 Pi 持有每一種型別資源的資源數。所以,Cij 表示 Pi 持有資源 j 的數量。Rij 表示 Pi 所需要獲得的資源 j 的數量

一般來說,已分配資源 j 的數量加起來再和所有可供使用的資源數相加 = 該類資源的總數。

死鎖的檢測就是基於向量的比較。每個程式起初都是沒有被標記過的,演算法會開始對程式做標記,程式被標記後說明程式被執行了,不會進入死鎖,當演算法結束時,任何沒有被標記過的程式都會被判定為死鎖程式。

上面我們探討了兩種檢測死鎖的方式,那麼現在你知道怎麼檢測後,你何時去做死鎖檢測呢?一般來說,有兩個考量標準:

  • 每當有資源請求時就去檢測,這種方式會佔用昂貴的 CPU 時間。
  • 每隔 k 分鐘檢測一次,或者當 CPU 使用率降低到某個標準下去檢測。考慮到 CPU 效率的原因,如果死鎖程式達到一定數量,就沒有多少程式可以執行,所以 CPU 會經常空閒。

還有死鎖避免的演算法

銀行家演算法

銀行家演算法是 Dijkstra 在 1965 年提出的一種排程演算法,它本身是一種死鎖的排程演算法。它的模型是基於一個城鎮中的銀行家,銀行家向城鎮中的客戶承諾了一定數量的貸款額度。演算法要做的就是判斷請求是否會進入一種不安全的狀態。如果是,就拒絕請求,如果請求後系統是安全的,就接受該請求。

比如下面的例子,銀行家一共為所有城鎮居民提供了 15 單位個貸款額度,一個單位表示 1k 美元,如下所示

城鎮居民都喜歡做生意,所以就會涉及到貸款,每個人能貸款的最大額度不一樣,在某一時刻,A/B/C/D 的貸款金額如下

上面每個人的貸款總額加起來是 13,馬上接近 15,銀行家只能給 A 和 C 進行放貸,可以拖著 B 和 D、所以,可以讓 A 和 C 首先完成,釋放貸款額度,以此來滿足其他居民的貸款。這是一種安全的狀態。

如果每個人的請求導致總額會超過甚至接近 15 ,就會處於一種不安全的狀態,如下所示

這樣,每個人還能貸款至少 2 個單位的額度,如果其中有一個人發起最大額度的貸款請求,就會使系統處於一種死鎖狀態。

這裡注意一點:不安全狀態並不一定引起死鎖,由於客戶不一定需要其最大的貸款額度,但是銀行家不敢抱著這種僥倖心理。

銀行家演算法就是對每個請求進行檢查,檢查是否請求會引起不安全狀態,如果不會引起,那麼就接受該請求;如果會引起,那麼就推遲該請求。

類似的,還有多個資源的銀行家演算法,讀者可以自行了解。

相關文章