gl00my-内存与进程管理器1

本文深入探讨了Windows NT操作系统的内存管理机制,包括进程线程结构、页表管理、HyperSpace用途、系统PTEs的功能、PageFrame Number数据库的作用、工作集管理策略以及向pagefile换页的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

gl00my-内存与进程管理器1
==========================

                                   But I fear tomorrow I'll be crying,
                                   Yes I fear tomorrow I'll be crying.

                                                  King Crimson'69 -Epitaph


关于Windows NT内存管理器的高层次信息已经够多的了,所以这里不会再讲什么FLAT模型、虚拟内存之类的东西。这里我们只讲具体的底层的东西。我假定大家都了解>i386的体系结构。


目录
==========
     00.内核进程线程结构体
     01.页表
     02.Hyper Space
     03.System PTE'S
     04.Frame data base (MmPfnDatabase)
     05.Working Set
     06.向pagefile换页
     07.page fault的处理
     08.从内存管理器角度看进程的创建
     09.上下文切换
     0a.某些未公开的内存管理器函数
     0b.结语

附录
     0c.某些未公开的系统调用
     0d.附注及代码分析草稿


00.内核进程线程结构体
===================================

Windows NT中的每一个进程都是EPROCESS结构体。此结构体中除了进程的属性之外还引用了其它一些与实现进程紧密相关的结构体。例如,每个进程都有一个或几个线程,线程在系统中就是ETHREAD结构体。我来简要描述一下存在于这个结构体中的主要的信息,这些信息都是由对内核函数的研究而得知的。首先,结构体中有KPROCESS结构体,这个结构体中又有指向这些进程的内核线程(KTHREAD)链表的指针(分配地址空间),基优先级,在内核模式或是用户模式执行进程的线程的时间,处理器affinity(掩码,定义了哪个处理器能执行进程的线程),时间片值。在ETHREAD结构体中还存在着这样的信息:进程ID、父进程ID、进程映象名、section指针。quota定义了所能使用的分页和非分页池的极限值。VAD(virtual  address  descriptors)树定义了用户地址空间内存区的状况。关于Working Set的信息定义了在给定时间内有那些物理页是属于进程的。同时还有limit与statistics。ACCESS TOKEN描述了当前进程的安全属性。句柄表描述了进程打开的对象的句柄。该表允许不在每一次访问对象时检查访问权限。在EPROCESS结构体中还有指向PEB的指针。

ETHREAD结构体还包含有创建时间和退出时间、进程ID和指向EPROCESS的指针,启动地址,I/O请求链表和KTHREAD结构体。在KTHREAD中包含有以下信息:内核模式和用户模式线程的创建时间,指向内核堆栈基址和顶点的指针、指向服务表的指针、基优先级与当前优先级、指向APC的指针和指向TEB的指针。KTHREAD中包含有许多其它的数据,通过观察这些数据可以分析出KTHREAD的结构。


01.页表
==================

通常操作系统使用页表来进行内存操作。在Windows NT中,每一个进程都有自己私有的页表(进程的所有线程共享此页表)。相应的,在进程切换时会发生页表的切换。为了加快对页表的访问,硬件中有一个translation lookaside buffer(TLB)。在Windows NT中实现了两级的转换机制。在386+处理器上将虚拟地址转换为物理地址过程(不考虑分段)如下:

 Virtual Address
+-------------------+-------------------+-----------------------+
|3 3 2 2 2 2 2 2 2 2|2 2 1 1 1 1 1 1 1 1|1 1                    |
|1 0 9 8 7 6 5 4 3 2|1 0 9 8 7 6 5 4 3 2|1 0 9 8 7 6 5 4 3 2 1 0|
+-------------------+-------------------+-----------------------+
|  Directory index  |  Page Table index |    Offset in page     |
+-+-----------------+----+--------------+-----+-----------------+
  |                      |                    |
  |                      |                    |
  |  Page Directory (4Kb)|  Page Table (4Kb)  |    Frame(4Kb)
  |  +-------------+     |  +-------------+   |  +-------------+
  |  |     0       |     |  |     0       |   |  |             |
  |  +-------------+     |  +-------------+   |  |             |
  |  |     1       |     |  |     1       |   |  |             |
  |  +-------------+     |  +-------------+   |  |             |
  |  |             |     +->|    PTE      +-+ |  |             |
  |  +-------------+        +-------------+ | |  | ----------- |
  +->|    PDE      +-+      |             | | +->| byte        |
     +-------------+ |      +-------------+ |    | ----------- |
     |             | |      |             | |    |             |
     +-------------+ |      +-------------+ |    |             |
     |             | |      |             | |    |             |
        ...          |        ...           |    |             |
     |    1023     | |      |    1023     | |    |             |
