深入虛擬內存(Virtual Memory,VM)

我們應該知道物理內存(Physical Memory)指的是硬體上的內存,即 RAM。它通常指的是插在主板上的內存條,給進程提供臨時數據存儲的設備。因為 CPU 可以直接從物理內存中讀取數據和指令,所以物理內存又叫做主存

虛擬內存(virtual memory,VM)又叫做虛擬存儲(virtual storage),是一種內存管理技術。它是操作系統提供的一種對主存的抽象。虛擬內存的實現由操作系統軟體和硬體結合完成,包括硬體異常、地址翻譯、磁碟文件、內核程序等。

本文將深入虛擬內存的實現機制,討論它是怎麼將磁碟和主存結合共同提供這種抽象的。

1. 虛擬內存解決了什麼問題?

1)虛擬內存給進程提供了一個更大的內存空間,不再受物理內存大小的限制。它將物理內存看作是存儲在磁碟上的地址空間的緩存。 現在的電腦好一點的差不多就是 16GB 或者 32GB 的內存,而且內存越大,肯定就越貴。那如果只有物理內存,在很多情況下根本不夠用,特別是需要運行很多程序的情況下。而磁碟空間相對來說是很便宜的,即使是 SSD,在同樣的容積下也便宜太多了。虛擬內存技術在主存中只保留活動區域,然後根據需要在磁碟和主存之間來回傳送數據,這樣,它就可以更加高效的利用主存。

2)虛擬內存為程序提供內存管理。我們在敲代碼的時候,不需要考慮這個變數會不會被其它程序錯誤的修改。因為虛擬內存幫我們做了這些事情,它給程序提供了內存隔離,為程序提供了安全的共享物理內存的途徑。使得每個進程的地址空間不會被其它進程破壞。 比如說我們在程序中定義了一個指針,並且為它分配了空間,這塊內存最終會分配到物理內存上。你不用擔心其它程序會分配相同的物理內存。

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

2. 內存管理單元——頁(Page)

前文我們已經瞭解過,虛擬內存將主存視為磁碟的緩存,主存和磁碟上會通過數據傳輸來完成同步。然而,磁碟(特別是機械磁碟)的設計不能快速的讀取或者寫入一個位元組一個位元組的數據,因為它的隨機讀寫性能比較差。比如系統要讀取一個數組的所有數據,它就要訪問數組的所有內存,而如果這些內存不在主存中,就得從磁碟上去裝載數據到主存。那麼如果是一個位元組一個位元組的讀,可能就要在磁碟和主存之間傳輸 N 次數據,這樣就會導致性能變得很差。

另外我們得為每個位元組記錄點什麼信息,纔可以知道這個內存是否已經被分配了,是否已經存在於主存中了。如果是按照一個位元組一個位元組的記錄,那我們的大部分內存空間會用在了信息記錄上面,而不是用於數據存儲。

所以要想虛擬內存獲得比較高的性能和內存利用率,必須由另外一種機制來提供。通過將虛擬內存分割為虛擬頁(Virtual Page, VP)的大小固定的塊來解決這些問題。也就是說,在磁碟和主存中傳輸數據,每次至少傳輸一個虛擬頁,記錄內存信息,也是按照虛擬頁來記錄。即虛擬頁是磁碟和主存的數據傳輸和管理單元。這樣如果是訪問剛才那個數組,大部分情況下只要在磁碟和主存之間傳輸一次數據就夠了(當然如果你的數組內存佔用比較大,超過了一個虛擬頁所能表示的大小,就要傳輸多次,但也比一個位元組一個位元組傳輸來得快非常多)。

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

一個虛擬頁的大小通常通常由處理器的結構決定,一般情況下系統中的頁大小都是一致的,比如說都是 4KB。當然,有些處理器還支持同時存在多個頁大小。虛擬頁的大小可以通過 sysconf 函數查詢:

#include <stdio.h>
#include <unistd.h> /* sysconf(3) */

int main(void) {
printf("The page size for this system is %ld bytes.
",
sysconf(_SC_PAGESIZE)); /* _SC_PAGE_SIZE is OK too. */

return 0;
}

3. 虛擬定址

那一個進程可以用的內存究竟是多大呢?這主要受兩方面的限制,1)設置的交換空間的大小與物理內存大小的總和,虛擬內存存儲在磁碟上面的空間就叫做交換空間,它通常對應一個文件或者是一個分區。所有進程共享同一個交換空間。如果交換空間和物理內存都被耗盡了,那麼就不能再分配內存了。2)進程可用的內存大小還受虛擬地址空間大小的影響。當一個進程的虛擬地址空間的所有地址都被分配了,那也不能再分配內存了。

在 32 位的程序中,由於指針的大小是 4 位元組,所以它只能訪問地址為 [0, 2^{32} ) 的內存,它的地址數的總和是 4GB。而在 64 位的程序中,它能訪問的地址範圍是 [0, 2^{64} ),地址數的總和為 16EB(E = 2^{60},exa,千兆兆)。

