徹底搞懂虛擬內存,虛擬地址,虛擬地址空間

程序經過編譯後,變成了可執行的文件,可執行文件主要包括代碼和數據兩部分,代碼是隻讀的,數據則是可讀可寫的。

可執行文件由操作系統加載到內存中,交由 CPU 去執行,現在問題來了,CPU 怎麼去訪問代碼和數據?,訪問的方式經歷過四個階段:

  1. 直接訪問

  2. 段基址 + 段偏移地址

  3. 段選擇子 + 段偏移地址

  4. 虛擬地址

現代操作系統採用的是虛擬地址, 這也是本篇文章闡述的重點, 但虛擬地址是由 1~3 階段發展而來的,所以也有必要闡述 1~3 三種訪問方式。

直接訪問


直接訪問很好理解,程序經過編譯後, 生成了可執行文件,編譯器給每行數據和代碼分配了一個唯一的地址,如下圖

可執行文件

如上圖可執行文件中 1000~1024 之間的地址,加載到內存後,內存的地址也是 1000~1024,在可執行文件中分配的唯一地址就是內存中的物理地址,這就叫直接訪問,直接定訪問乾脆直接,沒有那些彎彎繞。

當時問題也不少,例如同一個可執行文件不能同時執行,它們的物理地址一樣,衝突了,必須一個接一個,還有就是可執行文件的物理地址已經固定了,如果想在其它物理地址運行,必須地重新編譯,生成新的物理地址。

可見直接定位是計算機發展早期的產物,早期沒有那麼多的程序要運行,程序都是一個接一個地去執行的,因此早期這種定位比較簡單,直接高效。

段基址 + 段偏移地址


隨着多任務需求的來臨,現在內存中要併發運行多個程序,雖然採用直接定位把每個不同的程序放在不同的內存段中,勉強可以滿足,但是一個可執行文件不能同時運行多個,另外程序必須在固定的物理地址運行,靈活性大大減弱,調度起來也是非常麻煩,因此 CPU 設計師和操作系統開發人員發明了段基址 + 段偏移地址。

Inter 8086 處理器就是採用這種定位方式,我們知道可執行文件主要分爲數據段和內存段,如下圖

由上圖紅色部分可知,0,4,8 就是相對於數據段的偏移地址,0,4,8,12 是相對於代碼段的偏移地址。

在可執行文件中,一個段的偏移地址是固定的,無論可執行文件加載到內存的什麼位置, 這個偏移地址是固定的。

當可執行文件加載到內存時, 先在內存中分配一個數據段和代碼段,這兩個段理論上可以不挨着,一般情況下,代碼段和數據段是挨着的,代碼段和數據段在內存中都有一個起始地址,這個起始地址就叫做段基址, 這個段基址是放在段寄存器裏,例如代碼段基址放在 CS 寄存器, 數據段基址放在 DS 寄存器, 當然還有其他的段例如棧段, 棧段剛開始大小爲 0,隨着程序的運行入棧, 出棧,這個棧段在不斷擴展,當然,咋們主要說的是數據段和代碼段,棧段只是簡單帶過。

假設可執行文件被加載到了內存中,如下圖

如上圖所示, 代碼段被佈局到以 0x00600000 爲起始地址的內存中, 數據段被佈局到以 0x00601000 爲起始地址的內存中。

當 CPU 開始執行代碼段的第一條指令時, 會將代碼段的起始地址放入到段寄存器中, 此時 CS 代碼段寄存器中存儲的就是 0x00600000, 然後開始從起始地址處開始執行第一條代碼指令,此時把代碼指令的偏移地址放入到 IP 寄存器中, IP 寄存器存儲的就是 0,所以 CPU 要定位一條代碼指令時通過 CS:IP 的方式定位的,如下圖所示

定位指令

