Windows NT File System Internals----Chapter 5 The NT Virtual Memory Manage

//-----------------------------------------------------------------------------------------------------------------------

//sahikaru(sahikaru88@gmail.com)

//2012.6.13

//英文翻译笔记

//----------------------------------------------------------------------------------------------------------------------

这段时间在学习文件系统,把文件系统内幕这本书看了下,但是,很容易忘记知识,所以把这些翻译出来。并做些笔记。一不小心就坚持了下来了,共勉之。

在用户进程地址空间那部分没翻译完,其实都知道怎么回事。有时间补上。

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

         现代的操作系统的一个重要的个功能就是管理物理地址。典型的,电脑上大多数有效的RAM比所有的应用程序和操作系统要求的还要少。因此,操作系统必须干预和分享他的有限的资源,给那经常争抢需求的组件。

         在不久,在同一个机器上运行的不同的程序,操作系统能保证他们能独立运行。因此,每个程序的代码和数据结构必须安排妥当,使得其他进程不能干扰。操作系统必须也保护他自己的内存资源(代码和数据)不受进程的干扰,可以通过管理系统来实现。这需要保证机器的完整和安全。最后,在同一个机器上的交互程序经常需要分享内存数据。系统必须实现顺序分享数据使得只有一些有着允许的程序能够分享特定的代码和数据。

         TheVirtual Manager (VMM)虚拟内存管理负责提供所有这些功能。VMM就是因为能帮助提供系统程序的执行的抽象而命名的:每个程序在相信系统内所有的内存资源都为每个程序所用的条件下执行他自己的任务。而且,程序相信自己能执行无限大小的内存资源。这个无限大小的为单个程序使用的内存就是虚拟内存的抽象。VMM负责提供这个抽象的是内核模式组件。

        

         NT VMM提供给其他组件的功能有:

  • VMM提供了一个需求页(有聚合支持),虚拟内存系统。每个进程有自己的稀有虚拟内存空间与他联系。这个虚拟内存空间是基于物理页的,从总的可以分配的物理页池中按需分配。
  • 管理进程的虚拟地址空间是和物理页分离的。VMM为控制虚拟地址空间的分配,提交,维护和释放的应用提供支持,
  • 虚拟内存支持有本地文件系统的支持。为了提供幻想的大量可用的内存(大于RAM的实际内存),内存的内容都是是存储在二级存储介质的。基于磁盘的内存存储叫做提交内存(committed memory)。提交内存是基于可以动态改变大小的页文件或者是在二级存储器中的数据\镜像文件。
  • VMM提供内存映射文件的支持,这些文件可以是随意大小;文件大于2GB可以使用分文件视图映射(be mapedusing partial views of the file)。
  • 它提供在系统的不同进程之间共享内存。这个也被用于进程间的通信。
  • 它完成每个进程的配额。
  • 它管理进程物理地址的分配,也就是工作集。
  • 而且,所有物理地址分配和释放决定都是VMM来决定的。这个在进程间的做法是不论是用户程序创建还是内核文件爱你数据的缓存。
  • 它使用访问控制列表(Access Control Lists)提供保护内存的支持。
  • 它提供POSIX fork和exec操作,因此能够符合POSIX标准。
  • 它提供写时复制(copy-on-write)支持。可以建立保护页和设置页等级保护。

 

 

进程地址空间

         然后是系统内存分配。高2GB和低2GB。

        

尽管2GB的用户模式虚拟地址是关于进程私有的数据,2GB内核模式地址在系统中是关于相同的物理页(不算他们访问的线程上下文),包含着操作系统的代码和数据。

         另一个概念要掌握的是4GB虚拟地址空间的每个进程的超空间。这个超空间是一段从2GB的内核空间中保留出的虚拟地址,但是通过特别设计,因为他包括了进程的内部数据结构,这些结构是NT VMM维持的。任何时候一个上下文切换发生,VMM询问这个虚拟地址空间来设置新进程的信息。这些数据包括进程页表页,w和其他VMM数据结构。

         如果你开发内核模式驱动,你也得在你的代码执行中注意上下文切换。例如,如果你设计一个文件系统驱动,你派遣入口点必须执行在用户进程的上下文,这包括了一致的系统调用。在这样的情况下,你的驱动使用地址在进程的虚拟低2GB内存中。然而,如果你写中间层驱动或者底层的驱动。你的派遣例程将会在任何的线程的上下文中调入,就像线程在当时的处理器中执行代码一样。这样的情况下,你不能假设任何用户空间虚拟地址都可以在IRP包中仍然有效,因为你的代码是执行在处理请求的用户线程的上下文中;因此低2GB的虚拟地址现在要映射到其他进程的物理页。

         另一种情况是,如果你开发一个内核模式驱动,任何分配内存,返回的内指针一般是内核虚拟地址。因为内核空间的虚拟地址在系统中是同一个,分配的内存能够让任何线程执行。

如果代码在你的驱动内执行会在任何的线程上线文,如何保证你的驱动的IRP里面用户空间的buffer指针可以访问?VMM支持映射用户空间地址到内核空间地址来避免。(MmGetSystemAddressForMdl())。

最后一点,你的内核模式的驱动可能需要访问其他进程的空间。有一种方式就是调用函数KeAttachProcess(),这个函数在NT DDK中没有文档支持的,但是定义如下

VOID

KeAttachProess(IN PEPROCESS    Process) ;

参数:

PROCESS:指向你要附上的进程,这可以通过GetCurrentProcess()得到。

提供的功能:

KeAttachProcess()允许你内核模式线程附加到目标进程,任何你的线程就可以执行进程的上下文,使得线程能访问进程的所有虚拟地址空间。

 

注意:

         你要访问其他进程的虚拟内存空间的一种情况是如果内存要映射到目标进程的虚拟内存空间而你要访问它们。另一种情况是如果你要使用目标进程的资源,比如文件句柄。

         必须非常小心,因为附加到其他进程是一个对操作系统来说非常耗费的事情,会导致2个上下文交换,至少,如果目标进程被换出。而且,这将会导致冲刷Translation Lookaside Buffer在所有处理器在均衡多处理器系统中,这会有害于系统实行。

        

         不要在IRQL大于DIAPATCH_LEAVEL中调用这个函数。在这个函数调用的时候,会使用一个自旋锁在IRQL DISPATCH_LEVEL级别上保护内部数据的执行。因此,调用这个函数在比DISPATCH_LEAVEL级别上的话会导致死锁的情况出现。同样,不要尝试附加到2个进程上如果你调用KeAttachProcess而没有调用相应的detach函数。这样的话,bugcheck就会发生。

         解附加就是利用下面的函数:

         VOIDKeDetachProcess(void);

参数:没有

功能:使得你的内核模式线程从之前的进程中解开附加。不用在大于DIAPATCH_LEVEL上使用这个函数。

 

 

物理地址管理

         写一个内核模式驱动(特别是文件系统驱动),会帮助你扩宽内存管理器对管理物理内存方法的理解。当你理解物理内存是怎么被管理的,我要描述虚拟地址怎么映射到物理地址的。这方面的知识在调试NT系统和尝试理解为什么特定事件发生非常有用。

 

