Windows核心编程017.pdf

上传人:qwe****56 文档编号:70009192 上传时间:2023-01-14 格式:PDF 页数:54 大小:2.83MB
返回 下载 相关 举报
Windows核心编程017.pdf_第1页
第1页 / 共54页
Windows核心编程017.pdf_第2页
第2页 / 共54页
点击查看更多>>
资源描述

《Windows核心编程017.pdf》由会员分享,可在线阅读,更多相关《Windows核心编程017.pdf(54页珍藏版)》请在得力文库 - 分享文档赚钱的网站上搜索。

1、下载第1 7章内存映射文件对文件进行操作几乎是所有应用程序都必须进行的,并且这常常是人们争论的一个问题。应用程序究竟是应该打开文件,读取文件并关闭文件,还是打开文件,然后使用一种缓冲算法,从文件的各个不同部分进行读取和写入呢?M i c r o s o f t提供了一种两全其美的方法,那就是内存映射文件。与虚拟内存一样,内存映射文件可以用来保留一个地址空间的区域,并将物理存储器提交给该区域。它们之间的差别是,物理存储器来自一个已经位于磁盘上的文件,而不是系统的页文件。一旦该文件被映射,就可以访问它,就像整个文件已经加载内存一样。内存映射文件可以用于3个不同的目的:系统使用内存映射文件,以便加载

2、和执行.e x e和D L L文件。这可以大大节省页文件空间和应用程序启动运行所需的时间。可以使用内存映射文件来访问磁盘上的数据文件。这使你可以不必对文件执行 I/O操作,并且可以不必对文件内容进行缓存。可以使用内存映射文件,使同一台计算机上运行的多个进程能够相互之间共享数据。Wi n d o w s确实提供了其他一些方法,以便在进程之间进行数据通信,但是这些方法都是使用内存映射文件来实现的,这使得内存映射文件成为单个计算机上的多个进程互相进行通信的最有效的方法。本章将要介绍内存映射文件的各种使用方法。17.1 内存映射的可执行文件和D L L文件当线程调用C r e a t e P r o

3、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位应用程序

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。每当调

5、用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

6、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必须加载到它的首选地址中,否则它就根本无法加载)。第

7、二,系统必须在 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文件,那么系统就会向用户显示一个消息框,并且

8、释放进程的地址空间和进程对象。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

9、 M页面映射到进程的地址空间中的相应位置,并且让线程继续运行,就像这页代码已经加载了一样。当然,这一切是应用程序看不见的。当进程中的线程每次试图访问尚未加载到R A M的代码或数据时,该进程就会重复执行。17.1.1 可执行文件或D L L的多个实例不能共享静态数据当为正在运行的应用程序创建新进程时,系统将打开用于标识可执行文件映像的文件映射对象的另一个内存映射视图,并创建一个新进程对象和(为主线程创建)一个新线程对象。系统还要将新的进程I D和线程I D赋予这些对象。通过使用内存映射文件,同一个应用程序的多个正在运行的实例就能够共享R A M中的相同代码和数据。这里有一个小问题需要注意。进程

10、使用的是一个平面地址空间。当编译和链接你的程序时,所有的代码和数据都被合并在一起,组成一个很大的结构。数据与代码被分开,但仅限于跟在.e x e文件中的代码后面的数据而已。图1 7-1简单说明了应用程序的代码和数据究竟是如何加载到虚拟内存中,然后又被映射到应用程序的地址空间中的。作为一个例子,假设应用程序的第二个实例正在运行。系统只是将包含文件的代码和数据的虚拟内存页面映射到第二个应用程序的地址空间,如图1 7-2所示。如果应用程序的一个实例改变了驻留在数据页面中的某些全局变量,那么该应用程序的所有实例的内存内容都将改变。这种类型的改变可能带来灾难性的后果,因此是决不允许的。398计计第三部分

11、 内 存 管 理下载实际上,文件的内容被分割为不同的节。代码放在一个节中,全局变量放在另一个节中。各个节按照页面边界来对齐。通过调用Get SystemInfo 函数,应用程序可以确定正在使用的页面的大小。在.e x e或D L L文件中,代码节通常位于数据数据节的前面。系统运用内存管理系统的c o p y-o n-w r i t e(写入时拷贝)特性来防止进行这种改变。每当应用程序尝试将数据写入它的内存映射文件时,系统就会抓住这种尝试,为包含应用程序尝试写入数据的内存页面分配一个新内存块,再拷贝该页面的内容,并允许该应用程序将数据写入这个新分配的内存块。结果,同一个应用程序的所有其他实例的运

