《Windows核心编程》读书笔记十七章 内存映射文件

第十七章 内存映射文件

本章内容

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

17.2 映射到内存的数据文件

17.3 使用内存映射文件

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

17.5 内存映射文件和一致性

17.6 给内存映射文件制定基地址

17.7 内存映射文件的实现细节

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

17.9 以页交换文件为后备存储器的内存映射文件

17.10 稀疏调拨的内存映射文件


内存映射文件允许开发人员预定一块地址空间区域并给区域调拨物理存储器。内存映射文件的物理存储器来自磁盘上已有的文件,而不是来自系统的页交换文件。一旦把文件映射到地址空间,就可以对它进行访问,好像整个文件已经被载入内存一样。


内存映射文件主要用于以下三种情况:

1) 系统使用内存映射文件来载入并运行exe和动态链接库(DLL)文件。这大量节省了页交换文件的空间以及应用程序的启动时间。

2)开发人员可以用内存映射文件来访问磁盘上的数据文件。避免直接对文件进行IO操作和对文件内容进行缓存。

3)通过内存映射文件,可以在同一台机器的不同进程之间共享数据。(系统虽然有提供其他方法,但内存映射文件是最高效的方法)


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

当一个线程调用了CreateProcess的时候,系统会执行以下步骤。

1)系统会确定CreateProcess所指定exe所在的位置,若无法找到exe系统不会创建进程,CreateProcess返回FALSE.

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

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

4)系统预定一块足够大的地址空间来容纳exe文件。待预定的地址空间区域的具体位置已经在exe文件中指定。默认情况下exe文件的基地址是0x00400000(在64位windows下的64位程序来说可能会不同)但是若在链接时使用/BASE连接器开关,就可以自己给应用程序指定不同的地址。

5)系统会对地址空间区域进行标注,表明该区域的后备物理存储器来自磁盘上的exe文件,而并非来自系统的页交换文件。


系统将exe映射到进程地址空间以后会访问exe文件中的一个段,这个段列出了一些DLL文件,包含了exe文件调用到的函数。然后系统会调用LoadLibrary来加载入每一个DLL,如果DLL还需要调用到其他DLL,系统同样会使用LoadLibrary来载入。载入每个DLL的时候执行的步骤类似前面的4)5)

1)系统会预定一块足够大的地址空间区域来容纳DLL文件。通常区域的具体位置在DLL文件中已经指定。 默认连接器设置DLL的基地址是0x10000000 x64平台的DLL基地址是0x00400000. 也可以使用/BASE连接器开关来设定。与系统一起发布的DLL都具有不同的基地址,这样不会发生重叠。


2)如果系统无法在DLL文件指定的基地址出预定区域(可能是因为该区域已经被别的DLL或exe占用,也可能是区域不够大),系统会尝试在另一个地址为DLL预定地址空间。如果DLL不包含重定位信息,那么系统将无法加载DLL(通常是使用了/FIXED开关构建DLL,会去除DLL的重定位信息)如果系统对DLL执行重定位操作,会需要占用页交换文件中额外的存储空间,也会增加载入DLL所需要的时间。


3)系统会对地址空间区域进行标注,表明该区域的后备物理存储器来自磁盘上的DLL文件,而非来自系统的页交换文件。如果Windows不能将DLL载入到指定的基地址而必须重定位的话,系统还会另外进行标注,表明DLL中有一部分存储器被映射到了页交换文件。


如果因为某些原因无法将exe文件和所需DLL映射到地址空间区域,系统会先给用户显示一个对话框,然后释放进程地址空间和进程内存对象。CreateProcess返回FALSE。


所有exe文件和DLL文件都映射到进程地址空间以后,系统会开始执行exe文件的启动代码。当完成对exe文件的映射以后,系统会负责所有换页(paging),缓存(buffering)以及高速缓存(caching)操作。


17.1.1 同一个执行文件或DLL的多个实例不会共享静态数据

如果一个应用程序已经在运行,那么当我们为这个应用程序创建一个新的进程时,系统只不过是打开另一个内存映射视图(memory-mapped view),创建一个新的进程内核对象,并(主线程)创建一个新的线程对象。

这个新打开的内存映射视图隶属一个文件映射对象(file-mapping object),后者用来标识可执行文件的映像。

系统同时给进程对象和线程对象分别制定新的进程ID(process id)和线程ID(thread id)。

通过内存映像文件,同一个应用程序的多个实例可以共享内存中的代码和数据。


下图是一个简单的视图,描绘了如何把应用程序的代码和数据载入虚拟内存,并将它们映射到地址空间中。

