作業系統常見面試題

三分惡發表於2021-10-02

引論

什麼是作業系統?

可以這麼說,作業系統是一種執行在核心態的軟體。

它是應用程式和硬體之間的媒介,嚮應用程式提供硬體的抽象,以及管理硬體資源。

作業系統是什麼

作業系統主要有哪些功能?

作業系統最主要的功能:

  • 處理器(CPU)管理:CPU的管理和分配,主要指的是程式管理。
  • 記憶體管理:記憶體的分配和管理,主要利用了虛擬記憶體的方式。
  • 外存管理:外存(磁碟等)的分配和管理,將外存以檔案的形式提供出去。
  • I/O管理:對輸入/輸出裝置的統一管理。

除此之外,還有保證自身正常執行的健壯性管理,防止非法操作和入侵的安全性管理。

作業系統主要功能

作業系統結構

什麼是核心?

可以這麼說,核心是一個計算機程式,它是作業系統的核心,提供了作業系統最核心的能力,可以控制作業系統中所有的內容。

什麼是使用者態和核心態?

核心具有很⾼的許可權,可以控制 cpu、記憶體、硬碟等硬體,出於許可權控制的考慮,因此⼤多數作業系統,把記憶體分成了兩個區域:

  • 核心空間,這個記憶體空間只有核心程式可以訪問;
  • ⽤戶空間,這個記憶體空間專⻔給應⽤程式使⽤,許可權比較小;

⽤戶空間的程式碼只能訪問⼀個區域性的記憶體空間,⽽核心空間的程式碼可以訪問所有記憶體空間。因此,當程式使⽤⽤戶空間時,我們常說該程式在⽤戶態執⾏,⽽當程式使核心空間時,程式則在核心態執⾏。

使用者態和核心態是如何切換的?

應⽤程式如果需要進⼊核心空間,就需要通過系統調⽤,來進入核心態:

使用者態&核心態切換

核心程式執⾏在核心態,⽤戶程式執⾏在⽤戶態。當應⽤程式使⽤系統調⽤時,會產⽣⼀箇中斷。發⽣中斷後, CPU 會中斷當前在執⾏的⽤戶程式,轉⽽跳轉到中斷處理程式,也就是開始執⾏核心程式。核心處理完後,主動觸發中斷,把 CPU 執⾏許可權交回給⽤戶程式,回到⽤戶態繼續⼯作。

程式和執行緒

並行和併發有什麼區別?

併發就是在一段時間內,多個任務都會被處理;但在某一時刻,只有一個任務在執行。單核處理器做到的併發,其實是利用時間片的輪轉,例如有兩個程式A和B,A執行一個時間片之後,切換到B,B執行一個時間片之後又切換到A。因為切換速度足夠快,所以巨集觀上表現為在一段時間內能同時執行多個程式。

並行就是在同一時刻,有多個任務在執行。這個需要多核處理器才能完成,在微觀上就能同時執行多條指令,不同的程式被放到不同的處理器上執行,這個是物理上的多個程式同時進行。

併發和並行

什麼是程式上下文切換?

對於單核單執行緒 CPU 而言,在某一時刻只能執行一條 CPU 指令。上下文切換 (Context Switch) 是一種將 CPU 資源從一個程式分配給另一個程式的機制。從使用者角度看,計算機能夠並行執行多個程式,這恰恰是作業系統通過快速上下文切換造成的結果。在切換的過程中,作業系統需要先儲存當前程式的狀態 (包括記憶體空間的指標,當前執行完的指令等等),再讀入下一個程式的狀態,然後執行此程式。

程式上下文切換-來源參考[3]

程式有哪些狀態?

當一個程式開始執行時,它可能會經歷下面這幾種狀態:

上圖中各個狀態的意義:

  • 運⾏狀態(Runing):該時刻程式佔⽤ CPU;
  • 就緒狀態(Ready):可運⾏,由於其他程式處於運⾏狀態⽽暫時停⽌運⾏;
  • 阻塞狀態(Blocked):該程式正在等待某⼀事件發⽣(如等待輸⼊/輸出操作的完成)⽽暫時停⽌運⾏,這時,即使給它CPU控制權,它也⽆法運⾏;

程式3種狀態

當然,程式還有另外兩個基本狀態:

  • 建立狀態(new):程式正在被建立時的狀態;
  • 結束狀態(Exit):程式正在從系統中消失時的狀態;

程式5種狀態

什麼是殭屍程式?

殭屍程式是已完成且處於終止狀態,但在程式表中卻仍然存在的程式。

