從 8 個緯度談操作系統內存管理

目錄

01

什麼是物理內存?

我們常說的物理內存大小就是指內存條的大小,一般買電腦時都會看下內存條是多大容量的,話說如果內存條大小是 100G,那這 100G 就都能夠被使用嗎?不一定的,更多的還是要看 CPU 地址總線的位數,如果地址總線只有 20 位,那麼它的尋址空間就是 1MB,即使可以安裝 100G 的內存條也沒有意義,也只能視物理內存大小爲 1MB。

02

使用物理內存有什麼缺點?

這種方式下每個程序都可以直接訪問物理內存,有兩種情況:

  1. 系統中只有一個進程在運行:如果用戶程序可以操作物理地址空間的任意地址,它們就很容易在不經意間破壞了操作系統,使系統出現各種奇奇怪怪的問題;

  2. 系統有多個進程同時在運行:如圖,理想情況下可以使進程 A 和進程 B 各佔物理內存的一邊,兩者互不干擾,但這只是理想情況下,誰能確保程序沒有 bug 呢,進程 B 在後臺正常運行着,程序員在調試進程 A 時有可能就會誤操作到進程 B 正在使用的物理內存,導致進程 B 運行出現異常,兩個程序操作了同一地址空間,第一個程序在某一地址空間寫入某個值,第二個程序在同一地址又寫入了不同值,這就會導致程序運行出現問題,所以直接使用物理內存會使所有進程的安全性得不到保證

如何解決上述問題?

可以考慮爲存儲器創造新的抽象概念:地址空間,地址空間爲程序創造了一種抽象的內存,是進程可用於尋址內存的一套地址集合,同時每個進程都有一套自己的地址空間,一個進程的地址空間獨立於其它進程的地址空間。

如何爲程序創造獨立的地址空間?

最簡單的辦法就是把每個進程的地址空間分別映射到物理內存的不同部分。這樣就可以保證不同進程使用的是獨立的地址空間。

方法一**:**如果是因爲程序太大,大到超過了內存的容量,可以採用手動覆蓋技術,只把需要的指令和數據保存在內存中。

方法二**:**如果是因爲程序太多,導致超過了內存的容量,可以採用自動交換技術,把暫時不需要執行的程序移動到外存中。

覆蓋技術

把程序按照自身邏輯結構,劃分成多個功能相互獨立的程序模塊,那些不會同時執行的模塊可以共享到同一塊內存區域,按時間順序來運行:

將常用功能需要的代碼和數據常駐在內存中;

將不常用的功能劃分成功能相互獨立的程序模塊,平時放到外存中,在需要的時候將對應的模塊加載到內存中;

那些沒有調用關係的模塊平時不需要裝入到內存,它們可以共用一塊內存區,需要時加載到內存,不需要時換出到外存中;

如圖:

交換技術

多個程序同時運行,可以將暫時不能運行的程序送到外存,獲得更多的空閒內存,操作系統將一個進程的整個地址空間內容換出到外存中,再將外存中某個進程的整個地址空間信息換入到內存中,換入換出內容的大小是整個程序的地址空間。

交換技術在實現上有很多困難:

需要確定什麼時候發生交換:簡單的辦法是可以在內存空間不夠用時換出一些程序;

交換區必須足夠大:多個程序運行時,交換區(外存)必須足夠大,大到可以存放所有程序所需要的地址空間信息;

程序如何換入:一個程序被換出後又重新換入,換入的內存位置可能不會和上一次程序所在的內存位置相同,這就需要動態地址映射機制。

03

什麼是虛擬內存?

虛擬內存,那就是虛擬出來的內存,它的基本思想就是確保每個程序擁有自己的地址空間,地址空間被分成多個塊,每一塊都有連續的地址空間,同時物理空間也分成多個塊,塊大小和虛擬地址空間的塊大小一致,操作系統會自動將虛擬地址空間映射到物理地址空間,程序所關注的只是虛擬內存,請求的也是虛擬內存,其實真正使用的是物理內存。

