《windows核心编程》 17章 内存映射文件

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

  • 系统使用内存映射文件载入并运行exe和dll,这大量节省了页交换文件的空间以及应用程序的启动时间
  • 开发人员可以使用内存映射文件来访问磁盘上的数据文件。这使得我们可以避免直接对文件IO操作和对文件内存进行缓存
  • 进程间通讯

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

当一个线程调用CreateProcess的时候,系统会执行收入步骤:

1.判断exe位置,如果无法找到exe那么不会创建进程,这时会CreateProcess返回FALSE

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

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

4.系统预订一块足够大的地址空间容纳exe

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

当系统把exe文件映射到进程地址空间之后,会访问exe文件中的一个段,这个段列来出一些DLL文件。系统每次载入DLL,执行的操作与刚才列出的第4步和第5步相似。

1.系统会预订足够大的地址空间区域来容纳DLL文件。待预订的地址空间区域的具体位置已经在DLL文件中指定

2.如果系统无法在DLL文件指定基地直处预订区域,这可能是因为该区域已经被另一个DLL或EXE占用,也可能因为区域不够大,这时系统会尝试在另一个地址区域预订。如果系统无法将DLL载入到指定基地址,那么这种情况就不太走运了。这有两个原因。首先,如果DLL不包含重定位信息,那么系统将无法载入DLL。其次,系统必须为DLL进行重定位操作,重定位不仅需要占用页交换文件中额外的地址空间,还会使载入DLL载入变慢

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

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

当我们为这个应用程序合建一个新进程时,系统只不过是打开另量个内存映射视图,创建一个新的进程对象,并创建一个线程对象。这个新打开内存映射视图隶属于一个文件映射对象,后者用来标识可执行文件映象。系统同时给进程对象和线程对象分配指定的ID。通过使用内存映射文件,同一个应用程序的多个实例可以共享内存中的代码和数据。

《windows核心编程》 17章 内存映射文件_第1张图片

假设应用程序的第二个实例开始运行,这时系统只不过是把包含应用程序代码和数据的虚拟内存页映射到第二个实例地址空间中。如下图

《windows核心编程》 17章 内存映射文件_第2张图片

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

操作系统通过内存管理系统的写时复制特性来防止这种情况的发生。当写入内存映射文件时,系统会截获此尝试,接着为应用程序分配一块新内存,然后复制 页面内容,最终的结果是其它实例不会受到影响。

下图描绘了当应用程序的第一个实例试图修改数据页面2虽的一个全局变量时,会产生的结果。

《windows核心编程》 17章 内存映射文件_第3张图片

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

每个exe文件和dll文件映象都有许多段组成。比如编译器会将代码放在一个叫.text段中,将已经初始化的数据放在.data段中。

除了使用编译器和链接器创建的标准段之外,我们可以自定义自己的段。

#pragma data_seg("sectionname")

例如我们可以用下面代码来合建一个名为Share的段,它只包含一个LONG的变量

#pragma data_seg("Shared")
LONG g_lInstanceCount = 0;
#pragma data_set();

注意:编译器只会将已经初始化的变量放入自己定义的段当中,如果上面代码中g_lInstanceCount 没有初始化,则不会放到我们指定的段之中。

但是Vc++ 编译器提供了一个allocate声明符,它允许我们将未经初始化的数据放到任何我们想要放入的段中。

#pragma data_seg("Shared")
int a = 0; //初始化的会放在shared段内
int b ;//没有初始化的不会放在shared段内
#pragma data_set();

__declspec(allocate("Shared")) int c = 0; //初始化的会放在shared段内
__declspec(allocate("Shared")) int d ; //没有初始化的也会放在shared段内
int e = 0;//初始化确不在Shared段中
int f ; //未初始化不在Shared段中

之所以将变量放在一个单独的段中,最常见的原因就是为了共享exe或dll多个实例中共享数据。

