天天看點

Windows核心程式設計 第十五章 在應用程式中使用虛拟記憶體

第1 5章 在應用程式中使用虛拟記憶體

提供了3種進行記憶體管理的方法,它們是:

    • 虛拟記憶體,最适合用來管理大型對象或結構數組。

    • 記憶體映射檔案,最适合用來管理大型資料流(通常來自檔案)以及在單個計算機上運作的多個程序之間共享資料。

    • 記憶體堆棧,最适合用來管理大量的小對象。

本章将要介紹第一種方法,即虛拟記憶體。記憶體映射檔案和堆棧分别在第 1 7章和第1 8章介紹。用于管理虛拟記憶體的函數可以用來直接保留一個位址空間區域,将實體存儲器(來自頁檔案)送出給該區域,并且可以設定你自己的保護屬性。

5.1 在位址空間中保留一個區域

Vi r t u a l A l l o c函數,可以在程序的位址空間中保留一個區域:

Windows核心程式設計 第十五章 在應用程式中使用虛拟記憶體

p v A d d r e s s包含一個記憶體位址,用于設定想讓系統将位址空間保留在什麼地方。在大多數情況下,你為該參數傳遞NU L L。它告訴Vi r t u a l A l l o c,儲存着一個空閑位址區域的記錄的系統應該将區域保留在它認為合适的任何地方。系統可以從程序的位址空間的任何位置來保留一個區域,因為不能保證系統可以從位址空間的底部向上或者從上面向底部來配置設定各個區域。可以使用M E M _ TO P _ D O W N标志來說明該配置設定方式。這個标志将在本章的後面加以介紹。

    對大多數程式員來說,能夠選擇一個特定的記憶體位址,并在該位址保留一個區域,這是個非同尋常的想法。當你在過去配置設定記憶體時,作業系統隻是尋找一個其大小足以滿足需要的記憶體塊,并配置設定該記憶體塊,然後傳回它的位址。但是,由于每個程序有它自己的位址空間,是以可以設定一個基本記憶體位址,在這個位址上讓作業系統保留位址空間區域。

50 MB開始的區域保留在程序的位址空間中。這時,可以傳遞52 428 800(5 0 × 1 0 2 4 × 1 0 2 4)作為p v A d d r e s s參數。如果該記憶體位址有一個足夠大的空閑區域滿足你的要求,那麼系統就保留這個區域并傳回。如果在特定的位址上不存在空閑區域,或者如果空閑區域不夠大,那麼系統就不能滿足你的要求,Vi r t u a l A l l o c函數傳回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。

1 3章講過,位址空間區域總是按照配置設定粒度的邊界來保留的(迄今為止在所有的Wi n d o w s環境下均是6 4 K B )。是以,如果試圖在程序位址空間中保留一個從19 668 992(300 × 65 536 + 8192)這個位址開始的區域,系統就會将這個位址圓整為 6 4 K B的倍數,然後保留從19 660 800(3 0 0× 65 536)這個位址開始的區域。

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邊界值。

函數的第二個參數是d w S i z e,用于設定想保留的區域的大小(以位元組為計量機關)。由于系統保留的區域始終必須是 C P U頁面大小的倍數,是以,如果試圖保留一個跨越6 2 K B的區域,結果就會在使用 4 KB、8 KB或16 KB頁面的計算機上産生一個跨越 6 4 K B的區域。

函數的第三個參數是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參數的值。

p v A d d r e s s參數和f d w A l l o c a t i o n Ty p e參數傳遞 N U L L,還必須逐位使用 O R将M E M _ TO P _ D O W N标志和M E M _ R E S E RV E标志連接配接起來。

Windows 98下,M E M _ TO P _ D O W N标志将被忽略。

f d w P r o t e c t,用于指明應該賦予該位址空間區域的保護屬性。與該區域相關聯的保護屬性對映射到該區域的已送出記憶體沒有影響。無論賦予區域的保護屬性是什麼,如果沒有送出任何實體存儲器,那麼通路該範圍中的記憶體位址的任何企圖都将導緻該線程引發一個通路違規。

PA G E _ R E A D W R I T E(這是最常用的保護屬性),那麼應該用PA G E _ R E A D W R I T E保護屬性來保留該區域。當區域的保護屬性與已送出記憶體的保護屬性相比對時,系統儲存的内部記錄的運作效率最高。