當 CPU 執行到 0x00600000 處的代碼指令時,該指令爲 MOV AX,[0],該指令的意思是把地址 0 處的數據存儲到 AX 寄存器,這個 0 就是數據段的偏移地址,此時 CPU 會將數據段的起始地址加入到 DS 段寄存器中, 然後將數據段寄存器的值 + 偏移地址即 0x00601000+0=0x00601000 定位到了數據 123,然後將 123 存儲到 AX 寄存器中。

定位數據

上述過程就是【段基址 + 段偏移地址】的定位方式,之所以把起始地址加入到寄存器中,也是爲了後續再執行指令或者獲取數據時,可以直接從寄存器獲取,加快 CPU 執行的速度。

段選擇子 + 段偏移地址


【段選擇子 + 段偏移地址】與【段基址 + 段偏移地址】有些相似之處,之所以採用【段選擇子 + 段偏移地址】主要是爲了安全,原來的【段基址 + 段偏移地址】方式,程序員可以直接跳轉到其他代碼段和數據段,沒有任何限制,安全性全依賴於程序員的職業操守和水平,因此 CPU 設計者就發明了【段選擇子 + 段偏移地址】。

【段選擇子 + 段偏移地址】中的段選擇子可以認爲是一個索引,這個索引指向了全局段描述符表中的一項,全局段描述表存儲在內存中,它的起始地址存儲在全局段描述符寄存器中。

全局段描述符表有很多個段描述符,每個段描述佔用 8 個字節,這個段描述符裏面就包括了段基址,另外還有一些安全性相關的描述信息例如段的可讀,可寫,可執行,段的大小等。

段選擇子存儲在了段寄存器中,總共 16 位, 其中高 13 位就是全局段描述表的索引。

當 CPU 開始執行代碼段的第一條指令時, 會將代碼段的選擇子放入到 CS 段寄存器中, 然後 CPU 從段寄存器中的獲取段選擇子,然後截取選擇子的高 13 位獲取索引,然後根據全局描述符表寄存器的地址找到全局描述符表的起始地址,根據起始地址 + 索引 * 8 找到段描述符, 然後根據段描述符獲取段的基址,段的基址加上 ip 寄存器中的偏移地址就是指令的物理地址,如下圖所示 1~6 步驟所示

定位指令

當 CPU 執行到 0x00600000 處的代碼指令時,該指令爲 MOV AX,[0],該指令的意思是把地址 0 處的數據存儲到 AX 寄存器,這個 0 就是數據段的偏移地址,此時 CPU 會將數據段的選擇子加入到 DS 段寄存器中, 然後 CPU 獲取段選擇的高 13 位獲取索引,然後根據全局描述符表寄存器的地址找到全局描述符表的起始地址,根據起始地址 + 索引 * 8 找到段描述符, 然後根據段描述符獲取段的基址,段的基址加上數據段的偏移地址就是數據的物理地址,如下圖 1~6 步驟所示

定位數據

上述過程就是【段選擇子 + 段偏移地址】的定位方式。

虛擬地址


現代的操作系統和 CPU 未打開分頁時採用的是【段選擇子 + 段偏移地址】訪問代碼和數據,而一旦打開分頁時,經過【段選擇子 + 段偏移地址】得到的地址不再是物理地址了,而是叫做虛擬地址,默認則是打開分頁的。

現代的操作系統和 CPU 採用的平坦模型,平坦模型就是整個內存就一個段,因此段基址就是 0,段偏移地址就等於虛擬地址了。

下面將從以下幾個方面來闡述虛擬地址相關的話題。

  1. 什麼是虛擬地址,物理地址,虛擬地址空間,物理地址空間,虛擬內存,物理內存?

  2. 什麼是進程虛擬地址空間?

  3. 什麼是虛擬頁,物理頁?

  4. 什麼是頁表?

  5. 虛擬地址怎麼樣訪問物理內存?

什麼是虛擬地址,物理地址,虛擬地址空間,物理地址空間,虛擬內存,物理內存?


