第17章 内存映射文件

对文件进行操作几乎是所有应用程序都必须进行的,并且这常常是人们争论的一个问题。应用程序究竟是应该打开文件,读取文件并关闭文件,还是打开文件,然后使用一种缓冲算法,从文件的各个不同部分进行读取和写入呢?M i c r o s o f t提供了一种两全其美的方法,那就是内存映射文件。

与虚拟内存一样,内存映射文件可以用来保留一个地址空间的区域,并将物理存储器提交给该区域。它们之间的差别是,物理存储器来自一个已经位于磁盘上的文件,而不是系统的页文件。一旦该文件被映射,就可以访问它,就像整个文件已经加载内存一样。

内存映射文件可以用于3个不同的目的:

• 系统使用内存映射文件,以便加载和执行. e x e和D L L文件。这可以大大节省页文件空间和应用程序启动运行所需的时间。

• 可以使用内存映射文件来访问磁盘上的数据文件。这使你可以不必对文件执行I / O操作,并且可以不必对文件内容进行缓存。

• 可以使用内存映射文件,使同一台计算机上运行的多个进程能够相互之间共享数据。Wi n d o w s确实提供了其他一些方法,以便在进程之间进行数据通信,但是这些方法都是使用内存映射文件来实现的,这使得内存映射文件成为单个计算机上的多个进程互相进行通信的最有效的方法。

本章将要介绍内存映射文件的各种使用方法。


17.1 内存映射的可执行文件和DLL文件

当线程调用C r e a t e P r o c e s s时,系统将执行下列操作步骤:

1) 系统找出在调用C r e a t e P r o c e s s时设定的. e x e文件。如果找不到这个. e x e文件,进程将无法创建,C r e a t e P r o c e s s将返回FA L S E。

2) 系统创建一个新进程内核对象。

3) 系统为这个新进程创建一个私有地址空间。

4) 系统保留一个足够大的地址空间区域,用于存放该. e x e文件。该区域需要的位置在. e x e文件本身中设定。按照默认设置, . e x e文件的基地址是0 x 0 0 4 0 0 0 0 0(这个地址可能不同于在6 4位Windows 2000上运行的6 4位应用程序的地址),但是,可以在创建应用程序的. e x e文件时重载这个地址,方法是在链接应用程序时使用链接程序的/ B A S E选项。

5) 系统注意到支持已保留区域的物理存储器是在磁盘上的. e x e文件中,而不是在系统的页文件中。

当. e x e文件被映射到进程的地址空间中之后,系统将访问. e x e文件的一个部分,该部分列出了包含. e x e文件中的代码要调用的函数的D L L文件。然后,系统为每个D L L文件调用L o a d L i b r a r y函数,如果任何一个D L L需要更多的D L L,那么系统将调用L o a d L i b r a r y函数,以便加载这些D L L。每当调用L o a d L i b r a r y来加载一个D L L时,系统将执行下列操作步骤,它们均类似上面的第4和第5个步骤:

1) 系统保留一个足够大的地址空间区域,用于存放该D L L文件。该区域需要的位置在D L L文件本身中设定。按照默认设置, M i c r o s o f t的Visual C++ 建立的D L L文件基地址是0 x 1 0 0 0 0 0 0 0(这个地址可能不同于在6 4位Windows 2000上运行的6 4位D L L的地址)但是,你可以在创建D L L文件时重载这个地址,方法是使用链接程序的/ B A S E选项。Wi n d o w s提供的所有标准系统D L L都拥有不同的基地址,这样,如果加载到单个地址空间,它们就不会重叠。

2) 如果系统无法在该D L L的首选基地址上保留一个区域,其原因可能是该区域已经被另一个D L L或. e x e占用,也可能是因为该区域不够大,此时系统将设法寻找另一个地址空间的区域来保留该D L L。如果一个D L L无法加载到它的首选基地址,这将是非常不利的,原因有二。首先,如果系统没有再定位信息,它就无法加载该D L L(可以在D L L创建时,使用链接程序的/ F I X E D开关,从D L L中删除再定位信息,这能够使D L L变得比较小,但是这也意味着该D L L必须加载到它的首选地址中,否则它就根本无法加载)。第二,系统必须在D L L中执行某些再定位操作。在Windows 98中,系统可以在页面被转入R A M时执行再定位操作。在Windows 2000中,这些再定位操作需要由系统的页文件提供更多的存储器,它们也增加了加载D L L所需要的时间量。

3) 系统会注意到支持已保留区域的物理存储器位于磁盘上的D L L文件中,而不是在系统的页文件中。如果由D L L无法加载到它的首选基地址,Windows 2000必须执行再定位操作,那么系统也将注意到D L L的某些物理存储器已经被映射到页文件中。

如果由于某个原因系统无法映射. e x e和所有必要的D L L文件,那么系统就会向用户显示一个消息框,并且释放进程的地址空间和进程对象。C r e a t e P r o c e s s函数将向调用者返回FA L S E,调用者可以调用G e t L a s t E r r o r函数,以便更好地了解为什么无法创建该进程。

当所有的. e x e和D L L文件都被映射到进程的地址空间之后,系统就可以开始执行. e x e文件的启动代码。当. e x e文件被映射后,系统将负责所有的分页、缓冲和高速缓存的处理。例如,如果. e x e文件中的代码使它跳到一个尚未加载到内存的指令地址,那么就会出现一个错误。系统能够发现这个错误,并且自动将这页代码从该文件的映像加载到一个R A M页面。然后,系统将这个R A M页面映射到进程的地址空间中的相应位置,并且让线程继续运行,就像这页代码已经加载了一样。当然,这一切是应用程序看不见的。当进程中的线程每次试图访问尚未加载到R A M的代码或数据时,该进程就会重复执行。

17.1.1 可执行文件或DLL的多个实例不能共享静态数据

当为正在运行的应用程序创建新进程时,系统将打开用于标识可执行文件映像的文件映射对象的另一个内存映射视图,并创建一个新进程对象和(为主线程创建)一个新线程对象。系统还要将新的进程I D和线程I D赋予这些对象。通过使用内存映射文件,同一个应用程序的多个正在运行的实例就能够共享R A M中的相同代码和数据。

这里有一个小问题需要注意。进程使用的是一个平面地址空间。当编译和链接你的程序时,所有的代码和数据都被合并在一起,组成一个很大的结构。数据与代码被分开,但仅限于跟在. e x e文件中的代码后面的数据而已。(实际上,文件的内容被分割为不同的节。代码放在一个节中,全局变量放在另一个节中。各个节按照页面边界来对齐。通过调用Get SystemInfo 函数,应用程序可以确定正在使用的页面的大小。在. e x e或D L L文件中,代码节通常位于数据数据节的前面。)图1 7 - 1简单说明了应用程序的代码和数据究竟是如何加载到虚拟内存中,然后又被映射到应用程序的地址空间中的。

作为一个例子,假设应用程序的第二个实例正在运行。系统只是将包含文件的代码和数据的虚拟内存页面映射到第二个应用程序的地址空间,如图1 7 - 2所示。

如果应用程序的一个实例改变了驻留在数据页面中的某些全局变量,那么该应用程序的所有实例的内存内容都将改变。这种类型的改变可能带来灾难性的后果,因此是决不允许的。

系统运用内存管理系统的c o p y - o n - w r i t e(写入时拷贝)特性来防止进行这种改变。每当应用程序尝试将数据写入它的内存映射文件时,系统就会抓住这种尝试,为包含应用程序尝试写入数据的内存页面分配一个新内存块,再拷贝该页面的内容,并允许该应用程序将数据写入这个新分配的内存块。结果,同一个应用程序的所有其他实例的运行都不会受到影响。图1 7 - 3显示了当应用程序的第一个实例尝试改变数据页面2时出现的情况。


图17-1 应用程序的代码和数据加载及映射示意图


图17-2 应用程序与虚拟内存地址空间之间的关系示意图


图17-3 应用程序的第一个实例尝试改变数据页面2时的情况

系统分配一个新的虚拟内存页面,并且将数据页面2的内容拷贝到新页面中。第一个实例的地址空间发生了变更,这样,新数据页面就被映射到与原始地址页面相同位置上的地址空间中。这时系统就可以让进程修改全局变量,而不必担心改变同一个应用程序的另一个实例的数据。

当应用程序被调试时,将会发生类似的事件。比如说,你正在运行一个应用程序的多个实例,并且只想调试其中的一个实例。你访问调试程序,在一行源代码中设置一个断点。调试程序修改了你的代码,将你的一个汇编语言指令改为能使调试程序自行激活的指令。因此你再次遇到了同样的问题。当调试程序修改代码时,它将导致应用程序的所有实例在修改后的汇编语言指令运行时激活该调试程序。为了解决这个问题,系统再次使用c o p y - o n - w r i t e内存。当系统发现调试程序试图修改代码时,它就分配一个新内存块,将包含该指令的页面拷贝到新的内存页面中,并且允许调试程序修改页面拷贝中的代码。

Windows 98 当一个进程被加载时,系统要查看文件映像的所有页面。系统立即为通常用c o p y - o n - w r i t e属性保护的那些页面提交页文件中的存储器。这些页面只是被提交而已,它们并不被访问。当文件映像中的页面被访问时,系统就加载相应的页面。如果该页面从来没有被修改,它就可以从内存中删除,并在必要时重新加载。但是,如果文件的页面被修改了,系统就将修改过的页面转到页文件中以前被提交的页面之一。

Windows 2000与Windows 98之间的行为特性的唯一差别,是在你加载一个模块的两个拷贝并且可写入的数据尚未被修改的时候显示出来的。在这种情况下,在Windows 2000下运行的进程能够共享数据,而在Windows 98下,每个进程都可以得到它自己的数据拷贝。如果只加载模块的一个拷贝,或者可写入的数据已经被修改(这是通常的情况),那么Windows 2000与Windows 98的行为特性是完全相同的。

17.1.2 在可执行文件或DLL的多个实例之间共享静态数据

全局数据和静态数据不能被同一个. e x e或D L L文件的多个映像共享,这是个安全的默认设置。但是,在某些情况下,让一个. e x e文件的多个映像共享一个变量的实例是非常有用和方便的。例如,Wi n d o w s没有提供任何简便的方法来确定用户是否在运行应用程序的多个实例。但是,如果能够让所有实例共享单个全局变量,那么这个全局变量就能够反映正在运行的实例的数量。当用户启动应用程序的一个实例时,新实例的线程能够简单地查看全局变量的值(它已经被另一个实例更新);如果这个数量大于1,那么第二个实例就能够通知用户,该应用程序只有一个实例可以运行,而第二个实例将终止运行。

本节将介绍一种方法,它允许你共享. e x e或D L L文件的所有实例的变量。不过在介绍这个方法之前,首先让我们介绍一些背景知识。

每个. e x e或D L L文件的映像都由许多节组成。按照规定,每个标准节的名字均以圆点开头。例如,当编译你的程序时,编译器会将所有代码放入一个名叫. t e x t的节中。该编译器还将所有未经初始化的数据放入一个. b s s节,而已经初始化的所有数据则放入. d a t a节中。

每一节都拥有与其相关的一组属性,这些属性如表1 7 - 1所示。

表17-1 .exe或D L L文件各节的属性

属性 含义
R E A D 该节中的字节可以读取
W R I T E 该节中的字节可以写入
E X E C U T E 该节中的字节可以执行
S H A R E D 该节中的字节可以被多个实例共享(本属性能够有效地关闭c o p y - o n - w r i t e机制)

使用M i c r o s o f t的Visual Studio的D u m p B i n实用程序(带有/ H e a d e r s开关),可以查看. e x e或D L L映射文件中各个节的列表。下面选录的代码是在一个可执行文件上运行D u m p B i n程序而生成的:

SECTION HEADER #1
   .text name
   11A70 virtual size
    1000 virtual address
   12000 size of raw data
    1000 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
60000020 flags
         Code
         Execute Read

SECTION HEADER #2
  .rdata name
     1F6 virtual size
   13000 virtual address
    1000 size of raw data
   13000 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
40000040 flags
         Initialized Data
         Read Only

SECTION HEADER #3
   .data name
     560 virtual size
   14000 virtual address
    1000 size of raw data
   14000 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         Read Write

SECTION HEADER #4
  .idata name
     58D virtual size
   15000 virtual address
    1000 size of raw data
   15000 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         Read Write

SECTION HEADER #5
  .didat name
     7A2 virtual size
   16000 virtual address
    1000 size of raw data
   16000 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
C0000040 flags
         Initialized Data
         Read Write

SECTION HEADER #6
  .reloc name
     26D virtual size
   17000 virtual address
    1000 size of raw data
   17000 file pointer to raw data
       0 file pointer to relocation table
       0 file pointer to line numbers
       0 number of relocations
       0 number of line numbers
42000040 flags
         Initialized Data
         Discardable
         Read Only

   Summary
        1000 .data
        1000 .didat
        1000 .idata
        1000 .rdata
        1000 .reloc
       12000 .text
表1 7 - 2显示了比较常见的一些节的名字,并且说明了每一节的作用。

除了编译器和链接程序创建的标准节外,也可以在使用下面的命令进行编译时创建自己的节:

表17-2 常见的节名及作用

节名 作用
. b s s 未经初始化的数据
. C RT C运行期只读数据
. d a t a 已经初始化的数据
. d e b u g 调试信息
. d i d a t a 延迟输入文件名表
. e d a t a 输出文件名表
. i d a t a 输入文件名表
. r d a t a 运行期只读数据
. r e l o c 重定位表信息
. r s r c 资源
. t e x t . e x e或D L L文件的代码
. t l s 线程的本地存储器
. x d a t a 异常处理表

#pragma data_seg("sectionname")
我可以创建一个称为“S h a r e d”的节,它包含单个L O N G值,如下所示:

#pragma data_seg("Shared")
LONG g_lInstanceCount = 0;
#pragma data_seg()
当编译器对这个代码进行编译时,它创建一个新节,称为S h a r e d,并将它在编译指示后面看到的所有已经初始化(i n i t i a l i z e d)的数据变量放入这个新节中。在上面这个例子中,变量放入S h a r e d节中。该变量后面的#pragma dataseg()一行告诉编译器停止将已经初始化的变量放入S h a r e d节,并且开始将它们放回到默认数据节中。需要记住的是,编译器只将已经初始化的变量放入新节中。例如,如果我从前面的代码段中删除初始化变量(如下面的代码所示),那么编译器将把该变量放入S h a r e d节以外的节中。

#pragma data_seg("Shared")
LONG g_lInstanceCount;
#pragma data_seg()
Microsoft 的Visual C++编译器提供了一个A l l o c a t e说明符,使你可以将未经初始化的数据放入你希望的任何节中。请看下面的代码:

// Create Shared section & have compiler place initialized data in it.
#pragma data_seg("Shared")

// Initialized, in Shared section
int a = 0;

// Uninitialized, not in Shared section
int b;

// Have compiler stop placing initialized data in Shared section.
#pragma data_seg()

// Initialized, in Shared section
__declspec(allocate("Shared")) int c = 0;

// Uninitialized, in Shared section
__declspec(allocate("Shared")) int d;

// Initialized, not in Shared section
int e = 0;

// Uninitialized, not in Shared section
int f;        
上面的注释清楚地指明了指定的变量将被放入哪一节。若要使A l l o c a t e声明的规则正确地起作用,那么首先必须创建节。如果删除前面这个代码中的第一行#pragma data_seg,上面的代码将不进行编译。

之所以将变量放入它们自己的节中,最常见的原因也许是要在. e x e或D L L文件的多个映像之间共享这些变量。按照默认设置, . e x e或D L L文件的每个映像都有它自己的一组变量。然而,可以将你想在该模块的所有映像之间共享的任何变量组合到它自己的节中去。当给变量分组时,系统并不为. e x e或D L L文件的每个映像创建新实例。

仅仅告诉编译器将某些变量放入它们自己的节中,是不足以实现对这些变量的共享的。还必须告诉链接程序,某个节中的变量是需要加以共享的。若要进行这项操作,可以使用链接程序的命令行上的/ S E C T I O N开关:

