為何使用虛擬內存#
通用操作系統中一般會運行多個進程,使用同一份物理內存資源。操作系統在進行內存管理時,如果允許進程直接使用物理內存,會有一些問題:
- 操作系統給進程分配的空間會不夠用,再次申請內存時空間可能並不連續
- 由於共享同一份物理地址空間 A 進程可能會寫入到 B 進程內使用的地址
出於效率和安全性考量,引入了虛擬內存:應用進程通過虛擬地址讀寫物理內存 CPU 內部的 MMU 負責將虛擬地址轉換成具體的物理地址再進行對應的讀寫操作,操作系統負責為每個進程建立虛擬地址到物理地址的映射。對於每個進程來說,都能夠認為自己擁有連續完整的地址空間。
早期大多數系統使用的虛擬內存管理方式是分段:操作系統將虛擬地址空間和物理地址空間分割成大小不同的段比如代碼段數據段等等,通過段表實現虛擬地址到物理地址的映射,但是在物理段之間容易出現碎片
- 虛擬地址包含:段號 + 段內地址 (偏移量)
- MMU 根據
段表基址寄存器
找到段表 - 通過段號在段表中找到對應段的起始物理地址
- 物理段起始地址 + 偏移量即得到物理地址
虛擬內存分頁#
現代操作系統通常使用分頁機制管理虛擬內存。
操作系統首先將虛擬地址空間和物理地址空間分割成較小的連續等長的頁,通過頁表實現虛擬地址到物理地址的映射。用戶進程可以使用全部的虛擬地址空間,內核為每個進程維護一個頁表,這樣即使兩個進程使用相同的虛擬地址,如果頁表中所映射的物理地址不同也不會產生衝突。
- 虛擬地址包含:頁號 + 頁內偏移量
- MMU 根據
頁表基址寄存器
找到對應進程的頁表 - 在頁表中找到頁號對應的物理頁的起始地址
- 物理頁起始地址 + 頁內偏移量即得到物理地址
多級頁表#
為了壓縮頁表項所佔用的物理內存空間 (考慮 64 位虛擬地址空間每頁大小 4KB 使用單頁表所佔用的空間), 通常會使用多級頁表。多級頁表將虛擬地址的頁號部分分為若干份,其中每份對應一個頁表項目。以 AArch64
架構為例,虛擬地址使用 48 個有效位 (即虛擬地址空間大小為 2^48) 其中低 12 位表示頁內偏移量,剩下的 36 位分割成 4 級頁表
- 首先通過
TTBR0_EL1
寄存器找到第 0 級頁表物理地址 - 通過第 47-39 位找到在第 0 級頁表中的項:第 1 級頁表的物理地址
- 通過第 38-30 位找到在第 1 級頁表中的項:第 2 級頁表的物理地址
- 通過第 29-21 位找到在第 2 級頁表中的項:第 3 級頁表的物理地址
- 通過第 20-12 位找到在第 3 級頁表中的項:對應的物理頁號
- 通過第 11-0 位 (頁內偏移量) 加上物理頁號得到真正的物理地址
通常在頁表項中除了存儲物理地址外,還會存一些 flag 比如頁表項對應物理地址是否存在;是否是髒頁等等。在多級頁表項的查找過程中如果某一個頁表項對應物理地址不存在則可以直接終止查詢。
多級頁表中第 i 級並非所有頁表項都指向一個第 i+1 級頁表,假設某個進程只使用了 4KB 的內存空間,則其第 0 級頁表中只有一項真正指向一個第 1 級頁表,第 1 級頁表中也只有一項指向一個第 2 級頁表,後續同理。這樣一共也就使用了 4 個頁表頁。如果其使用了連續的 4KB*64 的內存空間,如果這 64 個頁的前三級頁表地址恰好一樣,最終也只會使用到 4 個頁表頁,非常節省空間。參考下圖:
轉址旁路快取 TLB 和頁表切換#
為了提升轉址效率 MMU 內有一個 TLB 硬件用來快取一個完整的虛擬頁號到物理頁號的映射。在 TLB 內部會有類似於 CPU 的多層快取比如 L1 指令快取 L1 數據快取 L2 層快取 等。一般 TLB 內能夠存儲的條目較少,只有幾千個條目。在一次轉址過程中 MMU 首先會通過 TLB 查詢快取,若快取命中則直接返回;如果未命中則執行多級頁表查詢最後將結果回寫到 TLB 如果 TLB 滿則會根據硬件策略替換掉某一項。另外,在操作系統中一般會運行多個進程,不同進程使用的虛擬地址可能是相同的,所以需要確保 TLB 中快取的內容和當前進程的頁表一致,操作系統在執行進程切換 (頁表切換時) 時主動刷新 TLB
考慮到每次切換進程刷新 (清空) TLB 會導致進程剛開始運行時產生大量的 TLB Miss 現代的 CPU 硬件提供了進程標籤功能如 amd64 下的 Process Context IDentifier
操作系統可以給不同的進程分配不同的 PCID 再將其寫入到頁表基址寄存器和 TLB 的快取項中,這樣即使切換了進程 TLB 也能通過當前頁表基址寄存器中的 PCID 和快取條目中的 PCID 區分不同進程的快取條目而無需清空 TLB 但是在修改頁表時操作系統還是需要及時刷新 TLB 以保證一致性。這樣做的壞處就是單個進程能夠使用的 TLB 條目進一步減少了。
在 AArch64 架構中一般內核和用戶進程使用不同的頁表基址寄存器因此在切換到內核態 (執行系統調用) 時無需切換頁表。而在 x86-64 架構中雖然頁表基址寄存器只有一個,但是內核並不使用單獨的頁表,而是將自己的地址空間映射到每個進程地址空間的高位部分 (相當於多個用戶進程共享同一份內核地址空間) 因此也不需要切換頁表。
換頁與缺頁#
注意:下文中分配指的是進程向操作系統申請一定大小的空間,操作系統為進程在虛擬地址空間中分配一段空間並將其開始和結束地址返回給進程。
在應用進程執行過程中會預先申請一段內存空間 (比如在 Linux 下使用 brk 或者 mmap 系統調用) 在未對這些空間上的地址進行讀寫之前,操作系統並不為其映射到真實的物理頁,即頁表中這些地址所表示的虛擬頁對應的的物理頁的映射是不存在的。在首次讀寫時 CPU 檢查頁表發現對應的物理頁不存在會觸發缺頁異常此時 CPU 會執行操作系統預先設置的 page fault handler
函數,為其找到到一個合適的物理頁並將地址寫入到頁表中。實際情況下,為了減少缺頁異常觸發的次數,每次在執行缺頁處理函數時可能會同時為鄰近的多個虛擬頁映射物理頁。
進程在運行過程中可能會使用超過物理內存大小的內存資源,此時如果再觸發缺頁異常,操作系統可以通過將部分物理頁寫入到磁碟等更低級的存儲設備中,讓出一部分空閒的物理頁給當前正在運行的進程,這個過程稱之為換頁 / 換出在執行換頁過程中,操作系統需要將物理頁內容寫入到磁碟,清空對應頁表項,保存物理頁在磁碟上的位置。當被換出的虛擬頁被重新訪問時,操作系統會把之前換出到磁碟上的內容重新寫入到一個物理頁,修改進程頁表重新將虛擬頁映射道新的物理頁,這個過程稱為換入
檔案系統中的交換分區即是虛擬內存在換頁時使用的磁碟分區。考慮到磁碟 IO 效率較低,操作系統在執行換入時會預測多個可能會被訪問的頁,一並換入以減少 IO 次數。
當應用程序訪問某個虛擬頁觸發缺頁異常時,操作系統需要判斷這個虛擬頁是處於未分配狀態還是已分配但未映射狀態。操作系統只會給已分配的虛擬頁執行到物理頁的映射。在 Linux 下進程的虛擬地址空間會分為多個 Virtual Memory Area
每個 VMA 都會有一定的範圍 (區域的起始地址到區域的結束地址) 比如代碼,堆疊,堆分別對應不同的 VMA 觸發缺頁異常時操作系統可以通過檢查虛擬頁是否處於某個 VMA 來判斷是否已分配。
虛擬內存功能#
共享內存#
由於虛擬頁是通過進程的頁表映射到物理頁,操作系統可以通過讓兩個進程 A 和 B 的兩個虛擬頁 A1 和 B1 映射到同一個物理頁 P 上來實現進程間的內存共享。任意一個進程對共享內存的寫入都可以被另外一個進程讀取到。
寫時拷貝#
通過共享內存可以實現 Copy On Write
即寫時拷貝功能。比如在 Linux 下進程可以通過 fork 創建子進程,子進程創建之初父子進程的內存數據是完全一樣的所以 Linux 僅僅為子進程複製一份頁表,不修改任何映射,同時 Linux 會在頁表中將這一片共享內存權限設置為只讀,當任一進程對此虛擬頁進行寫入就會由於權限不足觸發缺頁異常,後續操作系統會將缺頁部分的數據拷貝到新的物理頁,並將新的物理頁寫入到觸發缺頁異常進程的頁表項並恢復權限等。除了 fork 以外多個進程也可以通過共享內存映射到相同的動態鏈接庫,減少內存使用。
內存去重#
基於 COW 操作系統還可以 (定期) 主動將多個具有相同內容的物理頁合併成一個,然後將所有關聯的頁表映射修改到這個唯一的物理頁上來提升內存利用效率,這項功能稱為內存去重。在 Linux 下實現了此功能稱之為 Kernel Same-page Merging
可以想像使用此功能會對性能造成一定的影響,另外還會產生某些安全性問題:比如攻擊者可以通過枚舉構造數據,然後等待內存去重,根據訪問延遲 (由 COW 產生缺頁中斷造成) 確認是否發生了內存去重,如果發生了則攻擊者可以猜測目前物理地址空間內存在與當前構造相同的數據。不過操作系統可以通過只在同一用戶的進程之間去重來避免這個問題。
內存壓縮#
操作系統可以通過將不太常使用的內存數據壓縮的方式減少物理頁的使用。在 Linux 下內存中有一個 zswap 區域用來存儲被壓縮的內存數據,內存數據被壓縮後首先是放到 zswap 下當物理內存資源不足時 zswap 會批量換出到磁碟。這樣能夠減少甚至能夠避免立即換出造成的磁碟 IO 提高內存使用效率。
大頁#
前面提高過 TLB 的快取條目很少,在 4KB 內存頁大小的情況下可能會不夠用 (經常 TLB Miss 使得效率很低) 所以很多操作系統提供了大頁的功能,通過增大內存頁的大小到 2MB 甚至 1GB 減少 TLB 的佔用量。在 Linux 下就提供了 transparent huge page
即透明大頁機制自動將連續的 4KB 頁合併成一個 2MB 的頁。
通過使用大頁可以極大降低 TLB 快取條目的佔用,進而提升 TLB 命中率,但是大頁也可能會降低內存的使用效率,比如一個 2MB 的大內存頁實際只使用了 256KB 會造成造成很高的資源浪費。
參考與引用
《現代操作系統:原理與實現》