页帧和页帧基地址(Page Frameand Page Frame Databse)

         NT VMM必须管理系统中有效的的物理地址。VMM使用的方法是现在使用的商业操作系统例如Solaris,HPUX,或者其他系统V Revision4基于UNIX等使用的标准基于页的管理。

         NT VMM把可以执行的RAM分为固定大小的页帧(pageframes),页帧的大小可以是4K到64K,在X86框架下是4k bytes。每个页帧被一个叫页帧基地址的数据结构的条目描述。页帧基地址是简单的在非分页内存中的一个条目(entry)数组。每一项是一个页帧的的物理地址。对于每个页帧,下面的信息要掌握:

  • 页帧的物理地址在PFN中,物理地址占20位,12位的页位移,组成的32位地址就是4GB物理地址
  • 物理页帧的属性: ------修改位表示页帧的内容有没有改变------状态位标志页帧是在read或者write状态吗 ------页颜色------页帧是否包括进程的共享页或者私有页

  • 一个指向页表入口的指针
  •  页面引用次数。表示PTE是否refer to 在页帧database中的页
  • 页帧可能在的列表的前和后指针
  • 一个事件指针。指向页I/O读操作是否进行。     有效的页帧是非零引用次数的页帧。这些页帧包含活动进程的信息页信息。当一个页帧不再被一个PTE指向,引用次数就会减少。当引用次数减小到0的时候,页帧就等于不会被使用了。每个不被使用的页帧在5个链表之一中,每个链表表现出页帧的状态。
  • 坏页链表,把有奇偶验较错误的(ECC)页帧连接起来
  • 空闲链表,包含可以立即再使用但是没有被置零的页帧           NT VMM(为了确定US DOS设定的C2级别安全)除非内容被置零不然不会从新使用一个页帧。然而,为了低消耗,当页空闲的时候不是每次都被置零的。当一大堆的空闲和非零页被获得,系统的一个工作线程被唤醒来异步置零空闲链表中的页。
  • 零页链表。连接可以马上被再使用的页。
  • 修改链,连接不再引用的页帧但是直到内容被写到二级存储才能被回收的页
  • l  Standby 链,包含从进程工作集移除的页的页帧。    NT VMM大量尝试减小进程的页帧数目,这种方式是基于进程的访问模式。这些能立即给进程的页,叫做进程的工作集。通过自动修剪工作集,从而NT VMM更好的管理物理内存。然而,如果配到一个进程的一个页是因为修剪工作集而被侵占的(stolen),VMM不会马上收回页帧。反而,把页帧放入到standby链表中,VMM 延迟页帧的再使用,给进程通过访问地址而重新获得页帧的机会。当一个页帧在这样的表里面,就会被标识为正在被转换的状态,因此他不是空闲的也不属于任何的进程。

 

         NT VMM为总的空闲和standby页帧维护一个最大和最小的数。当stadby链表的页帧数在这个范围之外,一个VMM全局事件就会标记。这个事件是VMM用来决定系统内是否有足够的页。

         经常的境况是,VMM调用一个内部的函数来检查一个确定的操作是否有足够的内存使用。例如,你的驱动内一个需要调用一个系统服务叫MmAllocateNonCacheMemory。这个函数需要分配空闲页,因此会调用一个内部的函数(不是直接被内核开发者调用)叫MiEnsureAvailablePageOrWait来检查空闲链表或者standby链表是否有需要的页的数目。如果没有,MiEnsureAvailablePageOrWait函数会阻塞2个事件来等待足够的页够使用。如果这2个事件在一个固定的时间内被设置,系统会调用KeBugCheck

         注意到对于页帧基地址的操作是个频繁的操作。在VMM如何工作才能通过为页帧基地址(其他平台的一些基本结构)使用密纹理的锁机制来完成更大的并发操作已经有很好的研究。然而,NT VMM不会跟着使用细密纹理的锁机制(fine—grained locking)。当访问PFN的时候,会适当的IRQL(APC或者DISPATCH)使用自旋锁。这会减少并发。因此会使得当PFN基地址被访问的时候使用单线程,但是这样会使得代码简洁。没有哪个I/O操作需要PFN的锁来完成的。但是,这个锁是在DISPATCH_LEVEL或者更底层的级别,你能完全确信你的在比这些高的IRQL执行的代码中任何页错误对系统来说都是会错误的。

 

 

虚拟地址支持

         NT 虚拟地址管理器提供虚拟地址来支持系统:

 

  • 系统物理内存映射的虚拟地址能被独立的修改
  • 如果一个虚拟地址无关于物理地址和磁盘上的存储,NT VMM协助处理器硬件把虚拟地址的数据转到适合的物理地址。
  • 如果页包含转换的物理地址需要从二级存储中读取,NT VMM管理I/O操作
  • 为了使得从磁盘传数据成功,VMM使用文件系统驱动的支持
  • Vmm决定页策略来控制从磁盘和主要内存信息的转换来使得系统的吞吐量最大化。        正如前面提到的,VMM提供给每个进程比可用的物理地址还大的虚拟地址。虚拟地址必须对应于一些在物理RAM的数据或者代码。因此,为了支持这些大量的地址,VMM和系统硬件必须透明地转换虚拟地址到物理地址。然而,很明显,所有的进程需要的地址大于已有的可用的物理地址,VMM必须移除一些数据和代码到二级存储里面。   NT VMM是感觉操作和系统RAM使用量的核心组件,尽管RAM越来越便宜了,但是任然是很耗费金钱的组件。同时,用对于他们的机器和可怜的VMM对系统吞吐量巨大贡献需求非常大。因此,VMM对小化系统内存需求非常敏感。因为关系到每个设计的决定,要使用折中的方法。接下来的章节,我会讨论详细的VMM设计中的折中方法,导致在NT环境下分布式文件系统的实现问题。

虚拟地址的操作

    为了提供独立的虚拟地址给每个进程,VMM为每个进程维护一个平衡二叉树为虚拟地址描述符VAD。进程的每块地址都是插入到这由VAD结构表示的树里面,一个虚拟地址描述符结构包括了下面的信息:

  •  VAD中地址范围的开始地址
  •  VAD范围的结束地址
  •  在splay tree中一个指向其他VAD的指针。
  • 决定分配虚拟地址范围的性质:             这些性质包括以下信息:-------分配的内存是否已经提交。对于已经提交的内存,VMM从一个追溯到内配内存是否需要转换到磁盘上的页文件分配。-------分配的地址是进程私有的还是共享的。 -------描述内存地址范围是的保护的字节。保护属性是这些属性的结合:            PAGE_NOACESS,PAGE_READONLY,PAGE_READWRITE,PAGE_WRITECOPY,PAGE_EXECUTE,PAGE_EXETUTE_READ,PAGE_EXECUTE_READWRITE,PAGE_EXEUTE_WRITECOPY,PAGE_GUARD,PAGE_NOCACHHE,--------这段地址是否支持写时复制。写时复制的功能允许对POSLX风格的fork()操作的搞笑支持,POSLX风格是初始化的时候,父进程和子进程共享虚拟地址。如果,父进程或者子进程想修改共享的页,那么就会创建一份私有拷贝来完成这样的操作。------当一个fork()操作发生的时候,地址是否可以给子进程共享。(view_unmap=do not share, view_share = shared by parent and child),这些信息只是对于映射过的文件视图有效,以后将会谈到。---------VAD是否表示一个映射过的共享内存对象的视图。 --------VAD提交的内存的总数

        

         当一个进程内的内存分配操作或者一个进程映射文件视图到虚拟地址,NT VMM分配一个VAD结构体然后插入到splay tree里面。在分配的时候,进程能指明是否需要提交内存,或者是否只是简单需要保留这段虚拟地址。分配提交内存导致进程的quota数目改变。(Allocating committed memoryresults in the amount ofmemory requested being charged against the quotaallocated to the process).保留一个虚拟地址,是一个良性的操作,它只是创建一个VAD结构和把它插进splay tree上,虚拟地址的开始地址返回给请求的进程。注意,内存必须在使用之前提交。

    NT VMM允许进程分配和解分配完全的虚拟地址空间,例如,内存可能永远不提交。如果一个进程分配了一段虚拟内存然后发现只是需要提交它的一部分,NT VMM也会允许进程这样做。

    NT VMM提供的一个分配内的native函数是NtAllocateVirtualMemory(),这个韩式对内核开发者不适合。内核模式驱动用下面的函数代替:

 NTSTATUS

 ZwAllocateVirtualMemory(

IN HANDLE                                   ProcessHandle,

IN OUT PVOID                             *BaseAddress,

IN ULONG                                     ZeroBites,

IN OUT PULONG                         RegionSize,

IN ULONG                                     AllocationType,

IN ULONG                                    Protect

);

这个函数只是能分配低2GB的虚拟内(即使是系统进程分配的)。因此,这个不是使用在内核模式的驱动,除非你确定你会只访问特定进程的上下文。如果你需要分配在任何进程上下文内都能访问的内存,使用ExAllocatePool()。

正确的释放这段内存的函数是:

NTSTATUS