为了共享变量,我们还需要告诉链接器要共享这个段中的变量。通过使用链接器命令行/SECTION 来实现

/SECTION:name,attributes

在本例中我们想要改变Share的属性,因此应用使用下面的开关

/SECTION:Shared,RWS

我们也可以把这条命令嵌入到代码中

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

但是MricroSoft并不鼓励使用共享段。1.有潜在安全漏洞  2,意味着一个应用程序中的错误可能影响到另一应用程序。

17.2 映射到内存的数据文件

例子:颠倒文件内容。

17.2.1 方法1:一个文件,一块缓存

分配足够大的内存来存放整个文件。接着把文件所有内容读取到内存中。这时我们可以对内存中的文件内容进行操作,第第一个字节和最后一个字节交换,第二个字节和倒数第二个字节交换。以此类推。

缺点:

1.必须根据文件大小来分配内存,如果文件过大就不行了

2.把已经颠倒的内容写入文件时中断,那么文件内容遭到破坏。避免这种错误的方法是合建副本,但是这样会浪费磁盘空间

17.2.2方法2:两个文件,一块缓存

创建一个长度为0的新文件,接着分配一块小的内部缓存,比如8KB。然后将文件指针定位到原始文件末尾减去8KB的地方,最后8KB的内容读取到缓存中,然后颠倒内容写入到刚才创建的文件中。这个定位文件指针、读取文件、写入文件的过程一直继续。直到原始文件达到起始位置。

缺点:

1.每次操作必须对文件指针进行定位移动操作,因为字的速度比第一种方法要慢

2.处理过程中文件内容一直在增大,如果原始文件为1GB,那么在原始文件删除之前这两个文件占用空间用2GB的磁盘空间。

17.2.3 方法3:一个文件,两块缓存

申请两块大小为8KB的缓存。程序接着把文件开始的8KB内容读取到第一块缓存中,把文件末尾的8KB读到第二块缓存中。然后后两块缓存的内容颠倒,并把第一块缓存的内容写回到文件末尾,把第二块缓存的内容吃回到文件开头。这个过程一直继续。

与前一种方法相比,这种方法更好的节省磁盘空间,由于 所有数据都读取自和写入到了同一个文件,因此不需要额外的磁盘空间。在内存方面这种方式也不差,只使用了16kb内存。

17.2.4 方法3:一个文件,零个缓存

使用内存映射文件来颠倒文件内容时,我们先打开文件并向系统预订一块虚拟地址空间区域。接着让系统把文件的第一个字节映射到该区域的第一个字节。然后就可以访问这块虚拟内存区域,就好像它实际上包含了文件一样。事实上,如果要颠倒的是一个文本文件,而且文件末尾字节为0,则可以把这个文件当作内存中的一个字符串来处理,在这种情况下,直接调用c运行库函数_tcsrev就能颠倒文件中的数据。

这种方法最大的优点就是让系统为我们处理文件缓存有关操作。但遗憾的是,使用内存映射文件的时候,如果操作过程被中途打断(如断电),仍然可能导致数据被破坏

17.3  使用内存映射文件

(1)创建或打开一个文件内核对象,该对象标识了我们想要用作内存映射文件的哪 个磁盘文件。

(2)创建一个文件映射内核对象,来告诉系统文件的大小以及我们打算如何访问文件

(3)告诉系统把文件映射对象的部分或全部进程地址空间中。

用完内存映射文件之后,必须执行下面三个步骤做清理工作

(1)告诉系统从进程地址空间中取消对文件映射内核对象的映射

(2)关闭文件映射内存对象

(3)关闭文件内核对象

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

HANDLE WINAPI CreateFile(
  __in      LPCTSTR lpFileName,
  __in      DWORD dwDesiredAccess,
  __in      DWORD dwShareMode,
  __in_opt  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  __in      DWORD dwCreationDisposition,
  __in      DWORD dwFlagsAndAttributes,
  __in_opt  HANDLE hTemplateFile
);