12、行都不会受到影响。图 1 7-3显示了当应用程序的第一个实例尝试改变数据页面2时出现的情况。图17-1 应用程序的代码和数据加载及映射示意图图17-2 应用程序与虚拟内存地址空间之间的关系示意图图17-3 应用程序的第一个实例尝试改变数据页面2时的情况系统分配一个新的虚拟内存页面,并且将数据页面2的内容拷贝到新页面中。第一个实例的地址空间发生了变更,这样,新数据页面就被映射到与原始地址页面相同位置上的地址空间中。这时系统就可以让进程修改全局变量,而不必担心改变同一个应用程序的另一个实例的数据。当应用程序被调试时,将会发生类似的事件。比如说,你正在运行一个应用程序的多个实例,并且只想调试其中的一

13、个实例。你访问调试程序,在一行源代码中设置一个断点。调试程序修改了你的代码,将你的一个汇编语言指令改为能使调试程序自行激活的指令。因此你再次第 1 7章内存映射文件计计399下载代码节包含3个代码页面数据节包含2个数据页面代码页面 2代码页面 1代码页面 1代码页面 2代码页面 3数据页面 2代码页面 3数据页面 1数据页面 2数据页面 1磁盘上的可执行文件虚拟内存应用程序的地址空间代码页面 2代码页面 1代码页面 2代码页面 3数据页面 1数据页面 2代码页面 1代码页面 1代码页面 2代码页面 3数据页面 2代码页面 3数据页面 1数据页面 2数据页面 1第二个实例的地址空间虚拟内存第一个

14、实例的地址空间代码页面 2代码页面 1代码页面 2代码页面 3数据页面 1数据页面 2代码页面 1代码页面 1代码页面 2代码页面 3数据页面 2代码页面 3数据页面 1新页面数据页面 2数据页面 1第二个实例的地址空间虚拟内存第一个实例的地址空间遇到了同样的问题。当调试程序修改代码时,它将导致应用程序的所有实例在修改后的汇编语言指令运行时激活该调试程序。为了解决这个问题,系统再次使用 c o p y-o n-w r i t e内存。当系统发现调试程序试图修改代码时,它就分配一个新内存块,将包含该指令的页面拷贝到新的内存页面中,并且允许调试程序修改页面拷贝中的代码。Windows 98当一个进

15、程被加载时,系统要查看文件映像的所有页面。系统立即为通常用c o p y-o n-w r i t e属性保护的那些页面提交页文件中的存储器。这些页面只是被提交而已,它们并不被访问。当文件映像中的页面被访问时,系统就加载相应的页面。如果该页面从来没有被修改,它就可以从内存中删除,并在必要时重新加载。但是,如果文件的页面被修改了,系统就将修改过的页面转到页文件中以前被提交的页面之一。Windows 2000与Windows 98之间的行为特性的唯一差别,是在你加载一个模块的两个拷贝并且可写入的数据尚未被修改的时候显示出来的。在这种情况下,在Windows 2000下运行的进程能够共享数据,而在Wi

16、ndows 98下,每个进程都可以得到它自己的数据拷贝。如果只加载模块的一个拷贝,或者可写入的数据已经被修改(这是通常的情况),那么Windows 2000与Windows 98的行为特性是完全相同的。17.1.2 在可执行文件或D L L的多个实例之间共享静态数据全局数据和静态数据不能被同一个.e x e或D L L文件的多个映像共享,这是个安全的默认设置。但是,在某些情况下,让一个.e x e文件的多个映像共享一个变量的实例是非常有用和方便的。例如,Wi n d o w s没有提供任何简便的方法来确定用户是否在运行应用程序的多个实例。但是,如果能够让所有实例共享单个全局变量,那么这个全局变

17、量就能够反映正在运行的实例的数量。当用户启动应用程序的一个实例时,新实例的线程能够简单地查看全局变量的值(它已经被另一个实例更新);如果这个数量大于 1,那么第二个实例就能够通知用户,该应用程序只有一个实例可以运行,而第二个实例将终止运行。本节将介绍一种方法,它允许你共享.e x e或D L L文件的所有实例的变量。不过在介绍这个方法之前,首先让我们介绍一些背景知识。每个.e x e或D L L文件的映像都由许多节组成。按照规定,每个标准节的名字均以圆点开头。例如,当编译你的程序时,编译器会将所有代码放入一个名叫.t e x t的节中。该编译器还将所有未经初始化的数据放入一个.b s s节,而