CR3->+-------------+ +----->+-------------+ +--->+-------------+

Windows NT 4.0使用平面寻址。NT的地址空间为4G。这4G地址空间中,低2G(地址0-0x7fffffff)属于当前用户进程,而高2G(0x80000000-0xffffffff)属于内核。在上下文切换时,要更新CR3寄存器的值,结果就更换了用户地址空间,这样就达到了进程间相互隔绝的效果。

注:在Windows NT中,从第4版起,除4Kb的页之外同时还使用了4Mb的页(Pentium及更高)来映射内核代码。但是在Windows NT中没有实际对可变长的页提供支持。

PTE和PDE的格式实际上是一样的。

PTE
+---------------+---------------+---------------+---------------+
|3 3 2 2 2 2 2 2|2 2 2 2 1 1 1 1|1 1 1 1 1 1    |               |
|1 0 9 8 7 6 5 4|3 2 1 0 9 8 7 6|5 4 3 2 1 0 9 8|7 6 5 4 3 2 1 0|
+---------------------------------------+-----------------------+
|                                       |T P C U R D A P P U R P|
|  Base address 20 bits                 |R P W         C W S W  |
|                                       |N T           D T      |
+---------------------------------------+-----------------------+

一些重要的位在i386+下的定义如下:
---------------------------------------------------------------------------
P     - 存在位。此位如果未设置,则在地址转换时会产生异常。一般说来,在一些情况下NT内核会使用未设置此位的PTE。
        例如,如果向pagefile换出页,保留这些位可以说明其在页面文件中的位置和pagefile号。
U/S   - 是否能从user模式访问页。正是借助于此位提供了对内核空间的保护(通常为高2G)。
RW    - 是否能写入

NT使用的为OS设计者分配的空闲位
---------------------------------------------------------------------------
PPT   - proto pte
TRN   - transition pte

当P位未设置时,第5到第9位即派上用场(用于page fault处理)。它们叫做Protection Mask,样子如下:
--------------------------------------------------------------------------------------

* MiCreatePagingFileMap

9 8 7 6 5
---------
| | | | |
| | | | +- Write Copy
| | | +--- Execute
| | +----- Write
| +------- NO CACHE
+--------- Guard

GUARD | NOCACHE组合就是NO ACCESS


* MmGetPhysicalAddress

函数很短,但能从中获得很多信息。在虚地址0xc0000000  -  0xc03fffff上映射有进程的页表。并且,映射的机制非常精巧。在Directory Table(以下称DT)有1100000000b个表项(对应于地址0xc000..-0xc03ff..)指向自己,也就是说对于这些地址DT用作了页表(Page Table)!如果我们使用,比如说,地址(为方便起见使用二进制)

     1100000000.0000000101.0000001001.00b
        ---------- ---------- --------------
     0xc0...    页表选择   页表内偏移
     页目录      
           
通过页表101b的1001b号,我们得到了PTE。但这还没完——DT本身映射在地址0xc0300000-0xc0300ffc上。在MmSystemPteBase中有值0xc0300000。为什么这样——看个例子就知道了:

     1100000000.1100000000.0000001001.00b
        ---------- ---------- --------------
     0xc0...    0xc0...    页目录偏移
     页目录     页表-
                页目录
                选择
               
最后,在c0300c00包含着用于目录本身的PDE。这个PDE的基地址的值保存在MmSystemPageDirectory中。同时系统为映射物理页MmSystemPageDirectory保留了一个PTE,这就是MmSystemPagePtes。

这样做能简化寻址操作。例如,如果有PTE的地址,则PTE描述的页的地址就等于PTE<<10。反过来:PTE=(Addr>>10)+0xc0000000。

除此之外,在内核中存在着全局变量MmKseg2Frame = 0x20000。该变量指示在从0x80000000开始的哪个地址区域直接映射到了物理内存,也就是说,此时虚拟地址0x80000000 - 0x9fffffff映射到了物理地址00000000-1f000000。

