与虚拟内存相似,内存映射文件允许开发人员预定一块地址空间区域并给区域调拨物理存储器。不同之处在于,内存映射文件的物理存储器来自磁盘上已有的文件,而不是来自系统的页交换文件。一旦把文件映射到地址空间,我们就可以对它进行访问,就好像整个文件已经被载入内存一样
内存映射文件可以用于以下3种情况:
系统使用内存映射文件来载入并运行.exe和DLL文件。这大量节省了页交换文件的空间以及应用程序启动的时间
使用内存映射文件来访问磁盘中的数据。这使得我们可以避免直接对文件进行I/0操作和对文件内容进行缓存
通过使用内存映射文件,我们可以在同一台机器的不同进程之间共享数据。Windows提供的所有进程间共享数据的方法都是通过内存映射文件来实现的。在同一台机器的不同进程之间共享数据的最高效方法是内存映射文件
当调用CreateProcess时,系统将执行以下步骤:
系统会先确定CreateProcess所指定的可执行文件所在的位置。如果无法找到.exe文件,那么系统将不会创建进程,这时CreateProcess会返回FALSE
系统创建一个新的进程内核对象
系统为这个新进程创建一个私有地址空间
系统预定一块足够大的地址空间来容纳.exe文件。这个区域的基地址在.exe文件中设定,默认为0x00400000(64位Windows下的64位应用程序可能会不同),该地址可在构建应用程序的.exe文件时使用/BASE链接器指定
系统会对地址空间进行标注,表明该区域的后备物理存储器来自磁盘的.exe文件,而并非来自系统页交换文件
在将.exe文件映射到进程的地址空间之后,系统会访问.exe文件中的一个段,这个段列出了一些DLL文件,它们包含该.exe文件调用到的函数;然后系统会调用LoadLibrary来载入每个DLL,如果哪个DLL需要用到其他DLL,那么系统同样会调用LoadLibrary来载入相应的DLL
每次调用LoadLibrary加载一个DLL,系统会执行下列步骤:
如果DLL不包含重定位信息,就意味着DLL“必须”被载入到指定的基地址,否则它将无法被载入
若系统对DLL执行重定位操作。重定位不仅需要占用页交换文件的额外的存储空间,而且会增加载入DLL所需的时间
预定一块足够大的地址空间区域来容纳DLL文件。待预定的地址空间区域的具体位置已经在DLL文件中指定。只需在构建DLL文件时使用/BASE链接器开关,我们就可以指定一个不同的基地址。所有与Windows一起发布的系统DLL都有不同的基地址,这样即使把他们载入到同一个地址空间,也不会发生重叠
如果系统无法在DLL文件指定的基地址处预定区域,这可能是因为该区域已经被另一个DLL或.exe占用,也可能是因为区域不够大,这时系统会尝试在另一个地址来为DLL预定地址空间区域
如果系统无法将DLL载入到指定的基地址,那么有两个麻烦点:
系统会对地址空间区域进行标注,表明该区域的后备物理存储器来自磁盘上的DLL文件,而并非来自系统的页交换文件
同一个可执行文件或DLL的多个实例不会共享静态数据:
如果一个应用程序已经在运行,那么当我们为这个应用程序创建一个新进程时,系统只不过是打开另一个内存映射视图,创建一个新的进程对象,并(为主线程)创建一个新的线程对象。这个新打开的内存映射视图隶属一个文件映射对象。系统同时给进程对象和线程对象分别制定新的进程ID和线程ID。通过使用内存映射文件,同一个应用程序的多个实例可以共享内存中的代码和数据
由1可知,如果应用程序的一个实例修改了数据页面中的一些全局变量,那么应用程序所有实例的内存都会被修改。这种类型的修改将导致灾难性的结果,因此必须避免。系统通过内存管理系统的写时复制(copy-on-write)特性来防止这种情况的发生。任何时候当应用程序试图写入内存映射文件的时候,系统会首先截获此类尝试,接着为应用程序试图写入的内存页面分配一块新的内存,然后复制页面内容,最后让应用程序写入到刚分配的内存块
当系统创建一个进程时,会检查文件映像的所有页面。对那些通常需要用写时复制属性进行保护的页面,系统会立即从页交换文件中调拨存储器。但系统只是调拨这些页面,而不会实际载入页面的内容。当程序访问到文件映像中的一个页面时,系统会载入相应的页面。如果该页面从未修改过,那么可以舍弃其中的内容并在需要的时重新载入。但如果文件映像的该页面被修改过,那么系统必须把修改过的页面调换到页交换文件中
在同一个可执行文件或DLL的多个实例之间共享静态数据:
在安全的默认设置下,同一个DLL或.exe的多个映像不能共享全局数据和静态数据
每个.exe或DLL文件的映像都由许多段组成
每个段都有一些与之相关联的属性
用DumpBin可以查看.exe或DLL映射文件的段列表
创建自己的段的方法: #pragma data_seg("my_section")LONG g_count = 0;#pragma data_seg()
以上代码创建了my_section段,并在该段中放入一个已初始化的变量 #pragma data_seg("my_section")
LONG g_count; #pragma data_seg()
以上代码创建了my_section段,但变量g_count并未放入my_section段中;因为编译器指示将已初始化的变量放入自定义段中,未初始化的变量将放入my_section以外的段中
VC++编译器提供了一个allocate说明符,可以使用这个说明符,将未初始化的数据放入任何段中,如 #pragma data_seg("my_section")
#pragma data_seg()
__declspec(allocate("my_section")) int c = 0; // 将已初始化的c放入my_section中
__declspec(allocate("my_section")) int d; // 将未初始化的d放入my_section中
这样做之前必须先声明"my_section"段,否则不会编译这段代码
仅仅让编译器将某些变量放入自己的段中,还不能够实现对这些变量的共享,若要共享这些变量,还必须告诉链接程序,哪个段的变量是共享的:
方法1:使用链接程序的命令行的/SECTION开关 /SECTION:Shared,RWS // R:READ W:WRITE E:EXECUTE S:SHARE
方法2: #pragma comment(linker, "/SECTION:Shared, RWS")
不建议使用共享段: