10 分鐘看懂虛擬記憶體底層原理

10 分鐘看懂虛擬記憶體底層原理

前文我們又挖了許多坑,虛擬記憶體、DMA 等,越來越底層了。當然,我們一往無前,今天我們來學習虛擬記憶體到底是為何物,發現一篇好文,重新編輯了下,推薦大家!

一、什麼是虛擬記憶體?

在理解什麼是虛擬記憶體前,我們得先清楚什麼是物理記憶體以及物理定址,物理記憶體指的是硬體上的記憶體,因為 CPU 可以直接從物理記憶體中讀取資料和指令,所以物理記憶體又叫做主存。

主存可以看做是一個由 M 個連續的位元組大小的單元組成的陣列。每個位元組都有一個唯一的物理地址(Physical Address,PA)。第一個位元組的地址為 0,接下來的地址為 1,以此類推。

CPU 訪問記憶體的最簡單的方式是使用物理定址(physical addressing)。

10 分鐘看懂虛擬記憶體底層原理

上圖的是一條載入指令, CPU 在執行這條指令的時候,生成一個有效物理地址,透過記憶體匯流排,把這個物理地址傳遞給主存,主存取出從物理地址4處開始的 4 個位元組字,然後將它返回給 CPU。

這種直接使用物理記憶體的狀態下就會產生一些問題:

1、

記憶體空間利用率的問題

,各個程序對記憶體的使用會導致記憶體碎片化,當要用 malloc 分配一塊很大的記憶體空間時,可能會出現雖然有足夠多的空閒物理記憶體,卻沒有足夠大的連續空閒記憶體這種情況,東一塊西一塊的記憶體碎片就被浪費掉了。

2、

讀寫記憶體的安全性問題

,物理記憶體本身是不限制訪問的,任何地址都可以讀寫,而現代作業系統需要實現不同的頁面具有不同的訪問許可權,例如只讀的資料等等。

3、

程序間的安全問題

,各個程序之間沒有獨立的地址空間,一個程序由於執行錯誤指令或是惡意程式碼都可以直接修改其它程序的資料,甚至修改核心地址空間的資料,這是作業系統所不願看到的。

4、

記憶體讀寫的效率問題

,當多個程序同時執行,需要分配給程序的記憶體總和大於實際可用的物理記憶體時,需要將其他程式暫時複製到硬碟當中,然後將新的程式裝入記憶體執行。由於大量的資料頻繁裝入裝出,記憶體的使用效率會非常低。

為了解決上述的問題,大佬們設計了虛擬記憶體。

虛擬記憶體(virtual memory,VM)

,是一種記憶體管理技術。它是作業系統提供的一種對主存的抽象。虛擬記憶體的實現由作業系統軟體和硬體結合完成,包括硬體異常、地址翻譯、磁碟檔案、核心程式等。

二、虛擬記憶體解決了什麼問題?

1)虛擬記憶體給程序提供了一個更大的記憶體空間,不再受物理記憶體大小的限制。它將物理記憶體看作是儲存在磁碟上的地址空間的快取。虛擬記憶體技術在主存中只保留活動區域,然後根據需要在磁碟和主存之間來回傳送資料,這樣,它就可以更加高效的利用主存。

2)虛擬記憶體為程式提供記憶體管理。我們在敲程式碼的時候,不需要考慮這個變數會不會被其它程式錯誤的修改。因為虛擬記憶體幫我們做了這些事情,它給程式提供了

記憶體隔離

,為程式提供了安全的共享物理記憶體的途徑。

使得每個程序的地址空間不會被其它程序破壞。

比如說我們在程式中定義了一個指標,並且為它分配了空間,這塊記憶體最終會分配到物理記憶體上,你不用擔心其它程式會分配相同的物理記憶體。

3)虛擬記憶體技術也給每個程序提供了一致的、完整的地址空間。比如在作業系統上執行若干個程序,每個程序都有相同的地址空間,都在同樣的起始位置放置了堆、棧以及程式碼段等。這樣,它簡化了像連結器、載入器這樣的程式的記憶體管理。

