深入虛擬內存(Virtual Memory,VM)
深入虛擬內存(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, ) 的內存,它的地址數的總和是 4GB。而在 64 位的程序中,它能訪問的地址範圍是 [0, ),地址數的總和為 16EB(E = ,exa,千兆兆)。
上面說的範圍,如 [0, )表示的就是虛擬地址空間,指的是進程所能訪問的所有的虛擬內存地址的集合。虛擬地址空間主要受程序的位數影響。除此之外,它還受 CPU 的實現的影響,比如 i7 處理器,它所支持的虛擬地址空間的範圍是 [0, ),即 256TB,不過一般這也夠了。
除了虛擬地址空間之外,還有一個叫做物理地址空間的東西。顧名思義,物理地址空間表示的是所有能訪問的物理地址的集合,它受計算機的主存大小影響。比如說,計算機的內存是 4GB,那麼物理地址空間就是 [0, )。
虛擬定址 的意思就是將 虛擬地址空間 中的地址翻譯成 物理地址空間 中的地址,然後再執行相關的讀指令或者寫指令。
3.1 頁表(Page Table)
頁表是記錄頁的狀態的表,不同的進程間的頁表是獨立的。頁表中的項叫做頁表項(Page Table Entry, PTE)。
PTE 的數量為 X=N/P
,其中 N 表示虛擬地址空間中的地址數量,P 表示頁的大小。可以看出,在虛擬地址空間大小不變的情況下,頁的大小越大,那麼 PTE 的數量就越多;頁的大小越小, PTE 的數量就越少。
PTE 記錄了很多信息,這裡列舉幾個重要的:
- 有效位(P),它標識對應的虛擬頁面是否在物理內存中。
- 關聯的物理頁地址(Base addr),它表示的是對應的虛擬頁存儲在物理內存中的哪一頁。
- 讀寫訪問許可權(R/W),表示對應的頁是否為只讀的,或者是可讀可寫的。
- 超級許可權(U/S)表示該頁是否只允許內核模式訪問,還是用戶模式也可以訪問。
- 修改位(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( )位來描述這個地址在某頁中的偏移量。那麼剩下的位就用來索引 PTE。
在 CPU 中地址翻譯由一個叫做 MMU(Memory Management Unit,內存管理單元)的硬體完成。 MMU 接收一個虛擬地址,並且輸出一個物理地址。如果這個虛擬地址在物理內存中存在,那麼就叫做頁命中。如果這個虛擬地址在物理內存中不存在,那麼 MMU 將產生一個缺頁錯誤。
下圖展示了 MMU 如何利用頁表來實現虛擬地址到物理地址的映射。n 位的虛擬地址包括兩個部分:一個 p 位的虛擬 VPO,和一個 n-p 位的 VPN。MMU 利用 VPN 來選擇適當的 PTE。將 PTE 中的物理頁號(PPN)與 VPO串聯起來,就得到了相應的物理地址。注意:物理頁面偏移(PPO)和 VPO 是相同的。