Windows API笔记(六)内存映射文件

Windows API笔记(一)内核对象
Windows API笔记(二)进程和进程间通信、进程边界
Windows API笔记(三)线程和线程同步、线程局部存储
Windows API笔记(四)win32内存结构
Windows API笔记(五)虚拟内存
Windows API笔记(六)内存映射文件
Windows API笔记(七)堆
Windows API笔记(八)文件系统
Windows API笔记(九)窗口消息
Windows API笔记(十)动态链接库
Windows API笔记(十一)设备I/O


文章目录

  • 1. 内存映射exe和dll
    • 1.1 不被exe或dll的多个实例共享的静态数据
  • 2. 内存映射数据文件
    • 2.1 方法1:一个文件,一个缓冲区
    • 2.2 方法2:两个文件,一个缓冲区
    • 2.3 方法3:一个文件,两个缓冲区
    • 2.4 方法4:一个文件,零个缓冲区
  • 3. 使用内存映射文件
    • 3.1 第1步:创建或打开文件内核对象
    • 3.2 第2步:创建文件映射内核对象
    • 3.3 第3步:将文件数据映射入进程地址空间


使用内存映射文件主要有3个目的:

  • 系统使用内存映射文件来装入和执行exe和dll文件。这大大节省了页面文件空间和弃用程序开始执行的时间。
  • 可以使用内存映射文件来访问磁盘上的数据文件,无需进行文件I/O操作或缓冲文件的内容。
  • 可以使用内存映射文件来允许运行在同一计算机上的多个进程之间共享数据。(Win32还提供了其他方法在进程间进行数据通信,但这些方法的实现都使用了内存映射文件)。

1. 内存映射exe和dll

当线程调用CreateProcess时,系统执行下列步骤:

  1. 系统定位在CreateProcess中指定的exe文件。如果找不到,就不会创建进程,CreateProcess返回FALSE。

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

  3. 系统为新进程创建一个4GB的地址空间。

  4. 系统在地址空间中保留了足够装下exe文件的一块区域。该区域的位置是在exe文件中指定的。默认,exe文件的基本地址是0x00400000。不过,在链接程序时,可以使用链接器的/BASE选项来重载这一地址。

  5. 系统注意到支持该保留区域的物理存储是磁盘上的exe文件,而不是系统的页面文件。在exe文件被映射到进程的地址空间之后,系统访问exe文件里的某一节,那里列出了包含exe调用的函数的dll文件。然后系统为每个dll调用LoadLibrary,如果某个dll还需要其他的dll,系统也会调用LoadLibrary来加载这些dll。每当调用LoadLibrary来加载一个dll时,系统执行蕾仕于上面的第4和5步的行动:

    1. 系统保留一块足够装得下dll文件的区域。该区域的位置是dll文件自己指定的。缺省时,vc++使得dll的基本地址为0x10000000。不过,在建立dll时,可以使用链接器的/BASE选项来重设该值。
    2. 如果系统不能再dll指定的基本地址处保留区域,或者是因为该区域被其他dll或exe占据了,或者是因为该区域不够大,系统就将在地址空间寻找另一块区域来保留给该dll。如果dll不能加载到它指定的基本地址是很不幸的,这有两个原因。首先,如果该dll不含有修正信息,系统就可能不能加载该dll。(在创建dll时,可以使用链接器的/FIXED开关来删除修正信息。这能使dll文件变小,但也意味着该dll必须加载到它指定的地址。)其次,系统必须在dll内部进行一些重定位。在Windows NT上,这些重定位需要系统的页面文件中的一些额外存储;这还增加了加载dll所需的时间。
    3. 系统会记下来支持保留区域的文件存储是在磁盘上的dll文件而不是在系统的页面文件中。如果因为dll不能加载到它指定的基本地址,Windows NT必须进行重定位的话,系统也会记下来该dll的一些物理存储被映射到了页面文件中。
  6. 如果系统因故不能映射exe和所需的dll,CreateProcess将向调用者返回FALSE,可以调用GetLastError来弄清进程为什么不能被创建。