虛擬內存技術有交換技術的功能,能夠實現進程在內存和外存之間的交換,因而獲得更多的空閒內存空間。它比交換技術做的更好,它只對進程的部分內容在內存和外存之間進行交換。

虛擬內存技術的具體實現**:**

虛擬內存技術一般是在頁式管理(下面介紹)的基礎上實現

在裝入程序時,不必將其全部裝入到內存,而只需將當前需要執行的部分頁面裝入到內存,就可讓程序開始執行;

在程序執行過程中,如果需執行的指令或訪問的數據尚未在內存(稱爲缺頁)。則由處理器通知操作系統將相應的頁面調入到內存,然後繼續執行程序;

另一方面,操作系統將內存中暫時不使用的頁面調出保存在外存上,從而騰出更多空閒空間存放將要裝入的程序以及將要調入的頁面。

虛擬內存技術的特點**:**

大的用戶空間:通過把物理內存與外存相結合,提供給用戶的虛擬內存空間通常大於實際的物理內存,即實現了兩者的分離。如 32 位的虛擬地址理論上可以訪問 4GB,而可能計算機上僅有 256M 的物理內存,但硬盤容量大於 4GB;

部分交換:與交換技術相比較,虛擬存儲的調入和調出是對部分虛擬地址空間進行的;

連續性:程序可以使用一系列相鄰連續的虛擬地址來映射物理內存中不連續的大內存緩衝區;

安全性:不同進程使用的虛擬地址彼此隔離。一個進程中的代碼無法更改正在由另一進程或操作系統使用的物理內存。

04

虛擬內存如何映射到物理內存?

如圖,CPU 裏有一個內存管理單元(Memory Management Unit),簡稱 MMU,虛擬內存不是直接送到內存總線,而是先給到 MMU,由 MMU 來把虛擬地址映射到物理地址,程序只需要管理虛擬內存就好,映射的邏輯自然有其它模塊自動處理。

操作系統如何表示的內存被佔用還是空閒?

05

分頁內存管理

將虛擬地址空間分成若干個塊,每個塊都有固定的大小,物理地址空間也被劃分成若干個塊,每個塊也都有固定的大小,物理地址空間的塊和虛擬地址空間的塊大小相等,虛擬地址空間這些塊就被稱爲頁面,物理地址空間這些塊被稱爲

關於分頁這裏有個問題,頁面的大小是多少合適呢?頁面太大容易產生空間浪費,程序假如只使用了 1 個字節卻被分配了 10M 的頁面,這豈不是極大的浪費,頁面太小會導致頁表(下面介紹)佔用空間過大,所以頁面需要折中選擇合適的大小,目前大多數系統都使用 4KB 作爲頁的大小。

上面關於虛擬內存如何映射到物理內存程序喵只介紹了 MMU,但是 MMU 是如何工作的還沒有介紹,MMU 通過頁表這個工具將虛擬地址轉換爲物理地址。32 位的虛擬地址分成兩部分(虛擬頁號和偏移量),MMU 通過頁表找到了虛擬頁號對應的物理頁號,物理頁號 + 偏移量就是實際的物理地址。

06 什麼是缺頁中斷?

缺頁中斷就是要訪問的頁不在主存中,需要操作系統將頁調入主存後再進行訪問,此時會暫時停止指令的執行,產生一個頁不存在的異常,對應的異常處理程序就會從選擇一頁調入到內存,調入內存後之前的異常指令就可以繼續執行。

缺頁中斷的處理過程如下:

  1. 如果內存中有空閒的物理頁面,則分配一物理頁幀 r,然後轉第 4 步,否則轉第 2 步;

  2. 選擇某種頁面置換算法,選擇一個將被替換的物理頁幀 r,它所對應的邏輯頁爲 q,如果該頁在內存期間被修改過,則需把它寫回到外存;

  3. 將 q 所對應的頁表項進行修改,把駐留位置 0;

  4. 將需要訪問的頁 p 裝入到物理頁面 r 中;

  5. 修改 p 所對應的頁表項的內容,把駐留位置 1,把物理頁幀號置爲 x;

  6. 重新運行被中斷的指令。

