# 虛擬內存,第 1 部分:虛擬內存簡介
> 原文:<https://github.com/angrave/SystemProgramming/wiki/Virtual-Memory%2C-Part-1%3A-Introduction-to-Virtual-Memory>
## 什么是虛擬內存?
在非常簡單的嵌入式系統和早期計算機中,進程直接訪問存儲器,即“地址 1234”對應于存儲在物理存儲器的特定部分中的特定字節。在現代系統中,情況已不再如此。而是每個過程都是孤立的;并且在特定 CPU 指令的地址或進程的數據與物理存儲器的實際字節(“RAM”)之間存在轉換過程。內存地址不再是“真實的”;該進程在虛擬內存中運行。虛擬內存不僅可以保證進程的安全(因為一個進程無法直接讀取或修改另一個進程的內存),它還允許系統有效地為不同的進程分配和重新分配內存部分。
## 什么是 MMU?
內存管理單元是 CPU 的一部分。它將虛擬內存地址轉換為物理地址。如果當前沒有從特定虛擬地址到物理地址的映射,或者當前 CPU 指令嘗試寫入該進程僅具有讀訪問權的位置,則 MMU 也可以中斷 CPU。
## 那么我們如何將虛擬地址轉換為物理地址呢?
想象一下,你有一臺 32 位機器。指針可以保持 32 位,即它們可以尋址 2 ^ 32 個不同的位置,即 4GB 的存儲器(我們將遵循一個地址的標準約定可以保持一個字節)。
想象一下,我們有一個大表 - 這里是聰明的部分 - 存儲在內存中!對于每個可能的地址(全部 40 億個),我們將存儲“真實”即物理地址。每個物理地址需要 4 個字節(保存 32 位)。該方案需要 160 億個字節來存儲所有條目。哎呀 - 我們的查找方案會占用我們可能為 4GB 機器購買的所有內存。我們需要做得比這更好。我們的查找表最好小于我們的內存,否則我們的實際程序和操作系統數據將沒有空間。解決方案是將內存分塊為稱為“頁面”和“幀”的小區域,并為每個頁面使用查找表。
## 什么是頁面?他們中有多少人?
頁面是一塊虛擬內存。 Linux 操作系統上的典型塊大小為 4KB(即 2 ^ 12 個地址),但您可以找到更大塊的示例。
因此,我們不是談論單個字節,而是討論 4KB 的塊,而每個塊都稱為頁面。我們也可以為我們的頁面編號(“第 0 頁”“第 1 頁”等)
## EX:32 位機器中有多少頁(假設頁面大小為 4KB)?
答案:2 ^ 32 地址/ 2 ^ 12 = 2 ^ 20 頁。
請記住,2 ^ 10 是 1024,所以 2 ^ 20 有點超過一百萬。
對于 64 位機器,2 ^ 64/2 ^ 12 = 2 ^ 52,大約 10 ^ 15 頁。
## 什么是框架?
幀(或有時稱為“頁面幀”)是 _ 物理存儲器 _ 或 RAM(=隨機存取存儲器)的塊。這種內存有時被稱為“主存儲”(與較慢的二級存儲形成對比,例如具有較低訪問時間的旋轉磁盤)
幀與虛擬頁面的字節數相同。如果 32 位機器具有 2 ^ 32(4GB)的 RAM,那么在機器的可尋址空間中將有相同數量的 RAM。 64 位機器不太可能擁有 2 ^ 64 字節的 RAM - 你能明白為什么嗎?
## 什么是頁面表,它有多大?
頁表是頁面與幀之間的映射。例如,第 1 頁可能映射到第 45 幀,第 2 頁映射到第 30 幀。其他幀當前可能未使用或分配給其他正在運行的進程,或者由操作系統在內部使用。
一個簡單的頁表只是一個數組,`int frame = table[ page_num ];`
對于具有 4KB 頁面的 32 位機器,每個條目需要保持幀號 - 即 20 位,因為我們計算出有 2 ^ 20 幀。這是每個條目 2.5 個字節!實際上,我們將每個條目最多四個字節舍入,并找到這些備用位的用途。每個條目 4 個字節 x 2 ^ 20 個條目= 4 MB 的物理內存需要保存頁表。
對于具有 4KB 頁面的 64 位機器,每個條目需要 52 位。讓每個條目向上舍入到 64 位(8 字節)。有 2 ^ 52 個條目,即 2 ^ 55 個字節(大約 40 peta 字節...)哎呀我們的頁面表太大了。
在 64 位體系結構中,內存地址是稀疏的,因此我們需要一種機制來減少頁表大小,因為大多數條目永遠不會被使用。

