耗時兩週,新版的操作系統常見知識點/問題總結總算搞完了,手繪了30多張圖。大家可以用來複習操作系統或者準備操作系統面試。對於大部分公司的面試來說基本夠用了,不過,像騰訊、字節這種大廠的面試還是要適當深入一些。
這篇文章總結了一些我覺得比較重要的操作系統相關的問題比如 用戶態和內核態、系統調用、進程和線程、死鎖、內存管理、虛擬內存、文件系統等等。另外,這篇文章只是對一些操作系統比較重要概念的一個概覽,深入學習的話,建議大家還是老老實實地去看書。
操作系統基礎
什麼是操作系統?
通過以下四點可以概括操作系統到底是什麼:
- 操作系統(Operating System,簡稱 OS)是管理計算機硬件與軟件資源的程序,是計算機的基石。
- 操作系統本質上是一個運行在計算機上的軟件程序 ,主要用於管理計算機硬件和軟件資源。 舉例:運行在你電腦上的所有應用程序都通過操作系統來調用系統內存以及磁盤等等硬件。
- 操作系統存在屏蔽了硬件層的複雜性。 操作系統就像是硬件使用的負責人,統籌着各種相關事項。
- 操作系統的內核(Kernel)是操作系統的核心部分,它負責系統的內存管理,硬件設備的管理,文件系統的管理以及應用程序的管理。 內核是連接應用程序和硬件的橋樑,決定着系統的性能和穩定性。
很多人容易把操作系統的內核(Kernel)和中央處理器(CPU,Central Processing Unit)弄混。你可以簡單從下面兩點來區別:
- 操作系統的內核(Kernel)屬於操作系統層面,而 CPU 屬於硬件。
- CPU 主要提供運算,處理各種指令的能力。內核(Kernel)主要負責系統管理比如內存管理,它屏蔽了對硬件的操作。
下圖清晰說明了應用程序、內核、CPU 這三者的關係。
操作系統主要有哪些功能?
從資源管理的角度來看,操作系統有 6 大功能:
- 進程和線程的管理 :進程的創建、撤銷、阻塞、喚醒,進程間的通信等。
- 存儲管理 :內存的分配和管理、外存(磁盤等)的分配和管理等。
- 文件管理 :文件的讀、寫、創建及刪除等。
- 設備管理 :完成設備(輸入輸出設備和外部存儲設備等)的請求或釋放,以及設備啓動等功能。
- 網絡管理 :操作系統負責管理計算機網絡的使用。網絡是計算機系統中連接不同計算機的方式,操作系統需要管理計算機網絡的配置、連接、通信和安全等,以提供高效可靠的網絡服務。
- 安全管理 :用戶的身份認證、訪問控制、文件加密等,以防止非法用戶對系統資源的訪問和操作。
常見的操作系統有哪些?
Windows
目前最流行的個人桌面操作系統 ,不做多的介紹,大家都清楚。界面簡單易操作,軟件生態非常好。
玩玩電腦遊戲還是必須要有 Windows 的,所以我現在是一臺 Windows 用於玩遊戲,一臺 Mac 用於平時日常開發和學習使用。
Unix
最早的多用戶、多任務操作系統 。後面崛起的 Linux 在很多方面都參考了 Unix。
目前這款操作系統已經逐漸逐漸退出操作系統的舞臺。
Linux
Linux 是一套免費使用、開源的類 Unix 操作系統。 Linux 存在着許多不同的發行版本,但它們都使用了 Linux 內核 。
嚴格來講,Linux 這個詞本身只表示 Linux 內核,在 GNU/Linux 系統中,Linux 實際就是 Linux 內核,而該系統的其餘部分主要是由 GNU 工程編寫和提供的程序組成。單獨的 Linux 內核並不能成爲一個可以正常工作的操作系統。
很多人更傾向使用 “GNU/Linux” 一詞來表達人們通常所說的 “Linux”。
Mac OS
蘋果自家的操作系統,編程體驗和 Linux 相當,但是界面、軟件生態以及用戶體驗各方面都要比 Linux 操作系統更好。
用戶態和內核態
什麼是用戶態和內核態?
根據進程訪問資源的特點,我們可以把進程在系統上的運行分爲兩個級別:
- 用戶態(User Mode) : 用戶態運行的進程可以直接讀取用戶程序的數據,擁有較低的權限。當應用程序需要執行某些需要特殊權限的操作,例如讀寫磁盤、網絡通信等,就需要向操作系統發起系統調用請求,進入內核態。
- 內核態(Kernel Mode) :內核態運行的進程幾乎可以訪問計算機的任何資源包括系統的內存空間、設備、驅動程序等,不受限制,擁有非常高的權限。當操作系統接收到進程的系統調用請求時,就會從用戶態切換到內核態,執行相應的系統調用,並將結果返回給進程,最後再從內核態切換回用戶態。
內核態相比用戶態擁有更高的特權級別,因此能夠執行更底層、更敏感的操作。不過,由於進入內核態需要付出較高的開銷(需要進行一系列的上下文切換和權限檢查),應該儘量減少進入內核態的次數,以提高系統的性能和穩定性。
爲什麼要有用戶態和內核態?只有一個內核態不行麼?
- 在 CPU 的所有指令中,有一些指令是比較危險的比如內存分配、設置時鐘、IO 處理等,如果所有的程序都能使用這些指令的話,會對系統的正常運行造成災難性地影響。因此,我們需要限制這些危險指令只能內核態運行。這些只能由操作系統內核態執行的指令也被叫做 特權指令 。
- 如果計算機系統中只有一個內核態,那麼所有程序或進程都必須共享系統資源,例如內存、CPU、硬盤等,這將導致系統資源的競爭和衝突,從而影響系統性能和效率。並且,這樣也會讓系統的安全性降低,畢竟所有程序或進程都具有相同的特權級別和訪問權限。
因此,同時具有用戶態和內核態主要是爲了保證計算機系統的安全性、穩定性和性能。
用戶態和內核態是如何切換的?
用戶態切換到內核態的 3 種方式:
- 系統調用(Trap) :用戶態進程 主動 要求切換到內核態的一種方式,主要是爲了使用內核態才能做的事情比如讀取磁盤資源。系統調用的機制其核心還是使用了操作系統爲用戶特別開放的一箇中斷來實現。
- 中斷(Interrupt) :當外圍設備完成用戶請求的操作後,會向 CPU 發出相應的中斷信號,這時 CPU 會暫停執行下一條即將要執行的指令轉而去執行與中斷信號對應的處理程序,如果先前執行的指令是用戶態下的程序,那麼這個轉換的過程自然也就發生了由用戶態到內核態的切換。比如硬盤讀寫操作完成,系統會切換到硬盤讀寫的中斷處理程序中執行後續操作等。
- 異常(Exception):當 CPU 在執行運行在用戶態下的程序時,發生了某些事先不可知的異常,這時會觸發由當前運行進程切換到處理此異常的內核相關程序中,也就轉到了內核態,比如缺頁異常。
在系統的處理上,中斷和異常類似,都是通過中斷向量表來找到相應的處理程序進行處理。區別在於,中斷來自處理器外部,不是由任何一條專門的指令造成,而異常是執行當前指令的結果。
系統調用
什麼是系統調用?
我們運行的程序基本都是運行在用戶態,如果我們調用操作系統提供的內核態級別的子功能咋辦呢?那就需要系統調用了!
也就是說在我們運行的用戶程序中,凡是與系統態級別的資源有關的操作(如文件管理、進程控制、內存管理等),都必須通過系統調用方式向操作系統提出服務請求,並由操作系統代爲完成。
這些系統調用按功能大致可分爲如下幾類:
- 設備管理:完成設備(輸入輸出設備和外部存儲設備等)的請求或釋放,以及設備啓動等功能。
- 文件管理:完成文件的讀、寫、創建及刪除等功能。
- 進程管理:進程的創建、撤銷、阻塞、喚醒,進程間的通信等功能。
- 內存管理:完成內存的分配、回收以及獲取作業佔用內存區大小及地址等功能。
系統調用和普通庫函數調用非常相似,只是系統調用由操作系統內核提供,運行於內核態,而普通的庫函數調用由函數庫或用戶自己提供,運行於用戶態。
總結:系統調用是應用程序與操作系統之間進行交互的一種方式,通過系統調用,應用程序可以訪問操作系統底層資源例如文件、設備、網絡等。
系統調用的過程瞭解嗎?
系統調用的過程可以簡單分爲以下幾個步驟:
- 用戶態的程序發起系統調用,因爲系統調用中涉及一些特權指令(只能由操作系統內核態執行的指令),用戶態程序權限不足,因此會中斷執行,也就是 Trap(Trap 是一種中斷)。
- 發生中斷後,當前 CPU 執行的程序會中斷,跳轉到中斷處理程序。內核程序開始執行,也就是開始處理系統調用。
- 內核處理完成後,主動觸發 Trap,這樣會再次發生中斷,切換回用戶態工作。
進程和線程
什麼是進程和線程?
- 進程(Process) 是指計算機中正在運行的一個程序實例。舉例:你打開的微信就是一個進程。
- 線程(Thread) 也被稱爲輕量級進程,更加輕量。多個線程可以在同一個進程中同時執行,並且共享進程的資源比如內存空間、文件句柄、網絡連接等。舉例:你打開的微信裏就有一個線程專門用來拉取別人發你的最新的消息。
進程和線程的區別是什麼?
下圖是 Java 內存區域,我們從 JVM 的角度來說一下線程和進程之間的關係吧!
從上圖可以看出:一個進程中可以有多個線程,多個線程共享進程的堆和方法區 (JDK1.8 之後的元空間)資源,但是每個線程有自己的程序計數器、虛擬機棧 和 本地方法棧。
總結:
- 線程是進程劃分成的更小的運行單位,一個進程在其執行的過程中可以產生多個線程。
- 線程和進程最大的不同在於基本上各進程是獨立的,而各線程則不一定,因爲同一進程中的線程極有可能會相互影響。
- 線程執行開銷小,但不利於資源的管理和保護;而進程正相反。
有了進程爲什麼還需要線程?
- 進程切換是一個開銷很大的操作,線程切換的成本較低。
- 線程更輕量,一個進程可以創建多個線程。
- 多個線程可以併發處理不同的任務,更有效地利用了多處理器和多核計算機。而進程只能在一個時間幹一件事,如果在執行過程中遇到阻塞問題比如 IO 阻塞就會掛起直到結果返回。
- 同一進程內的線程共享內存和文件,因此它們之間相互通信無須調用內核。
爲什麼要使用多線程?
先從總體上來說:
- 從計算機底層來說: 線程可以比作是輕量級的進程,是程序執行的最小單位,線程間的切換和調度的成本遠遠小於進程。另外,多核 CPU 時代意味着多個線程可以同時運行,這減少了線程上下文切換的開銷。
- 從當代互聯網發展趨勢來說: 現在的系統動不動就要求百萬級甚至千萬級的併發量,而多線程併發編程正是開發高併發系統的基礎,利用好多線程機制可以大大提高系統整體的併發能力以及性能。
再深入到計算機底層來探討:
- 單核時代: 在單核時代多線程主要是爲了提高單進程利用 CPU 和 IO 系統的效率。 假設只運行了一個 Java 進程的情況,當我們請求 IO 的時候,如果 Java 進程中只有一個線程,此線程被 IO 阻塞則整個進程被阻塞。CPU 和 IO 設備只有一個在運行,那麼可以簡單地說系統整體效率只有 50%。當使用多線程的時候,一個線程被 IO 阻塞,其他線程還可以繼續使用 CPU。從而提高了 Java 進程利用系統資源的整體效率。
- 多核時代: 多核時代多線程主要是爲了提高進程利用多核 CPU 的能力。舉個例子:假如我們要計算一個複雜的任務,我們只用一個線程的話,不論系統有幾個 CPU 核心,都只會有一個 CPU 核心被利用到。而創建多個線程,這些線程可以被映射到底層多個 CPU 上執行,在任務中的多個線程沒有資源競爭的情況下,任務執行的效率會有顯著性的提高,約等於(單核時執行時間/CPU 核心數)。
線程間的同步的方式有哪些?
線程同步是兩個或多個共享關鍵資源的線程的併發執行。應該同步線程以避免關鍵的資源使用衝突。
下面是幾種常見的線程同步的方式:
- 互斥鎖(Mutex) :採用互斥對象機制,只有擁有互斥對象的線程纔有訪問公共資源的權限。因爲互斥對象只有一個,所以可以保證公共資源不會被多個線程同時訪問。比如 Java 中的
synchronized
關鍵詞和各種Lock
都是這種機制。 - 讀寫鎖(Read-Write Lock):允許多個線程同時讀取共享資源,但只有一個線程可以對共享資源進行寫操作。
- 信號量(Semaphore) :它允許同一時刻多個線程訪問同一資源,但是需要控制同一時刻訪問此資源的最大線程數量。
- 屏障(Barrier) :屏障是一種同步原語,用於等待多個線程到達某個點再一起繼續執行。當一個線程到達屏障時,它會停止執行並等待其他線程到達屏障,直到所有線程都到達屏障後,它們纔會一起繼續執行。比如 Java 中的
CyclicBarrier
是這種機制。 - 事件(Event) :Wait/Notify:通過通知操作的方式來保持多線程同步,還可以方便的實現多線程優先級的比較操作。
PCB 是什麼?包含哪些信息?
PCB(Process Control Block) 即進程控制塊,是操作系統中用來管理和跟蹤進程的數據結構,每個進程都對應着一個獨立的 PCB。你可以將 PCB 視爲進程的大腦。
當操作系統創建一個新進程時,會爲該進程分配一個唯一的進程 ID,並且爲該進程創建一個對應的進程控制塊。當進程執行時,PCB 中的信息會不斷變化,操作系統會根據這些信息來管理和調度進程。
PCB 主要包含下面幾部分的內容:
- 進程的描述信息,包括進程的名稱、標識符等等;
- 進程的調度信息,包括進程阻塞原因、進程狀態(就緒、運行、阻塞等)、進程優先級(標識進程的重要程度)等等;
- 進程對資源的需求情況,包括 CPU 時間、內存空間、I/O 設備等等。
- 進程打開的文件信息,包括文件描述符、文件類型、打開模式等等。
- 處理機的狀態信息(由處理機的各種寄存器中的內容組成的),包括通用寄存器、指令計數器、程序狀態字 PSW、用戶棧指針。
- ......
進程有哪幾種狀態?
我們一般把進程大致分爲 5 種狀態,這一點和線程很像!
- 創建狀態(new) :進程正在被創建,尚未到就緒狀態。
- 就緒狀態(ready) :進程已處於準備運行狀態,即進程獲得了除了處理器之外的一切所需資源,一旦得到處理器資源(處理器分配的時間片)即可運行。
- 運行狀態(running) :進程正在處理器上運行(單核 CPU 下任意時刻只有一個進程處於運行狀態)。
- 阻塞狀態(waiting) :又稱爲等待狀態,進程正在等待某一事件而暫停運行如等待某資源爲可用或等待 IO 操作完成。即使處理器空閒,該進程也不能運行。
- 結束狀態(terminated) :進程正在從系統中消失。可能是進程正常結束或其他原因中斷退出運行。
進程間的通信方式有哪些?
下面這部分總結參考了:《進程間通信 IPC (InterProcess Communication)》 這篇文章,推薦閱讀,總結的非常不錯。
- 管道/匿名管道(Pipes) :用於具有親緣關係的父子進程間或者兄弟進程之間的通信。
- 有名管道(Named Pipes) : 匿名管道由於沒有名字,只能用於親緣關係的進程間通信。爲了克服這個缺點,提出了有名管道。有名管道嚴格遵循先進先出(first in first out)。有名管道以磁盤文件的方式存在,可以實現本機任意兩個進程通信。
- 信號(Signal) :信號是一種比較複雜的通信方式,用於通知接收進程某個事件已經發生;
- 消息隊列(Message Queuing) :消息隊列是消息的鏈表,具有特定的格式,存放在內存中並由消息隊列標識符標識。管道和消息隊列的通信數據都是先進先出的原則。與管道(無名管道:只存在於內存中的文件;命名管道:存在於實際的磁盤介質或者文件系統)不同的是消息隊列存放在內核中,只有在內核重啓(即,操作系統重啓)或者顯式地刪除一個消息隊列時,該消息隊列纔會被真正的刪除。消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取.比 FIFO 更有優勢。消息隊列克服了信號承載信息量少,管道只能承載無格式字 節流以及緩衝區大小受限等缺點。
- 信號量(Semaphores) :信號量是一個計數器,用於多進程對共享數據的訪問,信號量的意圖在於進程間同步。這種通信方式主要用於解決與同步相關的問題並避免競爭條件。
- 共享內存(Shared memory) :使得多個進程可以訪問同一塊內存空間,不同進程可以及時看到對方進程中對共享內存中數據的更新。這種方式需要依靠某種同步操作,如互斥鎖和信號量等。可以說這是最有用的進程間通信方式。
- 套接字(Sockets) : 此方法主要用於在客戶端和服務器之間通過網絡進行通信。套接字是支持 TCP/IP 的網絡通信的基本操作單元,可以看做是不同主機之間的進程進行雙向通信的端點,簡單的說就是通信的兩方的一種約定,用套接字中的相關函數來完成通信過程。
進程的調度算法有哪些?
這是一個很重要的知識點!爲了確定首先執行哪個進程以及最後執行哪個進程以實現最大 CPU 利用率,計算機科學家已經定義了一些算法,它們是:
- 先到先服務調度算法(FCFS,First Come, First Served) : 從就緒隊列中選擇一個最先進入該隊列的進程爲之分配資源,使它立即執行並一直執行到完成或發生某事件而被阻塞放棄佔用 CPU 時再重新調度。
- 短作業優先的調度算法(SJF,Shortest Job First) : 從就緒隊列中選出一個估計運行時間最短的進程爲之分配資源,使它立即執行並一直執行到完成或發生某事件而被阻塞放棄佔用 CPU 時再重新調度。
- 時間片輪轉調度算法(RR,Round-Robin) : 時間片輪轉調度是一種最古老,最簡單,最公平且使用最廣的算法。每個進程被分配一個時間段,稱作它的時間片,即該進程允許運行的時間。
- 多級反饋隊列調度算法(MFQ,Multi-level Feedback Queue) :前面介紹的幾種進程調度的算法都有一定的侷限性。如短進程優先的調度算法,僅照顧了短進程而忽略了長進程 。多級反饋隊列調度算法既能使高優先級的作業得到響應又能使短作業(進程)迅速完成。,因而它是目前被公認的一種較好的進程調度算法,UNIX 操作系統採取的便是這種調度算法。
- 優先級調度算法(Priority) : 爲每個流程分配優先級,首先執行具有最高優先級的進程,依此類推。具有相同優先級的進程以 FCFS 方式執行。可以根據內存要求,時間要求或任何其他資源要求來確定優先級。
什麼是殭屍進程和孤兒進程?
在 Unix/Linux 系統中,子進程通常是通過 fork()系統調用創建的,該調用會創建一個新的進程,該進程是原有進程的一個副本。子進程和父進程的運行是相互獨立的,它們各自擁有自己的 PCB,即使父進程結束了,子進程仍然可以繼續運行。
當一個進程調用 exit()系統調用結束自己的生命時,內核會釋放該進程的所有資源,包括打開的文件、佔用的內存等,但是該進程對應的 PCB 依然存在於系統中。這些信息只有在父進程調用 wait()或 waitpid()系統調用時纔會被釋放,以便讓父進程得到子進程的狀態信息。
這樣的設計可以讓父進程在子進程結束時得到子進程的狀態信息,並且可以防止出現“殭屍進程”(即子進程結束後 PCB 仍然存在但父進程無法得到狀態信息的情況)。
- 殭屍進程 :子進程已經終止,但是其父進程仍在運行,且父進程沒有調用 wait()或 waitpid()等系統調用來獲取子進程的狀態信息,釋放子進程佔用的資源,導致子進程的 PCB 依然存在於系統中,但無法被進一步使用。這種情況下,子進程被稱爲“殭屍進程”。避免殭屍進程的產生,父進程需要及時調用 wait()或 waitpid()系統調用來回收子進程。
- 孤兒進程 :一個進程的父進程已經終止或者不存在,但是該進程仍在運行。這種情況下,該進程就是孤兒進程。孤兒進程通常是由於父進程意外終止或未及時調用 wait()或 waitpid()等系統調用來回收子進程導致的。爲了避免孤兒進程佔用系統資源,操作系統會將孤兒進程的父進程設置爲 init 進程(進程號爲 1),由 init 進程來回收孤兒進程的資源。
如何查看是否有殭屍進程?
Linux 下可以使用 Top 命令查找,zombie
值表示殭屍進程的數量,爲 0 則代表沒有殭屍進程。
下面這個命令可以定位殭屍進程以及該殭屍進程的父進程:
ps -A -ostat,ppid,pid,cmd |grep -e '^[Zz]'
死鎖
什麼是死鎖?
死鎖(Deadlock)描述的是這樣一種情況:多個進程/線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於進程/線程被無限期地阻塞,因此程序不可能正常終止。
能列舉一個操作系統發生死鎖的例子嗎?
假設有兩個進程 A 和 B,以及兩個資源 X 和 Y,它們的分配情況如下:
進程 | 佔用資源 | 需求資源 |
---|---|---|
A | X | Y |
B | Y | X |
此時,進程 A 佔用資源 X 並且請求資源 Y,而進程 B 已經佔用了資源 Y 並請求資源 X。兩個進程都在等待對方釋放資源,無法繼續執行,陷入了死鎖狀態。
產生死鎖的四個必要條件是什麼?
- 互斥:資源必須處於非共享模式,即一次只有一個進程可以使用。如果另一進程申請該資源,那麼必須等待直到該資源被釋放爲止。
- 佔有並等待:一個進程至少應該佔有一個資源,並等待另一資源,而該資源被其他進程所佔有。
- 非搶佔:資源不能被搶佔。只能在持有資源的進程完成任務後,該資源纔會被釋放。
- 循環等待:有一組等待進程
{P0, P1,..., Pn}
,P0
等待的資源被P1
佔有,P1
等待的資源被P2
佔有,......,Pn-1
等待的資源被Pn
佔有,Pn
等待的資源被P0
佔有。
注意 ⚠️ :這四個條件是產生死鎖的 必要條件 ,也就是說只要系統發生死鎖,這些條件必然成立,而只要上述條件之一不滿足,就不會發生死鎖。
下面是百度百科對必要條件的解釋:
如果沒有事物情況 A,則必然沒有事物情況 B,也就是說如果有事物情況 B 則一定有事物情況 A,那麼 A 就是 B 的必要條件。從邏輯學上看,B 能推導出 A,A 就是 B 的必要條件,等價於 B 是 A 的充分條件。
能寫一個模擬產生死鎖的代碼嗎?
下面通過一個實際的例子來模擬下圖展示的線程死鎖:
public class DeadLockDemo {
private static Object resource1 = new Object();//資源 1
private static Object resource2 = new Object();//資源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "線程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "線程 2").start();
}
}
Output
Thread[線程 1,5,main]get resource1
Thread[線程 2,5,main]get resource2
Thread[線程 1,5,main]waiting get resource2
Thread[線程 2,5,main]waiting get resource1
線程 A 通過 synchronized (resource1)
獲得 resource1
的監視器鎖,然後通過Thread.sleep(1000);
讓線程 A 休眠 1s 爲的是讓線程 B 得到執行然後獲取到 resource2
的監視器鎖。線程 A 和線程 B 休眠結束了都開始企圖請求獲取對方的資源,然後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。
解決死鎖的方法
解決死鎖的方法可以從多個角度去分析,一般的情況下,有預防,避免,檢測和解除四種。
-
預防 是採用某種策略,限制併發進程對資源的請求,從而使得死鎖的必要條件在系統執行的任何時間上都不滿足。
-
避免則是系統在分配資源時,根據資源的使用情況提前做出預測,從而避免死鎖的發生
-
檢測是指系統設有專門的機構,當死鎖發生時,該機構能夠檢測死鎖的發生,並精確地確定與死鎖有關的進程和資源。
-
解除 是與檢測相配套的一種措施,用於將進程從死鎖狀態下解脫出來。
死鎖的預防
死鎖四大必要條件上面都已經列出來了,很顯然,只要破壞四個必要條件中的任何一個就能夠預防死鎖的發生。
破壞第一個條件 互斥條件:使得資源是可以同時訪問的,這是種簡單的方法,磁盤就可以用這種方法管理,但是我們要知道,有很多資源 往往是不能同時訪問的 ,所以這種做法在大多數的場合是行不通的。
破壞第三個條件 非搶佔 :也就是說可以採用 剝奪式調度算法,但剝奪式調度方法目前一般僅適用於 主存資源 和 處理器資源 的分配,並不適用於所有的資源,會導致 資源利用率下降。
所以一般比較實用的 預防死鎖的方法,是通過考慮破壞第二個條件和第四個條件。
1、靜態分配策略
靜態分配策略可以破壞死鎖產生的第二個條件(佔有並等待)。所謂靜態分配策略,就是指一個進程必須在執行前就申請到它所需要的全部資源,並且知道它所要的資源都得到滿足之後纔開始執行。進程要麼佔有所有的資源然後開始執行,要麼不佔有資源,不會出現佔有一些資源等待一些資源的情況。
靜態分配策略邏輯簡單,實現也很容易,但這種策略 嚴重地降低了資源利用率,因爲在每個進程所佔有的資源中,有些資源是在比較靠後的執行時間裏採用的,甚至有些資源是在額外的情況下才使用的,這樣就可能造成一個進程佔有了一些 幾乎不用的資源而使其他需要該資源的進程產生等待 的情況。
2、層次分配策略
層次分配策略破壞了產生死鎖的第四個條件(循環等待)。在層次分配策略下,所有的資源被分成了多個層次,一個進程得到某一次的一個資源後,它只能再申請較高一層的資源;當一個進程要釋放某層的一個資源時,必須先釋放所佔用的較高層的資源,按這種策略,是不可能出現循環等待鏈的,因爲那樣的話,就出現了已經申請了較高層的資源,反而去申請了較低層的資源,不符合層次分配策略,證明略。
死鎖的避免
上面提到的 破壞 死鎖產生的四個必要條件之一就可以成功 預防系統發生死鎖 ,但是會導致 低效的進程運行 和 資源使用率 。而死鎖的避免相反,它的角度是允許系統中同時存在四個必要條件 ,只要掌握併發進程中與每個進程有關的資源動態申請情況,做出 明智和合理的選擇 ,仍然可以避免死鎖,因爲四大條件僅僅是產生死鎖的必要條件。
我們將系統的狀態分爲 安全狀態 和 不安全狀態 ,每當在未申請者分配資源前先測試系統狀態,若把系統資源分配給申請者會產生死鎖,則拒絕分配,否則接受申請,併爲它分配資源。
如果操作系統能夠保證所有的進程在有限的時間內得到需要的全部資源,則稱系統處於安全狀態,否則說系統是不安全的。很顯然,系統處於安全狀態則不會發生死鎖,系統若處於不安全狀態則可能發生死鎖。
那麼如何保證系統保持在安全狀態呢?通過算法,其中最具有代表性的 避免死鎖算法 就是 Dijkstra 的銀行家算法,銀行家算法用一句話表達就是:當一個進程申請使用資源的時候,銀行家算法 通過先 試探 分配給該進程資源,然後通過 安全性算法 判斷分配後系統是否處於安全狀態,若不安全則試探分配作廢,讓該進程繼續等待,若能夠進入到安全的狀態,則就 真的分配資源給該進程。
銀行家算法詳情可見:《一句話+一張圖說清楚——銀行家算法》 。
操作系統教程書中講述的銀行家算法也比較清晰,可以一看.
死鎖的避免(銀行家算法)改善了 資源使用率低的問題 ,但是它要不斷地檢測每個進程對各類資源的佔用和申請情況,以及做 安全性檢查 ,需要花費較多的時間。
死鎖的檢測
對資源的分配加以限制可以 預防和避免 死鎖的發生,但是都不利於各進程對系統資源的充分共享。解決死鎖問題的另一條途徑是 死鎖檢測和解除 (這裏突然聯想到了樂觀鎖和悲觀鎖,感覺死鎖的檢測和解除就像是 樂觀鎖 ,分配資源時不去提前管會不會發生死鎖了,等到真的死鎖出現了再來解決嘛,而 死鎖的預防和避免 更像是悲觀鎖,總是覺得死鎖會出現,所以在分配資源的時候就很謹慎)。
這種方法對資源的分配不加以任何限制,也不採取死鎖避免措施,但系統 定時地運行一個 “死鎖檢測” 的程序,判斷系統內是否出現死鎖,如果檢測到系統發生了死鎖,再採取措施去解除它。
進程-資源分配圖
操作系統中的每一刻時刻的系統狀態都可以用進程-資源分配圖來表示,進程-資源分配圖是描述進程和資源申請及分配關係的一種有向圖,可用於檢測系統是否處於死鎖狀態。
用一個方框表示每一個資源類,方框中的黑點表示該資源類中的各個資源,每個鍵進程用一個圓圈表示,用 有向邊 來表示進程申請資源和資源被分配的情況。
圖中 2-21 是進程-資源分配圖的一個例子,其中共有三個資源類,每個進程的資源佔有和申請情況已清楚地表示在圖中。在這個例子中,由於存在 佔有和等待資源的環路 ,導致一組進程永遠處於等待資源的狀態,發生了 死鎖。
進程-資源分配圖中存在環路並不一定是發生了死鎖。因爲循環等待資源僅僅是死鎖發生的必要條件,而不是充分條件。圖 2-22 便是一個有環路而無死鎖的例子。雖然進程 P1 和進程 P3 分別佔用了一個資源 R1 和一個資源 R2,並且因爲等待另一個資源 R2 和另一個資源 R1 形成了環路,但進程 P2 和進程 P4 分別佔有了一個資源 R1 和一個資源 R2,它們申請的資源得到了滿足,在有限的時間裏會歸還資源,於是進程 P1 或 P3 都能獲得另一個所需的資源,環路自動解除,系統也就不存在死鎖狀態了。
死鎖檢測步驟
知道了死鎖檢測的原理,我們可以利用下列步驟編寫一個 死鎖檢測 程序,檢測系統是否產生了死鎖。
- 如果進程-資源分配圖中無環路,則此時系統沒有發生死鎖
- 如果進程-資源分配圖中有環路,且每個資源類僅有一個資源,則系統中已經發生了死鎖。
- 如果進程-資源分配圖中有環路,且涉及到的資源類有多個資源,此時系統未必會發生死鎖。如果能在進程-資源分配圖中找出一個 既不阻塞又非獨立的進程 ,該進程能夠在有限的時間內歸還佔有的資源,也就是把邊給消除掉了,重複此過程,直到能在有限的時間內 消除所有的邊 ,則不會發生死鎖,否則會發生死鎖。(消除邊的過程類似於 拓撲排序)
死鎖的解除
當死鎖檢測程序檢測到存在死鎖發生時,應設法讓其解除,讓系統從死鎖狀態中恢復過來,常用的解除死鎖的方法有以下四種:
- 立即結束所有進程的執行,重新啓動操作系統 :這種方法簡單,但以前所在的工作全部作廢,損失很大。
- 撤銷涉及死鎖的所有進程,解除死鎖後繼續運行 :這種方法能徹底打破死鎖的循環等待條件,但將付出很大代價,例如有些進程可能已經計算了很長時間,由於被撤銷而使產生的部分結果也被消除了,再重新執行時還要再次進行計算。
- 逐個撤銷涉及死鎖的進程,回收其資源直至死鎖解除。
- 搶佔資源 :從涉及死鎖的一個或幾個進程中搶佔資源,把奪得的資源再分配給涉及死鎖的進程直至死鎖解除。
內存管理
內存管理主要做了什麼?
操作系統的內存管理非常重要,主要負責下面這些事情:
- 內存的分配與回收 :對進程所需的內存進行分配和釋放,malloc 函數:申請內存,free 函數:釋放內存。
- 地址轉換 :將程序中的虛擬地址轉換成內存中的物理地址。
- 內存擴充 :當系統沒有足夠的內存時,利用虛擬內存技術或自動覆蓋技術,從邏輯上擴充內存。
- 內存映射 :將一個文件直接映射到進程的進程空間中,這樣可以通過內存指針用讀寫內存的辦法直接存取文件內容,速度更快。
- 內存優化 :通過調整內存分配策略和回收算法來優化內存使用效率。
- 內存安全 :保證進程之間使用內存互不干擾,避免一些惡意程序通過修改內存來破壞系統的安全性。
- ......
什麼是內存碎片?
內存碎片是由內存的申請和釋放產生的,通常分爲下面兩種:
- 內部內存碎片(Internal Memory Fragmentation,簡稱爲內存碎片) :已經分配給進程使用但未被使用的內存。導致內部內存碎片的主要原因是,當採用固定比例比如 2 的冪次方進行內存分配時,進程所分配的內存可能會比其實際所需要的大。舉個例子,一個進程只需要 65 字節的內存,但爲其分配了 128(2^7) 大小的內存,那 63 字節的內存就成爲了內部內存碎片。
- 外部內存碎片(External Memory Fragmentation,簡稱爲外部碎片) :由於未分配的連續內存區域太小,以至於不能滿足任意進程所需要的內存分配請求,這些小片段且不連續的內存空間被稱爲外部碎片。也就是說,外部內存碎片指的是那些併爲分配給進程但又不能使用的內存。我們後面介紹的分段機制就會導致外部內存碎片。
內存碎片會導致內存利用率下降,如何減少內存碎片是內存管理要非常重視的一件事情。
常見的內存管理方式有哪些?
內存管理方式可以簡單分爲下面兩種:
- 連續內存管理 :爲一個用戶程序分配一個連續的內存空間,內存利用率一般不高。
- 非連續內存管理 :允許一個程序使用的內存分佈在離散或者說不相鄰的內存中,相對更加靈活一些。
連續內存管理
塊式管理 是早期計算機操作系統的一種連續內存管理方式,存在嚴重的內存碎片問題。塊式管理會將內存分爲幾個固定大小的塊,每個塊中只包含一個進程。如果程序運行需要內存的話,操作系統就分配給它一塊,如果程序運行只需要很小的空間的話,分配的這塊內存很大一部分幾乎被浪費了。這些在每個塊中未被利用的空間,我們稱之爲內部內存碎片。除了內部內存碎片之外,由於兩個內存塊之間可能還會有外部內存碎片,這些不連續的外部內存碎片由於太小了無法再進行分配。
在 Linux 系統中,連續內存管理採用了 夥伴系統(Buddy System)算法 來實現,這是一種經典的連續內存分配算法,可以有效解決外部內存碎片的問題。夥伴系統的主要思想是將內存按 2 的冪次劃分(每一塊內存大小都是 2 的冪次比如 2^6=64 KB),並將相鄰的內存塊組合成一對夥伴(注意:必須是相鄰的纔是夥伴)。
當進行內存分配時,夥伴系統會嘗試找到大小最合適的內存塊。如果找到的內存塊過大,就將其一分爲二,分成兩個大小相等的夥伴塊。如果還是大的話,就繼續切分,直到到達合適的大小爲止。
假設兩塊相鄰的內存塊都被釋放,系統會將這兩個內存塊合併,進而形成一個更大的內存塊,以便後續的內存分配。這樣就可以減少內存碎片的問題,提高內存利用率。
雖然解決了外部內存碎片的問題,但夥伴系統仍然存在內存利用率不高的問題(內部內存碎片)。這主要是因爲夥伴系統只能分配大小爲 2^n 的內存塊,因此當需要分配的內存大小不是 2^n 的整數倍時,會浪費一定的內存空間。舉個例子:如果要分配 65 大小的內存快,依然需要分配 2^7=128 大小的內存塊。
對於內部內存碎片的問題,Linux 採用 SLAB 進行解決。由於這部分內容不是本篇文章的重點,這裏就不詳細介紹了。
非連續內存管理
非連續內存管理存在下面 3 種方式:
- 段式管理 :以段(—段連續的物理內存)的形式管理/分配物理內存。應用程序的虛擬地址空間被分爲大小不等的段,段是有實際意義的,每個段定義了一組邏輯信息,例如有主程序段 MAIN、子程序段 X、數據段 D 及棧段 S 等。
- 頁式管理 :把物理內存分爲連續等長的物理頁,應用程序的虛擬地址空間劃也被分爲連續等長的虛擬頁,現代操作系統廣泛使用的一種內存管理方式。
- 段頁式管理機制 :結合了段式管理和頁式管理的一種內存管理機制,把物理內存先分成若干段,每個段又繼續分成若干大小相等的頁。
虛擬內存
什麼是虛擬內存?有什麼用?
虛擬內存(Virtual Memory) 是計算機系統內存管理非常重要的一個技術,本質上來說它只是邏輯存在的,是一個假想出來的內存空間,主要作用是作爲進程訪問主存(物理內存)的橋樑並簡化內存管理。
總結來說,虛擬內存主要提供了下面這些能力:
- 隔離進程 :物理內存通過虛擬地址空間訪問,虛擬地址空間與進程一一對應。每個進程都認爲自己擁有了整個物理內存,進程之間彼此隔離,一個進程中的代碼無法更改正在由另一進程或操作系統使用的物理內存。
- 提升物理內存利用率 :有了虛擬地址空間後,操作系統只需要將進程當前正在使用的部分數據或指令加載入物理內存。
- 簡化內存管理 :進程都有一個一致且私有的虛擬地址空間,程序員不用和真正的物理內存打交道,而是藉助虛擬地址空間訪問物理內存,從而簡化了內存管理。
- 多個進程共享物理內存:進程在運行過程中,會加載許多操作系統的動態庫。這些庫對於每個進程而言都是公用的,它們在內存中實際只會加載一份,這部分稱爲共享內存。
- 提高內存使用安全性 :控制進程對物理內存的訪問,隔離不同進程的訪問權限,提高系統的安全性。
- 提供更大的可使用內存空間 : 可以讓程序擁有超過系統物理內存大小的可用內存空間。這是因爲當物理內存不夠用時,可以利用磁盤充當,將物理內存頁(通常大小爲 4 KB)保存到磁盤文件(會影響讀寫速度),數據或代碼頁會根據需要在物理內存與磁盤之間移動。
沒有虛擬內存有什麼問題?
如果沒有虛擬內存的話,程序直接訪問和操作的都是物理內存,看似少了一層中介,但多了很多問題。
具體有什麼問題呢? 這裏舉幾個例子說明(參考虛擬內存提供的能力回答這個問題):
- 用戶程序可以訪問任意物理內存,可能會不小心操作到系統運行必需的內存,進而造成操作系統崩潰,嚴重影響系統的安全。
- 同時運行多個程序容易崩潰。比如你想同時運行一個微信和一個 QQ 音樂,微信在運行的時候給內存地址 1xxx 賦值後,QQ 音樂也同樣給內存地址 1xxx 賦值,那麼 QQ 音樂對內存的賦值就會覆蓋微信之前所賦的值,這就可能會造成微信這個程序會崩潰。
- 程序運行過程中使用的所有數據或指令都要載入物理內存,根據局部性原理,其中很大一部分可能都不會用到,白白佔用了寶貴的物理內存資源。
- ......
什麼是虛擬地址和物理地址?
物理地址(Physical Address) 是真正的物理內存中地址,更具體點來說是內存地址寄存器中的地址。程序中訪問的內存地址不是物理地址,而是 虛擬地址(Virtual Address) 。
也就是說,我們編程開發的時候實際就是在和虛擬地址打交道。比如在 C 語言中,指針裏面存儲的數值就可以理解成爲內存裏的一個地址,這個地址也就是我們說的虛擬地址。
操作系統一般通過 CPU 芯片中的一個重要組件 MMU(Memory Management Unit,內存管理單元) 將虛擬地址轉換爲物理地址,這個過程被稱爲 地址翻譯/地址轉換(Address Translation) 。
通過 MMU 將虛擬地址轉換爲物理地址後,再通過總線傳到物理內存設備,進而完成相應的物理內存讀寫請求。
MMU 將虛擬地址翻譯爲物理地址的主要機制有兩種: 分段機制 和 分頁機制 。
什麼是虛擬地址空間和物理地址空間?
- 虛擬地址空間是虛擬地址的集合,是虛擬內存的範圍。每一個進程都有一個一致且私有的虛擬地址空間。
- 物理地址空間是物理地址的集合,是物理內存的範圍。
虛擬地址與物理內存地址是如何映射的?
MMU 將虛擬地址翻譯爲物理地址的主要機制有 3 種:
- 分段機制
- 分頁機制
- 段頁機制
其中,現代操作系統廣泛採用分頁機制,需要重點關注!
分段機制
分段機制(Segmentation) 以段(—段 連續 的物理內存)的形式管理/分配物理內存。應用程序的虛擬地址空間被分爲大小不等的段,段是有實際意義的,每個段定義了一組邏輯信息,例如有主程序段 MAIN、子程序段 X、數據段 D 及棧段 S 等。
段表有什麼用?地址翻譯過程是怎樣的?
分段管理通過 段表(Segment Table) 映射虛擬地址和物理地址。
分段機制下的虛擬地址由兩部分組成:
- 段號 :標識着該虛擬地址屬於整個虛擬地址空間中的哪一個段。
- 段內偏移量 :相對於該段起始地址的偏移量。
具體的地址翻譯過程如下:
- MMU 首先解析得到虛擬地址中的段號;
- 通過段號去該應用程序的段表中取出對應的段信息(找到對應的段表項);
- 從段信息中取出該段的起始地址(物理地址)加上虛擬地址中的段內偏移量得到最終的物理地址。
段表中還存有諸如段長(可用於檢查虛擬地址是否超出合法範圍)、段類型(該段的類型,例如代碼段、數據段等)等信息。
通過段號一定要找到對應的段表項嗎?得到最終的物理地址後對應的物理內存一定存在嗎?
不一定。段表項可能並不存在:
- 段表項被刪除 :軟件錯誤、軟件惡意行爲等情況可能會導致段表項被刪除。
- 段表項還未創建 :如果系統內存不足或者無法分配到連續的物理內存塊就會導致段表項無法被創建。
分段機制爲什麼會導致內存外部碎片?
分段機制容易出現外部內存碎片,即在段與段之間留下碎片空間(不足以映射給虛擬地址空間中的段)。從而造成物理內存資源利用率的降低。
舉個例子:假設可用物理內存爲 5G 的系統使用分段機制分配內存。現在有 4 個進程,每個進程的內存佔用情況如下:
- 進程 1:0~1G(第 1 段)
- 進程 2:1~3G(第 2 段)
- 進程 3:3~4.5G(第 3 段)
- 進程 4:4.5~5G(第 4 段)
此時,我們關閉了進程 1 和進程 4,則第 1 段和第 4 段的內存會被釋放,空閒物理內存還有 1.5G。由於這 1.5G 物理內存並不是連續的,導致沒辦法將空閒的物理內存分配給一個需要 1.5G 物理內存的進程。
分頁機制
分頁機制(Paging) 把主存(物理內存)分爲連續等長的物理頁,應用程序的虛擬地址空間劃也被分爲連續等長的虛擬頁。現代操作系統廣泛採用分頁機制。
注意:這裏的頁是連續等長的,不同於分段機制下不同長度的段。
在分頁機制下,應用程序虛擬地址空間中的任意虛擬頁可以被映射到物理內存中的任意物理頁上,因此可以實現物理內存資源的離散分配。分頁機制按照固定頁大小分配物理內存,使得物理內存資源易於管理,可有效避免分段機制中外部內存碎片的問題。
頁表有什麼用?地址翻譯過程是怎樣的?
分頁管理通過 頁表(Page Table) 映射虛擬地址和物理地址。我這裏畫了一張基於單級頁表進行地址翻譯的示意圖。
在分頁機制下,每個應用程序都會有一個對應的頁表。
分頁機制下的虛擬地址由兩部分組成:
- 頁號 :通過虛擬頁號可以從頁表中取出對應的物理頁號;
- 頁內偏移量 :物理頁起始地址+頁內偏移量=物理內存地址。
具體的地址翻譯過程如下:
- MMU 首先解析得到虛擬地址中的虛擬頁號;
- 通過虛擬頁號去該應用程序的頁表中取出對應的物理頁號(找到對應的頁表項);
- 用該物理頁號對應的物理頁起始地址(物理地址)加上虛擬地址中的頁內偏移量得到最終的物理地址。
頁表中還存有諸如訪問標誌(標識該頁面有沒有被訪問過)、頁類型(該段的類型,例如代碼段、數據段等)等信息。
通過虛擬頁號一定要找到對應的物理頁號嗎?找到了物理頁號得到最終的物理地址後對應的物理頁一定存在嗎?
不一定!可能會存在 頁缺失 。也就是說,物理內存中沒有對應的物理頁或者物理內存中有對應的物理頁但虛擬頁還未和物理頁建立映射(對應的頁表項不存在)。關於頁缺失的內容,後面會詳細介紹到。
單級頁表有什麼問題?爲什麼需要多級頁表?
以 32 位的環境爲例,虛擬地址空間範圍共有 2^32(4G)。假設 一個頁的大小是 2^12(4KB),那頁表項共有 4G / 4K = 2^20 個。每個頁表項爲一個地址,佔用 4 字節,2^20 * 2^2/1024*1024= 4MB。也就是說一個程序啥都不幹,頁表大小就得佔用 4M。
系統運行的應用程序多起來的話,頁表的開銷還是非常大的。而且,絕大部分應用程序可能只能用到頁表中的幾項,其他的白白浪費了。
爲了解決這個問題,操作系統引入了 多級頁表 ,多級頁表對應多個頁表,每個頁表也前一個頁表相關聯。32 位系統一般爲二級頁表,64 位系統一般爲四級頁表。
這裏以二級頁表爲例進行介紹:二級列表分爲一級頁表和二級頁表。一級頁表共有 1024 個頁表項,一級頁表又關聯二級頁表,二級頁表同樣共有 1024 個頁表項。二級頁表中的一級頁表項是一對多的關係,二級頁表按需加載(只會用到很少一部分二級頁表),進而節省空間佔用。
假設只需要 2 個二級頁表,那兩級頁表的內存佔用情況爲: 4KB(一級頁表佔用) + 4KB * 2(二級頁表佔用) = 12 KB。
多級頁表屬於時間換空間的典型場景,利用增加頁表查詢的次數減少頁表佔用的空間。
TLB 有什麼用?使用 TLB 之後的地址翻譯流程是怎樣的?
爲了提高虛擬地址到物理地址的轉換速度,操作系統在 頁表方案 基礎之上引入了 **轉址旁路緩存(Translation Lookasjde Buffer,TLB,也被稱爲快表) ** 。
在主流的 AArch64 和 x86-64 體系結構下,TLB 屬於 (Memory Management Unit,內存管理單元) 內部的單元,本質上就是一塊高速緩存(Cache),緩存了虛擬頁號到物理頁號的映射關係,你可以將其簡單看作是存儲着鍵(虛擬頁號)值(物理頁號)對的哈希表。
使用 TLB 之後的地址翻譯流程是這樣的:
- 用虛擬地址中的虛擬頁號作爲 key 去 TLB 中查詢;
- 如果能查到對應的物理頁的話,就不用再查詢頁表了,這種情況稱爲 TLB 命中(TLB hit)。
- 如果不能查到對應的物理頁的話,還是需要去查詢主存中的頁表,同時將頁表中的該映射表項添加到 TLB 中,這種情況稱爲 TLB 未命中(TLB miss)。
- 當 TLB 填滿後,又要登記新頁時,就按照一定的淘汰策略淘汰掉快表中的一個頁。
由於頁表也在主存中,因此在沒有 TLB 之前,每次讀寫內存數據時 CPU 要訪問兩次主存。有了 TLB 之後,對於存在於 TLB 中的頁表數據只需要訪問一次主存即可。
TLB 的設計思想非常簡單,但命中率往往非常高,效果很好。這就是因爲被頻繁訪問的頁就是其中的很小一部分。
看完了之後你會發現快表和我們平時經常在開發系統中使用的緩存(比如 Redis)很像,的確是這樣的,操作系統中的很多思想、很多經典的算法,你都可以在我們日常開發使用的各種工具或者框架中找到它們的影子。
換頁機制有什麼用?
換頁機制的思想是當物理內存不夠用的時候,操作系統選擇將一些物理頁的內容放到磁盤上去,等要用到的時候再將它們讀取到物理內存中。也就是說,換頁機制利用磁盤這種較低廉的存儲設備擴展的物理內存。
這也就解釋了一個日常使用電腦常見的問題:爲什麼操作系統中所有進程運行所需的物理內存即使比真實的物理內存要大一些,這些進程也是可以正常運行的,只是運行速度會變慢。
這同樣是一種時間換空間的策略,你用 CPU 的計算時間,頁的調入調出花費的時間,換來了一個虛擬的更大的物理內存空間來支持程序的運行。
什麼是頁缺失?
根據維基百科:
頁缺失(Page Fault,又名硬錯誤、硬中斷、分頁錯誤、尋頁缺失、缺頁中斷、頁故障等)指的是當軟件試圖訪問已映射在虛擬地址空間中,但是目前並未被加載在物理內存中的一個分頁時,由 MMU 所發出的中斷。
常見的頁缺失有下面這兩種:
- 硬性頁缺失(Hard Page Fault) :物理內存中沒有對應的物理頁。於是,Page Fault Hander 會指示 CPU 從已經打開的磁盤文件中讀取相應的內容到物理內存,而後交由 MMU 建立相應的虛擬頁和物理頁的映射關係。
- 軟性頁缺失(Soft Page Fault):物理內存中有對應的物理頁,但虛擬頁還未和物理頁建立映射。於是,Page Fault Hander 會指示 MMU 建立相應的虛擬頁和物理頁的映射關係。
發生上面這兩種缺頁錯誤的時候,應用程序訪問的是有效的物理內存,只是出現了物理頁缺失或者虛擬頁和物理頁的映射關係未建立的問題。如果應用程序訪問的是無效的物理內存的話,還會出現 無效缺頁錯誤(Invalid Page Fault) 。
常見的頁面置換算法有哪些?
當發生硬性頁缺失時,如果物理內存中沒有空閒的物理頁面可用的話。操作系統就必須將物理內存中的一個物理頁淘汰出去,這樣就可以騰出空間來加載新的頁面了。
用來選擇淘汰哪一個物理頁的規則叫做 頁面置換算法 ,我們可以把頁面置換算法看成是淘汰物物理頁的規則。
頁缺失太頻繁的發生會非常影響性能,一個好的頁面置換算法應該是可以減少頁缺失出現的次數。
常見的頁面置換算法有下面這 5 種(其他還有很多頁面置換算法都是基於這些算法改進得來的):
- 最佳頁面置換算法(OPT,Optimal) :優先選擇淘汰的頁面是以後永不使用的,或者是在最長時間內不再被訪問的頁面,這樣可以保證獲得最低的缺頁率。但由於人們目前無法預知進程在內存下的若干頁面中哪個是未來最長時間內不再被訪問的,因而該算法無法實現,只是理論最優的頁面置換算法,可以作爲衡量其他置換算法優劣的標準。
- 先進先出頁面置換算法(FIFO,First In First Out) : 最簡單的一種頁面置換算法,總是淘汰最先進入內存的頁面,即選擇在內存中駐留時間最久的頁面進行淘汰。該算法易於實現和理解,一般只需要通過一個 FIFO 隊列即可需求。不過,它的性能並不是很好。
- 最近最久未使用頁面置換算法(LRU ,Least Recently Used) :LRU 算法賦予每個頁面一個訪問字段,用來記錄一個頁面自上次被訪問以來所經歷的時間 T,當須淘汰一個頁面時,選擇現有頁面中其 T 值最大的,即最近最久未使用的頁面予以淘汰。LRU 算法是根據各頁之前的訪問情況來實現,因此是易於實現的。OPT 算法是根據各頁未來的訪問情況來實現,因此是不可實現的。
- 最少使用頁面置換算法(LFU,Least Frequently Used) : 和 LRU 算法比較像,不過該置換算法選擇的是之前一段時間內使用最少的頁面作爲淘汰頁。
- 時鐘頁面置換算法(Clock) :可以認爲是一種最近未使用算法,即逐出的頁面都是最近沒有使用的那個。
FIFO 頁面置換算法性能爲何不好?
主要原因主要有二:
- 經常訪問或者需要長期存在的頁面會被頻繁調入調出 :較早調入的頁往往是經常被訪問或者需要長期存在的頁,這些頁會被反覆調入和調出。
- 存在 Belady 現象 :被置換的頁面並不是進程不會訪問的,有時就會出現分配的頁面數增多但缺頁率反而提高的異常現象。出現該異常的原因是因爲 FIFO 算法只考慮了頁面進入內存的順序,而沒有考慮頁面訪問的頻率和緊迫性。
哪一種頁面置換算法實際用的比較多?
LRU 算法是實際使用中應用的比較多,也被認爲是最接近 OPT 的頁面置換算法。
不過,需要注意的是,實際應用中這些算法會被做一些改進,就比如 InnoDB Buffer Pool( InnoDB 緩衝池,MySQL 數據庫中用於管理緩存頁面的機制)就改進了傳統的 LRU 算法,使用了一種稱爲"Adaptive LRU"的算法(同時結合了 LRU 和 LFU 算法的思想)。
分頁機制和分段機制有哪些共同點和區別?
共同點 :
- 都是非連續內存管理的方式。
- 都採用了地址映射的方法,將虛擬地址映射到物理地址,以實現對內存的管理和保護。
區別 :
- 分頁機制以頁面爲單位進行內存管理,而分段機制以段爲單位進行內存管理。頁的大小是固定的,由操作系統決定,通常爲 2 的冪次方。而段的大小不固定,取決於我們當前運行的程序。
- 頁是物理單位,即操作系統將物理內存劃分成固定大小的頁面,每個頁面的大小通常是 2 的冪次方,例如 4KB、8KB 等等。而段則是邏輯單位,是爲了滿足程序對內存空間的邏輯需求而設計的,通常根據程序中數據和代碼的邏輯結構來劃分。
- 分段機制容易出現外部內存碎片,即在段與段之間留下碎片空間(不足以映射給虛擬地址空間中的段)。分頁機制解決了外部內存碎片的問題,但仍然可能會出現內部內存碎片。
- 分頁機制採用了頁表來完成虛擬地址到物理地址的映射,頁表通過一級頁表和二級頁表來實現多級映射;而分段機制則採用了段表來完成虛擬地址到物理地址的映射,每個段表項中記錄了該段的起始地址和長度信息。
- 分頁機制對程序沒有任何要求,程序只需要按照虛擬地址進行訪問即可;而分段機制需要程序員將程序分爲多個段,並且顯式地使用段寄存器來訪問不同的段。
段頁機制
結合了段式管理和頁式管理的一種內存管理機制,把物理內存先分成若干段,每個段又繼續分成若干大小相等的頁。
在段頁式機制下,地址翻譯的過程分爲兩個步驟:
- 段式地址映射。
- 頁式地址映射。
局部性原理
要想更好地理解虛擬內存技術,必須要知道計算機中著名的 局部性原理(Locality Principle)。另外,局部性原理既適用於程序結構,也適用於數據結構,是非常重要的一個概念。
局部性原理是指在程序執行過程中,數據和指令的訪問存在一定的空間和時間上的局部性特點。其中,時間局部性是指一個數據項或指令在一段時間內被反覆使用的特點,空間局部性是指一個數據項或指令在一段時間內與其相鄰的數據項或指令被反覆使用的特點。
在分頁機制中,頁表的作用是將虛擬地址轉換爲物理地址,從而完成內存訪問。在這個過程中,局部性原理的作用體現在兩個方面:
- 時間局部性 :由於程序中存在一定的循環或者重複操作,因此會反覆訪問同一個頁或一些特定的頁,這就體現了時間局部性的特點。爲了利用時間局部性,分頁機制中通常採用緩存機制來提高頁面的命中率,即將最近訪問過的一些頁放入緩存中,如果下一次訪問的頁已經在緩存中,就不需要再次訪問內存,而是直接從緩存中讀取。
- 空間局部性 :由於程序中數據和指令的訪問通常是具有一定的空間連續性的,因此當訪問某個頁時,往往會順帶訪問其相鄰的一些頁。爲了利用空間局部性,分頁機制中通常採用預取技術來預先將相鄰的一些頁讀入內存緩存中,以便在未來訪問時能夠直接使用,從而提高訪問速度。
總之,局部性原理是計算機體系結構設計的重要原則之一,也是許多優化算法的基礎。在分頁機制中,利用時間局部性和空間局部性,採用緩存和預取技術,可以提高頁面的命中率,從而提高內存訪問效率
文件系統
文件系統主要做了什麼?
文件系統主要負責管理和組織計算機存儲設備上的文件和目錄,其功能包括以下幾個方面:
- 存儲管理 :將文件數據存儲到物理存儲介質中,並且管理空間分配,以確保每個文件都有足夠的空間存儲,並避免文件之間發生衝突。
- 文件管理 :文件的創建、刪除、移動、重命名、壓縮、加密、共享等等。
- 目錄管理 :目錄的創建、刪除、移動、重命名等等。
- 文件訪問控制 :管理不同用戶或進程對文件的訪問權限,以確保用戶只能訪問其被授權訪問的文件,以保證文件的安全性和保密性。
硬鏈接和軟鏈接有什麼區別?
在 Linux/類 Unix 系統上,文件鏈接(File Link)是一種特殊的文件類型,可以在文件系統中指向另一個文件。常見的文件鏈接類型有兩種:
1、硬鏈接(Hard Link)
- 在 Linux/類 Unix 文件系統中,每個文件和目錄都有一個唯一的索引節點(inode)號,用來標識該文件或目錄。硬鏈接通過 inode 節點號建立連接,硬鏈接和源文件的 inode 節點號相同,兩者對文件系統來說是完全平等的(可以看作是互爲硬鏈接,源頭是同一份文件),刪除其中任何一個對另外一個沒有影響,可以通過給文件設置硬鏈接文件來防止重要文件被誤刪。
- 只有刪除了源文件和所有對應的硬鏈接文件,該文件纔會被真正刪除。
- 硬鏈接具有一些限制,不能對目錄以及不存在的文件創建硬鏈接,並且,硬鏈接也不能跨越文件系統。
ln
命令用於創建硬鏈接。
2、軟鏈接(Symbolic Link 或 Symlink)
- 軟鏈接和源文件的 inode 節點號不同,而是指向一個文件路徑。
- 源文件刪除後,硬鏈接依然存在,但是指向的是一個無效的文件路徑。
- 軟連接類似於 Windows 系統中的快捷方式。
- 不同於硬鏈接,可以對目錄或者不存在的文件創建軟鏈接,並且,軟鏈接可以跨越文件系統。
ln -s
命令用於創建軟鏈接。
硬鏈接爲什麼不能跨文件系統?
我們之前提到過,硬鏈接是通過 inode 節點號建立連接的,而硬鏈接和源文件共享相同的 inode 節點號。
然而,每個文件系統都有自己的獨立 inode 表,且每個 inode 表只維護該文件系統內的 inode。如果在不同的文件系統之間創建硬鏈接,可能會導致 inode 節點號衝突的問題,即目標文件的 inode 節點號已經在該文件系統中被使用。
提高文件系統性能的方式有哪些?
- 優化硬件 :使用高速硬件設備(如 SSD、NVMe)替代傳統的機械硬盤,使用 RAID(Redundant Array of Inexpensive Disks)等技術提高磁盤性能。
- 選擇合適的文件系統選型 :不同的文件系統具有不同的特性,對於不同的應用場景選擇合適的文件系統可以提高系統性能。
- 運用緩存 :訪問磁盤的效率比較低,可以運用緩存來減少磁盤的訪問次數。不過,需要注意緩存命中率,緩存命中率過低的話,效果太差。
- 避免磁盤過度使用 :注意磁盤的使用率,避免將磁盤用滿,儘量留一些剩餘空間,以免對文件系統的性能產生負面影響。
- 對磁盤進行合理的分區 :合理的磁盤分區方案,能夠使文件系統在不同的區域存儲文件,從而減少文件碎片,提高文件讀寫性能。
常見的磁盤調度算法有哪些?
磁盤調度算法是操作系統中對磁盤訪問請求進行排序和調度的算法,其目的是提高磁盤的訪問效率。
一次磁盤讀寫操作的時間由磁盤尋道/尋找時間、延遲時間和傳輸時間決定。磁盤調度算法可以通過改變到達磁盤請求的處理順序,減少磁盤尋道時間和延遲時間。
常見的磁盤調度算法有下面這 6 種(其他還有很多磁盤調度算法都是基於這些算法改進得來的):
- 先來先服務算法(First-Come First-Served,FCFS) :按照請求到達磁盤調度器的順序進行處理,先到達的請求的先被服務。FCFS 算法實現起來比較簡單,不存在算法開銷。不過,由於沒有考慮磁頭移動的路徑和方向,平均尋道時間較長。同時,該算法容易出現飢餓問題,即一些後到的磁盤請求可能需要等待很長時間才能得到服務。
- 最短尋道時間優先算法(Shortest Seek Time First,SSTF) :也被稱爲最佳服務優先(Shortest Service Time First,SSTF)算法,優先選擇距離當前磁頭位置最近的請求進行服務。SSTF 算法能夠最小化磁頭的尋道時間,但容易出現飢餓問題,即磁頭附近的請求不斷被服務,遠離磁頭的請求長時間得不到響應。實際應用中,需要優化一下該算法的實現,避免出現飢餓問題。
- 掃描算法(SCAN) :也被稱爲電梯(Elevator)算法,基本思想和電梯非常類似。磁頭沿着一個方向掃描磁盤,如果經過的磁道有請求就處理,直到到達磁盤的邊界,然後改變移動方向,依此往復。SCAN 算法能夠保證所有的請求得到服務,解決了飢餓問題。但是,如果磁頭從一個方向剛掃描完,請求才到的話。這個請求就需要等到磁頭從相反方向過來之後才能得到處理。
- 循環掃描算法(Circular Scan,C-SCAN) :SCAN 算法的變體,只在磁盤的一側進行掃描,並且只按照一個方向掃描,直到到達磁盤邊界,然後回到磁盤起點,重新開始循環。
- 邊掃描邊觀察算法(LOOK) :SCAN 算法中磁頭到了磁盤的邊界才改變移動方向,這樣可能會做很多無用功,因爲磁頭移動方向上可能已經沒有請求需要處理了。LOOK 算法對 SCAN 算法進行了改進,如果磁頭移動方向上已經沒有別的請求,就可以立即改變磁頭移動方向,依此往復。也就是邊掃描邊觀察指定方向上還有無請求,因此叫 LOOK。
- 均衡循環掃描算法(C-LOOK) :C-SCAN 只有到達磁盤邊界時才能改變磁頭移動方向,並且磁頭返回時也需要返回到磁盤起點,這樣可能會做很多無用功。C-LOOK 算法對 C-SCAN 算法進行了改進,如果磁頭移動的方向上已經沒有磁道訪問請求了,就可以立即讓磁頭返回,並且磁頭只需要返回到有磁道訪問請求的位置即可。
參考
- 《計算機操作系統—湯小丹》第四版
- 《深入理解計算機系統》
- 《重學操作系統》
- 《現代操作系統原理與實現》
- 王道考研操作系統知識點整理: https://wizardforcel.gitbooks.io/wangdaokaoyan-os/content/13.html
- 操作系統爲什麼要分用戶態和內核態:https://blog.csdn.net/chen134225/article/details/81783980
- 從根上理解用戶態與內核態:https://juejin.cn/post/6923863670132850701
- 什麼是殭屍進程與孤兒進程:https://blog.csdn.net/a745233700/article/details/120715371
- 內存管理之夥伴系統與 SLAB:https://blog.csdn.net/qq_44272681/article/details/124199068
- 爲什麼 Linux 需要虛擬內存:https://draveness.me/whys-the-design-os-virtual-memory/
- 程序員的自我修養(七):內存缺頁錯誤:https://liam.page/2017/09/01/page-fault/
- 虛擬內存的那點事兒:https://juejin.cn/post/6844903507594575886