殭屍程式一般發生有父子關係的程式中,一個子程式的程式描述符在子程式退出時不會釋放,只有當父程式通過 wait() 或 waitpid() 獲取了子程式資訊後才會釋放。如果子程式退出,而父程式並沒有呼叫 wait() 或 waitpid(),那麼子程式的程式描述符仍然儲存在系統中。

什麼是孤兒程式?

一個父程式退出,而它的一個或多個子程式還在執行,那麼這些子程式將成為孤兒程式。孤兒程式將被 init 程式 (程式 ID 為 1 的程式) 所收養,並由 init 程式對它們完成狀態收集工作。因為孤兒程式會被 init 程式收養,所以孤兒程式不會對系統造成危害。

程式有哪些排程演算法?

程式排程就是確定某一個時刻CPU執行哪個程式,常見的程式排程演算法有:

程式排程演算法

  • 先來先服務

非搶佔式的排程演算法,按照請求的順序進行排程。有利於長作業,但不利於短作業,因為短作業必須一直等待前面的長作業執行完畢才能執行,而長作業又需要執行很長時間,造成了短作業等待時間過長。另外,對I/O密集型程式也不利,因為這種程式每次進行I/O操作之後又得重新排隊。

先來先服務

  • 短作業優先

非搶佔式的排程演算法,按估計執行時間最短的順序進行排程。長作業有可能會餓死,處於一直等待短作業執行完畢的狀態。因為如果一直有短作業到來,那麼長作業永遠得不到排程。

短作業優先

  • 優先順序排程

為每個程式分配一個優先順序,按優先順序進行排程。為了防止低優先順序的程式永遠等不到排程,可以隨著時間的推移增加等待程式的優先順序。

優先順序排程

  • 時間片輪轉

將所有就緒程式按 先來先服務的原則排成一個佇列,每次排程時,把 CPU 時間分配給隊首程式,該程式可以執行一個時間片。當時間片用完時,由計時器發出時鐘中斷,排程程式便停止該程式的執行,並將它送往就緒佇列的末尾,同時繼續把 CPU 時間分配給隊首的程式。

時間片輪轉演算法的效率和時間片的大小有很大關係:因為程式切換都要儲存程式的資訊並且載入新程式的資訊,如果時間片太小,會導致程式切換得太頻繁,在程式切換上就會花過多時間。 而如果時間片過長,那麼實時性就不能得到保證。

時間片輪轉

  • 最短剩餘時間優先

最短作業優先的搶佔式版本,按剩餘執行時間的順序進行排程。 當一個新的作業到達時,其整個執行時間與當前程式的剩餘時間作比較。如果新的程式需要的時間更少,則掛起當前程式,執行新的程式。否則新的程式等待。

程式間通訊有哪些方式?

程式間通訊方式

  • 管道:管道可以理解成不同程式之間的對白,一方發聲,一方接收,聲音的介質可是是空氣或者電纜,程式之間就可以通過管道,所謂的管道就是核心中的一串快取,從管道的一端寫入資料,就是快取在了核心裡,另一端讀取,也是從核心中讀取這段資料。

    管道可以分為兩類:匿名管道命名管道。匿名管道是單向的,只能在有親緣關係的程式間通訊;命名管道是雙向的,可以實現本機任意兩個程式通訊。

    “奉先我兒”

  • 訊號 : 訊號可以理解成一種電報,傳送方傳送內容,指定接收程式,然後發出特定的軟體中斷,作業系統接到中斷請求後,找到接收程式,通知接收程式處理訊號。

    比如kill -9 1050就表示給PID為1050的程式傳送SIGKIL訊號。Linux系統中常用訊號:

    (1)SIGHUP:使用者從終端登出,所有已啟動程式都將收到該程式。系統預設狀態下對該訊號的處理是終止程式。
    (2)SIGINT:程式終止訊號。程式執行過程中,按Ctrl+C鍵將產生該訊號。
    (3)SIGQUIT:程式退出訊號。程式執行過程中,按Ctrl+\鍵將產生該訊號。
    (4)SIGBUS和SIGSEGV:程式訪問非法地址。
    (5)SIGFPE:運算中出現致命錯誤,如除零操作、資料溢位等。
    (6)SIGKILL:使用者終止程式執行訊號。shell下執行kill -9傳送該訊號。
    (7)SIGTERM:結束程式訊號。shell下執行kill 程式pid傳送該訊號。
    (8)SIGALRM:定時器訊號。
    (9)SIGCLD:子程式退出訊號。如果其父程式沒有忽略該訊號也沒有處理該訊號,則子程式退出後將形成殭屍程式。

  • 訊息佇列:訊息佇列就是儲存在核心中的訊息連結串列,包括Posix訊息佇列和System V訊息佇列。有足夠許可權的程式可以向佇列中新增訊息,被賦予讀許可權的程式則可以讀走佇列中的訊息。訊息佇列克服了訊號承載資訊量少,管道只能承載無格式位元組流以及緩衝區大小受限等缺點。