18、已经初始化的所有数据则放入.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映射文件中各个节的列

19、表。下面选录的代码是在一个可执行文件上运行 D u m p B i n程序而生成的:400计计第三部分 内 存 管 理下载第 1 7章内存映射文件计计401下载表1 7-2显示了比较常见的一些节的名字,并且说明了每一节的作用。除了编译器和链接程序创建的标准节外,也可以在使用下面的命令进行编译时创建自己的节:表17-2 常见的节名及作用节名作用.b s s未经初始化的数据.C RTC运行期只读数据.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

20、 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异常处理表402计计第三部分 内 存 管 理下载我可以创建一个称为“S h a r e d”的节,它包含单个L O N G值,如下所示:当编译器对这个代码进行编译时,它创建一个新节,称为 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()一行告诉编译器停止将已经初始化

21、的变量放入S h a r e d节,并且开始将它们放回到默认数据节中。需要记住的是,编译器只将已经初始化的变量放入新节中。例如,如果我从前面的代码段中删除初始化变量(如下面的代码所示),那么编译器将把该变量放入S h a r e d节以外的节中。Microsoft 的Visual C+编译器提供了一个A l l o c a t e说明符,使你可以将未经初始化的数据放入你希望的任何节中。请看下面的代码:上面的注释清楚地指明了指定的变量将被放入哪一节。若要使 A l l o c a t e声明的规则正确地起作用,那么首先必须创建节。如果删除前面这个代码中的第一行#pragma data_seg,上

22、面的代码将不进行编译。之所以将变量放入它们自己的节中,最常见的原因也许是要在.e x e或D L L文件的多个映像之间共享这些变量。按照默认设置,.e x e或D L L文件的每个映像都有它自己的一组变量。然而,可以将你想在该模块的所有映像之间共享的任何变量组合到它自己的节中去。当给变量分组时,系统并不为.e x e或D L L文件的每个映像创建新实例。仅仅告诉编译器将某些变量放入它们自己的节中,是不足以实现对这些变量的共享的。还第 1 7章内存映射文件计计403下载必须告诉链接程序,某个节中的变量是需要加以共享的。若要进行这项操作,可以使用链接程序的命令行上的/S E C T I O N开关

23、:在冒号的后面,放入你想要改变其属性的节的名字。在我们的例子中,我们想要改变S h a r e d节的属性。因此应该创建下面的链接程序开关:在逗号的后面,我们设定了需要的属性。用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开关。也可以使用下面的句法将链接程序开关嵌入你的源代码中:这一行代码告诉编译器将上面的

24、字符串嵌入名字为“.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并不鼓励你使用共享节。第一,用这种方法共享内存有可能破坏系统的安全。第二,共享变量意味着一个应用程序中的错误可能影响另一个应

25、用程序的运行,因为它没有办法防止某个应用程序将数据随机写入一个数据块。假设你编写了两个应用程序,每个应用程序都要求用户输入一个口令。然而你又决定给应用程序添加一些特性,使用户操作起来更加方便些:如果在第二个应用程序启动运行时,用户正在运行其中的一个应用程序,那么第二个应用程序就可以查看共享内存的内容,以便获得用户的口令。这样,如果程序中的某一个已经被使用,那么用户就不必重新输入他的口令。这听起来没有什么问题。毕竟没有别的应用程序而只有你自己的应用程序加载了 D L L,并且知道到什么地方去查找包含在共享节中的口令。但是,黑客正在窥视着你的行动,如果他们想要得到你的口令,只需要编写一段很短的程序

26、,加载到你的公司的 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”)显示了

27、应用程序如何能够知道每次有多少个应用程序的实例正在运行。该应用程序的源代码和资源文件位于本书所附光盘上的1 7-A p p I n s t目录下。当运行A p p I n s t程序时,就会出现它的对话框(见图1 7-4),指明该应用程序的一个实例正在运行。如果运行该应用程序的第二个实例,那么第一和第二个实例的对话框都会发生变化,以反映目前两个实例都在运行(见图 1 7-5,图1 7-6)。404计计第三部分 内 存 管 理下载图17-4 运行AppInst 时出现的对话框可以根据你的喜好,运行和撤消任意多个实例,实例的数量始终都能正确地反映在仍然保留的实例中。在靠近A p p I n s t