07

頁面置換算法都有哪些?

當缺頁中斷髮生時,需要調入新的頁面到內存中,而內存已滿時,選擇內存中哪個物理頁面被置換是個學問,由此引入了多種頁面置換算法,致力於儘可能減少頁面的換入換出次數(缺頁中斷次數)。儘量把未來不再使用的或短期內較少使用的頁面換出,通常在程序局部性原理指導下依據過去的統計數據來進行預測。

最優頁面置換算法:當一個缺頁中斷髮生時,對於保存在內存當中的每一個邏輯頁面,計算在它的下一次訪問之前,還需等待多長時間,從中選擇等待時間最長的那個,作爲被置換的頁面。注意這只是一種理想情況,在實際系統中是無法實現的,因爲操作系統不可能預測未來,不知道每一個頁面要等待多長時間以後纔會再次被訪問。該算法可用作其它算法的性能評價的依據(在一個模擬器上運行某個程序,並記錄每一次的頁面訪問情況,在第二遍運行時即可使用最優算法)。

先進先出算法:最先進入的頁面最先被淘汰,這種算法很簡單,就不過多介紹啦。

最近最久未使用算法:傳說中的 LUR 算法,當發生缺頁中斷時,選擇最近最久沒有使用過的頁面淘汰,該算法會給每個頁面一個字段,用於記錄自上次訪問以來所經歷的時間 T,當需要淘汰一個頁面時,選擇已有頁面中 T 值最大的頁面進行淘汰。

第二次機會頁面置換算法:先進先出算法的升級版,只是在先進先出算法的基礎上做了一點點改動,因爲先進先出算法可能會把經常使用的頁面置換出去,該方法會給這些頁面多一次機會,給頁面設置一個修改位 R,每次淘汰最老頁面時,檢查最老頁面的 R 位,如果 R 位是 0,那麼代表這個頁面又老又沒有被二次使用過,直接淘汰,如果這個頁面的 R 位是 1,表示該頁面被二次訪問過,將 R 位置 0,並且把該頁面放到鏈表的尾端,像該頁面是最新進來的一樣,然後繼續按這種方法淘汰最老的頁面。

時鐘頁面置換算法:第二次機會頁面算法的升級版,儘管二次機會頁面算法是比較合理的算法,但它需要在鏈表中經常移動頁面,效率比較低,時鐘頁面置換算法如圖,該算法把所有的頁面都保存在一個類似時鐘的環形鏈表中,一個錶針指向最老的頁面,當發生缺頁中斷時,算法首先檢查錶針指向的頁面,如果它的 R 位是 0 就淘汰該頁面,並且把新的頁面插入這個位置,然後錶針移動到下一個位置,如果 R 位是 1 就將 R 位置 0 並把錶針移動到下一個位置,重複這個過程直到找到一個 R 位是 0 的頁面然後淘汰。

時鐘頁面置換算法

最不常用算法:當發生缺頁中斷時,選擇訪問次數最少的那個頁面去淘汰。該算法可以給每個頁面設置一個計數器,被訪問時,該頁面的訪問計數器 + 1,在需要淘汰時,選擇計數器值最小的那個頁面。

工作集時鐘頁面置換算法

在工作集頁面置換算法中,當缺頁中斷髮生後,需要掃描整個頁表才能直到頁面的狀態,進而才能確定被淘汰的是哪個頁面,因此比較耗時,所以引入了工作集時鐘頁面算法。與時鐘算法改進了先進先出算法類似,工作集頁面置換算法 + 時鐘算法 = 工作集時鐘頁面置換算法。避免了每次缺頁中斷都需要掃描整個頁表的開銷。

08

什麼是分段內存管理?