在所有的exe和dll文件被映射进进程的地址空间之后,系统就能开始执行exe文件的启动代码了。在exe我呢见被映射之后,系统会负责所有的页面、缓冲和缓存。例如,如果exe中的代码跳到了还没有装入内存的一条指令的地址,会产生一个错误。系统检测到错误后,会自动把该代码所在页从我呢见的映象装入到RAM的页中。而后系统把RAM页映射到进程地址空间中的正确位置。允许线程继续执行,就好像代码页早已被装入一样。当然,这些对程序是不可见的。每当进程中的线程试图访问的数据或代码没有被装入RAM时,就会重复该过程。

1.1 不被exe或dll的多个实例共享的静态数据

系统通过使用该内存管理系统和写拷贝特性来防止某一实例改变了共享的静态数据导致所有实例的内存内容都被改变。每当应用试图写它的内存映射文件时,系统捕捉到请求,对包含有被写的内存的页分配一块新内存,拷贝页的内容,然后允许应用程序写这块新分配的内存。这样,其他的实例就不会受到影响

可以在exe或dll中创建能被所有实例共享的全局遍历。简单的说,该方法要求使用#pragma data_seg()编译器指令将要共享的变量放在它们自己的节中。然后必须使用/SECTION: name,attributes开关来告诉链接器要让该节中的数据被所有的实例或文件的映象共享。

2. 内存映射数据文件

在exe或dll文件被加载时,操作系统自动使用前一节中讲述过的技术。不过,还可能在进程的地址空间中映射一个数据文件。这使得操纵大数据流非常方便。
为了理解这样使用内存映射文件的强大功能,让我们看一下实现一个程序将文件中的所有字节倒放的4中可能的方法。

2.1 方法1:一个文件,一个缓冲区

流程:

  1. 申请一块足够大的内存
  2. 将文件读入内存
  3. 倒置内存中的数据
  4. 写入文件

缺点:

  • 必须分配一块与文件大小相同的内存,如果文件较大(超过内存限制)就可能无法实现
  • 写回文件时,如果过程被中断了,文件的内容就被破坏了

2.2 方法2:两个文件,一个缓冲区

流程:

  1. 创建一个新文件
  2. 创建一个较小的内部缓冲区,比如8KB
  3. 读入源文件的最后8KB至内部缓冲区
  4. 倒置内部缓冲区的内容,然后写入新文件
  5. 重复3-4,直至读完源文件
  6. 删除源文件,保留新文件

缺点:

  • 比方法1复杂
  • 处理速度比方法1要慢
  • 可能要占用巨大的硬盘空间

2.3 方法3:一个文件,两个缓冲区

流程:

  1. 申请两个8KB的内存缓冲区
  2. 将文件的开始8KB字节读入一个缓冲区
  3. 将文件的最后8KB字节读入另一个缓冲区
  4. 分别倒置两个缓冲区的数据,然后分别写入文件的开始和结尾
  5. 重复2-4,只至文件全部读完

优点:

  • 节省内存和硬盘空间

缺点:

  • 实现复杂
  • 处理过程被打断则可能破坏源文件

2.4 方法4:一个文件,零个缓冲区

使用内存映射文件倒置文件内容。

流程:

  1. 将文件映射到虚拟地址空间
  2. 调用_strrev将文件中的数据倒置即可

优点:

  • 系统替你管理所有的文件缓存,不必分配任何内存,不必将文件装入内存,页不必将文件写回文件和释放任何内存块

缺点:

  • 掉电等意外事故可能在处理过程中破坏源文件

3. 使用内存映射文件

要使用内存映射文件,必须执行下列3步:

  1. 创建或打开一个文件内核对象来标识硬盘上的想用作内存映射文件的文件
  2. 创建一个文件映射内核对象来告诉系统文件的大小和想要如何访问文件
  3. 告诉系统把文件映射对象的全部或部分映射到进程的地址空间中

使用完内存映射文件后,必须执行下列3步进行清理工作:

  1. 告诉文件把文件映射对象从进行的地址空间中解除映射
  2. 关闭文件映射内核对象
  3. 关闭文件内核对象

3.1 第1步:创建或打开文件内核对象

调用CreateFIle创建或打开文件内核对象:

HANDLE
CreateFileA(
    _In_ LPCSTR lpFileName,
    _In_ DWORD dwDesiredAccess,
    _In_ DWORD dwShareMode,
    _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    _In_ DWORD dwCreationDisposition,
    _In_ DWORD dwFlagsAndAttributes,
    _In_opt_ HANDLE hTemplateFile
    );

dwDesiredAccess 以何种方式访问文件,取值范围:

含义
0 不能读写文件的内容
GENERIC_READ 可读
GENERIC_WRITE 可写
GENERIC_READ|GENERIC_WRITE 可读可写

对于内存映射文件,必须以只读或读写方式打开文件。
dwShareMode 如何共享该文件,取值范围:

含义
0 不共享,独占文件
FILE_SHARE_READ 读共享,其他以带有写的方式打开文件都会失败
FILE_SHARE_WRITE 写共享,其他以带有读的方式打开文件都会失败
FILE_SHRE_READ|FILE_SHARE_WRITE 读写共享,其他打开文件的尝试都会成功

3.2 第2步:创建文件映射内核对象

调用CreateFile是告诉操作系统文件映射的物理存储的位置。传送的路径名指出了支持文件映射的物理存储的确切位置,是在硬盘上、网络上、CD_ROM盘上等等。现在,必须告诉系统文件映射对象需要多少物理存储。这时调用CreateFileMapping:

HANDLE
CreateFileMappingA(
    _In_     HANDLE hFile,
    _In_opt_ LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
    _In_     DWORD flProtect,
    _In_     DWORD dwMaximumSizeHigh,
    _In_     DWORD dwMaximumSizeLow,
    _In_opt_ LPCSTR lpName
    );

第1个参数hFile标识了想要映射到进程的地址空间中的文件的句柄。该句柄是由CreateFile创建的。
当创建文件映射对象时,系统并不保留地址空间中的区域,并把文件的内存映射到该区域。不过,当系统要向进程的地址空间映射存储时,系统必须知道赋给物理存储页的保护属性。dwProtect允许指定保护属性,大多数时候指定的是下表中给出的3个保护属性之一:

保护属性 含义
PAGE_READONLY 文件映射对象为只读,必须向CreateFile传递GENERIC_READ
PAGE_READWRITE 文件映射对象为可读可写,必须向CreateFile传递GENERIC_READ|GENERIC_WRITE
PAGE_WRITECOPY 文件映射对象为写拷贝,可读可写;写时会创建一份页面的私有拷贝。必须向CreateFile传递GENERIC_READ或GENERIC_READ|GENERIC_WRITE

dwMaximumSizeHigh和dwMaximumSizeLow是最重要的参数。因为该函数的主要目的是确保对于文件映射对象有足够的物理存储。这两个参数告诉系统文件的最大字节大小。使用两个32位值是因为Win32支持64位的文件大小,dwMaximumSizeLow指定低32位,dwMaximumSizeHigh指定高32位。对于4GB以下的文件,dwMaximumSizeHigh总是为0。
如果在调用CreateFileMapping时传递PAGE_READWRITE标志,系统将会确保在磁盘上的相关数据文件的大小至少是由dwMaximumSizeHigh和dwMaximumSizeLow所给出的大小。如果文件比指定大小要小,将会增大磁盘上的文件。

#include 

int main()
{
    // 创建新文件
    HANDLE hFile = CreateFile("MMFTest.dat",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);

    // 将使文件大小为100byte
    HANDLE hFilemap = CreateFileMapping(hFile,NULL,PAGE_READWRITE,0,100,NULL);

    CloseHandle(hFilemap);
    CloseHandle(hFile);

    return 0;
}

最后一个参数lpName是一个字符串,用于给文件映射对象命名。该名字是用来与其他进程共享该对象的。不需要共享时,该参数一般为NULL。

3.3 第3步:将文件数据映射入进程地址空间

在创建文件映射对象后,还要让系统保留一块地址空间区域,将文件数据作为物理存储提交到该空间。这通过调用MapViewOfFile来实现:

LPVOID
MapViewOfFile(
    _In_ HANDLE hFileMappingObject,
    _In_ DWORD dwDesiredAccess,
    _In_ DWORD dwFileOffsetHigh,
    _In_ DWORD dwFileOffsetLow,
    _In_ SIZE_T dwNumberOfBytesToMap
    )

你可能感兴趣的:(C/C++)