NTAPI

ZwFreeVirtualMemory(

IN HANDLE                                   ProcessHandle,

IN OUT PVOID                             *BaseAddress,

IN OUT PULONG                         RegionSize,

IN ULONG                                     FreeType

);

这个函数是稳定的,它允许你修改你之前分配的地址。注意,你不能期望同时能free或者release2段用ZwAllocateVirtualMemory申请的内存;例如,你指定的整段地址必须包含在一个实现分配好的VAD里面。如果你利用参数RegionSize指定的值等于0,VMM判定这个意思是整个VAD必须free/deommited。然而,这种情况下,你必须指定正确的BaseAddress(等于VAD的BaseAddress,或者当你分配地址的时候的BaseAddress)。

如果你free之前分配的内存中的一段,可能会获得一个错误,这个错误表示,你释放的量过多了,这个会很奇怪。原因是VMM劈开VAD,如果允许,为2个VAD来相应你的请求,释放掉了原来分配的内存中的一段。所以,这个请求允许一个属于目标进程新的VAD结构体。如果这使得分配的内存比允许的大,就会产生错误。

 

 

虚拟地址的转换

         这一节,我讲简短的讨论虚拟地址到物理地址的转换。这主题在资料中用得非常多,如果你想看更多的信息,我推荐你查看附录 E,推荐阅读和参考。

         Windows NT中的每个虚拟地址现在是32位。这个虚拟地址必须转换为物理地址。2个系统组件配合来完成:

 

  • The MemoryManagement Unit(MMU)提供处理器的硬件支持。
  • The VirtualMemory Manager 通过操作系统执行

 

如果是单方向的转换,是不必要的,例如,从虚拟地址到物理地址的转换。VMM也必须能反方向转换地址,从一个物理地址到虚拟地址的转换。当为了给其他的数据腾出空位,一个物理地址页的内容写入二级存储,适当的虚拟地址必须标记为“内存中不再有效”。如果要换回来的话,物理地址必须能转换到正确的虚拟地址。

虚拟地址转换是典型的MMU通过硬件执行。VMM复制转换的映射和页表的操作,之后这些映射和页表为MMU所用。概括的说,转换通过下面的步骤:

1.        上下文转换的过程或导致一个进程开始执行,VMM建立适当的页表,页表里面有进程的虚拟地址到物理地址的转换信息。

2.        当正在执行的进程访问虚拟地址,MMU尝试讲虚拟地址到物理地址的转换,通过使用TranslationLookaside Buffer(TLB),如果不在TLB中,使用VMM维持的页表。每次转换地址必须包含在一个页中,由系统的页帧表示。

3.        如果转换到的物理地址的内容在主内存中,进程被允许访问数据。

4.        如果页目录不在页帧中,就会引起一个异常,页错误发生,控制到达VMM页错误处理器,这会使得适当的数据到达系统内存。如果在访问的尝试或者其他原因引起页保护冲突,也会有硬件引起的异常。

 

MMU的设计和VMM子系统的设计紧紧相连。自然的,VMM子系统和MUU交互是很方便的。

         之前说过,VMM在非换页内存池中维护页帧基地址来管理系统中的物理内存。这个基地址是由页帧入口组成的,页帧入口是表示连续的物理地址页帧的入口。系统中的每个物理页帧都是有编号的(在RAM中0到n-1),为每个页帧计算PFN基地址是很烦的,当一个虚拟地址被映射到一个物理地址页帧号乘PFN基地址的再加上物理基地址就是FN基地址。这个结构是一个物理地址指针,指向在PFN基地址中的页帧的入口。

         NT是32为的虚拟地址平台。页大小是4096字节,所以表示一页需要12位(最少需要12位)。这使得MMU使用剩下的20位来定义一个页帧。页帧是通过在页表中的页表项(PTEs)来唯一确定的,页表页就是页帧的数组,这个数组的每项都是页表项。很多框架(包括Inte X86框架)都清楚的定义了PTE结构。

         在Intel平台,每个PTE必须是32位的(或者4字节),一共有 (1亿)中PTE可能,每个PTE是 字节,为每个4GB虚拟地址转换而存储是信息是 字节(4MB)。每个页表能存储1页大小的信息( 字节),1024页帧页帧能为一个进程的虚拟地址保存所有PTEs。

         为了防止转换的信息浪费这么多的内存,页表也是可以换出的页面。为了到达这样,Intel x86进程定义了一个二级页表。每个进程有一个页目录来保存页表的PTEs。这个目录是一个简单的页,因此包含1024项PTE(每页都是4KB,每项4字节),每个项都关于一个页表的。一个进程的典型的虚拟地址有10位保留来表示一个页目录中页表索引,10为来表示一个页表的页帧,12位来留给页内的偏移。

         图5-2显示出虚拟地址怎么转换为物理地址的。即使像MIPS R3000这样的没有限制PTEs结构的系统(而是提供了没有硬件虚拟地址转换支持特别是TLB lookups),VMM维持一个简单的数据结构来简化VMM子系统的设计和维护。


         到此为止每件事都很顺利。MMU检查TLB然后如果给了一个TLB hit,就简单返回转换过的物理地址。另一方面,如果给一个TLB missMMU必须为进程检查页表来定位正确的决定可能包含要方位的物理地址页帧的PTE。如果PTE显示这页已经留在内存中,保护属性也符合访问模式,MMU允许访问继续。不然的话,一个适当的异常(页异常或者违反保护属性)就会发生,控制到达VMM。然而,善于观察的读者必须发现一个另外的表叫Protype Page Table原页表在图5-2中,那么PPT有什么用呢?

        原页表是用来为多进程共享的页的页帧的数组。共享页和页帧出现当多个进程映射在同一个映射过的对象中(同一个映射)。因此,为了理解PPT,你必须先理解共享内存和内存映射文件。

 

 

共享内存和内存映射文件支持

         访问内存对今天的应用开发来说很方便。一个应用进程很简单的调用malloc调用(或者相似的调用),得到VMM给的一个虚拟地址,然后开始使用这个虚拟地址来访问分配过的物理地址。操作系统和硬件共同合作管理物理地址和维护适合的虚拟地址到物理地址转换信息。因此,操作系统能观察到所有进程在系统内的操作。使得进程分配好物理地址。

         同时,大多数应用程序必须在计算活动的需要的内存后要还做一些事情。明显的,所有的应用程序需要完成一下诶I/O操作到二级缓存。另外,交互式应用程序有时候希望能与其他程序共享一段内存。

         典型的。I/O通过文件系统的读写系统调用。完成这些调用需要系统陷阱来转换处理器从用户模式到内核模式and vice versa。对一个读操作,文件系统必须先把数据读到胸内存然后拷贝到应用程序的buffer。对于每个写请求,操作系统必须先拷贝从应用程序的buffer数据到系统内存。这些数据的拷贝,从或者到系统内存,读结合I/O请求。导致应用程序的重大开支。

         现在考虑,同一个系统有2个进程访问同一个文件。这些进程也许会访问同样的数据段,每个进程有自己的buffer,这些buffer的物理内存不一样,数据也不一样。进程1也许已经读出数据到内存然后修改这些数据但是没有把修改写入磁盘;进程2读同样的数据,它不会看到修改的数据而是从磁盘得到原始的文件的数据。这是2个进程共享内存去的一种妨碍,因为每个进程必须保证在其他进程读数据前把数据修改写入磁盘。

         想象如果现在每个进程能简单的映射磁盘中的文件到他们的自己的虚拟内存地址中。VMM提供虚拟地址支持通过当需要的时候交换从磁盘页文件中交换数据(换入页或者换出页),一个应用程序分配一些内存,尝试去访问它然后可能得到一个页错误。页错误解决(我们在这个章节后看到),然后应用程序就能访问一些保留给自己用的页物理内存。

         现在考虑当数据从磁盘文件中读出,然后写入到同一个磁盘文件。这样的情况下,为什么使用文件而不用一个页文件?让应用程序调用读/写操作来访问数据,还不如简单让应用程序保留一段关联到磁盘字节范围的虚拟地址,尝试访问这段虚拟地址映射到的内存(事实上,访问这段虚拟地址相关的字节范围),会得到一个页错误,然后操作系统会通过分配一些物理内存和从磁盘文件获得适合的数据来解决这个页错误。简单的,应用横须能简单修改在内存中的数据然后操作系统会---当需要的时候---请求---写修改后的数据到磁盘文件,然后,可能,通过释放物理内存为其他的进程腾出空间。

         上面介绍在一个文件中的映射有一个额外的好处;所有尝试映射同一个文件的应用程序能保留他们的基于同样物理地址的虚拟地址,这样所有的应用程序就能看到连续的数据的视图,无视任何程序在任何时间都能修改数据。

         NT VMM支持文件映射,映射后的对象是磁盘文件。当你执行一个文件(比如Word),可执行体(这样的情况下的映射后的对象)是映射到你的进程虚拟地址空间的可执行的结构体。现在,如果同一台机器上的一些用户,尝试执行word,同样的执行体映射到他的或者她的虚拟地址空间,因为基于VADs上的物理地址可能已经在内存中,其他用户可以得到一个相对快的反应时间。看图5-3

