共享内存

Windows 2000 中进程之间共享内存的几个主要应用是, 一个 dll(动态链接库)可能被多个进程使用,应该被共享。一个程序也应该可以被多个运行的实例共

享。通过文件映射(Memory Mapped File)实现的进程之间通过内存通信,传输数据。

    每个进程有自己的4G地址空间,地址空间通过进程自己的页目录和页表,以页为单位映射物理内存。把两个不同进程的一页地址空间映射到同一个物理页上

,两个进程的一页地址空间就实现了共享,两个进程对该页地址空间的读写,都是对同一个物理页的读写。两个进程自己的某个 PTE(页表项,对应一页地址空

间)中的物理页帧号相同,就使两个进程的某页映射到了同一个物理页。比如进程1的0x10000-0x10FFF这一页映射物理内存的第100页,进程2的0x20000-

0x20FFF这一页也映射物理内存的第100页,那么进程1的0x10000-0x10FFF 的就是 进程2的0x20000-0x20FFF ,就是物理内存的第100页。


Prototype PTE(原型PTE)

    和共享的实现有着密切关系的一个数据结构是 页帧号数据库项 ( PFN DataBase Entry ) ,对处于Active(Valid) 状态的物理页,该物理页的 PFN DataBase

Entry 的 +08 处,4个字节,是 share count,共享计数。

下面我们分析一下进程间共享物理页的几种情况。

进程1第一次将一些可以被共享的内容读入一个物理页。于是进程1相应的PTE有效,并指向这个物理页。被共享的物理页对应的 PFN DataBase Entry 状态为

Active(Valid) ,share count 值为1。

进程2需要共享这个物理页,于是进程2相应的PTE有效,并指向这个物理页。由于这个物理页现在被进程1和进程2共享,所以对应的 PFN DataBase Entry 的

share count 的值加1,变为2。

进程3需要共享这个物理页,于是进程3相应的PTE有效,并指向这个物理页。这个物理页对应的 PFN DataBase Entry 的 share count 的值加1,变为3。

进程2修整 Working Set (工作集),决定把该页从自己的 Working Set 中移出,于是相应的PTE无效。这个物理页对应的 PFN DataBase Entry 的 share

count 的值减1,变为2。

进程1结束,这个物理页对应的 PFN DataBase Entry 的 share count 的值又减1,变为1。

进程2又需要访问这个共享页。于是PTE重新有效,并指向这个物理页。物理页的 share count 加1,又变为2。

进程2,进程3,修整 Working Set (工作集),都把这页从 Working Set 中移出,于是两个进程相应的PTE都无效。物理页的 share count 从 2 变成了 0。当

变成0时,该物理页的状态从 Active(Valid) 变成了 Standby,链入了 Standby 链,不过该物理页中的内容不会被改变。

进程2又需要访问这个共享页。由于该物理页在 Standby 链上,内容没有被改变。于是直接取回该物理页,PTE重新有效并指向这个物理页。物理页状态从

Standby 变为 Active(Valid) , share count 变为1。

进程2又修整 Working Set (工作集),决定把该页从自己的 Working Set 中移出,于是相应的PTE无效。这个物理页的 share count 的值减1,变为0。当变

成0时,该物理页的状态从 Active(Valid) 变成了 Standby,链入了 Standby 链,不过该物理页中的内容不会被改变。

系统需要内存,从 Standby 链上取走了该物理页。

进程2又需要访问这个共享页。没有物理页有原来的数据了,于是分配一个新的物理页,从文件中将数据读入。于是进程2相应的PTE有效,并指向这个新的物理

页。这个物理页对应的 PFN DataBase Entry 状态为Active(Valid) ,share count 值为1。

    需要强调的一点是,只有被共享的物理页的 share count 减为 0 时,才能被移入 Standby 链,然后被用来做其他事。如果 share count 不为0,就说明还有

进程在使用这个物理页,如果这时把这个物理页移入 Standby 链,可能会有非常严重的后果。比如放着被共享的dll的程序代码的一个物理页,被多个进程共享

,而其中一个结束,share count 减1但不为0,这时如果把这个物理页移入 Standby 链,系统又把这个物理页给清零,放入 Zeroed 链中待用。而这时其他几

个进程的相应的PTE仍然是有效的,并且指向这个本来该是程序代码的物理页。当这几个进程执行这里的代码的时候,将会彻底出错。如果 share count 为0 ,

所有进程相应的 PTE 都已经无效了。

    共享机制还有一个问题。一个被共享的物理页 share count 为0 ,并且已经被用来做其他事,所有进程相应的 PTE 都已经无效了。当一个进程访问相应 PTE

对应的地址空间,系统会分配一个新的物理页,从文件中将数据读入新的物理页,相应的PTE有效,并指向这个新的物理页,这样这个进程就可以访问这个共享

页。但是其他的进程无法知道这个新的物理页,他们相应的PTE仍然无效。他们如果也重新访问这个共享页,他们将如何得知新的物理页来填入PTE?Win2k 使

用 Prototype PTE 来解决这个问题。

    对于一个载入内存的应用程序,载入内存的动态链接库,或者一个文件映射,他们都有可能被共享。比如一个可能被共享的东西,映射到地址空间,需要100

页,那么每页都有一个相应的 Prototype PTE (4个字节)。一个进程把这个100页的共享的东西映射到地址空间,需要用100个PTE。这100个PTE中,有效的

PTE指向共享的物理页,无效的PTE将指向相应的 Prototype PTE ,Prototype PTE 指向物理页。这样一来,当遇到前面的问题,一个进程重新访问一个共享页

,而这个共享页原来映射的物理页已经做其他事,系统分配新的物理页,把进程相应的PTE指向新的物理页,并把PTE变为有效之后,也把 Prototype PTE 指向

新的物理页。其他的进程重新访问这个共享页时,他们的PTE是无效的,并且指向 Prototype PTE ,而 Prototype PTE 这时就已经指向了新的物理页,于是他

们就可以把相应的PTE指向新的物理页,并把PTE变为有效。

    载入内存的应用程序,载入内存的动态链接库,或者一个文件映射,他们都位于用户地址空间。为了描述方便我们把这些都叫做共享的东西。一个共享的东西

都会有一个位于系统地址空间的 Segment 结构,这个共享的东西的 Prototype PTE 都顺序的放在这个 Segment 结构的 ProtoPtes数组中。比如一个共享的东

西,映射到地址空间需要100页,那么它就有100个 Prototype PTE 放在它的 Segment 结构的 ProtoPtes数组中,并且它的第0页对应的 Prototype PTE 在数

组的第0项,它的第1页对应的 Prototype PTE 在数组的第1项。Segment 结构0x38字节长,其中比较重要的是偏移+34处的 4个字节长的 ProtoPtes。

ProtoPtes 中为 ProtoPtes数组 的首地址,不过通常情况下,ProtoPtes数组 就紧跟在 0x38字节长的 Segment 结构之后。Segment 结构偏移+8处4个字节长

的 Total Ptes 指明了 ProtoPtes数组 中元素的个数。Segment 结构偏移+0处的4个字节长的 ControlArea,是一个指向 ControlArea 结构的指针,

ControlArea 结构是一个很重要的结构,通过它我们可以找到相应的 File Object ,ControlArea 结构还有指回 Segment 结构 的指针。

有密切关系的几个结构是,映射了一个共享东西到一个进程地址空间,这块空间的 Vad

typedef struct _VAD_HEADER {
/*00*/ PVOID StartVPN;
/*04*/ PVOID EndVPN;
/*08*/ _VAD_HEADER* ParentLink;
/*0C*/ _VAD_HEADER* LeftLink;
/*10*/ _VAD_HEADER* RightLink;
/*14*/ ULONG CommitCharge : 20;
/*14*/ ULONG Flags : 12;
/*18*/ PVOID ControlArea;
/*1C*/ PVOID FirstProtoPte;
/*20*/ PVOID LastPTE;
/*24*/ ULONG Unknown;
/*28*/ LIST_ENTRY Secured;
/*30*/ } VAD_HEADER, *PVAD_HEADER;