/SECTION:name,attributes
在冒号的后面,放入你想要改变其属性的节的名字。在我们的例子中,我们想要改变S h a r e d节的属性。因此应该创建下面的链接程序开关:

/SECTION:Shared,RWS
在逗号的后面,我们设定了需要的属性。用R代表R E A D ,W代表W E I T E,E代表E X E C U T E,S代表S H A R E D。上面的开关用于指明位于S h a r e d节中的数据是可以读取、写入和共享的数据。如果想要改变多个节的属性,必须多次设定/ S E C T I O N开关,也就是为你要改变属性的每个节设定一个/ S E C T I O N开关。

也可以使用下面的句法将链接程序开关嵌入你的源代码中:

#pragma comment(linker, "/SECTION:Shared,RWS")
这一行代码告诉编译器将上面的字符串嵌入名字为“ . d r e c t v e”的节。当链接程序将所有的. o b j模块组合在一起时,链接程序就要查看每个. o b j模块的“ . d r e c t v e”节,并且规定所有的字符串均作为命令行参数传递给该链接程序。我一直使用这种方法,因为它非常方便。如果将源代码文件移植到一个新项目中,不必记住在Visual C++的Project Settings(项目设置)对话框中设置链接程序开关。

虽然可以创建共享节,但是,由于两个原因, M i c r o s o f t并不鼓励你使用共享节。第一,用这种方法共享内存有可能破坏系统的安全。第二,共享变量意味着一个应用程序中的错误可能影响另一个应用程序的运行,因为它没有办法防止某个应用程序将数据随机写入一个数据块。

假设你编写了两个应用程序,每个应用程序都要求用户输入一个口令。然而你又决定给应用程序添加一些特性,使用户操作起来更加方便些:如果在第二个应用程序启动运行时,用户正在运行其中的一个应用程序,那么第二个应用程序就可以查看共享内存的内容,以便获得用户的口令。这样,如果程序中的某一个已经被使用,那么用户就不必重新输入他的口令。

这听起来没有什么问题。毕竟没有别的应用程序而只有你自己的应用程序加载了D L L,并且知道到什么地方去查找包含在共享节中的口令。但是,黑客正在窥视着你的行动,如果他们想要得到你的口令,只需要编写一段很短的程序,加载到你的公司的D L L文件中,然后监控共享内存块。当用户输入口令时,黑客的程序就能知道该用户的口令。

黑客精心编制的程序也可能试图反复猜测用户的口令并将它们写入共享内存。一旦该程序猜测到正确的口令,它就能够将各种命令发送给两个应用程序中的一个。如果有一种办法只为某些应用程序赋予访问权,以便加载一个特定的D L L,那么这个问题也许是可以解决的。但是目前还不行,因为任何程序都能够调用L o a d L i b r a r y函数来显式加载D L L。

17.1.3 AppInst示例应用程序

清单1 7 - 1列出的A p p I n s t示例应用程序(“17 AppInst.exe”)显示了应用程序如何能够知道每次有多少个应用程序的实例正在运行。该应用程序的源代码和资源文件位于本书所附光盘上的1 7 - A p p I n s t目录下。当运行A p p I n s t程序时,就会出现它的对话框(见图1 7 - 4),指明该应用程序的一个实例正在运行。


图17-4 运行AppInst 时出现的对话框

如果运行该应用程序的第二个实例,那么第一和第二个实例的对话框都会发生变化,以反映目前两个实例都在运行(见图1 7 - 5,图1 7 - 6)


图17-5 运行AppInst 的第二个实例时,第一个实例对话框的变化


图17-6 运行AppInst 的第二个实例时,第二个实例对话框的变化

可以根据你的喜好,运行和撤消任意多个实例,实例的数量始终都能正确地反映在仍然保留的实例中。

在靠近A p p I n s t . c p p应用程序的顶部,可以看到下面的代码行:

// Tell the compiler to put this initialized variable in its own Shared
// section so it is shared by all instances of this application.
#pragma data_seg("Shared")
volatile LONG g_lApplicationInstances = 0;
#pragma data_seg()

// Tell the linker to make the Shared section
// readable, writable, and shared.
#pragma comment(linker, "/Section:Shared,RWS")
这些代码行用于创建一个称为S h a r e d的节,该节拥有读取、写入和共享保护属性。在这个节中,有一个变量是g _ l A p p l i c a t i o n I n s t a n c e s。该应用程序的所有实例均可以共享该变量。注意,该变量是个易失性变量,因此优化程序对我们不起多大的作用。

当每个实例的_ t Wi n M a i n函数执行时, g _ l A p p l i c a t i o n I n s t a n c e s变量就递增1。在_ t Wi n M a i n退出之前,该变量将递减1。我使用I n t e r l o c k e d E x c h a n g e A d d来改变这个变量,因为多个线程将要访问该共享资源。

当每个实例的对话框出现时, D l g _ O n I n i t D i a l o g函数就被调用。该函数将一个注册窗口消息广播发送到所有的高层窗口(该消息的I D包含在g _ a M s g A p p I n s t C o u n t U p d a t e变量中)

PostMessage(HWND_BROADCAST, g_aMsgAppInstCountUpdate, 0, 0);
系统中的所有窗口将忽略这个注册窗口消息,但A p p I n s t的各个窗口例外。当我们的各个窗口中的一个接收到该消息时, D l g _ P r o c中的代码将更新该对话框中的实例数量,以反映当前的实例数量(该数量在g _ l A p p l i c a t i o n I n s t a n c e s共享变量中进行维护)。

清单17-1 AppInst示例应用程序

/******************************************************************************
Module:  AppInst.cpp
Notices: Copyright (c) 2000 Jeffrey Richter
******************************************************************************/


#include "..\CmnHdr.h"     /* See Appendix A. */
#include <windowsx.h>
#include <tchar.h>
#include "Resource.h"


///////////////////////////////////////////////////////////////////////////////


// The system-wide unique window message
UINT g_uMsgAppInstCountUpdate = INVALID_ATOM;


///////////////////////////////////////////////////////////////////////////////


// Tell the compiler to put this initialized variable in its own Shared 
// section so it is shared by all instances of this application.
#pragma data_seg("Shared")
volatile LONG g_lApplicationInstances = 0;
#pragma data_seg()

// Tell the linker to make the Shared section readable, writable, and shared.
#pragma comment(linker, "/Section:Shared,RWS")


///////////////////////////////////////////////////////////////////////////////


BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {

   chSETDLGICONS(hwnd, IDI_APPINST);

   // Force the static control to be initialized correctly.
   PostMessage(HWND_BROADCAST, g_uMsgAppInstCountUpdate, 0, 0);
   return(TRUE);
}


///////////////////////////////////////////////////////////////////////////////


void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {

   switch (id) {
      case IDCANCEL:
         EndDialog(hwnd, id);
         break;
   }
}


///////////////////////////////////////////////////////////////////////////////


INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

   if (uMsg == g_uMsgAppInstCountUpdate) {
      SetDlgItemInt(hwnd, IDC_COUNT, g_lApplicationInstances, FALSE);
   }

   switch (uMsg) {
      chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
      chHANDLE_DLGMSG(hwnd, WM_COMMAND,    Dlg_OnCommand);
   }
   return(FALSE);
}


///////////////////////////////////////////////////////////////////////////////


int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) {

   // Get the numeric value of the systemwide window message used to notify 
   // all top-level windows when the module's usage count has changed.
   g_uMsgAppInstCountUpdate =
      RegisterWindowMessage(TEXT("MsgAppInstCountUpdate"));

   // There is another instance of this application running
   InterlockedExchangeAdd((PLONG) &g_lApplicationInstances, 1);

   DialogBox(hinstExe, MAKEINTRESOURCE(IDD_APPINST), NULL, Dlg_Proc);

   // This instance of the application is terminating
   InterlockedExchangeAdd((PLONG) &g_lApplicationInstances, -1);

   // Have all other instances update their display
   PostMessage(HWND_BROADCAST, g_uMsgAppInstCountUpdate, 0, 0);

   return(0);
}


//////////////////////////////// End of File //////////////////////////////////
//Microsoft Developer Studio generated resource script.
//
#include "Resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// English (U.S.) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32

#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE 
BEGIN
    "Resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE 
BEGIN
    "#include ""afxres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE DISCARDABLE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

IDD_APPINST DIALOG DISCARDABLE  0, 0, 140, 21
STYLE WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Application Instances"
FONT 8, "MS Sans Serif"
BEGIN
    LTEXT           "Number of instances running:",IDC_STATIC,12,4,93,8,
                    SS_NOPREFIX
    RTEXT           "#",IDC_COUNT,112,4,16,12,SS_NOPREFIX
END


/////////////////////////////////////////////////////////////////////////////
//
// Icon
//

// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_APPINST             ICON    DISCARDABLE     "AppInst.Ico"

/////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
//

#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO DISCARDABLE 
BEGIN
    IDD_APPINST, DIALOG
    BEGIN
        RIGHTMARGIN, 76
        BOTTOMMARGIN, 20
    END
END
#endif    // APSTUDIO_INVOKED

#endif    // English (U.S.) resources
/////////////////////////////////////////////////////////////////////////////



#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//


/////////////////////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED


17.2 内存映射数据文件

操作系统使得内存能够将一个数据文件映射到进程的地址空间中。因此,对大量的数据进行操作是非常方便的。

为了理解用这种方法来使用内存映射文件的功能,让我们看一看如何用4种方法来实现一个程序,以便将文件中的所有字节的顺序进行倒序。

17.2.1 方法1:一个文件,一个缓存

第一种方法也是理论上最简单的方法,它需要分配足够大的内存块来存放整个文件。该文件被打开,它的内容被读入内存块,然后该文件被关闭。文件内容进入内存后,我们就可以对所有字节的顺序进行倒序,方法是将第一个字节倒腾为最后一个字节,第二个字节倒腾为倒数第二个字节,依次类推。这个倒腾操作将一直进行下去直到文件的中间位置。当所有的字节都已经倒腾之后,就可以重新打开该文件,并用内存块的内容来改写它的内容。

这种方法实现起来非常容易,但是它有两个缺点。首先,必须分配一个与文件大小相同的内存块。如果文件比较小,那么这没有什么问题。但是如果文件非常大,比如说有2 G B大,那该怎么办呢?一个3 2位的系统不允许应用程序提交那么大的物理内存块。因此大文件需要使用不同的方法。

第二,如果进程在运行过程的中间被中断,也就是说当倒序后的字节被重新写入该文件时进程被中断,那么文件的内容就会遭到破坏。防止出现这种情况的最简单的方法是在对它的内容进行倒序之前先制作一个原始文件的拷贝。如果整个进程运行成功,那么可以删除该文件的拷贝。这种方法需要更多的磁盘空间。

17.2.2 方法2:两个文件,一个缓存

在第二种方法中,你打开现有的文件,并且在磁盘上创建一个长度为0的新文件。然后分配一个比较小的内部缓存,比如说8 KB。你找到离原始文件结尾还有8 KB的位置,将这最后的8 KB读入缓存,将字节倒序,再将缓存中的内容写入新创建的文件。这个寻找、读入、倒序和写入的操作过程要反复进行,直到到达原始文件的开头。如果文件的长度不是8 KB的倍数,那么必须进行某些特殊的处理。当原始文件完全处理完毕之后,将原始文件和新文件关闭,并删除原始文件。

这种方法实现起来比第一种方法要复杂一些。它对内存的使用效率要高得多,因为它只需要分配一个8 KB的缓存块,但是它存在两个大问题。首先,它的处理速度比第一种方法要慢,原因是在每个循环操作过程中,在执行读入操作之前,必须对原始文件进行寻找操作。第二,这种方法可能要使用大量的硬盘空间。如果原始文件是400 MB,那么随着进程的不断运行,新文件就会增大为400 MB。在原始文件被删除之前,两个文件总共需要占用800 MB的磁盘空间。这比应该需要的空间大400 MB。由于存在这个缺点,因此引来了下一个方法。

17.2.3 方法3:一个文件,两个缓存

如果使用这个方法,那么我们假设程序初始化时分配了两个独立的8 KB缓存。程序将文件的第一个8 KB读入一个缓存,再将文件的第二个8 KB 读入另一个缓存。然后进程将两个缓存的内容进行倒序,并将第一个缓存的内容写回文件的结尾处,将第二个缓存的内容写回同一个文件的开始处。每个迭代操作不断进行(以8 KB为单位,从文件的开始和结尾处移动文件块)。如果文件的长度不是16 KB的倍数,并且有两个8 KB的文件块相重叠,那么就需要进行一些特殊的处理。这种特殊处理比上一种方法中的特殊处理更加复杂,不过这难不倒经验丰富的编程员。

与前面的两种方法相比,这种方法在节省硬盘空间方面有它的优点。由于所有内容都是从同一个文件读取并写入同一个文件,因此不需要增加额外的磁盘空间,至于内存的使用,这种方法也不错,它只需要使用16 KB的内存。当然,这种方法也许是最难实现的方法。与第一种方法一样,如果进程被中断,本方法会导致数据文件被破坏。

下面让我们来看一看如何使用内存映射文件来完成这个过程。

17.2.4 方法4:一个文件,零缓存

当使用内存映射文件对文件内容进行倒序时,你打开该文件,然后告诉系统将虚拟地址空间的一个区域进行倒序。你告诉系统将文件的第一个字节映射到该保留区域的第一个字节。然后可以访问该虚拟内存的区域,就像它包含了这个文件一样。实际上,如果在文件的结尾处有一个单个0字节,那么只需要调用C运行期函数_ s t r r e v,就可以对文件中的数据进行倒序操作。

这种方法的最大优点是,系统能够为你管理所有的文件缓存操作。不必分配任何内存,或者将文件数据加载到内存,也不必将数据重新写入该文件,或者释放任何内存块。但是,内存映射文件仍然可能出现因为电源故障之类的进程中断而造成数据被破坏的问题。


17.3 使用内存映射文件

若要使用内存映射文件,必须执行下列操作步骤:

1) 创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件。

2) 创建一个文件映射内核对象,告诉系统该文件的大小和你打算如何访问该文件。

3) 让系统将文件映射对象的全部或一部分映射到你的进程地址空间中。

当完成对内存映射文件的使用时,必须执行下面这些步骤将它清除:

1) 告诉系统从你的进程的地址空间中撤消文件映射内核对象的映像。

2) 关闭文件映射内核对象。

3) 关闭文件内核对象。

下面将详细介绍这些操作步骤。

17.3.1 步骤1:创建或打开文件内核对象

若要创建或打开一个文件内核对象,总是要调用C r e a t e F i l e函数:

HANDLE CreateFile(
   PCSTR pszFileName,
   DWORD dwDesiredAccess,
   DWORD dwShareMode,
   PSECURITY_ATTRIBUTES psa,
   DWORD dwCreationDisposition,
   DWORD dwFlagsAndAttributes,
   HANDLE hTemplateFile);
C r e a t e F i l e函数拥有好几个参数。这里只重点介绍前3个参数,即p s z F i l e N a m e,d w D e s i r e d A c c e s s和d w S h a r e M o d e。

你可能会猜到,第一个参数p s z F i l e N a m e用于指明要创建或打开的文件的名字(包括一个选项路径)。第二个参数d w D e s i r e d A c c e s s用于设定如何访问该文件的内容。可以设定表1 7 - 3所列的4个值中的一个。

表17-3 dwDesiredAccess的值

含义
0 不能读取或写入文件的内容。当只想获得文件的属性时,请设定0
G E N E R I C _ R E A D 可以从文件中读取数据
G E N E R I C _ W R I T E 可以将数据写入文件
GENERIC_READ |GENERIC_WRITE 可以从文件中读取数据,也可以将数据写入文件