图5-3 2个进程映射相同的页到他们的虚拟地址空间

         文件映射不是在2个进程分享物理地址的唯一方式。虚拟地址描述符VAD独立地管理基于虚拟地址的物理地址,VMM通过基于同一物理页帧的2个进程的VAD来允许进程分配内存是完全有可能的。文件映射是简单的共享内存对象是基于磁盘的概念的扩展。固定的文件对象,而不是页文件。就像你能创建基于文件的共享内存对象,同样能创建基于系统页文件的共享内存对象。当你希望喜爱2个进程的模块共享内存,内核驱动设计者需要在用户空间和内核驱动共享内存这就很典型了。共享的内存支持是VMM提供的。当一段共享内存对象被创建(一种情况是不基于磁盘文件),虚拟地址的开始由偏移为0的地址指出。因此,所有的进程共享这个对象能索引到正确的字节偏移和管理数据。你必须注意,修改不基于磁盘文件的共享内存对象将会不会是永久的;例如,这样的修改将会丢失当共享对象会被使用这个对象的所有进程关闭。

         所以映射是怎么工作的呢?VMM会创建什么数据结构来支持映射/共享对象。在回答这些问题之前,让我妈来回顾原页表。

        

原页表(protype page table)

         页帧包含共享页被特别的结构表示-----原页表(PPT),这个结构从分页或者非分页内存池中非配。

         当VMM为一个进程创建一个映射或者共享内存,它同样分配原页表条目来描述物理将基于文件映射的页帧。映射对象的PPT是被所有进程所共享的,他们映射到一个对象上。每个PPTE是关于一个页可能在或者不在内存中;例如,页也许在物理页帧中,或者需要从二级缓存中带入当访问的时候,因为所有的进程都是用同样的PPT(相同的PPTE),所有的进程使用同样的物理页帧因此看见映射数据的同一视图。

         当一个页帧和一个PPTE联系起来,PPTE就被标记为有效的。在PFN基地址中的页帧目录被初始化指向PPTE。不仅仅是Intel X86MMU还是MIPS R3000或者相同的构架都支持原页表。VMM怎么安排才能使得MMU和共享内存工作?

         考Intel X86构架。Intel X86 MMU严格定义页表和PTE结构。VMM在分配的内存中创建一个PPT(同时创建PPTE)。当一个进程创建一个文件映射。想象进程尝试访问映射文件对象一部分的虚拟地址。MMU将会转化虚拟地址到页目录表的位移和正确页表的位移。在第一次访问这虚拟地址,页表目录将会表示这个页不是基于任何物理内存。

         这会产生一个页错误然后控制到达VMM页错误处理。页错误处理提示VAD包括要范围的虚拟地址是被标记为基于映射的对象的。VMM能发现合适的PPTE和把页换入。这个时候,PPTE是标记为有效的和PFN基地址的目录联系在一起,同时PFN基地址目录指向PPTE。同时,VMM初始化PTE为有效的然后标记它指向一个物理地址。这样就会使得PPTEPTE包括物理地址的信息,但是PFN基地址目录里面只是指向PPTE。现在,再次访问内存,MMU发现PTE正确初始化(它不知道或者不在乎PPTEs),从虚拟地址到物理地址的转化完成。

 

 

PPT设计的小问题

         你必须注意,PFN基地址目录从来不指向PTE,VMM从PFN无法找到所有映射了共享内存到他们虚拟地址的进程的PTE。VMM最多能从PFN找到PPTE因而管理PPTE的内容。

         这样的设计有个缺点:想象下内核模式组建想要请求VMM来从物理内存中清除特定的页,一般(对于非共享文件),你能确定地询问VMM来做然后VMM会标志PFN基地址目录为无效来回应。然后,VMM会使用存在PFN基地址目录项中的信息啦发现适当的PTE,这些PTE都是在进程的虚拟地址中PFN基地址目录项中指向的。这会标记PTE目录项为无效,保证MMU在下次访问这页包括的虚拟地址会导致页错误。

         然而,如果页属于一个映射的对象,VMM无法访问所有的包含共享页的页帧的PTEs。因此,如果你请求VMM从内存中清除这样的页,VMM将会返回一个错误,说明这个功能对映射的对象不提供。这个是个第三方开发者要清除从系统内存中要求的页的严重错误。

        

 

共享区和视图

         Windows NT系统是对象为中心的;例如,许多功能的提供是基于对象和操作对象的方法的。文件映射创建和访问是2步进行:

1.        一个共享区对象被VMM创建来回应一个文件映射请求或者共享区对象。

2.        当进程真的需要访问映射文件或者一个共享内存对象,调用者必须请求VMM映射一段视图到问文件。从概念上说,这个视图就像文件的窗口,允许访问一定范围的内存。当然,进程访问同个文件的多个视图是可能的,多个进程对于一个映射文件有多个视图也是可能的。

共享区对象有一系列保护属性,就想其他Windows NT对象一样。通过指定一系列的共享区对象的保护属性,进程能找出维护对象的方法(文件对象的任何数据可能映射进和由共享区对象表示)。

基于磁盘文件共享区对象包括2种:

  • 可执行镜像文件映射(Executableimage file mapping)
  • 映射文件(Filenonimage mapping)

当你告诉VMM创建一个映射过的文件的共享区对象,你能指定这个映射过的文件对象怎么被处理。系统加载器使用文件映射来执行可执行和执行文件映射将被对待为可执行镜像文件映射。然而,你需要请求一个可执行(比如Word)被映射为一个无镜像的文件映射。

VMM检测可执行映像文件映射创建的共享区对象是不是映射在一个有效的执行体中。如果你尝试映射一个文本文件为可执行映像文件映射,VMM会给你一个错误。同样,如果你给文本文件同时映射为可执行映像文件和一个简单的文件映射;这些映射的地址对齐会不同。

可执行映像文件映射和无映像文件映射(或者简单的共享内存)的最大区别是VMM如何修改映射范围。当一个无映像文件映射被进程修改,这个修改就立即被所有映射这个文件的进程看到,因为共享的物理地址页被VMM修改了。当修改被冲刷到二级存储,这些修改最终也会反应在磁盘映射对象上。然而,当一个映像文件映射被修改,一个基于页文件的私有的页就会被创建,因为一个映像文件映射的修改从来不被写到映射过的对象(磁盘文件)。这些修改在进程接映射文件的时候被抛弃。

为了创建一个共享内存对象(一个section 对象),NT VMM提供一个叫NtCreateSection()的例程。尽管这个例程没有导出给内核开发者,ZwCreateSection()可以代替它,它是这样定义的:

NTSTATUS

NTAPI

ZwCreateSetion(

OUT PHANDLE                            SectionHandle,

IN ACCESS_MASK              DesireAccess,

INPOBJECT_ATTRIBUTES  ObjectAttributesOPTIONAL,

IN PLARGE_INTEGER       MaximumSize OPTIONAL,

IN ULONG                           SectionPageProtection,

IN ULONG                          AllocationAttributes,

IN HANDLE                         FileHandle  OPTIONAL,

);