Vad 结构偏移+18处4个字节的 ControlArea 是指向 ControlArea 结构的指针。

被共享物理页的 PfnDataBaseEntry

Zeroed 0x00
Free 0x01
Standby 0x02
Modified 0x03
Modified no-write 0x04
Bad 0x05
Active (Valid) 0x06
Transition 0x07

struct PfnDataBaseEntry (大小24个字节,即0x18个字节)
/*00*/ uint32 flink
/*04*/ uint32 pteaddress
/*08*/ uint32 blink / share count
/*0C*/ byte flags
/*0D*/ byte page state
/*0E*/ uint16 reference count
/*10*/ uint32 restore pte
/*14*/ uint32 containing page

处于 Active (Valid) 状态,被共享的物理页的 PfnDataBaseEntry 结构偏移+4处4个字节的 pteaddress ,
是 Prototype PTE 的地址。

实现机制

对于某个进程使用的某个共享页,PTE 有效时和正常一样,访问时 CPU 通过页目录,页表把虚拟地址转换成物理地址,访问内存。PTE 无效时,使用

Prototype PTE 保证共享机制的正确。PTE 无效时,系统设置 无效 PTE 的 Prototype 标志位。这样当一个进程访问该页的时候,因为PTE无效,所以会引起

Page-Fault 异常(Exception)。从而使 CPU 转去执行异常处理程序,异常处理程序检查PTE发现设置了 Prototype 标志,就会做相应的处理,重新使PTE有

效,并指向正确的物理页。最后 CPU 重新执行引起异常的指令,这时该指令所访问的虚拟地址已经是正确的共享物理页了,并且该虚拟地址的PTE也有效了,

于是就可以顺利执行。下面我们针对 x86 CPU 做更详细的说明。x86 CPU 的无效页表项定义如下

struct _HARDWARE_PTE_X86 (sizeof=4)
bits0-0 Valid
bits1-31 reserved

首先要说明的是,这个格式是由 CPU 定义的,CPU 将按照这个定义,对每一位做出解释,然后决定处理方式。
对于无效页来说,CPU只定义了bits0-0,用来判断是否有效。无效页的 bits0-0 值为0。其他都留给操作系统使用。Win2k 用 bits10-10 Prototype 来表示是否

使用 Prototype,该位为0表示不使用,为1表示使用。

当某页的页表项无效,并设置了 Prototype ,当某条指令访问该页时,比如指令 MOV EAX , AddressInPrototypePage ,执行这条指令时,CPU 会自动通过

页目录和页表把 虚拟地址AddressInPrototypePage 转换成物理地址,在地址转换过程中,CPU 在从页表项得到物理页地址的同时,会进行页保护检查,比如

看该页表项是否有效,是否是只读等等。在这里我们指令中地址的页表项无效,于是就会引发异常。异常也是由 CPU 实现的。这里引起的是一个 Page Fault

异常,它的中断号是 0xe (十进制14),需要注意的是 Page Fault 的中断号是 0xe 这是由 CPU 定义的( x86 CPU 的 从 0 - 31 这32个中断是由 CPU 定义的

,CPU 将根据这个定义做相应工作)。在发生异常时,CPU 自动把一些寄存器压入堆栈,然后根据中断号,(通过IDTR找到中断描述符表)在中断描述符表中

找到相应的中断描述符,根据中断描述符中的地址,转到异常处理程序。中断描述符是由Win2k设置,异常处理程序也是由Win2k决定。对于 Win2k Build

2195 来说,中断 0xe 的处理程序是 ntoskrnl!KiTrap0E 地址在 804648a4 。当转到 KiTrap0E 时,CPU 已经在堆栈中压入了下面的内容

|-------------|
|    EFLAGS   |
|-------------|
|      CS     |
|-------------|
|     EIP     |
|-------------|
| Error Code |
|-------------|<---- [ ESP ]


