大廠面試愛問的「排程演算法」,20 張圖一舉拿下

小林coding發表於2020-09-08

前言

最近,我偷偷潛伏在各大技術群,因為秋招在即,看到不少小夥伴分享的大廠面經。

然後發現,作業系統的知識點考察還是比較多的,大廠就是大廠就愛問基礎知識。其中,關於作業系統的「排程演算法」考察也算比較頻繁。

所以,我這邊總結了作業系統的三大排程機制,分別是「程式排程/頁面置換/磁碟排程演算法」,供大家複習,希望大家在秋招能斬獲自己心意的 offer。

本文提綱本文提綱

正文

程式排程演算法

程式排程演算法也稱 CPU 排程演算法,畢竟程式是由 CPU 排程的。

當 CPU 空閒時,作業系統就選擇記憶體中的某個「就緒狀態」的程式,並給其分配 CPU。

什麼時候會發生 CPU 排程呢?通常有以下情況:

  1. 當程式從執行狀態轉到等待狀態;
  2. 當程式從執行狀態轉到就緒狀態;
  3. 當程式從等待狀態轉到就緒狀態;
  4. 當程式從執行狀態轉到終止狀態;

其中發生在 1 和 4 兩種情況下的排程稱為「非搶佔式排程」,2 和 3 兩種情況下發生的排程稱為「搶佔式排程」。

非搶佔式的意思就是,當程式正在執行時,它就會一直執行,直到該程式完成或發生某個事件而被阻塞時,才會把 CPU 讓給其他程式。

而搶佔式排程,顧名思義就是程式正在執行的時,可以被打斷,使其把 CPU 讓給其他程式。那搶佔的原則一般有三種,分別是時間片原則、優先權原則、短作業優先原則。

你可能會好奇為什麼第 3 種情況也會發生 CPU 排程呢?假設有一個程式是處於等待狀態的,但是它的優先順序比較高,如果該程式等待的事件發生了,它就會轉到就緒狀態,一旦它轉到就緒狀態,如果我們的排程演算法是以優先順序來進行排程的,那麼它就會立馬搶佔正在執行的程式,所以這個時候就會發生 CPU 排程。

那第 2 種狀態通常是時間片到的情況,因為時間片到了就會發生中斷,於是就會搶佔正在執行的程式,從而佔用 CPU。

排程演算法影響的是等待時間(程式在就緒佇列中等待排程的時間總和),而不能影響程式真在使用 CPU 的時間和 I/O 時間。

接下來,說說常見的排程演算法:

  • 先來先服務排程演算法
  • 最短作業優先排程演算法
  • 高響應比優先排程演算法
  • 時間片輪轉排程演算法
  • 最高優先順序排程演算法
  • 多級反饋佇列排程演算法

先來先服務排程演算法

最簡單的一個排程演算法,就是非搶佔式的先來先服務(First Come First Severd, FCFS)演算法了。

FCFS 排程演算法FCFS 排程演算法

顧名思義,先來後到,每次從就緒佇列選擇最先進入佇列的程式,然後一直執行,直到程式退出或被阻塞,才會繼續從佇列中選擇第一個程式接著執行。

這似乎很公平,但是當一個長作業先執行了,那麼後面的短作業等待的時間就會很長,不利於短作業。

FCFS 對長作業有利,適用於 CPU 繁忙型作業的系統,而不適用於 I/O 繁忙型作業的系統。

最短作業優先排程演算法

最短作業優先(Shortest Job First, SJF)排程演算法同樣也是顧名思義,它會優先選擇執行時間最短的程式來執行,這有助於提高系統的吞吐量。

SJF 排程演算法SJF 排程演算法

這顯然對長作業不利,很容易造成一種極端現象。

比如,一個長作業在就緒佇列等待執行,而這個就緒佇列有非常多的短作業,那麼就會使得長作業不斷的往後推,週轉時間變長,致使長作業長期不會被執行。

高響應比優先排程演算法