PA G E _ N O A C C E S S、PA G E _ R E A D W R I T E、PA G E _ R E A D O N LY、PA G E _ E X E C U T E、PA G E _ E X E C U T E _ R E A D或PA G E _ E X E C U T E _R E A D W R I T E。但是,既不能設定 PA G E _ W R I T E C O P Y屬性,也不能設定PA G E _ E X E C U T E _W R I T E C O P Y屬性。如果設定了這些屬性,Vi r t u a l A l l o c函數将不保留該區域,并且傳回N U L L。另外,當保留位址空間區域時,不能使用保護屬性标志 PA G E _ G U A R D,PA G E _ N O C A C H E或PA G E _ W R I T E C O M B I N E,這些标志隻能用于已送出的記憶體。

注意 Windows 98隻支援PA G E _ N O A C C E S S、PA G E _ R E A D O N LY和PA G E _ R E A D W R I T E保護屬性。如果試圖保留使用 PA G E _ E X E C U T E或PA G E _ E X E C U T E _ R E A D兩個保護屬性的區域,将會産生一個帶有 PA G E _ R E A D O N LY保護屬性的區域。同樣,如果保留一個使用 PA G E _ E X E C U T E _ R E A D W R I T E保護屬性的區域,就會産生一個帶有PA G E _ R E A D W R I T E保護屬性的區域。

15.2 在保留區域中的送出存儲器

    當保留一個區域後,必須将實體存儲器送出給該區域,然後才能通路該區域中包含的記憶體位址。系統從它的頁檔案中将已送出的實體存儲器配置設定給一個區域。實體存儲器總是按頁面邊界和頁面大小的塊來送出的。

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參數中設定實體存儲器的數量(以位元組為計量機關)。注意,不必立即将實體存儲器送出給整個區域。

x86 CPU上運作的,該應用程式保留了一個從位址 5 242 880開始的512 KB的區域。你想讓應用程式将實體存儲器送出給已保留區域的6 KB部分,從2 KB的地方開始,直到已保留區域的位址空間。為此,可以調用帶有M E M _ C O M M I T标志的Vi r t u a l A l l o c函數,如下所示:

Windows核心程式設計 第十五章 在應用程式中使用虛拟記憶體

8 KB的實體存儲器,位址範圍從5 242 880到5 251 071 (5 242880 + 8 KB  - 1位元組)。這兩個送出的頁面都擁有PA G E _ R E A D W R I T E保護屬性。保護屬性隻以整個頁面為機關來賦予。同一個記憶體頁面的不同部分不能使用不同的保護屬性。然而,區域中的一個頁面可以使用一種保護屬性(比如 PA G E _ R E A D W R I T E),而同一個區域中的另一個頁面可以使用不同的保護屬性(比如PA G E _ R E A D O N LY)。

15.3 同時進行區域的保留和記憶體的送出

有時你可能想要在保留區域的同時,将實體存儲器送出給它。隻需要一次調用 Vi r t u a l A l l o c函數就能進行這樣的操作,如下所示:

Windows核心程式設計 第十五章 在應用程式中使用虛拟記憶體

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。

p v A d d r e s s參數傳遞給 Vi r t u a l A l l o c當然是可能的。否則就必須用 O R将M E M _ TO P _ D O W N 标志與f d w A l l o c a t i o n Ty p e參數連接配接起來,并為p v A d d r e s s參數傳遞N U L L,讓系統在程序的位址空間的頂部標明一個适當的區域。

15.4 何時送出實體存儲器

2 0 0行 x 256列。對于每一個單元格,都需要一個C E L L D ATA結構來描述單元格的内容。若要處理這種二維單元格矩陣,最容易的方法是在應用程式中聲明下面的變量:

Windows核心程式設計 第十五章 在應用程式中使用虛拟記憶體

C E L L D ATA結構的大小是1 2 8位元組,那麼這個二維矩陣将需要6 553 600(200 x 256 x1 2 8)個位元組的實體存儲器。對于電子表格來說,如果直接用頁檔案來配置設定實體存儲器,那麼這是個不小的數目了,尤其是考慮到大多數使用者隻是将資訊放入少數的單元格中,而大部分單元格卻空閑不用,是以顯得有些浪費。記憶體的使用率非常低。

