1. 程式人生 > >全面介紹Windows記憶體管理機制及C++記憶體分配例項(一):程序空間

全面介紹Windows記憶體管理機制及C++記憶體分配例項(一):程序空間

本文背景:

在程式設計中,很多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不斷的取指令、執行、儲存資料的過程了,當程序結束後,系統會清空之前的各種結構、釋放相關的實體記憶體和刪除頁檔案。