当创建或打开一个文件,将它作为一个内存映射文件来使用时,请选定最有意义的一个或多个访问标志,以说明你打算如何访问文件的数据。对内存映射文件来说,必须打开用于只读访问或读写访问的文件,因此,可以分别设定G E N E R I C _ R E A D 或GENERIC_READ |G E N E R I C _ W R I T E。

第三个参数d w S h a r e M o d e告诉系统你想如何共享该文件。可以为d w S h a r e M o d e设定表1 7 - 4所列的4个值之一。

表17-4 dwShareMode 的值

含义
0 打开文件的任何尝试均将失败
F I L E _ S H A R E _ R E A D 使用G E N E R I C _ W R I T E打开文件的其他尝试将会失败
F I L E _ S H A R E _ W R I T E 使用G E N E R I C _ R E A D打开文件的其他尝试将会失败
FILE_SHARE_READ FILE_SHARE_WRITE| 打开文件的其他尝试将会取得成功

如果C r e a t e F i l e函数成功地创建或打开指定的文件,便返回一个文件内核对象的句柄,否则返回I N VA L I D _ H A N D L E _ VA L U E。

注意能够返回句柄的大多数Wi n d o w s函数如果运行失败,那么就会返回N U L L。但是,C r e a t e F i l e函数将返回I N VA L I D _ H A N D L E _ VA L U E,它定义为((H A N D L E)- 1)。

17.3.2 步骤2:创建一个文件映射内核对象

调用C r e a t e F i l e函数,就可以将文件映像的物理存储器的位置告诉操作系统。你传递的路径名用于指明支持文件映像的物理存储器在磁盘(或网络或光盘)上的确切位置。这时,必须告诉系统,文件映射对象需要多少物理存储器。若要进行这项操作,可以调用C r e a t e F i l e M a p p i n g函数:

HANDLE CreateFileMapping(
   HANDLE hFile,
   PSECURITY_ATTRIBUTES psa,
   DWORD fdwProtect,
   DWORD dwMaximumSizeHigh,
   DWORD dwMaximumSizeLow,
   PCTSTR pszName);
第一个参数h F i l e用于标识你想要映射到进程地址空间中的文件句柄。该句柄由前面调用的C r e a t e F i l e函数返回。p s a参数是指向文件映射内核对象的S E C U R I T Y _ AT T R I B U T E S结构的指针,通常传递的值是N U L L(它提供默认的安全特性,返回的句柄是不能继承的)。

本章开头讲过,创建内存映射文件就像保留一个地址空间区域然后将物理存储器提交给该区域一样。因为内存映射文件的物理存储器来自磁盘上的一个文件,而不是来自从系统的页文件中分配的空间。当创建一个文件映射对象时,系统并不为它保留地址空间区域,也不将文件的存储器映射到该区域(下一节将介绍如何进行这项操作)。但是,当系统将存储器映射到进程的地址空间中去时,系统必须知道应该将什么保护属性赋予物理存储器的页面。C r e a t e F i l e M a p p i n g函数的f d w P r o t e c t参数使你能够设定这些保护属性。大多数情况下,可以设定表1 7 - 5中列出的3个保护属性之一。

表17-5 使用fdwProtect 参数设定的部分保护属性

保护属性 含义
PA G E _ R E A D O N LY 当文件映射对象被映射时,可以读取文件的数据。必须已经将G E N E R I C _ R E A D传递给C r e a t e F i l e函数
PA G E _ R E A D W R I T E 当文件映射对象被映射时,可以读取和写入文件的数据。必须已经将GENERIC_READ | GENERIC_WRITE传递给C r e a t e F i l e
PA G E _ W R I T E C O P Y 当文件映射对象被映射时,可以读取和写入文件的数据。如果写入数据,会导致页面的私有拷贝得以创建。必须已经将G E N E R I C _ R E A D或G E N E R I C _ W R I T E传递给C r e a t e F i l e

在Windows 98下,可以将PA G E _ W R I T E C O P Y标志传递给C r e a t e F i l eM a p p i n g,这将告诉系统从页文件中提交存储器。该页文件存储器是为数据文件的数据拷贝保留的,只有修改过的页面才被写入页文件。你对该文件的数据所作的任何修改都不会重新填入原始数据文件。其最终结果是, PA G E _ W R I T E C O P Y标志的作用在Windows 2000和Windows 98上是相同的。

除了上面的页面保护属性外,还有4个节保护属性,你可以用O R将它们连接起来放入C r e a t e F i l e M a p p i n g函数的f d w P r o t e c t参数中。节只是用于内存映射的另一个术语。

节的第一个保护属性是S E C _ N O C A C H E,它告诉系统,没有将文件的任何内存映射页面放入高速缓存。因此,当将数据写入该文件时,系统将更加经常地更新磁盘上的文件数据。这个标志与PA G E _ N O C A C H E保护属性标志一样,是供设备驱动程序开发人员使用的,应用程序通常不使用。

Windows 98将忽略S E C _ N O C A C H E标志。

节的第二个保护属性是S E C _ I M A G E,它告诉系统,你映射的文件是个可移植的可执行(P E)文件映像。当系统将该文件映射到你的进程的地址空间中时,系统要查看文件的内容,以确定将哪些保护属性赋予文件映像的各个页面。例如, P E文件的代码节( . t e x t)通常用PA G E _ E X E C U T E _ R E A D 属性进行映射, 而P E 文件的数据节( . d a t a ) 则通常用PA G E _ R E A D W R I T E属性进行映射。如果设定的属性是S E C _ I M A G E,则告诉系统进行文件映像的映射,并设置相应的页面保护属性。

Windows 98将忽略S E C _ I M A G E标志。

最后两个保护属性是S E C _ R E S E RV E和S E C _ C O M M I T,它们是两个互斥属性,当使用内存映射数据文件时,它们不能使用。这两个标志将在本章后面介绍。当创建内存映射数据文件时,不应该设定这些标志中的任何一个标志。C r e a t e F i l e M a p p i n g将忽略这些标志。

C r e a t e F i l e M a p p i n g的另外两个参数是d w M a x i m u m S i z e H i g h和d w M a x i m u m S i z e L o w,它们是两个最重要的参数。C r e a t e F i l e M a p p i n g函数的主要作用是保证文件映射对象能够得到足够的物理存储器。这两个参数将告诉系统该文件的最大字节数。它需要两个3 2位的值,因为Wi n d o w s支持的文件大小可以用6 4位的值来表示。d w M a x i m u m S i z e H i g h参数用于设定较高的3 2位,而d w M a x i m u m S i z e L o w参数则用于设定较低的3 2位值。对于4 GB 或小于4 GB的文件来说,d w M a x i m u m S i z e H i g h的值将始终是0。

使用6 4位的值,意味着Wi n d o w s能够处理最大为1 6 E B(1 01 8字节)的文件。如果想要创建一个文件映射对象,使它能够反映文件当前的大小,那么可以为上面两个参数传递0。如果只打算读取该文件或者访问文件而不改变它的大小,那么为这两个参数传递0。如果打算将数据附加给该文件,可以选择最大的文件大小,以便为你留出一些富裕的空间。如果当前磁盘上的文件包含0字节,那么可以给C r e a t e F i l e M a p p i n g函数的d w M a x i m u m S i z e H i g h和d w M a x i m u mS i z e L o w传递两个0。这样做就可以告诉系统,你要的文件映射对象里面的存储器为0字节。这是个错误,C r e a t e F i l e M a p p i n g将返回N U L L。

如果你对我们讲述的内容一直非常关注,你一定认为这里存在严重的问题。Wi n d o w s支持最大为1 6 E B的文件和文件映射对象,这当然很好,但是,怎样将这样大的文件映射到3 2位进程的地址空间( 3 2位地址空间是4 G B文件的上限)中去呢?下一节介绍解决这个问题的办法。当然,6 4位进程拥有16 EB的地址空间,因此可以进行更大的文件的映射操作,但是,如果文件是个超大规模的文件,仍然会遇到类似的问题。

若要真正理解C r e a t e F i l e和C r e a t e F i l e M a p p i n g两个函数是如何运行的,建议你做一个下面的实验。建立下面的代码,对它进行编译,然后在一个调试程序中运行它。当你一步步执行每个语句时,你会跳到一个命令解释程序,并执行C:\目录上的“d i r”命令。当执行调试程序中的每个语句时,请注意目录中出现的变化。

int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE,
   PTSTR pszCmdLine, int nCmdShow)
{
   //Before executing the line below, C:\ does not have
   //a file called "MMFTest.Dat."
   HANDLE hfile = CreateFile("C:\\MMFTest.dat", 
      GENERIC_READ | GENERIC_WRITE,
      FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS,
      FILE_ATTRIBUTE_NORMAL, NULL);

   //Before executing the line below, the MMFTest.Dat
   //file does exist but has a file size of 0 bytes.
   HANDLE hfilemap = CreateFileMapping(hfile, NULL, PAGE_READWRITE,
      0, 100, NULL);

   //After executing the line above, the MMFTest.Dat
   //file has a size of 100 bytes.

   //Cleanup
   CloseHandle(hfilemap);
   CloseHandle(hfile);

   //When the process terminates, MMFTest.Dat remains
   //on the disk with a size of 100 bytes.
   return(0);
}
如果调用C r e a t e F i l e M a p p i n g函数,传递PA G E _ R E A D W R I T E标志,那么系统将设法确保磁盘上的相关数据文件的大小至少与d w M a x i m u m S i z e H i g h和d w M a x i m u m S i z e L o w参数中设定的大小相同。如果该文件小于设定的大小, C r e a t e F i l e M a p p i n g函数将扩展该文件的大小,使磁盘上的文件变大。这种扩展是必要的,这样,当以后将该文件作为内存映射文件使用时,物理存储器就已经存在了。如果正在用PA G E _ R E A D O N LY或PA G E _ W R I T E C O P Y标志创建该文件映射对象,那么C r e a t e F i l e M a p p i n g特定的文件大小不得大于磁盘文件的物理大小。这是因为你无法将任何数据附加给该文件。

C r e a t e F i l e M a p p i n g函数的最后一个参数是p s z N a m e。它是个以0结尾的字符串,用于给该文件映射对象赋予一个名字。该名字用于与其他进程共享文件映射对象(本章后面展示了它的一个例子。第3章详细介绍了内核对象的共享操作)。内存映射数据文件通常并不需要被共享,因此这个参数通常是N U L L。

系统创建文件映射对象,并将用于标识该对象的句柄返回该调用线程。如果系统无法创建文件映射对象,便返回一个N U L L句柄值。记住,当C r e a t e F i l e运行失败时,它将返回I N VA L I D _H A N D L E _ VA L U E(定义为-1),当C r e a t e F i l e M a p p i n g运行失败时,它返回N U L L。请不要混淆这些错误值。

17.3.3 步骤3:将文件数据映射到进程的地址空间

当创建了一个文件映射对象后,仍然必须让系统为文件的数据保留一个地址空间区域,并将文件的数据作为映射到该区域的物理存储器进行提交。可以通过调用M a p Vi e w O f F i l e函数来进行这项操作:

PVOID MapViewOfFile(
   HANDLE hFileMappingObject,
   DWORD dwDesiredAccess,
   DWORD dwFileOffsetHigh,
   DWORD dwFileOffsetLow,
   SIZE_T dwNumberOfBytesToMap);
参数h F i l e M a p p i n g O b j e c t用于标识文件映射对象的句柄,该句柄是前面调用CreateFile Mapping或O p e n F i l e M a p p i n g(本章后面介绍)函数返回的。参数d w D e s i r e d A c c e s s用于标识如何访问该数据。不错,必须再次设定如何访问文件的数据。可以设定表1 7 - 6所列的4个值中的一个。

表17-6 值及其含义

含义
F I L E _ M A P _ W R I T E 可以读取和写入文件数据。C r e a t e F i l e M a p p i n g函数必须通过传递PA G E _ R E A D W R I T E标志来调用
F I L E _ M A P _ R E A D 可以读取文件数据。C r e a t e F i l e M a p p i n g函数可以通过传递下列任何一个保护属性来调用:PA G E _ R E A D O N LY、PA G E _ R E A D W R I T E或PA G E _ W R I T E C O P Y
F I L E _ M A P _ A L L _ A C C E S S 与F I L E _ M A P _ W R I T E相同
F I L E _ M A P _ C O P Y 可以读取和写入文件数据。如果写入文件数据,可以创建一个页面的私有拷贝。在Windows 2000中,C r e a t e F i l e M a p p i n g函数可以用PA G E _ R E A D O N LY、PA G E _ R E A D W R I T E或PA G E _ W R I T E C O P Y等保护属性中的任何一个来调用。在Windows 98中,C r e a t e F i l e M a p p i n g必须用PA G E _ W R I T E C O P Y来调用

Wi n d o w s要求所有这些保护属性一次又一次地重复设置,这当然有些奇怪和烦人。我认为这样做可以使应用程序更多地对数据保护属性进行控制。

剩下的3个参数与保留地址空间区域及将物理存储器映射到该区域有关。当你将一个文件映射到你的进程的地址空间中时,你不必一次性地映射整个文件。相反,可以只将文件的一小部分映射到地址空间。被映射到进程的地址空间的这部分文件称为一个视图,这可以说明M a p Vi e w O f F i l e是如何而得名的。

当将一个文件视图映射到进程的地址空间中时,必须规定两件事情。首先,必须告诉系统,数据文件中的哪个字节应该作为视图中的第一个字节来映射。你可以使用d w F i l e O ff s e t H i g h和d w F i l e O ff s e t L o w参数来进行这项操作。由于Wi n d o w s支持的文件最大可达1 6 E B,因此必须用一个6 4位的值来设定这个字节的位移值。这个6 4位值中,较高的3 2位传递给参数d w F i l e O ff s e t H i g h,较低的3 2位传递给参数d w F i l e O ff s e t L o w。注意,文件中的这个位移值必须是系统的分配粒度的倍数(迄今为止,Wi n d o w s的所有实现代码的分配粒度均为64 KB)。第1 4章介绍了如何获取某个系统的分配粒度。

第二,必须告诉系统,数据文件有多少字节要映射到地址空间。这与设定要保留多大的地址空间区域的情况是相同的。可以使用d w N u m b e r O f B y t e s To M a p参数来设定这个值。如果设定的值是0,那么系统将设法把从文件中的指定位移开始到整个文件的结尾的视图映射到地址空间。

在Windows 98中,如果M a p Vi e w O f F i l e无法找到足够大的区域来存放整个文件映射对象,那么无论需要的视图是多大, M a p Vi e w O f F i l e均将返回N U L L。

在Windows 2000中,M a p Vi e w O f F i l e只需要为必要的视图找到足够大的一个区域,而不管整个文件映射对象是多大。

如果在调用M a p Vi e w O f F i l e函数时设定了F I L E _ M A P _ C O P Y标志,系统就会从系统的页文件中提交物理存储器。提交的地址空间数量由d w N u m b e r O f B y t e s To M a p参数决定。只要你不进行其他操作,只是从文件的映像视图中读取数据,那么系统将决不会使用页文件中的这些提交的页面。但是,如果进程中的任何线程将数据写入文件的映像视图中的任何内存地址,那么系统将从页文件中抓取已提交页面中的一个页面,将原始数据页面拷贝到该页交换文件中,然后将该拷贝的页面映射到你的进程的地址空间。从这时起,你的进程中的线程就要访问数据的本地拷贝,不能读取或修改原始数据。

当系统制作原始页面的拷贝时,系统将把页面的保护属性从PA G E _ W R I T E C O P Y改为PA G E _ R E A D W R I T E。下面这个代码段就说明了这个情况:

// Open the file that we want to map.
HANDLE hFile = CreateFile(pszFileName, GENERIC_READ | GENERIC_WRITE, 0, NULL,
   OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

// Create a file-mapping object for the file.
HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_WRITECOPY,
   0, 0, NULL);

// Map a copy-on-write view of the file; the system will commit
// enough physical storage from the paging file to accommodate
// the entire file. All pages in the view will initially have
// PAGE_WRITECOPY access.

PBYTE pbFile = (PBYTE) MapViewOfFile(hFileMapping, FILE_MAP_COPY,
   0, 0, 0);