C E L L D ATA結構。由于電子表格中的大多數單元格都是不用的,是以這種方法可以節省大量的記憶體。但是這種方法使得你很難獲得單元格的内容。如果想知道第5行第1 0列的單元格的内容,必須周遊連結表,才能找到需要的單元格,是以使用連結表方法比明确聲明的矩陣方法速度要慢。

    虛拟記憶體為我們提供了一種兼顧預先聲明二維矩陣和實作連結表的兩全其美的方法。運用虛拟記憶體,既可以使用已聲明的矩陣技術進行快速而友善的通路,又可以利用連結表技術大大節省記憶體的使用量。

    如果想利用虛拟記憶體技術的優點,你的程式必須按照下列步驟來編寫:

保留一個足夠大的位址空間區域,用來存放 C E L L D ATA結構的整個數組。保留一個根本不使用任何實體存儲器的區域。

當使用者将資料輸入一個單元格時,找出 C E L L D ATA結構應該進入的保留區域中的記憶體位址。當然,這時尚未有任何實體存儲器被映射到該位址,是以,通路該位址的記憶體的任何企圖都會引發通路違規。

就C E L L D ATA結構來說,隻将足夠的實體存儲器送出給第二步中找到的記憶體位址(你可以告訴系統将實體存儲器送出給保留區域的特定部分,這個區域既可以包含映射到實體存儲器的各個部分,也可以包含沒有映射到實體存儲器的各個部分)。

設定新的C E L L D ATA結構的成員。

    現在實體存儲器已經映射到相應的位置,你的程式能夠通路記憶體,而不會引發通路違規。

這個虛拟記憶體技術非常出色,因為隻有在使用者将資料輸入電子表格的單元格時,才會送出實體存儲器。由于電子表格中的大多數單元格是空的,是以大部分保留區域沒有送出給它的實體存儲器。

C E L L D ATA結構的記憶體在資料初次輸入時就已經送出了。

C E L L D ATA結構送出實體存儲器時(像上面的第二步那樣),系統實際上送出的是記憶體的一個完整的頁面。這并不像它聽起來那樣十分浪費:為單個C E L L D ATA結構送出實體存儲器的結果是,也要為附近的其他C E L L D ATA結構送出記憶體。如果這時使用者将資料輸入鄰近的單元格(這是經常出現的情況),就不需要送出更多的實體存儲器。

有4種方法可以用來确定是否要将實體存儲器送出給區域的一個部分:

Vi r t u a l A l l o c函數的時候,不要檢視實體存儲器是否已經映射到位址空間區域的一個部分,而是讓你的程式設法進行記憶體的送出。系統首先檢視記憶體是否已經被送出,如果已經送出,那麼就不要送出更多的實體存儲器。

這種方法最容易操作,但是它的缺點是每次改變 C E L L D ATA結構時要多進行一次函數的

調用,這會使程式運作得比較慢。

Vi r t u a l Q u e r y函數)确定實體存儲器是否已經送出給包含C E L L D ATA結構的位址空間。如果已經送出了,那麼就不要進行任何别的操作。如果尚未送出,則可以調用Vi r t u a l A l l o c函數以便送出記憶體。這種方法實際上比第一種方法差,它既會增加代碼的長度,又會降低程式運作的速度(因為增加了對Vi r t u a l A l l o c函數的調用)。

Vi r t u a l A l l o c函數,你的代碼能夠比系統更快地确定記憶體是否已經被送出。它的缺點是,必須不斷跟蹤頁面送出的資訊,這可能非常簡單,也可能非常困難,要根據你的情況而定。

S E H)方法,這是最好的方法。 S E H是一個作業系統特性,它使系統能夠在發生某種情況時将此情況通知你的應用程式。實際上可以建立一個帶有異常

處理程式的應用程式,然後,每當試圖通路未送出的記憶體時,系統就将這個問題通知應用程式。然後你的應用程式便進行記憶體的送出,并告訴系統重新運作導緻異常條件的指令。這時對記憶體的通路就能成功地進行了,程式将繼續運作,仿佛從未發生過問題一樣。這種方法是優點最多的方法,因為需要做的工作最少(也就是說要你編寫的代碼比較少),同時,你的程式可以全速運作。關于 S E H的全面介紹,請參見第2 3、2 4和2 5章。第2 5章中的電子表格示例應用程式說明了如何按照上面介紹的方法來使用虛拟記憶體。

15.5 回收虛拟記憶體和釋放位址空間區域

Vi r t u a l F r e e函數:

Windows核心程式設計 第十五章 在應用程式中使用虛拟記憶體

Vi r t u a l F r e e函數來釋放一個已保留區域的簡單例子。當你的程序不再通路區域中的實體存儲器時,就可以釋放整個保留的區域和所有送出給該區域的實體存儲器,方法是一次調用Vi r t u a l F r e e函數。

p v A d d r e s s參數必須是該區域的基位址。此位址與該區域被保留時Vi r t u a l A l l o c函數傳回的位址相同。系統知道在特定記憶體位址上的該區域的大小,是以可以為d w S i z e參數傳遞0。實際上,必須為d w S i z e參數傳遞0,否則對Vi r t u a l F r e e的調用就會失敗。對于第三個參數f d w F r e e Ty p e,必須傳遞M E M _ R E L E A S E,以告訴系統将所有映射的實體存儲器送出給該區域并釋放該區域。當釋放一個區域時,必須釋放該區域保留的所有位址空間。例如不能保留一個128 KB的區域,然後決定隻釋放它的64 KB。必須釋放所有的128 KB。

Vi r t u a l F r e e函數,若要回收某些實體存儲器,必須在Vi r t u a l F r e e函數的p v A d d r e s s參數中傳遞用于辨別要回收的第一個頁面的記憶體位址,還必須在 d w S i z e參數中設定要釋放的位元組數,并在 f d w F r e e Ty p e參數中傳遞M E M _ D E C O M M I T标志。

    與送出實體存儲器的情況一樣,回收時也必須按照頁面的配置設定粒度來進行。這就是說,設

定頁面中間的一個記憶體位址就可以回收整個頁面。當然,如果 pvAddress + dwSize的值位于一個頁面的中間,那麼包含該位址的整個頁面将被回收。是以位于 pvAddress 至pvAddress +d w S i z e範圍内的所有頁面均被回收。

d w S i z e是0,p v S d d r e s s是已配置設定區域的基位址,那麼 Vi r t u a l F r e e将回收全部範圍内的已配置設定頁面。當實體存儲器的頁面已經回收之後,已釋放的實體存儲器就可以供系統中的所有其他程序使用,如果試圖通路未回收的記憶體,将會造成通路違規。

15.6 改變保護屬性

PA G E _ R E A D W R I T E,然後在每個函數終止運作時将保護屬性重新改為PA G E _ N O A C C E S S。

    通過這樣的設定,就能夠使連結表資料不受隐藏在程式中的其他錯誤的影響。如果程序中的任何其他代碼存在一個迷失指針,試圖通路你的連結表資料,那麼就會引發通路違規。當試圖尋找應用程式中難以發現的錯誤時,利用保護屬性是極其有用的。

Vi r t u a l P r o t e c t函數:

Windows核心程式設計 第十五章 在應用程式中使用虛拟記憶體

p v A d d r e s s參數指向記憶體的基位址(它必須位于程序的使用者方式分區中),d w S i z e參數用于指明你想要改變保護屬性的位元組數,而 f l N e w P r o t e c t參數則代表PA G E _ *保護屬性标志中的任何一個标志,但PA G E _ W R I T E C O P Y和PA G E _ E X E C U T E _ W R I T E C O P Y這兩個标志除外。

p f l O l d P r o t e c t是D W O R D的位址,Vi r t u a l P r o t e c t将用原先與p v A d d r e s s位置上的位元組相關的保護屬性填入該D W O R D。盡管許多應用程式并不需要該資訊,但是必須為該參數傳遞一個有效位址,否則該函數的運作将會失敗。

    當然,保護屬性是與記憶體的整個頁面相關聯的,而不是賦予記憶體的各個位元組的。是以,如

果要使用下面的代碼來調用 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保護屬性賦予記憶體的兩個頁面:

Windows核心程式設計 第十五章 在應用程式中使用虛拟記憶體

隻支援PA G E _ R E A D O N LY和PA G E _ R E A D W R I T E兩個保護屬性。如果試圖将頁面的保護屬性改為PA G E _ E X E C U T E或PA G E _ E X E C U T E _ R E A D,該頁面可得到PA G E _ R E A D O N LY保護屬性。同樣,如果試圖将頁面的保護屬性改為PA G E _ E X E C U T E _ R E A D W R I T E,那麼該頁面将得到PA G E _ R E A D W R I T E保護屬性。