三、虛擬定址

那一個程序可以用的記憶體究竟是多大呢?這主要受兩方面的限制:

1)設定的

交換空間

的大小與物理記憶體大小的總和,虛擬記憶體儲存在磁碟上面的空間就叫做交換空間,它通常對應一個檔案或者是一個分割槽。所有程序共享同一個交換空間,如果交換空間和物理記憶體都被耗盡了,那麼就不能再分配記憶體了。

2)程序可用的記憶體大小還受

虛擬地址空間大小

的影響,當一個程序的虛擬地址空間的所有地址都被分配了,那也不能再分配記憶體了。

在 32 位的程式中,由於指標的大小是 4 位元組,所以它只能訪問地址為 [0, 232 ) 的記憶體,它的地址數的總和是 4GB。而在 64 位的程式中,它能訪問的地址範圍是 [0, 264),地址數的總和為 16EB

上面說的範圍,如 [0, 232)表示的就是

虛擬地址空間

,指的是程序所能訪問的所有的虛擬記憶體地址的集合。虛擬地址空間主要受程式的位數影響。除此之外,它還受 CPU 的實現的影響,比如 i7 處理器,它所支援的虛擬地址空間的範圍是 [0, 248),即 256TB,不過一般這也夠了。

除了虛擬地址空間之外,還有一個叫做

物理地址空間

的東西。顧名思義,物理地址空間表示的是所有能訪問的物理地址的集合,它受計算機的主存大小影響。比如說,計算機的記憶體是 4GB,那麼物理地址空間就是 [0, 232)。

使用虛擬定址,CPU需要將虛擬地址翻譯成物理地址,這樣才能訪問到真實的物理記憶體。

1、頁(Page)

虛擬記憶體被組織成為一個由存放在磁碟上的 N 個連續的位元組大小的單元組成的陣列,也就是位元組陣列。每個位元組都有一個唯一的虛擬地址作為陣列的索引。磁碟上活動的陣列內容被快取在主存中,主存和磁碟上會透過資料傳輸來完成同步。

然而,磁碟的設計不能快速的讀取或者寫入資料,因為它的

隨機讀寫效能比較差

。比如系統要讀取一個數組的所有資料,它就要訪問陣列的所有記憶體,而如果這些記憶體不在主存中,就得從磁碟上去裝載資料到主存。如果是一個位元組一個位元組的讀,可能就要在磁碟和主存之間傳輸 N 次資料,這樣就會導致效能變得很差。

另外我們得為每個位元組記錄點什麼資訊,才可以知道這個記憶體是否已經被分配了,是否已經存在於主存中了。

如果是按照一個位元組一個位元組的記錄,那我們的大部分記憶體空間會用在了資訊記錄上面,而不是用於資料儲存。

所以要想虛擬記憶體獲得比較高的效能和記憶體利用率,必須由另外一種機制來提供。

透過將虛擬記憶體分割為虛擬頁(Virtual Page, VP)的大小固定的塊來解決這些問題。

也就是說,在磁碟和主存中傳輸資料,每次至少傳輸一個虛擬頁,記錄記憶體資訊,也是按照虛擬頁來記錄。即虛擬頁是磁碟和主存的資料傳輸和管理單元。這樣如果是訪問剛才那個陣列,大部分情況下只要在磁碟和主存之間傳輸一次資料就夠了。

和虛擬頁對應的還有物理頁,概念和虛擬頁基本相同,除了它是儲存在主存中的。因為是按照頁作為傳輸單元的,所以

物理頁和虛擬頁的大小一致,

一般情況下系統中的頁大小都是一致的,比如說都是 4KB。

2、頁表(Page Table)

頁表

是記錄頁的狀態的表,

不同的程序間的頁表是獨立的