还有几个有意思的地方。从c0000000开始有个0x1000*0x200=0x200000=2M的描述地址的表(0-7fffffff)。描述这些页的PDE位于地址c0300000-0xc03007fc。对于i486,在地址c0200000-c027fffc应该是描述80000000到a0000000的512MB的表,但对于Pentium在区域0xc0300800-0xc03009fc是4MB的PDE,其描述了从0 到1fc00000的步长为00400000的4M的物理页,也就是说选择了4M的页。对应于这些PDE的虚地址为80000000, 9fffffff。

这样我们就得到了页表的分布:

范围 c0000000 - c01ffffc  用于00000000-7fffffff的页表
范围 c0200000 - c027ffff  "吃掉" 4M地址页的地址
范围 c0280000 - c02ffffc  包含用于a0000000 - bfffffff的页
范围 c0300000 - c0300ffc  PD 本身 (描述范围c0000000 - c03fffff)
范围 c0301000 - c03013fc  c0400000 - c04fffff HyperSpace  (更准确的说, 是1/4的hyper space)
范围 c0301400 - c03fffff  包含用于c050000 - ffffffff的页

注:在0xc0301000-0xc0301ffc包含有描述hyper space的页表。这是内核的地址空间,且对于不同的进程映射的内容是不同的(另一方面,内核空间又总是在每个用户进程的上下文中)。这是进程私有的区域。例如,working set就位于hyper space中。页表的前256个PTE(hyper space的前1/4)为内核保留,而且在需要快速向frame中映射虚拟地址时使用。

我给出一个向区域0xc0200000-0xc027f000中一个地址进行映射的例子。

1100000000.1000000000.000000000000 = 0xc0200000

1) 解析出 PDE #1100000000 (4k 页) 并选出 PageDirectory
2) 在 Directory 中选出 PTE #1000000000 (c0300800)
   这是个 4MB 的 PDE - 但这里忽略位长度,
   因为 PDE 用作了 PTE. 结果 c0200000 - c0200fff 被映射为
   80000000-80000fff

   c0201000 映射到下面的 - 80400000- 80400fff.
   等等直到 c027f000 - 9fc00000

PTE, 位于c0200000到c027fffc - 描述了80000000 -  9ffffc00 (512m)

02.Hyper Space
==============

HyperSpace是内核空间中的一块区域 (4mb), 不同的进程映射内容不同。对于转换,4MB足够放下页表完整的一页。这个表位于地址0xc0301000 - 0xc0301ffc(PDE的第0个表项位于0xc0300c04)。在内部,为向HyperSpace区域中映射物理页(当需要快速为某个frame组织虚拟地址时)要使用函数:

DWORD MiMapPageInHyperSpace(DWORD BaseAddr,OUT PDWORD Irql);

它返回HyperSpace中的虚拟地址,这个虚拟地址被映射到所要的物理页上。这个函数是如何工作的,工作的时候用到了什么?

在内核中有这样的变量:

MmFirstReservedMappingPte=0xc0301000
MmLastReservedMappingPte=0xc03013fc

这两个变量描述了255个pte,这些pte描述了区域:

0xc0400000-0xc04fffff (1/4 HyperSpace)

在MmFirstReservedMappingPte处是一个pte,其中的基址扮演了计数器的角色(从0到255)(当然,pte是无效的,p位无效)。为所需地址添加pte时要依赖计数器当前的值……并且计数器使用了下开口堆栈的原理,从ff开始。一般来说,页表中的pte用作信息上的目的并不是唯一的情况。


03.System PTE'S
===============

在内核中有一块这样的内存——系统pte。什么是系统pte,以及内核如何使用系统pte?

*见函数 MiReserveSystemPtes(...)

系统为空闲PTE维护了某些结构体。首先为了快速满足密集请求(当内核需要pte映射某些物理页时)系统中有个Sytem Ptes Pool。而且pool中有pte blocks(blocks表示请求是以block为单位来满足的,一个block中有一些pte,1、2、4、8和16个pte)。

系统中有以下这些表:

BYTE MmSysPteTables[16]={0,0,1,2,2,3,3,3,3,4,4,4,4,4,4,4,4};
DWORD MmSysPteIndex[5]={1,2,4,8,16};
DWORD MmFreeSysPteListBySize[5];
PPTE  MmLastSysPteListBySize[5];
DWORD MmSysPteListBySizeCount[5];
DWORD MmSysPteMinimumFree[5]={100,50,30,20,20}
PVOID MmSystemPteBase;// 0xc0200000

在pool中的空闲PTE被组织成了链表(当然,pte是位于页表中,也就是说链表结构体位于页表中,这是真的)。链表的元素:

typedef struct _FREE_SYSTEM_PTES_BLOCK{
/*pte0*/ SYSPTE_REF NextRef;                 // 指向后面的block
/*pte1*/ DWORD FlushUnkn;                    // 在Flush时使用
/*pte2*/ DWORD ArrayOfNulls[ANY_SIZE_ARRAY]; // 空闲 PTE
     }FREE_SYSTEM_PTES_BLOCK PFREE_SYSTEM_PTES_BLOCK;

用作指向后面元素指针的PTE的地址可如此获得:VA=(NextRef>>10)+MmSystemPteBase (低10位永远为0,相应的p位也为0)。链表最后一个元素NextRef域的值为0xfffff000 (-1) 。相应的,链表有5个(block大小分别为1,2,4,8和16个pte)。

*见函数 MiReserveSystemPtes2(...) / MiInitializeSystemPtes

除pool外还有一个undocumented的空闲系统pte链表。

PPTE MmSystemPtesStart[2];
PPTE MmSystemPtesEnd[2];
SYSPTE_REF MmFirstFreeSystemPte[2];
DWORD MmTotalFreeSystemPtes[2];

在两个链表中有两个引用。链表的元素:

typedef struct _FREE_SYSTEM_PTES{
  SYSPTE_REF Next; // #define ONLY_ONE_PTE_FLAG 2, last = 0xfffff000
  DWORD NumOfFreePtes;
}FREE_SYSTEM_PTES PFREE_SYSTEM_PTES;

而且,1号链表原则上没有组织。0号链表(MiReleaseSystemPtes)用于释放的pte。pte有可能进入System Ptes Pool。若在请求MiReserveSystemPtes(...)时pte的数目大于16,则同时pte从0号链表分配。也就是说,0号链表与pool有关联,而1号则没有。

为了使工作的结果不与TLB相矛盾,系统要么使用重载cr3,要么使用命令invlpg。“高级”函数

MiFlushPteList(PTE_LIST* PteList, BOOLEAN bFlushCounter, DWORD PteValue);

进行以下工作:

初始化PTE并调用invlpg(汇编指令)。

typedef struct PTE_LIST{
     DWORD Counter; // max equ 15
     PVOID PtePointersInTable[15];
     PVOID PteMappingAddresses[15];
     };

如果Counter大于15,则调用KeFlushCurrentTb(只是重载CR3),并且如果设置了bFlushCounter,则向MmFlushCounter加0x1000。


04.Page Frame Number Data Base (MmPfnDatabase)
======================================

内核将有关物理页的信息保存在pfn数据库中(MmPfnDatabase)。本质上讲,这只是个0x18字节长的结构体块。每一个结构体对应一个物理页(顺序排列,所以元素常被称为Pfn - page frame number)。结构体的数量对应于系统中4KB页的数量(或者说是内核可见的页的数量,需要的话可以在boot.ini中使用相应的选项来为NT内核做出这块“坏”页区)。通常,结构体形式如下:

typedef struct _PfnDatabaseEntry
    {
    union {
    DWORD NextRef; // 0x0 如果frame在链表中,则这个就是frame的号
                   // 最后的一个为 -1
    DWORD Misc;    // 同时另外一项信息, 依赖于上下文
                   // 见伪代码 (通常 TmpPfn->0...)
                   // 通常这里有 *KTHREAD, *KPROCESS,
             // *PAGESUPPORT_BLOCK...
          };
    PPTE PtePpte;  // 0x4 指向 pte 或 ppte
    union {        // 0x8
          DWORD PrevRef;      // 前面的frame或 (-1, 第一个)
          DWORD ShareCounter; // Share 计数器
          };
    WORD Flags;      // 0xc 见下面
    WORD RefCounter; // 0xe 引用计数
    DWORD Trans;     // 0x10 ?? 见下面. 用于 pagefile
    DWORD ContFrame;//ContainingFrame; // 14
    }PfnDatabaseEntry;