28、.c p p应用程序的顶部,可以看到下面的代码行:这些代码行用于创建一个称为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

29、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变量中):系统中的所有窗口将忽略这个注册窗口消息,但 A p p I n s t的各个窗口例外。当我们的各个窗口中的一个接收到该消息时,D l g _ P r o c中的代码将更新该对话框中的实例数量,以反映当前的实例数

30、量(该数量在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示例应用程序第 1 7章内存映射文件计计405下载图17-5 运行 AppInst 的第二个实例时,第一个实例对话框的变化图17-6 运行 AppInst 的第二个实例时,第二个实例对话框的变化406计计第三部分 内 存 管 理下载第 1 7章内存映射文件计计407下载408计计第三部分 内 存 管 理下载17.2 内存映射数据文件操作系统使得内存能够将一个数据文件映射到进程的地址空间中。因此,对大量的数据进行操作是非常方便的。为了理解用这种方

31、法来使用内存映射文件的功能,让我们看一看如何用 4种方法来实现一个程序,以便将文件中的所有字节的顺序进行倒序。17.2.1 方法1:一个文件,一个缓存第一种方法也是理论上最简单的方法,它需要分配足够大的内存块来存放整个文件。该文件被打开,它的内容被读入内存块,然后该文件被关闭。文件内容进入内存后,我们就可以对所有字节的顺序进行倒序,方法是将第一个字节倒腾为最后一个字节,第二个字节倒腾为倒数第二个字节,依次类推。这个倒腾操作将一直进行下去直到文件的中间位置。当所有的字节都已经倒腾之后,就可以重新打开该文件,并用内存块的内容来改写它的内容。这种方法实现起来非常容易,但是它有两个缺点。首先,必须分配

32、一个与文件大小相同的内存块。如果文件比较小,那么这没有什么问题。但是如果文件非常大,比如说有 2 G B大,那该怎么办呢?一个3 2位的系统不允许应用程序提交那么大的物理内存块。因此大文件需要使用不同的方法。第二,如果进程在运行过程的中间被中断,也就是说当倒序后的字节被重新写入该文件时进程被中断,那么文件的内容就会遭到破坏。防止出现这种情况的最简单的方法是在对它的内容进行倒序之前先制作一个原始文件的拷贝。如果整个进程运行成功,那么可以删除该文件的拷贝。这种方法需要更多的磁盘空间。17.2.2 方法2:两个文件,一个缓存在第二种方法中,你打开现有的文件,并且在磁盘上创建一个长度为 0的新文件。然

33、后分第 1 7章内存映射文件计计409下载配一个比较小的内部缓存,比如说 8 KB。你找到离原始文件结尾还有 8 KB的位置,将这最后的8 KB读入缓存,将字节倒序,再将缓存中的内容写入新创建的文件。这个寻找、读入、倒序和写入的操作过程要反复进行,直到到达原始文件的开头。如果文件的长度不是 8 KB的倍数,那么必须进行某些特殊的处理。当原始文件完全处理完毕之后,将原始文件和新文件关闭,并删除原始文件。这种方法实现起来比第一种方法要复杂一些。它对内存的使用效率要高得多,因为它只需要分配一个8 KB的缓存块,但是它存在两个大问题。首先,它的处理速度比第一种方法要慢,原因是在每个循环操作过程中,在执

34、行读入操作之前,必须对原始文件进行寻找操作。第二,这种方法可能要使用大量的硬盘空间。如果原始文件是 400 MB,那么随着进程的不断运行,新文件就会增大为400 MB。在原始文件被删除之前,两个文件总共需要占用 800 MB的磁盘空间。这比应该需要的空间大400 MB。由于存在这个缺点,因此引来了下一个方法。17.2.3 方法3:一个文件,两个缓存如果使用这个方法,那么我们假设程序初始化时分配了两个独立的 8 KB缓存。程序将文件的第一个8 KB读入一个缓存,再将文件的第二个8 KB 读入另一个缓存。然后进程将两个缓存的内容进行倒序,并将第一个缓存的内容写回文件的结尾处,将第二个缓存的内容写回

35、同一个文件的开始处。每个迭代操作不断进行(以8 KB为单位,从文件的开始和结尾处移动文件块)。如果文件的长度不是16 KB的倍数,并且有两个8 KB的文件块相重叠,那么就需要进行一些特殊的处理。这种特殊处理比上一种方法中的特殊处理更加复杂,不过这难不倒经验丰富的编程员。与前面的两种方法相比,这种方法在节省硬盘空间方面有它的优点。由于所有内容都是从同一个文件读取并写入同一个文件,因此不需要增加额外的磁盘空间,至于内存的使用,这种方法也不错,它只需要使用16 KB的内存。当然,这种方法也许是最难实现的方法。与第一种方法一样,如果进程被中断,本方法会导致数据文件被破坏。下面让我们来看一看如何使用内存

36、映射文件来完成这个过程。17.2.4 方法4:一个文件,零缓存当使用内存映射文件对文件内容进行倒序时,你打开该文件,然后告诉系统将虚拟地址空间的一个区域进行倒序。你告诉系统将文件的第一个字节映射到该保留区域的第一个字节。然后可以访问该虚拟内存的区域,就像它包含了这个文件一样。实际上,如果在文件的结尾处有一个单个0字节,那么只需要调用C运行期函数_ s t r r e v,就可以对文件中的数据进行倒序操作。这种方法的最大优点是,系统能够为你管理所有的文件缓存操作。不必分配任何内存,或者将文件数据加载到内存,也不必将数据重新写入该文件,或者释放任何内存块。但是,内存映射文件仍然可能出现因为电源故障

37、之类的进程中断而造成数据被破坏的问题。17.3 使用内存映射文件若要使用内存映射文件,必须执行下列操作步骤:1)创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件。2)创建一个文件映射内核对象,告诉系统该文件的大小和你打算如何访问该文件。3)让系统将文件映射对象的全部或一部分映射到你的进程地址空间中。当完成对内存映射文件的使用时,必须执行下面这些步骤将它清除:410计计第三部分 内 存 管 理下载1)告诉系统从你的进程的地址空间中撤消文件映射内核对象的映像。2)关闭文件映射内核对象。3)关闭文件内核对象。下面将详细介绍这些操作步骤。17.3.1 步骤1:创建或打开文件内