虛擬地址空間是虛擬地址的集合, 假設虛擬地址空間是 N 位的,它的地址範圍爲 {0~2 的 N 次方 - 1} 即它有 2 的 N 次方個虛擬地址, 例如 16 位的虛擬地址空間, 它的地址範圍爲{0~65535},這意味着 16 位的虛擬地址空間有 65536 個虛擬地址。

物理地址空間是物理地址的集合,假設物理地址空間有 M 個字節, 它的地址範圍爲 {0~M-1},M 不一定是 2 的多少次冪, 例如 M=100,表示物理地址空間大小爲 100 個字節, 它的地址範圍爲 {0~99},通常情況下物理地址空間是 2 的冪次方,例如 65536, 這也是爲了計算機方便處理而已,並不是強制要求的。

物理內存可以認爲是一個的物理字節數組,每個物理地址指向這個物理字節數組中的一項。

虛擬內存也一樣,它也可以認爲是一個物理字節數組,不過這個字節數組是存儲在磁盤上。

物理地址空間是物理內存的範圍,虛擬地址空間是虛擬內存的範圍,物理地址空間中的每個物理地址都是實打實地指向了具體的存儲單元,虛擬地址空間中每個虛擬地址指向哪裏有 3 種情況:

a. 未分配, 這個虛擬地址僅僅是個數字而已,沒有任何指向。

b. 未緩衝, 這個虛擬地址指向了磁盤的某個字節存儲單元,裏面存儲了指令或者數據。

c. 已緩衝, 這個虛擬地址指向了物理內存的某個字節存儲單元,裏面存儲了指令或者數據。

  1. 什麼是進程虛擬地址空間?

操作系統加載可執行文件後,創建了一個進程,這個進程就有了自己的虛擬地址空間,每個進程的虛擬地址空間都一樣,如下圖所示

進程虛擬地址空間

如上圖所示,進程的虛擬地址空間被統一劃分成了多個固定區域,例如代碼區,數據區,堆區,共享區,棧區,內核區。

代碼區和數據區域:來自於可執行文件,代碼區和數據區挨着,代碼區總是在 0x0040000 地址以上,0x0040000 地址以下另有它用。

運行時堆區域:它初始化大小爲 0,隨着動態分配內存 (malloc), 運行時堆不斷往高地址方向擴展,有個指針 brk 指向了堆的最高地址。

共享庫的內存映射區域:這個區域是一些標準的系統庫,這個共享庫在物理內存中只存儲一份,每個進程將這個區域的虛擬地址映射到同一份共享庫物理內存上。

用戶棧區域: 這個區域緊挨着內核區域,處於高地址處,隨着用戶棧的出棧,入棧,動態擴展,入棧向低地址方向擴展,出棧則向高地址方向收縮,棧頂指針存儲在棧寄存器 (ESP) 中。

內核區域:這個區域是操作系統自己代碼,數據,棧空間,內核在物理內存中只存儲一份,每個進程將這個區域的虛擬地址映射到同一份內核物理內存上。

內核和共享庫的映射

  1. 什麼是虛擬頁,物理頁?

現代操作操作和 CPU 將物理內存按照固定的頁大小分成很多份, 每一份叫做物理頁 (PP),每一份有一個編號叫做物理頁號(PPN), 這個物理頁大小通常是 4KB, 例如一個物理內存大小爲 20KB,這個物理內存可以分成 5 個物理頁,那麼物理頁號(PPN) 就是 0,1,2,3,4。

虛擬內存也一樣,它的頁大小與物理內存的頁大小相同,虛擬內存也被分成了很多份, 每一份叫做虛擬頁 (VP), 每一份的編號叫做虛擬頁號(VPN), 例如假設虛擬頁大小爲 4KB,一個虛擬內存大小爲 10KB,這個虛擬內存可以分成 2 個虛擬頁(VP), 虛擬頁號(VPN) 就是 0,1