/*
Flags (名字取自windbg !pfn的结果)

掩码   位     名字  值
-----  ----   ---   --------
0001   0      M     Modifyied
0002   1      R     Read In Progress
0004   2      W     WriteInProgress
0008   3      P     Shared
0070   [4:6]  Color Color (In fact Always null for x86)
0080   7      X     Parity Error
0700   [8:10] State 0- Zeroed
              /List 1- Free
                    2- StandBy
                    3- Modified
                    4- ModifiedNoWrite
                    5- BadPage
                    6- Active
                    7- Trans
0800   11     E     InPageError

Trans域的值用在frame的内容位于PageFile中的时候或是frame的内容位于与这个Page File PTE对应的其它映象文件中的时候。

我给出未设置P位的PTE的例子(这种PTE不由平台体系结构确定,而由OS确定)。

* 取自 @MiReleasePageFileSpace (Trans)

Page File PTE
+---------------------------------------+-+-+---------+-------+-+
|3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1|1|1|0 0 0 0 0|0 0 0 0|0|
|1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2|1|0|9 8 7 6 5|4 3 2 1|0|
+---------------------------------------+-+-+---------+-------+-+
| offset                                |T|P|Protect. |page   |0|
|                                       |R|P|mask     |file   | |
|                                       |N|T|         |Num    | |
+---------------------------------------+-+-+---------+-------+-+

Transition PTE
+---------------------------------------+-+-+---------+-------+-+
|3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1|1|1|0 0 0 0 0|0 0 0 0|0|
|1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2|1|0|9 8 7 6 5|4 3 2 1|0|
+---------------------------------------+-+-+---------+-------+-+
| PFN                                   |T|P|Protect. |C W O W|0|
|                                       |R|P|mask     |D T    | |
|                                       |N|T|         |       | |
+---------------------------------------+-+-+---------+-------+-+

W - write
O - owner
WT - write throuth
CD - cache disable

可能所有这些现在还不很易懂,但是看完下面就能明白了。当然,这个结构体是未公开的。显然,结构体能够组织成链表。frame由以下结构体支持:

struct _MmPageLocationList{

PPfnListHeader ZeroedPageListhead;         //&MmZeroedPageListhead
PPfnListHeader FreePageListHead;           //&MmFreePageListHead
PPfnListHeader StandbyPageListHead;        //&MmStandbyPageListHead
PPfnListHeader ModifiedPageListHead;       //&MmModifiedPageListHead
PPfnListHeader ModifiedNoWritePageListHead;//&MmModifiedNoWritePageListHead
PPfnListHeader BadPageListHead;            //&MmBadPageListHead
}MmPageLocationList;

这其中包含了6个链表。各域的名字很好的说明了它们的用处。frame的状态与这些链表密切关联。下面列举了frame的状态:

+---------------+----------------------------------------------------+------+
|状态           |描述                                                | 链表 |
+---------------+----------------------------------------------------+------+
|Zero           |清零的可用空闲页                                    |  0   |
|Free           |可用空闲页                                          |  1   |
|Standby        |不可用但可轻易恢复的页                              |  2   |
|Modified       |要换出的dirty页                                     |  3   |
|ModifiedNoWrite|不换出的dirty页                                     |  4   |
|Bad            |不可用的页(有错误)                                |  5   |
|Active         |活动页,至少映射一个虚拟地址                        |  -   |
+---------------+----------------------------------------------------+------+

frame可能处在6个链表中的某一个,也可能不在这些链表中(状态为Active)。如果页属于某个进程,则这个页就被记录在Working Set中(见后面)。同时,如果frame由内存管理器自己使用,则一般可以不考虑这些frame的位置。

每个链表的表头都是下面这个样子:

typedef struct _PfnListHeader{
     DWORD Counter; // 链表中frame的数目
     DWORD LogNum;  // 链表号.0 - zeroed, 1- Free etc...
     DWORD FirstFn; // MmPfnDatabase中的第一个frame号
     DWORD LastFn;  // --//--- 最后一个.
     }PfnListHeader PPfnListHeader;

除此之外,可以用“color”(就是cache)来寻址空闲frame(zeroed或是free)。如果看一下附录中的伪代码就容易理解了。我给出两个结构体:

struct  {
     ColorHashItem* Zeroed; //(-1) нет
     ColorHashItem* Free;
     }MmFreePagesByColor;

typedef struct _ColorHashItem{
          DWORD FrameNum;
                PfnDatabaseEntry* Pfn;
                } ColorHashItem;

有一套函数使用color来处理frame(处理cache)。例如,MiRemovePageByColor(FrameNum, Color); 看一下这些函数及其参数返回值的名称和函数的反汇编代码,很容易猜到相应的内容,所以这里就不描述了,在说一句,这些函数都是未导出的。在使用color的时候,要考虑color掩码,最后选择color。

Windows NT符合C2安全等级,所以应该在为进程分配页的时候应将页清零。我们来看一下将frame清零的系统进程的线程。最后,在Phase1Initialization()中所作的是调用MmZeroPageThread。不难猜到——线程将空闲页清零并将其移动到zeroed页的链表中。

MmZeroPageThread
{
//
//.... 没意思的东西我们略过 ;)
//
while(1)
 {
 KeWaitForSingleObject(MmZeroingPageEvent,8,0,0,0); // 等待事件
 while(!KeTryToAcquireSpinLock(MmPfnLock,&OldIrql)); // 获取 PfnDatabase
 while(MmFreePageListHead.Count){
            MiRemoveAnyPage(MmFreePageListHead.FirstFn&MmSecondaryColorMask);
               // 从空闲链表中取出页
            Va=MiMapPageToZeroInHyperSpace(MmFreePageListHead.FirstFn);
            KeLowerIrql(OldIrql);

            memset(Va,0,0x1000); // clear page

            while(!KeTryToAcquireSpinLock(MmPfnLock,&OldIrql);
            MiInsertPageInList(&MmZeroedpageListHead,FrameNum);
                         // 将已清零的页插入Zero链表
            }
  MmZeroingPageThreadActive=0; // 清标志
  KeLowerIrql(OldIrql);
  }
// 永不退出
}

// 函数只是将frame映射到定义的地址上
// 以使其可被清零
DWORD MiMapPageToZeroInHyperSpace(FrameNum)
{
 if(FrameNum                                      
 TmpPte=0xc0301404;
 TmpVa=0xc0501000;
 *TmpPte=0;
 invlpg((void*)TmpVa); // asm instruction in fact
 *TmpPte=FrameNum<<12|ValidPtePte;
 return TmpVa; // always 0xc0501000;
}

在何时MmZeroingPageEvent被激活?这发生在向空闲页链表中添加frame的时候:

MiInsertPageInList()
{
.....
if(MmFreePageListHead.Count>=MmMinimumFreePagesToZero&&
       !MmZeroingPageThreadActive)
    {
     MmZeroingPageThreadActive=1;
     KeSetEvent(&MmZeroingPageEvent,0,0);
    }
....
}

注:内核并不总是依赖这个线程,有时会遇到这样的代码,它获取一个空闲页,用过后自己将其清零。

05.Working Set
==============
Working Set——工作集,是属于当前进程的物理页集。内存管理器使用一定的机制跟踪进程的工作集。working set有两个限额:maximum  working set和minimum working set。这是工作集的最大值和最小值。内存管理器以这两个值为依据来维护进程的工作集(工作集大小不小于最小值,不大于最大值)。在定义条件的时候,工作集被裁减,这时工作集的frame落入空闲链表。内核工作集是结构体的总和。

在进程结构体的偏移0xc8(NT4.0)有以下结构体。

typedef struct _VM{
/* C8*/   LARGE_INTEGER UpdateTime;              //0
/* D0*/   DWORD Pages;                           //8 called so, by S-Ice authors
/* D4*/   DWORD PageFaultCount                   //0c faults;
//                    in fact number of MiLocateAndReserveWsle calls
/* D8*/   DWORD PeakWorkingSetSize;              //10 all
/* DC*/   DWORD WorkingSetSize;                  //14  in
/* E0*/   DWORD  MinimumWorkingSet;              //18   pages, not in
/* E4*/   DWORD  MaximumWorkingSet;              //1c     bytes
/* E8*/   PWS_LIST WorkingSetList;               //20 data table
/* EC*/   LIST_ENTRY WorkingSetExpansion;        //24 expansion
/* F4*/   BYTE fl0; // Operation???              //2c
     BYTE fl1; // always 2???               //2d
     BYTE fl2; // reserved??? always 0      //2e
     BYTE fl3; //                           //2f
     }VM *PVM;