頁面表的可視示例在這里。想象一下,訪問一個數組并抓住數組元素。
## 什么是偏移量以及它是如何使用的?
請記住,我們的頁面表將頁面映射到幀,但每個頁面都是一個連續的地址塊。我們如何計算在特定幀內使用哪個特定字節?解決方案是直接重用虛擬內存地址的最低位。例如,假設我們的進程正在讀取以下地址 - `VirtualAddress = 11110000111100001111000010101010 (binary)`
在頁面大小為 256 字節的計算機上,最低的 8 位(10101010)將用作偏移量。剩余的高位將是頁碼(111100001111000011110000)。
## 多級頁表
多級頁面是 64 位體系結構的頁表大小問題的一種解決方案。我們將看一下最簡單的實現 - 一個兩級頁表。每個表都是指向下一級表的指針列表,并非所有子表都需要存在。一個示例,32 位架構的兩級頁表如下所示 -
```
VirtualAddress = 11110000111111110000000010101010 (binary)
|_Index1_|| || | 10 bit Directory index
|_Index2_|| | 10 bit Sub-table index
|__________| 12 bit offset (passed directly to RAM)
```
在上述方案中,確定幀號需要兩次存儲器讀取:最頂層的 10 位用于頁表的目錄中。如果每個條目使用 2 個字節,我們只需要 2KB 來存儲整個目錄。每個子表將指向物理幀(即,需要 4 個字節來存儲 20 位)。但是,對于只有微小內存需求的進程,我們只需要為低內存地址(用于堆和程序代碼)和高內存地址(用于棧)指定條目。每個子表是 1024 個條目×4 個字節,即每個子表 4KB。因此,我們的多級頁表的總內存開銷從 4MB(單級)縮減到 3 幀內存(12KB)!
頁表會使內存訪問速度變慢嗎? (什么是 TLB)
是的 - 很重要! (但是由于聰明的硬件,通常沒有...)與直接讀取或寫入內存相比。對于單頁表,我們的機器現在慢兩倍! (需要兩次內存訪問)對于兩級頁表,內存訪問速度現在是后者的三倍。 (需要三次內存訪問)
為了克服這種開銷,MMU 包括最近使用的虛擬頁面到幀查找的關聯緩存。此緩存稱為 TLB(“轉換后備緩沖區”)。每次需要將虛擬地址轉換為物理內存位置時,都會與頁表并行查詢 TLB。對于大多數程序的大多數內存訪問,TLB 很可能緩存了結果。但是,如果程序沒有良好的高速緩存一致性(例如從許多不同頁面的隨機存儲器位置讀取),則 TLB 將不具有結果高速緩存,并且現在 MMU 必須使用更慢的頁表來確定物理幀。