38、核对象若要创建或打开一个文件内核对象,总是要调用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个值中的一个。表17-3

39、 dwDesiredAccess的值值含义0不能读取或写入文件的内容。当只想获得文件的属性时,请设定 0G 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_RE

40、AD|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_REA

41、D 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。第 1 7章内存映射文件计计411下载注意能够返回句柄的大多数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:创建一个文件映射内

42、核对象调用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结构的指针,通

43、常传递的值是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参数使你能够设定这

44、些保护属性。大多数情况下,可以设定表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 ePA G E _ W R I T E C O P

45、 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 eWindows 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,这将告诉系统从页文件中提交存储器。该页文件存储器是为数据文件的数据拷贝保留的,只有修改过的页面才被写入页文件。你对该文件的数据所作的任何修改都不会重新填入原始数据文件。其

46、最终结果是,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参数中。节只是用于内存映射的另一个术语。412计计第三部分 内 存 管 理下载节的第一个保护属性是 S E C _ N O C A C H E,它告诉系统,没有将文件的任何内存映射页面放入高速缓存。因此,当将数据写入该文件时,系统将更加经常地更新磁盘上的文件数据。这个标志与

47、PA G E _ N O C A C H E保护属性标志一样,是供设备驱动程序开发人员使用的,应用程序通常不使用。Windows 98Windows 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文 件的 数据

48、节(.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 98Windows 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

49、 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参数用于

50、设定较高的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。如果打算将数据附加给该文件,可以选择最大的文件大小,以便为你留出一些富裕的空间。如果当前磁

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 技术资料 > 其他杂项

本站为文档C TO C交易模式,本站只提供存储空间、用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。本站仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知得利文库网,我们立即给予删除!客服QQ:136780468 微信:18945177775 电话:18904686070

工信部备案号:黑ICP备15003705号-8 |  经营许可证:黑B2-20190332号 |   黑公网安备:91230400333293403D

© 2020-2023 www.deliwenku.com 得利文库. All Rights Reserved 黑龙江转换宝科技有限公司 

黑龙江省互联网违法和不良信息举报
举报电话:0468-3380021 邮箱:hgswwxb@163.com