17内存映射文件.docx
《17内存映射文件.docx》由会员分享,可在线阅读,更多相关《17内存映射文件.docx(77页珍藏版)》请在冰豆网上搜索。
![17内存映射文件.docx](https://file1.bdocx.com/fileroot1/2022-11/25/0ffc7f61-be4b-4b61-b33c-3ab5ba834af6/0ffc7f61-be4b-4b61-b33c-3ab5ba834af61.gif)
17内存映射文件
第17章内存映射文件
对文件进行操作几乎是所有应用程序都必须进行的,并且这常常是人们争论的一个问题。
应用程序究竟是应该打开文件,读取文件并关闭文件,还是打开文件,然后使用一种缓冲算法,从文件的各个不同部分进行读取和写入呢?
Microsoft提供了一种两全其美的方法,那就是内存映射文件。
与虚拟内存一样,内存映射文件可以用来保留一个地址空间的区域,并将物理存储器提交给该区域。
它们之间的差别是,物理存储器来自一个已经位于磁盘上的文件,而不是系统的页文件。
一旦该文件被映射,就可以访问它,就像整个文件已经加载内存一样。
内存映射文件可以用于3个不同的目的:
•系统使用内存映射文件,以便加载和执行.exe和DLL文件。
这可以大大节省页文件空间和应用程序启动运行所需的时间。
•可以使用内存映射文件来访问磁盘上的数据文件。
这使你可以不必对文件执行I/O操作,并且可以不必对文件内容进行缓存。
•可以使用内存映射文件,使同一台计算机上运行的多个进程能够相互之间共享数据。
Windows确实提供了其他一些方法,以便在进程之间进行数据通信,但是这些方法都是使用内存映射文件来实现的,这使得内存映射文件成为单个计算机上的多个进程互相进行通信的最有效的方法。
本章将要介绍内存映射文件的各种使用方法。
17.1内存映射的可执行文件和DLL文件
当线程调用CreateProcess时,系统将执行下列操作步骤:
1)系统找出在调用CreateProcess时设定的.exe文件。
如果找不到这个.exe文件,进程将无法创建,CreateProcess将返回FALSE。
2)系统创建一个新进程内核对象。
3)系统为这个新进程创建一个私有地址空间。
4)系统保留一个足够大的地址空间区域,用于存放该.exe文件。
该区域需要的位置在.exe文件本身中设定。
按照默认设置,.exe文件的基地址是0x00400000(这个地址可能不同于在64位Windows2000上运行的64位应用程序的地址),但是,可以在创建应用程序的.exe文件时重载这个地址,方法是在链接应用程序时使用链接程序的/BASE选项。
5)系统注意到支持已保留区域的物理存储器是在磁盘上的.exe文件中,而不是在系统的页文件中。
当.exe文件被映射到进程的地址空间中之后,系统将访问.exe文件的一个部分,该部分列出了包含.exe文件中的代码要调用的函数的DLL文件。
然后,系统为每个DLL文件调用LoadLibrary函数,如果任何一个DLL需要更多的DLL,那么系统将调用LoadLibrary函数,以便加载这些DLL。
每当调用LoadLibrary来加载一个DLL时,系统将执行下列操作步骤,它们均类似上面的第4和第5个步骤:
1)系统保留一个足够大的地址空间区域,用于存放该DLL文件。
该区域需要的位置在DLL文件本身中设定。
按照默认设置,Microsoft的VisualC++建立的DLL文件基地址是0x10000000(这个地址可能不同于在64位Windows2000上运行的64位DLL的地址)但是,你可以在创建DLL文件时重载这个地址,方法是使用链接程序的/BASE选项。
Windows提供的所有标准系统DLL都拥有不同的基地址,这样,如果加载到单个地址空间,它们就不会重叠。
2)如果系统无法在该DLL的首选基地址上保留一个区域,其原因可能是该区域已经被另一个DLL或.exe占用,也可能是因为该区域不够大,此时系统将设法寻找另一个地址空间的区域来保留该DLL。
如果一个DLL无法加载到它的首选基地址,这将是非常不利的,原因有二。
首先,如果系统没有再定位信息,它就无法加载该DLL(可以在DLL创建时,使用链接程序的/FIXED开关,从DLL中删除再定位信息,这能够使DLL变得比较小,但是这也意味着该DLL必须加载到它的首选地址中,否则它就根本无法加载)。
第二,系统必须在DLL中执行某些再定位操作。
在Windows98中,系统可以在页面被转入RAM时执行再定位操作。
在Windows2000中,这些再定位操作需要由系统的页文件提供更多的存储器,它们也增加了加载DLL所需要的时间量。
3)系统会注意到支持已保留区域的物理存储器位于磁盘上的DLL文件中,而不是在系统的页文件中。
如果由DLL无法加载到它的首选基地址,Windows2000必须执行再定位操作,那么系统也将注意到DLL的某些物理存储器已经被映射到页文件中。
如果由于某个原因系统无法映射.exe和所有必要的DLL文件,那么系统就会向用户显示一个消息框,并且释放进程的地址空间和进程对象。
CreateProcess函数将向调用者返回FALSE,调用者可以调用GetLastError函数,以便更好地了解为什么无法创建该进程。
当所有的.exe和DLL文件都被映射到进程的地址空间之后,系统就可以开始执行.exe文件的启动代码。
当.exe文件被映射后,系统将负责所有的分页、缓冲和高速缓存的处理。
例如,如果.exe文件中的代码使它跳到一个尚未加载到内存的指令地址,那么就会出现一个错误。
系统能够发现这个错误,并且自动将这页代码从该文件的映像加载到一个RAM页面。
然后,系统将这个RAM页面映射到进程的地址空间中的相应位置,并且让线程继续运行,就像这页代码已经加载了一样。
当然,这一切是应用程序看不见的。
当进程中的线程每次试图访问尚未加载到RAM的代码或数据时,该进程就会重复执行。
17.1.1可执行文件或DLL的多个实例不能共享静态数据
当为正在运行的应用程序创建新进程时,系统将打开用于标识可执行文件映像的文件映射对象的另一个内存映射视图,并创建一个新进程对象和(为主线程创建)一个新线程对象。
系统还要将新的进程ID和线程ID赋予这些对象。
通过使用内存映射文件,同一个应用程序的多个正在运行的实例就能够共享RAM中的相同代码和数据。
这里有一个小问题需要注意。
进程使用的是一个平面地址空间。
当编译和链接你的程序时,所有的代码和数据都被合并在一起,组成一个很大的结构。
数据与代码被分开,但仅限于跟在.exe文件中的代码后面的数据而已。
(实际上,文件的内容被分割为不同的节。
代码放在一个节中,全局变量放在另一个节中。
各个节按照页面边界来对齐。
通过调用GetSystemInfo函数,应用程序可以确定正在使用的页面的大小。
在.exe或DLL文件中,代码节通常位于数据数据节的前面。
)图17-1简单说明了应用程序的代码和数据究竟是如何加载到虚拟内存中,然后又被映射到应用程序的地址空间中的。
作为一个例子,假设应用程序的第二个实例正在运行。
系统只是将包含文件的代码和数据的虚拟内存页面映射到第二个应用程序的地址空间,如图17-2所示。
如果应用程序的一个实例改变了驻留在数据页面中的某些全局变量,那么该应用程序的所有实例的内存内容都将改变。
这种类型的改变可能带来灾难性的后果,因此是决不允许的。
系统运用内存管理系统的copy-on-write(写入时拷贝)特性来防止进行这种改变。
每当应用程序尝试将数据写入它的内存映射文件时,系统就会抓住这种尝试,为包含应用程序尝试写入数据的内存页面分配一个新内存块,再拷贝该页面的内容,并允许该应用程序将数据写入这个新分配的内存块。
结果,同一个应用程序的所有其他实例的运行都不会受到影响。
图17-3显示了当应用程序的第一个实例尝试改变数据页面2时出现的情况。
图17-1应用程序的代码和数据加载及映射示意图
图17-2应用程序与虚拟内存地址空间之间的关系示意图
图17-3应用程序的第一个实例尝试改变数据页面2时的情况
系统分配一个新的虚拟内存页面,并且将数据页面2的内容拷贝到新页面中。
第一个实例的地址空间发生了变更,这样,新数据页面就被映射到与原始地址页面相同位置上的地址空间中。
这时系统就可以让进程修改全局变量,而不必担心改变同一个应用程序的另一个实例的数据。
当应用程序被调试时,将会发生类似的事件。
比如说,你正在运行一个应用程序的多个实例,并且只想调试其中的一个实例。
你访问调试程序,在一行源代码中设置一个断点。
调试程序修改了你的代码,将你的一个汇编语言指令改为能使调试程序自行激活的指令。
因此你再次遇到了同样的问题。
当调试程序修改代码时,它将导致应用程序的所有实例在修改后的汇编语言指令运行时激活该调试程序。
为了解决这个问题,系统再次使用copy-on-write内存。
当系统发现调试程序试图修改代码时,它就分配一个新内存块,将包含该指令的页面拷贝到新的内存页面中,并且允许调试程序修改页面拷贝中的代码。
Windows98当一个进程被加载时,系统要查看文件映像的所有页面。
系统立即为通常用copy-on-write属性保护的那些页面提交页文件中的存储器。
这些页面只是被提交而已,它们并不被访问。
当文件映像中的页面被访问时,系统就加载相应的页面。
如果该页面从来没有被修改,它就可以从内存中删除,并在必要时重新加载。
但是,如果文件的页面被修改了,系统就将修改过的页面转到页文件中以前被提交的页面之一。
Windows2000与Windows98之间的行为特性的唯一差别,是在你加载一个模块的两个拷贝并且可写入的数据尚未被修改的时候显示出来的。
在这种情况下,在Windows2000下运行的进程能够共享数据,而在Windows98下,每个进程都可以得到它自己的数据拷贝。
如果只加载模块的一个拷贝,或者可写入的数据已经被修改(这是通常的情况),那么Windows2000与Windows98的行为特性是完全相同的。
17.1.2在可执行文件或DLL的多个实例之间共享静态数据
全局数据和静态数据不能被同一个.exe或DLL文件的多个映像共享,这是个安全的默认设置。
但是,在某些情况下,让一个.exe文件的多个映像共享一个变量的实例是非常有用和方便的。
例如,Windows没有提供任何简便的方法来确定用户是否在运行应用程序的多个实例。
但是,如果能够让所有实例共享单个全局变量,那么这个全局变量就能够反映正在运行的实例的数量。
当用户启动应用程序的一个实例时,新实例的线程能够简单地查看全局变量的值(它已经被另一个实例更新);如果这个数量大于1,那么第二个实例就能够通知用户,该应用程序只有一个实例可以运行,而第二个实例将终止运行。
本节将介绍一种方法,它允许你共享.exe或DLL文件的所有实例的变量。
不过在介绍这个方法之前,首先让我们介绍一些背景知识。
每个.exe或DLL文件的映像都由许多节组成。
按照规定,每个标准节的名字均以圆点开头。
例如,当编译你的程序时,编译器会将所有代码放入一个名叫.text的节中。
该编译器还将所有未经初始化的数据放入一个.bss节,而已经初始化的所有数据则放入.data节中。
每一节都拥有与其相关的一组属性,这些属性如表17-1所示。
表17-1.exe或DLL文件各节的属性
属性
含义
READ
该节中的字节可以读取
WRITE
该节中的字节可以写入
EXECUTE
该节中的字节可以执行
SHARED
该节中的字节可以被多个实例共享(本属性能够有效地关闭copy-on-write机制)
使用Microsoft的VisualStudio的DumpBin实用程序(带有/Headers开关),可以查看.exe或DLL映射文件中各个节的列表。
下面选录的代码是在一个可执行文件上运行DumpBin程序而生成的:
SECTIONHEADER#1
.textname
11A70virtualsize
1000virtualaddress
12000sizeofrawdata
1000filepointertorawdata
0filepointertorelocationtable
0filepointertolinenumbers
0numberofrelocations
0numberoflinenumbers
60000020flags
Code
ExecuteRead
SECTIONHEADER#2
.rdataname
1F6virtualsize
13000virtualaddress
1000sizeofrawdata
13000filepointertorawdata
0filepointertorelocationtable
0filepointertolinenumbers
0numberofrelocations
0numberoflinenumbers
40000040flags
InitializedData
ReadOnly
SECTIONHEADER#3
.dataname
560virtualsize
14000virtualaddress
1000sizeofrawdata
14000filepointertorawdata
0filepointertorelocationtable
0filepointertolinenumbers
0numberofrelocations
0numberoflinenumbers
C0000040flags
InitializedData
ReadWrite
SECTIONHEADER#4
.idataname
58Dvirtualsize
15000virtualaddress
1000sizeofrawdata
15000filepointertorawdata
0filepointertorelocationtable
0filepointertolinenumbers
0numberofrelocations
0numberoflinenumbers
C0000040flags
InitializedData
ReadWrite
SECTIONHEADER#5
.didatname
7A2virtualsize
16000virtualaddress
1000sizeofrawdata
16000filepointertorawdata
0filepointertorelocationtable
0filepointertolinenumbers
0numberofrelocations
0numberoflinenumbers
C0000040flags
InitializedData
ReadWrite
SECTIONHEADER#6
.relocname
26Dvirtualsize
17000virtualaddress
1000sizeofrawdata
17000filepointertorawdata
0filepointertorelocationtable
0filepointertolinenumbers
0numberofrelocations
0numberoflinenumbers
42000040flags
InitializedData
Discardable
ReadOnly
Summary
1000.data
1000.didat
1000.idata
1000.rdata
1000.reloc
12000.text
表17-2显示了比较常见的一些节的名字,并且说明了每一节的作用。
除了编译器和链接程序创建的标准节外,也可以在使用下面的命令进行编译时创建自己的节:
表17-2常见的节名及作用
节名
作用
.bss
未经初始化的数据
.CRT
C运行期只读数据
.data
已经初始化的数据
.debug
调试信息
.didata
延迟输入文件名表
.edata
输出文件名表
.idata
输入文件名表
.rdata
运行期只读数据
.reloc
重定位表信息
.rsrc
资源
.text
.exe或DLL文件的代码
.tls
线程的本地存储器
.xdata
异常处理表
#pragmadata_seg("sectionname")
我可以创建一个称为“Shared”的节,它包含单个LONG值,如下所示:
#pragmadata_seg("Shared")
LONGg_lInstanceCount=0;
#pragmadata_seg()
当编译器对这个代码进行编译时,它创建一个新节,称为Shared,并将它在编译指示后面看到的所有已经初始化(initialized)的数据变量放入这个新节中。
在上面这个例子中,变量放入Shared节中。
该变量后面的#pragmadataseg()一行告诉编译器停止将已经初始化的变量放入Shared节,并且开始将它们放回到默认数据节中。
需要记住的是,编译器只将已经初始化的变量放入新节中。
例如,如果我从前面的代码段中删除初始化变量(如下面的代码所示),那么编译器将把该变量放入Shared节以外的节中。
#pragmadata_seg("Shared")
LONGg_lInstanceCount;
#pragmadata_seg()
Microsoft的VisualC++编译器提供了一个Allocate说明符,使你可以将未经初始化的数据放入你希望的任何节中。
请看下面的代码:
//CreateSharedsection&havecompilerplaceinitializeddatainit.
#pragmadata_seg("Shared")
//Initialized,inSharedsection
inta=0;
//Uninitialized,notinSharedsection
intb;
//HavecompilerstopplacinginitializeddatainSharedsection.
#pragmadata_seg()
//Initialized,inSharedsection
__declspec(allocate("Shared"))intc=0;
//Uninitialized,inSharedsection
__declspec(allocate("Shared"))intd;
//Initialized,notinSharedsection
inte=0;
//Uninitialized,notinSharedsection
intf;
上面的注释清楚地指明了指定的变量将被放入哪一节。
若要使Allocate声明的规则正确地起作用,那么首先必须创建节。
如果删除前面这个代码中的第一行#pragmadata_seg,上面的代码将不进行编译。
之所以将变量放入它们自己的节中,最常见的原因也许是要在.exe或DLL文件的多个映像之间共享这些变量。
按照默认设置,.exe或DLL文件的每个映像都有它自己的一组变量。
然而,可以将你想在该模块的所有映像之间共享的任何变量组合到它自己的节中去。
当给变量分组时,系统并不为.exe或DLL文件的每个映像创建新实例。
仅仅告诉编译器将某些变量放入它们自己的节中,是不足以实现对这些变量的共享的。
还必须告诉链接程序,某个节中的变量是需要加以共享的。
若要进行这项操作,可以使用链接程序的命令行上的/SECTION开关:
/SECTION:
name,attributes
在冒号的后面,放入你想要改变其属性的节的名字。
在我们的例子中,我们想要改变Shared节的属性。
因此应该创建下面的链接程序开关:
/SECTION:
Shared,RWS
在逗号的后面,我们设定了需要的属性。
用R代表READ,W代表WEITE,E代表EXECUTE,S代表SHARED。
上面的开关用于指明位于Shared节中的数据是可以读取、写入和共享的数据。
如果想要改变多个节的属性,必须多次设定/SECTION开关,也就是为你要改变属性的每个节设定一个/SECTION开关。
也可以使用下面的句法将链接程序开关嵌入你的源代码中:
#pragmacomment(linker,"/SECTION:
Shared,RWS")
这一行代码告诉编译器将上面的字符串嵌入名字为“.drectve”的节。
当链接程序将所有的.obj模块组合在一起时,链接程序就要查看每个.obj模块的“.drectve”节,并且规定所有的字符串均作为命令行参数传递给该链接程序。
我一直使用这种方法,因为它非常方便。
如果将源代码文件移植到一个新项目中,不必记住在VisualC++的ProjectSettings(项目设置)对话框中设置链接程序开关。
虽然可以创建共享节,但是,由于两个原因,Microsoft并不鼓励你使用共享节。
第一,用这种方法共享内存有可能破坏系统的安全。
第二,共享变量意味着一个应用程序中的错误可能影响另一个应用程序的运行,因为它没有办法防止某个应用程序将数据随机写入一个数据块。
假设你编写了两个应用程序,每个应用程序都要求用户输入一个口令。
然而你又决定给应用程序添加一些特性,使用户操作起来更加方便些:
如果在第二个应用程序启动运行时,用户正在运行其中的一个应用程序,那么第二个应用程序就可以查看共享内存的内容,以便获得用户的口令。
这样,如果程序中的某一个已经被使用,那么用户就不必重新输入他的口令。
这听起来没有什么问题。
毕竟没