每個物理頁存儲在物理內存上,每個虛擬頁存儲在磁盤上,如下圖所示

虛擬內存和物理內存

上圖的虛擬內存有 8 個虛擬頁, 物理內存有 6 個物理內存頁,虛擬頁存儲在磁盤上,物理頁則存儲在 DRARM 上。

每個虛擬頁可以有三種狀態,未分配, 已緩衝,未緩衝

未分配: 虛擬頁還沒有分配磁盤空間

已緩衝: 虛擬頁緩衝或者映射在了物理頁上。

未緩衝: 虛擬頁分配了磁盤空間,但沒有在物理頁上緩衝。

通常操作系統加載可執行文件後,創建了一個進程,這個進程就有了虛擬地址空間,這並不意味着可執行文件已經從磁盤加載到內存中了,操作系統只是爲了進程虛擬地址空間的每個區域分配了虛擬頁。

代碼和數據區域的虛擬頁被分配到了可執行文件的適當位置,此時虛擬頁狀態爲未緩衝,虛擬頁指向了磁盤地址。

操作系統和共享庫的虛擬頁被映射到了物理內存,因爲操作系統和共享庫已經在物理內存了,這些虛擬頁的狀態爲已緩衝。

用戶棧,運行時堆的虛擬頁沒有任何分配,不佔用任何空間,這些虛擬頁的狀態爲未分配。

那麼進程虛擬地址空間的代碼和數據,用戶棧,運行時堆的物理內存什麼時候分配呢,答案就是處理器用虛擬地址執行代碼,讀取數據時, 這個下一章闡述。

虛擬地址訪問物理內存


先普及幾個概念:

VPO 即虛擬頁偏移量:

虛擬地址由虛擬頁號 + 虛擬頁偏移量組成,虛擬頁偏移量是相對某個虛擬頁的偏移量。

PPO 即物理頁偏移量:

物理地址由物理頁號 + 物理頁偏移量組成,物理頁偏移量是相對某個物理頁的偏移量,

VPO 等於 PPO

頁表 (Page Table)PT:

頁表是建立虛擬頁號和物理頁號映射關係的表結構,每個頁表項 (PTE) 包括了有效位,物理頁號,磁盤地址等信息,如下圖:

頁表與物理內存,虛擬內存的關係

由上圖可以得知,操作系統可以根據頁表項的有效位和地址信息判斷出虛擬頁目前所處的狀態即未分配,已緩衝,未緩衝

如上圖所示,頁表有 8 個頁表項,每個頁表項裏包含一個有效位和地址信息。

當頁表項 PTE n 的頁表項有效位爲 0 時, 表示虛擬頁 n 沒有緩衝在物理內存,可能在磁盤或者未分配,例如 PTE 0 頁表項裏存儲的是 null,表明虛擬頁 0 即 VP0 是未分配狀態,PTE 3 裏存儲的是磁盤地址,表明虛擬頁 3 即 VP3 在磁盤裏,但沒有緩衝,VP3 狀態爲未緩衝。

當頁表項 PTE n 的頁表項的有效位爲 1 時, 表示虛擬頁 n 緩衝在物理內存, PTE n 存儲了物理頁號, 虛擬頁 n 的狀態爲已緩衝, 例如 PTE 1 的頁表項,有效位爲 1,則虛擬頁 VP1 緩衝在了物理頁中。

頁表基址寄存器 (PTBR):

每個進程都有自己的頁表,CPU 執行某個進程時,會先把該進程的一級頁表起始地址存儲到頁表基址寄存器,這樣 CPU 查找一級頁表起始地址可以直接從寄存器查找,加快了查找效率。

好了,概念介紹到這裏,先來看看虛擬地址翻譯物理地址的過程, 按照一級頁表來演示,如下圖所示:

虛擬地址翻譯物理地址