// Read a byte from the mapped view.
BYTE bSomeByte = pbFile[0];

// When reading, the system does not touch the committed pages in
// the paging file. The page keeps its PAGE_WRITECOPY attribute.

// Write a byte to the mapped view.
pbFile[0] = 0;

// When writing for the first time, the system grabs a committed
// page from the paging file, copies the original contents of the
// page at the accessed memory address, and maps the new page
// (the copy) into the process's address space. The new page has
// an attribute of PAGE_READWRITE.

// Write another byte to the mapped view.
pbFile[1] = 0;

// Because this byte is now in a PAGE_READWRITE page, the system
// simply writes the byte to the page (backed by the paging file).

// When finished using the file's mapped view, unmap it.
// UnmapViewOfFile is discussed in the next section.
UnmapViewOfFile(pbFile);

// The system decommits the physical storage from the paging file.
// Any writes to the pages are lost.

// Clean up after ourselves.
CloseHandle(hFileMapping);
CloseHandle(hFile);
Windows 98 前面讲过,Windows 98必须预先为内存映射文件提交页文件中的存储器。然而,它只有在必要时才将修改后的页面写入页文件。

17.3.4 步骤4:从进程的地址空间中撤消文件数据的映像

当不再需要保留映射到你的进程地址空间区域中的文件数据时,可以通过调用下面的函数将它释放:

BOOL UnmapViewOfFile(PVOID pvBaseAddress);
该函数的唯一的参数p v B a s e A d d r e s s用于设定返回区域的基地址。该值必须与调用M a p Vi e w O f F i l e函数返回的值相同。必须记住要调用U n m a p Vi e w O f F i l e函数。如果没有调用这个函数,那么在你的进程终止运行前,保留的区域就不会被释放。每当你调用M a p Vi e w O f F i l e时,系统总是在你的进程地址空间中保留一个新区域,而以前保留的所有区域将不被释放。

为了提高速度,系统将文件的数据页面进行高速缓存,并且在对文件的映射视图进行操作时不立即更新文件的磁盘映像。如果需要确保你的更新被写入磁盘,可以强制系统将修改过的数据的一部分或全部重新写入磁盘映像中,方法是调用F l u s h Vi e w O f F i l e函数:

BOOL FlushViewOfFile(
   PVOID pvAddress,
   SIZE_T dwNumberOfBytesToFlush);
第一个参数是包含在内存映射文件中的视图的一个字节的地址。该函数将你在这里传递的地址圆整为一个页面边界值。第二个参数用于指明你想要刷新的字节数。系统将把这个数字向上圆整,使得字节总数是页面的整数。如果你调用F l u s h Vi e w O f F i l e函数并且不修改任何数据,那么该函数只是返回,而不将任何信息写入磁盘。

对于存储器是在网络上的内存映射文件来说, F l u s h Vi e w O f F i l e能够保证文件的数据已经从工作站写入存储器。但是F l u s h Vi e w O f F i l e不能保证正在共享文件的服务器已经将数据写入远程磁盘,因为服务器也许对文件的数据进行了高速缓存。若要保证服务器写入文件的数据,每当你为文件创建一个文件映射对象并且映射该文件映射对象的视图时,应该将F I L E _ F L A G _W R I T E _ T H R O U G H标志传递给C r e a t e F i l e函数。如果你使用该标志打开该文件,那么只有当文件的全部数据已经存放在服务器的磁盘驱动器中的时候, F l u s h Vi e w O f F i l e函数才返回。

记住U n m a p Vi e w O f F i l e函数的一个特殊的特性。如果原先使用F I L E _ M A P _ C O P Y标志来映射视图,那么你对文件的数据所作的任何修改,实际上是对存放在系统的页文件中的文件数据的拷贝所作的修改。在这种情况下,如果调用U n m a p Vi e w O f F i l e函数,该函数在磁盘文件上就没有什么可以更新,而只会释放页文件中的页面,从而导致数据丢失。

如果想保留修改后的数据,必须采用别的措施。例如,你可以用同一个文件创建另一个文件映射对象(使用PA G E _ R E A D W R I T E),然后使用F I L E _ M A P _ W R I T E标志将这个新文件映射对象映射到进程的地址空间。之后,你可以扫描第一个视图,寻找带有PA G E _ R E A D W R I T E保护属性的页面。每当你找到一个带有该属性的页面时,可以查看它的内容,并且确定是否将修改了的数据写入该文件。如果不想用新数据更新该文件,那么继续对视图中的剩余页面进行扫描,直到视图的结尾。但是,如果你确实想要保存修改了的数据页面,那么只需要调用M o v e M e m o r y函数,将数据页面从第一个视图拷贝到第二个视图。由于第二个视图是用PA G E _ R E A D W R I T E保护属性映射的,因此M o v e M e m o r y函数将更新磁盘上的实际文件内容。可以使用这种方法来确定文件的变更并保存你的文件的数据。

Windows 98不支持c o p y - o n - w r i t e(写入时拷贝)保护属性,因此,当扫描内存映射文件的第一个视图时,无法测试用PA G E _ R E A D W R I T E标志做上标记的页面。你必须设计一种方法来确定第一个视图中的哪些页面已经做了修改。

17.3.5 步骤5和步骤6:关闭文件映射对象和文件对象

不用说,你总是要关闭你打开了的内核对象。如果忘记关闭,在你的进程继续运行时会出现资源泄漏的问题。当然,当你的进程终止运行时,系统会自动关闭你的进程已经打开但是忘记关闭的任何对象。但是如果你的进程暂时没有终止运行,你将会积累许多资源句柄。因此你始终都应该编写清楚而又“正确的”代码,以便关闭你已经打开的任何对象。若要关闭文件映射对象和文件对象,只需要两次调用C l o s e H a n d l e函数,每个句柄调用一次:

让我们更加仔细地观察一下这个进程。下面的伪代码显示了一个内存映射文件的例子:

HANDLE hFile = CreateFile(...);
HANDLE hFileMapping = CreateFileMapping(hFile, ...);
PVOID pvFile = MapViewOfFile(hFileMapping, ...);

// Use the memory-mapped file.

UnmapViewOfFile(pvFile);
CloseHandle(hFileMapping);
CloseHandle(hFile);
上面的代码显示了对内存映射文件进行操作所用的“预期”方法。但是,它没有显示,当你调用M a p Vi e w O f F i l e时系统对文件对象和文件映射对象的使用计数的递增情况。这个副作用是很大的,因为它意味着我们可以将上面的代码段重新编写成下面的样子:

HANDLE hFile = CreateFile(...);
HANDLE hFileMapping = CreateFileMapping(hFile, ...);
CloseHandle(hFile);
PVOID pvFile = MapViewOfFile(hFileMapping, ...);
CloseHandle(hFileMapping);

// Use the memory-mapped file.

UnmapViewOfFile(pvFile);
当对内存映射文件进行操作时,通常要打开文件,创建文件映射对象,然后使用文件映射对象将文件的数据视图映射到进程的地址空间。由于系统递增了文件对象和文件映射对象的内部使用计数,因此可以在你的代码开始运行时关闭这些对象,以消除资源泄漏的可能性。

如果用同一个文件来创建更多的文件映射对象,或者映射同一个文件映射对象的多个视图,那么就不能较早地调用C l o s e H a n d l e函数——以后你可能还需要使用它们的句柄,以便分别对C r e a t e F i l e M a p p i n g和M a p Vi e w O f F i l e函数进行更多的调用。

17.3.6 文件倒序示例应用程序

清单1 7 - 2列出的F i l e R e v应用程序(“1 7F i l e R e v. e x e”)显示了如何使用内存映射对象来对A N S I或U n i c o d e文本文件的内容进行倒序。该应用程序的源代码和资源文件位于本书所附光盘上的1 7 - F i l e R e v目录下。当启动该程序时,会出现图1 7 - 7所示的窗口。


图17-7 运行FileRev 时出现的窗口

F i l e R e v应用程序首先允许选定一个文件,然后,当单击Reverse File Contents(对文件内容进行倒序)按钮时,该函数就会将文件中使用的字符进行倒序。该程序只能对文本文件进行正确的倒序,对二进制文件不能正确地进行倒序操作。F i l e R e v能够确定文本文件是A N S I文件还是U n i c o d e文件,方法是调用I s Te x t U n i c o d e函数(第2章中介绍)。

在Windows 98中,I s Te x t U n i c o d e函数没有可以使用的实现代码,它只是返回FALSE。如果调用GetLastError函数,则返回ERROR_CALL_NOT_ IMPLEMENTED。这意味着F i l e R e v示例应用程序总是认为,当它在Windows 98下运行时,它操作的是A N S I文本文件。

当单击Reverse File Contents按钮时,F i l e R e v便制作指定文件的一个拷贝,称为F i l e R e v. d a t。它制作该拷贝的目的是,原始文件不会因为内容被倒序而变得无法使用。接着, F i l e R e v调用F i l e R e v e r s e函数,该函数负责对文件进行倒序操作。F i l e R e v e r s e则调用C r e a t e F i l e函数,打开F i l e R e v. d a t ,以便进行读取和写入。

前面说过,对文件内容进行倒序的最容易的方法是调用C运行期函数_ s t r r e v。与所有C字符串一样,字符串的最后一个字符必须是个0结束符。由于文本文件不以0为结束符,因此F i l e R e v必须给文件附加一个0。若要进行这样的附加操作,首先要调用G e t F i l e S i z e函数:

dwFileSize = GetFileSize(hFile, NULL);
现在已经得到了文件的长度,你能够通过调用C r e a t e F i l e M a p p i n g函数创建文件映射对象。创建的文件映射对象的长度是d w F i l e S i z e加一个宽字符的大小(对于0字符来说)。当文件映射对象创建后,该对象的视图就被映射到F i l e R e v的地址空间。变量p v F i l e包含了M a p Vi e w O f F i l e函数的返回值,并指向文本文件的第一个字节。

下一步是在文件的结尾处写一个0,并对字符串进行倒序:

PSTR pchANSI = (PSTR) pvFile;
pchANSI[dwFileSize / sizeof(CHAR)] = 0;
在文本文件中,每一行的结尾都是一个回车符(‘\ r’)后随一个换行符(‘\ n’)。但是,当调用_ s t r r e v对文件进行倒序时,这些字符也会被倒序。如果将已经倒序的文本文件加载到文本编辑器,那么出现的每一对“ \ n \ r”字符都必须重新改为它的原始顺序。这个倒序操作是由下面的循环代码进行的:

while(pchANSI != NULL) 
{
   //We have found an occurrence....
   *pchANSI++ = '\r';     //Change '\n' to '\r'.
   *pchANSI++ = '\n';     //Change '\r' to '\n'.
   pchANSI = strchr(pchANSI, '\n'); //Find the next occurrence.
}
当你观察这样一个简单的代码时,可能很容易忘记你实际上是在对磁盘驱动器上的文件内容进行操作(这显示出内存映射文件的功能是多么大)

在文件被倒序后, F i l e R e v便进行清除操作,撤消文件映射对象的视图映象,关闭所有的内核对象句柄。此外,F i l e R e v必须删除附加给文件结尾处的0字符(记住_ s t r r e v并不对结尾的0字符进行倒序)。如果没有删除0字符,那么倒序的文件将会多出一个字符,如果再次调用F i l e R e v函数,将无法使文件还原成它的原始样子。若要删除文件结尾处的0字符,必须后退一步,使用文件管理函数,而不是通过内存映射对文件进行操作。

如果要强制已经倒序的文件在某个位置上结束,就需要将文件指针定位在指定的位置(原始文件的结尾处)并调用S e t E n d O f F i l e函数:

SetFilePointer(hFile, dwFileSize, NULL, FILE_BEGIN);
SetEndOfFile(hFile);
注意S e t E n d O f F i l e函数必须在撤消视图的映象并且关闭文件映射对象之后调用,否 则,该函数将返回FA L S E,G e t L a s t E r r o r则返回E R R O R _ U S E R _ M A P P E D _ F I L E。这个 错误表示不能在与文件映射对象相关联的文件上执行文件未尾的操作。

F i l e R e v函数做的最后一件事情是产生一个N o t e p a d实例,这样,就可以查看已经倒序的文件。图1 7 - 8显示了在F i l e R e v. c p p文件上运行F i l e R e v函数时产生的结果。


图17-8 FileRev 函数运行结果

清单17-2 FileRev示例应用程序

/******************************************************************************
Module:  FileRev.cpp
Notices: Copyright (c) 2000 Jeffrey Richter
******************************************************************************/


#include "..\CmnHdr.h"     /* See Appendix A. */
#include <windowsx.h>
#include <tchar.h>
#include <commdlg.h>
#include <string.h>       // For _strrev
#include "Resource.h"


///////////////////////////////////////////////////////////////////////////////


#define FILENAME  TEXT("FILEREV.DAT")


///////////////////////////////////////////////////////////////////////////////


BOOL FileReverse(PCTSTR pszPathname, PBOOL pfIsTextUnicode) {

   *pfIsTextUnicode = FALSE;  // Assume text is Unicode

   // Open the file for reading and writing.
   HANDLE hFile = CreateFile(pszPathname, GENERIC_WRITE | GENERIC_READ, 0, 
      NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

   if (hFile == INVALID_HANDLE_VALUE) {
      chMB("File could not be opened.");
      return(FALSE);
   }

   // Get the size of the file (I assume the whole file can be mapped).
   DWORD dwFileSize = GetFileSize(hFile, NULL);

   // Create the file-mapping object. The file-mapping object is 1 character 
   // bigger than the file size so that a zero character can be placed at the 
   // end of the file to terminate the string (file). Because I don't yet know
   // if the file contains ANSI or Unicode characters, I assume worst case
   // and add the size of a WCHAR instead of CHAR.
   HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 
      0, dwFileSize + sizeof(WCHAR), NULL);

   if (hFileMap == NULL) {
      chMB("File map could not be opened.");
      CloseHandle(hFile);
      return(FALSE);
   }

   // Get the address where the first byte of the file is mapped into memory.
   PVOID pvFile = MapViewOfFile(hFileMap, FILE_MAP_WRITE, 0, 0, 0);

   if (pvFile == NULL) {
      chMB("Could not map view of file.");
      CloseHandle(hFileMap);
      CloseHandle(hFile);
      return(FALSE);
   }

   // Does the buffer contain ANSI or Unicode?
   int iUnicodeTestFlags = -1;   // Try all tests
   *pfIsTextUnicode = IsTextUnicode(pvFile, dwFileSize, &iUnicodeTestFlags);

   if (!*pfIsTextUnicode) {
      // For all the file manipulations below, we explicitly use ANSI 
      // functions because we are processing an ANSI file.

      // Put a zero character at the very end of the file.
      PSTR pchANSI = (PSTR) pvFile;
      pchANSI[dwFileSize / sizeof(CHAR)] = 0;

      // Reverse the contents of the file.
      _strrev(pchANSI);

      // Convert all "\n\r" combinations back to "\r\n" to 
      // preserve the normal end-of-line sequence.
      pchANSI = strchr(pchANSI, '\n'); // Find first '\n'.

      while (pchANSI != NULL) {
         // We have found an occurrence....
         *pchANSI++ = '\r';   // Change '\n' to '\r'.
         *pchANSI++ = '\n';   // Change '\r' to '\n'.
         pchANSI = strchr(pchANSI, '\n'); // Find the next occurrence.
      }

   } else {
      // For all the file manipulations below, we explicitly use Unicode
      // functions because we are processing a Unicode file.

      // Put a zero character at the very end of the file.
      PWSTR pchUnicode = (PWSTR) pvFile;
      pchUnicode[dwFileSize / sizeof(WCHAR)] = 0;

      if ((iUnicodeTestFlags & IS_TEXT_UNICODE_SIGNATURE) != 0) {
         // If the first character is the Unicode BOM (byte-order-mark), 
         // 0xFEFF, keep this character at the beginning of the file.
         pchUnicode++;
      }

      // Reverse the contents of the file.
      _wcsrev(pchUnicode);

      // Convert all "\n\r" combinations back to "\r\n" to 
      // preserve the normal end-of-line sequence.
      pchUnicode = wcschr(pchUnicode, L'\n'); // Find first '\n'.

      while (pchUnicode != NULL) {
         // We have found an occurrence....
         *pchUnicode++ = L'\r';   // Change '\n' to '\r'.
         *pchUnicode++ = L'\n';   // Change '\r' to '\n'.
         pchUnicode = wcschr(pchUnicode, L'\n'); // Find the next occurrence.
      }
   }

   // Clean up everything before exiting.
   UnmapViewOfFile(pvFile);
   CloseHandle(hFileMap);

   // Remove trailing zero character added earlier.
   SetFilePointer(hFile, dwFileSize, NULL, FILE_BEGIN);
   SetEndOfFile(hFile);
   CloseHandle(hFile);

   return(TRUE);
}