17.3.2 第2步 创建文件映射内存对象

HANDLE WINAPI CreateFileMapping(
  __in      HANDLE hFile,
  __in_opt  LPSECURITY_ATTRIBUTES lpAttributes,
  __in      DWORD flProtect,
  __in      DWORD dwMaximumSizeHigh,
  __in      DWORD dwMaximumSizeLow,
  __in_opt  LPCTSTR lpName
);

 

正如本章开头所指出的,创建一个内存映射文件相当于先预订一块地址空间区域,然后再给区域调拨物理存储器。唯一不同之处在于内存映射文件的物理存储器来自磁盘上的文件,而不是从系统页交换文件文件中分配的。创建一个文件映射对象的时候,系统不会预订一块地址空间区域,并把文件映射到该区域中。但是,当系统在映射进程地址空间的时候,它必须知道应该给物理存储器的页面指定何种保护属性。fdwProtect参数就 是让我们指定保护属性的。

PAGE_READONLY、PAGE_READWRITE、PAGE_WRITECOPY、PAGE_EXECUTE_READ、PAGE_EXECUTE_READWRITE

除了上面的页保护属性,我们可以把5种段属性fdwProcted参数按位或起来。

SEC_NOCACHE,SEC_IMAGE,SEC_RESERVE,SEC_COMMIT,SEC_LARGE_PAGES

要使用大页面内存必须满足以下条件:

  • 在调用CreateFileMapping的时候必须同时指定SEC_COMMIT属性来调拨内存
  • 映像的大小必须大于GetLargePageMinimum函数返回值。
  • 必须用PAGE_READWRITE保护属性定义映射
  • 用户必须具有并启用内存中锁定页面用户权限

CreateFileMapping有两参数最重要dwMaximumSizeHigh,dwMaximumSizeLow这两个参数告诉系统内存映射文件的最大大小,以字节为单位。如果想要用当前文件大小创建一个文件映射对象时,只要传0就可以 了。

// TestCreateMappingFile.cpp : 定义应用程序的入口点。
//

#include "stdafx.h"
#include "TestCreateMappingFile.h"




int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPTSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    HANDLE hFile = ::CreateFile(TEXT("C:\\mmText.dat"),
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);   //创建一个空文件

    HANDLE hFileMap = ::CreateFileMapping(hFile,
        NULL,
        PAGE_READWRITE,
        0,
        100,
        NULL); //此时文件大小为100
    CloseHandle(hFile);
    CloseHandle(hFileMap);

    return 0;
}

17.3.3 第3步 将文件的数据映射到进程地址空间

在创建了文件映射对象之后,还需要为文件的数据预订一块地址空间区域并将文件的数据做为物理存储器调拨给区域。这可以通过调用MapViewOfFile来实现

 

LPVOID WINAPI MapViewOfFile(
  __in  HANDLE hFileMappingObject,
  __in  DWORD dwDesiredAccess,
  __in  DWORD dwFileOffsetHigh,
  __in  DWORD dwFileOffsetLow,
  __in  SIZE_T dwNumberOfBytesToMap
);

我们主要看一下第三个参数,第三个参数与预定地址空间和给区域调拨存储器有关,当我们把一个文件映射到地址空间的时候不必一下子映射整个文件。可以每次只把一小部分映射到地址空间中。文件中被映射到地址空间的部分被称为视图。

把文件映射到地址空间需要告诉系统两件事情:

1.我们必须告诉系统应该把数据文件中的哪个字节映射到视图中的第一个字节

2.我们必须告诉系统要把数据文件的多少映射到地址空间去。