前面的「先來先服務排程演算法」和「最短作業優先排程演算法」都沒有很好的權衡短作業和長作業。

那麼,高響應比優先 (Highest Response Ratio Next, HRRN)排程演算法主要是權衡了短作業和長作業。

每次進行程式排程時,先計算「響應比優先順序」,然後把「響應比優先順序」最高的程式投入執行,「響應比優先順序」的計算公式:

從上面的公式,可以發現:

  • 如果兩個程式的「等待時間」相同時,「要求的服務時間」越短,「響應比」就越高,這樣短作業的程式容易被選中執行;
  • 如果兩個程式「要求的服務時間」相同時,「等待時間」越長,「響應比」就越高,這就兼顧到了長作業程式,因為程式的響應比可以隨時間等待的增加而提高,當其等待時間足夠長時,其響應比便可以升到很高,從而獲得執行的機會;

時間片輪轉排程演算法

最古老、最簡單、最公平且使用最廣的演算法就是時間片輪轉(Round Robin, RR)排程演算法

RR 排程演算法RR 排程演算法

每個程式被分配一個時間段,稱為時間片(Quantum),即允許該程式在該時間段中執行。

  • 如果時間片用完,程式還在執行,那麼將會把此程式從 CPU 釋放出來,並把 CPU 分配另外一個程式;
  • 如果該程式在時間片結束前阻塞或結束,則 CPU 立即進行切換;

另外,時間片的長度就是一個很關鍵的點:

  • 如果時間片設得太短會導致過多的程式上下文切換,降低了 CPU 效率;
  • 如果設得太長又可能引起對短作業程式的響應時間變長。將

通常時間片設為 20ms~50ms 通常是一個比較合理的折中值。

最高優先順序排程演算法

前面的「時間片輪轉演算法」做了個假設,即讓所有的程式同等重要,也不偏袒誰,大家的執行時間都一樣。

但是,對於多使用者計算機系統就有不同的看法了,它們希望排程是有優先順序的,即希望排程程式能從就緒佇列中選擇最高優先順序的程式進行執行,這稱為最高優先順序(Highest Priority First,HPF)排程演算法

程式的優先順序可以分為,靜態優先順序或動態優先順序:

  • 靜態優先順序:建立程式時候,就已經確定了優先順序了,然後整個執行時間優先順序都不會變化;
  • 動態優先順序:根據程式的動態變化調整優先順序,比如如果程式執行時間增加,則降低其優先順序,如果程式等待時間(就緒佇列的等待時間)增加,則升高其優先順序,也就是隨著時間的推移增加等待程式的優先順序

該演算法也有兩種處理優先順序高的方法,非搶佔式和搶佔式:

  • 非搶佔式:當就緒佇列中出現優先順序高的程式,執行完當前程式,再選擇優先順序高的程式。
  • 搶佔式:當就緒佇列中出現優先順序高的程式,當前程式掛起,排程優先順序高的程式執行。

但是依然有缺點,可能會導致低優先順序的程式永遠不會執行。

多級反饋佇列排程演算法

多級反饋佇列(Multilevel Feedback Queue)排程演算法是「時間片輪轉演算法」和「最高優先順序演算法」的綜合和發展。

顧名思義:

  • 「多級」表示有多個佇列,每個佇列優先順序從高到低,同時優先順序越高時間片越短。
  • 「反饋」表示如果有新的程式加入優先順序高的佇列時,立刻停止當前正在執行的程式,轉而去執行優先順序高的佇列;
多級反饋佇列多級反饋佇列

來看看,它是如何工作的:

  • 設定了多個佇列,賦予每個佇列不同的優先順序,每個佇列優先順序從高到低,同時優先順序越高時間片越短
  • 新的程式會被放入到第一級佇列的末尾,按先來先服務的原則排隊等待被排程,如果在第一級佇列規定的時間片沒執行完成,則將其轉入到第二級佇列的末尾,以此類推,直至完成;
  • 當較高優先順序的佇列為空,才排程較低優先順序的佇列中的程式執行。如果程式執行時,有新程式進入較高優先順序的佇列,則停止當前執行的程式並將其移入到原佇列末尾,接著讓較高優先順序的程式執行;