關於分段內存管理我們平時見的最多的應該就是 Linux 可執行程序的代碼段數據段之類的啦,要了解分段最好的方式就是了解它的歷史。分段起源於 8086CPU,那時候程序訪問內存還是直接給出相應單元的物理地址,爲了方便多道程序併發執行,需要支持對各個程序進行重定位,如果不支持重定位,涉及到內存訪問的地方都需要將地址寫死,進而把某個程序加載到物理內存的固定區間。通過分段機制,程序中只需要使用段的相對地址,然後更改段的基址,就方便對程序進行重定位。

而且 8086CPU 的地址線寬度是 20 位,可尋址範圍可以達到 1MB,但是它們的寄存器都是 16 位,直接使用 1 個 16 位寄存器不可能訪存達到 1MB,因此引入了段,引入了段寄存器,段寄存器左移 4 位 + 偏移量就可以生成 20 位的地址,從而達到 1MB 的尋址範圍。

**計算機運行全過程 **

當然,這是從 CPU 角度看到的視圖:對於 CPU 來說,“計算” 過程從計算機加電啓動, 執行 BIOS 程序的第一條指令開始,到最後計算機關機,整個就是一個完整的 “計算” 過 程。這個過程有多少個 “子的 ‘計算’過程”,它並不關心。但是從操作系統的視角來看,計算機從開機到關機,整個 “計算” 過程,由很多軟件,也 就是子 “計算” 過程,共同完成。從時序來說,計算機完整的 “計算” 過程如下:

整個 “計算” 過程的每個子過程都有其明確的考量。 

首先,BIOS 程序沒有固化在 CPU 中,而是獨立放到主板的 ROM 上,是因爲不同歷史時 期的計算機輸入輸出設備很不一樣,有鍵盤 + 鼠標 + 顯示器的,有觸摸屏的,也有純語音 交互的,外置存儲則有軟盤,硬盤,閃存,這些變化我們通過調整 BIOS 程序就可以應對, 而不需要修改 CPU。

引導區引導程序,則是程序從內置存儲(ROM)轉到外置存儲的邊界。引導區引導程序很 短,BIOS 只需要把它加載到內存執行就可以,但是這樣系統的控制權就很巧妙地轉到外置 存儲了。 

引導區引導程序不固化在 BIOS 中,而是寫在外置存儲的引導區,是爲了避免 BIOS 程序需 要經常性修改。畢竟 BIOS 還是硬件,而引導區引導程序已經屬於軟件範疇了,修改起來會 方便很多。

OS 引導程序,則是外置存儲接手計算機控制權的真正開始。這裏 OS 是操作系統 (Operating System)的縮寫。操作系統從這裏開始幹活了。這個過程發生了很多很多事情,這裏我們先略過。但是最終所有的初始化工作完成後,操作系統會把執行權交給 OS Shell 程序。 

OS Shell 程序負責操作系統與用戶的交互。最早的時候,計算機的交互界面是字符界面, OS Shell 程序是一個命令行程序。DOS 中叫 command.com,而在 Linux 下則叫 sh 或者 bash 之類。這裏的 sh 就是 shell 的縮寫。

這個時期啓動一個軟件的方式就是在 Shell 程序中輸入一個命令行,Shell 負責解釋命令行 理解用戶的意圖,然後啓動相應的軟件。到了圖形界面時期,在 Shell 中啓動軟件就變成點 點鼠標,或者動動手指(觸摸屏)就行了,交互範式簡化了很多。 

在瞭解了計算機從開機到關機的整個過程後,你可能很快會發現,這裏面有一個很關鍵的細 節沒有交代:計算機是如何運行外置存儲上的軟件的?

這和內存管理有關。結合內存的作用,我們談內存管理,只需要談清楚兩個問題: 如何分配內存(給運行中的軟件,避免它們發生資源爭搶); 如何運行外置存儲(比如硬盤)上的軟件?