功能:

这个例程能被内核驱动创建一个共享内存对象(命名或者没有)或者为磁盘文件创建一个文件映射。即使你是一个文件系统开发者完成一个磁盘或者网络文件系统,你能使用这个例程来创建一个共享内存对象或者映射的文件对象(不要尝试创建个映射的文件对像在你自己的文件系统使用这个例程除非你真的知道自己在做什么)

有时候,内核模式驱动开发者希望和用户空间共享在内存中的数据。或者,如果你设计一个内核模式驱动包含从网络或者使用一个用户进程的服务通过网络传递数据,你能使用这个调用来创建一诶简单的共享内存对象或者一个基于文件共享内存对象来实现简单而有效的在内核驱动和用户空间进程间的数据传递(在用户模式的服务中使用一个命名对象使得更容易打开对象)。

在对于ZwCreateSection()例程的描述中,我提到了对象管理器例程,给它一个对象指针,可以用来获得一个任意进程上下文中的对象的句柄。这个例程是ObOpenObjectByPointer(),定义如下:

NTSTATUS

ObOpenObjectPointer(

IN PVOID                  Object,

IN ULONG                  HandleAttributes,

IN PACCESS_STATEPassedAccessState OPTIONAL,

IN ACESS_MASK       DesireAccess OPTIONAL,

IN POBJECT_TYPE   ObjectTyep OPTIONAL,

IN KPROCESSOR_MODEAccessMode,

OUT PHANDLE                   Handle

);

典型的,你能传递一个NULL给PasseAccessState和ObjectType。要小心DesireAccess的请求,这个是原始打开操作维护的。HandleAttributes能从ObReferenceObjectByHandle()获得,这个函数返回HnaldeInformation,里面包含HandleAttributes。

还有一个函数ObRerenceObjectByPointer(),这个函数只是简单增加对象引用的次数。这个函数定义在Windows NTIPS kit中:

NTSTATUS

ObReferenceObjectByPointer(

IN PVOID                    Object,

IN ACCESS_MASK    DesiredAccess OPTIONAL,

IN POBJECT_TYPE   ObjectType OPTIONAL,

IN KPORCESSOR_MODEAccessMode

);

在DDK文档中还有其他的函数,可以用来打开和关闭一个之前创建的共享区对象和映射,解映射一个共享区对象的视图:

  • ZwOpenSection()
  • ZwMapViewOfSetion()
  • ZwUnmapViewOfSetion()

 

 

File-Mapping结构

         当一个进程创建一个文件映射,必须指明是一个可执行文件还是一个其他类型的文件,尽管这2种文件映射最后都把文件的内容映射到进程的虚拟地址空间,但是VMM对待这2种请求是不同的。

         之前提到,对映像文件映射的页的任何修改不会反应到磁盘映射的执行体上。基于页文件的页来承担这个改变,所有对于这个页的改变将会在映射关闭的时候被抛弃。

         然而,VMM为每个映射文件对象维护2种类型的共享区对象(和数据结构),每种类型的映射,VMM维护SEGMENT数据结构来描述映射。因此,有2种可能的segment数据结构与相应的映射文件关联:image segment和data segment。每种segment结构指向映射对象的原页表。

         尽管segment对象对于内核模式开发者来说不透明,这里要说的是,2种类型的映射能同时存在。一个可执行体可以同时映射为映像文件和一个无映像文件爱你。对于每种映射,VMM会创建和维护segment数据结构与内存中的文件关联。因为有2种不同的数据结构创建,基于映射类型的执行,在一个页内的同一文件的数据可以在内存中同时存在2个页帧!因为每种类型的映射有它自己的segment数据结构和与这个结构关联的原页表。

 

修改和映射的页写者(Modified and Mapped Page Writer)

 

         前面讨论过,NT VMM有任务去呈现每个进程的可以使用的大量的虚拟地址,尽管系统物理地址是有限的。为了完成这个任务,VMM必须使用二级存储设备像为内存数据和页数据的换入和换出的辅助储存器。这个页策略对进程来说是透明的。

       NT VMM自动冲刷脏或者修改过的页面到二级存储来为系统其他进程得到页帧。页帧内的修改数据将会被写入16个可能页文件之一或者如果页帧是分配给一个共享内存对象的话就到磁盘上一个命名的文件。除非修改的页帧被冲刷到磁盘,VMM不能重新使用页帧,因为这样做会使得数据丢失。

       为了保证在需要的时候有充分的RAM够用,VMM总是保持一个固定数模的页帧可用。这些页帧必须不包含任何修改的后的数据,因此,当VMM决定这样做的时候他们能被重新分配给用户。如果VMM不维护可用页帧池,可能会使得在进程在能够分配到页帧之前都在等待修改的数据冲刷到二级存储。使得进程阻塞对系统的执行力来说不是很好。

       因此,VMM创建至少2个特殊检检测线程叫Modified(修改过)Mapped Page Writer线程。注意到创建的线程数可能大于2.至少一个修改页写者线程是被创建来异步写修改页帧到页文件。至少另一个线程,叫映射页写者线程,用来异步写出修改的数据到映射的页帧到文件。这2个线程本质都是一样的,因此,在这本书里面,映射页写者线程和修改页写者线程可以交换。

       这些检测线程的作用是冲刷修改后的页帧到二级存储中,因此保持一个固定的可以重新分配的页帧数目。每个这样的线程是一个实时线程,他们的优先权(priority)至少是LOW_REALTIME_PRIORITY + 1。

         修改页写者线程的算法在下面列出。下面的伪代码是基于映射和修改页写者线程冲刷页帧到映射映射或者页文件;当需要的时候,列出这2个线程的不同操作

// The following routine summarizes the MPW code executed by a dedicated
// worker thread. Note, however, that although the specific method used
// in various versions of the operating system might be different, the
// fundamental methodology described here should be consistent.