這可能是分割多級頁表的方式。
## 高級框架和頁面保護
## 框架可以在進程之間共享嗎?他們可以專業嗎?
是!除了存儲幀號之外,頁表還可用于存儲進程是寫入還是僅讀取特定幀。然后可以在多個進程之間安全地共享只讀幀。例如,可以在將代碼動態加載到進程內存中的所有進程之間共享 C 庫指令代碼。每個進程只能讀取該內存。這意味著如果您嘗試寫入內存中的只讀頁面,您將獲得`SEGFAULT`。這就是為什么有時內存訪問段錯誤,有時它們沒有,這一切都取決于你的硬件是否說你可以訪問。
此外,進程可以使用`mmap`系統調用與子進程共享頁面。 `mmap`是一個有趣的調用,因為它不是將每個虛擬地址綁定到物理幀,而是將其與其他東西聯系起來。其他東西可以是文件,GPU 單元或您可以想到的任何其他內存映射操作!寫入內存地址可能會寫入設備,或者寫入可能會被操作系統暫停,但這是一個非常強大的抽象,因為操作系統通常能夠執行優化(多個進程內存映射同一個文件可以有內核)創建一個映射)。
## 頁面表中還存儲了什么?為什么?
除了上面討論的只讀位和使用統計信息之外,通常至少存儲只讀,修改和執行信息。
## 什么是頁面錯誤?
頁面錯誤是指正在運行的程序試圖訪問未映射到物理內存的地址空間中的某個虛擬內存。頁面錯誤也會在其他情況下發生。
頁面錯誤有三種類型
**次要**如果頁面尚未映射,但它是有效地址。這可能是`sbrk(2)`要求的內存,但尚未寫入,這意味著操作系統可以在分配空間之前等待第一次寫入。操作系統只需創建頁面,將其加載到內存中,然后繼續操作。
**Major** 如果映射到頁面不在內存中但在磁盤上。這樣做是將頁面交換到內存并交換另一頁。如果這種情況經常發生,你的程序就會被稱為 _thrash_ MMU。
**無效**當您嘗試寫入不可寫存儲器地址或讀取不可讀存儲器地址時。 MMU 生成無效錯誤,操作系統通常會生成`SIGSEGV`意味著分段違規,這意味著您在可以寫入的段外編寫。
### 只讀位
只讀位將頁面標記為只讀。嘗試寫入頁面將導致頁面錯誤。然后頁面錯誤將由內核處理。只讀頁面的兩個示例包括在多個進程之間共享 c 運行時庫(為了安全起見,您不希望允許一個進程修改該庫);和 Copy-On-Write,其中復制頁面的成本可以延遲到第一次寫入發生。
### 骯臟的一點
[http://en.wikipedia.org/wiki/Page_table#Page_table_data](http://en.wikipedia.org/wiki/Page_table#Page_table_data)
> 臟位允許性能優化。磁盤上的頁面被分頁到物理內存,然后從中讀取,然后再次被分頁,不需要寫回磁盤,因為頁面沒有更改。但是,如果頁面在頁面被寫入之后被寫入,則將設置其臟位,指示必須將頁面寫回到后備存儲。此策略要求后備存儲在將頁面分頁到內存后保留頁面的副本。當不使用臟位時,后備存儲只需要與任何時刻所有頁面調出頁面的瞬時總大小一樣大。使用臟位時,物理內存和后備存儲中都會存在一些頁面。
### 執行位
執行位定義頁面中的字節是否可以作為 CPU 指令執行。通過禁用頁面,它可以防止惡意存儲在進程存儲器中的代碼(例如,通過棧溢出)被輕易執行。 (進一步閱讀: [http://en.wikipedia.org/wiki/NX_bit#Hardware_background](http://en.wikipedia.org/wiki/NX_bit#Hardware_background) )
### 了解更多
在[ [http://wiki.osdev.org/Paging](http://wiki.osdev.org/Paging) ]中討論了對 x86 平臺上的分頁和頁面位的越來越多的技術討論。
- UIUC CS241 系統編程中文講義
- 0. 簡介
- #Informal 詞匯表
- #Piazza:何時以及如何尋求幫助
- 編程技巧,第 1 部分
- 系統編程短篇小說和歌曲
- 1.學習 C
- C 編程,第 1 部分:簡介
- C 編程,第 2 部分:文本輸入和輸出
- C 編程,第 3 部分:常見問題
- C 編程,第 4 部分:字符串和結構
- C 編程,第 5 部分:調試
- C 編程,復習題
- 2.進程
- 進程,第 1 部分:簡介
- 分叉,第 1 部分:簡介
- 分叉,第 2 部分:Fork,Exec,等等
- 進程控制,第 1 部分:使用信號等待宏
- 進程復習題
- 3.內存和分配器
- 內存,第 1 部分:堆內存簡介
- 內存,第 2 部分:實現內存分配器
- 內存,第 3 部分:粉碎堆棧示例
- 內存復習題
- 4.介紹 Pthreads
- Pthreads,第 1 部分:簡介
- Pthreads,第 2 部分:實踐中的用法
- Pthreads,第 3 部分:并行問題(獎金)
- Pthread 復習題
- 5.同步
- 同步,第 1 部分:互斥鎖
- 同步,第 2 部分:計算信號量
- 同步,第 3 部分:使用互斥鎖和信號量
- 同步,第 4 部分:臨界區問題
- 同步,第 5 部分:條件變量
- 同步,第 6 部分:實現障礙
- 同步,第 7 部分:讀者編寫器問題
- 同步,第 8 部分:環形緩沖區示例
- 同步復習題
- 6.死鎖
- 死鎖,第 1 部分:資源分配圖
- 死鎖,第 2 部分:死鎖條件
- 死鎖,第 3 部分:餐飲哲學家
- 死鎖復習題
- 7.進程間通信&amp;調度
- 虛擬內存,第 1 部分:虛擬內存簡介
- 管道,第 1 部分:管道介紹
- 管道,第 2 部分:管道編程秘密
- 文件,第 1 部分:使用文件
- 調度,第 1 部分:調度過程
- 調度,第 2 部分:調度過程:算法
- IPC 復習題
- 8.網絡
- POSIX,第 1 部分:錯誤處理
- 網絡,第 1 部分:簡介
- 網絡,第 2 部分:使用 getaddrinfo
- 網絡,第 3 部分:構建一個簡單的 TCP 客戶端
- 網絡,第 4 部分:構建一個簡單的 TCP 服務器
- 網絡,第 5 部分:關閉端口,重用端口和其他技巧
- 網絡,第 6 部分:創建 UDP 服務器
- 網絡,第 7 部分:非阻塞 I O,select()和 epoll
- RPC,第 1 部分:遠程過程調用簡介
- 網絡復習題
- 9.文件系統
- 文件系統,第 1 部分:簡介
- 文件系統,第 2 部分:文件是 inode(其他一切只是數據...)
- 文件系統,第 3 部分:權限
- 文件系統,第 4 部分:使用目錄
- 文件系統,第 5 部分:虛擬文件系統
- 文件系統,第 6 部分:內存映射文件和共享內存
- 文件系統,第 7 部分:可擴展且可靠的文件系統
- 文件系統,第 8 部分:從 Android 設備中刪除預裝的惡意軟件
- 文件系統,第 9 部分:磁盤塊示例
- 文件系統復習題
- 10.信號
- 過程控制,第 1 部分:使用信號等待宏
- 信號,第 2 部分:待處理的信號和信號掩碼
- 信號,第 3 部分:提高信號
- 信號,第 4 部分:信號
- 信號復習題
- 考試練習題
- 考試主題
- C 編程:復習題
- 多線程編程:復習題
- 同步概念:復習題
- 記憶:復習題
- 管道:復習題
- 文件系統:復習題
- 網絡:復習題
- 信號:復習題
- 系統編程笑話