訊息佇列

  • 共享記憶體:共享記憶體的機制,就是拿出⼀塊虛擬地址空間來,對映到相同的實體記憶體中。這樣這個程式寫⼊的東西,另外的程式⻢上就能看到。共享記憶體是最快的 IPC 方式,它是針對其他程式間通訊方式執行效率低而專門設計的。它往往與其他通訊機制,如訊號量,配合使用,來實現程式間的同步和通訊。

共享記憶體

  • 訊號量:訊號量我們可以理解成紅綠燈,紅燈行,綠燈停。它本質上是一個整數計數器,可以用來控制多個程式對共享資源的訪問。它常作為一種鎖機制,防止某程式正在訪問共享資源時,其他程式也訪問該資源。因此,主要作為程式間以及同一程式內不同執行緒之間的同步手段。

    訊號量表示資源的數量,控制訊號量的⽅式有兩種原⼦操作:

    • ⼀個是 P 操作,這個操作會把訊號量減去 1,相減後如果訊號量 < 0,則表明資源已被佔⽤,程式需阻塞等待;相減後如果訊號量 >= 0,則表明還有資源可使⽤,程式可正常繼續執⾏。
    • 另⼀個是 V 操作,這個操作會把訊號量加上 1,相加後如果訊號量 <= 0,則表明當前有阻塞中的程式,於是會將該程式喚醒運⾏;相加後如果訊號量 > 0,則表明當前沒有阻塞中的程式;

    P 操作是⽤在進⼊共享資源之前,V 操作是⽤在離開共享資源之後,這兩個操作是必須成對出現的。

    訊號量

  • Socket:與其他通訊機制不同的是,它可用於不同機器間的程式通訊。

優缺點:

  • 管道:簡單;效率低,容量有限;
  • 訊息佇列:不及時,寫入和讀取需要使用者態、核心態拷貝。
  • 共享記憶體區:能夠很容易控制容量,速度快,但需要注意不同程式的同步問題。
  • 訊號量:不能傳遞複雜訊息,一般用來實現程式間的同步;
  • 訊號:它是程式間通訊的唯一非同步機制。
  • Socket:用於不同主機程式間的通訊。

程式和執行緒的聯絡和區別?

執行緒和程式的聯絡:

執行緒是程式當中的⼀條執⾏流程。

同⼀個程式內多個執行緒之間可以共享程式碼段、資料段、開啟的⽂件等資源,但每個執行緒各⾃都有⼀套獨⽴的暫存器和棧,這樣可以確保執行緒的控制流是相對獨⽴的。

多執行緒-來源參考[3]

執行緒與程式的⽐較如下:

  • 排程:程式是資源(包括記憶體、開啟的⽂件等)分配的單位執行緒是 CPU 排程的單位
  • 資源:程式擁有⼀個完整的資源平臺,⽽執行緒只獨享必不可少的資源,如暫存器和棧;
  • 擁有資源:執行緒同樣具有就緒、阻塞、執⾏三種基本狀態,同樣具有狀態之間的轉換關係;
  • 系統開銷:執行緒能減少併發執⾏的時間和空間開銷——建立或撤銷程式時,系統都要為之分配或回收系統資源,如記憶體空間,I/O裝置等,OS所付出的開銷顯著大於在建立或撤銷執行緒時的開銷,程式切換的開銷也遠大於執行緒切換的開銷。

執行緒上下文切換了解嗎?

這還得看執行緒是不是屬於同⼀個程式:

  • 當兩個執行緒不是屬於同⼀個程式,則切換的過程就跟程式上下⽂切換⼀樣;

  • 當兩個執行緒是屬於同⼀個程式,因為虛擬記憶體是共享的,所以在切換時,虛擬記憶體這些資源就保持不動,只需要切換執行緒的私有資料、暫存器等不共享的資料

所以,執行緒的上下⽂切換相⽐程式,開銷要⼩很多。

執行緒有哪些實現方式?

主要有三種執行緒的實現⽅式:

  • 核心態執行緒實現:在核心空間實現的執行緒,由核心直接管理直接管理執行緒。

核心態執行緒實現

  • ⽤戶態執行緒實現:在⽤戶空間實現執行緒,不需要核心的參與,核心對執行緒無感知。

