一、引言
WIN32 API为我们提供了一种进行文件操作的高效途径,即内存映射文件。
内存映射文件允许我们在WIN32进程的虚拟地址空间中保留一段内存区域,把目
标文件映射到这段虚拟内存之中。我们可以用存取内存数据的方式直接操作文件
中的数据,就好像这些数据放在内存中一样。而实际上,我们并没有、也不需要
调用API函数来读写文件,更不需要自己提供任何缓冲算法,操作系统将会为我们
完成这些工作。使用内存映射文件能给程序开发工作提供极大的方便,程序的运
行效率也非常高。 内存映射文件在Windows NT和Windows95中的实现机制略有
不同,下面主要介绍Windows95下内存映射文件的工作原理及使用方法。
二、Windows95如何管理WIN32进程的内存空间
内存映射文件的实现与Windows95的内存管理有密切的关系,因此先讨论一
下Windows95在运行WIN32进程时的内存管理与划分。
在Windows3.x下,所有Windows应用程序共享单一的地址空间,任何进程都
能够对这一空间中属于其他进程的内存进行读写操作,甚至可以存取操作系统本
身的重要数据。在这种环境中,编写不当的应用程序或者带有恶意的应用程序,
就可能破坏其他程序的数据或代码,使得系统运行不正常,严重时甚至会导致系
统崩溃。
在实现了WIN32的操作系统Windows NT和Windows95中,每个WIN32进程拥有
自己的地址空间,一个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
那样,这段空间也没有受到保护,任何进程都可能破坏其中的数据。
三、内存映射文件的工作原理
内存映射文件分三种情况,第一种是可执行文件的内存映射,主要由
Windows95自身使用;第二种是数据文件的内存映射;最后一种是借助于页面交
换文件的内存映射。应用程序可以使用后面两种内存映射文件。
1、可执行文件的内存映射
Windows95在执行一个WIN32应用程序时使用内存映射文件,它为将要执行
的EXE文件保留足够大的地址空间。一般情况下,这段空间是从WIN32进程的载入
地址0x00400000开始,系统给这段空间提交的物理存储就是硬盘上的EXE文件本
身。做好各项准备工作后,系统开始执行这个程序。刚开始,程序的代码并不在
RAM中,执行程序入口的第一条指令时会产生一个页面异常,系统捕获到这个异
常后,分配一块RAM,将其映射到0x00400000处,并把实际代码读入其中,然后
继续执行。以后在执行到不在RAM中的代码时,同样会产生页面异常,从而系统
有机会读入这些代码。系统以类似的方式处理WIN32DLL,只是DLL被映射到的地
址空间是由所有WIN32进程共享的。
当用户运行同一个应用程序的第二个实例时,系统知道程序已经有一个实
例了,EXE文件的代码和数据已经被读到RAM中,系统只需要把这段RAM再映射到
新进程的地址空间就行了,这就实现了共享RAM中的代码和数据。事实上,这种
共享只是针对只读数据,一旦出现进程改写自身代码和数据,操作系统会把被修
改数据所在页面拷贝一份,分配给执行写操作的进程,从而避免了多个实例之间
的相互干扰。
当然,操作系统执行一个WIN32应用程序的实际过程非常复杂,上面所描述
的只是工作原理。我们可以用Softicefor Windows95来验证操作系统是以映射文
件的方式来执行一个应用程序的:使用Wldr第一次调入一个应用程序如Notepad时
,Softice被激活。它所列出的程序的入口代码处全是Invalid(无效),这表明
将要执行的代码所在页面并不在RAM之中。按下F8,单步执行一条指令,屏幕上
立刻列出了真正的程序指令,这是因为指令执行时首先产生了一个页面异常,操
作系统在处理页面异常时,将代码读入RAM之中。Softice再次被激活时,就能看
见刚读入的指令了。进一步检查还可以发现,系统每次只读一个页面(4KB)到
RAM中,以便尽量节约内存。我们再用Wldr调入Notepad的第二个实例,这一次
Softice被激活后列出的入口代码不再是Invalid,而是真正的程序指令。由于
Softice是系统级的调试器,用它修改内存中的应用程序时,操作系统并不做页
面拷贝。我们将Notepad的入口代码做一点改动,然后再用Wldr调入第三个实例。
这次可以发现,列出的入口代码是刚刚修改过的,而实际的EXE文件并无任何变
化,这表明,操作系统把同一块RAM中的程序代码映射到多个进程的地址空间中,
从而实现了共享RAM中的程序代码。
2、数据文件的内存映射
数据文件内存映射的工作原理与可执行文件的内存映射原理是一样的。首先
把数据文件的一部分映射到虚拟地址空间(映射到的区域是在
0x80000000-0xBFFFFFFF内),但不提交RAM,存取这段内存的指令同样会产生
页面异常。操作系统捕获到这个异常后,分配一页RAM,并把它映射到当前进程
发生异常的地址处,然后系统把文件中相应的数据读到这个页面中,继续执行
刚才产生异常的指令。这就是应用程序自己不需要调用文件I/O函数的原因。
3、基于页面交换文件的内存映射
内存映射文件的第三种情况是基于页面交换文件的。一个WIN32进程可以利
用内存映射文件在WIN32进程共享的地址空间中保留一块区域,这块区域与系统
的页面交换文件相联系。我们可以用它来存储临时数据,但更常见的用法是,利
用它与其他WIN32进程进行通信。事实上,WIN32实现多进程间通信的各种方法都
是通过内存映射文件来实现的,例如PostMessage()函数或SendMessage()函数,
在内部都使用了内存映射文件。
四、使用内存映射文件的方法
1、利用内存映射文件进行文件I/O操作,进行文件I/O操作需要下面几个步骤:
步骤一:调用CreateFile()函数,以适当的方式创建或打开一个文件核心对象;
步骤二:把CreateFile()函数返回的文件句柄作为参数,传给CreateFileMapping()
函数,由CreateFileMapping()函数创建一个文件映射核心对象的适当属性;
步骤三:创建了文件映射核心对象后,调用MapViewOfFile()函数,告诉系
统把文件的哪一部分映射到进程的地址空间中,以何种方式映射;
步骤四:利用MapViewOfFile()函数返回的指针来使用文件数据;
步骤五:操作完毕后,调用UnmapViewOfFile()函数,告诉系统撤销对文件
映射核心对象的映射;
步骤六:使用CloseHandle()函数关闭文件映射核心对象;
步骤七:使用CloseHandle()函数关闭文件核心对象;
各个API函数的详细说明请参考Windows95SDK或一些编程工具的联机帮助。
2、利用内存映射文件实现WIN32进程间的通信
在Windows95下,一个进程打开的文件映射对象的映射区对所有的WIN32进程
都是可视的,并且映射区的地址对所有WIN32进程都是一样的。一个进程可以打开
一个文件,创建文件映射核心对象,用MapViewOfFile()函数打开文件视图,然后
将文件映射的地址传给另一个进程,第二个进程就可以读出文件中的数据。这种
方法需要进行各进程间的同步,实现起来较困难。并且在Windows NT中,一个映
射区在不同的WIN32进程空间中对应的地址不同,因此为了与Windows NT兼容,
尽量不要使用这种方法。
第二种方法是两个进程使用同一文件映射核心对象,打开各自的视图,或
者父进程把自己创建的文件映射核心对象继承给子进程使用。这种方法比较安全
有效。
第三种方法是创建基于页面交换文件的内存映射对象。在调用
CreateFileMapping()函数时,传递文件句柄为0xFFFFFFFF,系统就从页面交换
文件中提交物理存储,然后进程之间按照第二种方法进行通信。这种方法不用
事先准备一个特殊的文件,非常方便。