任何时候系统内存资源相对磁盘空间来说都是相形见拙的。因为虚拟内存机制,使我们可以有相对丰富的地址资源(通常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等的理解也是可以参照本文的。还有同样的一句话,错误地方希望得到你的指点。