这就是所谓的内存映射图(memory-mapped view)

如果第二个实例现在开始运行。。系统只不过把包含应用程序代码和数据的虚拟内存页面映射到第二个实例的地址空间中。





如果应用程序的一个实例修改了数据页面中的一些全局变量,那么应用程序的所有实例的内存都会被修改。由于这种类型的修改可能会导致灾难性的后宫,因此必须避免。


系统通过内存管理系统的写时复制(copy-on-write)特性来防止这种情况发生。任何时候当应用程序试图写入内存映射文件的时候,系统会首先截获此类尝试(PAGE_GUARD),接着为应用程序试图写入的内存页面分配一块新的内存,然后复制页面内容,最后让应用程序写入到刚分配的内存块。最终结果就是,应用程序的其他实例不会受到任何影响。

下图描绘了第一个实例试图修改数据页面2的一个全局变量时,会产生怎样的结果。



系统先分配一页新的虚拟内存(新页面)然后把数据页面2中的内容复制到新页面中。并更新第一个实例的地址空间,这样新数据就会和原始数据页面一样,映射到进程地址空间的同一位置。这样系统运行让进程修改全局变量的值也不用担心会修改到同一个应用程序的其他实例的数据了。


调试器也采用了类似的技术,增加断点会导致调试器修改代码。写时复制保证只影响被调试的进程而不影响其他正常运行的实例。



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

有时候需要在应用程序的多个实例间共享全局或静态数据。例如应用程序可以用这种方法来统计已经运行的实例。

每个exe或dll文件映像由许多段组成。每个标准的段名都以点号开始。例如编译程序的时候,编译器会把代码放在一个叫.text的段中。

编译器会把未经初始化的数据放在.bss段中,将已经初始化的数据放在.data段中。


每个段都有一个与之关联的属性,如图所示。


可以用Visual Studio的DumpBin工具(需要指定/Headers开关)来查看.exe或DLL映像文件的各个段。

例如以下内容载入dumpbin /Headers  所获取的一个exe的段内容。

c:\program files (x86)\microsoft visual studio 12.0\vc\bin>dumpbin /Headers d:\t
est\ConsoleApplication3.exe
Microsoft (R) COFF/PE Dumper Version 12.00.40629.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file d:\test\ConsoleApplication3.exe

PE signature found

File Type: EXECUTABLE IMAGE

FILE HEADER VALUES
             14C machine (x86)
               5 number of sections
        59E02D5C time date stamp Fri Oct 13 11:05:00 2017
               0 file pointer to symbol table
               0 number of symbols
              E0 size of optional header
             102 characteristics
                   Executable
                   32 bit word machine

OPTIONAL HEADER VALUES
             10B magic # (PE32)
           12.00 linker version
             A00 size of code
            1000 size of initialized data
               0 size of uninitialized data
            13EA entry point (004013EA) _wmainCRTStartup
            1000 base of code
            2000 base of data
          400000 image base (00400000 to 00405FFF)
            1000 section alignment
             200 file alignment
            6.00 operating system version
            0.00 image version
            6.00 subsystem version
               0 Win32 version
            6000 size of image
             400 size of headers
               0 checksum
               3 subsystem (Windows CUI)
            8140 DLL characteristics
                   Dynamic base
                   NX compatible
                   Terminal Server Aware
          100000 size of stack reserve
            1000 size of stack commit
          100000 size of heap reserve
            1000 size of heap commit
               0 loader flags
              10 number of directories
               0 [       0] RVA [size] of Export Directory
            2294 [      3C] RVA [size] of Import Directory
            4000 [     1E0] RVA [size] of Resource Directory
               0 [       0] RVA [size] of Exception Directory
               0 [       0] RVA [size] of Certificates Directory
            5000 [     170] RVA [size] of Base Relocation Directory
            20F0 [      38] RVA [size] of Debug Directory
               0 [       0] RVA [size] of Architecture Directory
               0 [       0] RVA [size] of Global Pointer Directory
               0 [       0] RVA [size] of Thread Storage Directory
            2140 [      40] RVA [size] of Load Configuration Directory
               0 [       0] RVA [size] of Bound Import Directory
            2000 [      CC] RVA [size] of Import Address Table Directory
               0 [       0] RVA [size] of Delay Import Directory
               0 [       0] RVA [size] of COM Descriptor Directory
               0 [       0] RVA [size] of Reserved Directory