///////////////////////////////////////////////////////////////////////////////


BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {

   chSETDLGICONS(hwnd, IDI_FILEREV);

   // Initialize the dialog box by disabling the Reverse button
   EnableWindow(GetDlgItem(hwnd, IDC_REVERSE), FALSE);
   return(TRUE);
}



///////////////////////////////////////////////////////////////////////////////


void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {
   
   TCHAR szPathname[MAX_PATH];

   switch (id) {
      case IDCANCEL:
         EndDialog(hwnd, id);
         break;

      case IDC_FILENAME:
         EnableWindow(GetDlgItem(hwnd, IDC_REVERSE), 
            Edit_GetTextLength(hwndCtl) > 0);
         break;

      case IDC_REVERSE:
         GetDlgItemText(hwnd, IDC_FILENAME, szPathname, chDIMOF(szPathname));

         // Make copy of input file so that we don't destroy it
         if (!CopyFile(szPathname, FILENAME, FALSE)) {
            chMB("New file could not be created.");
            break;
         }

         BOOL fIsTextUnicode;
         if (FileReverse(FILENAME, &fIsTextUnicode)) {
            SetDlgItemText(hwnd, IDC_TEXTTYPE, 
               fIsTextUnicode ? TEXT("Unicode") : TEXT("ANSI"));

            // Spawn Notepad to see the fruits of our labors.
            STARTUPINFO si = { sizeof(si) };
            PROCESS_INFORMATION pi;
            TCHAR sz[] = TEXT("Notepad ") FILENAME;
            if (CreateProcess(NULL, sz,
               NULL, NULL, FALSE, 0, NULL, NULL, &si, π)) {

               CloseHandle(pi.hThread);
               CloseHandle(pi.hProcess);
            }
         }
         break;

      case IDC_FILESELECT:
         OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };
         ofn.hwndOwner = hwnd;
         ofn.lpstrFile = szPathname;
         ofn.lpstrFile[0] = 0;
         ofn.nMaxFile = chDIMOF(szPathname);
         ofn.lpstrTitle = TEXT("Select file for reversing");
         ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST;
         GetOpenFileName(&ofn);
         SetDlgItemText(hwnd, IDC_FILENAME, ofn.lpstrFile);
         SetFocus(GetDlgItem(hwnd, IDC_REVERSE));
         break;
   }
}


///////////////////////////////////////////////////////////////////////////////


INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
   
   switch (uMsg) {
      chHANDLE_DLGMSG(hwnd, WM_INITDIALOG,  Dlg_OnInitDialog);
      chHANDLE_DLGMSG(hwnd, WM_COMMAND,     Dlg_OnCommand);
   }
   return(FALSE);
}


///////////////////////////////////////////////////////////////////////////////


int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) {

   DialogBox(hinstExe, MAKEINTRESOURCE(IDD_FILEREV), NULL, Dlg_Proc);
   return(0);
}


//////////////////////////////// End of File //////////////////////////////////
//Microsoft Developer Studio generated resource script.
//
#include "Resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// English (U.S.) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32

/////////////////////////////////////////////////////////////////////////////
//
// Icon
//

// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_FILEREV             ICON    DISCARDABLE     "FileRev.Ico"

#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE 
BEGIN
    "Resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE 
BEGIN
    "#include ""afxres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE DISCARDABLE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

IDD_FILEREV DIALOG DISCARDABLE  15, 24, 216, 46
STYLE WS_MINIMIZEBOX | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "File Reverse"
FONT 8, "MS Sans Serif"
BEGIN
    LTEXT           "&Pathname:",IDC_STATIC,4,4,35,8
    EDITTEXT        IDC_FILENAME,44,4,168,12,ES_AUTOHSCROLL
    PUSHBUTTON      "&Browse...",IDC_FILESELECT,4,16,36,12,WS_GROUP
    DEFPUSHBUTTON   "&Reverse file contents",IDC_REVERSE,4,32,80,12
    LTEXT           "Type of characters in file:",IDC_STATIC,88,34,80,8
    LTEXT           "(unknown)",IDC_TEXTTYPE,172,34,34,8
END


/////////////////////////////////////////////////////////////////////////////
//
// DESIGNINFO
//

#ifdef APSTUDIO_INVOKED
GUIDELINES DESIGNINFO DISCARDABLE 
BEGIN
    IDD_FILEREV, DIALOG
    BEGIN
        RIGHTMARGIN, 192
        BOTTOMMARGIN, 42
    END
END
#endif    // APSTUDIO_INVOKED

#endif    // English (U.S.) resources
/////////////////////////////////////////////////////////////////////////////



#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//


/////////////////////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED


17.4 使用内存映射文件来处理大文件

上一节讲过我要告诉你如何将一个16 EB的文件映射到一个较小的地址空间中。当然,你是无法做到这一点的。你必须映射一个只包含一小部分文件数据的文件视图。首先映射一个文件的开头的视图。当完成对文件的第一个视图的访问时,可以取消它的映像,然后映射一个从文件中的一个更深的位移开始的新视图。必须重复这一操作,直到访问了整个文件。这使得大型内存映射文件的处理不太方便,但是,幸好大多数文件都比较小,因此不会出现这个问题。

让我们看一个例子,它使用一个8 GB的文件和一个3 2位的地址空间。下面是一个例程,它使用若干个步骤来计算一个二进制数据文件中的所有0字节的数目:

__int64 Count0s(void)
{
   //Views must always start on a multiple
   //of the allocation granularity
   SYSTEM_INFO sinf;
   GetSystemInfo(&sinf);

   //Open the data file.
   HANDLE hFile = CreateFile("C:\\HugeFile.Big", GENERIC_READ,
      FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);

   //Create the file-mapping object.
   HANDLE hFileMapping = CreateFileMapping(hFile, NULL, 
      PAGE_READONLY, 0, 0, NULL);

   DWORD dwFileSizeHigh;
   __int64 qwFileSize = GetFileSize(hFile, &dwFileSizeHigh);
   qwFileSize += (((__int64) dwFileSizeHigh) << 32);

   //We no longer need access to the file object's handle.
   CloseHandle(hFile);

   __int64 qwFileOffset = 0, qwNumOf0s = 0;

   while (qwFileSize > 0) 
   {
      // Determine the number of bytes to be mapped in this view
      DWORD dwBytesInBlock = sinf.dwAllocationGranularity;

      if(qwFileSize < sinf.dwAllocationGranularity)
         dwBytesInBlock =(DWORD) qwFileSize;

      PBYTE pbFile = (PBYTE) MapViewOfFile(hFileMapping, FILE_MAP_READ,
         (DWORD)(qwFileOffset >> 32),          // Starting byte
         (DWORD)(qwFileOffset & 0xFFFFFFFF),   // in file
         dwBytesInBlock);                      // # of bytes to map

      // Count the number of Js in this block.
      for(DWORD dwByte = 0; dwByte < dwBytesInBlock; dwByte++)
      {
         if(pbFile[dwByte] == 0)
         qwNumOf0s++;
      }

      // Unmap the view; we don't want multiple views
      // in our address space.
      UnmapViewOfFile(pbFile);

      // Skip to the next set of bytes in the file.
      qwFileOffset += dwBytesInBlock;
      qwFileSize -= dwBytesInBlock;
   }

   CloseHandle(hFileMapping);
   return(qwNumOf0s);
}
这个算法用于映射64 KB(分配粒度的大小)或更小的视图。另外,要记住, M a p Vi e wO f F i l e函数要求文件的位移是分配粒度大小的倍数。当每个视图被映射到地址空间时,对0的扫描不断进行。当每个64 KB的文件块已经映射和扫描完毕时,就要通过关闭文件映射对象来对每个文件块进行整理。


17.5 内存映射文件与数据视图的相关性

系统允许你映射一个文件的相同数据的多个视图。例如,你可以将文件开头的10 KB映射到一个视图,然后将同一个文件的头4 KB映射到另一个视图。只要你是映射相同的文件映射对象,系统就会确保映射的视图数据的相关性。例如,如果你的应用程序改变了一个视图中的文件内容,那么所有其他视图均被更新以反映这个变化。这是因为尽管页面多次被映射到进程的虚拟地址空间,但是系统只将数据放在单个R A M页面上。如果多个进程映射单个数据文件的视图,那么数据仍然是相关的,因为在数据文件中,每个R A M页面只有一个实例——正是这个R A M页面被映射到多个进程的地址空间。

注意Wi n d o w s允许创建若干个由单个数据文件支持的文件映射对象。Wi n d o w s不能保证这些不同的文件映射对象的视图具有相关性。它只能保证单个文件映射对象的多个视图具有相关性。

然而,当对文件进行操作时,没有理由使另一个应用程序无法调用C r e a t e F i l e函数以打开由另一个进程映射的同一个文件。这个新进程可以使用R e a d F i l e和Wr i t e F i l e函数来读取该文件的数据和将数据写入该文件。当然,每当一个进程调用这些函数时,它必须从内存缓冲区读取文件数据或者将文件数据写入内存缓冲区。该内存缓冲区必须是进程自己创建的一个缓冲区,而不是映射文件使用的内存缓冲区。当两个应用程序打开同一个文件时,问题就可能产生:一个进程可以调用R e a d F i l e函数来读取文件的一个部分,并修改它的数据,然后使用Wr i t e F i l e函数将数据重新写入文件,而第二个进程的文件映射对象却不知道第一个进程执行的这些操作。由于这个原因,当你为将被内存映射的文件调用C r e a t e F i l e函数时,最好将d w S h a r e M o d e参数的值设置为0。这样就可以告诉系统,你想要单独访问这个文件,而其他进程都不能打开它。

只读文件不存在相关性问题,因此它们可以作为很好的内存映射文件。内存映射文件决不应该用于共享网络上的可写入文件,因为系统无法保证数据视图的相关性。如果某个人的计算机更新了文件的内容,其他内存中含有原始数据的计算机将不知道它的信息已经被修改。


17.6 设定内存映射文件的基地址

正如你可以使用Vi r t u a l A l l o c函数来确定对地址空间进行倒序所用的初始地址一样,你也可以使用M a p Vi e w O f F i l e E x函数而不是使用M a p Vi e w O f F i l e函数来确定一个文件被映射到某个特定的地址。请看下面的代码:

PVOID MapViewOfFileEx(
   HANDLE hFileMappingObject,
   DWORD dwDesiredAccess,
   DWORD dwFileOffsetHigh,
   DWORD dwFileOffsetLow,
   SIZE_T dwNumberOfBytesToMap,
   PVOID pvBaseAddress);
该函数的所有参数和返回值均与M a p Vi e w O f F i l e函数相同,唯一的差别是最后一个参数p v B a s e A d d r e s s有所不同。在这个参数中,你为要映射的文件设定一个目标地址。与Vi r t u a l A l l o c一样,你设定的目标地址应该是分配粒度边界( 64 KB)的倍数,否则M a p Vi e w O f F i l e E x将返回N U L L,表示出现了错误。

在Windows 2000下,如果设定的地址不是分配粒度的倍数,就会导致函数运行失败,同时G e t L a s t E r r o r将返回11 3 2(E R R O R _ M A P P E D _ A L I G N M E N T)。在Windows 98中,该地址将圆整为分配粒度边界值。

如果系统无法将文件映射到该位置上(通常由于文件太大并且与另一个保留的地址空间相重叠),那么该函数的运行就会失败并且返回N U L L。M a p Vi e w O f F i l e E x并不设法寻找另一个地址空间来放置该文件。当然,你可以设定N U L L作为p v B a s e A d d r e s s参数的值,这时,M a p Vi e w O f F i l e E x函数的运行特性与M a p Vi e w O f F i l e函数完全相同。

当你使用内存映射文件与其他进程共享数据时,你可以使用M a p Vi e w O f F i l e E x函数。例如,当两个或多个应用程序需要共享包含指向其他数据结构的一组数据结构时,可能需要在某个特定地址上的内存映射文件。链接表是个极好的例子。在链接表中,每个节点或元素均包含列表中的另一个元素的内存地址。若要遍历该列表,必须知道第一个元素的地址,然后参考包含下一个元素地址的元素成员。当使用内存映射文件时,这可能成为一个问题。

如果一个进程建立了内存映射文件中的链接表,然后与另一个进程共享该文件,那么另一个进程就可能将文件映射到它的地址空间中的一个完全不同的位置上。当第二个进程视图遍历该链接表时,它查看链接表的第一个元素,检索下一个元素的内存地址,然后设法引用下一个元素。然而,第一个节点中的下一个元素的地址并不是第二个进程需要查找的地址。

可以用两种办法来解决这个问题。首先,当第二个进程将包含链接表的内存映射文件映射到它自己的地址空间中去时,它只要调用M a p Vi e w O f F i l e E x函数而不是调用M a p Vi e w O f F i l e。当然,这种方法要求第二个进程必须知道第一个进程原先在建立链接表时将文件映射到了什么地方。当两个应用程序打算互相进行交互操作时(这是非常可能的),这就不会出现任何问题,因为地址可以通过硬编码放入两个应用程序,或者一个进程可以通知另一个进程使用另一种进程间通信的方式,比如将消息发送到窗口。

第二个方法是创建链接表的进程将下一个节点所在的地址中的位移存放在每个节点中。这要求应用程序将该位移添加给内存映射文件的基地址,以便访问每个节点。这种方法并不高明,因为它的运行速度可能比较慢,它会使程序变得更大(因为编译器要生成附加代码来执行所有的计算操作),而且它很容易出错。但是,它仍然是个可行的方法, M i c r o s o f t的编译器为使用_ _ b a s e d关键字的基本指针提供了辅助程序。

Windows 98 当调用M a p Vi e w O f F i l e E x时,必须设定0 x 8 0 0 0 0 0 0 0与0 x B F F F F F F F之间的一个地址,否则M a p Vi e w O f F i l e E x将返回U L L。

Windows 20000 当调用M a p Vi e w O f F i l e E x时,必须设定在你的进程的用户方式分区中的一个地址,否则M a p Vi e w O f F i l e E x将返回N U L L。


17.7 实现内存映射文件的具体方法

Windows 98和Windows 2000实现内存映射文件的方法是不同的。必须知道这些差别,因为它们会影响你编写代码的方法,也会影响其他应用程序对你的数据进行不利的操作。

在Windows 98下,视图总是映射到0 x 8 0 0 0 0 0 0 0至0 x B F F F F F F F范围内的地址空间分区中。因此,对M a p Vi e w O f F i l e函数的成功调用都会返回这个范围内的一个地址。你也许还记得,所有进程都共享该分区中的数据。这意味着如果进程映射了文件映射对象的视图,那么该文件映射对象的数据实际上就可以被所有进程访问,而不管它们是否已经映射了该文件映射对象的视图。如果另一个进程调用使用同一个文件映射对象的M a p Vi e w O f F i l e函数,Windows 98便将返回给第一个进程的同一个内存地址返回给第二个进程。这两个进程访问相同的数据,并且它们的视图具有相关性。

