Win32 API提供了一种进行文件操作的高效途径,即内存映射文件。内存映射文件允许在Win32进程的虚拟地址空间中保留一段内存区域,把目标文件映射到这段虚拟内存之中。可以用存取内存数据的方式,直接操作文件中的数据,就像这些数据放在没存中一样。而实际上,我们并没有、也不需要调用API函数来读写文件,更不需要自己提供任何缓冲算法,操作系统将会完成这些工作。使用内存映射文件能给程序开发工作提供极大方便,程序的运行效率也很高。
内存映射文件在Windows NT和Windows 95中的实现机制略有不同,下面主要介绍Windows95下内存映射文件的工作原理及使用方法:
1. Windows95如何管理Win32进程的内存空间
内存映射文件的实现与Windows95的内存管理有密切的关系,因此先讨论一下Windows95在运行Win32进程时的内存管理与划分。
在Windows3.x下,所有Windows应用程序共享单一的地址空间,任何进程都能对这一空间中属于其他进程的内存进行读写,甚至可以存取操作系统本身的重要数据。在这种环境中,编写不当的应用程序或者带有恶意的应用程序,就可能破坏其他程序的数据或代码,使得系统运行不正常,严重时能导致系统崩溃。
在实现了Win32的操作系统Windows NT和Windows95中,每个进程拥有自己的地址空间,一个Win32进程不能存取另一个进程地址空间的私有数据,两个进程可以用具有相同值的指针寻址,但所读写的知识它们各自的数据,这样就大大减少了进程之间的相互干扰,增强了系统的为稳定性;另一方面,每个Win32进程拥有4GB的地址空间,但并不代表它真正拥有4GB的实际物理内存,而只是操作系统利用CPU的内存分页功能提供的虚拟地址空间。在一般情况下,绝大多数虚拟地址并没有物理内存与之相对应,在真正可以使用这些地址空间之前,还要由操作系统提供真正的实际物理内存。为虚拟地址提供实际物理内存的过程叫做提交(Commit)。在不同情况下,系统提交的物理内存的类型是不同的,可能是RAM。也可能是硬盘模拟的虚拟内存。
Windows95对Win32进程地址空间的划分如下:
地址空间底部的4MB由Windows95用来维护与DOS和16位Windows的兼容性。理想情况下,Win32进程应该不能访问这段内存,但由于实现上的困难,Windows95只能保护低端从0x00000000到0x00000FFF的4KB区域,这4KB区间用来捕获NULL指针。从0x80000000到0xBFFFFFFF的1GB空间由所有的Win32进程共享,内存映射文件就是用这段地址空间。高端的1GB空间由Windows95自己使用,不想Windows NT那样,这段空间也没有受到保护,任何进程都可能破坏其中的数据。
2. 内存映射文件的工作原理
内存映射文件分为三种情况:第一种是可执行文件的内存映射,主要由Windows95自身使用;第二种是数据文件的内存映射;最后一种是借助于页面交换文件的内存映射。应用程序可是使用后面两种内存映射文件。
(1).可执行文件的内存映射
Windows95在执行一个Win32应用程序是使用内存映射文件,它为将要执行的EXE文件保留足够大的地址空间。一般情况下,这段空间是从Win32进程的载入地址空间0x00400000开始,系统给这段空间提交的物理存储就是硬盘上的EXE文件本身。做好各项准备工作之后,系统开始执行这个程序。刚开始,程序的代码并不在RAM中,执行程序入口的第一条指令时会产生一个页面异常,系统捕获这个异常以后,分配一块RAM,将其映射到0x00400000处,并把实际代码读入其中,然后继续执行。以后在执行到不在RAM中的代码时,同样会产生页面异常,从而系统有机会读入这些代码。系统以类似的方式处理WIN32DLL,知识DLL被映射到的地址空间是由所有Win32所共享的。
当用户运行同一个应用程序的第二个实例时,系统指导程序已经有一个实例了,EXE文件的代码和数据已经被读到RAM中,系统只需要把这段RAM在映射到新进程的地址空间就行了,这就实现了共享RAM中的代码和数据。事实上,这种共享只是针对只读数据,一旦出现进程改写自身代码和数据时,操作系统会把被修改数据所在页面拷贝一份,分配给执行写操作的进程, 从而避免了多个实例之间的互相干扰。
(2) 数据文件的内存映射
数据文件内存映射的工作原理与可执行文件的内存映射原理是一样的。首先把数据文件的一部分映射到虚拟地址空间(映射到的区域是在0x800000000~0xBFFFFFFF内),但不提交RAM,存取这段内存的指令同样会产生页面异常。操作系统捕获到这个异常后,分配一页RAM,并把它映射到当前进程发生异常的地址处,然后系统把文件中相应的数据独到这个页面中,继续执行刚才产生异常的指令。这就是应用程序自己不需要调用文件I/O函数的原因。
(3) 基于页面交换文件的内存映射
内存映射文件的第三种情况是基于页面交换文件的。一个Win32进程可以利用内存映射文件在Win32进程共享的地址空间中保留一块区域,这块区域与系统的页面交换文件相联系。可以用它来存储临时数据,但更常见的用法是,利用他与其他Win32进程进行通信。事实上,Win32实现多进程间通信的各种方式都是通过内存映射文件来实现的,例如PostMessage()函数或SendMessage()函数在内部都使用了内存映射文件。
3. 用内存映射文件的使用方法
(1)利用内存映射文件进行文件爱你I/O操作
进行文件爱你I/O操作需要下面几个步骤
@,调用CreateFile()函数,以适当的方式创建或打开一个文件核心对象
@,把CreateFile()函数返回的文件句柄作为参数,传给CreateFileMapping()函数,由CreateFileMapping()函数创建一个文件映射核心对象的适当属性。
@,创建了文件映射核心对象后,调用MapViewOfFile()函数,告诉系统把文件的哪一部分映射到进程的地址空间中,以何种方式映射。
@,利用MapViewOfFile()函数返回的指针来使用文件数据。
@,操作完毕后,调用UnmapViewOfFile()函数,告诉系统撤销对文件映射核心对象的映射。
@,使用ColseHandle()函数关闭文件映射核心对象
@, 使用ColseHandle()函数关闭文件核心对象。
各个API函数的详细说明请参考Windows95 SDK或一些编程工具的联机帮助。
(2) 利用内存映射文件实现Win32进程间通信
Windows95下,一个进程打开的文件映射对像的映射区对所有的Win32进程都是可视的,并且映射区的地址对所有Win32进程都是一样的。一个进程可以打开一个文件,创建文件映射核心对象,用MapViewOfFile函数打开文件视图,然后将文件映射的地址传给另一个进程,第二个进程就可以读出文件中的数据。这种方法需要进行个进程的同步,实现起来较困难;而且在Windows NT中,一个映射区在不同的Win32进程空间中对应的地址不同。因此,为了与Windows NT兼容,尽量不要使用此方法。
第二种方法是两个进程使用同一文件映射核心对象,打开各自视图,或者父进程把自己创建的文件映射核心对象继承给子进程使用。这种方法比较安全有效。
第三种方法是创建基于页面交换文件的内存映射对象。在调用CreateFileMapping()函数时,传递文件句柄为0xFFFFFFFF,系统就差那个页面交换文件中提取物理存储,然后进程之间按照第二种方法进行通信。这种方法不用事先准备一个特设的文件,非常方便。