MiModifiedPageWriterWorker() {
for (;;) {
// Wait for event to get set, indicating that insufficient "free"
// (not modified) pages exist. This event is set when the system
// is running low on available pages and the VMM wants some
// modified pages written out so they can be reassigned.
// This event is also set when the total number of modified pages
// in the system becomes greater than a pre-determined
// threshold value (the "threshold value" in turn depends on
// whether the system is configured as a workstation or as a server
// and on how much RAM is present on the system).
KeWaitForSingleObject(ModifiedPageWriterEvent, ...);

//Now, lock the PFN database.
for (;;) {
// The event was set indicating that some pages need to
// be flushed. Pick a page frame to be flushed (the first on
// the modified pages list from the PFN database?) and invoke
// an appropriate routine.
MiGatherMappedPages (PageFramelndex, ...) ;


// The above routine is responsible for the actual flush.
// To perform the flush I/O, the PFN database
// lock will have been dropped and reacquired by the
// MiGatherMappedPages() routine. Therefore, check whether
// adequate clean pages are now available and if so, stop
// flushing.
if (enough free pages are available) {


// Unlock PFN database.
break;
}
} // End of loop in which the MPW thread flushed modified pages to
// disk.


} // End of infinite loop awaiting event to be set so that the

// MPW thread can begin flushing pages.
} // end of MiModifiedPageWriterWorker() routine
// The following routine is responsible for collecting a bunch of
// contiguous modified pages and writing them out to the page file.
// The similar routine responsible for writing out mapped file pages is
// called MiGatherMappedPages().
MiGatherMappedPages (...) / MiGatherPageFilePages (...) {


// Find a paging file for page file backed pages only,
if (paging file not available) {

// Nothing can be done as some I/O is already in progress
// to all paging files.
return;

}
// Find a contiguous chunk of available paging file space using a
// bitmap per paging file.
// OR
// If this is a mapped file, ensure that the mapped file is not an
// image file.
// Initialize a MDL (Memory descriptor List) to be used in the
// I/O operation
// Scan both backward and forward, starting from the sent-in page frame

// index, to find a contiguous set of modified pages that can be
// written out to the page file or to the mapped file.

for (each candidate PTE) {
if (PTE is modified and backed by the page file or by the mapped
// Increment reference count on this PTE

PTE->ref erence_count++ ;

// Mark this PTE as "not modified," anticipating that our write
// will succeed.
PTE->modified = FALSE;


// Mark the fact that this PTE is being flushed. Any flush
// requests for this PTE (say from a file system or from the
// Cache Manager will be blocked until this I/O completes) .
PTE->being_flushed = TRUE;


// Put the page file page address into the PTE.
// Add this page frame into the physical page list described by
// the MDL.
// OK, so now we have a list of page frames that we wish to flush.
if (number of pages reserved in the page file > number of contiguous
modified page frames encountered) {
// Release extra space pre-allocated from the page file
// (if any) .
// Unlock the PFN database lock.
/*********************************************************************/
// NOTE: If this were the routine handling mapped files, some
// additional processing would be performed here. This processing is
//as follows:
{
// Only for mapped files.
if (this file is marked as "fail all i/o, " forget it and return) {
return;

}
// Make a callback into the file system advising the file system
// that a paging I/O is on its way.
// THIS IS VERY IMPORTANT:
// The file system must - in response to this callback - acquire
// all resources that might be needed to satisfy the paging-IO
// operation. We will cover this call-back in detail later in
// this chapter and in Part 3 of this book.

if (FsRtlAcquireFileForModWrite (...)) {
// Call-back succeeded, issue I/O here

ZoAsynchronousPageWrite (...)
} else {
// Call-back failed.
// Return error locally = STATUS_FILE_LOCK_CONFLICT;
// Note that pages will stay marked dirty and the operation
// will be retried sometime later.
/ / Return
} // End of code that is executed only for mapped files.

/*********************************************************************/
// NOTE: The following code is only executed for pagefile backed pages
// Perform an asynchronous, paging-IO operation. This operation is
// a special request handled by the I/O manager who quickly
// redirects it to the appropriate file system on which the page
// file is located . . .
loAsynchronousPageWrite (...) ;
// Return;
} // end of code executed only for page files.
// Lock the page frame database lock.

} // end of MiGatherPageFilePages ( ) / MiGatherMappedPages ( )
// The following routine is invoked as an Asynchronous Procedure Call
// (APC) when the asynchronous paging I/O is completed by the file system.
// Note that the file system might choose to handle the I/O
// synchronously though that is not recommended ...
MiWriteComplete (Context, StatusOfOperation, Reserved) {
BOOLEAN FailAllIoWasSet = FALSE;

// The Context is actually the MDL that was sent to the file system
MdlPointer = Context;

// Lock the PFN database
for (each page that comprised the MDL that was written out) {
// Set write-in-progress to false
PTE->being_flushed = FALSE;

// If an error was encountered ...
if (error AND this was a write to a mapped file AND the mapped
file belongs to a networked file system) {

// THIS IS IMPORTANT TO FILE SYSTEM DEVELOPERS WRITING
// REDIRECTORS.
// The VMM assumes that if a paging I/O to a file across the
// network has failed, the network MUST BE DOWN.
// In this case, the VMM marks the file as "fail all I/O" and
// all modified data to the file will now be discarded!
FailAllIOWasSet = TRUE;

// Dereference the page.
PTE->reference_count—;

if (error AND not file on networked file system) {
// Mark page as modified once again so write will be retried
// later.
PTE->modified = TRUE;
}
} // Loop for each page.
// FOR MAPPED FILES ONLY ...
ReleaseFileResources(); // Resources acquired using file system
// callback
// Unlock PFN database.

if (FailAllIOWasSet) {
// The user sees the famous error message
// "system lost write-behind data" now.
loRaiselnformationalHardError(STATUS_LOST_WRITE_BEHIND_DATA,

FileName, Status);
}
} // end of MiWriteComplete()

 

    在上面的伪代码中,VMM使用一个I/O管理器函数叫IoAsynchronousPageWrite的函数来冲刷修改的数据到二级存储,这个调用将会很快从I/O管理器到达目标页文件或者映射页存在的挂载的文件系统文件系统驱动。

    文件系统驱动能容易认出到页I/O的写请求因为I/O请求包发送到文件系统是有IRP_PAGING_IO和IRP_NOCACHE标志的。文件系统是不允许在处理也I/O写请求的时候发生页错误的。当执行描述页I/O请求的IRP结构被处理后,I/O管理器处理异步页写不同。本质上,I/O管理器在异步请求I/O IRP完成后,通过内核APC调用MiWriterComplete函数。这个函数在MPW线程中调用。

 

页错误处理

    当虚拟地址不映射到物理地址,访问这个虚拟地址就会出现错误,VMM处理这样的错误。尽管硬件MMU转换虚拟地址到物理地址,当MMU发现PTE不表示页不在内存中,MMU会把这个问题发给VMM来处理,当页错误发生的时候,VMM的处理函数就会启动,在内核模式或者用户模式,这个函数是MmAccessFault,他有三个参数:

  • 发送错误的虚拟地址
  • 一个布尔参数,表示是否是一个存储/写操作导致的一个页错误(FALSE值表示是一个读/加载操作)
  • 发生错误时候的模式(内核或者用户模式)      首先,MmAccessFault检查当前的IRQL是不是大于APC_LEVEL,如果2个页表(PDE PTE)都表示页是无效的,那么VMM会bugcheck系统然后显示这样的调试信息.  MM:***PAGE FAULT AT IRQL > 1 Va %x, IRQL %x        VMM中解决页错误的MiDispatchFault。MmAccessFault调用MiDispatchFault来解决页错误和使得页帧的内容有效。这个函数处理访问系统地址空间和用户进程地址空间的页错误。错误分发到一个子例程是基于错误的地址:
  • 如果错误地址是基于一个页文件的,那么执行到MiResolvePageFileFault。     这个函数的执行过程是: -----为从页文件中读数据到内存分配足够的物理页帧(也就是物理页)-----这个函数调用MiEnsureAvailablePageOrWait函数,这个之的章节说过。-----PTE指出要读的文件。(在PTE的结构中存在) -----创建一个MDL来包含这些物理页-----使得PTE标记为’intransition’(在转换中) -----返回一个特殊的值0xC0033333给调用者,MiDispatchFault         因为MiResolvePageFileFault返回0xC0033333,MiDispatchFault将会使用I/O管理器的IoPageRead函数来执行页I/O读操作。就像之前在修改页写者处理中的页I/O请求一样,I/O管理器调用文件系统驱动来完成这个页读请求。将会认出这个请求是个页读,由于IRP_PAGING_IO和IRP_NOCACHE标志。在处理页的I/O读请求文件系统不能引起任何页错误。       VMM会等页错误去请求完成,如果成功,把这些页加入到进程工作集。
  • 如果虚拟地址的PTE指出页正在转换中”intransition”,MiResolveTransitionFault将会被调用。一个转换页被标识为正在被转换因为以下的原因:      ------页帧包含有效的数据,但是页被放在了free list 因为自动调整平衡。      ------页帧包含有效数据,但是因为进程的工作集的自动修剪而在修改链表modified list中      ------页是正在从二级存储中读出,是一个冲突的页错误collided page fault    这个函数执行过程是这样的: ----- 如果是正在从二级存储中读出的页,MiResolveTransitionFault函数就会阻塞,等待I/O完成。如果有一个粗无发生,就会标记PTE无效,返回success,强制调用者承受另一个页错误,这样PTE就会不再标记为in-transition了 ------不然的话,这个函数就会标记转换的PTE有效和加入到当前进程的工作集。 因为没有页读操作被MiDispatchFault初始化,这个函数不会返回0xC0033333。
  •  MmAccessFault调用MiDispatchFault,如果一个虚拟地址指向一个共享内存或者一个内存映射文件,会有PPTE。这样MiDispatchFault调用MiResolveProtoPteFault,他的执行过程如下:------如果PPTE属于一个映射文件,MiResolveMappedFileFault函数就会被调用来决定到内存的页面,分配一个MDL然后返回0xC0033333,VMM尝试集中页面来提高效率。      ------如PPTE是包含在一个页文件中基于一个分享的内存,MiResolvePageFileFault函数就会被调用。这个函数决定执行分页I/O读操作的页文件数目,创建一个MDL结构体用来执行读操作,然后返回0xC0033333. ------如果PPTE预示这是一个转换当中,这个函数就会调用MiResolveTransitionFault,随后会讨论。------如果请求一个零页,MiResolveDemandZeroFault就会被调用。      当一个适当的字例程被调用成功的时候(也就是上面的函数),MiResolveProtoPteFault函数就会使得PTE反映PPTE。现在进程的PTE将会关联到PFN基地址项(databaseentry),这个项的页帧的内容将会被读入(0xc0033333被返回)或者已经有效如果转换(transition)错误解决的话。有时候,VMM只是简答的需要实现一个页帧包含0来回应页错误。这个可能在一个线程尝试扩展一个磁盘上的文件的时候发生,或者如果一个线程尝试访问一些新的分配然后提交的内存。这样的情况下,MiDispatchFaultui简单调用MiResolveDemandZeroFault,这个函数会从适当的页帧链表中分配一个零页页帧。如果这样的一个页帧不适合,MiResolveDemandZeroFault返回0xc7303001,这个数字会导致错误复发然后同时一个页就会变为适合(记住MPW线程总是尝试保证有足够大空闲未修改的页帧被分配)。
正如你所看到的,在解决虚拟地址到物理地址VMM通过使得不在系统内存中的页错误来支持MMU。如果你开发一个内核模式驱动,尝试一个页错误在IRQL大于或者等于DIAPATCH_LEVEL,你会导致系统bugcheck,因为VMM不会在这样的IRQL下完成页错误。从而保证所有的代码都在一个高的IRQL下被访问都被提前锁进非配分页系统内存。

 

 

 

与文件系统驱动交互

         NT虚拟内存管理器和文件系统驱动彼此共同依存。VMM依靠文件系统驱动来提供分页文件I/Oh支持和提供给表示内存映射文件的共享区对象支持。文件系统,依靠VMM来解决发生在文件系统驱动中的页错误,来管理系统和用户缓存;分配、操作和释放内存;来帮助缓冲文件流数据。

         下面列出NT平台下VMM对文件系统驱动提供的支持的功能:

  • 文件系统驱动是可执行的,动态加载的驱动,载入系统虚拟地址空间的时候是与VMM和包含可执行体的系统文件驱动协调合作的。默认的,文件系统和其他内核驱动的代码是非分页的。例如,这些驱动留在RAM只要他们被加载。相同的,所有与内核驱动联系的全局内存永远不会被换页换出去。有个编译器项使得你的驱动可以指定某段能从分页内存中分配。这个编译指示在NT下的编译器是这样的:                    #pragma                alloc_text (PAGEXXXX,     NameOfRoutine)

XXXX是一个唯一确定一个分页代码的四个字符。而且,在运行的时候,你的驱动也可以调用MmLockPageableDataSection()或者MmLockPageableCodeSection()函数来动态锁定代码和数据。这些函数和相对应的解锁函数都在DDK文档中有介绍。在标记为分页的驱动中的有些信息同样在第二章 File SystemDriver Development已经提供。

  • 文件系统驱动过滤驱动和设备驱动都可能在运行的时候分配。典型的,你的驱动会调用ExAllocatePoolWithTag()的一个版本函数来请求分配分页内存,非分页内存或者线性缓存内存。你甚至能请求如果分配失败的话就睡导致系统自动的安静。尽管执行的支持函数管理这些你驱动的内存池,物理地址和他的管理仅有VMM来管理。任何虚拟地址指针(地址)返回使用ExAllocatePool函数就会爆炸在内核模式地址。(也就是说ExAllocatePool分配内核地址).    你的驱动能调用ZwAllocateVirtualMemory()函数来直接从VMM请求地址。尽管返回虚拟地址将会在低2GB的进程虚拟地址空间;因此,这样的内存就只能在进程/线程上下文中进行访问。
  • 因为当执行在任何进程的上下文中你的内核驱动必须可以访问,VMM管理每个进程的虚拟地址空间所以低2GB对于每个进程都是私有的而高2GB空间是系统所有的虚拟地址空间,可以映射到同样的进程上下文件的物理地址。
  • 一个文件系统或者一个内核驱动,你的代码将会经常需要使用从用户模式传过来的buffer(例如,一个线程执行在用户模式分配内存和传递这个块内存到你的驱动).你的驱动必须使用这buffer来传递数据进入buffer或者出buffer。然而,有2个问题你的内核驱动必须要注意:------除非你的驱动能保证执行在用户模式线程,你的驱动不能使用从用户空间线程传过来的虚拟地址,因为他们只能在特定的线程有效。-----有时候,你的驱动可能需要访问传递过来的buffer在IRQL大于APC_LEVEL。在这样的情况下,你必须保证buffer是基于锁定的物理地址页因为一个页错误可能导致系统崩溃。       VMM帮助你的上面提到的问题。通过调用VMM的函数例如MmProbeAndLokPages,MmBuildMdl(),和其他相似的函数任何buffer能有他自己的物理地址锁在内存。这些请求,VMM创建MDL,一个透明的结构指出与你的分配的虚拟内存相联系的物理页帧链。随意的,依靠VMM函数调用,页会也锁定在内存;直到解开锁定分配给buffer的页帧将才会被回收。如果你需要映射传递过来的地址到系统虚拟地址空间,你可以用MmGetSystemAddressForMdl.
  • VMM管理分配给所有系统执行的线程的栈帧。分配给一个线程的栈执行在内核模式是固定长度的。在NT 3.51和之前的版本,这个栈是限制为2个页帧。在WindwosNT4.0,栈扩展到3个4KB页。
  • VMM辅助文件系统(和NT缓存管理器)在缓存文件数据。所有物理地址管理都是与VMM有关。因此VMM的支持是使用物理地址来缓冲数据流是活跃的请求,这会增加系统吞吐量。
  • 当满足的页错误发生,VMM提供簇支持帮助提供系统的执行。
  • 典型的,VMM尝试聚(cluster)I/O操作到16页。在x86平台,这会导致64KBI/O大小,在Alpha机器上,这个会是128KBI/O操作。
  • 有时候,过滤驱动需要做一些不寻常的事情,像缓存数据到本地文件系统的文件。或者,用户模式代码和内核驱动可能需要传递数据buffer。为了解决这些问题,内核模式驱动和用户模式应用程序能使用VMM提供的服务来创建共享内存对象和内存映射文件。
  • NT 虚拟地址管理器提供MmQuerySystemSize来支持可能用到文件系统的驱动。

MmQuerySystemSize函数有2个参数,它简单的返回一个可能是下面的例举型结果:

------MmSmallSystem(0)

------MmMediumSystem(1)

------MmLargeSystem(2)

这个值的返回依赖于系统中的物理地址配置。VMM初始化一个全局的变量,MmSystemSize,可能是这3个值中之一在系统初始化的时候,在决定可用的物理地址在节点中。MmQuerySystemSize()函数返回这个全局变量的内容。

RAM的实际数量可能会导致一个值被返回而不是另外一个是服从于在不同windows NT版本之间的改变。例如,如果你的系统小于12MB的物理地址,你能期待返回MmSmallSystem值当你调用MmQureySystemSize函数的时候。相同的,如果你有小于20MB的可以使用的物理地址,你能期待得到MmMediumSystem。

MmQureySsytemSize函数是内核模式组建调用来引导他们资源分配的决定。例如,考虑当MmSmallSystem值被返回。现在你的文件系统驱动可能不知道什么是“small system”真正的意思,但是你能推断,相对地说,可用的物理地址小于中或者大系统。因此,你的驱动能预分配小型的空间,或者回家比较小的工作线程相对于中或者大系统来说。使用这个函数来获得额外的系统信息帮助决定你的驱动可以使用的资源(物理地址)的大小。

这是毫无疑问你的驱动在做最后决定资源大小的时候要考虑的。

 

 

VMM在下面的方面同样依靠文件系统:

  • 页文件被创建和管理在挂载过的文件系统。因此,为了实现虚拟地址支持,VMM需要文件系统来执行分页I/O读写操作.            在part 3里面的代码显示,当接受I/O请求到一个页文件文件系统驱动必须完全的依靠VMM,因此文件系统应该防止获得任何的资源(为了同步),必须不引起一个页错误在执行读/写请求的时候,必须不拖延异步处理的请求,和必须不因为任何其他原因阻塞请求。这可能简单的导致请求直接(在决定请求的磁盘参数后)到适当的低层设备驱动。
  • 为了提供共享内存支持或者内存映射文件,VMM需要激活潜在的文件系统的支持。首先,VMM请求文件系统提供适当的回调来帮助维护在NT中锁的层次。然后,VMM请求文件系统准备接受当一个用户进程访问映射内存产生的直接页错误。

回调必须由文件系统驱动完成是AcquireFileForNTCreateSection和ReleaseFileNtCreateSection。文件系统期待获得所有的当NT VMM执行支持section请求的代码的时候资源。我会在Part 3详细讲述这些回调的实现。

 

 

文件系统驱动提供的支持函数

         VMM提供2种函数,MmFlushImageSection和MmCanFileBeTruncated,那是对于文件系统设计非常重要的啊后脑勺,但是他们没有文档说明。Part 3会有使用这些函数的例子。

 

         MmFlushImageSection

         这个函数是文件系统驱动使用来要求VMM抛弃包含特别镜像共享区对象(imagesection object)的信息的内存页.例如,考虑一个用户映射到内存的Word的可执行文件的拷贝,现在想要删除,可能是提升拷贝到下个版本。文件系统驱动必须保证,在删除文件前,所有包含页数据的页都被冲刷(抛弃)。在正常的执行过程中,这些页可能包含文件流数据,甚至在所有用户句柄被关闭了。然而,文件系统不允许这样的信息保留在内存如果他要删除文件流。

        

---------------------------------------------------------------------------------------------------------------------------------

提示:

         VMM强制限制当一个文件还有用户映射文件流的时候,是不能被删除的;如果文件被执行,他不能删除。然而,这节的讨论表示,你能知道VMM保存文件数据在内存中甚至是所有的映射文件流的句柄都被关闭,直到它真的不需要再使用物理地址了。这个帮助完成更快的反应如果用户关闭文件句柄但是不马上在打开它。

         在执行删除文件的操作之前,文件系统驱动必须冲刷系统页。

---------------------------------------------------------------------------------------------------------------------------------

 

         这个函数通常是一个文件系统驱动调用在允许一个线程为写访问打开文件流之前。同样,如果其他线程之前映射文件到内存为一个执行体VMM将不会允许一个用户为写访问打开文件。

         MmFlushImageSection函数定义如下:

 

BOOLEAN

MmFlushImageSection(

INPSETION_OBJECT_POINTERS     SectionObjectPointer,

IN MMFLUSH_TYPE                                      FlushType

);

 

Typedef enum_MMFLUSH_TYPE{

MmFlushForDelete,

MmFlushForWrite

}MMFLUSH_TYPE;

 

资源的获得:

         文件系统驱动必须保证文件流在调用这个函数之前被互斥得到。一般是文件流的MainResource被互斥获得在MmFlushImageSetion函数调用之前。

参数:

SectionObjectPointer

         在下面的章节中,SECTION_OBJECT_POINTERS结构体会更加仔细的讨论。对于现在,记住这个结构体类型的一个特别的实例是表示每个在内存中的文件流。VMM期望一个指向这个结构体的指针被传入MmFlushImageSection函数。

 

FlushType

         这个是2种值之一,MmFlushForDelete或者MmFlushForWrite。当检查当一个创建/打开请求一个磁盘文件流执行的时候,是否一个用户的打开能被允许来执行,文件系统应该传递MmFlushForWrite值。在真的想要删除一个磁盘文件(在设置文件信息的派遣函数和在cleanup派遣函数中,这2个都在Part3讨论),文件系统必须传递进MmFlushForDelete值。

 

函数功能:

  •  如果函数接受到一个参数是MmFlushForDelete,任何用户线程都已经映射文件流到他们自己的虚拟地址来作为一个适当的数据流(内存—映射文件),VMM将会马上返回错误。
  • 另一个实例是,不论MmFlushForDelete或者MmFlushForWrite被传进来,如果任何线程都已经映射文件流道他们自己的虚拟地址空间作为一个可执行体,VMM将会拒绝这个请求然会返回FALSE
  •  其他情况,VMM会得到页帧基地址锁然然后为删除标记镜像共享区对象。(imagesection object)

 

当VMM决定冲刷镜像共享区对象是安全的时候,VMM会历遍共享区对象的脏页表然后冲刷他们到二级存储如果他们是基于磁盘页文件。属于据映射文件的任何脏(修改过的)页简单的马上被丢弃。在开始冲刷操作之前,VMM将会保证所有在已经映射过的文件流上的异步修改页写者操作被停止(然后将会阻塞直到正在进行的写操作完成)。如果你的文件系统支持页文件和如果任何脏页都基于在文件系统的页文件,你的驱动必须期望接递归的接受到分页I/O写请求在这个时候。

当修改过的页都被冲刷了(如果需要),VMM将会拆卸掉文件流的镜像共享区对象,标记为对文件流执行一个删除或者打开操作安全。

还有另外2个重要的点你必须注意:

  • 当尝试为镜像共享区冲刷修改后的页到一个页文件,VMM会忽略任何I/O错误的发生。
  • VMM会解除在镜像共享区对象创建的时候对文件对象的引用。     对于文件系统的设计者,这意味着你的驱动能接受一个关闭的请求作为执行这个调用的一部分。如果你的文件系统在执行创建操作,不要惊讶于突然接受到一个最火关闭操作的这个函数的返回值。

 

MmCanFileBeTruncated

         这个函数是VMM提供来帮助文件系统决定是否一个文件流的截断操作是否被允许执行。VMM对于文件大小的修改或者删除是否被执行非常严格。如果文件被映射为一个可执行体一个用户是不能截断文件流的;如果一个镜像共享区对象已经被VMM创建和被一个用户线程使用截断请求将会被拒绝的。这样是因为如果一个页内容不再存在磁盘是因为截断操作的页错误,这会搞混一个文件,尽管文件内容之前是存在的。同样,如果任何线程有映射文件爱你流为一个数据文件(而不是一个镜像共享区),如果新的文件大小小于现在映射的文件流的视图长度,VMM不会允许截断请求。

       MmCanFileBeTruncated函数是文件系统在允许截断请求执行前调用的。一个使用这个函数的例子将会在Chapter 10提供,Writing A File System Driver II。这个函数的定义如下:

BOOLEAN

MmCanFileBeTruncated(

IN PSECTION_OBJECT_POINTERS            SectionPointer,

IN PLARGE_INTEGER                                   NewFileSize

);

资源的获取:

         文件系统驱动必须保证文件流在这之前已经互斥获得。一般是MainResource。

 

参数:

SectionObjectPointer

         SECTION_OBJECT_POINTERS结构体在下面的章节会有仔细的讨论。现在一个这个结构体是关联这一个内存中的文件流对象的。

 

NewFileSize

         一个指向large 整型的指针,包含这目标新文件大小。

 

函数作用:

  • MmCanFileBeTruncated调用MmFlushImageSection,提供MmFlushForWrite为冲刷请求的理由。
  • 如果MmFlushImageSection函数返回FALSE,MmCanFileBeTruncated函数将会返回FALSE和拒绝截断请求。
  • 不然的话,函数检查是否有用户线程映射视图存在;如果他们存在,新的文件大小小于映射视图,MmCanFileBeTruncated函数返回FALSE
  • 不然的话,函数返回TRUE,允许截断请求。

 

基本的原则是这样的:

  • 如果一个镜像共享区被一个文件流使用,VMM会返回FALSE。
  • 如果文件流的用户数据共享区存在,和如果新文件大小是小于现在映射视图的大小的,VMM返回FALSE。
  • 如果上面的2种情况之一返回TRUE,VMM会返回TRUE。

 

这章是说虚拟内存管理器。下面三章会介绍NT CacheManager,在缓存方面辅助文件系统驱动的内核模式组件。这个组件特别依靠NT VMM,同样也支持着VMM。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值