可以發現,對於短作業可能可以在第一級佇列很快被處理完。對於長作業,如果在第一級佇列處理不完,可以移入下次佇列等待被執行,雖然等待的時間變長了,但是執行時間也會更長了,所以該演算法很好的兼顧了長短作業,同時有較好的響應時間。


記憶體頁面置換演算法

在瞭解記憶體頁面置換演算法前,我們得先談一下缺頁異常(缺頁中斷)

當 CPU 訪問的頁面不在實體記憶體時,便會產生一個缺頁中斷,請求作業系統將所缺頁調入到實體記憶體。那它與一般中斷的主要區別在於:

  • 缺頁中斷在指令執行「期間」產生和處理中斷訊號,而一般中斷在一條指令執行「完成」後檢查和處理中斷訊號。
  • 缺頁中斷返回到該指令的開始重新執行「該指令」,而一般中斷返回回到該指令的「下一個指令」執行。

我們來看一下缺頁中斷的處理流程,如下圖:

缺頁中斷的處理流程缺頁中斷的處理流程
  1. 在 CPU 裡訪問一條 Load M 指令,然後 CPU 會去找 M 所對應的頁表項。
  2. 如果該頁表項的狀態位是「有效的」,那 CPU 就可以直接去訪問實體記憶體了,如果狀態位是「無效的」,則 CPU 則會傳送缺頁中斷請求。
  3. 作業系統收到了缺頁中斷,則會執行缺頁中斷處理函式,先會查詢該頁面在磁碟中的頁面的位置。
  4. 找到磁碟中對應的頁面後,需要把該頁面換入到實體記憶體中,但是在換入前,需要在實體記憶體中找空閒頁,如果找到空閒頁,就把頁面換入到實體記憶體中。
  5. 頁面從磁碟換入到實體記憶體完成後,則把頁表項中的狀態位修改為「有效的」。
  6. 最後,CPU 重新執行導致缺頁異常的指令。

上面所說的過程,第 4 步是能在實體記憶體找到空閒頁的情況,那如果找不到呢?

找不到空閒頁的話,就說明此時記憶體已滿了,這時候,就需要「頁面置換演算法」選擇一個物理頁,如果該物理頁有被修改過(髒頁),則把它換出到磁碟,然後把該被置換出去的頁表項的狀態改成「無效的」,最後把正在訪問的頁面裝入到這個物理頁中。

這裡提一下,頁表項通常有如下圖的欄位:

那其中:

  • 狀態位:用於表示該頁是否有效,也就是說是否在實體記憶體中,供程式訪問時參考。
  • 訪問欄位:用於記錄該頁在一段時間被訪問的次數,供頁面置換演算法選擇出頁面時參考。
  • 修改位:表示該頁在調入記憶體後是否有被修改過,由於記憶體中的每一頁都在磁碟上保留一份副本,因此,如果沒有修改,在置換該頁時就不需要將該頁寫回到磁碟上,以減少系統的開銷;如果已經被修改,則將該頁重寫到磁碟上,以保證磁碟中所保留的始終是最新的副本。
  • 硬碟地址:用於指出該頁在硬碟上的地址,通常是物理塊號,供調入該頁時使用。

這裡我整理了虛擬記憶體的管理整個流程,你可以從下面這張圖看到:

虛擬記憶體的流程虛擬記憶體的流程

所以,頁面置換演算法的功能是,當出現缺頁異常,需調入新頁面而記憶體已滿時,選擇被置換的物理頁面,也就是說選擇一個物理頁面換出到磁碟,然後把需要訪問的頁面換入到物理頁。

