淺議Windows 2000/XP Pagefile組織管理
WebCrazy(http://webcrazy.yeah.net)
任何時候系統記憶體資源相對磁碟空間來說都是相形見拙的。因為虛擬記憶體機制,使我們可以有相對豐富的地址資源(通常32bit的虛擬位址,可以有4G的定址空間),而這些資源對實體記憶體來說一般情況是總是綽綽有餘的。所以在現代作業系統中,總是在相對緊張時使用一些策略,如FIFO、LRU等將實體記憶體的一些頁面置入相對便宜的磁碟空間資源中。一般的UNIX系統,獨立使用一個分區,即swap partition。而這方面Windows只是使用普通的檔案,通常命名為pagefile.sys,位於各分區的根目錄中。由於受到用於pagefile的PTE的限制(PTE中使用4bit來識別操作的pagefile),所以Windows最多可以支援16個pagefile.sys。
從上描述,pagefile.sys本身就是一個比較特殊的檔案,根據系統的情況它的大小是可擴充的,通常我們可以使用“控制台”的“系統”小Applet來設定。由於其特殊性,Windows在啟動階段會對每個pagefile.sys建立相應的FILE_OBJECT,並且設定SharedRead欄位為False,而且在System進程,每個FILE_OBJECT都分別有一個控制代碼指向,這樣即只允許系統自身對其操作,避免使用者對其進行刪除等誤操作。
為了對pagefile.sys進行管理,Windows中有一個長度為16的數組,用於對pagefile.sys的組織。每個成員分別對應一個pagefile。這個數組由系統變數MmPagingFile指向,每個成員都是一個指向MMPAGING_FILE的結構,這個結構有如下的格式:
+0x000 Size : Uint4B
+0x004 MaximumSize : Uint4B
+0x008 MinimumSize : Uint4B
+0x00c FreeSpace : Uint4B
+0x010 CurrentUsage : Uint4B
+0x014 PeakUsage : Uint4B
+0x018 Hint : Uint4B
+0x01c HighestPage : Uint4B
+0x020 Entry : [2] Ptr32 _MMMOD_WRITER_MDL_ENTRY
+0x028 Bitmap : Ptr32 _RTL_BITMAP
+0x02c File : Ptr32 _FILE_OBJECT
+0x030 PageFileName : _UNICODE_STRING
+0x038 PageFileNumber : Uint4B
+0x03c Extended : UChar
+0x03d HintSetToZero : UChar
+0x03e BootPartition : UChar
+0x040 FileHandle : Ptr32 Void
通過這個結構,我們可以很容易的得到相應pagefile的使用方式(MaximumSize、MinimumSize、FreeSpace、CurrentUsage、PeakUsage,請參閱windbg的!vm命令),其對應的FILE_OBJECT等。另外通過FILE_OBJECT的DeviceObject與Vpb欄位,我們就可知道這個pagefile所處的分區及分區使用的檔案系統等等資訊。我們來詳細介紹一下Bitmap成員。
Bitmap是一個RTL_BITMAP的結構,其定義在ntddk.h中:
typedef struct _RTL_BITMAP {
ULONG SizeOfBitMap; // Number of bits in bit map
PULONG Buffer; // Pointer to the bit map itself
} RTL_BITMAP;
與頁框資料庫(Pfn Database)與虛擬記憶體(x86平台PAGE_SIZE 4k)一樣,Windows也將pagefile分割成4K一塊塊的大小,稱為一頁,每頁由Bitmap對應的1 bit指定狀態。1為佔用,0為空白閑。通過使用RtlFindClearBits或是RtlFindClearBitsAndSet等函數對Bitmap進行操作,尋找這些檔案的未用頁面。雖然Bitmap指明佔用狀態時,Windows常以4k為單位,但為了提高效能,Windows在一次寫pagefile的單位通常為64k(MmModifiedWriteClusterSize個頁)。還有MMMOD_WRITER_MDL_ENTRY,等我下面提及相關內容時再加以說明。
先用windbg來消化一下上面的討論:
kd> dd MmPagingFile l 10 //從輸出結果可以看出我的機子上設了兩個pagefile。
80547020 80d2af80 feec1548 00000000 00000000
80547030 00000000 00000000 00000000 00000000
80547040 00000000 00000000 00000000 00000000
80547050 00000000 00000000 00000000 00000000
kd> dd @$p l 40 //第一個pagefile的情況。
80d2af80 00006400 0000c800 00006400 00000c38
80d2af90 000057c7 000057c7 00000000 00000000
80d2afa0 feea1cb8 feea1c18 fecbb000 feddc428
.
.
.
kd> dd feddc428 l 4 //從上面給出的MMPAGING_FILE,很容易得到file object(Offset 0x2c)。
feddc428 00700005 80ecf2f0 80ecf268 fee66c10
kd> !devobj 80ecf2f0 //aFILE_OBJECT的結構在ntddk.h中給出,其第三個dword就是DEVICE_OBJECT。
Device object (80ecf2f0) is for:
HarddiskVolume2 /Driver/Ftdisk DriverObject 80d97030
Current Irp 00000000 RefCount 1316 Type 00000007 Flags 00001150
Vpb 80ecf268 Dacl e13d1484 DevExt 80ecf3a8 DevObjExt 80ecf490 Dope 80ecf210 DevNode 80d95bd0
ExtensionFlags (0000000000)
AttachedDevice (Upper) 80d954b8 /Driver/VolSnap
Device queue is not busy.
另外FILE_OBJECT的第四個dword(fee66c10)就是VPB結構,你使用!vpb分析分析,限於篇幅,我就不在這兒列出了。
通過上面windbg的分析,我們已經基本上對pagefile有了一定的瞭解了,下面轉入記憶體子系列與IO子系統(調用FSD)對pagefile的組織管理。
通常情況下,對於進程可見的永遠是虛擬位址,存取某個虛擬位址,對於不存在的地址(對於X86,即其PTE的P位為0),通過觸發硬體中斷(X86為int e),由軟體來對這些PTE進行解析,譬如原型PTE(我在《探究Windows 2000/XP原型PTE》中詳細介紹),或是過渡PTE(Transition PTE,某些頁面由於進程工作集修整等原因,成為可被使用的頁面,但這些頁面的內容當前對這些進程仍有效,可隨時重新使用,所以Windows使用Transition這個術語區別於純粹的Free或Zeroed列表,我在《解析Winndows 2000/XP實體記憶體管理》中提及PFN列表)等,而對於Page File,實際上也有一個對應的pte指向相應pagefile.sys,完成解析工作(MiResolvePageFileFault),處理分頁錯誤(通過IoPageRead,下面會介紹)。
所以在繼續討論之前我們來說明一下Pagefile PTE,它的格式如下:
Valid : Pos 0, 1 Bit
PageFileLow : Pos 1, 4 Bits
Protection : Pos 5, 5 Bits
Prototype : Pos 10, 1 Bit
Transition : Pos 11, 1 Bit
PageFileHigh : Pos 12, 20 Bits
對於Prototype PTE與Transition PTE,總有1bit用於識別相應的PTE,如上的Prototype欄位,但對於PageFile PTE,卻沒有對應的識別bit,實際上MiDispatchFault(由KiTrap0E調用),是在解析完Prototype PTE(MiResolveProtoPteFault)、Transition PTE(MiResolveTransitionFault)、還有MiResolveDemandZeroFault後,才調用MiResolvePageFileFault的,當然在MiResolveProtoPteFault處理過程中也是最後調用MiResolvePageFileFault的。
假設我們存取一個當前駐留在pagefile中的頁面,通過MiDispatchFault,控制權轉到MiResolvePageFileFault後,他會根據PTE的PageFileLow來索引MMPAGING_FILE數組,即判斷這一頁面位於哪個pagefile.sys中,因為PageFileLow為4個bit,所以Windows最多可以支援16個pagefile.sys。這樣記憶體子系統根據這個索引從MmPagingFile中描述的頁檔案結構取出這個pagefile的FILE_OBJECT(上面介紹過)。加上PageFileHigh所指定的pagefile.sys的位移值,MiResolvePageFileFault通過返回一個值為0xC0033333的特殊NTSTATUS通知MiDispatchFault調用IoPageRead得到此頁面。IoPageRead的原型如下(定義於ntifs.h中):
NTKERNELAPI
NTSTATUS
IoPageRead(
IN PFILE_OBJECT FileObject,
IN PMDL MemoryDescriptorList,
IN PLARGE_INTEGER StartingOffset,
IN PKEVENT Event,
OUT PIO_STATUS_BLOCK IoStatusBlock
);
當然在調用IoPageRead之前,記憶體管理器必須分配一個物理頁面,必要的時候還要調用MiRemoveAnyPage騰出空間,然後調用MiInitializeReadInProgressPfn,將這一頁框置成ReadInProgress狀態,然後將IoPageRead所需要的MDL參數MemoryDescriptorList指向這一頁面。MDL的虛擬位址欄位也就是IoPageRead讀入的頁面映射的虛擬位址,也即滿足我們先前假設的分頁錯誤。
IoPageRead實際上通過Allocate一個IRP,用DIRECT_IO的方式(即我們提供的MDL),然後設定一個Complete Routine,用於取消頁面讀取之前的ReadInProgress狀態,再通過IoCallDriver調用IO子系統調用對應的File System Driver(通常由FILE_OBJECT的VPB參數確定),至於FSD是如何讀取pagefile.sys的,這兒不加以討論,ntifs提供的fastfat的原始碼是學習的方向。
需要指出的是IoPageRead是一個同步操作,即只有等待頁面讀完畢以後才可以往下處理。這也是MiDispatchFault只能運行於DISPATCH_LEVEL IRQL之下的主要原因。IoPageRead通過裝置分配的IRP的IRP_SYNCHRONOUS_PAGING_IO的標誌來實現同步的。另外他也設定了IRP_PAGING_IO、IRP_NOCACHE標誌,用於與FSD之間的特殊通訊要求。
由於工作集修整等的需要,通過MiModifiedPageWriter(MPW)線程實行將某些頁面置入pagefile中。MPW使用MMPAGING_FILE結構的_MMMOD_WRITER_MDL_ENTRY類型的Entry進行操作,_MMMOD_WRITER_MDL_ENTRY不僅僅由MiModifiedPageWriter使用,他還要讓MiMappedPageWriter使用(用於Mapped file),所以_MMMOD_WRITER_MDL_ENTRY結構不僅函有MDL成員,還包含Control Area等等。限於篇幅,我不將其結構列出。MPW通過IoAsynchronousPageRead將頁面按前面說的一次MmModifiedWriteClusterSize個頁面寫入pagefile中。對於IoAsynchronousPageRead其使用的IRP flag是IRP_PAGING_IO與IRP_NOCACHE,說明他是非同步作業的。這也可從他的名字看出,區別於Windows提供的另一個相關過程IoSynchronousPageWrite,他是同步的。
講到這兒,對於page file的組織管理的一些基本的印象應該是有的。最後需要指出的一點是,對於IoPageRead不僅僅是對於pagefile的,其也可以針對mapped file,還有MiModifiedPageWriter,要不是避免死結也不會區分出MiMappedPageWriter,實際上Windows內部記憶體管理器對於pagefile與mappedfile的管理使用是基本相同的,而FSD的處理也只是一點點的區別而已。所以結合我以前介紹的如Control Area等概念,對mapped file等的理解也是可以參照本文的。還有同樣的一句話,錯誤地方希望得到你的指點。