上圖爲虛擬地址翻譯物理地址的示意圖,可以看出 VPO 等於 PPO。

下面看看計算機各個部件是怎麼通過虛擬地址訪問物理內存的。

處理器根據虛擬地址訪問物理內存的分爲頁表項命中和頁表項未命中兩種情況,頁表項命中意味着頁表項的有效位爲 1,頁表項存儲的是物理頁號,虛擬頁緩衝在物理頁中,未命中意味着頁表項有效位爲 0,此時需要發送缺頁中斷。

頁表項命中的步驟如下圖:

頁表項命中翻譯步驟

1.CPU 將虛擬地址 (VA) 送入 MMU,MMU 根據頁表基址寄存器中頁表的起始地址加上虛擬頁號,找到了頁表項的物理地址 PTEA。

2.MMU 將 PTEA 送入到高速緩衝或者內存。

  1. 從高速緩衝或者內存中找到頁表項 (PTE),返回頁表項(PTE) 給 MMU。

4.MMU 根據 PTE 找出物理頁號,然後加上虛擬頁偏移量形成物理地址 (PA), 送入到高速緩衝或者內存。

  1. 高速緩衝或者內存獲取數據,返回數據給處理器。

頁表項未命中的步驟如下圖:

頁表項未命中翻譯步驟

1.CPU 將虛擬地址 (VA) 送入 MMU,MMU 根據頁表基址寄存器中頁表的起始地址加上虛擬頁號,找到了頁表項的物理地址 PTEA。

2.MMU 將 PTEA 送入到高速緩衝或者內存。

  1. 從高速緩衝或者內存中找到頁表項 (PTE),返回頁表項(PTE) 給 MMU。

4.MMU 根據 PTE, 發現頁不在內存中,未命中,因此 MMU 發送一個缺頁中斷,交由缺頁異常處理程序處理。

  1. 缺頁異常處理程序根據頁置換算法,選擇出一個犧牲頁,如果這個頁面已經被修改了,則寫出到磁盤上,最後將這個犧牲頁的頁表項有效位設置爲 0,存入磁盤地址。

  2. 缺頁異常程序處理程序調入新的頁面,如果該虛擬頁尚未分配磁盤空間,則分配磁盤空間,然後磁盤空間的頁數據拷貝到空閒的物理頁上,並更新 PTE 的有效位爲 1,更新物理頁號,缺頁異常處理程序返回後,再回到發生缺頁中斷的指令處,重新按照頁表項命中的步驟執行。

虛擬地址翻譯物理地址的過程介紹完了, 另外要說的是現代的 CPU 和操作系統爲了加快虛擬地址翻譯物理地址的過程,做了以下兩點優化:

  1. 建立了虛擬號 (VPN) 和頁表項(PTE)的映射關係, 存儲在 TLB 中,當 MMU 根據虛擬地址獲取頁表項時,先查詢 TLB, 在 TLB 找到了頁表項後,就不需要從高速緩衝或者內存中獲取了,找不到了纔會計算頁表項地址 PTEA,然後再從高速緩衝或者內存中獲取頁表項(PTE)。

  2. 某些熱點物理地址對應的數據,存儲在 L1 緩衝中,MMU 根據物理地址獲取頁表項或者代碼數據時,先從 L1 緩衝中獲取,找不到再從內存中獲取。

上述的翻譯過程是通過一級頁表來翻譯,現在操作系統支持多級頁表,多級頁表與一級頁表比較類似,如下圖所示:

K 頁表

上圖爲 K 級頁表,頁表基址寄存器存儲的是一級頁表的地址,1 到 K-1 的頁表的每一項存儲的下一級頁表的起始地址,K 級頁表的每一項存儲的是物理頁號或者磁盤地址。

好了,關於虛擬地址,虛擬內存,虛擬地址空間的話題就介紹到這裏了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/XO0PKH9YWJVD0Ho-VKHENw