WinDbg !procfields的扩展命令用到VM。这里重要的是,跟踪page fault的数量(PageFaultCount),MaximumWorkingSet和MinimumWorkingSet,管理器以它们为基础来支持工作集。

注:实际上,PageFaultCount并非是严格的计数。这个计数在MiLocateAndReserveWsle函数中被扩大,因为这个函数不只在page fault时被调用,在某些其它情况下也会被调用(真的,很少见)。

下面这个结构体描述了包含工作集页的表。

typedef struct _WS_LIST{
DWORD        Quota;              //0 ??? i'm not shure....
DWORD        FirstFreeWsle;      // 4 start of indexed list of free items
DWORD        FirstDynamic;       // 8 Num of working set wsle entries in the start
                                 // FirstDynamic
DWORD        LastWsleIndex;      // c above - only empty items
DWORD        NextSlot;           // 10 in fact always == FirstDynamic
                                 // NextSlot
PWSLE        Wsle;               // 14 pointer to table with Wsle
DWORD        Reserved1           // 18 ???
DWORD        NumOfWsleItems;     // 1c Num of items in Wsle table
                       // (last initialized)
DWORD        NumOfWsleInserted;  // 20 of Wsle items inserted (WsleInsert/
                                 //                              WsleRemove)
PWSHASH_ITEM HashPtr;            // 24 pinter to hash, now we can get index of
                       //   Wsle item by address. Present only if
                                 //   NumOfWsleItems>0x180
DWORD        HashSize;           // 28 hash size
DWORD        Reserved2;          // 2c ???
}WS_LIST *PWS_LIST;