page-fault 异常 (#PF) 的 Error Code 定义如下( CPU 定义 )
                                   | 3 | 2 | 1 | 0 |
+---------------------------------------------------+
|          Reserved                |RSVD|U/S|R/W| P |
+---------------------------------------------------+

P   0 错误由无效页引起
    1 错误由违反页保护引起
W/R 0 引起错误的内存访问是读
    1 引起错误的内存访问是写
U/S 0 访问错误时处理器处在管理模式
    1 访问错误时处理器处在用户模式

需要说明的是堆栈中压入的 EIP 就是引发异常的指令地址,将来将根据这个地址重新执行该指令。而寄存器 cr2 中是引发异常时访问的地址。

#PF异常处理程序 KiTrap0E(由Win2k提供)将会调用 ntoskrnl!MmAccessFault ,MmAccessFault 通过 CR2 中的访问地址,计算出相应的 PDE,PTE地址,

检查 PTE 发现设置了 Prototype 标志(这个标志是由 Win2k 定义的,也是由它来处理),最终会调用 ntoskrnl!MiResolveProtoPteFault 来完成 Prototype

的相关工作。
把PTE指向正确的共享物理页,并把PTE重新设为有效。

执行完异常处理程序之后,CPU 重新执行引起异常的指令,这时指令可以正常执行了。至此 Prototype 已经被实现了。

对于 MmAccessFault 处理 Prototype 我们做更详细的说明。

通过分析汇编代码可以知道

    MmAccessFault 根据引起异常的访问地址,计算出相应的 PDE,PTE地址,发现 PTE 设置了 Prototype 标志。检查 PTE ,如果PTE 的高 20 bit 不为 0xfffff

,将根据公式 PrototypePteAddress=0xe1000000+((Pte>>2)&0x3ffffe00)+(Pte&0xff)*2; 计算出 PrototypePteAddress 。
    注意这个公式中,((Pte>>2)&0x3ffffe00)的值的范围 和 (Pte&0xff)*2的值的范围。由于是无效PTE,最低位本身就是0,(Pte&0xff)*2的值最低2位都会为0

,这也保证了 PrototypePteAddress 以4字节为边界。如果 PTE 的高 20 bit 为 0xfffff,将调用 MiCheckVirtualAddress()。MiCheckVirtualAddress()中调用

MiLocateAddress()。MiLocateAddress()将返回InValidAddress所在范围的 Vad 的地址。然后用下面的方法计算 PrototypePteAddress=(

(InValidAddress>>0xa)&0x3ffffc -TheVad.StartVPN<<0x2 ) + TheVad.FirstProtoPte 也就是计算 InValidAddress 的虚拟页号,用 InValidAddress 的虚拟

页号减去 Vad 中该范围的其实虚拟页号,然后找到 ProtoPtes数组 中的相应项的地址,就找到了 PrototypePteAddress 。

   然后 MmAccessFault 用 InValidAddress,InValidAddress_PteAddress,InValidAddress_PrototypePteAddress 等7个参数,调用

MiResolveProtoPteFault()。 MiResolveProtoPteFault()将根据 PrototypePte 的各种标志,做不同的处理。对于 PrototypePte 最低位为1,PrototypePte

有效。表明 PrototypePte 中的物理页号,就是正确的被共享的物理页的页号。调用MiCompleteProtoPteFault(),将 PrototypePte 中的物理页号设置到 Pte

中,把 Pte 变为有效就可以了。对于 PrototypePte 最低位为0,PrototypePte 无效。就会检查 PrototypePte 中,看被共享的物理页是不是被放入了

Standby 链,如果是,通过 PrototypePte 中的物理页号,把该物理页移回来,把 PrototypePte 变为有效,把 Pte 指向这个物理页,把 Pte 也变为有效。如

果原来被共享的物理页,已经彻底的不在了,就申请新的物理页,从文件中重新读入被共享的内容,把 PrototypePte 指向这个新的物理页,并且把

PrototypePte 变为有效,把 Pte 指向这个物理页,把 Pte 也变为有效。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值