24|虚实结合:虚拟内存和物理内存
24|虛實結合:虛擬內存和物理內存
你好,我是LMOS。
上一課中學習了內存地址空間,我們搞清楚了內存地址與地址空間的本質。
今天我們開始學習虛擬內存與物理內存。其實虛擬內存也好,物理內存也罷,我們從儲存并索引數據的角度來看,內存的重要組成部分就兩個:一個是地址,另一個就是儲存字節單元,即能存放8個二進制位的容器。把兩者合起來,我們可以將內存理解為能索引到具體儲存字節單元的地址集合。
這節課我會帶你解決以下三個問題:
1.虛擬內存的本質是什么?
2.物理內存是什么,它的結構長什么樣?
3.虛擬內存如何與物理內存結合在一起,真正實現儲存數據的功能?
課程配套代碼你可以從 這里 下載。讓我們帶著上面的問題,正式開始今天的探索之旅吧!
虛擬內存
上節課我們了解了內存地址的產生方式,以及應用程序的鏈接過程,也知道了內存就是能索引到具體儲存單元的地址集合。但是程序中的地址能否索引到具體儲存單元呢?具體的儲存單元,又是如何分配的呢?下面我們用兩個問題來說明其中的原理。
第一個問題
我的第一個問題來了,應用程序中使用的地址是什么內存地址?是不是感覺情況有很多種,一時很難回答清楚?遇到這種狀況不要慌,我們只要動手寫一個簡單的程序就可以驗證。
好,我們立刻動手寫一寫,代碼如下:
#include "stdio.h" #include "stdlib.h" void func_a() {//定義地址:0x40000000int* p = (int*)0x40000000;printf("內存地址:%p\n", p);//向該地址寫入數據*p = 0xABABBABA;printf("內存地址:%p處的值:%x\n", p, *p);return; }int main() {func_a();return 0; }上述應用程序非常簡單,我們在main函數中調用函數func_a,而在函數func_a中,我們定義一個整型指針,C語言中指針就是內存地址,其地址值為0x40000000。
代碼我給你存到了課程相關的工程目錄中,你可以打開工程目錄make一下,就會自動編譯好。然后,你需要在終端下運行這個main.elf程序,首先會出現“內存地址:0x40000000”,接著會出現“段錯誤,程序異常退出”的提示。
出現了段錯誤提示,在你的預料之中么?我來解釋一下,為什么會出現這種情況,這是因為我們使用了一個沒有分配的地址。很顯然,如果一個地址真的能索引到內存,該地址就能訪問內存,除非這地址是個假地址,在內部需要某種機制進行轉換才能訪問內存。這個轉換機制可能需要一些表或者數據結構進行控制,并且這個控制權掌握在操作系統的手里。
由于操作系統管理內存的規則,是先分配后使用,所以,我們就猜想操作系統分配內存的時候,就會處理控制地址轉換的相關表和數據結構。接下來我們寫段代碼,來驗證一下猜想,如下所示:
#include "stdio.h" #include "stdlib.h" void func_b() {//分配內存,返回其地址int* p = (int*)malloc(sizeof(int));if(p){printf("內存地址:%p\n", p);//向該地址寫入數據*p = 0xABABBABA;printf("內存地址:%p處的值:%x\n", p, *p);}return; } int main() {func_b();return 0; }這次我們編譯運行,就會正確地輸出結果了。
其實malloc函數在內部最終會調用Linux內核的API函數,在該進程的虛擬地址空間中分配一小塊虛擬內存,返回其首地址。這個過程我用一幅圖來為你展示,如下所示:
由于代碼優化的原因,malloc函數并不是每次調用,都會導致Linux內核建立一個vm_area_struct數據結構。我們假定malloc函數導致Linux內核建立了一個vm_area_struct數據結構,該結構中有描述虛擬內存的開始地址、大小、屬性等相關字段,表示已經分配的虛擬內存空間。
許多個這樣的結構可以一起表示進程的虛擬地址空間分配情況。但是,這個從vm_area_struct數據結構中返回的地址,仍然是虛擬的、是假的,是不能索引到內存單元的,直到訪問該地址時,會發生另一個故事,如下圖所示:
上圖中CPU拿著一個虛擬地址訪問內存,首先會經過MMU,對于調用malloc函數的情況是該虛擬地址沒有映射到物理內存,所以會通知CPU該地址禁止訪問。
上圖中1到4個步驟為硬件自動完成的,然后CPU中斷到Linux內核地址錯誤處理程序,軟件開始工作,也就是說Linux內核會對照著當前進程的虛擬地址空間,去查找對應的vm_area_struct數據結構,找不到就證明虛擬地址未分配,直接結束,進程會發出段錯誤;若是找到了,則證明虛擬地址已經分配,接著會分配物理內存,建立虛擬地址到物理地址的映射關系,接著程序就可以繼續運行了。
當然了,實際情況比圖中的復雜,這里我們只是要理清楚malloc函數的邏輯,并且明確malloc是返回的虛擬內存地址就可以了。
第二個問題
我們要想清楚的第二個問題就是,直接使用物理內存地址,會出現什么后果?我們來看一個程序,下面這段代碼是一個簡單版的memset函數。
void mymemset(void* start, char val, int size) {char* buf = (char*)start;for(int i = 0; i < size; i++){buf[i] = val;}return; }我們提出一個假設:這個函數被不同的應用程序調用,且使用的地址就是物理地址,能直接訪問物理內存單元。
你可以想一想,如果假設成立,惡果就是一個程序可以改變另一個程序的內存,甚至是全部的內存。想想吧!這是何等可怕。通過這個例子,我們發現物理地址不能有效地隔離內存,達到保護內存的結果。
想要隔離內存,就需要依賴虛擬內存這個東西。我畫了一幅圖,帶你總結一下虛擬內存的本質,如下所示:
由上圖可知,我們各種應用都可以擁有從0到最大虛擬地址的完整的虛擬內存空間,并且可以任意使用這個虛擬內存空間。每個應用,都認為自己擁有整個內存,這一點可以從所有的應用程序使用相同的鏈接腳本進行鏈接得到佐證。各個應用程序調用malloc函數,可能得到相同地址,是另一個佐證。
我們現在終于知道了,虛擬地址真的只是一個整數,一系列的這種整數集合,就構成了虛擬內存空間。這個整數能索引一個字節的虛擬內存單元,但這個虛擬內存單元不會對應到真正的物理設備,因此它雖然可以獨立存在,但卻需要下層的物理內存作為支撐,才能實現訪問和儲存數據。
物理內存
上一課中,我們了解到物理地址空間是CPU地址線位寬所能表示最大整數集合,只是一個地址,它能索引物理設備,或者什么都不索引,這里的物理設備中就包括了物理內存。
下面我們來看看真實的內存長什么樣,如下所示:
從上圖可以看到,在 PCB 板上有內存顆粒芯片,主要是用來存放數據的。SPD 芯片用于存放內存自身的容量、頻率、廠商等信息。還有最顯眼的金手指,用于連接數據總線和地址總線、電源等。
其實內存應該叫 DRAM,即動態隨機存儲器。內存儲存顆粒芯片中的存儲單元是由電容和相關元件做成的,電容存儲電荷的多、少代表數字信號 0 和 1。而隨著時間的流逝,電容存在漏電現象,就會引起電荷不足的情況,導致存儲單元的數據出錯。所以,DRAM 需要周期性刷新,以保持電荷狀態。
DRAM 結構比較簡單且集成度很高,通常用于制造內存條中的儲存顆粒芯片。我們無需過多關注內存硬件層面的技術規格標準,這里重點需要關注的是, 邏輯上內存和硬件系統的連接方式和結構。
我還是畫幅圖來說明吧,這樣方便你建立直觀印象,如下圖所示:
我們假定從物理地址0開始,索引的是物理內存,CPU發出的地址是虛擬地址,經由MMU轉換變成物理地址,物理地址經由地址譯碼單元就會對應到具體的內存字節儲存單元。一個字節單元能儲存8個二進制位,即一個地址能對應到8個二進制位。
你可以通過dmsg命令,查看你物理機上的情況。在我的x86機器里,情況如下圖所示:
從圖里我們可以看到,usable類型的物理地址區間,對應的是DRAM,即內存。其它的則是保留的或者硬件設備的地址空間,這些空間程序是不能當作內存來使用的。
講到這里,我們就明白了,邏輯上物理內存相當于幾個地址上不連續的字節數組,始終有一個物理地址能索引到其中一個字節。
虛實結合
提出虛擬內存這個概念,一是為了讓應用認為自己享有完整的地址空間,擁有整個內存的使用權。二是要對物理內存進行保護,即使各個應用程序都存放在物理內存之中,也不能隨意訪問自己的物理內存,更不能侵犯別的應用程序所占用的物理內存,不然就會出現互相改寫對方內存的情況,一旦出現這樣的情況后果就嚴重了,任何應用程序都不能正常運行了。
那接下來要考慮的問題就是,虛擬內存跟物理內存要如何對應起來?
虛擬內存必須要落實到物理內存才能真正完成工作,最簡單的方案是讓虛擬地址能夠索引到物理內存單元,但是虛擬地址和物理地址顯然不能一一對應,如果那樣的話,虛擬地址等于物理地址且不受控制,這樣虛擬地址就沒有任何意義了。
因此,我們需要在虛擬地址空間與物理地址空間之間加一個機構,這個機構相當于一個函數:p=f(v) 。對這函數傳入一個虛擬地址,它就能返回一個物理地址。該函數有自己的計算方法,對于沒法計算的地址或者沒有權限的地址,還能返回一個禁止訪問。
這個函數用硬件實現出來,就是CPU中的MMU,即內存管理單元。CPU發出的虛擬地址首先經過MMU,MMU內部計算得出物理地址,最后用物理地址去訪問內存。MMU的結構如下圖所示:
上圖中,展示了CPU發出的虛擬地址經過MMU轉換出物理地址,進而訪問內存的過程,但我們并沒有弄清楚MMU是使用什么方法進行轉換的,所以下面我們繼續探討MMU的地址轉換過程。
你不妨想一想,把一個數據轉換成另一個數據,最簡單的方案是什么?當然是建立一個對應表格,對照表格進行查詢就行了。MMU也是使用一個地址轉換表,但是它做很多優化和折中處理。不做任何折中處理的話,這種方案是無法實施的。
你可以想象一下32位的地址空間,有4G個虛擬地址和4G個物理地址。在這種情況下,每8個字節存放兩個地址數據,想要裝下所有的地址,這個表有多大?應該放在哪里?查詢代價有多大?所以這個方案直接pass掉。
我們現在來看看,通常情況下MMU是如何解決這個問題的,一共有三個關鍵環節。
首先,MMU對虛擬地址空間和物理地址空間進行 分頁處理,一個頁大小可以是4KB、16KB、2MB、4MB、1GB不等。這是為了增加地址的粒度,避免采用每個字節一個地址,現在一頁一個地址,地址數量就會大大減少,從而減少轉換表的大小。
其次,MMU采用的轉換表也稱為頁表,其中只會對應物理頁地址,不會儲存虛擬地址,而是 將虛擬地址作為頁表索引,這進一步縮小了頁表的大小。
最后MMU 對頁表本身進行了拆分,變成了多級頁表。假如不分級,4GB內存空間 ,按照4KB大小分頁,有1M個頁表項,每個頁表項只占用4個字節,也需要4MB空間。如果頁表分級,在建立頁表時就可以按需建立頁表,而不是一次建立4MB大小的頁表。
我們一起來畫一幅圖來描述一下這個過程,如下所示:
對照圖片我們可以看到,虛擬內存頁和物理內存頁是同等大小的,都為4KB,各級頁表占用的空間也是一個頁,即為4KB。MMU把虛擬地址分為5個位段,各位段的位數根據實際情況有所不同,按照這些位段的數據來索引各級頁表中的項,一級一級往下查找,直到頁表項,最后用頁表項中的地址加頁內偏移,就得到了物理地址。
我再畫一幅圖,為你描述這一過程。
看到這幅圖,我們就清楚了MMU用虛擬地址轉換物理地址的過程。如果轉換成功就可以直接訪問內存了;但如果轉換失敗,MMU就會通知CPU,地址轉換失敗,讓CPU產生一個異常中斷,進而通知操作系統內核,讓操作系統內核來處理這個異常,就像malloc分配內存的過程那樣。
我們已經知道了虛擬地址如何轉換成物理地址,但是如果只是按部就班地轉換可不行,別忘了,還需要對物理內存進行保護。這個保護物理內存的問題的關鍵就是,想清楚一個虛擬地址在什么情況下能被轉換成物理地址。
這就要說到MMU是如何控制轉換動作的。要進行控制就需要相關的控制信息,聰明如你,大概已經猜到了,控制信息就放在頁表項中,MMU在轉換過程中首先就會查看那些信息,以此作出判斷。
下面我們看一下控制信息的格式,如下所示:
從上圖中可以看到,頁表項中的低12位為屬性位段,這里保存一個物理內存頁面的讀寫、執行、存在的相關權限,還有頁面是否存在、可不可以緩存,是否已經訪問或者寫入,大小等信息。這些信息統統編碼在12個二進制位中。
為什么表示各種頁面地址的頁表項,能讓出12位用于編碼這些信息呢?這是因為一個頁面最小也是4KB且與4KB對齊,那么頁面開始地址的低12位永遠為0,所以可以挪為它用。
到這里,我們就已經搞清楚虛擬地址如何轉換成物理地址,并且知道了MMU如何控制轉換過程,恭喜你解鎖了虛實結合的思路和過程。
現在你可能隱約感覺到,只要操作系統牢牢控制頁表數據,就能實現對內存的完全控制和保護,使得各個應用程序在自己的虛擬地址空間中安全地運行,不被打擾,也不能打擾別人。每個應用程序都有相同的虛擬內存,但卻占用著不同的物理內存。
重點回顧
今天的課程就要結束了,下面我們來回顧一下今天的內容。
首先我們從兩個實際問題出發,研究了虛擬內存的本質。虛擬內存的應用,一是為了保護內存,二是為了限制訪問內存。讓應用程序擁有獨立的地址空間,誤以為自己能享用全部的內存。
接著我們分析了物理內存,了解了DRAM的特性和結構,因為DRAM就是我們常說的內存設備。這里你重點要關注的是內存的邏輯結構和系統連接方式。
最后我們討論了虛實結合究竟是怎么實現的。硬件工程設計了MMU,讓它把虛擬內存地址通過頁表中的信息轉換成物理地址,并控制轉換過程。如果轉換失敗就會通知CPU,然后CPU產生地址異常中斷,最后由操作系統處理這個異常。操作系統將會通過修改頁表的數據來修復這個問題,進而完全控制內存的訪問。
我畫了一張導圖梳理這節課內容,供你參考。
應用程序的虛擬地址空間里還有更多奧秘,我會在下節課繼續為你展開,敬請期待。
思考題
請問頁表數據究竟放在什么地方呢?
歡迎你在留言區跟我交流互動,說說你對虛實結合的認識。如果覺得這節課還不錯,也推薦你把它分享給身邊的朋友。
總結
以上是生活随笔為你收集整理的24|虚实结合:虚拟内存和物理内存的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: EA-IRMS操作(以MAT253 pl
- 下一篇: 广告与公关 广告主与媒体