上面說的範圍,如 [0, 2^{32} )表示的就是虛擬地址空間,指的是進程所能訪問的所有的虛擬內存地址的集合。虛擬地址空間主要受程序的位數影響。除此之外,它還受 CPU 的實現的影響,比如 i7 處理器,它所支持的虛擬地址空間的範圍是 [0, 2^{48} ),即 256TB,不過一般這也夠了。

除了虛擬地址空間之外,還有一個叫做物理地址空間的東西。顧名思義,物理地址空間表示的是所有能訪問的物理地址的集合,它受計算機的主存大小影響。比如說,計算機的內存是 4GB,那麼物理地址空間就是 [0, 2^{32} )。

虛擬定址 的意思就是將 虛擬地址空間 中的地址翻譯成 物理地址空間 中的地址,然後再執行相關的讀指令或者寫指令。

3.1 頁表(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.2 地址翻譯

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( 2^{12}=4KB )位來描述這個地址在某頁中的偏移量。那麼剩下的位就用來索引 PTE。

在 CPU 中地址翻譯由一個叫做 MMU(Memory Management Unit,內存管理單元)的硬體完成。 MMU 接收一個虛擬地址,並且輸出一個物理地址。如果這個虛擬地址在物理內存中存在,那麼就叫做頁命中。如果這個虛擬地址在物理內存中不存在,那麼 MMU 將產生一個缺頁錯誤

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

使用頁表的地址翻譯

下面具體描述頁命中和缺頁的處理流程。

3.2.1 頁命中

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

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

  1. 處理器生成一個虛擬地址,並把它傳送給 MMU
  2. MMU 生成根據虛擬地址生成 VPN,然後請求高速緩存/主存,獲取 PTE 的數據。
  3. 高速緩存/主存向 MMU 返回 PTE 的數據
  4. 從 PTE 獲取對應的物理頁號 PPN。用物理頁的基址加上頁偏移 PPO(假設頁大小為 4KB,那麼頁偏移就是虛擬地址的低 12 位,物理頁的頁偏移和虛擬頁的頁偏移相同),獲取對應的物理地址。
  5. 主存/高速緩存將數據返回給 CPU。
頁命中

3.2.2 缺頁

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

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

  1. 處理器生成一個虛擬地址,並把它傳送給 MMU
  2. MMU 生成根據虛擬地址生成 VPN,然後請求高速緩存/主存,獲取 PTE 的數據。
  3. 高速緩存/主存向 MMU 返回 PTE 的數據
  4. 由於判斷出 PTE 的有效位是 0,所以 CPU 將出發一次異常,將控制權轉移給內核中的缺頁異常處理程序。
  5. 缺頁異常處理程序確定出物理內存中的犧牲頁,如果這個頁面被修改過了(D 標誌位為 1),那麼將犧牲頁換出到磁碟。
  6. 缺頁處理程序從磁碟中調入新的頁面到主存中,並且更新 PTE
  7. 缺頁處理程序將控制權返回給原來的進程,再次執行導致缺頁的指令。再次執行後,就會產生頁命中時的情況了。
缺頁

3.2.3 翻譯加速

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

這樣在最壞的情況下,相當於從內存/高速緩存中多讀取了一次數據。許多 MMU 包含了一個關於 PTE 的小緩存,叫做 TLB(Translation Lookaside Buffer,翻譯後備緩衝器)來消除這樣的開銷。

TLB 將虛擬內存的 VPN 視為由索引和標記組成,索引部分(TLBI)用來定位 TLB 中的緩存數據項,標記部分(TLBT)用來校驗存儲的數據項是否為指定的 VPN 對應的數據。

TLB

如果 TLB 命中了,那麼所有的地址翻譯步驟都是在 MMU 中執行的,所以非常快。下面是 TLB 命中時的操作流程

  1. 處理器生成 1 個虛擬地址
  2. MMU 向 TLB 請求 PTE
  3. TLB 返回 PTE 到 MMU

如下圖所示,其中第 4 步和第 5 步與之前的流程一致。

TLB 命中

如果 TLB 未命中,MMU 就必須從高速緩存/內存中獲取相應的 PTE,然後將新取出來的 PTE 放在 TLB 中。如下圖所示

TLB 未命中

理解 TLB 需要注意的是,因為不同進程的頁表內容是不一致的,因此內核在切換上下文時,會重置 TLB。

3.4 多級頁表

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

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

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

兩級頁表層次結構

注意,常駐內存的只是一級頁表,系統可以在需要時才創建、頁面調入二級頁表。這樣就減少了主存的壓力。另外如果一級頁表中的一個 PTE 是空的,那麼相應的二級頁表就根本不存在。這樣在一個只需要少量內存的程序上,絕大部分二級頁表是不存在的。

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

k 級頁表的地址翻譯

4. 參考資料

本文主要參考資料為《深入理解計算機系統》,圖片大多來源於書本。


推薦閱讀:
相關文章