int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPTSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    //打开要映射的文件
    HANDLE hFile = ::CreateFile(pszFileName,
        GERERIC_READ | GENERIC_WRITE,
        0,
        NULL,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);

    //为文件创建一个文件映射对象
    HANDLE hFileMapping = ::CreateFileMapping(hFile,NULL,PAGE_WRITECOPPY,0,0,NULL);

    //以copy-on-write的方式映射到地址空间
    PBYTE pbFile = (PBYTE) MapViewOfFile(hFileMapping,FILE_MAP_COPY,0,0,0);

    //从映射读一个字节
    //当读的时候系统不会改变页属性,依然保持PAGE_WRITECOPY属性
    BYTE bSomeByte = pbaFile[0];
    

    //写一个字节
    //当第一次写入一个字节的时候,系统会捕获,会复制原内容到新的页面次的页面有PAGE_READWRITE属性
    pbFile[0] = 0;

    //写另一个字节
    //由于这个字节现在是PAGE_READWRITE属性,系统会写入字节到页面
    pbFile[1] = 0;

    //当完成内存映射时,要取消映射
    UnmapViewOfFile(pFile);

    CloseHandle(hFileMapping);
    CloseHandle(hFile);


    
    return 0;
}

17.3.4 第4步 从进程的地址空间撤销对文件数据的映射

BOOL WINAPI UnmapViewOfFile(
  __in  LPCVOID lpBaseAddress
);

如果需要确保所有修改已经被写入到磁盘中可以调用

BOOL WINAPI FlushViewOfFile(
  __in  LPCVOID lpBaseAddress,
  __in  SIZE_T dwNumberOfBytesToFlush
);
 

17.3.5  第5步和第6步 关闭文件映射对象

CloseHandle(hFile);

CloseHandle(hMappfile);

17.3.6  示例程序

FileReverse程序展示了如何使用内存映射文件马一个Ansi或Unicode文本文件内容颠倒过来主要代码如下\

BOOL FileReverse(PCTSTR pszPathname, PBOOL pbIsTextUnicode) {

   *pbIsTextUnicode = FALSE;  // Assume text is Unicode

   // Open the file for reading and writing.
   HANDLE hFile = CreateFile(pszPathname, GENERIC_WRITE | GENERIC_READ, 0, 
      NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

   if (hFile == INVALID_HANDLE_VALUE) {
      chMB("File could not be opened.");
      return(FALSE);
   }

   // Get the size of the file (I assume the whole file can be mapped).
   DWORD dwFileSize = GetFileSize(hFile, NULL);

   // Create the file-mapping object. The file-mapping object is 1 character 
   // bigger than the file size so that a zero character can be placed at the 
   // end of the file to terminate the string (file). Because I don't yet know
   // if the file contains ANSI or Unicode characters, I assume worst case
   // and add the size of a WCHAR instead of CHAR.
   HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 
      0, dwFileSize + sizeof(WCHAR), NULL);

   if (hFileMap == NULL) {
      chMB("File map could not be opened.");
      CloseHandle(hFile);
      return(FALSE);
   }

   // Get the address where the first byte of the file is mapped into memory.
   PVOID pvFile = MapViewOfFile(hFileMap, FILE_MAP_WRITE, 0, 0, 0);

   if (pvFile == NULL) {
      chMB("Could not map view of file.");
      CloseHandle(hFileMap);
      CloseHandle(hFile);
      return(FALSE);
   }

   // Does the buffer contain ANSI or Unicode?
   int iUnicodeTestFlags = -1;   // Try all tests
   *pbIsTextUnicode = IsTextUnicode(pvFile, dwFileSize, &iUnicodeTestFlags);

   if (!*pbIsTextUnicode) {
      // For all the file manipulations below, we explicitly use ANSI 
      // functions because we are processing an ANSI file.

      // Put a zero character at the very end of the file.
      PSTR pchANSI = (PSTR) pvFile;
      pchANSI[dwFileSize / sizeof(CHAR)] = 0;

      // Reverse the contents of the file.
      _strrev(pchANSI);

      // Convert all "\n\r" combinations back to "\r\n" to 
      // preserve the normal end-of-line sequence.
      pchANSI = strstr(pchANSI, "\n\r"); // Find first "\r\n".

      while (pchANSI != NULL) {
         // We have found an occurrence....
         *pchANSI++ = '\r';   // Change '\n' to '\r'.
         *pchANSI++ = '\n';   // Change '\r' to '\n'.
         pchANSI = strstr(pchANSI, "\n\r"); // Find the next occurrence.
      }

   } else {
      // For all the file manipulations below, we explicitly use Unicode
      // functions because we are processing a Unicode file.

      // Put a zero character at the very end of the file.
      PWSTR pchUnicode = (PWSTR) pvFile;
      pchUnicode[dwFileSize / sizeof(WCHAR)] = 0;

      if ((iUnicodeTestFlags & IS_TEXT_UNICODE_SIGNATURE) != 0) {
         // If the first character is the Unicode BOM (byte-order-mark), 
         // 0xFEFF, keep this character at the beginning of the file.
         pchUnicode++;
      }

      // Reverse the contents of the file.
      _wcsrev(pchUnicode);

      // Convert all "\n\r" combinations back to "\r\n" to 
      // preserve the normal end-of-line sequence.
      pchUnicode = wcsstr(pchUnicode, L"\n\r"); // Find first '\n\r'.

      while (pchUnicode != NULL) {
         // We have found an occurrence....
         *pchUnicode++ = L'\r';   // Change '\n' to '\r'.
         *pchUnicode++ = L'\n';   // Change '\r' to '\n'.
         pchUnicode = wcsstr(pchUnicode, L"\n\r"); // Find the next occurrence.
      }
   }

   // Clean up everything before exiting.
   UnmapViewOfFile(pvFile);
   CloseHandle(hFileMap);

   // Remove trailing zero character added earlier.
   SetFilePointer(hFile, dwFileSize, NULL, FILE_BEGIN);
   SetEndOfFile(hFile);
   CloseHandle(hFile);

   return(TRUE);
}

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