使用者態執行緒

  • 混合執行緒實現:現代作業系統基本都是將兩種方式結合起來使用。使用者態的執行系統負責程式內部執行緒在非阻塞時的切換;核心態的作業系統負責阻塞執行緒的切換。即我們同時實現核心態和使用者態執行緒管理。其中核心態執行緒數量較少,而使用者態執行緒數量較多。每個核心態執行緒可以服務一個或多個使用者態執行緒。

混合執行緒實現

執行緒間如何同步?

同步解決的多執行緒操作共享資源的問題,目的是不管執行緒之間的執行如何穿插,最後的結果都是正確的。

我們前面知道執行緒和程式的關係:執行緒是程式當中的⼀條執⾏流程。所以說下面的一些同步機制不止針對執行緒,同樣也可以針對程式。

臨界區:我們把對共享資源訪問的程式片段稱為臨界區,我們希望這段程式碼是互斥的,保證在某時刻只能被一個執行緒執行,也就是說一個執行緒在臨界區執行時,其它執行緒應該被阻止進入臨界區。

臨界區互斥-來源參考[3]

臨界區不僅針對執行緒,同樣針對程式。

臨界區同步的一些實現方式:

1、

使⽤加鎖操作和解鎖操作可以解決併發執行緒/程式的互斥問題。

任何想進⼊臨界區的執行緒,必須先執⾏加鎖操作。若加鎖操作順利通過,則執行緒可進⼊臨界區;在完成對臨界資源的訪問後再執⾏解鎖操作,以釋放該臨界資源。

加鎖和解鎖鎖住的是什麼呢?可以是臨界區物件,也可以只是一個簡單的互斥量,例如互斥量是0無鎖,1表示加鎖。

加鎖和解鎖-來源參考[3]

根據鎖的實現不同,可以分為忙等待鎖和⽆忙等待鎖

忙等待鎖和就是加鎖失敗的執行緒,會不斷嘗試獲取鎖,也被稱為自旋鎖,它會一直佔用CPU。

⽆忙等待鎖就是加鎖失敗的執行緒,會進入阻塞狀態,放棄CPU,等待被排程。

2、訊號量

訊號量是作業系統提供的⼀種協調共享資源訪問的⽅法。

通常訊號量表示資源的數量,對應的變數是⼀個整型( sem )變數。

另外,還有兩個原⼦操作的系統調⽤函式來控制訊號量的,分別是:

  • P 操作:將 sem 減 1 ,相減後,如果 sem < 0 ,則程式/執行緒進⼊阻塞等待,否則繼續,表明 P操作可能會阻塞;

  • V 操作:將 sem 加 1 ,相加後,如果 sem <= 0 ,喚醒⼀個等待中的程式/執行緒,表明 V 操作不會阻塞;

P 操作是⽤在進⼊臨界區之前,V 操作是⽤在離開臨界區之後,這兩個操作是必須成對出現的。

什麼是死鎖?

在兩個或者多個併發執行緒中,如果每個執行緒持有某種資源,而又等待其它執行緒釋放它或它們現在保持著的資源,在未改變這種狀態之前都不能向前推進,稱這一組執行緒產生了死鎖。通俗的講就是兩個或多個執行緒無限期的阻塞、相互等待的一種狀態。

死鎖

死鎖產生有哪些條件?

死鎖產生需要同時滿足四個條件:

  • 互斥條件:指執行緒對己經獲取到的資源進行它性使用,即該資源同時只由一個執行緒佔用。如果此時還有其它執行緒請求獲取獲取該資源,則請求者只能等待,直至佔有資源的執行緒釋放該資源。
  • 請求並持有條件:指一個 執行緒己經持有了至少一個資源,但又提出了新的資源請求,而新資源己被其它執行緒佔有,所以當前執行緒會被阻塞,但阻塞 的同時並不釋放自己已經獲取的資源。
  • 不可剝奪條件:指執行緒獲取到的資源在自己使用完之前不能被其它執行緒搶佔,只有在自己使用完畢後才由自己釋放該資源。
  • 環路等待條件:指在發生死鎖時,必然存在一個執行緒——資源的環形鏈,即執行緒集合 {T0,T1,T2,…… ,Tn} 中 T0 正在等待一 T1 佔用的資源,Tl1正在等待 T2用的資源,…… Tn 在等待己被 T0佔用的資源。

如何避免死鎖呢?

產⽣死鎖的有四個必要條件:互斥條件、持有並等待條件、不可剝奪條件、環路等待條件。

避免死鎖,破壞其中的一個就可以。

消除互斥條件

這個是沒法實現,因為很多資源就是隻能被一個執行緒佔用,例如鎖。

消除請求並持有條件

消除這個條件的辦法很簡單,就是一個執行緒一次請求其所需要的所有資源。

