5萬字、97 張圖總結作業系統核心知識點
阿新 • • 發佈:2020-07-14
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714090222742-1940021681.png)
文末領取大圖。
這不是一篇教你如何建立一個作業系統的文章,相反,這是一篇指導性文章,教你從幾個方面來理解作業系統。首先你需要知道你為什麼要看這篇文章以及為什麼要學習作業系統。
## 搞清楚幾個問題
首先你要搞明白你學習作業系統的目的是什麼?作業系統的重要性如何?學習作業系統會給我帶來什麼?下面我會從這幾個方面為你回答下。
作業系統也是一種軟體,但是作業系統是一種非常複雜的軟體。作業系統提供了幾種抽象模型
* 檔案:對 I/O 裝置的抽象
* 虛擬記憶體:對程式儲存器的抽象
* 程序:對一個正在執行程式的抽象
* 虛擬機器:對整個作業系統的抽象
這些抽象和我們的日常開發息息相關。搞清楚了作業系統是如何抽象的,才能培養我們的抽象性思維和開發思路。
很多問題都和作業系統相關,作業系統是解決這些問題的基礎。如果你不學習作業系統,可能會想著從框架層面來解決,那是你瞭解的還不夠深入,當你學習了作業系統後,能夠培養你的全域性性思維。
學習作業系統我們能夠有效的解決`併發`問題,併發幾乎是網際網路的重中之重了,這也從側面說明了學習作業系統的重要性。
學習作業系統的重點不是讓你從頭製造一個作業系統,而是告訴你**作業系統是如何工作的**,能夠讓你對計算機底層有所瞭解,打實你的基礎。
相信你一定清楚什麼是程式設計
**Data structures + Algorithms = Programming**
作業系統內部會涉及到眾多的資料結構和演算法描述,能夠讓你瞭解演算法的基礎上,讓你編寫更優秀的程式。
我認為可以把計算機比作一棟樓
計算機的底層相當於就是樓的根基,計算機應用相當於就是樓的外形,而作業系統就相當於是告訴你大樓的構造原理,編寫高質量的軟體就相當於是告訴你構建一個穩定的房子。
## 認識作業系統
在瞭解作業系統前,你需要先知道一下什麼是計算機系統:現代計算機系統由**一個或多個處理器、主存、印表機、鍵盤、滑鼠、顯示器、網路介面以及各種輸入/輸出裝置構成的系統**。這些都屬於`硬體`的範疇。我們程式設計師不會直接和這些硬體打交道,並且每位程式設計師不可能會掌握所有計算機系統的細節。
所以電腦科學家在硬體的基礎之上,安裝了一層軟體,這層軟體能夠根據使用者輸入的指令達到控制硬體的效果,從而滿足使用者的需求,這樣的軟體稱為 `作業系統`,它的任務就是為使用者程式提供一個更好、更簡單、更清晰的計算機模型。也就是說,作業系統相當於是一箇中間層,為使用者層和硬體提供各自的藉口,遮蔽了不同應用和硬體之間的差異,達到統一標準的作用。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084605556-1013677777.png)
上面一個作業系統的簡化圖,最底層是硬體,硬體包括**晶片、電路板、磁碟、鍵盤、顯示器**等我們上面提到的裝置,在硬體之上是軟體。大部分計算機有兩種執行模式:`核心態` 和 `使用者態`,軟體中最基礎的部分是`作業系統`,它執行在 `核心態` 中。作業系統具有硬體的訪問權,可以執行機器能夠執行的任何指令。軟體的其餘部分執行在 `使用者態` 下。
在大概瞭解到作業系統之後,我們先來認識一下硬體都有哪些
## 計算機硬體
計算機硬體是計算機的重要組成部分,其中包含了 5 個重要的組成部分:**運算器、控制器、儲存器、輸入裝置、輸出裝置**。
* `運算器`:運算器最主要的功能是對資料和資訊進行加工和運算。它是計算機中執行算數和各種邏輯運算的部件。運算器的基本運算包括加、減、乘、除、移位等操作,這些是由 `算術邏輯單元(Arithmetic&logical Unit)` 實現的。而運算器主要由算數邏輯單元和暫存器構成。
* `控制器`:指按照指定順序改變主電路或控制電路的部件,它主要起到了控制命令執行的作用,完成協調和指揮整個計算機系統的操作。控制器是由程式計數器、指令暫存器、解碼譯碼器等構成。
>運算器和控制器共同組成了 CPU
* `儲存器`:儲存器就是計算機的`記憶裝置`,顧名思義,儲存器可以儲存資訊。儲存器分為兩種,一種是主存,也就是記憶體,它是 CPU 主要互動物件,還有一種是外存,比如硬碟軟盤等。下面是現代計算機系統的儲存架構
* `輸入裝置`:輸入裝置是給計算機獲取外部資訊的裝置,它主要包括鍵盤和滑鼠。
* `輸出裝置`:輸出裝置是給使用者呈現根據輸入裝置獲取的資訊經過一系列的計算後得到顯示的裝置,它主要包括顯示器、印表機等。
這五部分也是馮諾伊曼的體系結構,它認為計算機必須具有如下功能:
把需要的程式和資料送至計算機中。必須具有長期記憶程式、資料、中間結果及最終運算結果的能力。能夠完成各種算術、邏輯運算和資料傳送等資料加工處理的能力。能夠根據需要控制程式走向,並能根據指令控制機器的各部件協調操作。能夠按照要求將處理結果輸出給使用者。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084613666-1629003807.png)
下面是一張 intel 家族產品圖,是一個詳細的計算機硬體分類,我們在根據圖中涉及到硬體進行介紹
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084619096-571366405.png)
* `匯流排(Buses)`:在整個系統中執行的是稱為匯流排的電氣管道的集合,這些匯流排在元件之間來回傳輸位元組資訊。通常匯流排被設計成傳送定長的位元組塊,也就是 `字(word)`。字中的位元組數(字長)是一個基本的系統引數,各個系統中都不盡相同。現在大部分的字都是 4 個位元組(32 位)或者 8 個位元組(64 位)。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084625911-1062062854.png)
* `I/O 裝置(I/O Devices)`:Input/Output 裝置是系統和外部世界的連線。上圖中有四類 I/O 裝置:用於使用者輸入的鍵盤和滑鼠,用於使用者輸出的顯示器,一個磁碟驅動用來長時間的儲存資料和程式。剛開始的時候,可執行程式就儲存在磁碟上。
每個I/O 裝置連線 I/O 匯流排都被稱為`控制器(controller)` 或者是 `介面卡(Adapter)`。控制器和介面卡之間的主要區別在於封裝方式。控制器是 I/O 裝置本身或者系統的主印製板電路(通常稱作主機板)上的晶片組。而介面卡則是一塊插在主機板插槽上的卡。無論組織形式如何,它們的最終目的都是彼此交換資訊。
* `主存(Main Memory)`,主存是一個`臨時儲存裝置`,而不是永久性儲存,磁碟是 `永久性儲存` 的裝置。主存既儲存程式,又儲存處理器執行流程所處理的資料。從物理組成上說,主存是由一系列 `DRAM(dynamic random access memory)` 動態隨機儲存構成的集合。邏輯上說,記憶體就是一個線性的位元組陣列,有它唯一的地址編號,從 0 開始。一般來說,組成程式的每條機器指令都由不同數量的位元組構成,C 程式變數相對應的資料項的大小根據型別進行變化。比如,在 Linux 的 x86-64 機器上,short 型別的資料需要 2 個位元組,int 和 float 需要 4 個位元組,而 long 和 double 需要 8 個位元組。
* `處理器(Processor)`,`CPU(central processing unit)` 或者簡單的處理器,是解釋(並執行)儲存在主儲存器中的指令的引擎。處理器的核心大小為一個字的儲存裝置(或暫存器),稱為`程式計數器(PC)`。在任何時刻,PC 都指向主存中的某條機器語言指令(即含有該條指令的地址)。
從系統通電開始,直到系統斷電,處理器一直在不斷地執行程式計數器指向的指令,再更新程式計數器,使其指向下一條指令。處理器根據其指令集體系結構定義的指令模型進行操作。在這個模型中,指令按照嚴格的順序執行,執行一條指令涉及執行一系列的步驟。處理器從程式計數器指向的記憶體中讀取指令,解釋指令中的位,執行該指令指示的一些簡單操作,然後更新程式計數器以指向下一條指令。指令與指令之間可能連續,可能不連續(比如 jmp 指令就不會順序讀取)
下面是 CPU 可能執行簡單操作的幾個步驟
* `載入(Load)`:從主存中拷貝一個位元組或者一個字到記憶體中,覆蓋暫存器先前的內容
* `儲存(Store)`:將暫存器中的位元組或字複製到主儲存器中的某個位置,從而覆蓋該位置的先前內容
* `操作(Operate)`:把兩個暫存器的內容複製到 `ALU(Arithmetic logic unit) `。把兩個字進行算術運算,並把結果儲存在暫存器中,重寫暫存器先前的內容。
>算術邏輯單元(ALU)是對數字二進位制數執行算術和按位運算的組合數位電子電路。
* `跳轉(jump)`:從指令中抽取一個字,把這個字複製到`程式計數器(PC)` 中,覆蓋原來的值
## 程序和執行緒
關於程序和執行緒,你需要理解下面這張腦圖中的重點
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084636497-792390654.png)
## 程序
作業系統中最核心的概念就是 `程序`,程序是對正在執行中的程式的一個抽象。作業系統的其他所有內容都是圍繞著程序展開的。
在多道程式處理的系統中,CPU 會在`程序`間快速切換,使每個程式執行幾十或者幾百毫秒。然而,嚴格意義來說,在某一個瞬間,CPU 只能執行一個程序,然而我們如果把時間定位為 1 秒內的話,它可能執行多個程序。這樣就會讓我們產生`並行`的錯覺。因為 CPU 執行速度很快,程序間的換進換出也非常迅速,因此我們很難對多個並行程序進行跟蹤。所以,作業系統的設計者開發了用於描述並行的一種概念模型(順序程序),使得並行更加容易理解和分析。
### 程序模型
一個程序就是一個正在執行的程式的例項,程序也包括程式計數器、暫存器和變數的當前值。從概念上來說,每個程序都有各自的虛擬 CPU,但是實際情況是 CPU 會在各個程序之間進行來回切換。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084645887-872058370.png)
如上圖所示,這是一個具有 4 個程式的多道處理程式,在程序不斷切換的過程中,程式計數器也在不同的變化。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084653500-1813287813.png)
在上圖中,這 4 道程式被抽象為 4 個擁有各自控制流程(即每個自己的程式計數器)的程序,並且每個程式都獨立的執行。當然,實際上只有一個物理程式計數器,每個程式要執行時,其邏輯程式計數器會裝載到物理程式計數器中。當程式執行結束後,其物理程式計數器就會是真正的程式計數器,然後再把它放回程序的邏輯計數器中。
從下圖我們可以看到,在觀察足夠長的一段時間後,所有的程序都運行了,**但在任何一個給定的瞬間僅有一個程序真正執行**。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084700409-571820975.png)
因此,當我們說一個 CPU 只能真正一次執行一個程序的時候,即使有 2 個核(或 CPU),**每一個核也只能一次執行一個執行緒**。
由於 CPU 會在各個程序之間來回快速切換,所以每個程序在 CPU 中的執行時間是無法確定的。並且當同一個程序再次在 CPU 中執行時,其在 CPU 內部的執行時間往往也是不固定的。
這裡的關鍵思想是`認識到一個程序所需的條件`,程序是某一類特定活動的總和,它有程式、輸入輸出以及狀態。
### 程序的建立
作業系統需要一些方式來建立程序。下面是一些建立程序的方式
* 系統初始化(init):啟動作業系統時,通常會建立若干個程序。
* 正在執行的程式執行了建立程序的系統呼叫(比如 fork)
* 使用者請求建立一個新程序:在許多互動式系統中,輸入一個命令或者雙擊圖示就可以啟動程式,以上任意一種操作都可以選擇開啟一個新的程序,在基本的 UNIX 系統中執行 X,新程序將接管啟動它的視窗。
* 初始化一個批處理工作
從技術上講,在所有這些情況下,讓現有流程執行流程是通過建立系統呼叫來建立新流程的。該程序可能是正在執行的使用者程序,是從鍵盤或滑鼠呼叫的系統程序或批處理程式。這些就是系統呼叫建立新程序的過程。該系統呼叫告訴作業系統建立一個新程序,並直接或間接指示在其中執行哪個程式。
在 UNIX 中,僅有一個系統呼叫來建立一個新的程序,這個系統呼叫就是 `fork`。這個呼叫會建立一個與呼叫程序相關的副本。在 fork 後,一個父程序和子程序會有相同的`記憶體映像`,相同的環境字串和相同的開啟檔案。
在 Windows 中,情況正相反,一個簡單的 Win32 功能呼叫 `CreateProcess`,會處理流程建立並將正確的程式載入到新的程序中。這個呼叫會有 10 個引數,包括了需要執行的程式、輸入給程式的命令列引數、各種安全屬性、有關開啟的檔案是否繼承控制位、優先順序資訊、程序所需要建立的視窗規格以及指向一個結構的指標,在該結構中新建立程序的資訊被返回給呼叫者。**在 Windows 中,從一開始父程序的地址空間和子程序的地址空間就是不同的**。
### 程序的終止
程序在建立之後,它就開始執行並做完成任務。然而,沒有什麼事兒是永不停歇的,包括程序也一樣。程序早晚會發生終止,但是通常是由於以下情況觸發的
* `正常退出(自願的)` : 多數程序是由於完成了工作而終止。當編譯器完成了所給定程式的編譯之後,編譯器會執行一個系統呼叫告訴作業系統它完成了工作。這個呼叫在 UNIX 中是 `exit` ,在 Windows 中是 `ExitProcess`。
* `錯誤退出(自願的)`:比如執行一條不存在的命令,於是編譯器就會提醒並退出。
* `嚴重錯誤(非自願的)`
* `被其他程序殺死(非自願的)` : 某個程序執行系統呼叫告訴作業系統殺死某個程序。在 UNIX 中,這個系統呼叫是 kill。在 Win32 中對應的函式是 `TerminateProcess`(注意不是系統呼叫)。
### 程序的層次結構
在一些系統中,當一個程序建立了其他程序後,父程序和子程序就會以某種方式進行關聯。子程序它自己就會建立更多程序,從而形成一個程序層次結構。
#### UNIX 程序體系
在 UNIX 中,程序和它的所有子程序以及子程序的子程序共同組成一個程序組。當用戶從鍵盤中發出一個訊號後,該訊號被髮送給當前與鍵盤相關的程序組中的所有成員(它們通常是在當前視窗建立的所有活動程序)。每個程序可以分別捕獲該訊號、忽略該訊號或採取預設的動作,即被訊號 kill 掉。整個作業系統中所有的程序都隸屬於一個單個以 init 為根的程序樹。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084708694-213711101.png)
#### Windows 程序體系
相反,Windows 中沒有程序層次的概念,Windows 中所有程序都是平等的,唯一類似於層次結構的是在建立程序的時候,父程序得到一個特別的令牌(稱為控制代碼),該控制代碼可以用來控制子程序。然而,這個令牌可能也會移交給別的作業系統,這樣就不存在層次結構了。而在 UNIX 中,程序不能剝奪其子程序的 `程序權`。(這樣看來,還是 Windows 比較`渣`)。
### 程序狀態
儘管每個程序是一個獨立的實體,有其自己的程式計數器和內部狀態,但是,程序之間仍然需要相互幫助。當一個程序開始執行時,它可能會經歷下面這幾種狀態
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084720153-43120997.png)
圖中會涉及三種狀態
1. `執行態`,執行態指的就是程序實際佔用 CPU 時間片執行時
2. `就緒態`,就緒態指的是可執行,但因為其他程序正在執行而處於就緒狀態
3. `阻塞態`,除非某種外部事件發生,否則程序不能執行
### 程序的實現
作業系統為了執行程序間的切換,會維護著一張表,這張表就是 `程序表(process table)`。每個程序佔用一個程序表項。該表項包含了程序狀態的重要資訊,包括程式計數器、堆疊指標、記憶體分配狀況、所開啟檔案的狀態、賬號和排程資訊,以及其他在程序由執行態轉換到就緒態或阻塞態時所必須儲存的資訊。
下面展示了一個典型系統中的關鍵欄位
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084727554-570767453.png)
第一列內容與`程序管理`有關,第二列內容與 `儲存管理`有關,第三列內容與`檔案管理`有關。
現在我們應該對程序表有個大致的瞭解了,就可以在對單個 CPU 上如何執行多個順序程序的錯覺做更多的解釋。與每一 I/O 類相關聯的是一個稱作 `中斷向量(interrupt vector)` 的位置(靠近記憶體底部的固定區域)。它包含中斷服務程式的入口地址。假設當一個磁碟中斷髮生時,使用者程序 3 正在執行,則中斷硬體將程式計數器、程式狀態字、有時還有一個或多個暫存器壓入堆疊,計算機隨即跳轉到中斷向量所指示的地址。這就是硬體所做的事情。然後軟體就隨即接管一切剩餘的工作。
當中斷結束後,作業系統會呼叫一個 C 程式來處理中斷剩下的工作。在完成剩下的工作後,會使某些程序就緒,接著呼叫排程程式,決定隨後執行哪個程序。然後將控制權轉移給一段組合語言程式碼,為當前的程序裝入暫存器值以及記憶體對映並啟動該程序執行,下面顯示了中斷處理和排程的過程。
1. 硬體壓入堆疊程式計數器等
2. 硬體從中斷向量裝入新的程式計數器
3. 組合語言過程儲存暫存器的值
4. 組合語言過程設定新的堆疊
5. C 中斷伺服器執行(典型的讀和快取寫入)
6. 排程器決定下面哪個程式先執行
7. C 過程返回至彙編程式碼
8. 組合語言過程開始執行新的當前程序
一個程序在執行過程中可能被中斷數千次,但關鍵每次中斷後,被中斷的程序都返回到與中斷髮生前完全相同的狀態。
## 執行緒
在傳統的作業系統中,每個程序都有一個地址空間和一個控制執行緒。事實上,這是大部分程序的定義。不過,在許多情況下,經常存在同一地址空間中執行多個控制執行緒的情形,這些執行緒就像是分離的程序。下面我們就著重探討一下什麼是執行緒
### 執行緒的使用
或許這個疑問也是你的疑問,為什麼要在程序的基礎上再建立一個執行緒的概念,準確的說,這其實是程序模型和執行緒模型的討論,回答這個問題,可能需要分三步來回答
* 多執行緒之間會共享同一塊地址空間和所有可用資料的能力,這是程序所不具備的
* 執行緒要比程序`更輕量級`,由於執行緒更輕,所以它比程序更容易建立,也更容易撤銷。在許多系統中,建立一個執行緒要比建立一個程序快 10 - 100 倍。
* 第三個原因可能是效能方面的探討,如果多個執行緒都是 CPU 密集型的,那麼並不能獲得性能上的增強,但是如果存在著大量的計算和大量的 I/O 處理,擁有多個執行緒能在這些活動中彼此重疊進行,從而會加快應用程式的執行速度
### 經典的執行緒模型
程序中擁有一個執行的執行緒,通常簡寫為 `執行緒(thread)`。執行緒會有程式計數器,用來記錄接著要執行哪一條指令;執行緒實際上 CPU 上排程執行的實體。
下圖我們可以看到三個傳統的程序,每個程序有自己的地址空間和單個控制執行緒。每個執行緒都在不同的地址空間中執行
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084735345-1593031839.png)
下圖中,我們可以看到有一個程序三個執行緒的情況。每個執行緒都在相同的地址空間中執行。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084805740-1125662819.png)
執行緒不像是程序那樣具備較強的獨立性。同一個程序中的所有執行緒都會有完全一樣的地址空間,這意味著它們也共享同樣的全域性變數。由於每個執行緒都可以訪問程序地址空間內每個記憶體地址,**因此一個執行緒可以讀取、寫入甚至擦除另一個執行緒的堆疊**。執行緒之間除了共享同一記憶體空間外,還具有如下不同的內容
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084818042-1795260872.png)
上圖左邊的是同一個程序中`每個執行緒共享`的內容,上圖右邊是`每個執行緒`中的內容。也就是說左邊的列表是程序的屬性,右邊的列表是執行緒的屬性。
**執行緒之間的狀態轉換和程序之間的狀態轉換是一樣的**。
每個執行緒都會有自己的堆疊,如下圖所示
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084825062-1541629051.png)
#### 執行緒系統呼叫
程序通常會從當前的某個單執行緒開始,然後這個執行緒通過呼叫一個庫函式(比如 `thread_create `)建立新的執行緒。執行緒建立的函式會要求指定新建立執行緒的名稱。建立的執行緒通常都返回一個執行緒識別符號,該識別符號就是新執行緒的名字。
當一個執行緒完成工作後,可以通過呼叫一個函式(比如 `thread_exit`)來退出。緊接著執行緒消失,狀態變為終止,不能再進行排程。在某些執行緒的執行過程中,可以通過呼叫函式例如 `thread_join` ,表示一個執行緒可以等待另一個執行緒退出。這個過程阻塞呼叫執行緒直到等待特定的執行緒退出。在這種情況下,執行緒的建立和終止非常類似於程序的建立和終止。
另一個常見的執行緒是呼叫 `thread_yield`,它允許執行緒自動放棄 CPU 從而讓另一個執行緒執行。這樣一個呼叫還是很重要的,因為不同於程序,執行緒是無法利用時鐘中斷強制讓執行緒讓出 CPU 的。
### POSIX 執行緒
`POSIX 執行緒 通常稱為 pthreads`是一種獨立於語言而存在的執行模型,以及並行執行模型。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084842271-1031838805.png)
它允許程式控制時間上重疊的多個不同的工作流程。每個工作流程都稱為一個執行緒,可以通過呼叫 POSIX Threads API 來實現對這些流程的建立和控制。可以把它理解為執行緒的標準。
>POSIX Threads 的實現在許多類似且符合POSIX的作業系統上可用,例如 **FreeBSD、NetBSD、OpenBSD、Linux、macOS、Android、Solaris**,它在現有 Windows API 之上實現了**pthread**。
>
>IEEE 是世界上最大的技術專業組織,致力於為人類的利益而發展技術。
| 執行緒呼叫 | 描述 |
| -------------------- | ------------------------------ |
| pthread_create | 建立一個新執行緒 |
| pthread_exit | 結束呼叫的執行緒 |
| pthread_join | 等待一個特定的執行緒退出 |
| pthread_yield | 釋放 CPU 來執行另外一個執行緒 |
| pthread_attr_init | 建立並初始化一個執行緒的屬性結構 |
| pthread_attr_destory | 刪除一個執行緒的屬性結構 |
所有的 Pthreads 都有特定的屬性,每一個都含有識別符號、一組暫存器(包括程式計數器)和一組儲存在結構中的屬性。這個屬性包括堆疊大小、排程引數以及其他執行緒需要的專案。
### 執行緒實現
主要有三種實現方式
* 在使用者空間中實現執行緒;
* 在核心空間中實現執行緒;
* 在使用者和核心空間中混合實現執行緒。
下面我們分開討論一下
#### 在使用者空間中實現執行緒
第一種方法是把整個執行緒包放在使用者空間中,核心對執行緒一無所知,它不知道執行緒的存在。所有的這類實現都有同樣的通用結構
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084905724-2100485366.png)
執行緒在執行時系統之上執行,執行時系統是管理執行緒過程的集合,包括前面提到的四個過程: pthread_create, pthread_exit, pthread_join 和 pthread_yield。
### 在核心中實現執行緒
當某個執行緒希望建立一個新執行緒或撤銷一個已有執行緒時,它會進行一個系統呼叫,這個系統呼叫通過對執行緒表的更新來完成執行緒建立或銷燬工作。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084913153-1789413387.png)
核心中的執行緒表持有每個執行緒的暫存器、狀態和其他資訊。這些資訊和使用者空間中的執行緒資訊相同,但是位置卻被放在了核心中而不是使用者空間中。另外,核心還維護了一張程序表用來跟蹤系統狀態。
所有能夠阻塞的呼叫都會通過系統呼叫的方式來實現,當一個執行緒阻塞時,核心可以進行選擇,是執行在同一個程序中的另一個執行緒(如果有就緒執行緒的話)還是執行一個另一個程序中的執行緒。但是在使用者實現中,執行時系統始終執行自己的執行緒,直到核心剝奪它的 CPU 時間片(或者沒有可執行的執行緒存在了)為止。
### 混合實現
結合使用者空間和核心空間的優點,設計人員採用了一種`核心級執行緒`的方式,然後將使用者級執行緒與某些或者全部核心執行緒多路複用起來
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084923417-1464770154.png)
在這種模型中,程式設計人員可以自由控制使用者執行緒和核心執行緒的數量,具有很大的靈活度。採用這種方法,核心只識別核心級執行緒,並對其進行排程。其中一些核心級執行緒會被多個使用者級執行緒多路複用。
## 程序間通訊
程序是需要頻繁的和其他程序進行交流的。下面我們會一起討論有關 `程序間通訊(Inter Process Communication, IPC)` 的問題。大致來說,程序間的通訊機制可以分為 6 種
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084933900-367260219.png)
下面我們分別對其進行概述
### 訊號 signal
訊號是 UNIX 系統最先開始使用的程序間通訊機制,因為 Linux 是繼承於 UNIX 的,所以 Linux 也支援訊號機制,通過向一個或多個程序傳送`非同步事件訊號`來實現,訊號可以從鍵盤或者訪問不存在的位置等地方產生;訊號通過 shell 將任務傳送給子程序。
你可以在 Linux 系統上輸入 `kill -l` 來列出系統使用的訊號,下面是我提供的一些訊號
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084940641-791929855.png)
程序可以選擇忽略傳送過來的訊號,但是有兩個是不能忽略的:`SIGSTOP` 和 `SIGKILL` 訊號。SIGSTOP 訊號會通知當前正在執行的程序執行關閉操作,SIGKILL 訊號會通知當前程序應該被殺死。除此之外,程序可以選擇它想要處理的訊號,程序也可以選擇阻止訊號,如果不阻止,可以選擇自行處理,也可以選擇進行核心處理。如果選擇交給核心進行處理,那麼就執行預設處理。
作業系統會中斷目標程式的程序來向其傳送訊號、在任何非原子指令中,執行都可以中斷,如果程序已經註冊了新號處理程式,那麼就執行程序,如果沒有註冊,將採用預設處理的方式。
### 管道 pipe
Linux 系統中的程序可以通過建立管道 pipe 進行通訊
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714084948820-1310164790.png)
在兩個程序之間,可以建立一個通道,一個程序向這個通道里寫入位元組流,另一個程序從這個管道中讀取位元組流。管道是同步的,當程序嘗試從空管道讀取資料時,該程序會被阻塞,直到有可用資料為止。shell 中的`管線 pipelines` 就是用管道實現的,當 shell 發現輸出
```shell
sort 需要注意的是,在所有的程序都可以執行的情況下,最短作業優先的演算法才是最優的。
#### 最短剩餘時間優先
最短作業優先的搶佔式版本被稱作為 `最短剩餘時間優先(Shortest Remaining Time Next)` 演算法。使用這個演算法,排程程式總是選擇剩餘執行時間最短的那個程序執行。
### 互動式系統中的排程
互動式系統中在個人計算機、伺服器和其他系統中都是很常用的,所以有必要來探討一下互動式排程
#### 輪詢排程
一種最古老、最簡單、最公平並且最廣泛使用的演算法就是 `輪詢演算法(round-robin)`。每個程序都會被分配一個時間段,稱為`時間片(quantum)`,在這個時間片內允許程序執行。如果時間片結束時程序還在執行的話,則搶佔一個 CPU 並將其分配給另一個程序。如果程序在時間片結束前阻塞或結束,則 CPU 立即進行切換。輪詢演算法比較容易實現。排程程式所做的就是維護一個可執行程序的列表,就像下圖中的 a,當一個程序用完時間片後就被移到佇列的末尾,就像下圖的 b。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085035008-755923229.png)
#### 優先順序排程
輪詢排程假設了所有的程序是同等重要的。但事實情況可能不是這樣。例如,在一所大學中的等級制度,首先是院長,然後是教授、祕書、後勤人員,最後是學生。這種將外部情況考慮在內就實現了`優先順序排程(priority scheduling)`
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085044015-2002927147.png)
它的基本思想很明確,每個程序都被賦予一個優先順序,優先順序高的程序優先執行。
#### 多級佇列
最早使用優先順序排程的系統是 `CTSS(Compatible TimeSharing System)`。CTSS 在每次切換前都需要將當前程序換出到磁碟,並從磁碟上讀入一個新程序。為 CPU 密集型程序設定較長的時間片比頻繁地分給他們很短的時間要更有效(減少交換次數)。另一方面,如前所述,長時間片的程序又會影響到響應時間,解決辦法是設定優先順序類。屬於最高優先順序的程序執行一個時間片,次高優先順序程序執行 2 個時間片,再下面一級執行 4 個時間片,以此類推。當一個程序用完分配的時間片後,它被移到下一類。
#### 最短程序優先
最短程序優先是根據程序過去的行為進行推測,並執行估計執行時間最短的那一個。假設每個終端上每條命令的預估執行時間為 `T0`,現在假設測量到其下一次執行時間為 `T1`,可以用兩個值的加權來改進估計時間,即`aT0+ (1- 1)T1`。通過選擇 a 的值,可以決定是儘快忘掉老的執行時間,還是在一段長時間內始終記住它們。當 a = 1/2 時,可以得到下面這個序列
![image-20200220120452410](/Users/mr.l/Library/Application Support/typora-user-images/image-20200220120452410.png)
可以看到,在三輪過後,T0 在新的估計值中所佔比重下降至 1/8。
#### 保證排程
一種完全不同的排程方法是對使用者做出明確的效能保證。一種實際而且容易實現的保證是:若使用者工作時有 n 個使用者登入,則每個使用者將獲得 CPU 處理能力的 1/n。類似地,在一個有 n 個程序執行的單使用者系統中,若所有的程序都等價,則每個程序將獲得 1/n 的 CPU 時間。
#### 彩票排程
對使用者進行承諾並在隨後兌現承諾是一件好事,不過很難實現。但是存在著一種簡單的方式,有一種既可以給出預測結果而又有一種比較簡單的實現方式的演算法,就是 `彩票排程(lottery scheduling)`演算法。
其基本思想是為程序提供各種系統資源(例如 CPU 時間)的彩票。當做出一個排程決策的時候,就隨機抽出一張彩票,擁有彩票的程序將獲得該資源。在應用到 CPU 排程時,系統可以每秒持有 50 次抽獎,每個中獎者將獲得比如 20 毫秒的 CPU 時間作為獎勵。
#### 公平分享排程
到目前為止,我們假設被排程的都是各個程序自身,而不用考慮該程序的擁有者是誰。結果是,如果使用者 1 啟動了 9 個程序,而使用者 2 啟動了一個程序,使用輪轉或相同優先順序排程演算法,那麼使用者 1 將得到 90 % 的 CPU 時間,而使用者 2 將之得到 10 % 的 CPU 時間。
為了阻止這種情況的出現,一些系統在排程前會把程序的擁有者考慮在內。在這種模型下,每個使用者都會分配一些CPU 時間,而排程程式會選擇程序並強制執行。因此如果兩個使用者每個都會有 50% 的 CPU 時間片保證,那麼無論一個使用者有多少個程序,都將獲得相同的 CPU 份額。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085051388-805693476.png)
### 實時系統中的排程
`實時系統(real-time)` 是一個時間扮演了重要作用的系統。實時系統可以分為兩類,`硬實時(hard real time)` 和 `軟實時(soft real time)` 系統,前者意味著必須要滿足絕對的截止時間;後者的含義是雖然不希望偶爾錯失截止時間,但是可以容忍。
實時系統中的事件可以按照響應方式進一步分類為`週期性(以規則的時間間隔發生)`事件或 `非週期性(發生時間不可預知)`事件。一個系統可能要響應多個週期性事件流,根據每個事件處理所需的時間,可能甚至無法處理所有事件。例如,如果有 m 個週期事件,事件 i 以週期 Pi 發生,並需要 Ci 秒 CPU 時間處理一個事件,那麼可以處理負載的條件是
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085118891-1070593363.png)
只有滿足這個條件的實時系統稱為`可排程的`,這意味著它實際上能夠被實現。一個不滿足此檢驗標準的程序不能被排程,因為這些程序共同需要的 CPU 時間總和大於 CPU 能提供的時間。
下面我們來了解一下記憶體管理,你需要知道的知識點如下
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085127470-1663371494.png)
## 地址空間
如果要使多個應用程式同時執行在記憶體中,必須要解決兩個問題:`保護`和 `重定位`。第一種解決方式是用`保護金鑰標記記憶體塊`,並將執行過程的金鑰與提取的每個儲存字的金鑰進行比較。這種方式只能解決第一種問題(破壞作業系統),但是不能解決多程序在記憶體中同時執行的問題。
還有一種更好的方式是創造一個儲存器抽象:`地址空間(the address space)`。就像程序的概念建立了一種抽象的 CPU 來執行程式,地址空間也建立了一種抽象記憶體供程式使用。
#### 基址暫存器和變址暫存器
最簡單的辦法是使用`動態重定位(dynamic relocation)`技術,它就是通過一種簡單的方式將每個程序的地址空間對映到實體記憶體的不同區域。還有一種方式是使用基址暫存器和變址暫存器。
* 基址暫存器:儲存資料記憶體的起始位置
* 變址暫存器:儲存應用程式的長度。
每當程序引用記憶體以獲取指令或讀取、寫入資料時,CPU 都會自動將`基址值`新增到程序生成的地址中,然後再將其傳送到記憶體總線上。同時,它檢查程式提供的地址是否大於或等於`變址暫存器` 中的值。如果程式提供的地址要超過變址暫存器的範圍,那麼會產生錯誤並中止訪問。
### 交換技術
在程式執行過程中,經常會出現記憶體不足的問題。
針對上面記憶體不足的問題,提出了兩種處理方式:最簡單的一種方式就是`交換(swapping)`技術,即把一個程序完整的調入記憶體,然後再記憶體中執行一段時間,再把它放回磁碟。空閒程序會儲存在磁碟中,所以這些程序在沒有執行時不會佔用太多記憶體。另外一種策略叫做`虛擬記憶體(virtual memory)`,虛擬記憶體技術能夠允許應用程式部分的執行在記憶體中。下面我們首先先探討一下交換
#### 交換過程
下面是一個交換過程
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085135718-1904010396.png)
剛開始的時候,只有程序 A 在記憶體中,然後從建立程序 B 和程序 C 或者從磁碟中把它們換入記憶體,然後在圖 d 中,A 被換出記憶體到磁碟中,最後 A 重新進來。因為圖 g 中的程序 A 現在到了不同的位置,所以在裝載過程中需要被重新定位,或者在交換程式時通過軟體來執行;或者在程式執行期間通過硬體來重定位。基址暫存器和變址暫存器就適用於這種情況。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085142723-2056104555.png)
交換在記憶體建立了多個 `空閒區(hole)`,記憶體會把所有的空閒區儘可能向下移動合併成為一個大的空閒區。這項技術稱為`記憶體緊縮(memory compaction)`。但是這項技術通常不會使用,因為這項技術會消耗很多 CPU 時間。
### 空閒記憶體管理
在進行記憶體動態分配時,作業系統必須對其進行管理。大致上說,有兩種監控記憶體使用的方式
* `點陣圖(bitmap)`
* `空閒列表(free lists)`
#### 使用點陣圖的儲存管理
使用點陣圖方法時,記憶體可能被劃分為小到幾個字或大到幾千位元組的分配單元。每個分配單元對應於點陣圖中的一位,0 表示空閒, 1 表示佔用(或者相反)。一塊記憶體區域和其對應的點陣圖如下
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085150268-572806460.png)
`點陣圖`提供了一種簡單的方法在固定大小的記憶體中跟蹤記憶體的使用情況,因為**點陣圖的大小取決於記憶體和分配單元的大小**。這種方法有一個問題是,當決定為把具有 k 個分配單元的程序放入記憶體時,`內容管理器(memory manager)` 必須搜尋點陣圖,在點陣圖中找出能夠執行 k 個連續 0 位的串。在點陣圖中找出制定長度的連續 0 串是一個很耗時的操作,這是點陣圖的缺點。(可以簡單理解為在雜亂無章的陣列中,找出具有一大長串空閒的陣列單元)
#### 使用連結串列進行管理
另一種記錄記憶體使用情況的方法是,維護一個記錄已分配記憶體段和空閒記憶體段的連結串列,段會包含程序或者是兩個程序的空閒區域。可用上面的圖 c **來表示記憶體的使用情況**。連結串列中的每一項都可以代表一個 `空閒區(H)` 或者是`程序(P)`的起始標誌,長度和下一個連結串列項的位置。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085157166-241932461.png)
當按照地址順序在連結串列中存放程序和空閒區時,有幾種演算法可以為建立的程序(或者從磁碟中換入的程序)分配記憶體。我們先假設記憶體管理器知道應該分配多少記憶體,最簡單的演算法是使用 `首次適配(first fit)`。記憶體管理器會沿著段列表進行掃描,直到找個一個足夠大的空閒區為止。 除非空閒區大小和要分配的空間大小一樣,否則將空閒區分為兩部分,一部分供程序使用;一部分生成新的空閒區。首次適配演算法是一種速度很快的演算法,因為它會盡可能的搜尋連結串列。
首次適配的一個小的變體是 `下次適配(next fit)`。它和首次匹配的工作方式相同,只有一個不同之處那就是下次適配在每次找到合適的空閒區時就會記錄當時的位置,以便下次尋找空閒區時從上次結束的地方開始搜尋,而不是像首次匹配演算法那樣每次都會從頭開始搜尋。
另外一個著名的並且廣泛使用的演算法是 `最佳適配(best fit)`。最佳適配會從頭到尾尋找整個連結串列,找出能夠容納程序的最小空閒區。
## 虛擬記憶體
儘管基址暫存器和變址暫存器用來建立地址空間的抽象,但是這有一個其他的問題需要解決:`管理軟體的不斷增大(managing bloatware)`。虛擬記憶體的基本思想是,每個程式都有自己的地址空間,這個地址空間被劃分為多個稱為`頁面(page)`的塊。每一頁都是連續的地址範圍。這些頁被對映到實體記憶體,但並不是所有的頁都必須在記憶體中才能執行程式。當程式引用到一部分在實體記憶體中的地址空間時,硬體會立刻執行必要的對映。當程式引用到一部分不在實體記憶體中的地址空間時,由作業系統負責將缺失的部分裝入實體記憶體並重新執行失敗的指令。
### 分頁
大部分使用虛擬記憶體的系統中都會使用一種 `分頁(paging)` 技術。在任何一臺計算機上,程式會引用使用一組記憶體地址。當程式執行
```assembly
MOV REG,1000
```
這條指令時,它會把記憶體地址為 1000 的記憶體單元的內容複製到 REG 中(或者相反,這取決於計算機)。地址可以通過索引、基址暫存器、段暫存器或其他方式產生。
這些程式生成的地址被稱為 `虛擬地址(virtual addresses)` 並形成`虛擬地址空間(virtual address space)`,在沒有虛擬記憶體的計算機上,系統直接將虛擬地址送到記憶體中線上,讀寫操作都使用同樣地址的實體記憶體。**在使用虛擬記憶體時,虛擬地址不會直接傳送到記憶體總線上**。相反,會使用 `MMU(Memory Management Unit)` 記憶體管理單元把**虛擬地址對映為實體記憶體地址**,像下圖這樣
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085205876-305983549.png)
下面這幅圖展示了這種對映是如何工作的
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085219354-1718482722.png)
頁表給出虛擬地址與實體記憶體地址之間的對映關係。每一頁起始於 4096 的倍數位置,結束於 4095 的位置,所以 4K 到 8K 實際為 4096 - 8191 ,8K - 12K 就是 8192 - 12287
在這個例子中,我們可能有一個 16 位地址的計算機,地址從 0 - 64 K - 1,這些是`虛擬地址`。然而只有 32 KB 的實體地址。所以雖然可以編寫 64 KB 的程式,但是程式無法全部調入記憶體執行,在磁碟上必須有一個最多 64 KB 的程式核心映像的完整副本,以保證程式片段在需要時被調入記憶體。
### 頁表
虛擬頁號可作為頁表的索引用來找到虛擬頁中的內容。由頁表項可以找到頁框號(如果有的話)。然後把頁框號拼接到偏移量的高位端,以替換掉虛擬頁號,形成實體地址。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085228595-845668595.png)
因此,頁表的目的是把虛擬頁對映到頁框中。從數學上說,頁表是一個函式,它的引數是虛擬頁號,結果是物理頁框號。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085233808-1651600095.png)
通過這個函式可以把虛擬地址中的虛擬頁轉換為頁框,從而形成實體地址。
#### 頁表項的結構
下面我們探討一下頁表項的具體結構,上面你知道了頁表項的大致構成,是由頁框號和在/不在位構成的,現在我們來具體探討一下頁表項的構成
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085241766-805387936.png)
頁表項的結構是與機器相關的,但是不同機器上的頁表項大致相同。上面是一個頁表項的構成,不同計算機的頁表項可能不同,但是一般來說都是 32 位的。頁表項中最重要的欄位就是`頁框號(Page frame number)`。畢竟,頁表到頁框最重要的一步操作就是要把此值對映過去。下一個比較重要的就是`在/不在`位,如果此位上的值是 1,那麼頁表項是有效的並且能夠被`使用`。如果此值是 0 的話,則表示該頁表項對應的虛擬頁面`不在`記憶體中,訪問該頁面會引起一個`缺頁異常(page fault)`。
`保護位(Protection)` 告訴我們哪一種訪問是允許的,啥意思呢?最簡單的表示形式是這個域只有一位,**0 表示可讀可寫,1 表示的是隻讀**。
`修改位(Modified)` 和 `訪問位(Referenced)` 會跟蹤頁面的使用情況。當一個頁面被寫入時,硬體會自動的設定修改位。修改位在頁面重新分配頁框時很有用。如果一個頁面已經被修改過(即它是 `髒` 的),則必須把它寫回磁碟。如果一個頁面沒有被修改過(即它是 `乾淨`的),那麼重新分配時這個頁框會被直接丟棄,因為磁碟上的副本仍然是有效的。這個位有時也叫做 `髒位(dirty bit)`,因為它反映了頁面的狀態。
`訪問位(Referenced)` 在頁面被訪問時被設定,不管是讀還是寫。這個值能夠幫助作業系統在發生缺頁中斷時選擇要淘汰的頁。不再使用的頁要比正在使用的頁更適合被淘汰。這個位在後面要討論的`頁面置換`演算法中作用很大。
最後一位用於禁止該頁面被快取記憶體,這個功能對於對映到裝置暫存器還是記憶體中起到了關鍵作用。通過這一位可以禁用快取記憶體。具有獨立的 I/O 空間而不是用記憶體對映 I/O 的機器來說,並不需要這一位。
## 頁面置換演算法
下面我們就來探討一下有哪些頁面置換演算法。
### 最優頁面置換演算法
最優的頁面置換演算法的工作流程如下:在缺頁中斷髮生時,這些頁面之一將在下一條指令(包含該指令的頁面)上被引用。其他頁面則可能要到 10、100 或者 1000 條指令後才會被訪問。每個頁面都可以用在該頁首次被訪問前所要執行的指令數作為標記。
最優化的頁面演算法表明應該標記最大的頁面。如果一個頁面在 800 萬條指令內不會被使用,另外一個頁面在 600 萬條指令內不會被使用,則置換前一個頁面,從而把需要調入這個頁面而發生的缺頁中斷推遲。計算機也像人類一樣,會把不願意做的事情儘可能的往後拖。
這個演算法最大的問題時無法實現。當缺頁中斷髮生時,作業系統無法知道各個頁面的下一次將在什麼時候被訪問。這種演算法在實際過程中根本不會使用。
### 最近未使用頁面置換演算法
為了能夠讓作業系統收集頁面使用資訊,大部分使用虛擬地址的計算機都有兩個狀態位,R 和 M,來和每個頁面進行關聯。**每當引用頁面(讀入或寫入)時都設定 R,寫入(即修改)頁面時設定 M**,這些位包含在每個頁表項中,就像下面所示
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085304344-970286659.png)
因為每次訪問時都會更新這些位,因此由`硬體`來設定它們非常重要。一旦某個位被設定為 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)` 演算法,這種型別的資料結構也適用在頁面置換演算法中。由作業系統維護一個所有在當前記憶體中的頁面的連結串列,最早進入的放在表頭,最新進入的頁面放在表尾。在發生缺頁異常時,會把頭部的頁移除並且把新的頁新增到表尾。
### 第二次機會頁面置換演算法
我們上面學到的 FIFO 連結串列頁面有個`缺陷`,那就是出鏈和入鏈並不會進行 check `檢查`,這樣就會容易把經常使用的頁面置換出去,為了避免這一問題,我們對該演算法做一個簡單的修改:我們檢查最老頁面的 `R 位`,如果是 0 ,那麼這個頁面就是最老的而且沒有被使用,那麼這個頁面就會被立刻換出。如果 R 位是 1,那麼就清除此位,此頁面會被放在連結串列的尾部,修改它的裝入時間就像剛放進來的一樣。然後繼續搜尋。
這種演算法叫做 `第二次機會(second chance)`演算法,就像下面這樣,我們看到頁面 A 到 H 保留在連結串列中,並按到達記憶體的時間排序。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085312983-279241456.png)
a)按照先進先出的方法排列的頁面;b)在時刻 20 處發生缺頁異常中斷並且 A 的 R 位已經設定時的頁面連結串列。
假設缺頁異常發生在時刻 20 處,這時最老的頁面是 A ,它是在 0 時刻到達的。如果 A 的 R 位是 0,那麼它將被淘汰出記憶體,或者把它寫回磁碟(如果它已經被修改過),或者只是簡單的放棄(如果它是未被修改過)。另一方面,如果它的 R 位已經設定了,則將 A 放到連結串列的尾部並且重新設定`裝入時間`為當前時刻(20 處),然後清除 R 位。然後從 B 頁面開始繼續搜尋合適的頁面。
尋找第二次機會的是在最近的時鐘間隔中未被訪問過的頁面。如果所有的頁面都被訪問過,該演算法就會被簡化為單純的 `FIFO 演算法`。具體來說,假設圖 a 中所有頁面都設定了 R 位。作業系統將頁面依次移到連結串列末尾,每次都在新增到末尾時清除 R 位。最後,演算法又會回到頁面 A,此時的 R 位已經被清除,那麼頁面 A 就會被執行出鏈處理,因此演算法能夠正常結束。
### 時鐘頁面置換演算法
一種比較好的方式是把所有的頁面都儲存在一個類似鐘面的環形連結串列中,一個錶針指向最老的頁面。如下圖所示
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085322344-254050457.png)
當缺頁錯誤出現時,演算法首先檢查錶針指向的頁面,如果它的 R 位是 0 就淘汰該頁面,並把新的頁面插入到這個位置,然後把錶針向前移動一位;如果 R 位是 1 就清除 R 位並把錶針前移一個位置。重複這個過程直到找到了一個 R 位為 0 的頁面位置。瞭解這個演算法的工作方式,就明白為什麼它被稱為 `時鐘(clokc)`演算法了。
### 最近最少使用頁面置換演算法
在前面幾條指令中頻繁使用的頁面和可能在後面的幾條指令中被使用。反過來說,已經很久沒有使用的頁面有可能在未來一段時間內仍不會被使用。這個思想揭示了一個可以實現的演算法:在缺頁中斷時,置換未使用時間最長的頁面。這個策略稱為 `LRU(Least Recently Used)` ,最近最少使用頁面置換演算法。
雖然 LRU 在理論上是可以實現的,但是從長遠看來代價比較高。為了完全實現 LRU,會在記憶體中維護一個所有頁面的連結串列,最頻繁使用的頁位於表頭,最近最少使用的頁位於表尾。困難的是在每次記憶體引用時更新整個連結串列。在連結串列中找到一個頁面,刪除它,然後把它移動到表頭是一個非常耗時的操作,即使使用`硬體`來實現也是一樣的費時。
### 用軟體模擬 LRU
儘管上面的 LRU 演算法在原則上是可以實現的,**但是很少有機器能夠擁有那些特殊的硬體**。上面是硬體的實現方式,那麼現在考慮要用`軟體`來實現 LRU 。一種可以實現的方案是 `NFU(Not Frequently Used,最不常用)`演算法。它需要一個軟體計數器來和每個頁面關聯,初始化的時候是 0 。在每個時鐘中斷時,作業系統會瀏覽記憶體中的所有頁,會將每個頁面的 R 位(0 或 1)加到它的計數器上。這個計數器大體上跟蹤了各個頁面訪問的頻繁程度。當缺頁異常出現時,則置換計數器值最小的頁面。
只需要對 NFU 做一個簡單的修改就可以讓它模擬 LRU,這個修改有兩個步驟
* 首先,在 R 位被新增進來之前先把計數器右移一位;
* 第二步,R 位被新增到最左邊的位而不是最右邊的位。
修改以後的演算法稱為 `老化(aging)` 演算法,下圖解釋了老化演算法是如何工作的。
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085330342-706528504.png)
我們假設在第一個時鐘週期內頁面 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。剩下的四列顯示了接下來的四個時鐘週期內的六個計數器變化。
> CPU正在以某個頻率前進,該頻率的週期稱為`時鐘滴答`或`時鐘週期`。一個 100Mhz 的處理器每秒將接收100,000,000個時鐘滴答。
當缺頁異常出現時,將`置換(就是移除)`計數器值最小的頁面。如果一個頁面在前面 4 個時鐘週期內都沒有被訪問過,那麼它的計數器應該會有四個連續的 0 ,因此它的值肯定要比前面 3 個時鐘週期內都沒有被訪問過的頁面的計數器小。
這個演算法與 LRU 演算法有兩個重要的區別:看一下上圖中的 `e`,第三列和第五列
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085337205-1208389842.png)
### 工作集時鐘頁面置換演算法
當缺頁異常發生後,需要掃描整個頁表才能確定被淘汰的頁面,因此基本工作集演算法還是比較浪費時間的。一個對基本工作集演算法的提升是基於時鐘演算法但是卻使用工作集的資訊,這種演算法稱為`WSClock(工作集時鐘)`。由於它的實現簡單並且具有高效能,因此在實踐中被廣泛應用。
與時鐘演算法一樣,所需的資料結構是一個以頁框為元素的迴圈列表,就像下面這樣
![](https://img2020.cnblogs.com/blog/1515111/202007/1515111-20200714085347885-1818537574.png)
工作集時鐘頁面置換演算法的操作:a) 和 b) 給出 R = 1 時所發生的情形;c) 和 d) 給出 R = 0 的例子
最初的時候,該表是空的。當裝入第一個頁面後,把它載入到該表中。隨著更多的頁面的加入,它們形成一個環形結構。每個表項包含來自基本工作集演算法的上次使用時間,以及 R 位(已標明)和 M 位(未標明)。
與時鐘演算法一樣,在每個缺頁異常時,首先檢查指標指向的頁面。如果 R 位被是設定為 1,該頁面在當前時鐘週期內就被使用過,那麼該頁面就不適合被淘汰。然後把該頁面的 R 位置為 0,指標指向下一個頁面,並重復該演算法。該事件序列化後的狀態參見圖 b。
現在考慮指標指向的頁面 R = 0 時會發生什麼,參見圖 c,如果頁面的使用期限大於 t