頁表中的項叫做頁表項(Page Table Entry, PTE)。

PTE 的數量為 X=N/P,其中 N 表示虛擬地址空間中的地址數量,P 表示頁的大小。可以看出,在虛擬地址空間大小不變的情況下,頁的大小越大,那麼 PTE 的數量就越少;頁的大小越小, PTE 的數量就越多。

PTE 記錄了很多資訊,這裡列舉幾個重要的:

1)有效位( P ),它標識對應的虛擬頁面是否在物理記憶體中。

2)關聯的物理頁地址(Base addr),它表示的是對應的虛擬頁儲存在物理記憶體中的哪一頁。

3)讀寫訪問許可權(R/W),表示對應的頁是否為只讀的,或者是可讀可寫的。

4)超級許可權(U/S)表示該頁是否只允許核心模式訪問,還是使用者模式也可以訪問。

5)修改位(D),表示被載入到物理記憶體之後,頁面的內容是否發生了修改。

3、地址翻譯

PTE 按照

虛擬頁索引(VPN)

排序,比如第 0 頁位於的起始位置,第 1 頁位於第 0 頁後面,依此類推。VPN 是根據虛擬地址、頁大小算出來的,比如頁大小為 4KB,那第 0 頁的地址就是頁表的起始地址,第 1 頁的地址就是頁表地址+頁大小,即 0x00001000。位於第 0 頁和第 1 頁之間的地址都屬於第 0 頁。

假設頁大小為 4KB,地址空間為 32 位。系統將虛擬地址視為兩部分組成,前 20 位表示頁索引(VPN),後 12 位表示頁偏移(VPO)。如果根據虛擬地址( VA ) 來寫一個獲取頁索引( VPN )的公式就是:VPN=VA>>12。因為頁大小是 4KB,所以一個虛擬地址需要使用 12( 212 = 4KB )位來描述這個地址在某頁中的偏移量K。那麼剩下的位就用來索引 PTE。

在 CPU 中地址翻譯由一個叫做

MMU(Memory Management Unit,記憶體管理單元)

的硬體完成。MMU 接收一個虛擬地址,並且輸出一個物理地址。如果這個虛擬地址在物理記憶體中存在,那麼就叫做頁命中。如果這個虛擬地址在物理記憶體中不存在,那麼 MMU 將產生一個

缺頁錯誤

下圖展示了 MMU 如何利用頁表來實現虛擬地址到物理地址的對映。n 位的虛擬地址包括兩個部分:一個 p 位的虛擬 VPO,和一個 n-p 位的 VPN。MMU 利用 VPN 來選擇適當的 PTE。將 PTE 中的物理頁號(PPN)與 VPO串聯起來,就得到了相應的物理地址。注意:

物理頁面偏移(PPO)和 VPO 是相同的。

10 分鐘看懂虛擬記憶體底層原理

3.1 頁命中

頁命中指的是當 MMU 需要根據虛擬地址輸出物理地址時,這個地址所在的頁已經被裝載到物理記憶體中了。即對應的 PTE 的有效為為 1。

下面是頁命中時的地址翻譯的過程:

處理器生成一個虛擬地址,並把它傳送給 MMU

MMU 生成根據虛擬地址生成 VPN,然後請求快取記憶體/主存,獲取 PTE 的資料。

快取記憶體/主存向 MMU 返回 PTE 的資料

從 PTE 獲取對應的物理頁號 PPN。用物理頁的基址加上頁偏移 PPO(假設頁大小為 4KB,那麼頁偏移就是虛擬地址的低 12 位,物理頁的頁偏移和虛擬頁的頁偏移相同),獲取對應的物理地址。

主存/快取記憶體將資料返回給 CPU。

10 分鐘看懂虛擬記憶體底層原理

3.2 缺頁