消除不可剝奪條件

佔用部分資源的執行緒進一步申請其他資源時,如果申請不到,可以主動釋放它佔有的資源,這樣不可剝奪這個條件就破壞掉了。

消除環路等待條件

可以靠按序申請資源來預防。所謂按序申請,是指資源是有線性順序的,申請的時候可以先申請資源序號小的,再申請資源序號大的,這樣線性化後就不存在環路了。

活鎖和飢餓鎖瞭解嗎?

飢餓鎖:

飢餓鎖,這個飢餓指的是資源飢餓,某個執行緒一直等不到它所需要的資源,從而無法向前推進,就像一個人因為飢餓無法成長。

活鎖:

在活鎖狀態下,處於活鎖執行緒組裡的執行緒狀態可以改變,但是整個活鎖組的執行緒無法推進。

活鎖可以用兩個人過一條很窄的小橋來比喻:為了讓對方先過,兩個人都往旁邊讓,但兩個人總是讓到同一邊。這樣,雖然兩個人的狀態一直在變化,但卻都無法往前推進。

記憶體管理

什麼是虛擬記憶體?

我們實際的實體記憶體主要是主存,但是物理主存空間有限,所以一般現代作業系統都會想辦法把一部分記憶體塊放到磁碟中,用到的時候再裝入主存,但是對使用者程式而言,是不需要注意實際的實體記憶體的,為什麼呢?因為有虛擬記憶體的機制。

簡單說,虛擬記憶體是作業系統提供的⼀種機制,將不同程式的虛擬地址和不同記憶體的實體地址對映起來。

每個程式都有自己獨立的地址空間,再由作業系統對映到到實際的實體記憶體。

於是,這⾥就引出了兩種地址的概念:

程式所使⽤的記憶體地址叫做虛擬記憶體地址Virtual Memory Address

實際存在硬體⾥⾯的空間地址叫實體記憶體地址Physical Memory Address)。

虛擬記憶體

什麼是記憶體分段?

程式是由若⼲個邏輯分段組成的,如可由程式碼分段、資料分段、棧段、堆段組成。不同的段是有不同的屬性的,所以就⽤分段(Segmentation)的形式把這些段分離出來。

分段機制下的虛擬地址由兩部分組成,段號段內偏移量

虛擬地址和實體地址通過段表對映,段表主要包括段號段的界限

虛擬地址、段表、實體地址

我們來看一個對映,虛擬地址:段3、段偏移量500 ----> 段基地址7000+段偏移量500 ----> 實體地址:7500。

段虛擬地址對映

什麼是記憶體分頁?

分⻚是把整個虛擬和實體記憶體空間切成⼀段段固定尺⼨的⼤⼩。這樣⼀個連續並且尺⼨固定的記憶體空間,我們叫Page)。在 Linux 下,每⼀⻚的⼤⼩為 4KB 。

訪問分頁系統中記憶體資料需要兩次的記憶體訪問 :一次是從記憶體中訪問頁表,從中找到指定的物理頁號,加上頁內偏移得到實際實體地址,第二次就是根據第一次得到的實體地址訪問記憶體取出資料。

記憶體分頁

多級頁表知道嗎?

作業系統可能會有非常多程式,如果只是使用簡單分頁,可能導致的後果就是頁表變得非常龐大。

所以,引入了多級頁表的解決方案。

所謂的多級頁表,就是把我們原來的單級頁表再次分頁,這裡利用了區域性性原理,除了頂級頁表,其它級別的頁表一來可以在需要的時候才被建立,二來記憶體緊張的時候還可以被置換到磁碟中。

多級頁表示意圖

什麼是塊表?

同樣利用了區域性性原理,即在⼀段時間內,整個程式的執⾏僅限於程式中的某⼀部分。相應地,執⾏所訪問的儲存空間也侷限於某個記憶體區域。

利⽤這⼀特性,把最常訪問的⼏個⻚表項儲存到訪問速度更快的硬體,於是電腦科學家們,就在 CPU 芯⽚中,加⼊了⼀個專⻔存放程式最常訪問的⻚表項的 Cache,這個 Cache 就是 TLB(Translation Lookaside Buffer) ,通常稱為⻚表快取、轉址旁路快取、快表等。

TLB示意圖-來源參考[3]

分頁和分段有什麼區別?

  • 段是資訊的邏輯單位,它是根據使用者的需要劃分的,因此段對使用者是可見的 ;頁是資訊的物理單位,是為了管理主存的方便而劃分的,對使用者是透明的。
  • 段的大小不固定,有它所完成的功能決定;頁的大小固定,由系統決定
  • 段向使用者提供二維地址空間;頁向使用者提供的是一維地址空間
  • 段是資訊的邏輯單位,便於儲存保護和資訊的共享,頁的保護和共享受到限制。