在回答這兩個問題之前,我們先了解一個背景知識:CPU 的實模式和保護模式。這兩個模 式 CPU 對內存的操作方式完全不同。在實模式下,CPU 直接通過物理地址訪問內存。在保 護模式下,CPU 通過一個地址映射表把虛擬的內存地址轉爲物理的內存地址,然後再去讀 取數據。相應的,工作在實模式下的操作系統,我們叫實模式操作系統;工作在保護模式下的操作系 統,我們叫保護模式操作系統。 

**實模式下的內存管理 **

先看實模式操作系統。在實模式操作系統下,所有軟件包括操作系統本身,都在同一個物理地址空間下。在 CPU 看來,它們是同一個程序。操作系統如何分配內存?至少有兩種可行的方法。 

其一,把操作系統內存管理相關的函數地址,放到一個大家公認的地方(比如 0x10000 處),每個軟件要想申請內存就到這個地方取得內存管理函數並調用它。 

其二,把內存管理功能設計爲一箇中斷請求。所謂中斷,是 CPU 響應硬件設備事件的一個 機制。當某個輸入輸出設備發生了一件需要 CPU 來處理的事情,它就會觸發一箇中斷。 

內存的全局有一箇中斷向量表,本質上就是在一個大家公認的地方放了一堆函數地址。比如 鍵盤按了一個鍵,它會觸發 9 號中斷。在 CPU 收到中斷請求時,它會先停下手頭的活來響 應中斷請求(到中斷向量表找到第 9 項對應的函數地址並去執行它),完成後再回去幹原 來的活。

中斷機制設計之初本來爲響應硬件事件之用,但是 CPU 也提供了指令允許軟件觸發一箇中 斷,我們把它叫軟中斷。比如我們約定 77 號中斷爲內存管理中斷,操作系統在初始化時把 自己的內存管理函數寫到中斷向量表的第 77 項。 

所以,上面兩種方法實質上是同一個方法,只是機制細節有所不同而已。中斷機制遠不止是 函數向量表那麼簡單。比如中斷會有優先級,高優先級中斷可以打斷低優先級中斷,反之則不能。

那麼,在實模式下,操作系統如何運行外置存儲(比如硬盤)上的軟件?很簡單,就是把軟件完整從外置存儲讀入到內存然後執行它。不過,在執行前它幹了一件事 情,把浮動地址固定下來。爲什麼會有浮動地址?因爲軟件還沒有加載到內存的時候並不知 道自己會在哪裏,所以有很多涉及數據的地址、函數的地址都沒法固定下來,要在操作系統 把它加載到內存時來確定。

整體來說,實模式內存管理的機制是非常容易理解的。因爲它畢竟實質上是一個程序被拆分 爲很多個軟件(程序代碼片段),實現了程序代碼片段的動態加載而已。

保護模式下的內存管理

但實模式有兩個問題。其一是安全性。操作系統以及所有軟件都運行在一起,相互之間可以隨意修改對方的數據甚 至程序指令,這樣搞破壞就非常容易。其二是支持的軟件複雜性低,同時可運行的軟件數量少。

一方面,軟件越複雜,它的程序代碼量就越多,需要的存儲空間越大,甚至可能出現單個軟 件的大小超過計算機的可用內存,這時在實模式下就沒法執行它。另一方面,哪怕單個軟件可運行,但是一旦我們同時運行的軟件多幾個,操作系統對內存的 需求量就會急劇增加。相比這麼多軟件加起來的內存需求量,內存的存儲空間往往仍然是不 足的。

但是爲什麼平常我們可以毫無顧忌地不斷打開新的軟件,從來不曾擔心過內存會不足呢?這就是保護模式的作用了。保護模式下,內存訪問不再是直接通過物理內存,而是基於虛擬 內存。虛擬內存模式下,整個內存空間被分成很多個連續的內存頁。每個內存頁大小是固定 的,比如 64K。這樣,每次 CPU 訪問某個虛擬內存地址中的數據,它都會先計算出這是要訪問哪個內存 頁,然後 CPU 再通過一個地址映射表,把虛擬的內存地址轉爲物理的內存地址,然後到這 個物理內存地址去讀取數據。地址映射表是一個數組,下標是內存頁頁號,值是該內存頁對 應的物理內存首地址。

