Wi n d o w s提供了3種進行記憶體管理的方法,它們是:
• 虛擬記憶體,最適合用來管理大型物件或結構數組。
• 記憶體對應檔,最適合用來管理大型資料流(通常來自檔案)以及在單個電腦上啟動並執行多個進程之間共用資料。
• 記憶體堆棧,最適合用來管理大量的小對象。
虛擬記憶體的狀態
Wi n d o w s函數G l o b a l M e m o r y S t a t u s可用於檢索關於當前記憶體狀態的動態資訊:
VOID GlobalMemoryStatus(LPMEMORYSTATUS pmst);
如果希望應用程式在記憶體大於4 G B的電腦上運行,或者合計分頁檔的大小大於4 G B,那麼可以使用新的G l o b a l M e m o r y S t a t u s E x函數:
BOOL GlobalMemoryStatusEx(LPMEMORYSTATUSEX pmst);
確定地址空間的狀態
Wi n d o w s提供了一個函數,可以用來查詢地址空間中記憶體位址的某些資訊(如大小,儲存空間類型和保護屬性等)。
這個函數稱為Vi r t u a l Q u e r y:
DWORD VirtualQuery(
LPCVOID pvAddress,
PMEMORY_BASIC_INFORMATION pmbi,
DWORD dwLength);
Wi n d o w s還提供了另一個函數,它使一個進程能夠查詢另一個進程的記憶體資訊:
DWORD VirtualQueryEx(
HANDLE hProcess,
LPCVOID pvAddress,
PMEMORY_BASIC_INFORMATION pmbi,
DWORD dwLength);
這兩個函數基本相同,差別在於使用Vi r t u a l Q u e r y E x時,可以傳遞你想要查詢的地址空間資訊的進程的控制代碼。偵錯工具和其他公用程式使用這個函數最多,幾乎所有的應用程式都只需要調用Vi r t u a l Q u e r y函數。
為了獲得完整的記憶體資訊,我建立了一個函數,即V M Q u e r y:
BOOL VMQuery(
HANDLE hProcess,
PVOID pvAddress,
PVMQUERY pVMQ);
系統資訊
許多作業系統的值是根據主機而定的,比如頁面的大小,分配粒度的大小等。這些值決不應該用硬式編碼形式放入你的原始碼。相反,你始終都應該在進程初始化的時候檢索這些值,並在你的原始碼中使用檢索到的值。G e t S y s t e m I n f o函數將用於檢索與主機相關的值:
VOID GetSystemInfo(LPSYSTEM_INFO psinf);
必須傳遞S Y S T E M _ I N F O結構的地址給這個函數。這個函數將初始化所有的結構成員然後返回。下面是S Y S T E M _ I N F O資料結構的樣子。
typedef struct _SYSTEM_INFO
{
union
{
DWORD dwOemId; // Obsolete, do not use
struct
{
WORD wProcessorArchitecture;
WORD wReserved;
} ;
} ;
DWORD dwPageSize;
LPVOID lpMinimumApplicationAddress;
LPVOID lpMaximumApplicationAddress;
DWORD_PTR dwActiveProcessorMask;
DWORD dwNumberOfProcessors;
DWORD dwProcessorType;
DWORD dwAllocationGranularity;
WORD wProcessorLevel;
WORD wProcessorRevision;
} SYSTEM_INFO, * LPSYSTEM_INFO;
當系統引導時,它要確定這些成員的值是什麼。對於任何既定的系統來說,這些值總是相同的,因此決不需要為任何既定的進程多次調用該函數。
表14-1 與記憶體有關的成員函數
成員名 |
描述 |
d w P a g e S i z e |
用於顯示C P U的頁面大小。在x86 CPU上,這個值是4 0 9 6位元組。在Alpha CPU 上,這個值是8 1 9 2位元組。在I A - 6 4上,這個值是8 1 9 2位元組 |
l p M i n i m u m A p p l i c a t i o n A d d r e s s |
用於給出每個進程的可用地址空間的最小記憶體位址。在Windows 98上,這個值是4 194 304,或0 x 0 0 4 0 0 0 0 0,因為每個進程的地址空間中下面的4 M B是不能使用的。在Windows 2000上,這個值是65 536或0 x 0 0 0 1 0 0 0 0,因為每個進程的地址空間中開頭的6 4 K B總是閒置 |
l p M a x i m u m A p p l i c a t i o n A d d r e s s |
用於給出每個進程的可用地址空間的最大記憶體位址。在Windows 98 上,這個地址是2 147 483 647或0 x 7 F F F F F F F,因為共用記憶體對應檔地區和共用作業系統程式碼封裝含在上面的2 GB分區中。在Windows 2000上,這個地址是核心方式記憶體開始的地址,它不足6 4 K B |
d w A l l o c a t i o n G r a n u l a r i t y |
顯示保留的地址空間地區的分配粒度。截止到撰寫本書時,在所有Wi n d o w s平台上,這個值都是65536 |
該結構的其他成員與記憶體管理毫無關係,為了完整起見,下面也對它們進行了介紹(見表1 4 - 2)。
表14-2 與記憶體無關的成員函數
成員名 |
描述 |
d w O e m I d |
已作廢,不引用 |
W R e d e r v e d |
保留供將來使用,不引用 |
d w N u m b e r O f P r o c e s s o r s |
用於指明電腦中的C P U數目 |
d w A c t i v e P r o c e s s o r M a s k |
一個位屏蔽,用於指明哪個C P U是活動的(允許運行線程) |
d w P r o c e s s o r Ty p e |
只用於Windows 98,不用於Windows 2000,用於指明處理器的類型,如Intel 386、4 8 6或P e n t i u m |
w P r o c e s s o r A r c h i t e c t u r e |
只用於Windows 2000,不用於Windows 98,用於指明處理的結構,如I n t e l、A l p h a、Intel 64位或Alpha 64位 |
w P r o c e s s o r L e v e l |
只用於Windows 2000,不用於Windows 98,用於進一步細分處理器的結構,如用於設定Intel Pentium Pro或Pentium II |
w P r o c e s s o r R e v i s i o n |
只用於Windows 2000 ,不用於Windows 98,用於進一步細分處理器的層級 |
在地址空間中保留一個地區
通過調用Vi r t u a l A l l o c函數,可以在進程的地址空間中保留一個地區:
PVOID VirtualAlloc( PVOID pvAddress, SIZE_T dwSize, DWORD fdwAllocationType, DWORD fdwProtect);
第一個參數p v A d d r e s s包含一個記憶體位址,用於設定想讓系統將地址空間保留在什麼地方。在大多數情況下,你為該參數傳遞M U L L。它告訴Vi r t u a l A l l o c,儲存著一個空閑地址地區的記錄的系統應該將地區保留在它認為合適的任何地方。系統可以從進程的地址空間的任何位置來保留一個地區,因為不能保證系統可以從地址空間的底部向上或者從上面向底部來分配各個地區。可以使用M E M _ TO P _ D O W N標誌來說明該分配方式。如果Vi r t u a l A l l o c函數能夠滿足你的要求,那麼它就返回一個值,指明保留地區的基地址。如果傳遞一個特定的地址作為Vi r t u a l A l l o c的p v A d d r e s s 參數,那麼該傳回值與傳遞給Vi r t u a l A l l o c的值相同,並被圓整為(如果需要的話) 6 4 K B邊界值。
Vi r t u a l A l l o c函數的第二個參數是d w S i z e,用於設定想保留的地區的大小(以位元組為計量單位)。由於系統保留的地區始終必須是C P U頁面大小的倍數,因此,如果試圖保留一個跨越6 2 K B的地區,結果就會在使用4 KB、8 KB或16 KB頁面的電腦上產生一個跨越6 4 K B的地區。
Vi r t u a l A l l o c函數的第三個參數是f d w A l l o c a t i o n Ty p e,它能夠告訴系統你想保留一個地區還是提交實體儲存體器(這樣的區分是必要的,因為Vi r t u a l A l l o c函數也可以用來提交實體儲存體器)。若要保留一個地址空間地區,必須傳遞M E M _ R E S E RV E標識符作為F d w A l l o c a t i o n Ty p e參數的值。
最後一個參數是f d w P r o t e c t,用於指明應該賦予該地址空間地區的保護屬性。與該地區相關聯的保護屬性對映射到該地區的已提交記憶體沒有影響。無論賦予地區的保護屬性是什麼,如果沒有提交任何實體儲存體器,那麼訪問該範圍中的記憶體位址的任何企圖都將導致該線程引發一個訪問違規。
在保留地區中的提交儲存空間
當保留一個地區後,必須將實體儲存體器提交給該地區,然後才能訪問該地區中包含的記憶體位址。系統從它的頁檔案中將已提交的實體儲存體器分配給一個地區。實體儲存體器總是按頁面邊界和頁面大小的塊來提交的。
若要提交實體儲存體器,必須再次調用Vi r t u a l A l l o c函數。不過這次為f d w A l l o c a t i o n Ty p e參數傳遞的是M E M _ C O M M I T標誌,而不是M E M _ R E S E RV E標誌。傳遞的頁面保護屬性通常與調用Vi r t u a l A l l o c來保留地區時使用的保護屬性相同(大多數情況下是PA G E _ R E A D W R I T E),不過也可以設定一個不同的保護屬性。
在已保留的地區中,你必須告訴Vi r t u a l A l l o c函數,你想將實體儲存體器提交到何處,以及要提交多少實體儲存體器。為了做到這一點,可以在p v A d d r e s s參數中設定你需要的記憶體位址,並在d w S i z e參數中設定實體儲存體器的數量(以位元組為計量單位)。注意,不必立即將實體儲存體器提交給整個地區。
同時進列區域的保留和記憶體的提交
有時你可能想要在保留地區的同時,將實體儲存體器提交給它。只需要一次調用Vi r t u a l A l l o c函數就能進行這樣的操作,如下所示:
PVOID pvMem = VirtualAlloc(NULL, 99 * 1024 , MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
這個函數調用請求保留一個99 KB的地區,並且將99 KB的實體儲存體器提交給它。當系統處理這個函數調用時,它首先要搜尋你的進程的地址空間,找出未保留的地址空間中一個地址連續的地區,它必須足夠大,能夠存放100 KB(在4 KB頁面的電腦上)或104 KB(在8 KB頁面的電腦上)。
系統之所以要搜尋地址空間,原因是已將p v A d d r e s s參數設定為N U L L。如果為p v A d d r e s s設定了記憶體位址,系統就要查看在該記憶體位址上是否存在足夠大的未保留地址空間。如果系統找不到足夠大的未保留地址空間,Vi r t u a l A l l o c將返回N U L L,
如果能夠保留一個合適的地區,系統就將實體儲存體器提交給整個地區。無論是該地區還是提交的記憶體,都將被賦予PA G E _ R E A D W R I T E保護屬性。
最後需要說明的是,Vi r t u a l A l l o c將返回保留地區和提交地區的虛擬位址,然後該虛擬位址被儲存在p v M e m變數中。如果系統無法找到足夠大的地址空間,或者不能提交該實體儲存體器,Vi r t u a l A l l o c將返回N U L L。
回收虛擬記憶體和釋放地址空間地區
若要回收映射到一個地區的實體儲存體器,或者釋放這個地址空間地區,可調用Vi r t u a l F r e e函數:
BOOL VirtualFree( LPVOID pvAddress, SIZE_T dwSize, DWORD fdwFreeType);
改變保護屬性
雖然實踐中很少這樣做,但是可以改變已經提交的實體儲存體器的一個或多個頁面的保護屬性。
若要改變記憶體頁面的保護屬性,可以調用Vi r t u a l P r o t e c t函數:
BOOL VirtualProtect(
PVOID pvAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD pflOldProtect);
當然,保護屬性是與記憶體的整個頁面相關聯的,而不是賦予記憶體的各個位元組的。因此,如果要使用下面的代碼來調用4 KB 頁面的電腦上的Vi r t u a l P r o t e c t函數,其結果是把PA G E _ N O A C C E S S保護屬性賦予記憶體的兩個頁面:
VirtualProtect(pvRgnBase + (3 * 1024), 2 * 1024, PAGE_NOACCESS, &flOldProtect);
清除實體儲存體器的內容
為了說明記憶體的內容已經被清除,我們必須對系統的R A M提出大量的使用需求。若要進行這項操作,可以分3步來進行:
1) 調用G l o b a l M e m o r y S t a t u s函數,擷取電腦中R A M的總容量。
2) 調用Vi r t u a l A l l o c函數,提交該數量的記憶體。這項操作的運行速度非常快,因為在進程試圖訪問頁面之前,系統實際上並不為該記憶體配置R A M。
3) 調用Z e r o M e m o r y函數,使新提交的頁面可以被訪問。這將給系統的R A M帶來沉重的負擔,導致當前正在R A M中的某些頁面被寫入頁檔案。
如果使用者指明該資料將在以後被訪問,那麼該資料將不被清除,並且在以後訪問該資料時將資料轉入R A M。但是,如果使用者指明以後將不再訪問該資料,那麼資料將被清除,並且系統不把資料寫入頁檔案,這樣就可以提高應用程式的運行效能。