下现代码统计一个二进制文件中所有值为0的字节数

__int64 Count0s(void)
{
    SYSTEM_INFO sinf;
    GetSystemInfo(&sinf);

    HANDLE hFile = CreateFile(TEXT("C:\\HugeFile.big"),GENERIC_READ,
        FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_FLAG_SEQUENTIAL_SCAN,NULL);

    HANDLE hFileMapping = CreateFileMapping(hFile,NULL,PAGE_READONLY,0,0,NULL);

    DWORD dwFileSizeHight;
    __int64 qwFileSize = GetFileSize(hFile,&dwFileSizeHight);
    qwFileSize += (((__int64)dwFileSizeHight) << 32);

    CloseHandle(hFile);

    __int64 qwFileOffset = 0,qwNumOf0s = 0;

    while(qwFileOffset > 0)
    {
        DWORD dwBytesInBlock = sinf.dwAllocationGranularity;
        if(qwFileSize < sinf.dwAllocationGranularity)
        {
            dwBytesInBlock = (DWORD) qwFileSize;
        }

        PBYTE pbFile = (PBYTE) MapViewOfFile(hFileMapping,FILE_MAP_READ,
            (DWORD) (qwFileOffset >> 32),
            (DWORD) (qwFileOffset & 0xFFFFFFFF),
            dwBytesInBlock);

        for(DWORD dwByte = 0;dwByte < dwBytesInBlock;dwByte ++ )
        {
            if(pbFile[dwByte] == 0)
                qwNumOf0s ++;
        }

        UnmapViewOfFile(pbFile);
        qwFileOffset += dwBytesInBlock;
        qwFileOffset -= dwBytesInBlock;
    }
    CloseHandle(hFileMapping);
    return qwNumOf0s;
}

 

17.5 内存映射文件和一致性

