本文背景:
在編程中,很多Windows或C++的內存函數不知道有什麼區別,更別談有效使用;根本的原因是,沒有清楚的理解操作系統的內存管理機制,本文企圖通過簡單的總結描述,結合實例來闡明這個機制。
本文目的:
對Windows內存管理機制瞭解清楚,有效的利用C++內存函數管理和使用內存。
本文內容:
本文一共有六節,由於篇幅較多,故按節發表。其他章節請看本人博客的Windows內存管理及C++內存分配實例(二)(三)(四)(五)和(六)。
1. 進程地址空間
1.1地址空間
· 32|64位的系統|CPU
操作系統運行在硬件CPU上,32位操作系統運行於32位CPU上,64位操作系統運行於64位CPU上;目前沒有真正的64位CPU。
32位CPU一次只能操作32位二進制數;位數多CPU設計越複雜,軟件設計越簡單。
軟件的進程運行於32位系統上,其尋址位也是32位,能表示的空間是232=4G,範圍從0x0000 0000~0xFFFF FFFF。
· NULL指針分區
範圍:0x0000 0000~0x0000 FFFF
作用:保護內存非法訪問
例子:分配內存時,如果由於某種原因分配不成功,則返回空指針0x0000 0000;當用戶繼續使用比如改寫數據時,系統將因爲發生訪問違規而退出。
那麼,爲什麼需要那麼大的區域呢,一個地址值不就行了嗎?我在想,是不是因爲不讓8或16位的程序運行於32位的系統上呢?!因爲NULL分區剛好範圍是16的進程空間。
· 獨享用戶分區
範圍:0x0001 0000~0x7FFE FFFF
作用:進程只能讀取或訪問這個範圍的虛擬地址;超越這個範圍的行爲都會產生違規退出。
例子:
程序的二進制代碼中所用的地址大部分將在這個範圍,所有exe和dll文件都加載到這個。每個進程將近2G的空間是獨享的。
注意:如果在boot.ini上設置了/3G,這個區域的範圍從2G擴大爲3G:0x0001 0000~0xBFFE FFFF。
· 共享內核分區
範圍:0x8000 0000~0xFFFF FFFF
作用:這個空間是供操作系統內核代碼、設備驅動程序、設備I/O高速緩存、非頁面內存池的分配、進程目表和頁表等。
例子:
這段地址各進程是可以共享的。
注意:如果在boot.ini上設置了/3G,這個區域的範圍從2G縮小爲1G:0xC000 0000~0xFFFF FFFF。
通過以上分析,可以知道,如果系統有n個進程,它所需的虛擬空間是:2G*n+2G (內核只需2G的共享空間)。
1.2地址映射
· 區域
區域指的是上述地址空間中的一片連續地址。區域的大小必須是粒度(64k) 的整數倍,不是的話系統自動處理成整數倍。不同CPU粒度大小是不一樣的,大部分都是64K。
區域的狀態有:空閒、私有、映射、映像。
在你的應用程序中,申請空間的過程稱作保留(預訂),可以用VirtualAlloc;刪除空間的過程爲釋放,可以用VirtualFree。
在程序裏預訂了地址空間以後,你還不可以存取數據,因爲你還沒有付錢,沒有真實的RAM和它關聯。
這時候的區域狀態是私有;
默認情況下,區域狀態是空閒;
當exe或DLL文件被映射進了進程空間後,區域狀態變成映像;
當一般數據文件被映射進了進程空間後,區域狀態變成映射。
· 物理存儲器
Windows各系列支持的內存上限是不一樣的,從2G到64G不等。理論上32位CPU,硬件上只能支持4G內存的尋址;能支持超過4G的內存只能靠其他技術來彌補。順便提一下,Windows個人版只能支持最大2G內存,Intel使用Address Windows Extension (AWE) 技術使得尋址範圍爲236=64G。當然,也得操作系統配合。
內存分配的最小單位是4K或8K,一般來說,根據CPU不同而不同,後面你可以看到可以通過系統函數得到區域粒度和頁面粒度。
· 頁文件
頁文件是存在硬盤上的系統文件,它的大小可以在系統屬性裏面設置,它相當於物理內存,所以稱爲虛擬內存。事實上,它的大小是影響系統快慢的關鍵所在,如果物理內存不多的情況下。
每頁的大小和上述所說內存分配的最小單位是一樣的,通常是4K或8K。
· 訪問屬性
物理頁面的訪問屬性指的是對頁面進行的具體操作:可讀、可寫、可執行。CPU一般不支持可執行,它認爲可讀就是可執行。但是,操作系統提供這個可執行的權限。
PAGE_NOACCESS
PAGE_READONLY
PAGE_READWRITE
PAGE_EXECUTE
PAGE_EXECUTE_READ
PAGE_EXECUTE_READWRITE
這6個屬性很好理解,第一個是拒絕所有操作,最後一個是接受收有操作;
PAGE_WRITECOPY
PAGE_EXECUTE_WRITECOPY
這兩個屬性在運行同一個程序的多個實例時非常有用;它使得程序可以共享代碼段和數據段。一般情況下,多個進程只讀或執行頁面,如果要寫的話,將會Copy頁面到新的頁面。通過映射exe文件時設置這兩個屬性可以達到這個目的。
PAGE_NOCACHE
PAGE_WRITECOMBINE
這兩個是開發設備驅動的時候需要的。
PAGE_GUARD
當往頁面寫入一個字節時,應用程序會收到堆棧溢出通知,在線程堆棧時有用。
· 映射過程
進程地址空間的地址是虛擬地址,也就是說,當取到指令時,需要把虛擬地址轉化爲物理地址才能夠存取數據。這個工作通過頁目和頁表進行。
從圖中可以看出,頁目大小爲4K,其中每一項(32位)保存一個頁表的物理地址;每個頁表大小爲4K,其中每一項(32位)保存一個物理頁的物理地址,一共有1024個頁表。利用這4K+4K*1K=4.4M的空間可以表示進程的1024*1024* (一頁4K) =4G的地址空間。
進程空間中的32位地址如下:
高10位用來找到1024個頁目項中的一項,取出頁表的物理地址後,利用中10位來得到頁表項的值,根據這個值得到物理頁的地址,由於一頁有4K大小,利用低12位得到單元地址,這樣就可以訪問這個內存單元了。
每個進程都有自己的一個頁目和頁表,那麼,剛開始進程是怎麼找到頁目所在的物理頁呢?答案是CPU的CR3寄存器會保存當前進程的頁目物理地址。
當進程被創建時,同時需要創建頁目和頁表,一共需要4.4M。在進程的空間中,0xC030 0000~0xC030 0FFF是用來保存頁目的4k空間。0xC000 0000~0xC03F FFFF是用來保存頁表的4M空間。也就是說程序裏面訪問這些地址你是可以讀取頁目和頁表的具體值的(要工作在內核方式下)。有一點我不明白的是,頁表的空間包含了頁目的空間!
至於說,頁目和頁表是保存在物理內存還是頁文件中,我覺得,頁目比較常用,應該在物理內存的概率大點,頁表需要時再從頁文件導入物理內存中。
頁目項和頁表項是一個32位的值,當頁目項第0位爲1時,表明頁表已經在物理內存中;當頁表項第0位爲1時,表明訪問的數據已經在內存中。還有很多數據是否已經被改變,是否可讀寫等標誌。另外,當頁目項第7位爲1時,表明這是一個4M的頁面,這值已經是物理頁地址,用虛擬地址的低22位作爲偏移量。還有很多:數據是否已經被改變、是否可讀寫等標誌。
1.3 一個例子
· 編寫生成軟件程序exe
軟件描述如下:
Main ()
{
1:定義全局變量
2:處理函數邏輯(Load 所需DLL庫,調用方法處理邏輯)
3:定義並實現各種方法(方法含有局部變量)
4:程序結束
}
將程序編譯,生成exe文件,附帶所需的DLL庫。
· exe文件格式
exe文件有自己的格式,有若干節(section):.text用來放二進制代碼(exe或dll);.data用來放各種全局數據。
.text
指令1:move a, b
指令2:add a, b
…
.data
數據1:a=2
數據2:b=1
…
這些地址都是虛擬地址,也就是進程的地址空間。
· 運行exe程序
建立進程:運行這個exe程序時,系統會創建一個進程,建立進程控制塊PCB,生成進程頁目和頁表,放到PCB中。
數據對齊:數據的內存地址除以數據的大小,餘數爲0時說明數據是對齊的。現在的編譯器編譯時就考慮數據對齊的問題,生成exe文件後,數據基本上是對齊的,CPU運行時,寄存器有標誌標識CPU是否能夠自動對齊數據,如果遇到不能對齊的情況,或者通過兩次訪問內存,或者通知操作系統處理。
要注意的是,如果數據沒有對齊,CPU處理的效率是很低的。
文件映射:系統不會將整個exe文件和所有的DLL文件裝載進物理內存中,同時它也不會裝載進頁面文件中。相反,它會建立文件映射,也就是利用exe本身當作頁面文件。系統將部分二進制代碼裝載進內存,分配頁面給它。
假設分配了一個頁面,物理地址爲0x0232 FFF1。其中裝載的一個指令虛擬地址爲0x4000 1001=0100 0000 00 0000 0000 01 0000 0000 0001。一個頁面有4K,系統會將指令保存在低12位0x0001的地址處。同時,系統根據高10位0x0100找到頁目項,如果沒有關聯的頁表,系統會生成一個頁表,分配一個物理頁;然後,根據中10位0x0001找到表項,將物理地址0x0232 FFF1存進去。
執行過程:
執行時,當系統拿到一個虛擬地址,就根據頁目和頁表找到數據的地址,根據頁目上的值可以判斷頁表是在頁文件中還是在內存中;
如果在頁文件中,會將頁面導入內存,更新頁目項。讀取頁表項的值後,可以判斷數據頁文件中還是在物理內存中;如果在頁文件中,會導入到內存中,更新頁表項。最終,拿到了數據。
在分配物理頁的過程中,系統會根據內存分配的狀況適當淘汰暫時不用的頁面,如果頁面內容改變了(通過頁表項的標誌位),保存到頁文件中,系統會維護內存與頁文件的對應關係。
由於將exe文件當作內存映射文件,當需要改變數據,如更改全局變量的值時,利用Copy-On-Write的機制,重新生成頁文件,將結果保存在這個頁文件中,原來的頁文件還是需要被其他進程實例使用的。
在清楚了指令和數據是如何導入內存,如何找到它們的情況下,剩下的就是CPU不斷的取指令、運行、保存數據的過程了,當進程結束後,系統會清空之前的各種結構、釋放相關的物理內存和刪除頁文件。
其他章節請看本人博客的Windows內存管理及C++內存分配實例(二)(三)(四)(五)和(六)。
2. 內存狀態查詢函數
3. 內存管理機制--虛擬內存 (VM)
4. 內存管理機制--內存映射文件 (Map)
5. 內存管理機制--堆 (Heap)
6. 內存管理機制--堆棧 (Stack)