什麼是交換空間?

作業系統把實體記憶體(Physical RAM)分成一塊一塊的小記憶體,每一塊記憶體被稱為頁(page)。當記憶體資源不足時,Linux把某些頁的內容轉移至磁碟上的一塊空間上,以釋放記憶體空間。磁碟上的那塊空間叫做交換空間(swap space),而這一過程被稱為交換(swapping)。實體記憶體和交換空間的總容量就是虛擬記憶體的可用容量。

用途:

  • 實體記憶體不足時一些不常用的頁可以被交換出去,騰給系統。
  • 程式啟動時很多記憶體頁被用來初始化,之後便不再需要,可以交換出去。

頁面置換演算法有哪些?

在分頁系統裡,一個虛擬的頁面可能在主存裡,也可能在磁碟中,如果CPU發現虛擬地址對應的物理頁不在主存裡,就會產生一個缺頁中斷,然後從磁碟中把該頁調入主存中。

如果記憶體裡沒有空間,就需要從主存裡選擇一個頁面來置換。

常見的頁面置換演算法:

常見頁面置換演算法

  • 最佳⻚⾯置換演算法(OPT

最佳⻚⾯置換演算法是一個理想的演算法,基本思路是,置換在未來最⻓時間不訪問的⻚⾯

所以,該演算法實現需要計算記憶體中每個邏輯⻚⾯的下⼀次訪問時間,然後⽐較,選擇未來最⻓時間不訪問的⻚⾯。

但這個演算法是無法實現的,因為當缺頁中斷髮生時,作業系統無法知道各個頁面下一次將在什麼時候被訪問。

  • 先進先出置換演算法(FIFO)

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

FIFO的實現機制是使用連結串列將所有在記憶體的頁面按照進入時間的早晚連結起來,然後每次置換連結串列頭上的頁面就行了,新加進來的頁面則掛在連結串列的末端。

按照進入記憶體早晚構建的頁面連結串列

  • 最近最久未使⽤的置換演算法(LRU)

最近最久未使⽤(LRU)的置換演算法的基本思路是,發⽣缺⻚時,選擇最⻓時間沒有被訪問的⻚⾯進⾏置換,也就是說,該演算法假設已經很久沒有使⽤的⻚⾯很有可能在未來較⻓的⼀段時間內仍然不會被使⽤。

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

LRU 在理論上是可以實現的,但代價很⾼。為了完全實現 LRU,需要在記憶體中維護⼀個所有⻚⾯的連結串列,最近最多使⽤的⻚⾯在表頭,最近最少使⽤的⻚⾯在表尾。

LRU實現

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

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

  • 時鐘頁面置換演算法

這個演算法的思路是,把所有的⻚⾯都儲存在⼀個類似鍾⾯的環形連結串列中,⼀個錶針指向最⽼的⻚⾯。

時鐘頁面置換演算法

當發⽣缺⻚中斷時,演算法⾸先檢查錶針指向的⻚⾯:

如果它的訪問位位是 0 就淘汰該⻚⾯,並把新的⻚⾯插⼊這個位置,然後把錶針前移⼀個位置;

如果訪問位是 1 就清除訪問位,並把錶針前移⼀個位置,重複這個過程直到找到了⼀個訪問位為 0 的⻚⾯為⽌;

  • 最不常⽤置換演算法

最不常用演算法(LFU),當發⽣缺⻚中斷時,選擇訪問次數最少的那個⻚⾯,將其置換

它的實現⽅式是,對每個⻚⾯設定⼀個「訪問計數器」,每當⼀個⻚⾯被訪問時,該⻚⾯的訪問計數器就累加 1。在發⽣缺⻚中斷時,淘汰計數器值最⼩的那個⻚⾯。

檔案

硬連結和軟連結有什麼區別?

  • 硬連結就是在目錄下建立一個條目,記錄著檔名與 inode 編號,這個 inode 就是原始檔的 inode。刪除任意一個條目,檔案還是存在,只要引用數量不為 0。但是硬連結有限制,它不能跨越檔案系統,也不能對目錄進行連結。

硬連結-來源參考[3]

  • 軟連結相當於重新建立⼀個⽂件,這個⽂件有獨⽴的 inode,但是這個⽂件的內容是另外⼀個⽂件的路徑,所以訪問軟連結的時候,實際上相當於訪問到了另外⼀個⽂件,所以軟連結是可以跨⽂件系統的,甚⾄⽬標⽂件被刪除了,連結⽂件還是在的,只不過打不開指向的檔案了而已。

    軟連結-來源參考[3]

IO

零拷貝瞭解嗎?

假如需要檔案傳輸,使用傳統I/O,資料讀取和寫入是使用者空間到核心空間來回賦值,而核心空間的資料是通過作業系統的I/O介面從磁碟讀取或者寫入,這期間發生了多次使用者態和核心態的上下文切換,以及多次資料拷貝。

傳統檔案傳輸示意圖-來源參考[3]

為了提升I/O效能,就需要減少使用者態與核心態的上下文切換記憶體拷貝的次數

這就用到了我們零拷貝的技術,零拷貝技術實現主要有兩種:

  • mmap + write

mmap() 系統調⽤函式會直接把核心緩衝區⾥的資料「對映」到⽤戶空間,這樣,作業系統核心與⽤戶空間就不需要再進⾏任何的資料拷⻉操作。

mmap示意圖-來源參考[3]

  • sendfile

在 Linux 核心版本 2.1 中,提供了⼀個專⻔傳送⽂件的系統調⽤函式 sendfile() 。

⾸先,它可以替代前⾯的 read() 和 write() 這兩個系統調⽤,這樣就可以減少⼀次系統調⽤,也就減少了 2 次上下⽂切換的開銷。

其次,該系統調⽤,可以直接把核心緩衝區⾥的資料拷⻉到 socket 緩衝區⾥,不再拷⻉到⽤戶態,這樣就只有 2 次上下⽂切換,和 3 次資料拷⻉。

sendfile示意圖-來源參考[3]

很多開源專案如Kafka、RocketMQ都採用了零拷貝技術來提升IO效率。

聊聊阻塞與⾮阻塞 **I/O **、 同步與非同步 I/O

  • 阻塞I/O

先來看看阻塞 I/O,當⽤戶程式執⾏ read ,執行緒會被阻塞,⼀直等到核心資料準備好,並把資料從核心緩衝區拷⻉到應⽤程式的緩衝區中,當拷⻉過程完成, read 才會返回。

注意,阻塞等待的是核心資料準備好資料從核心態拷⻉到⽤戶態這兩個過程

阻塞I/O

  • 非阻塞I/O

⾮阻塞的 read 請求在資料未準備好的情況下⽴即返回,可以繼續往下執⾏,此時應⽤程式不斷輪詢核心,直到資料準備好,核心將資料拷⻉到應⽤程式緩衝區, read 調⽤才可以獲取到結果。

非阻塞I/O

  • 基於非阻塞的I/O多路複用

我們上面的非阻塞I/O有一個問題,什麼問題呢?應用程式要一直輪詢,這個過程沒法幹其它事情,所以引入了I/O 多路復⽤技術。

當核心資料準備好時,以事件通知應⽤程式進⾏操作。

基於非阻塞的I/O多路複用

注意:⽆論是阻塞 I/O、還是⾮阻塞 I/O、非阻塞I/O多路複用,都是同步調⽤。因為它們在read調⽤時,核心將資料從核心空間拷⻉到應⽤程式空間,過程都是需要等待的,也就是說這個過程是同步的,如果核心實現的拷⻉效率不⾼,read調⽤就會在這個同步過程中等待⽐較⻓的時間。

  • 非同步I/O

真正的非同步 I/O核心資料準備好資料從核心態拷⻉到⽤戶態這兩個過程都不⽤等待。

發起 aio_read 之後,就⽴即返回,核心⾃動將資料從核心空間拷⻉到應⽤程式空間,這個拷⻉過程同樣是非同步的,核心⾃動完成的,和前⾯的同步操作不⼀樣,應⽤程式並不需要主動發起拷⻉動作。

非同步/IO

拿例子理解幾種I/O模型

老三關注了很多UP主,有些UP主是老鴿子,到了更新的時間:

阻塞I/O就是,老三不幹別的,就乾等著,盯著UP的更新。

非阻塞I/O就是,老三發現UP沒更,就去喝個茶什麼的,過一會兒來盯一次,一直等到UP更新。

基於⾮阻塞的 I/O 多路復⽤好⽐,老三發現UP沒更,就去幹別的,過了一會兒B站推送訊息了,老三一看,有很多條,就去翻動態,看看等的UP是不是更新了。

非同步I/O就是,老三說UP你該更了,UP趕緊爆肝把視訊做出來,然後把視訊親自呈到老三面前,這個過程不用等待。

鴿宗

詳細講一講I/O多路複用?

我們先了解什麼是I/O多路複用?

我們在傳統的I/O模型中,如果服務端需要支援多個客戶端,我們可能要為每個客戶端分配一個程式/執行緒。

不管是基於重一點的程式模型,還是輕一點的執行緒模型,假如連線多了,作業系統是扛不住的。

所以就引入了I/O多路複用 技術。

簡單說,就是一個程式/執行緒維護多個Socket,這個多路複用就是多個連線複用一個程式/執行緒。

I/O多路複用

我們來看看I/O多路複用三種實現機制:

  • select

select 實現多路復⽤的⽅式是:

將已連線的 Socket 都放到⼀個⽂件描述符集合fd_set,然後調⽤ select 函式將fd_set集合拷⻉到核心⾥,讓核心來檢查是否有⽹絡事件產⽣,檢查的⽅式很粗暴,就是通過遍歷fd_set的⽅式,當檢查到有事件產⽣後,將此 Socket 標記為可讀或可寫, 接著再把整個fd_set拷⻉回⽤戶態⾥,然後⽤戶態還需要再通過遍歷的⽅法找到可讀或可寫的 Socket,再對其處理。

select 使⽤固定⻓度的 BitsMap,表示⽂件描述符集合,⽽且所⽀持的⽂件描述符的個數是有限制的,在Linux 系統中,由核心中的 FD_SETSIZE 限制, 預設最⼤值為 1024 ,只能監聽 0~1023 的⽂件描述符。

select機制的缺點:

(1)每次呼叫select,都需要把fd_set集合從使用者態拷貝到核心態,如果fd_set集合很大時,那這個開銷也很大,比如百萬連線卻只有少數活躍連線時這樣做就太沒有效率。

(2)每次呼叫select都需要在核心遍歷傳遞進來的所有fd_set,如果fd_set集合很大時,那這個開銷也很大。

(3)為了減少資料拷貝帶來的效能損壞,核心對被監控的fd_set集合大小做了限制,一般為1024,如果想要修改會比較麻煩,可能還需要編譯核心。

(4)每次呼叫select之前都需要遍歷設定監聽集合,重複工作。

  • poll

poll 不再⽤ BitsMap 來儲存所關注的⽂件描述符,取⽽代之⽤動態陣列,以連結串列形式來組織,突破了select 的⽂件描述符個數限制,當然還會受到系統⽂件描述符限制。

但是 poll 和 select 並沒有太⼤的本質區別,都是使⽤線性結構儲存程式關注的Socket集合,因此都需要遍歷⽂件描述符集合來找到可讀或可寫的Socke,時間複雜度為O(n),⽽且也需要在⽤戶態與核心態之間拷⻉⽂件描述符集合,這種⽅式隨著併發數上來,效能的損耗會呈指數級增⻓。

  • epoll

epoll 通過兩個⽅⾯,很好解決了 select/poll 的問題。

第⼀點,epoll 在核心⾥使⽤紅⿊樹來跟蹤程式所有待檢測的⽂件描述字,把需要監控的 socket 通過epoll_ctl() 函式加⼊核心中的紅⿊樹⾥,紅⿊樹是個⾼效的資料結構,增刪查⼀般時間複雜度是O(logn) ,通過對這棵⿊紅樹進⾏操作,這樣就不需要像 select/poll 每次操作時都傳⼊整個 socket 集合,只需要傳⼊⼀個待檢測的 socket,減少了核心和⽤戶空間⼤量的資料拷⻉和記憶體分配

第⼆點, epoll 使⽤事件驅動的機制,核心⾥維護了⼀個連結串列來記錄就緒事件,當某個 socket 有事件發⽣時,通過回撥函式,核心會將其加⼊到這個就緒事件列表中,當⽤戶調⽤ epoll_wait() 函式時,只會返回有事件發⽣的⽂件描述符的個數,不需要像 select/poll 那樣輪詢掃描整個 socket 集合,⼤⼤提⾼了檢測的效率。

epoll介面作用-來源參考[3]

epoll 的⽅式即使監聽的 Socket 數量越多的時候,效率不會⼤幅度降低,能夠同時監聽的 Socket 的數⽬也⾮常的多了,上限就為系統定義的程式開啟的最⼤⽂件描述符個數。因⽽,epoll 被稱為解決 C10K 問題的利器



博主水平有限,參閱的資料在某些問題上也有一些出入,如有錯漏,歡迎指出!

參考:

[1].這可能最全的作業系統面試題

[2]. 作業系統常見面試題(2021最新版)

[3]. 小林coding《圖解系統》

[4].《現代作業系統》

[5]. 《深入理解計算機系統》

[6]. 《作業系統之哲學原理》

[7]. IO多路複用與epoll原理探究

相關文章