只要我们映射的是同一个文件映象,那么系统会确保各个视图中的数据是一致的。如果多个进程把同一个数据文件映射到多个视图中,那么数据也是一致的,这是因为数据文件中每一个页在内存中只有一份,但这些内面页面会被映射到多个进程地址空间而已。

如果打算将打开的文件用于内存映射,那么调用 CraeteFile时最好传0给dwShareMode参数。这等于告诉系统我想要独占文件的访问,使其它进程无法访问此文件。

由于只读文件不存在 一致性的问题,因此它们非常适合内存映射文件。我们绝对不应该用内存映射文件来跨网络共享可写文件,因为系统无法保证数据视图的一致性。、

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

LPVOID WINAPI MapViewOfFileEx(
  __in      HANDLE hFileMappingObject,
  __in      DWORD dwDesiredAccess,
  __in      DWORD dwFileOffsetHigh,
  __in      DWORD dwFileOffsetLow,
  __in      SIZE_T dwNumberOfBytesToMap,
  __in_opt  LPVOID lpBaseAddress
);

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

第一个进程调用MapViewOfFile时返回的内存地址,与第二个进程调用MapViewOfFile时返回的内存地址很可能是不相同的。即使两个映射同一个文件,情况也是如此。

 

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

让 我们来看一个例子:启动应用程序。当一个应用程序启动时,系统会先调用CreateFile来打开磁盘上的.exe文件。接着系统会调用 CreateFileMapping来创建文件映射对象。最后系统会以新创建的进程的名义调用MapViewOfFileEx(并传入SEC_IMAGE 标志),这样就把.exe文件映射到了进程的地址空间中。值所以调用MapViewOfFileEx而不是MapViewOfFile,是为了把文件映射 到指定的基地址,这个基地址保存在.exe的PE文件头中。系统然后创建进程的主线程,在映射得到的视图中取得可执行代码的第一个字节的地址,把该地址放 到线程的指令指针中,最后让CPU开始执行其中的代码。
     如 果用户启动同一个应用程序的第二个实例,那么系统会发现该.exe文件已经有一个文件映射对象,因此就不会再创建一个新的文件对象或文件映射对象。取而代 之的是,系统会再次映射.exe文件的一个视图,但这次是在新创建的进程的地址空间中。至此,系统已经把同一个文件同时映射到了两个地址空间中。显然,由 于物理内存中包含.exe文件可执行代码的那些页面为两个进程所共享,因此内存的使用率更高。

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

Microsoft加入了相应的支持,让系统能够创建以页交换文件为后备存储器的内存映射文件,这样就不需要用磁盘上专门的文件来作为后备存储器了。这种方法和为磁盘文件创建内存映射文件的方法几乎完全相同,甚至更简单。 一方面,由于不必创建或打开一个专门的磁盘文件,因此不需要调用CreateFile。我们只需要像原来那样调用CreateFileMapping,并将INVALID_HANDLE_VALUE作为hFile参数传入。这告诉系统我们创建的文件映射对象的物理存储器不是磁盘上的文件,而是希望系统从页交换文件中调拨物理存储器。 所需分配的存储器大小由CreateFileMapping的dwMaximumSizeHigh和dwMaximumSizeLow参数决定

Memory-Mapped File Sharing 示例程序

主要关键代码

