![在 Windows(x86)上,PFN 数据库如何建立索引?](https://linux22.com/image/1547385/%E5%9C%A8%20Windows%EF%BC%88x86%EF%BC%89%E4%B8%8A%EF%BC%8CPFN%20%E6%95%B0%E6%8D%AE%E5%BA%93%E5%A6%82%E4%BD%95%E5%BB%BA%E7%AB%8B%E7%B4%A2%E5%BC%95%EF%BC%9F.png)
Windows 内部原理有一个关于虚拟内存的部分。我理解 L4 条目、PDPE 和 PDE 中的 PFN 字段指的是下一级表的基地址,而 PTE 的 PFN 指的是页面在内存中的位置(移位后)。
我还了解到 Windows 的 PFN 数据库包含有关每个页面的附加信息。它似乎是由 PTE 的 PFN 编制索引的。这意味着每个 PDE 都应该有一个。是真的吗?它如何定位?
也许它在书里但我肯定忽略了它。
答案1
PFN 数据库位于高规范虚拟地址0xFFFFFA8000000000
。它的最大虚拟大小为0x57FFFFFFFFF
(6TiB)(每个条目0x1D5555556
大小为0x30
字节)。这涵盖了 512 TiB(49 位),尽管通常只允许 256TiB 的物理空间64 位 Intel 芯片组(受 <=48 位 PTE 地址字段限制 - 请记住,在 PML4 x64 窗口上虚拟地址始终为 48 位)(0x1D5555556*PageSize = 512TiB
)。在我的计算机上,PFN 数据库似乎来自0xFFFFFA8000000000 – FFFFFA80197EC000
,后者的地址为nt!MmNonPagedPoolStart-1
。PFN 0 对应于 PA 处的第一个物理帧0x0000
;PFN 1 对应于第二个物理帧,依此0x1000
类推。
要查找 PFN 数据库的大小,
? poi(nt!MmNonPagedPoolStart) - poi(nt!MmPfnDatabase)
可以在调试器中使用表达式。要查找 PFN 数据库中的条目总数,可以使用表达式?(poi(nt!MmNonPagedPoolStart) - poi(nt!MmPfnDatabase))/ @@(sizeof(nt!_MMPFN))
[1]
使用 PFN 号(48 位物理地址的高 36 位)索引到 PFN(乘以 0x30 条目大小)以获取nt!_MMPFN
描述该物理帧的。
MMPFN 格式:
某些 MMPFN 将位于某些列表(零、空闲、待机、已修改)上,这些列表通过前向和后向链接将它们连接到列表上连接的另一个 MMPFN。
类型:此 PFN 所代表的页面类型。(类型包括活动/有效、待机、已修改、已修改-未写入、空闲、归零、坏和过渡。)
颜色:除了在列表中链接在一起之外,PFN 数据库条目还使用附加字段通过“颜色”链接物理帧,颜色是页面的 NUMA 节点号。
PTE 的 PFN 又称“包含页面”:包含指向此物理框架的 PTE 的页面的物理框架号:!pfn 包含页面
PTE 地址是指向页面(映射到所有进程的内核内存 VA)的 PTE 结构的虚拟地址。请参见这里如何获取某个虚拟地址的 PTE、PDE、PDPTE 和 PML4E 的虚拟地址。要获取此 PTE 覆盖的虚拟地址,您需要反转上面链接中的公式,因此
#define MiPteToAddress(x) ((PVOID)((ULONG)(x) << 10))
。此成员的两个低位用作锁定机制,以序列化对 PFN 条目的访问。
原始 PTE 内容 –MiModifiedPageWriter
将修改列表上的修改页面写入页面文件(这些页面已被invlpg
删除[1],已失效并转换为过渡 PTE,并且其 PFN 条目已由工作集管理器添加到修改列表(如果已修改,则添加到备用列表)),并用包含页面文件中页面地址的软件 PTE 替换它们的 PTE,并将 PFN 添加到备用列表的末尾。它还必须修改 PTE 并将其转换为页面文件软件 PTE,其中包含页面写入的页面文件地址。我认为它通过使用 PTE 的 PFN 来计算 PTE 的物理地址来实现这一点,如果它不是内核虚拟地址,它会将其映射到超空间以对其进行修改(它还必须读取和设置进程的 PDE/PDPTE/PML4E Access/Dirty 位(如果尚未设置)因为 TLB 将使用系统上下文而不是进程上下文写入 A/D 位)。MiMappedPageWriter
是将修改后的文件映射页面写回到其文件或将文件映射的 CoW 写入页面文件。据我所知,这个原始/恢复 PTE 在某些情况下包含页面文件软件 PTE,在其他情况下包含 PPTE。这可能会为这个特定的数据页面提供一个永久的页面文件地址,这样当它再次被写出时,它就会被写入相同的地址,并可用于在一个连续的 I/O 请求中写回页面。PFN 字段还可以允许仅当页面从备用页面移动到零列表时将此页面文件地址复制到 PTE 中,这样当它在备用列表中时,PTE 仍然可以是备用故障,并且可以从 RAM 中满足页面,尽管这显示 PTE 变为页面文件无效类型,而 PFN 仍在进行修改写入。
当页面首次添加到工作集和/或页面在内存中被锁定以进行 I/O(例如,由设备驱动程序)时,引用计数会增加。当共享计数变为 0 或页面从内存中解锁时,引用计数会减少。当共享计数变为 0 时,页面不再由工作集拥有。然后,如果引用计数也为零,则更新描述页面的 PFN 数据库条目以将该页面添加到空闲、备用或已修改列表中。
共享计数字段表示引用此页面的 PTE 数量(标记为只读、写时复制或共享读/写的页面可以由多个进程共享)。
如果框架是进程工作集的一部分,那么它将有一个工作集索引,该索引是进程的 MMWSLE 数组的索引。该数组由 MMWSL 结构指向,而 MMWSL 结构又由 MMSUPPORT 结构指向,而 MMSUPPORT 结构又由 EPROCESS 结构指向。
缓存属性表明该页面是否可缓存,或是否可写入合并等。标志指示页面是否用于内核堆栈、页面是否处于分页或调出过程中、该过程中是否存在“页内错误”等错误。类型(又称页面位置)表明物理帧的类型(活动、待机、已修改等)
[1]它必须通过将其他具有 IPI 的核心中断到刷新并使其 TLB 无效的中断向量(称为 TLB 击落)来实现,就像INVALIDATE_TLB_VECTOR
在 Linux 上一样,它似乎是使用 mov 到 cr3 实现的,将 IPI 发送到 allbutself 的核心会在smp_invalidate_needed
完成的 TLB 清除的位掩码(在 Linux 上)上旋转等待(这是必要的,因为被中断的 CPU 可能正在执行一个可能会写入该页面的大型 MSROM 例程,并且中断只能在宏指令退休结束时处理),以便在该进程的上下文中执行的任何核心都不会在写出时写入或读取新页面,而是在下次执行页面遍历时很快引发页面错误。修改后的页面写入器也会发生同样的情况,但其他核心上的页面错误将无法处理,因为页面写入器获取了 PFN 条目锁,因此它可以在发送 IPI 之前在 PFN 条目中写入页面写入正在进行位,并且这些线程将等待 PFN 中指定的互斥事件。它需要在修改 PTE 和执行 TLB 击落之前获取锁并设置正在进行位,以便它是一个原子过程,即,同一页面的页面错误不会看到页面文件 PTE 和空闲的 PFN 条目,并启动尚未写入的页面的磁盘读取。
这是我根据手册和一般原理得出的猜测。我还没有通过查看内存管理器的 ReactOS 源代码来验证。