PFN数据库的概念
前面我们看到,有效的PTE 中有一个页面帧编号,此页面帧编号为20 位,指向一个物理内存中的页面,而且我们在前面的介绍中看到了利用MiRemoveAnyPage 或 MiRemoveZeroPage 函数申请物理内存页面的用法,其返回值也是一个页面帧。
所谓PFN 数据库,就是一个数组,每一项都描述了一个物理页面的状态,大小为8字节*6。
如下:
typedef struct _MMPFN {
union {
PFN_NUMBER Flink;
WSLE_NUMBER WsIndex;
PKEVENT Event;
NTSTATUS ReadStatus;
//
// Note: NextStackPfn is actually used asSLIST_ENTRY, however
// because of its alignmentcharacteristics, using that type would
// unnecessarily add padding to thisstructure.
//
SINGLE_LIST_ENTRY NextStackPfn;
} u1;
PMMPTE PteAddress;
union {
PFN_NUMBER Blink;
//
// ShareCount transitions are protectedby the PFN lock.
//
ULONG_PTR ShareCount;
} u2;
union {
//
// ReferenceCount transitions aregenerally done with InterlockedXxxPfn
// sequences, and only the 0->1 and1->0 transitions are protected
// by the PFN lock. Note that a *VERY* intricate synchronization
// scheme is being used to maximizescalability.
//
struct {
USHORT ReferenceCount;
MMPFNENTRY e1;
};
struct {
USHORT ReferenceCount;
USHORT ShortFlags;
} e2;
} u3;
#if defined (_WIN64)
ULONG UsedPageTableEntries;
#endif
union {
MMPTE OriginalPte;
LONG AweReferenceCount;
};
union {
ULONG_PTR EntireFrame;
struct {
#if defined (_WIN64)
ULONG_PTR PteFrame: 57;
#else
ULONG_PTR PteFrame: 25;
#endif
ULONG_PTR InPageError : 1;
ULONG_PTR VerifierAllocation : 1;
ULONG_PTR AweAllocation : 1;
ULONG_PTR Priority : MI_PFN_PRIORITY_BITS;
ULONG_PTR MustBeCached : 1;
};
} u4;
} MMPFN, *PMMPFN;
extern PMMPFN MmPfnDatabase;
#define MI_PFN_ELEMENT(index) (&MmPfnDatabase[index])
可以看出,MmPfnDatabase数组是以页帧编号为索引的,因此,要查看一个物理页面的状态,只需直接以页帧编号为索引,就可以获取到该页面的PFN 项。
一个物理页面可能的状态:
活动(active valid) |
页面处于活动状态是指正在被某个 进程使用,或者被用于系统空间(非换页内存池,或者在系统工作集)。对应有一个 有效的PTE指向该页面。 |
备用状态(standby) |
这种页面原来属于某个进程或系统工作集,但现在已经从工 作集中移除。这种页面包含的数据对于原来的工作集仍然是有效的,也就是说,在原 来工作集中,页面的内容尚未被修改。原来工作集中的PTE仍然指向该页面,但是 已被标记成正在转移的无效PTE。这种页面处于被回收状态,既可以被系统回收以作 他用,也可以被原来的工作集回收而继续留用。 |
已修改状态(modified) |
类似于备用状态,已经从原来的工作集中移除,但是,页 面包含的内容已经被修改过。原来工作集中的PTE仍然指向物理页面,但已被标记 成正在转移的无效PTB。如果系统要把这种页面回收作他用,则必须将其中的内容写 到磁盘上。 |
已修改但不写出(modified no-write)。 |
类似于上一种状态,但区别在于,内存管理 器不会将它的内容写到磁盘上。 |
转移状态 |
说明一个页面正处于I/O操作进行中,当两个线程并发地在同一个页面上 引发页面错误时,页面错误处理例程通过这一状态可以判断出冲突的页面错误的情 形,从而正确地处理。在4.4.3节中我们看到过这种页面错误冲突的处理过程。值得 一提的是,这里的转移状态是针对一个物理页面中的内容,而无效PTE的转移状态 则是针对它所指的页面已被转移到备用链表或已修改链表中。两者含义大不相同。 |
空闲状态。 |
页面是空闲的,不属于任何一个工作集,它们包含了不确定的数据,内存 管理器在重新使用这些页面以前,应根据需要(尤其是出于安全考虑)清除脏数据。 |
零化状态 |
页面是空闲的,不属于任何一个工作集,其中的内容已经被全部清零。 |
坏状态 |
页面产生硬件错误。系统不再使用这样的页面。 |
其中 OriginalPte 包含了指向此页面的PTE 的原始内容,当一个物理页面分配给一个PTE ,它记录了原来的PTE,之后当该物理页面不再为它所用,可以恢复原来的PTE,
活动状态的页面不存在链表,而备用页面,修改页面,已修改但不写出页面,零化页面或空闲页面都组织成一个链表。
对于正在转移状态的PFN,第一项要么指向一个同步事件(I/O正在进行),要么是一个I/O 错误码(页面换入过程中产生错误),转移状态的PFN 的用途是用于识别和消除冲突的页面冲突。
贴几张PFN 数据库相关的图
遗留的几个函数的分析
首先了解一个结构体
typedef struct _MMINPAGE_SUPPORT {
KEVENT Event;
IO_STATUS_BLOCKIoStatus;
LARGE_INTEGERReadOffset;
LONG WaitCount;
#if defined (_WIN64)
ULONGUsedPageTableEntries;
#endif
PETHREADThread;
PFILE_OBJECTFilePointer;
PMMPTE BasePte;
PMMPFN Pfn;
union {
MMINPAGE_FLAGS e1;
ULONG_PTRLongFlags;
PMDLPrefetchMdl; // Only used under _PREFETCH_
} u1;
MDL Mdl;
PFN_NUMBERPage[MM_MAXIMUM_READ_CLUSTER_SIZE + 1];
SINGLE_LIST_ENTRY ListEntry;
} MMINPAGE_SUPPORT, *PMMINPAGE_SUPPORT;
NTSTATUS
MiResolvePageFileFault (
IN PVOID FaultingAddress,
IN PMMPTE PointerPte,
OUT PMMPTE CapturedPteContents,
OUT PMMINPAGE_SUPPORT *ReadBlock,
IN PEPROCESS Process,
IN KIRQL OldIrql
)
函数建立一个MDL 和相关的结构体,然后读取页面文件以解决页面错误
申请一个读块,计算周边可以一起读取的页面的大小
如果一次读取的页面只有一个
申请一个内存页,然后调用MiInitializeReadInProgressSinglePfn初始化PFN 元素为转译/正在读入的状态。初始一个ReadBlockLocal的一个事件,当I/O 操作完成,该事件被设置触发。
如果有多个页面需要读取
申请多个内存页,构建一个MDL 描述这些页面,然后调用MiInitializeReadInProgressPfn,设置所有PTE为转移状态,且无效,每次内部循环都增加页表页的共享计数,即PTE 个数。
// PageFileNumber为页面文件号,默认只有一个页面文件,最多16个
然后设置ReadBlockLocal 的文件对象为MmPagingFile[PageFileNumber]->File;
函数的最后判断,如果一次读取的页面只有一个,为其建立一个MDL。
NTSTATUS
IoPageRead(
IN PFILE_OBJECTFileObject,
IN PMDLMemoryDescriptorList,
IN PLARGE_INTEGERStartingOffset,
IN PKEVENT Event,
OUT PIO_STATUS_BLOCKIoStatusBlock
)
所有正在执行的I/O 操作,被设置为IRP_PAGING_IO,读页操作被标识为使用IRP IRP_INPUT_OPERATION
MDL 标识读写操作的内存页面,大小,如果MDL 的低字节被设置,为异步操作,否则为同步
函数内部得到文件对象关联的设备对象,然后申请IRP ,和IRP 堆栈,设置IRP读写方式(同步/异步)及其它成员。
然后设置IRPSPà主功能码为读,设置文件对象,长度和偏移,并将其IRP设置为刚申请的IRP。然后IoCallDriver(deviceObject,irp),调用驱动程序,进行读操作。
首先通过MiRemoveZeroPage 或MiRemoveAnyPage 或MiRemoveZeroPageIfAny 申请一个物理页面,得到 PageFrameIndex,然后调用 MiInitializePfn 函数初始化PFN项,该函数设置PFN指向PTE地址,以及保留PTE 的值。最后如果是用户空间的PTE,设置PTE 中的有效位,访问位,PFN 域,以及保护属性。如果是系统空间的PTE,只设置PFN,保护属性,还有一个检查判断全局访问标识的位
NTSTATUS
MiResolveProtoPteFault (
IN ULONG_PTR StoreInstruction,
IN PVOID FaultingAddress,
IN PMMPTE PointerPte,
IN PMMPTE PointerProtoPte,
IN OUT PMMPFN *LockedProtoPfn,
OUT PMMINPAGE_SUPPORT *ReadBlock,
OUT PMMPTE CapturedPteContents,
IN PEPROCESS Process,
IN KIRQL OldIrql,
IN PVOID TrapInformation
)
如果指令尝试修改错误的地址(比如修改,访问请求),非空
LockedProtoPfn 指向原型PTE 的PFN 的地址,如果锁定了PFN 非空,否则为空,如果此函数解锁PFN,也应该清除这个指针。
ReadBlock 已经了解过了。CapturedPteContents---捕获的PTE 内容,以便比较PTE 是否改变。当且仅当调用者要处理I/O 的情况(此函数返回STATUS_ISSUE_PAGING_IO)
首先,如果原型PTE 有效,直接调用MiCompleteProtoPteFault完成函数即可,该函数增加包含PTE 的页表页面的共享计数,如果是修改指令,且非拷贝写,设置PFN 修改位,PTE 脏位。如果LockedProtoPfn不为空,释放锁,清除指针。然后利用原型PTE 设置PTE 有效即可
如果需要重新检查访问权限,检查访问权限。然后判断是否为拷贝写。
如果原型PTE 要求零页面,且页面属性为拷贝写,让这个页面变为私有的需要零的页面,并申请一个零页面并返回。
然后就是分别解决原型PTE 的几个页面错误问题:页面文件,转移,需要零页面,映射文件错误。
其中的三个,页面文件,转移,要求零页面已经介绍过,下面主要看映射文件错误:
NTSTATUS
MiResolveMappedFileFault (
IN PMMPTEPointerPte,
OUTPMMINPAGE_SUPPORT *ReadBlock,
IN PEPROCESSProcess,
IN KIRQLOldIrql
)
函数建立MDL 和其它需要的结构以处理页面错误
首先得到PTE 对应的子内存区对象和子内存区对象对应的控制域。然后申请ReadBlock 以在之后的读磁盘操作中使用。
然后建立MDL,尽量增加一次读取磁盘的数据大小。
然后计算要读取的文件内偏移,通过函数:MiStartingOffset 得到Subsection 和 PTE 指定的文件的偏移,镜像文件是512 字节对齐的,而数据文件是4KB 对齐的。SubsectionBase 为第一个PTE 地址,当前PTE 地址-第一个pte地址之后得到的是页面文件的数量,然后*页面大小4KB,然后+subsection 中的startsector 指定的开始簇*(镜像文件/数据文件对应的对齐大小,即可得到文件偏移)。
offset = base + (thispte-basepte)<< PAGE_SHIFT)
后面申请内存页面,初始化MDL 和 ReadBlock 之后返回一个STATUS_ISSUE_PAGING_IO。
转换PTE是在空闲或修改的列表上,如果不在两个联表上,则是由于它的ReferenceCount或当前正在从磁盘读入(正在读取)。如果正在读取该页面,则这是一个冲突的访问请求,应该做相应处理
首先获得PTE 对应的PFN Index 和 PFN
如果当前页面得到一个读页错误,证明有其它线程正在冲突访问当前页面,延缓操作,并让其它线程完成并返回。
在释放锁定之前捕捉相关的pfn字段,因为页面可能会立即重新使用,返回一个I/O status
如果当前操作的页面正在读----冲突的页面错误,首先增加pfn 的引用计数,这样在所有的冲突的页面错误完成之前,这个页面不会被复用。
设置InPageBlock 的地址,此函数的调用者必须释放这个块。
然后调用函数MiWaitForInPageComplete 该函数内部进行的操作,如上一篇“冲突的页面错误”锁描述。
如果是普通的转移状态的PTE,直接设置PTE 的状态???