void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {
   
   // Handle of the open memory-mapped file
   static HANDLE s_hFileMap = NULL;

   switch (id) {
      case IDCANCEL:
         EndDialog(hWnd, id);
         break;

      case IDC_CREATEFILE:
         if (codeNotify != BN_CLICKED)
            break;

         // Create a paging file-backed MMF to contain the edit control text.
         // The MMF is 4 KB at most and is named MMFSharedData.
         s_hFileMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, 
            PAGE_READWRITE, 0, 4 * 1024, TEXT("MMFSharedData"));

         if (s_hFileMap != NULL) {

            if (GetLastError() == ERROR_ALREADY_EXISTS) {
               chMB("Mapping already exists - not created.");
               CloseHandle(s_hFileMap);

            } else {

               // File mapping created successfully.

               // Map a view of the file into the address space.
               PVOID pView = MapViewOfFile(s_hFileMap,
                  FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);

               if (pView != NULL) {
                  // Put edit text into the MMF.
                  Edit_GetText(GetDlgItem(hWnd, IDC_DATA), 
                     (PTSTR) pView, 4 * 1024);

                  // Protect the MMF storage by unmapping it.
                  UnmapViewOfFile(pView);

                  // The user can't create another file right now.
                  Button_Enable(hWndCtl, FALSE);

                  // The user closed the file.
                  Button_Enable(GetDlgItem(hWnd, IDC_CLOSEFILE), TRUE);

               } else {
                  chMB("Can't map view of file.");
               }
            }

         } else {
            chMB("Can't create file mapping.");
         }
         break;

      case IDC_CLOSEFILE:
         if (codeNotify != BN_CLICKED)
            break;

         if (CloseHandle(s_hFileMap)) {
            // User closed the file, fix up the buttons.
            Button_Enable(GetDlgItem(hWnd, IDC_CREATEFILE), TRUE);
            Button_Enable(hWndCtl, FALSE);
         }
         break;

      case IDC_OPENFILE:
         if (codeNotify != BN_CLICKED)
            break;

         // See if a memory-mapped file named MMFSharedData already exists.
         HANDLE hFileMapT = OpenFileMapping(FILE_MAP_READ | FILE_MAP_WRITE,
            FALSE, TEXT("MMFSharedData"));

         if (hFileMapT != NULL) {
            // The MMF does exist, map it into the process's address space.
            PVOID pView = MapViewOfFile(hFileMapT, 
               FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);

            if (pView != NULL) {

               // Put the contents of the MMF into the edit control.
               Edit_SetText(GetDlgItem(hWnd, IDC_DATA), (PTSTR) pView);
               UnmapViewOfFile(pView);
            } else {
               chMB("Can't map view.");
            }

            CloseHandle(hFileMapT);

         } else {
            chMB("Can't open mapping.");
         }
         break;
   }
}

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

我们只需要给CreateFileMapping第一个参数传INVALID_HANDLE_VALUE的系统就以页交换文件中调拨物理存储器。

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

CreateFileMapping为我们提供了一种方法,即在fdwProtect参数中指定SEC_RESERVE或SEC_COMMIT标志。这样我们把先前的那个电子的数据作为一个文件映射对象来共享,但又不希望在一开始就给它调拨所有物理存储器。

SEC_RESERVE:系统不会从页交换文件中调拨物理存储器,它只返回文件映射对象的句柄。现在我们可以调用MapViewOfFile来给这个文件映射对象创建一个视图。MapViewOfFile会预订地址空间,但不会调任何物理存储器。设计图访问会违规。通过使用SEC_RERVER和VirtualAlloc我们不仅能与其它进程共享电子表格的数组,而且还能高效的使用物理存储器。如果内存映射文件是通过SEC_RESERVE标志得到的,便不能用VirtualFree来撤销调拨给它的存储

NT文件系统(NTFS)提供了对稀疏文件的支持。这是一项非常棒的特征。我们可以用这项特性来创建和处理稀疏内存映射文件,这样一来,存储器就不必总是在页交换文件中,而可以在普通文件中。

假设我们要创建一个内存映射文件来存储录音数据。当用户说话时,我们希望把数字音频数据写入到内存缓存中,并以磁盘文件为内存缓存的后备存储器。一个部分调拨的内存映射文件当然是最简单高效的办法。问题是我们不知道用户在单击停止按钮前会说多久,可能是五分钟,但也可以是5小时。差距不可谓不大!我们需要一个足够大的文件来保存这些数据。但是,在使用稀疏调拨的内存映射文件时,大小并没有多大关系。

你可能感兴趣的:(《windows核心编程》 17章 内存映射文件)