在Windows 98中,一个进程可以调用M a p Vi e w O f F i l e函数,并且可以使用某种进程间的通信方式将返回的内存地址传递给另一个进程的线程。一旦该线程收到这个内存地址,该线程就可以成功地访问文件映射对象的同一个视图。但是,不应该这样做,原因有二。

• 你的应用程序将无法在Windows 2000下运行,其原因将在下面说明。

• 如果第一个进程调用U n m a p Vi e w O f F i l e函数,地址空间区域将恢复为空闲状态,这意味着第二个进程的线程如果尝试访问视图曾经位于其中的内存,会引发一次访问违规。

如果第二个进程访问内存映射对象的视图,那么第二个进程中的线程应该调用M a p Vi e wO f F i l e函数。当第二个进程这样做的时候,系统将对内存映射视图的使用计数进行递增。因此,如果第一个进程调用U n m a p Vi e w O f F i l e函数,那么在第二个进程也调用U n m a p Vi e w O f F i l e之前,系统将不会释放视图占用的地址空间区域。

当第二个进程调用M a p Vi e w O f F i l e函数时,返回的地址将与第一个进程返回的地址相同。这样,第一个进程就没有必要使用进程间的通信方式将内存地址传送给第二个进程。

Windows 2000实现内存映射文件的方法要比Windows 98好,因为Windows 2000要求在进程的地址空间中的文件数据可供访问之前,该进程必须调用M a p Vi e w O f F i l e函数。如果一个进程调用M a p Vi e w O f F i l e函数,系统将为调用进程的地址空间中的视图进行地址空间区域的倒序操作,这样,其他进程都将无法看到该视图。如果另一个进程想要访问同一个文件映射对象中的数据,那么第二个进程中的线程就必须调用M a p Vi e w O f F i l e,同时,系统将为第二个进程的地址空间中的视图进行地址空间区域的倒序操作。

值得注意的是,第一个进程调用M a p Vi e w O f F i l e函数后返回的内存地址,很可能不同于第二个进程调用M a p Vi e w O f F i l e函数后返回的内存地址。即使这两个进程映射了相同文件映射对象的视图,它们返回的地址也可能不同。在Windows 98下,M a p Vi e w O f F i l e函数返回的内存地址是相同的,但是,如果想让你的应用程序在Windows 2000下运行,那么绝对不应该指望它们也返回相同的地址。

让我们再来观察另一个实现代码的差别。下面是一个小程序,它映射了单个文件映射对象的两个视图:

#include <Windows.h>

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE,
   PTSTR pszCmdLine, int nCmdShow) 
{
   //Open an existing file-it must be bigger than 64 KB.
   HANDLE hFile = CreateFile(pszCmdLine, GENERIC_READ | GENERIC_WRITE,
      0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

   //Create a file-mapping object backed by the data file.
   HANDLE hFileMapping = CreateFileMapping(hFile, NULL,
      PAGE_READWRITE, 0, 0, NULL);

   //Map a view of the whole file into our address space.
   PBYTE pbFile = (PBYTE) MapViewOfFile(hFileMapping,
      FILE_MAP_WRITE, 0, 0, 0);

   //Map a view of the file (starting 64 KB in) into our address space
   PBYTE pbFile2 = (PBYTE) MapViewOfFile(hFileMapping,
      FILE_MAP_WRITE, 0, 65536, 0);

   if((pbFile + 65536) == pbFile2) 
   {
      // If the addresses overlap, there is one address
      // space region for both views: this must be Windows 98.
      MessageBox(NULL, "We are running under Windows 98", NULL, MB_OK);
   } 
   else 
   {
      // If the addresses do not overlap, each view has its own
      // address space region: this must be Windows 2000.
      MessageBox(NULL, "We are running under Windows 2000", NULL, MB_OK);
   }

   UnmapViewOfFile(pbFile2);
   UnmapViewOfFile(pbFile);
   CloseHandle(hFileMapping);
   CloseHandle(hFile);

   return(0);
}
在Windows 98中,当文件映射对象的视图被映射时,系统将为整个文件映射对象保留足够的地址空间。即使调用M a p Vi e w O f F i l e函数时它的参数指明你想要系统只映射文件映射对象的一小部分,系统也会为它保留足够的地址空间。这意味着即使你规定只映射文件映射对象的一个64 KB的部分,也不能将一个1 GB的文件映射对象映射到一个视图中。

每当进程调用M a p Vi e w O f F i l e时,该函数将返回一个为整个文件映射对象保留的地址空间区域中的地址。因此,在上面的代码段中,第一次调用M a p Vi e w O f F i l e函数时返回包含整个映射文件的区域的基地址,第二次调用M a p Vi e w O f F i l e函数时返回离同一个地址空间区域64 KB位置上的地址。

Windows 2000的实现代码在这里同样存在很大的差别。在上面的代码段中,两次调用M a p Vi e w O f F i l e函数将导致Windows 2000保留两个不同的地址空间区域。第一个区域的大小是文件映射对象的大小,第二个区域的大小是文件映射对象的大小减去64 KB。尽管存在两个不同的区域,但是它们的数据能够保证其相关性,因为两个视图都是从相同的文件映射对象映射而来的。在Windows 98下,各个视图具有相关性,因为它们位于同一个内存中。


17.8 使用内存映射文件在进程之间共享数据

Wi n d o w s总是出色地提供各种机制,使应用程序能够迅速而方便地共享数据和信息。这些机制包括R P C、C O M、O L E、D D E、窗口消息(尤其是W M _ C O P Y D ATA)、剪贴板、邮箱、管道和套接字等。在Wi n d o w s中,在单个计算机上共享数据的最低层机制是内存映射文件。不错,如果互相进行通信的所有进程都在同一台计算机上的话,上面提到的所有机制均使用内存映射文件从事它们的烦琐工作。如果要求达到较高的性能和较小的开销,内存映射文件是举手可得的最佳机制。

数据共享方法是通过让两个或多个进程映射同一个文件映射对象的视图来实现的,这意味着它们将共享物理存储器的同一个页面。因此,当一个进程将数据写入一个共享文件映射对象的视图时,其他进程可以立即看到它们视图中的数据变更情况。注意,如果多个进程共享单个文件映射对象,那么所有进程必须使用相同的名字来表示该文件映射对象。

让我们观察一个例子,启动一个应用程序。当一个应用程序启动时,系统调用C r e a t e F i l e函数,打开磁盘上的. e x e文件。然后系统调用C r e a t e F i l e M a p p i n g函数,创建一个文件映射对象。最后,系统代表新创建的进程调用M a p Vi e w O f F i l e E x函数(它带有S E C _ I M A G E标志),这样, . e x e文件就可以映射到进程的地址空间。这里调用的是M a p Vi e w O f F i l e E x,而不是M a p Vi e w O f F i l e,这样,文件的映像将被映射到存放在. e x e文件映像中的基地址中。系统创建该进程的主线程,将该映射视图的可执行代码的第一个字节的地址放入线程的指令指针,然后C P U启动该代码的运行。

如果用户运行同一个应用程序的第二个实例,系统就认为规定的. e x e文件已经存在一个文件映射对象,因此不会创建新的文件对象或者文件映射对象。相反,系统将第二次映射该文件的一个视图,这次是在新创建的进程的地址空间环境中映射的。系统所做的工作是将相同的文件同时映射到两个地址空间。显然,这是对内存的更有效的使用,因为两个进程将共享包含正在执行的这部分代码的物理存储器的同一个页面。

与所有内核对象一样,可以使用3种方法与多个进程共享对象,这3种方法是句柄继承性、句柄命名和句柄复制。关于这3种方法的详细说明,参见第3章的内容。


17.9 页文件支持的内存映射文件

到现在为止,已经介绍了映射驻留在磁盘驱动器上的文件视图的方法。许多应用程序在运行时都要创建一些数据,并且需要将数据传送给其他进程,或者与其他进程共享。如果应用程序必须在磁盘驱动器上创建数据文件,并且将数据存储在磁盘上以便对它进行共享,那么这将是非常不方便的。

M i c r o s o f t公司认识到了这一点,并且增加了一些功能,以便创建由系统的页文件支持的内存映射文件,而不是由专用硬盘文件支持的内存映射文件。这个方法与创建内存映射磁盘文件所用的方法几乎相同,不同之处是它更加方便。一方面,它不必调用C r e a t e F i l e函数,因为你不是要创建或打开一个指定的文件,你只需要像通常那样调用C r e a t e F i l e M a p p i n g函数,并且传递I N VA L I D _ H A N D L E _ VA L U E作为h F i l e参数。这将告诉系统,你不是创建其物理存储器驻留在磁盘上的文件中的文件映射对象,相反,你想让系统从它的页文件中提交物理存储器。分配的存储器的数量由C r e a t e F i l e M a p p i n g函数的d w M a x i m u m S i z e H i g h和d w M a x i m u m S i z e L o w两个参数来决定。

当创建了文件映射对象并且将它的一个视图映射到进程的地址空间之后,就可以像使用任何内存区域那样使用它。如果你想要与其他进程共享该数据,可调用C r e a t e F i l e M a p p i n g函数,并传递一个以0结尾的字符串作为p s z N a m e参数。然后,想要访问该存储器的其他进程就可以调用C r e a t e F i l e M a p p i n g或O p e n F i l e M a p p i n g函数,并传递相同的名字。

当进程不再想要访问文件映射对象时,该进程应该调用C l o s e H a n d l e函数。当所有句柄均被关闭后,系统将从系统的页文件中收回已经提交的存储器。

注意下面是一个非常有意思的问题,它使单纯的程序员大吃一惊。你能猜到下面这个代码段错在哪里吗?

HANDLE hFile = CreateFile(...);
HANDLE hMap = CreateFileMapping(hFile, ...);
if(hMap == NULL)
   return(GetLastError());
如果上面这个对C r e a t e F i l e函数的调用失败,它将返回I N VA L I D _ H A N D L E _VA L U E。但是,编写这个代码的程序员没有测试一下,看文件是否已经创建成功。当C r e a t e F i l e M a p p i n g函数被调用时,在h F i l e参数中传递了I N VA L I D _ H A N D L E _VA L U E,这使得系统创建一个使用来自页文件而不是来自指定的磁盘文件存储器的文件映像。使用内存映射文件的任何辅助代码都能够正确地运行。但是,当文件映射对象被撤消时,写入文件映射存储器(页文件)的全部数据将被系统撤消。这时,程序员就坐在那里绞尽脑汁,不知道问题究竟出在哪里。因此,必须始终检查C r e a t e F i l e函数的返回值,以确定是否出现了错误,因为C r e a t e F i l e运行失败的原因太多了。

共享内存映射文件的示例应用程序

清单1 7 - 3列出的M M F S h a r e应用程序(“17 MMFShare.exe”)显示了如何使用内存映射文件在两个或多个独立的进程间传送数据。该应用程序的源代码和资源文件位于本书所附光盘上的1 7 - M M F S h a r e目录下。

至少需要执行M M F S h a r e程序的两个实例。每个实例创建它自己的对话框,如图1 7 - 9所示。


图17-9 运行MMFShare 时出现的对话框

若要将数据从M M F S h a r e的一个实例传送到另一个实例,请将要传送的数据键入D a t a编辑框。然后单击Create Mapping Of Data(创建数据映像)按钮。当进行这项操作时, M M F S h a r e调用C r e a t e F i l e M a p p i n g函数,创建一个由系统的页文件支持的4 KB内存映射文件对象,并将该对象命名为M M F S h a r e d D a t a。如果M M F S h a r e发现已经存在一个带有这个名字的文件映射对象,它就显示一个消息框,告诉你它不能创建该对象。如果M M F S h a r e成功地创建了该对象,那么它将进一步将文件的视图映射到进程的地址空间,并将数据从编辑控件拷贝到内存映射文件中。

当数据被拷贝后,M M F S h a r e就撤消文件的视图,使Create Mapping Of Data按钮不起作用,并激活Close Mapping Of Data(关闭数据映像)按钮。这时,命名为M M F S h a r e d D a t a的内存映射文件仍然位于系统中的某个位置。没有任何进程映射了包含在文件中的数据视图。

如果这时转入M M F S h a r e的另一个实例,并且单击该实例的Open Mapping And Get Data(打开映像并获取数据)按钮,那么M M F S h a r e将设法通过调用O p e n F i l e M a p p i n g函数,寻找一个称为M M F S h a r e d D a t a的文件映射对象。如果无法找到带有该名字的对象, M M F S h a r e就会显示另一个消息框,将这个情况通知你。如果M M F S h a r e找到了这个对象,它将把该对象的视图映射到它的进程的地址空间,将数据从内存映射文件拷贝到对话框的编辑控件中,然后撤消它的映像,关闭文件映射对象。好极了,你已经成功地将数据从一个进程传送到另一个进程。

对话框中的Close Mapping Of Data(关闭数据映像)按钮用于关闭文件映射对象,它能够释放页文件中的存储器。如果不存在任何文件映射对象,那么M M F S h a r e的其他实例将无法打开文件映像并从中取出数据。另外,如果一个实例已经创建了一个内存映射文件,那么其他实例均不得创建内存映射文件并改写文件中包含的数据。

清单17-3 MMFShare示例应用程序

/******************************************************************************
Module:  MMFShare.cpp
Notices: Copyright (c) 2000 Jeffrey Richter
******************************************************************************/


#include "..\CmnHdr.h"     /* See Appendix A. */
#include <windowsx.h>
#include <tchar.h>
#include "Resource.h"


///////////////////////////////////////////////////////////////////////////////


BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {

   chSETDLGICONS(hwnd, IDI_MMFSHARE);

   // Initialize the edit control with some test data.
   Edit_SetText(GetDlgItem(hwnd, IDC_DATA), TEXT("Some test data"));

   // Disable the Close button because the file can't
   // be closed if it was never created or opened.
   Button_Enable(GetDlgItem(hwnd, IDC_CLOSEFILE), FALSE);
   return(TRUE);
}


///////////////////////////////////////////////////////////////////////////////


void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {
   
   // Handle of the open memory-mapped file
   static HANDLE s_hFileMap = NULL;

   switch (id) {
      case IDCANCEL:
         EndDialog(hwnd, id);
         break;

      case IDC_CREATEFILE:
         if (codeNotify != BN_CLICKED)
            break;

         // Create a paging file-backed MMF to contain the edit control text.
         // The MMF is 4 KB at most and is named MMFSharedData.
         s_hFileMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, 
            PAGE_READWRITE, 0, 4 * 1024, TEXT("MMFSharedData"));

         if (s_hFileMap != NULL) {

            if (GetLastError() == ERROR_ALREADY_EXISTS) {
               chMB("Mapping already exists - not created.");
               CloseHandle(s_hFileMap);

            } else {

               // File mapping created successfully.

               // Map a view of the file into the address space.
               PVOID pView = MapViewOfFile(s_hFileMap,
                  FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);

               if (pView != NULL) {
                  // Put edit text into the MMF.
                  Edit_GetText(GetDlgItem(hwnd, IDC_DATA), 
                     (PTSTR) pView, 4 * 1024);

                  // Protect the MMF storage by unmapping it.
                  UnmapViewOfFile(pView);

                  // The user can't create another file right now.
                  Button_Enable(hwndCtl, FALSE);

                  // The user closed the file.
                  Button_Enable(GetDlgItem(hwnd, IDC_CLOSEFILE), TRUE);

               } else {
                  chMB("Can't map view of file.");
               }
            }

         } else {
            chMB("Can't create file mapping.");
         }
         break;

      case IDC_CLOSEFILE:
         if (codeNotify != BN_CLICKED)
            break;

         if (CloseHandle(s_hFileMap)) {
            // User closed the file, fix up the buttons.
            Button_Enable(GetDlgItem(hwnd, IDC_CREATEFILE), TRUE);
            Button_Enable(hwndCtl, FALSE);
         }
         break;

      case IDC_OPENFILE:
         if (codeNotify != BN_CLICKED)
            break;

         // See if a memory-mapped file named MMFSharedData already exists.
         HANDLE hFileMapT = OpenFileMapping(FILE_MAP_READ | FILE_MAP_WRITE,
            FALSE, TEXT("MMFSharedData"));

         if (hFileMapT != NULL) {
            // The MMF does exist, map it into the process's address space.
            PVOID pView = MapViewOfFile(hFileMapT, 
               FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);

            if (pView != NULL) {

               // Put the contents of the MMF into the edit control.
               Edit_SetText(GetDlgItem(hwnd, IDC_DATA), (PTSTR) pView);
               UnmapViewOfFile(pView);
            } else {
               chMB("Can't map view.");
            }

            CloseHandle(hFileMapT);

         } else {
            chMB("Can't open mapping.");
         }
         break;
   }
}


///////////////////////////////////////////////////////////////////////////////


INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
   
   switch (uMsg) {
      chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
      chHANDLE_DLGMSG(hwnd, WM_COMMAND,    Dlg_OnCommand);
   }
   return(FALSE);
}


///////////////////////////////////////////////////////////////////////////////


int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) {

   DialogBox(hinstExe, MAKEINTRESOURCE(IDD_MMFSHARE), NULL, Dlg_Proc);
   return(0);
}


//////////////////////////////// End of File //////////////////////////////////
//Microsoft Developer Studio generated resource script.
//
#include "Resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// English (U.S.) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32

/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

IDD_MMFSHARE DIALOG DISCARDABLE  38, 36, 186, 61
STYLE WS_MINIMIZEBOX | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Memory-Mapped File Sharing"
FONT 8, "MS Sans Serif"
BEGIN
    PUSHBUTTON      "&Create mapping of Data",IDC_CREATEFILE,4,4,84,14,
                    WS_GROUP
    PUSHBUTTON      "&Close mapping of Data",IDC_CLOSEFILE,96,4,84,14
    LTEXT           "&Data:",IDC_STATIC,4,26,18,8
    EDITTEXT        IDC_DATA,28,24,153,12
    PUSHBUTTON      "&Open mapping and get Data",IDC_OPENFILE,40,44,104,14,
                    WS_GROUP
END


/////////////////////////////////////////////////////////////////////////////
//
// Icon
//

// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_MMFSHARE            ICON    DISCARDABLE     "MMFShare.Ico"

#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE 
BEGIN
    "Resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE 
BEGIN
    "#include ""afxres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE DISCARDABLE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED

#endif    // English (U.S.) resources
/////////////////////////////////////////////////////////////////////////////



#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//


/////////////////////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED


17.10 稀疏提交的内存映射文件

在迄今为止介绍的所有内存映射文件中,我们发现系统要求为内存映射文件提交的所有存储器必须是在磁盘上的数据文件中或者是在页文件中。这意味着我们不能根据我们的喜好来有效地使用存储器。让我们回到第1 5章中介绍电子表格的内容上来,比如说,你想要与另一个进程共享整个电子表格。如果我们使用内存映射文件,那么必须为整个电子表格提交物理存储器:

CELLDATA CellData[200][256];
如果C E L L D ATA结构的大小是1 2 8字节,那么这个数组需要6 553 600(200 x 256 x 128)字节的物理存储器。第1 5章讲过,如果用页文件为电子表格分配物理存储器,那么这是个不小的数目了,尤其是考虑到大多数用户只是将信息放入少数的单元格中,而大多数单元格却空闲不用时,这就显得有些浪费。

显然,我们宁愿将电子表格作为一个文件映射对象来共享,而不必预先提交所有的物理存储器。C r e a t e F i l e M a p p i n g函数为这种操作提供了一种方法,即可以在f d w P r o t e c t参数中设定S E C _ R E S E RV E或S E C _ C O M M I T标志。

只有当创建由系统的页文件支持的文件映射对象时,这些标志才有意义。S E C _ C O M M I T标志能使C r e a t e F i l e M a p p i n g从系统的页文件中提交存储器。如果两个标志都不设定,其结果也一样。

当调用C r e a t e F i l e M a p p i n g函数并传递S E C _ R E S E RV E标志时,系统并不从它的页文件中提交物理存储器,它只是返回文件映射对象的一个句柄。这时可以调用M a p Vi e w O f F i l e或M a p Vi e w O f F i l e E x函数,创建该文件映射对象的视图。M a p Vi e w O f F i l e和M a p Vi e w O f F i l e E x将保留一个地址空间区域,并且不提交支持该区域的任何物理存储器。对保留区域中的内存地址进行访问的任何尝试均将导致线程引发访问违规。

现在我们得到的是一个保留的地址空间区域和用于标识该区域的文件映射对象的句柄。其他进程可以使用相同的文件映射对象来映射同一个地址空间区域的视图。物理存储器仍然没有被提交给该区域。如果其他进程中的线程试图访问它们区域中的视图的内存地址,这些线程将会引发访问违规。

下面是令人感兴趣的一些事情。若要将物理存储器提交给共享区域,线程需要做的操作只是调用Vi r t u a l A l l o c函数:

PVOID VirtualAlloc(
   PVOID pvAddress,
   SIZE_T dwSize,
   DWORD fdwAllocationType,
   DWORD fdwProtect);
第1 5章已经介绍了这个函数。调用Vi r t u a l A l l o c函数将物理存储器提交给内存映射视图区域,就像是调用Vi r t u a l A l l o c函数将存储器提交给开始时通过调用带有M E M _ R E S E RV E标志的Vi r t u a l A l l o c函数而保留的区域一样。而且,就像你可以提交稀疏地存在于用Vi r t u a l A l l o c保留的区域中的存储器一样,你也可以提交稀疏地存在于用M a p Vi e w O f F i l e或M a p Vi e w O f F i l e E x保留的区域中的存储器。但是,当你将存储器提交给用M a p Vi e w O f F i l e或M a p Vi e w O f F i l e E x保留的区域时,已经映射了相同文件映射对象视图的所有进程这时就能够成功地访问已经提交的页面。

使用S E C _ R E S E RV E标志和Vi r t u a l A l l o c函数,就能够成功地与其他进程共享电子表格应用程序的C e l l D a t a数组,并且能够非常有效地使用物理存储器。

Windows 98 通常情况下,当给Vi r t u a l A l l o c函数传递的内存地址位于0 x 0 0 4 0 0 0 0 0至0 x 7 F F F F F F F以外时,Vi r t u a l A l l o c的运行就会失败。但是,当将物理存储器提交给使用S E C _ R E S E RV E标志创建的内存映射文件时,必须调用Vi r t u a l A l l o c函数,传递一个位于0 x 8 0 0 0 0 0 0 0至0 x B F F F F F F F之间的内存地址。Windows 98知道你正在把存储器提交给一个保留的内存映射文件,并且让这个函数调用取得成功。

注意在Windows 2000下,无法使用Vi r t u a l F r e e函数从使用S E C _ R E S E RV E标志保留的内存映射文件中释放存储器。但是, Windows 98允许在这种情况下调用Vi r t u a l F r e e函数来释放存储器。

N T文件系统(NTFS 5)提供了对稀疏文件的支持。这是个非常出色的新特性。使用这个新的稀疏文件特性,能够很容易地创建和使用稀疏内存映射文件,在这些稀疏内存映射文件中,存储器包含在通常的磁盘文件中,而不是在系统的页文件中。

下面是如何使用稀疏文件特性的一个例子。比如,你想要创建一个M M F文件,以便存放记录的音频数据。当用户说话时,你想要将数字音频数据写入内存缓冲区,并且让该缓冲区得到磁盘上的一个文件的支持。稀疏M M F当然是在你的代码中实现这个要求的最容易和最有效的方法。问题是你不知道用户在单击S t o p(停止)按钮之前讲了多长时间。你可能需要一个足够大的文件来存放5分钟或5小时的数据,这两个时间长度的差别太大了。但是,当使用稀疏M M F时,数据文件的大小确实无关紧要。

稀疏内存映射文件示例应用程序

清单1 7 - 4列出的M M F S p a r s e应用程序(“17 MMFS p a r s e . e x e”)显示了如何创建一个由NTFS 5支持的内存映射文件。该应用程序的源代码和资源文件位于本书所附光盘上的1 7 - M M F S p a r s e目录下。当启动该程序时,便会出现图1 7 - 1 0所示的窗口。


图17-10 MMF Sparse窗口

当单击Create a 1MB(1024 KB)Sparse MMF(创建一个1 M B(1024 KB)稀疏M M F)按钮时,该程序将设法创建一个称为“C:\ M M F S p a r s e”的稀疏文件。如果你的C驱动器不是个NTFS 5卷,那么它的运行将会失败,并且该进程将终止运行。如果你的NTFS 5卷在另一个驱动器名上,你必须修改源代码,并且将它重建,以了解应用程序运行的情况。

一旦稀疏文件创建完成,它就被映射到进程的地址空间中。底部的Allocated Ranges(分配的范围)编辑框显示了文件的哪些部分实际上是由磁盘存储器支持的。开始时,该文件中没有任何存储器在里面,而编辑控件中则包含了“ No allocated ranges in the file”(文件中没有分配范围)这一消息文本。

若要读取一个字节,只需将一个数字输入O ff s e t(位移)编辑框中,并单击Read Byte(读取字节)按钮。输入的数字与1 0 2 4(1 KB)相乘,在该位置上的字节被读取并放入B y t e编辑框中。如果从没有支持存储器的任何部分中读取字节,将始终只能读取一个0字节。如果从拥有支持存储器的文件部分读取字节,将能够读取那里的任何字节。

若要写入一个字节,请将一个数字输入O ff s e t编辑框,并且将一个字节值( 0至2 5 5)输入B y t e编辑框。然后,当单击Write Byte(写入字节)按钮时,位移数字就与1 0 2 4相乘,同时,该位置上的字节被修改以反映指定的字节值。这个写入操作可使系统为该部分文件提交支持的存储器。当读取或写入操作执行完成后, Allocated Ranges编辑框总是会得到更新,以便向你显示文件的哪些部分实际上得到存储器的支持。图1 7 - 11显示了在1 024 000(1000 x 1024)这个位移上仅仅写入一个字节后对话框是个什么样子。

注意图1 7 - 11中只存在一个分配范围,它从文件中的逻辑位移983 040字节开始,并且支持存储器的65 536个字节已经被分配。也可以使用E x p l o r e r来找出文件C:\ M M F S p a r s e并显示它的属性页,如图1 7 - 1 2所示。


图1 7 - 11 写入一个字节后的MMF Sparse 对话框


图17-12 MMF Sparse Properties 对话框

注意,该属性页显示了文件的大小是1 MB(这是文件的虚拟大小),但是该文件实际上只占用64 KB磁盘空间。

最后一个按钮是Free All Allocated Regions(释放所有分配的区域),该程序可以用它来释放用于文件的所有存储器。这个特性能够释放磁盘空间,使文件中的所有字节均显示为0。

下面让我们来介绍一下该程序是如何运行的。为了简便起见,我创建了一个C S p a r s e S t r e a m的C + +类(在S p a r s e S t r e a m . h文件中实现)。这个类封装了可以用稀疏文件或数据流执行的任务。然后,在M M F S p a r s e . c p p文件中,我创建了另一个C + +类C M M F S p a r s e,它是由C S p a r s e S t r e a m派生而来的。因此,C M M F S p a r s e对象将拥有C S p a r s e S t r e a m的所有特性,并且要加上将稀疏数据流用作内存映射文件时特定的几个特性。该进程拥有C M M F S p a r s e对象的单个全局实例,称为g _ m m f。应用程序在它的整个代码中都要引用这个全局变量,以便对稀疏内存映射文件进行操作。

当用户单击Create a 1 MB(1024 KB)Sparse MMF(创建一个1 MB(1024 KB)的稀疏M M F)按钮时,C r e a t e F i l e函数被调用,以便在NTFS 5磁盘分区上创建一个新文件。这是个普通的常规文件。但是,这时我使用g _ m m f对象并且调用它的I n i t i a l i z e方法,传递该文件的句柄和文件的最大长度(1 MB)。在系统内部,I n i t i a l i z e方法调用C r e a t e F i l e M a p p i n g函数,按照指定的大小创建文件映射内核对象,然后调用M a p Vi e w O f F i l e函数,使得该稀疏文件出现在进程的地址空间中。

当I n i t i a l i z e方法返回时, D l g _ S h o w A l l o c a t e d R a n g e s函数被调用。该函数在内部调用各个Wi n d o w s函数,以便枚举已经为之分配了存储器的稀疏文件的逻辑范围。每个分配范围的起始位移和长度显示在对话框底部的编辑控件中。当g _ m m f对象首次被初始化时,已经为磁盘上的文件分配的物理存储器实际上是0,编辑控件将反映出这个情况。

这时,用户可以设法从稀疏内存映射文件中读取数据,或者将数据写入该文件。如果试图写入数据,用户的位移和字节值可以从它们各自的编辑控件中获得,并将数据写入g _ m m f对象中的内存地址。如果将数据写入g _ m m f,文件系统就会将存储器分配给文件的这个逻辑分区,不过分配情况对应用程序来说是透明的。

如果用户试图从g _ m m f对象中读取一个字节,那么该读取操作将设法在已经分配了存储器的文件中读取一个字节,否则该字节也许会标识一个尚未分配存储器的字节。如果该字节尚未被分配存储器,那么读取该字节就会返回0。同样,这对于应用程序来说是透明的。如果存在供被读取的字节使用的存储器,当然就返回它的实际值。

该应用程序说明的最后一个问题是如何清除文件,使它的所有已分配的存储器范围被释放,文件不再需要磁盘存储器。若要释放所有的已分配范围,用户可以单击Free All AllocatedR a n g e s按钮。Wi n d o w s无法为内存映射文件释放所有已分配的范围,因此应用程序要做的第一件事情是调用g _ m m f对象的F o r c e C l o s e方法。F o r c e C l o s e方法在内部调用U n m a p Vi e w O f F i l e函数,然后调用C l o s e H a n d l e,传递文件映射内核对象的句柄。

接着D e c o m m i t P o r t i o n O f S t r e a m函数被调用,为文件中的0至1 MB的逻辑字节释放所有的存储器。最后,再次为g _ m m f对象调用I n i t i a l i z e方法,将内存映射文件重新初始化到进程的地址空间中。为了证明文件已经释放它的全部分配范围,需调用D l g _ S h o w A l l o c a t e d R a n g e s函数,它在编辑控件中显示“No allocated ranges in the file”(文件中没有已分配范围)字符串。

最后需要说明的是,如果在实际的应用程序中使用稀疏内存映射文件,当关闭文件时,可能必须截断文件实是的逻辑长度。虽然将包含0字节的稀疏文件的结尾截去不会对磁盘空间产生任何影响,这确实是很好的事情—E x p l o r e r和命令外壳的D I R命令能够向用户报告比较准确的文件大小。若要为文件设置文件标记的结尾,可以在调用F o r c e C l o s e方法后,调用S e t F i l e P o i n t e r和S e t E n d O f F i l e函数。

清单17-4 MMFSparse示例应用程序

/******************************************************************************
Module:  MMFSparse.cpp
Notices: Copyright (c) 2000 Jeffrey Richter
******************************************************************************/


#include "..\CmnHdr.h"     /* See Appendix A. */
#include <tchar.h>
#include <WindowsX.h>
#include <WinIoCtl.h>
#include "SparseStream.h"
#include "Resource.h"


//////////////////////////////////////////////////////////////////////////////


// This class makes it easy to work with memory-mapped sparse files
class CMMFSparse : public CSparseStream {
private:
   HANDLE m_hfilemap;      // File-mapping object
   PVOID  m_pvFile;        // Address to start of mapped file

public:
   // Creates a Sparse MMF and maps it in the process's address space.
   CMMFSparse(HANDLE hstream = NULL, SIZE_T dwStreamSizeMax = 0);

   // Closes a Sparse MMF
   virtual ~CMMFSparse() { ForceClose(); }

   // Creates a sparse MMF and maps it in the process's address space.
   BOOL Initialize(HANDLE hstream, SIZE_T dwStreamSizeMax);