缺頁是指當 CPU 請求一個虛擬地址時,虛擬地址所對應的頁在物理記憶體中不存在。此時 MMU 會產生缺頁錯誤,然後由核心的缺頁處理程式從磁碟中調入對應的頁到主存中。在處理完成後,CPU 會重新執行導致錯誤的指令,從而讀取到對應的記憶體資料。

下面是缺頁時的地址翻譯的過程(第 1 步到第 3 步與頁命中時相同):

處理器生成一個虛擬地址,並把它傳送給 MMU

MMU 生成根據虛擬地址生成 VPN,然後請求快取記憶體/主存,獲取 PTE 的資料。

快取記憶體/主存向 MMU 返回 PTE 的資料

由於判斷出 PTE 的有效位是 0,所以 CPU 將觸發一次異常,將控制權轉移給核心中的缺頁異常處理程式。

缺頁異常處理程式確定出物理記憶體中的犧牲頁,如果這個頁面被修改過了(D 標誌位為 1),那麼將犧牲頁換出到磁碟。

缺頁處理程式從磁碟中調入新的頁面到主存中,並且更新 PTE

缺頁處理程式將控制權返回給原來的程序,再次執行導致缺頁的指令。再次執行後,就會產生頁命中時的情況了。

10 分鐘看懂虛擬記憶體底層原理

3.3 翻譯加速

從頁命中的流程圖中可以看出,CPU 每次需要請求一個虛擬地址,MMU 就需要從記憶體/快取記憶體中獲取 PTE ,然後再根據 PTE 的內容去從物理記憶體中載入資料。

這樣在最壞的情況下,相當於從記憶體/快取記憶體中多讀取了一次資料。許多 MMU 包含了一個關於 PTE 的小快取,叫做

TLB(Translation Lookaside Buffer,翻譯後備緩衝器

,也可簡稱為“快表”)來消除這樣的開銷。

如果 TLB 命中了,那麼所有的地址翻譯步驟都是在 MMU 中執行的,所以非常快;如果 TLB 未命中,MMU 就必須從快取記憶體/記憶體中獲取相應的 PTE,然後將新取出來的 PTE 放在 TLB 中。

3.4 多級頁表

前面有提到過,PTE 的數量由虛擬地址空間的大小和頁大小決定。也就是:X=N/P。那如果我們有一個 32 位的物理地址空間、4KB 的頁面和一個 4 位元組的 PTE。即使程式只使用了一小部分虛擬地址空間,也總是需要一個 4MB的頁表常駐主存。對於 64 位的系統來說,情況將變得更加複雜。

設計者非常聰明,它將頁表設計成一個包括多級的層次結構來解決這個問題。

下圖展示了一個兩級頁表的層次結構。二級頁表中的每個 PTE 項都負責一個 4KB 頁面,而一級頁表中的每個 PTE 負責 1024 個二級頁表項。

10 分鐘看懂虛擬記憶體底層原理

注意,

常駐記憶體的只是一級頁表,系統可以在需要時才建立、頁面調入二級頁表。

這樣就減少了主存的壓力。另外如果一級頁表中的一個 PTE 是空的,那麼相應的二級頁表就根本不存在。這樣在一個只需要少量記憶體的程式上,絕大部分二級頁表是不存在的。

下圖展示的是一個 k 級層次頁表的結構圖,起始就是將 VPN 部分劃分為多個段,每個段都代表某一級頁表。而每一級中的 PTE 的 Base addr 為下一級提供入口地址。最後一級的 Base addr 則表示最終物理地址的 PPN。

10 分鐘看懂虛擬記憶體底層原理

ps:編輯不易,求個關注

挖坑序列文章

I/O Zero Copy是什麼?看完這篇你絕對會了

10分鐘看懂 Java IO 底層原理

深入分析 Java 需要編碼的場景

Java 編碼很難嗎?看完這篇文章你就懂了

編碼字符集和字符集編碼傻傻分不清楚!看完這篇文章你就懂了?

為什麼 String 要設計成 final ,又如何設計一個不可變類呢?

你真的懂 Java 的 String 嗎?