SECTION HEADER #1
   .text name
     9A4 virtual size
    1000 virtual address (00401000 to 004019A3)
     A00 size of raw data
     400 file pointer to raw data (00000400 to 00000DFF)
       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
     6FC virtual size
    2000 virtual address (00402000 to 004026FB)
     800 size of raw data
     E00 file pointer to raw data (00000E00 to 000015FF)
       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

  Debug Directories

        Time Type       Size      RVA  Pointer
    -------- ------ -------- -------- --------
    59E02D5C cv           81 00002188      F88    Format: RSDS, {349955F6-EE25-4
CC8-92FE-074DADB6BA13}, 10, C:\Users\admin\Documents\Visual Studio 2013\Projects
\ConsoleApplication3\Release\ConsoleApplication3.pdb
    59E02D5C feat         14 0000220C     100C    Counts: Pre-VC++ 11.00=0, C/C+
+=22, /GS=22, /sdl=1, reserved=0

SECTION HEADER #3
   .data name
     37C virtual size
    3000 virtual address (00403000 to 0040337B)
     200 size of raw data
    1600 file pointer to raw data (00001600 to 000017FF)
       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
   .rsrc name
     1E0 virtual size
    4000 virtual address (00404000 to 004041DF)
     200 size of raw data
    1800 file pointer to raw data (00001800 to 000019FF)
       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 #5
  .reloc name
     170 virtual size
    5000 virtual address (00405000 to 0040516F)
     200 size of raw data
    1A00 file pointer to raw data (00001A00 to 00001BFF)
       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 .rdata
        1000 .reloc
        1000 .rsrc
        1000 .text

下表列出了常用的段名以及用途:


除了编译器和连接器所创建的标准段以外,还可以在编译的时候使用下面的编译器指示符来创建自己的段。

#pragma data_seg("sectionname")

例如一下代码创建了一个名为"Shared"的段,它只包含一个LONG变量

#pragma data_seg("Shared")
LONG g_lInstanceCount = 0;
#pragma data_seg()
编译这段代码会创建一个名为Shared的段,并将pragma指示符之间所有带初值的变量放到这个新的段中。最后的#pragma data_seg()通知编译器停止把已初始化的变量放到Shared段中。而重新开始把变量保存到默认的数据段中。

注意:编译器只会把已经初始化的变量放在这个段中。如果不给变量初始化编译器会将其放在Shared段以外的其他段中:

#pragma data_seg("Shared")
LONG g_lInstanceCount;
#pragma data_seg()

VC++也提供了一个allocate声明符(declaration specifier)允许我们将未初始化的数据放到任何想要放的段里。

// 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;

为了让allocate声明符能够正常工作,必须先创建相应的段。

为了让段数据在同一个应用程序的多个实例之间共享,需要指定连接器的/SECTION属性。

/SECTION:Shared, RWS

R表示READ

W表示WRITE

E表示EXECUTE

S表示SHARED

如果要改变多个段的属性,必须使用多个/SECTION开关。

也可以直接嵌入代码。

#pragma comment(linker, "/SECTION:Shared, RWS")

这行代码告诉编译器把其中的字符嵌入到所生产的.obj文件的一个特殊的段中,这个段明叫.drectve 当连接器把所有.obj模块合并在一起的时候,连接器会检查每个.obj模块的.drectve段。并把所有的字符串当做是给连接器的命令行参数。这样如果新加了一个源文件到项目中,也不必每次去VC++的Project里重新设置连接器开关。


虽然可以创建共享段,但是Microsoft并不鼓励使用共享段。这是有可能存在潜在的安全漏洞,其次意味着一个应用程序的错误会影响到另一个应用程序,因为没有办法保护共享变量,使其不被错误的改写。


17.1.3 Application Instances示例程序

应用程序能够知道任何一个时刻有多少个自己的实例正在运行。


AppInst.cpp中存在一下代码。

// 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_lApplicationInstance = 0;
#pragma data_seg()

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


在_tWinMain函数的入口处会使用InterlockedChangeAdd来修改g_lApplicationInstance

然后调用Dlg_OnInitDialog并给所有顶层窗口(top-level window)广播一条窗口消息

PostMessage(HWND_BROADCAST, g_uMsgAppInstCountUpdate, 0, 0);

系统中的所有其他窗口会忽略该消息。当前示例的窗口收到这条消息的时候,Dlg_Proc中的代码会根据当前示例的数量来更新对话框的值。


完整代码:

/******************************************************************************
Module:  AppInst.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/

#include "..\CommonFiles\CmnHdr.h"
#include <windowsx.h>
#include <tchar.h>
#include "Resource.h"

//

// The system-wide window message, unique to the application
UINT g_uMsgAppInstCountUpdate = WM_APP + 123;

//

// 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_lApplicationInstance = 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 stati
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值