   // MMF to BYTE cast operator returns address of first byte 
   // in the memory-mapped sparse file. 
   operator PBYTE() const { return((PBYTE) m_pvFile); }

   // Allows you to explicitly close the MMF without having
   // to wait for the destructor to be called.
   VOID ForceClose();
};


//////////////////////////////////////////////////////////////////////////////


CMMFSparse::CMMFSparse(HANDLE hstream, SIZE_T dwStreamSizeMax) {

   Initialize(hstream, dwStreamSizeMax);
}


//////////////////////////////////////////////////////////////////////////////


BOOL CMMFSparse::Initialize(HANDLE hstream, SIZE_T dwStreamSizeMax) {

   if (m_hfilemap != NULL) 
      ForceClose();

   // Initialize to NULL in case something goes wrong
   m_hfilemap = m_pvFile = NULL;

   BOOL fOk = TRUE;  // Assume success

   if (hstream != NULL) {
      if (dwStreamSizeMax == 0) {
         DebugBreak();  // Illegal stream size
      }

      CSparseStream::Initialize(hstream);
      fOk = MakeSparse();  // Make the stream sparse
      if (fOk) {
         // Create a file-mapping object
         m_hfilemap = ::CreateFileMapping(hstream, NULL, PAGE_READWRITE, 
            (DWORD) (dwStreamSizeMax >> 32I64), (DWORD) dwStreamSizeMax, NULL);

         if (m_hfilemap != NULL) {
            // Map the stream into the process's address space
            m_pvFile = ::MapViewOfFile(m_hfilemap, 
               FILE_MAP_WRITE | FILE_MAP_READ, 0, 0, 0);
         } else {
            // Failed to map the file, cleanup
            CSparseStream::Initialize(NULL);
            ForceClose();
            fOk = FALSE;
         }
      }
   }
   return(fOk);
}


//////////////////////////////////////////////////////////////////////////////


VOID CMMFSparse::ForceClose() {

   // Cleanup everything that was done sucessfully
   if (m_pvFile != NULL) { 
      ::UnmapViewOfFile(m_pvFile); 
      m_pvFile = NULL; 
   }
   if (m_hfilemap != NULL) { 
      ::CloseHandle(m_hfilemap);   
      m_hfilemap = NULL; 
   }
}


//////////////////////////////////////////////////////////////////////////////


#define STREAMSIZE      (1 * 1024 * 1024)    // 1 MB (1024 KB)
TCHAR szPathname[] = TEXT("C:\\MMFSparse.");
HANDLE g_hstream = INVALID_HANDLE_VALUE;
CMMFSparse g_mmf;


///////////////////////////////////////////////////////////////////////////////


BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) {

   chSETDLGICONS(hwnd, IDI_MMFSPARSE);

   // Initialize the dialog box controls.
   EnableWindow(GetDlgItem(hwnd, IDC_OFFSET), FALSE);
   Edit_LimitText(GetDlgItem(hwnd, IDC_OFFSET), 4);
   SetDlgItemInt(hwnd, IDC_OFFSET, 1000, FALSE);

   EnableWindow(GetDlgItem(hwnd, IDC_BYTE), FALSE);
   Edit_LimitText(GetDlgItem(hwnd, IDC_BYTE), 3);
   SetDlgItemInt(hwnd, IDC_BYTE, 5, FALSE);

   EnableWindow(GetDlgItem(hwnd, IDC_WRITEBYTE), FALSE);
   EnableWindow(GetDlgItem(hwnd, IDC_READBYTE),  FALSE);
   EnableWindow(GetDlgItem(hwnd, IDC_FREEALLOCATEDREGIONS), FALSE);

   return(TRUE);
}


///////////////////////////////////////////////////////////////////////////////


void Dlg_ShowAllocatedRanges(HWND hwnd) {

   // Fill in the Allocated Ranges edit control
   DWORD dwNumEntries;
   FILE_ALLOCATED_RANGE_BUFFER* pfarb = 
      g_mmf.QueryAllocatedRanges(&dwNumEntries);

   if (dwNumEntries == 0) {
      SetDlgItemText(hwnd, IDC_FILESTATUS, 
         TEXT("No allocated ranges in the file"));
   } else {
      TCHAR sz[4096] = { 0 };
      for (DWORD dwEntry = 0; dwEntry < dwNumEntries; dwEntry++) {
         wsprintf(_tcschr(sz, 0), TEXT("Offset: %7.7u, Length: %7.7u\r\n"), 
            pfarb[dwEntry].FileOffset.LowPart, pfarb[dwEntry].Length.LowPart);
      }
      SetDlgItemText(hwnd, IDC_FILESTATUS, sz);
   }
   g_mmf.FreeAllocatedRanges(pfarb);
}


///////////////////////////////////////////////////////////////////////////////


void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) {
   
   switch (id) {
      case IDCANCEL:
         if (g_hstream != INVALID_HANDLE_VALUE) 
            CloseHandle(g_hstream);
         EndDialog(hwnd, id);
         break;

      case IDC_CREATEMMF:
         // Create the file
         g_hstream = CreateFile(szPathname, GENERIC_READ | GENERIC_WRITE, 
            0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
         if (g_hstream == INVALID_HANDLE_VALUE) {
            chFAIL("Failed to create file.");
         }

         // Create a 1MB (1024 KB) MMF using the file
         if (!g_mmf.Initialize(g_hstream, STREAMSIZE)) {
            chFAIL("Failed to initialize Sparse MMF.");
         }
         Dlg_ShowAllocatedRanges(hwnd);

         // Enable/disable the other controls.
         EnableWindow(GetDlgItem(hwnd, IDC_CREATEMMF), FALSE);
         EnableWindow(GetDlgItem(hwnd, IDC_OFFSET),    TRUE);
         EnableWindow(GetDlgItem(hwnd, IDC_BYTE),      TRUE);
         EnableWindow(GetDlgItem(hwnd, IDC_WRITEBYTE), TRUE);
         EnableWindow(GetDlgItem(hwnd, IDC_READBYTE),  TRUE);
         EnableWindow(GetDlgItem(hwnd, IDC_FREEALLOCATEDREGIONS), TRUE);

         // Force the Offset edit control to have the focus.
         SetFocus(GetDlgItem(hwnd, IDC_OFFSET));
         break;

      case IDC_WRITEBYTE:
         {
         BOOL fTranslated;
         DWORD dwOffset = GetDlgItemInt(hwnd, IDC_OFFSET, &fTranslated, FALSE);
         if (fTranslated) {
            g_mmf[dwOffset * 1024] = (BYTE) 
               GetDlgItemInt(hwnd, IDC_BYTE, NULL, FALSE);
            Dlg_ShowAllocatedRanges(hwnd);
         }
         }
         break;

      case IDC_READBYTE:
         {
         BOOL fTranslated;
         DWORD dwOffset = GetDlgItemInt(hwnd, IDC_OFFSET, &fTranslated, FALSE);
         if (fTranslated) {
            SetDlgItemInt(hwnd, IDC_BYTE, g_mmf[dwOffset * 1024], FALSE);
            Dlg_ShowAllocatedRanges(hwnd);
         }
         }
         break;

      case IDC_FREEALLOCATEDREGIONS:
         // Normally the destructor causes the file-mapping to close.
         // But, in this case, we wish to force it so that we can reset 
         // a portion of the file back to all zeroes.
         g_mmf.ForceClose();

         // We call ForceClose above because attempting to zero a portion of 
         // the file while it is mapped, causes DeviceIoControl to fail with 
         // error ERROR_USER_MAPPED_FILE ("The requested operation cannot 
         // be performed on a file with a user-mapped section open.")
         g_mmf.DecommitPortionOfStream(0, STREAMSIZE);
         g_mmf.Initialize(g_hstream, STREAMSIZE);
         Dlg_ShowAllocatedRanges(hwnd);
         break;
   }
}


///////////////////////////////////////////////////////////////////////////////


INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {

   switch (uMsg) {
      chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog);
      chHANDLE_DLGMSG(hwnd, WM_COMMAND,    Dlg_OnCommand);
   }
   return(FALSE);
}


///////////////////////////////////////////////////////////////////////////////


int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) {

   chWindows2000Required();

   DialogBox(hinstExe, MAKEINTRESOURCE(IDD_MMFSPARSE), NULL, Dlg_Proc);
   return(0);
}


//////////////////////////////// End of File //////////////////////////////////
//Microsoft Developer Studio generated resource script.
//
#include "Resource.h"

#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#include "afxres.h"

/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS

/////////////////////////////////////////////////////////////////////////////
// English (U.S.) resources

#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32

#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//

1 TEXTINCLUDE DISCARDABLE 
BEGIN
    "Resource.h\0"
END

2 TEXTINCLUDE DISCARDABLE 
BEGIN
    "#include ""afxres.h""\r\n"
    "\0"
END

3 TEXTINCLUDE DISCARDABLE 
BEGIN
    "\r\n"
    "\0"
END

#endif    // APSTUDIO_INVOKED


/////////////////////////////////////////////////////////////////////////////
//
// Dialog
//

IDD_MMFSPARSE DIALOG DISCARDABLE  15, 24, 172, 197
STYLE DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "MMF Sparse"
FONT 8, "MS Sans Serif"
BEGIN
    DEFPUSHBUTTON   "&Create a 1MB (1024 KB) Sparse MMF",IDC_CREATEMMF,4,4,
                    164,14,WS_GROUP
    LTEXT           "&Offset (0 - 1023KB):",IDC_STATIC,4,24,63,8
    EDITTEXT        IDC_OFFSET,72,22,44,12
    PUSHBUTTON      "&Read byte",IDC_READBYTE,122,20,46,14
    LTEXT           "&Byte (0-255):",IDC_STATIC,4,36,41,8
    EDITTEXT        IDC_BYTE,72,36,44,12,ES_UPPERCASE | ES_NUMBER
    PUSHBUTTON      "&Write byte",IDC_WRITEBYTE,122,36,46,14
    PUSHBUTTON      "&Free all allocated regions",IDC_FREEALLOCATEDREGIONS,
                    76,52,92,14
    LTEXT           "&Allocated Ranges:",IDC_STATIC,4,68,59,8
    EDITTEXT        IDC_FILESTATUS,4,80,164,116,ES_MULTILINE | 
                    ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_READONLY | 
                    WS_VSCROLL
END


/////////////////////////////////////////////////////////////////////////////
//
// Icon
//

// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_MMFSPARSE           ICON    DISCARDABLE     "MMFSparse.Ico"
#endif    // English (U.S.) resources
/////////////////////////////////////////////////////////////////////////////



#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//


/////////////////////////////////////////////////////////////////////////////
#endif    // not APSTUDIO_INVOKED

/******************************************************************************
Module:  SparseStream.h
Notices: Copyright (c) 2000 Jeffrey Richter
******************************************************************************/


#include "..\CmnHdr.h"     /* See Appendix A. */
#include <WinIoCtl.h>


///////////////////////////////////////////////////////////////////////////////


#pragma once


///////////////////////////////////////////////////////////////////////////////


class CSparseStream {
public:
   static BOOL DoesFileSystemSupportSparseStreams(PCTSTR pszVolume);
   static BOOL DoesFileContainAnySparseStreams(PCTSTR pszPathname);

public:
   CSparseStream(HANDLE hstream = INVALID_HANDLE_VALUE) { 
      Initialize(hstream); 
   }

   virtual ~CSparseStream() { }

   void Initialize(HANDLE hstream = INVALID_HANDLE_VALUE) { 
      m_hstream = hstream; 
   }

public:
   operator HANDLE() const { return(m_hstream); }

public:
   BOOL IsStreamSparse() const;
   BOOL MakeSparse();
   BOOL DecommitPortionOfStream(
      __int64 qwFileOffsetStart, __int64 qwFileOffsetEnd);

   FILE_ALLOCATED_RANGE_BUFFER* QueryAllocatedRanges(PDWORD pdwNumEntries);
   BOOL FreeAllocatedRanges(FILE_ALLOCATED_RANGE_BUFFER* pfarb);

private:
   HANDLE m_hstream;

private:
   static BOOL AreFlagsSet(DWORD fdwFlagBits, DWORD fFlagsToCheck) {
      return((fdwFlagBits & fFlagsToCheck) == fFlagsToCheck);
   }
};


///////////////////////////////////////////////////////////////////////////////


inline BOOL CSparseStream::DoesFileSystemSupportSparseStreams(
   PCTSTR pszVolume) {

   DWORD dwFileSystemFlags = 0;
   BOOL fOk = GetVolumeInformation(pszVolume, NULL, 0, NULL, NULL, 
      &dwFileSystemFlags, NULL, 0);
   fOk = fOk && AreFlagsSet(dwFileSystemFlags, FILE_SUPPORTS_SPARSE_FILES);
   return(fOk);
}


///////////////////////////////////////////////////////////////////////////////


inline BOOL CSparseStream::IsStreamSparse() const {

   BY_HANDLE_FILE_INFORMATION bhfi;
   GetFileInformationByHandle(m_hstream, &bhfi);
   return(AreFlagsSet(bhfi.dwFileAttributes, FILE_ATTRIBUTE_SPARSE_FILE));
}


///////////////////////////////////////////////////////////////////////////////


inline BOOL CSparseStream::MakeSparse() {

   DWORD dw;
   return(DeviceIoControl(m_hstream, FSCTL_SET_SPARSE, 
      NULL, 0, NULL, 0, &dw, NULL));
}


///////////////////////////////////////////////////////////////////////////////


inline BOOL CSparseStream::DecommitPortionOfStream(
   __int64 qwOffsetStart, __int64 qwOffsetEnd) {

   // NOTE: This function does not work if this file is memory-mapped.
   DWORD dw;
   FILE_ZERO_DATA_INFORMATION fzdi;
   fzdi.FileOffset.QuadPart = qwOffsetStart;
   fzdi.BeyondFinalZero.QuadPart = qwOffsetEnd + 1;
   return(DeviceIoControl(m_hstream, FSCTL_SET_ZERO_DATA, (PVOID) &fzdi, 
      sizeof(fzdi), NULL, 0, &dw, NULL));
}


///////////////////////////////////////////////////////////////////////////////


inline BOOL CSparseStream::DoesFileContainAnySparseStreams(
   PCTSTR pszPathname) {

   DWORD dw = GetFileAttributes(pszPathname);
   return((dw == 0xfffffff) 
      ? FALSE : AreFlagsSet(dw, FILE_ATTRIBUTE_SPARSE_FILE));
}


///////////////////////////////////////////////////////////////////////////////


inline FILE_ALLOCATED_RANGE_BUFFER* CSparseStream::QueryAllocatedRanges(
   PDWORD pdwNumEntries) {

   FILE_ALLOCATED_RANGE_BUFFER farb;
   farb.FileOffset.QuadPart = 0;
   farb.Length.LowPart = 
      GetFileSize(m_hstream, (PDWORD) &farb.Length.HighPart);

   // There is no way to determine the correct memory block size prior to 
   // attempting to collect this data, so I just picked 100 * sizeof(*pfarb)
   DWORD cb = 100 * sizeof(farb);
   FILE_ALLOCATED_RANGE_BUFFER* pfarb = (FILE_ALLOCATED_RANGE_BUFFER*) 
      HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cb);

   DeviceIoControl(m_hstream, FSCTL_QUERY_ALLOCATED_RANGES,
      &farb, sizeof(farb), pfarb, cb, &cb, NULL);
   *pdwNumEntries = cb / sizeof(*pfarb);
   return(pfarb);
}


///////////////////////////////////////////////////////////////////////////////


inline BOOL CSparseStream::FreeAllocatedRanges(
   FILE_ALLOCATED_RANGE_BUFFER* pfarb) {

   // Free the queue entry's allocated memory
   return(HeapFree(GetProcessHeap(), 0, pfarb));
}


///////////////////////////////// End Of File /////////////////////////////////

CZVC编程网出品,一剑[QQ:28077188]整理编译,欢迎联系
MSN:loomman@hotmail.com
  更多精彩VC编程资源尽在CZVC编程网!