浅议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等的理解也是可以参照本文的。还有同样的一句话,错误地方希望得到你的指点。