typedef struct _WSLE{ // 工作集表的元素
        DWORD PageAddress;
     }WSLE *PWSLE;

// PageAddress 本身是工作集页的虚地址
// 低12位用作页属性(虚地址总是4K的倍数)

#define WSLE_DONOTPUTINHASH 0x400 // 不放在cache中
#define WSLE_PRESENT 0x1 // 非空元素
#define WSLE_INTERNALUSE 0x2 // 被内存管理器使用的frame

// 未设置WSLE_PRESENT的空闲WSLE本身是下一个空闲WSLE的索引。这样,空闲的WSLE就组织成了链表。最后一个空闲WSLE表示为-1。

#define EMPTY_WSLE (next_emty_wsle_index) (next_emty_wsle_index<<4)
#define LAST_EMPTY_WSLE 0xfffffff0

typedef struct _WSHASH_ITEM{
     DWORD PageAddress; //Value
     DWORD WsleIndex; //index in Wsle table
}WSHASH_ITEM *PWSHASH_ITEM;

//cache函数很简单。内部函数的伪代码:
//MiLookupWsleHashIndex(Value,WorkingSetList)
//{
//Val=value&0xfffff000;
//TmpPtr=WorkingSetList->HashPtr;
//Mod=(Val>>0xa)%(WorkingSetList->HashSize-1);
//if(*(TmpPtr+Mod*8)==Val)return Mod;
//while(*(TmpPtr+Mod*8)!=Val)){
//   Mod++;
//   if(WorkingSetList->HashSize>Mod)continue;
//   Mod=0;
//   if(fl)KeBugCheckEx(0x1a,0x41884,Val,Value,WorkingSetList);
//   fl=1;
//   }
//return Mod;
//}

我们来看一下典型的进程working set。WorkingSetList位于地址MmWorkingSetList (0xc0502000)。这是hyper space的区域,所以在进程切换时,要更新这些虚地址,这样,每个进程都有自己的工作集结构体。在地址MmWsle (0xc0502690)上是Wsle动态表的起始地址。表的结尾的地址总是0x1000的倍数,也就是说表可以结束在地址0xc0503000、0xc0504000等等上(这是为了简化对Wsle表大小的操作)。Cache(如果有)位于一个偏移上,Wsle不会向这个偏移增长。我们来详细看一下这个表:

// WsList-0xc0502000---
// ....
// -------0xc0502030----
// pde 00 fault counter
// pde 01 fault counter
// pde 02 fault counter
//
// +-Wsle==0xc0502690---             +--Pde/pte     +-----Pfn[0]------
// |0 c0300000|403 Page Directory    |c0300c00 pde  |pProcess
// |4 c0301000|403 Hyper Space       |c0300c04 pte  |1
// |8 MmWorkingSetList(c0502000)|403 |c0301408 pte  |2
// |c MmWorkingSetList+0x1000 | 403  |.             |3
// |10 MmWorkingSetList+0x2000 | 403 |.              .
// |         ....
// |FirstDynamic*4 FrameN
// |....                             |.              .
//                                                   .
// |LastWsleIndex*4 FrameM
// +--------                         +------        +-------
// | free items
// ....
// | 0xfffffff0
// +-------------------


// Cache
// ....