那其演算法目標則是,儘可能減少頁面的換入換出的次數,常見的頁面置換演算法有如下幾種:

  • 最佳頁面置換演算法(OPT
  • 先進先出置換演算法(FIFO
  • 最近最久未使用的置換演算法(LRU
  • 時鐘頁面置換演算法(Lock
  • 最不常用置換演算法(LFU

最佳頁面置換演算法

最佳頁面置換演算法基本思路是,置換在「未來」最長時間不訪問的頁面

所以,該演算法實現需要計算記憶體中每個邏輯頁面的「下一次」訪問時間,然後比較,選擇未來最長時間不訪問的頁面。

我們舉個例子,假設一開始有 3 個空閒的物理頁,然後有請求的頁面序列,那它的置換過程如下圖:

最佳頁面置換演算法最佳頁面置換演算法

在這個請求的頁面序列中,缺頁共發生了 7 次(空閒頁換入 3 次 + 最優頁面置換 4 次),頁面置換共發生了 4 次。

這很理想,但是實際系統中無法實現,因為程式訪問頁面時是動態的,我們是無法預知每個頁面在「下一次」訪問前的等待時間。

所以,最佳頁面置換演算法作用是為了衡量你的演算法的效率,你的演算法效率越接近該演算法的效率,那麼說明你的演算法是高效的。

先進先出置換演算法

既然我們無法預知頁面在下一次訪問前所需的等待時間,那我們可以選擇在記憶體駐留時間很長的頁面進行中置換,這個就是「先進先出置換」演算法的思想。

還是以前面的請求的頁面序列作為例子,假設使用先進先出置換演算法,則過程如下圖:

先進先出置換演算法先進先出置換演算法

在這個請求的頁面序列中,缺頁共發生了 10 次,頁面置換共發生了 7 次,跟最佳頁面置換演算法比較起來,效能明顯差了很多。

最近最久未使用的置換演算法

最近最久未使用(LRU)的置換演算法的基本思路是,發生缺頁時,選擇最長時間沒有被訪問的頁面進行置換,也就是說,該演算法假設已經很久沒有使用的頁面很有可能在未來較長的一段時間內仍然不會被使用。

這種演算法近似最優置換演算法,最優置換演算法是通過「未來」的使用情況來推測要淘汰的頁面,而 LRU 則是通過「歷史」的使用情況來推測要淘汰的頁面。

還是以前面的請求的頁面序列作為例子,假設使用最近最久未使用的置換演算法,則過程如下圖:

最近最久未使用的置換演算法最近最久未使用的置換演算法

在這個請求的頁面序列中,缺頁共發生了 9 次,頁面置換共發生了 6 次,跟先進先出置換演算法比較起來,效能提高了一些。

雖然 LRU 在理論上是可以實現的,但代價很高。為了完全實現 LRU,需要在記憶體中維護一個所有頁面的連結串列,最近最多使用的頁面在表頭,最近最少使用的頁面在表尾。

困難的是,在每次訪問記憶體時都必須要更新「整個連結串列」。在連結串列中找到一個頁面,刪除它,然後把它移動到表頭是一個非常費時的操作。

所以,LRU 雖然看上去不錯,但是由於開銷比較大,實際應用中比較少使用。

時鐘頁面置換演算法

那有沒有一種即能優化置換的次數,也能方便實現的演算法呢?

時鐘頁面置換演算法就可以兩者兼得,它跟 LRU 近似,又是對 FIFO 的一種改進。

該演算法的思路是,把所有的頁面都儲存在一個類似鐘面的「環形連結串列」中,一個錶針指向最老的頁面。

當發生缺頁中斷時,演算法首先檢查錶針指向的頁面:

  • 如果它的訪問位位是 0 就淘汰該頁面,並把新的頁面插入這個位置,然後把錶針前移一個位置;
  • 如果訪問位是 1 就清除訪問位,並把錶針前移一個位置,重複這個過程直到找到了一個訪問位為 0 的頁面為止;

我畫了一副時鐘頁面置換演算法的工作流程圖,你可以在下方看到:

時鐘頁面置換演算法時鐘頁面置換演算法

瞭解了這個演算法的工作方式,就明白為什麼它被稱為時鐘(Clock)演算法了。

最不常用演算法

最不常用(LFU)演算法,這名字聽起來很調皮,但是它的意思不是指這個演算法不常用,而是當發生缺頁中斷時,選擇「訪問次數」最少的那個頁面,並將其淘汰

它的實現方式是,對每個頁面設定一個「訪問計數器」,每當一個頁面被訪問時,該頁面的訪問計數器就累加 1。在發生缺頁中斷時,淘汰計數器值最小的那個頁面。

看起來很簡單,每個頁面加一個計數器就可以實現了,但是在作業系統中實現的時候,我們需要考慮效率和硬體成本的。

要增加一個計數器來實現,這個硬體成本是比較高的,另外如果要對這個計數器查詢哪個頁面訪問次數最小,查詢連結串列本身,如果連結串列長度很大,是非常耗時的,效率不高。

但還有個問題,LFU 演算法只考慮了頻率問題,沒考慮時間的問題,比如有些頁面在過去時間裡訪問的頻率很高,但是現在已經沒有訪問了,而當前頻繁訪問的頁面由於沒有這些頁面訪問的次數高,在發生缺頁中斷時,就會可能會誤傷當前剛開始頻繁訪問,但訪問次數還不高的頁面。

那這個問題的解決的辦法還是有的,可以定期減少訪問的次數,比如當發生時間中斷時,把過去時間訪問的頁面的訪問次數除以 2,也就說,隨著時間的流失,以前的高訪問次數的頁面會慢慢減少,相當於加大了被置換的概率。


磁碟排程演算法

我們來看看磁碟的結構,如下圖:

磁碟的結構磁碟的結構

常見的機械磁碟是上圖左邊的樣子,中間圓的部分是磁碟的碟片,一般會有多個碟片,每個盤面都有自己的磁頭。右邊的圖就是一個碟片的結構,碟片中的每一層分為多個磁軌,每個磁軌分多個扇區,每個扇區是 512 位元組。那麼,多個具有相同編號的磁軌形成一個圓柱,稱之為磁碟的柱面,如上圖裡中間的樣子。

磁碟排程演算法的目的很簡單,就是為了提高磁碟的訪問效能,一般是通過優化磁碟的訪問請求順序來做到的。

尋道的時間是磁碟訪問最耗時的部分,如果請求順序優化的得當,必然可以節省一些不必要的尋道時間,從而提高磁碟的訪問效能。

假設有下面一個請求序列,每個數字代表磁軌的位置:

98,183,37,122,14,124,65,67

初始磁頭當前的位置是在第 53 磁軌。

接下來,分別對以上的序列,作為每個排程演算法的例子,那常見的磁碟排程演算法有:

  • 先來先服務演算法
  • 最短尋道時間優先演算法
  • 掃描演算法演算法
  • 迴圈掃描演算法
  • LOOK 與 C-LOOK 演算法

先來先服務

先來先服務(First-Come,First-Served,FCFS),顧名思義,先到來的請求,先被服務。

那按照這個序列的話:

98,183,37,122,14,124,65,67

那麼,磁碟的寫入順序是從左到右,如下圖:

先來先服務先來先服務

先來先服務演算法總共移動了 640 個磁軌的距離,這麼一看這種演算法,比較簡單粗暴,但是如果大量程式競爭使用磁碟,請求訪問的磁軌可能會很分散,那先來先服務演算法在效能上就會顯得很差,因為尋道時間過長。

最短尋道時間優先

最短尋道時間優先(Shortest Seek First,SSF)演算法的工作方式是,優先選擇從當前磁頭位置所需尋道時間最短的請求,還是以這個序列為例子:

98,183,37,122,14,124,65,67

那麼,那麼根據距離磁頭( 53 位置)最近的請求的演算法,具體的請求則會是下列從左到右的順序:

65,67,37,14,98,122,124,183

最短尋道時間優先最短尋道時間優先

磁頭移動的總距離是 236 磁軌,相比先來先服務效能提高了不少。

但這個演算法可能存在某些請求的飢餓,因為本次例子我們是靜態的序列,看不出問題,假設是一個動態的請求,如果後續來的請求都是小於 183
磁軌的,那麼 183 磁軌可能永遠不會被響應,於是就產生了飢餓現象,這裡產生飢餓的原因是磁頭在一小塊區域來回移動

掃描演算法

最短尋道時間優先演算法會產生飢餓的原因在於:磁頭有可能再一個小區域內來回得移動。

為了防止這個問題,可以規定:磁頭在一個方向上移動,訪問所有未完成的請求,直到磁頭到達該方向上的最後的磁軌,才調換方向,這就是掃描(Scan)演算法

這種演算法也叫做電梯演算法,比如電梯保持按一個方向移動,直到在那個方向上沒有請求為止,然後改變方向。

還是以這個序列為例子,磁頭的初始位置是 53:

98,183,37,122,14,124,65,67

那麼,假設掃描排程算先朝磁軌號減少的方向移動,具體請求則會是下列從左到右的順序:

37,14,0,65,67,98,122,124,183

掃描演算法掃描演算法

磁頭先響應左邊的請求,直到到達最左端( 0 磁軌)後,才開始反向移動,響應右邊的請求。

掃描排程演算法效能較好,不會產生飢餓現象,但是存在這樣的問題,中間部分的磁軌會比較佔便宜,中間部分相比其他部分響應的頻率會比較多,也就是說每個磁軌的響應頻率存在差異。

迴圈掃描演算法

掃描演算法使得每個磁軌響應的頻率存在差異,那麼要優化這個問題的話,可以總是按相同的方向進行掃描,使得每個磁軌的響應頻率基本一致。

迴圈掃描(Circular Scan, CSCAN )規定:只有磁頭朝某個特定方向移動時,才處理磁軌訪問請求,而返回時直接快速移動至最靠邊緣的磁軌,也就是復位磁頭,這個過程是很快的,並且返回中途不處理任何請求,該演算法的特點,就是磁軌只響應一個方向上的請求

還是以這個序列為例子,磁頭的初始位置是 53:

98,183,37,122,14,124,65,67

那麼,假設迴圈掃描排程算先朝磁軌增加的方向移動,具體請求會是下列從左到右的順序:

65,67,98,122,124,183,1990,14,37

迴圈掃描演算法迴圈掃描演算法

磁頭先響應了右邊的請求,直到碰到了最右端的磁軌 199,就立即回到磁碟的開始處(磁軌 0),但這個返回的途中是不響應任何請求的,直到到達最開始的磁軌後,才繼續順序響應右邊的請求。

迴圈掃描演算法相比於掃描演算法,對於各個位置磁軌響應頻率相對比較平均。

LOOK 與 C-LOOK演算法

我們前面說到的掃描演算法和迴圈掃描演算法,都是磁頭移動到磁碟「最始端或最末端」才開始調換方向。

那這其實是可以優化的,優化的思路就是磁頭在移動到「最遠的請求」位置,然後立即反向移動。

那針對 SCAN 演算法的優化則叫 LOOK 演算法,它的工作方式,磁頭在每個方向上僅僅移動到最遠的請求位置,然後立即反向移動,而不需要移動到磁碟的最始端或最末端,反向移動的途中會響應請求

LOOK 演算法LOOK 演算法

而針 C-SCAN 演算法的優化則叫 C-LOOK,它的工作方式,磁頭在每個方向上僅僅移動到最遠的請求位置,然後立即反向移動,而不需要移動到磁碟的最始端或最末端,反向移動的途中不會響應請求

C-LOOK 演算法C-LOOK 演算法

絮叨

大家好,我是小林,一個專為大家圖解的工具人,我們下次見!

相關文章