深入理解 linux 虛擬內存機制
概述
現代操作系統了提供了一種對主存的抽象概念,叫做虛擬內存。它爲每個進程提供了一個非常大的,一致的和私有的地址空間。虛擬內存提供了以下的三個關鍵能力:
-
它將主存看成是一個存儲在磁盤空間上的地址空間的高速緩存,主存中只保存活動區域,並根據需要在磁盤和主存之間來回傳送數據。
-
它爲內閣進程提供了一致的地址空間, 簡化了內存管理。
-
它保護了每個進程的地址空間不被其他進程破壞。
物理和虛擬尋址
計算機的主存可以看做是一個由 M 個連續的字節大小的單元組成的數組。每個字節都有一個唯一的物理地址(Physical Address,PA)。第一個字節的地址爲 0,接下來的地址爲 1,以此類推。CPU 訪問內存的最簡單的方式是使用物理尋址(physical addressing)。
物理尋址
該圖例的上下文是一條加載指令,塔讀取從物理地址 4 處開始的 4 字節字。CPU 在執行這條指令的時候,生成一個有效物理地址,通過內存總線,把這個物理地址傳遞給主存,主存取出從物理地址 4 處開始的 4 個字節字,然後將它返回給 CPU,CPU 將它存放在一個寄存器裏。早期使用物理尋址的系統通常都比較簡單,比如數字信號處理器,計算器等嵌入式設備。
虛擬尋址
現在處理器採用的是一個程序虛擬尋址(virtual addressing)的尋址方式,如上圖所示。CPU 通過生成一個虛擬地址(virtual address,VA)來訪問主存,這個虛擬地址在被送到主存之前會先轉換成一個物理地址。將虛擬地址轉換成物理地址的任務叫做地址翻譯(address translation),地址翻譯需要 CPU 硬件和操作系統之間的配合。CPU 芯片上叫做內存管理單元(Menory Management Unit, MMU)的專用硬件,利用存放在主存中的查詢表來動態翻譯虛擬地址,該表的內容由操作系統管理。
地址空間
地址空間(address space)是一個非負整數 地址 的有序合集:{0,1,2,...}
地址空間
在一個帶虛擬內存的系統中,CPU 從一個有 N= 2 的 n 次方 個地址的地址空間中生成虛擬地址,這個地址空間就稱爲虛擬地址空間:
{0,1,2,3,...., N-1}。
一個系統還有一個地理地址空間,對應於系統中物理內存的 M 個字節:{0,1,2,3,..., M-1}。
一個地址空間的大小通常是由表示最大地址所需要的位數來描述的,比如,一個包含 N = 2 的 n 次方 個地址的虛擬地址空間就叫做一個 n 位地址空間,現代操作系統通常支持 32 位或者 64 位虛擬地址空間。
虛擬內存做爲緩存的工具
從概念上來說,虛擬內存被組織成爲一個由存放在磁盤上的 N 個連續的字節大小的單元組成的數組,也就是字節數組。每個字節都有一個唯一的虛擬地址作爲數組的索引。磁盤上活動的數組內容被緩存在主存中。在存儲器結構中,較低層次上的磁盤的數據被分割成塊,這些塊作爲和較高層次的主存之間的傳輸單元。主存作爲虛擬內存的緩存。
虛擬內存(VM)系統將虛擬內存分割成稱爲虛擬頁(Virtual Page,VP)的大小固定的塊,每個虛擬頁的大小爲 P = 2 的 p 次方 字節。同樣的,物理內存被分割爲物理頁(Physical Page,PP), 大小也爲 P 字節(物理頁也稱作頁幀(page frame))。
在任意時刻,虛擬頁面的集合都分爲三個不相交的子集:
-
未分配的,VM 系統還未分配(或者創建)的頁,未分配的頁沒有任何數據和它們關聯,因此不佔用任何內存空間。
-
緩存的,當前已緩存在物理內存中的已分配頁。
-
未緩存的,未緩存在物理內存中的已分配頁。
VM 系統如何使用主存作爲緩存
上圖展示了在一個有 8 個虛擬內存的虛擬內存中,虛擬頁 0 和 3 還沒有被分配,所以在磁盤上不存在。虛擬頁 1,4,6 被緩存在物理內存中。虛擬頁 2,5,7 已經被分配了,但是當前並沒有緩存在主存中。
DRAM 緩存的組織結構
我們使用 SRAM 緩存來表示位於 CPU 和 主存之間的 L1, L2 和 L3 高速緩存,使用 DRAM 緩存來表示虛擬內存系統中的緩存,也就是主存。
在存儲器層次結構中, DRAM 比 SRAM 慢個大約 10x 倍,磁盤比 DRAM 慢大約 10, 000x 倍。因此 DRAM 緩存的不命中比 SRAM 緩存中的不命中要昂貴的多,因爲 DRAM 緩存不命中需要和磁盤傳送數據,而 SRAM 緩存不命中是和 DRAM 傳送數據。
歸根到底, DRAM 緩存的組織結構是由巨大的不命中開銷驅動的。
頁表
VA 是通過什麼方式轉成 PA 呢 ?
同任何緩存一樣,虛擬內存系統必須有某種方法來判定一個虛擬也是否緩存在 DRAM 的某個地方。如果命中緩存,那麼虛擬內存系統還必須確認這個虛擬頁存在哪個物理頁中。如果沒有命中緩存,那麼虛擬內存系統必須判斷虛擬頁存放在磁盤的哪個位置,在物理內存中選擇一個犧牲頁,並將虛擬頁從磁盤複製到 DRAM,替換這個犧牲頁。
這些功能由軟硬件聯合提供,包括操作系統軟件,MMU 中的地址翻譯硬件和一個存放在物理內存中叫頁表(page table)的數據結構,頁表將虛擬頁映射到物理頁。每次地址翻譯硬件將一個虛擬地址轉換成物理地址時都會讀取頁表。
Page Tables
上圖展示了一個頁表的基本結構,頁表就是一個頁表條目(Page Table Entry,PTE)的數組。虛擬地址空間中的每個頁在頁表中都有一個 PTE。在這裏我們假設每個 PTE 是由一個有效位(Valid bit)和一個 n 位地址字段組成的。有效位表明了該虛擬頁當前是否被緩存在 DRAM 中。如果有效位爲 1,那麼地址字段就表示 DRAM 中相應的物理頁的起始位置,這個物理頁緩存了該虛擬頁。如果有效位爲 0,那麼一個 null 地址表示這個虛擬頁還未被分配,否則對應的這個地址就指向該虛擬頁在磁盤上的起始位置。
上圖所示中一共有 8 個虛擬頁和 4 個物理頁的頁表,4 個虛擬頁 VP1, VP2, VP4, VP7 當前被緩存在 DRAM 中,VP0 和 VP5 還未被分配,而剩下的 VP3 和 VP6 已經被分配了,但是當前未被緩存。
頁命中
當 CPU 想要讀取 VP2 中的虛擬內存中的一個字時,地址翻譯硬件將虛擬地址作爲一個索引來定位到 PTE2, 並從主存中讀取它。因爲 PTE2 設置了有效位,所以 VP2 是緩存在主存中的,所以地址翻譯硬件使用 PTE 中的物理內存地址構造出這個字的物理地址。
缺頁
在虛擬內存中,DRAM 緩存不命中稱爲缺頁(page fault)。如上圖所示,CPU 引用了 VP3 中的一個字, VP3 並未緩存在 DRAM 中。地址翻譯硬件從內存中讀取 PTE3, 從有效位判斷出 VP3 未被緩存,並且觸發了一個缺頁異常。缺頁異常會調用內核的缺頁異常處理程序,該程序會選擇一個犧牲頁。如下圖所示,在這個案例中就是存放在 PP3 中的 VP4。
犧牲頁
此時如果 VP4 已經被修改了,那麼內核程序會將它複製回磁盤。接下來,內核程序從磁盤賦值 VP3 到內存中的 PP3 並更新 PTE3。隨後返回用戶進程。當異常處理程序返回時,它會重啓執行導致缺頁的指令,該指令會將導致缺頁的虛擬地址重新發送到地址翻譯硬件。如下圖所示,現在 VP3 已經在主存中了,那麼就是頁命中了。
頁命中
局部性
當我們瞭解了虛擬內存的運作機制之後,是不是覺得虛擬內存的效率會很低呢?因爲頁面沒有命中的代價非常大。是不是擔心虛擬內存會影響程序的性能呢?其實虛擬內存運作的非常好。它充分利用了局部性( locality )的原理。
在程序整個運作過程中,程序引用的不同頁面的總數可能超出了物理內存的總大小,但是局部性原則可以保證在任意時刻,程序將趨向於在一個較小的活動頁面(active page)集合上工作,這個集合被稱作工作集(working set)或者常駐集合(resident set)。在程序初始開銷之後也就是將工作集頁面調入主存,接下來對這個工作集的訪問會產生命中,這樣就不會產生額外的磁盤消耗。
如果程序有良好的時間局部性,那麼虛擬內存將工作的非常好。如果程序沒有良好的時間局部性也就是工作集的大小超出了主存的大小,那麼程序將會進入一個稱作 抖動(thrashing)的狀態,這個時候頁面將不斷地換進換出,程序會出現性能問題。
虛擬內存作爲內存管理的工具
虛擬內存大大簡化了內存管理,操作系統爲每個進程提供了一個獨立的虛擬地址空間。
內存管理工具
在上圖中,進程 1 的頁表將 VP1 映射到 PP2, VP2 映射到 PP6。進程 2 的頁表將 VP1 映射到 PP8, VP2 映射到 PP6。在這裏可以看到多個虛擬頁面可以映射到同一個共享的物理頁面上。
按需頁面調度和獨立的虛擬地址空間的結合,讓 虛擬內存簡化了鏈接和加載,代碼和數據共享,以及應用程序的內存分配。
-
簡化鏈接。獨立的地址空間允許每個進程的內存映像使用相同的基本格式,而不管代碼和數據實際存放在物理內存的何處。
-
簡化加載。虛擬內存使得容易向內存中加載可執行文件和共享對象文件。將一組連續的虛擬頁面映射到任意一個文件中的任意位置的表示法稱作內存映射(memory mapping)。Linux 提供了一個 nmap 的系統調用,允許應用程序自己做內存映射。
-
簡化共享。獨立地址空間爲操作系統提供了一個管理用戶進程和操作系統自身之間共享的一致機制。一般情況下,每個進程都有自己私有的代碼、數據、堆棧。這些內容不與其他進程共享。在這種情況下,操作系統創建頁表,將相應的虛擬頁映射到不連續的物理頁面。
-
簡化內存分配。虛擬內存向用戶進程提供一個簡單的分配額外內存的機制。當一個用戶程序要求額外的堆空間時候,操作系統分配 k 個適當的連續的虛擬內存頁面,並且將他們映射到物理內存的中的 k 個任意頁面,操作系統沒有必要分配 k 個連續的物理內存頁面。
虛擬內存作爲內存保護的工具
虛擬內存大大簡化了內存管理,操作系統提供獨立的地址空間使得區分不同進程的私有內存變得容易,但是地址翻譯機制可以使用一種自然的方式拓展到提供更好的訪問控制。每次 CPU 生成一個地址時,地址翻譯硬件都會讀一個 PTE ,通過在 PTE 上添加一些額外的控制位來控制對一個虛擬頁面內容的訪問。
訪問控制
在上圖中,每個 PTE 添加了三個控制位, SUP 位表示進程是否必須運行在超級用也就是內核模式下才能訪問該頁,WRITE 位控制頁面的寫訪問, EXRC 位控制頁面的執行。如果有指令違反了這些控制條件,那麼 CPU 會觸發一個一般保護故障,將控制傳遞給內核中的異常處理程序。
地址翻譯
頁面命中
上圖中展示了頁面命中的場景,CPU 硬件的執行步驟:
-
處理器 生成一個虛擬地址,並把它傳送給 MMU。
-
MMU 生成 PTE 地址,並從高速緩存 / 主存中請求這個 PTE 。
-
高速緩存 / 主存向 MMU 返回 PTE。
-
MMU 構造物理地址,並把它傳送給高速緩存 / 主存。
-
高速緩存 / 主存返回所請求的數據字給處理器。
頁面命中是全部由硬件來處理的,既然有頁面命中,那麼就有頁面不命中的場景。
頁面不命中
上圖展示了頁面不命中的場景, CPU 硬件的執行步驟:
-
處理器 生成一個虛擬地址,並把它傳送給 MMU。
-
MMU 生成 PTE 地址,並從高速緩存 / 主存中請求這個 PTE 。
-
高速緩存 / 主存向 MMU 返回 PTE。
-
PTE 中的有效控制位爲 0 ,所以 MMU 觸發了一次異常,傳遞 CPU 中的控制到操作系統內核中的缺頁異常處理程序。
-
缺頁處理程序確定出物理內存中的犧牲頁,如果這個頁面已經被修改了,則把它換出到磁盤。
-
缺頁處理程序調入新的頁面,並更新內存中的 PTE。
-
缺頁處理程序返回原來的進程,再次執行導致缺頁的指令, CPU 將引起缺頁的虛擬地址重新發送給 MMU ,因爲虛擬頁面現在存在主存中,所以會命中,主存將請求字返回給處理器。
地址翻譯的過程執行起來太慢了?怎麼解決呢?答案你應該也猜到了,就是添加緩存。在 MMU 中包含了一個 TLB (Translation Lookaside Buffer)緩存。
TLB 命中
我們來看看 TLB 命中的場景,
-
第 1 步 CPU 產生一個虛擬地址
-
第 2 和 3 步 MMU 從 TLB 中取出對應的 PTE 。
-
第 4 步 MMU 將這個虛擬地址翻譯成一個物理地址,並且將它發送到高速緩存 / 主存。
-
第 5 步 高速緩存 / 主存將所請求的數據字返回 CPU。
如下圖所示,當 TLB 不命中的時候, 多了步驟 3 和 4 ,MMU 必須從 L1 緩存中取出對應的 PTE , 新取出的 PTE 存放在 TLB 中,可能會覆蓋一個已經存在的 PTE 。
TLB 不命中
總結
虛擬內存對於計算機系統來說是核心的,虛擬內存也是強大的,同時虛擬內存也是危險的。虛擬內存還包括多級頁表,內存映射,動態內存分配,垃圾收集等等內容。限於篇幅,這裏只是敘述了一些基本的概念,幫助程序員理解 虛擬內存 這個概念,對具體細節不做深究。若是有興趣的話,可以參考 《深入理解計算機系統》書籍,瞭解更多關於計算機虛擬內存要點。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/SOb6KQlSAzAdhwfXqqPhJw