為什麼 Linux 需要虛擬記憶體

draveness發表於2020-06-09

作業系統中的 CPU 和主記憶體(Main memory)都是稀缺資源,所有執行在當前作業系統的程式會共享系統中的 CPU 和記憶體資源,作業系統會使用 CPU 排程器分配 CPU 時間並引入虛擬記憶體系統以管理實體記憶體,本文會分析作業系統為什麼需要虛擬記憶體。

在回答虛擬記憶體存在的必要性之前,我們需要理解作業系統中的虛擬記憶體是什麼,它在作業系統中起到什麼樣的作用。正如軟體工程中的其他抽象,虛擬記憶體是作業系統實體記憶體和程式之間的中間層,它為程式隱藏了實體記憶體這一概念,為程式提供了更加簡潔和易用的介面以及更加複雜的功能。

圖 1 – 程式和作業系統的中間層

如果需要我們從頭設計一個作業系統,讓系統中的程式直接訪問主記憶體中的實體地址應該是非常自然的決定,早期的作業系統確實也都是這麼實現的,程式會使用目標記憶體的實體地址(Physical Address)直接訪問記憶體中的內容,然而現代的作業系統都引入了虛擬記憶體,程式持有的虛擬地址(Virtual Address)會經過記憶體管理單元(Memory Mangament Unit)的轉換變成實體地址,然後再通過實體地址訪問記憶體:

圖 2 – 虛擬記憶體系統

主儲存是相對比較稀缺的資源,雖然順序讀取只比磁碟快 1 個數量級,但是它能提供極快的隨機訪問速度,從記憶體上隨機讀取資料是磁碟的 100,000 倍,充分利用記憶體的隨機訪問速度是改善程式執行效率的有效方式。

作業系統以頁為單位管理記憶體,當程式發現需要訪問的資料不在記憶體時,作業系統可能會將資料以頁的方式載入到記憶體中,這個過程是由上圖中的記憶體管理單元(MMU)完成的。作業系統的虛擬記憶體作為一個抽象層,起到了以下三個非常關鍵的作用:

  • 虛擬記憶體可以利用磁碟起到快取的作用,提高程式訪問磁碟的速度;
  • 虛擬記憶體可以為程式提供獨立的記憶體空間,簡化程式的連結、載入過程並通過動態庫共享記憶體;
  • 虛擬記憶體可以控制程式對實體記憶體的訪問,隔離不同程式的訪問許可權,提高系統的安全性;

快取

我們可以將虛擬記憶體看作是在磁碟上一片空間,當這片空間中的一部分訪問比較頻繁時,該部分資料會以頁為單位被快取到主存中以加速 CPU 訪問資料的效能,虛擬記憶體利用空間較大的磁碟儲存作為『記憶體』並使用主儲存快取進行加速,讓上層認為作業系統的記憶體很大而且很快,然而區域很大的磁碟並不快,而很快的記憶體也並不大

圖 3 – 虛擬記憶體、主存和磁碟

虛擬記憶體中的虛擬頁(Virtual Page,PP)可能處於以下的三種狀態 — 未分配(Unallocated)、未快取(Uncached)和已快取(Cached),其中未分配的記憶體頁是沒有被程式申請使用的,也就是空閒的虛擬記憶體,不佔用虛擬記憶體磁碟的任何空間,未快取和已快取的記憶體頁分別表示僅載入到磁碟中的記憶體頁和已經載入到主存中的記憶體頁。如上圖所示,圖中綠色的虛擬記憶體頁由主存中的實體記憶體頁(Physical Page,PP)支撐,所以它是已經快取過的,而黃色的虛擬記憶體頁僅在磁碟中,所以沒有被實體記憶體快取。

當使用者程式訪問未被快取的虛擬頁時,硬體就會觸發缺頁中斷(Page Fault,PF),在部分情況下,被訪問的頁面已經載入到了實體記憶體中,但是使用者程式的頁表(Page Table)並不存在該對應關係,這時我們只需要在頁表中建立虛擬記憶體到實體記憶體的關係;在其他情況下,作業系統需要將磁碟上未被快取的虛擬頁載入到實體記憶體中。

圖 4 – 虛擬記憶體的缺頁中斷

因為主記憶體的空間是有限的,當主記憶體中不包含可以使用的空間時,作業系統會從選擇合適的實體記憶體頁驅逐回磁碟,為新的記憶體頁讓出位置,選擇待驅逐頁的過程在作業系統中叫做頁面替換(Page Replacement)。缺頁中斷和頁面替換技術都是作業系統調頁演算法(Paging)的一部分,該演算法的目的就是充分利用記憶體資源作為磁碟的快取以提高程式的執行效率。

記憶體管理