这里有个有意思的地方,在表的起始部分有FirstDynamic的页,用于建立Wsle,WorkingSetList和cache。同时这里还有页目录frame,HyperSpace和某些其它的页,这些页是内存管理器所需要的,不能从工作集中移出(标志WSLE_INTERNALUSE)。之后,我们还能看到两种对Pfn frame域偏移0使用的变体。对于页目录frame,这是指向进程的指针,对于通常的属于工作集的页,这是在表内的索引。

在WorkingSetList和Wsle表的起始地址之间还有不大的0x660字节的空闲空间。关于如何分配这些空间的信息是没有的,但是很快在WorkingSetList开始有用于用户空间(通常为低2GB)的page fault counter,也就是说如果,譬如说,索引0x100的元素有值3,则表示从3开始(如果不考虑可能的溢出)page fault用于范围[0x40000000-0x403fffff]的页。

工作集的限额在内核模式下可以通过导出的未公开函数来修改:

NTOSKRNL MmAdjustWorkingSetSize(
          DWORD MinimumWorkingSet OPTIONAL, // if both == -1
          DWORD MaximumWorkingSet OPTIONAL, // empty working set
          PVM Vm OPTIONAL);

为处理WorkingSet,管理器使用了许多内部函数,了解了这些函数就能明白其工作的原理。


06.向pagefile换页
========================================

frame可以是空闲的——当RefCounter等于0且位于一个链表中时。frame可以属于工作集。在缺少空闲frame时或是在达到treshhold时,就会发生frame的换出。这方面的高层次函数是有的。这里的任务是用伪代码来证实。

在NT中有最多16个pagefile。pagefile的创建发生于模块SMSS.EXE。这时打开文件及其句柄向PsInitialSystemProcess进程的句柄表拷贝。我给出创建pagefile的未公开系统函数的原型(如果不从核心调用的话就必须有创建这种文件的权限)。

NTSTATUS NTAPI NtCreatePagingFile(
     PUNICODE_STRING FileName,
     PLARGE_INTEGER MinLen, // 高位双字应为0
     PLARGE_INTEGER MaxLen, // minlen应大于1M
     DWORD Reserved // 忽略
       );

每个pagefile都有一个PAGING_FILE结构体。

typedef struct _PAGING_FILE{
       DWORD MinPagesNumber;      //0
       DWORD MaxPagesNumber;      //4
       DWORD MaxPagesForFlushing; //8 (换出页的最大值)
       DWORD FreePages;           //c(Free pages in PageFile)
       DWORD UsedPages;           //10 忙着的页
       DWORD MaxUsedPages;        //14
       DWORD CurFlushingPosition; //18 -???
       DWORD Reserved1;           //1c
       PPAGEFILE_MDL Mdl1; //       20 0x61 - empty ???
       PPAGEFILE_MDL Mdl2; //       24 0x61 - empty ???
       PRTL_BITMAP PagefileMap; //  28 0 - 空闲, 1 - 包含换出页
       PFILE_OBJECT FileObject;   //2c
       DWORD NumberOfPageFile;    //30
       UNICODE_STRING FileName;   //34
       DWORD Lock;                //3d
     }PAGING_FILE *PPAGING_FILE;

DWORD MmNumberOfActiveMdlEntries;
DWORD MmNumberOfPagingFiles;

#define MAX_NUM_OF_PAGE_FILES 16
PPAGING_FILE MmPagingFile[MAX_NUM_OF_PAGE_FILES];

在内存子系统启动时(MmInitSystem(...))会启动线程MiModifiedPageWriter,该线程进行以下工作:初始化MiPaging和 MiMappedFileHeader,在非换出域中创建并初始化MmMappedFileMdl,建立优先级LOW_REALTIME_PRIORITY+1,等待KEVENT,初始化MmMappedPageWriterEvent和MmMappedPageWriterList链表,启动MiMappedPageWriter线程,启动函数MiModifiedPageWriterWorker。

在任务MiModifiedPageWriterWorker中会等待事件MmModifiedPageWriterEvent,处理链表MmModifiedNoWritePageList和MmModifiedPageList并准备实现向映象文件或pagefile的页换出(调用MiGatherMappedPages或是MiGatherPagefilePages)。

在MiGatherPagefilePages中使用IoAsynchronousPageWrite( )函数进行frame的换出。而且不是一个frame,而是一簇(页数目总和为MmModifiedWriteClasterSize)。向pagefile换出页是由PAGING_FILE结构体中的PagefileMap来跟踪的。

研究函数的伪代码在appendix.txt中。这里描述伪代码没有什么意义——都很简单。

持续。。。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值