17.3 使用内存映射文件
若要使用内存映射文件,必须执行下列操作步骤:
1) 创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件。
2) 创建一个文件映射内核对象,告诉系统该文件的大小和你打算如何访问该文件。
3) 让系统将文件映射对象的全部或一部分映射到你的进程地址空间中。
当完成对内存映射文件的使用时,必须执行下面这些步骤将它清除:
1) 告诉系统从你的进程的地址空间中撤消文件映射内核对象的映像。
2) 关闭文件映射内核对象。
3) 关闭文件内核对象。
下面将详细介绍这些操作步骤。
17.3.1 步骤1:创建或打开文件内核对象
若要创建或打开一个文件内核对象,总是要调用C r e a t e F i l e函数:
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 dA 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个值中的一个。
当创建或打开一个文件,将它作为一个内存映射文件来使用时,请选定最有意义的一个或
多个访问标志,以说明你打算如何访问文件的数据。对内存映射文件来说,必须打开用于只读访问或读写访问的文件,因此,可以分别设定 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个值之一。
如果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函数:
第一个参数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个保护属性之一。
Windows 98 在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 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 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 0 1 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的地址空间,因此可以进行更大的文件的映射操作,但是,如果文件是个超大规模的文件,仍然会遇到类似的问题。
书中给了个例子,让自己一步一步调看文件大小变化:
上面是我写的,CreadeFile会创建一个0字节大小的aaaa.dat,然后下面那个CreateFileMapping会把文件大小编程100字节。里面内容是空(NULL)。
如果调用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函数来进行这项操作:
参数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个值中的一个。
剩下的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,那么系统将设法把从文件中的指定位移开始到整个文件的结尾的视图映射到地址空间。
如果在调用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参数决定。只要你不进行其他操作,只是从文件的映像视图中读取数据,那么系统将决不会使用页文件中的这些提交的页面。但是,如果进程中的任何线程将数据写入文件的映像视图中的任何内存地址,那么系统将从页文件中抓取已提交页面中的一个页面,将原始数据页面拷贝到该页交换文件中,然后将该拷贝的页面映射到你的进程的地址空间。从这时起,你的进程中的线程就要访问数据的本地拷贝,不能读取或修改原始数据。
17.3.4 步骤4:从进程的地址空间中撤消文件数据的映像
当不再需要保留映射到你的进程地址空间区域中的文件数据时,可以通过调用下面的函数将它释放:
17.3.5 步骤5和步骤6:关闭文件映射对象和文件对象
不用说,你总是要关闭你打开了的内核对象。如果忘记关闭,在你的进程继续运行时会出现资源泄漏的问题。当然,当你的进程终止运行时,系统会自动关闭你的进程已经打开但是忘记关闭的任何对象。但是如果你的进程暂时没有终止运行,你将会积累许多资源句柄。因此你始终都应该编写清楚而又“正确的”代码,以便关闭你已经打开的任何对象。若要关闭文件映射对象和文件对象,只需要两次调用C l o s e H a n d l e函数,每个句柄调用一次:
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函数。当所有句柄均被关闭后,系统将从系统的页文件中收回已经提交的存储器。