當然,也有可能某一個內存頁對應的物理內存地址還不存在,這種情況叫缺頁,沒法讀取數 據,這時 CPU 就會發起一個缺頁的中斷請求。

這個缺頁的中斷請求會被操作系統接管。發生缺頁時,操作系統會爲這個內存頁分配物理的 內存,並恢復這個內存頁的數據。如果沒有空閒的物理內存可以分配,它就會選擇一個最久 沒有被訪問的內存頁進行淘汰。

當然,淘汰前會把這個內存頁的數據保存起來,因爲下次 CPU 訪問這個被淘汰的內存頁時 一樣會發生缺頁中斷請求,那時操作系統還要去恢復數據。通過這個虛擬內存的機制,操作系統並不需要一上來就把整個軟件裝進內存中,而是通過缺 頁中斷按需加載對應的程序代碼片段。多個軟件同時運行的問題也解決了,內存不夠用的時 候,就把最久沒有用過的內存頁淘汰掉,騰出物理內存出來。

運行軟件的問題解決了。那麼,操作系統如何分配內存給運行中的軟件?其實,內存分配的問題也解決了,並不需要任何額外的機制。反正內存地址空間是虛擬的, 操作系統可以一上來就給要運行的軟件分配超級大的內存,你想怎麼用隨你。軟件如果不用 某個內存頁,什麼都不發生。軟件一旦用了某個內存頁,通過缺頁中斷,操作系統就分配真 正的物理內存給它。

通過引入虛擬內存及其缺頁機制,CPU 很好地解決了操作系統和軟件的配合關係。每個運行中的軟件,我們把它叫進程,都有自己的地址映射表。也就是說,虛擬地址並不是 全局的,而是每個進程有一個自己獨立的虛擬地址空間。

在保護模式下,計算機的基礎架構體系和操作系統共同在努力做的一件事情,就是讓每個軟 件 “感覺” 自己在獨佔整個計算機的資源。獨立的虛擬地址空間很好地僞裝了這一點:看起 來我獨自在享用所有內存資源。在實模式下的浮動地址的問題也解決了,軟件可以假設自己 代碼加載的絕對地址是什麼,不需要在加載的時候重新調整 CPU 指令操作的地址。

這和實模式很不一樣。在實模式下,所有進程都在同在物理內存的地址空間裏,它們相互可 以訪問對方的數據,修改甚至破壞對方的數據,進而導致其他進程(包括操作系統本身的進 程)崩潰。內存是進程運行的基礎資源,保持進程基礎資源的獨立性,是軟件治理的最基礎 的要求。這也是保護模式之所以叫 “保護” 模式的原因。

**架構思維上我們學到什麼? **

虛擬內存它本質上要解決這樣兩個很核心的需求。其一,軟件越來越大,我們需要考慮在外置存儲上執行指令,而不是完整加載到內存中。但 是外置存儲一方面它的數據 CPU 並不知道怎麼讀;另一方面就算知道怎麼讀,也不知道它 的數據格式是什麼樣的,這依賴文件系統的設計。讓 CPU 理解外置存儲的實現細節?這並 不是一個好的設計。 

其二,要同時運行的軟件越來越多,計算機內存的供給與軟件運行的內存需求相比,捉襟見 肘。怎麼才能把有限的內存的使用效率最大化?一個很容易想到的思路是把不經常使用的內 存數據交換到外置存儲。但是問題仍然是,CPU 並不瞭解外置存儲的實現細節,怎麼才能 把內存按需交換出去? 

通過把虛擬內存地址分頁,引入缺頁中斷,我們非常巧妙地解決了這個問題。缺頁中斷很像 是 CPU 留給操作系統的回調函數,通過它對變化點實現了很好的開放性設計。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/SS_QsoIk1XMEv8mpK_K4CQ