虛擬記憶體可以為正在執行的程式提供獨立的記憶體空間,製造一種每個程式的記憶體都是獨立的假象,在 64 位的作業系統上,每個程式都會擁有 256 TiB 的記憶體空間,核心空間和使用者空間分別佔 128 TiB,部分作業系統使用 57 位虛擬地址以提供 128 PiB 的定址空間。因為每個程式的虛擬記憶體空間是完全獨立的,所以它們都可以完整的使用 0×0000000000000000 到 0x00007FFFFFFFFFFF 的全部記憶體。

圖 5 – 作業系統的虛擬記憶體空間

虛擬記憶體空間只是作業系統中的邏輯結構,就像我們上面說的,應用程式最終還是需要訪問實體記憶體或者磁碟上的內容。因為作業系統加了一個虛擬記憶體的中間層,所以我們也需要為程式實現地址翻譯器,實現從虛擬地址到實體地址的轉換,頁表是虛擬記憶體系統中的重要資料結構,每一個程式的頁表中都儲存了從虛擬記憶體到實體記憶體頁的對映關係,為了儲存 64 位作業系統中 128 TiB 虛擬記憶體的對映資料,Linux 在 2.6.10 中引入了四層的頁表輔助虛擬地址的轉換,在 4.11 中引入了五層的頁表結構,在未來還可能會引入更多層的頁表結構以支援 64 位的虛擬地址。

圖 6 – 四層頁表結構

在如上圖所示的四層頁表結構中,作業系統會使用最低的 12 位作為頁面的偏移量,剩下的 36 位會分四組分別表示當前層級在上一層中的索引,所有的虛擬地址都可以用上述的多層頁表查詢到對應的實體地址。

因為有多層的頁表結構可以用來轉換虛擬地址,所以多個程式可以通過虛擬記憶體共享實體記憶體。我們在 為什麼 Redis 快照使用子程式 一文中介紹的寫時複製就利用了虛擬記憶體的這個特性,當我們在 Linux 中呼叫 fork 建立子程式時,實際上只複製了父程式的頁表。如下圖所示,父子程式會通過不同的頁表指向相同的實體記憶體:

圖 7 – 程式間共享記憶體

虛擬記憶體不僅可以在 fork 時用於共享程式的實體記憶體,提供寫時複製的機制,還能共享一些常見的動態庫減少實體記憶體的佔用,所有的程式都可能呼叫相同的作業系統核心程式碼,而 C 語言程式也會呼叫相同的標準庫。

除了能夠共享記憶體之外,獨立的虛擬記憶體空間也會簡化記憶體的分配過程,當使用者程式向作業系統申請堆記憶體時,作業系統可以分配幾個連續的虛擬頁,但是這些虛擬頁可以對應到實體記憶體中不連續的頁中。

記憶體保護

作業系統中的使用者程式不應該修改只讀的程式碼段,也不應該讀取或者修改核心中的程式碼和資料結構或者訪問私有的以及其他的程式的記憶體,如果無法對使用者程式的記憶體訪問進行限制,攻擊者就可以訪問和修改其他程式的記憶體影響系統的安全。

如果每一個程式都持有獨立的虛擬記憶體空間,那麼虛擬記憶體中頁表可以理解成程式和物理頁的『連線表』,其中可以儲存程式和物理頁之間的訪問關係,包括讀許可權、寫許可權和執行許可權:

圖 8 – 讀許可權、寫許可權和執行許可權

記憶體管理單元可以決定當前程式是否有許可權訪問目標的實體記憶體,這樣我們就最終將許可權管理的功能全部收斂到虛擬記憶體系統中,減少了可能出現風險的程式碼路徑。

總結

虛擬記憶體的設計方法可以說是軟體工程中的常見手段,通過結合磁碟和記憶體各自的優勢,利用中間層對資源進行更合理地排程充分提高資源的利用率並提供和諧以及統一的抽象,而在實際的業務場景中,類似的快取邏輯也比較常見。

作業系統的虛擬記憶體是非常複雜的元件,沒有工程師能夠了解其中的全部細節,不過了解虛擬記憶體的整體設計也很有價值,我們能夠從中找到很多軟體設計的方法。我們重新回到今天的問題 — Linux 作業系統中為什麼需要虛擬記憶體:

  • 虛擬記憶體可以結合磁碟和實體記憶體的優勢為程式提供看起來速度足夠快並且容量足夠大的儲存;
  • 虛擬記憶體可以為程式提供獨立的記憶體空間並引入多層的頁表結構將虛擬記憶體翻譯成實體記憶體,程式之間可以共享實體記憶體減少開銷,也能簡化程式的連結、裝載以及記憶體分配過程;
  • 虛擬記憶體可以控制程式對實體記憶體的訪問,隔離不同程式的訪問許可權,提高系統的安全性;

到最後,我們還是來看一些比較開放的相關問題,有興趣的讀者可以仔細思考一下下面的問題:

  • 為什麼每層的頁表結構只能夠負責 9 位虛擬地址的定址?
  • 64 位的虛擬記憶體在作業系統中需要多少層的頁表結構才能定址?

相關文章