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

来源:互联网 发布:国事访问知乎 编辑:程序博客网 时间:2024/06/07 07:13

第十七章 内存映射文件

本章内容

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:\test\ConsoleApplication3.exeMicrosoft (R) COFF/PE Dumper Version 12.00.40629.0Copyright (C) Microsoft Corporation.  All rights reserved.Dump of file d:\test\ConsoleApplication3.exePE signature foundFile Type: EXECUTABLE IMAGEFILE 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 machineOPTIONAL 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 DirectorySECTION 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 numbers60000020 flags         Code         Execute ReadSECTION 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 numbers40000040 flags         Initialized Data         Read Only  Debug Directories        Time Type       Size      RVA  Pointer    -------- ------ -------- -------- --------    59E02D5C cv           81 00002188      F88    Format: RSDS, {349955F6-EE25-4CC8-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=0SECTION 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 numbersC0000040 flags         Initialized Data         Read WriteSECTION 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 numbers40000040 flags         Initialized Data         Read OnlySECTION 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 numbers42000040 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 sectionint a = 0;// Uninitialized, not in Shared sectionint 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 sectionint e = 0;// Uninitialized, not in Shared sectionint 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.cppNotices: 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 applicationUINT 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 static control to be initialized correctly.PostMessage(HWND_BROADCAST, g_uMsgAppInstCountUpdate, 0, 0);return TRUE;}//////////////////////////////////////////////////////////////////////////void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {switch (id) {case IDCANCEL:EndDialog(hWnd, id);break;}}//////////////////////////////////////////////////////////////////////////INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {if (uMsg == g_uMsgAppInstCountUpdate) {SetDlgItemInt(hWnd, IDC_COUNT, g_lApplicationInstance, FALSE);}switch (uMsg) {chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog);chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand);}return FALSE;}//////////////////////////////////////////////////////////////////////////int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR, int) {// Get the numeric value of the systemwide window message used to notify// all top-level windows when the module's usage count has changed.g_uMsgAppInstCountUpdate =RegisterWindowMessage(TEXT("MsgAppInstCountUpdate"));// There is another instance of this application runningInterlockedExchangeAdd(&g_lApplicationInstance, 1);DialogBox(hInstExe, MAKEINTRESOURCE(IDD_APPINST), NULL, Dlg_Proc);// This instance of the application is terminatingInterlockedExchangeAdd(&g_lApplicationInstance, -1);// Have all other instance update their displayPostMessage(HWND_BROADCAST, g_uMsgAppInstCountUpdate, 0, 0);return 0;}

博主注:

后来做了一个测试发现,这里的所谓的应用程序实例间共享数据的背后原理是。以内存映像文件(磁盘上的exe)为后备的。意思就当设置了exe中存在一个共享数据段以后,系统只会允许同一个磁盘文件的的所有实例之间进行数据共享。(因为系统是根据exe文件的位置来判断进程是否是同一个实例的)

博主将同一个exe拷贝到不同目录下分别运行,发现应用程序并不能检测到已经运行的其他实例。虽然这些实例都可以收到广播的系统消息,但是广播消息也是以共享数据段中的变量为依据进行更新的。


17.2 映射到内存的数据文件

windows允许将数据文件映射到进程的地址空间中,这样一来对大型数据流进行操控就非常容易。

不使用内存映射的例子。颠倒文件

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

分配一块足够大的内存来放整个文件。接着打开文件,把文件内容读取到内存,然后关闭文件。这时可以对内存中的文件内容进行操作。把第一个字节和最后一个字节交换,把第二个字节和倒数第二个字节交换,以此类推。直到达到文件中间。所有字节都交换完成以后可以再次打开文件并用内存中的文件覆盖原来的内容。


这种方法简单,但是有两个主要缺点。

1)必须根据文件大小赖分配内存。如果文件比较小,那么还OK。如果文件无比大(例如大于2GB以上)那么32位操作系统不允许应用程序调拨那么大的物理存储器。大文件需要使用其他方法来处理。

2)如果在把已经颠倒了顺序的文件内容写入到文件的过程中处理器被中断,那么文件的内容将被破坏。(除非事先备份文件,等操作成功了再进行删除)

一个简单的实现:

// algorithm with 1 file and 1 large buffer.void reservefile1(const char * szFileName) {size_t sizeFile = file_size(szFileName);if (sizeFile == 0)// invalid size.return;char *pBuff = (char *)malloc(sizeof(char) * (sizeFile + 1));if (pBuff == NULL)// malloc failedreturn;FILE *fp = fopen(szFileName, "rb+");if (!fp)goto clean_up;size_t sizeRead = 0;// the fread will ignore the last control ctrl+z in text mode.// so we must open the file in binary mode. // the size will be equal to fseek/ftellsizeRead = fread(pBuff, 1, sizeFile, fp);// read data error.if (sizeFile != sizeRead)goto clean_up;pBuff[sizeRead] = '\0';_strrev(pBuff);fixnewline(pBuff);// set the point to start.fseek(fp, 0, SEEK_SET);fwrite(pBuff, 1, strlen(pBuff), fp);// release memory.clean_up:if (fp) {fclose(fp);}if (pBuff) {free(pBuff);}}


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

打开已有的文件并在磁盘上创建一个长度为0的新文件,接着分配一块较小的缓存,比如8KB。然后将文件指针定位到原始文件尾部减去8KB的地方。把最后8KB的内容读到缓存,颠倒缓存的内容。然后将颠倒的内容写入新创建的文件。

定位文件指针,读取文件,颠倒缓存,写入文件的过程一直继续,直到达到文件的起始位置。

如果文件长度不是8KB的整数倍,需要进行一些特殊处理,完全处理完原始文件后,把两个文件都关闭并删除原始文件。


这种方法复杂许多,但是只需要很少的内存。但是每次在读取原始文件之间都要执行文件指针定位操作,因此处理方法比第一种慢。这种方法可能会消耗大量的磁盘空间。如果原始文件大小是1GB。那么在处理过程中新文件将逐渐增大到1GB。在原始文件被删除以前,这两个文件将占有2GB的磁盘空间。与实际需要的磁盘相比整整多了1GB

// algorithm with 2 two file 1 buffer.#define BUFFER8 * 1024void reservefile2(const char * szFileName, const char * szFileOut) {size_t sizeFile = file_size(szFileName);if (sizeFile == 0)// invalid size.return;char *pBuff = (char *)malloc(sizeof(char) * (BUFFER + 1));if (pBuff == NULL)// malloc failedreturn;FILE *fp = NULL;FILE *fw = NULL;fp = fopen(szFileName, "rb+");if (!fp)goto clean_up;fw = fopen(szFileOut, "wb");if (!fw)goto clean_up;size_t sizeRead = 0;// the fread will ignore the last control ctrl+z in text mode.// so we must open the file in binary mode. // the size will be equal to fseek/ftellsize_t sizeLeft = sizeFile;while (sizeLeft > 0) {size_t sizeBlock = (sizeLeft > BUFFER) ?BUFFER : sizeLeft;sizeLeft -= sizeBlock;fseek(fp, sizeLeft, SEEK_SET);size_t sizeRead = fread(pBuff, 1, sizeBlock, fp);if (sizeRead != sizeBlock) {// there is a read error.goto clean_up;}pBuff[sizeRead] = '\0';_strrev(pBuff);fixnewline(pBuff);fwrite(pBuff, 1, strlen(pBuff), fw);}// release memory.clean_up:if (fp) {fclose(fp);}if (fw) {fclose(fw);}if (pBuff) {free(pBuff);}}


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

初始化的时候分配两块8KB的缓存,接着把文件开始的8KB读到缓存,末尾的8KB也读到缓存。然后把两块内容颠倒,并把第一块缓存的内容写回文件尾。第二块写回文件头。

每次都把靠近文件头的8KB和靠近文件尾的8KB内容互换。如果文件大小不是16KB的整数,俺么最后两块8KB的缓存会有一部分内容重叠,这种情况需要进行特殊处理。


// algorithm with 1 file 2 buffer.#define BUFFER8 * 1024void reservefile3(const char * szFileName) {size_t sizeFile = file_size(szFileName);if (sizeFile == 0)// invalid size.return;char * pBuff1 = NULL, *pBuff2 = NULL;FILE * fp = NULL;pBuff1 = (char *)malloc(sizeof(char) * (BUFFER + 1));if (pBuff1 == NULL)// malloc failedgoto clean_up;pBuff2 = (char *)malloc(sizeof(char) * (BUFFER + 1));if (pBuff2 == NULL)// malloc failedgoto clean_up;fp = fopen(szFileName, "rb+");if (!fp)goto clean_up;// the fread will ignore the last control ctrl+z in text mode.// so we must open the file in binary mode. // the size will be equal to fseek/ftellsize_t sizeLeft = sizeFile;unsigned int readCount = 0;while (sizeLeft > 1) {size_t sizeBlock = (sizeLeft > BUFFER * 2) ?// read 16KB each timeBUFFER : sizeLeft / 2;sizeLeft -= sizeBlock * 2;// read two blocks.unsigned int posHeader = 0 + readCount * sizeBlock;unsigned int posEnd = sizeFile - (readCount + 1) * sizeBlock;fseek(fp, posHeader, SEEK_SET);// seek to headersize_t sizeRead = fread(pBuff1, 1, sizeBlock, fp);// read headerif (sizeRead != sizeBlock)goto clean_up;// read error.pBuff1[sizeRead] = '\0';fseek(fp, posEnd, SEEK_SET);// seek to end.sizeRead = fread(pBuff2, 1, sizeBlock, fp);// read endif (sizeRead != sizeBlock)goto clean_up;// read error.pBuff2[sizeRead] = '\0';// write _strrev(pBuff2);fixnewline(pBuff2);fseek(fp, posHeader, SEEK_SET);fwrite(pBuff2, 1, sizeBlock, fp);// write the end to header_strrev(pBuff1);fixnewline(pBuff1);fseek(fp, posEnd, SEEK_SET);fwrite(pBuff1, 1, sizeBlock, fp);// write the end to headerreadCount++;}// release memory.clean_up:if (fp) {fclose(fp);}if (pBuff1) {free(pBuff1);}if (pBuff2) {free(pBuff2);}}



两个辅助函数用于计算文件的二进制模式下的尺寸和修复反转文件以后\r\n换行符错乱的问题。

// using the crt library.size_t file_size(const char * szFileName) {FILE *fp = fopen(szFileName, "rb");if (!fp)return 0;fseek(fp, 0L, SEEK_END);size_t size = ftell(fp);fclose(fp);return size;}// revert the \n\r to \r\nvoid fixnewline(char * szBuf) {char *p = szBuf;while (*p != '\0') {if (*p == '\r' && *(p - 1) == '\n') {*(p - 1) = '\r';*p = '\n';}p++;}}



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

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


这种方法最大的优点在于让系统处理所有与文件缓存有关的操作。我们不必再分配任何内存,把文件的数据载入内存,把数据写回文件,以及释放内存块。

不过如果操作过程中被中途打断,仍然可能导致数据被破坏。


17.3 使用内存映射文件

需要执行以下步骤:

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

2)创建一个文件映像内核对象(file-mapping kernel object)来告诉系统文件的大小以及我们打算如何访问文件。

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


用完内存映射文件以后需要执行下面三个步骤来执行清理工作。

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

2)关闭文件映射内核对象

3)关闭文件内核对象


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

通过CreateFile函数来创建或打开一个文件内核对象:

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

大部分参数参考第十章。

pszFileName 表示要创建或打开的文件的名称(可以使用绝对路径,也可以使用相对路径)

dwDesiredAccess 用来指定打算如何访问文件的内容。



在创建或打开一个作为内存映射文件来使用的文件时,应该根据我们打算如何访问文件来选择一个或一组标志。对内存映射文件来说,必须以只读方式或读写方式来打开文件。

使用 GENERIC_READ 或 GENERIC_READ | GENERIC_WRITE


dwShareMode 告知系统如何来共享这个文件。



CreateFile成功会创建或打开一个指定的文件,他会返回一个文件内核对象句柄。否则它返回INVALID_HANDLE_VALUE


17.3.2 第二步:创建文件映射内核对象

调用CreateFile也是为了告知系统文件映射的物理存储器所在的位置。传入的路径是文件在磁盘上的位置,文件映射对象的物理存储器来自该文件。


接着必须告诉系统文件映射对象需要多大的物理存储器。调用以下函数

WINBASEAPI_Ret_maybenull_HANDLEWINAPICreateFileMappingW(    _In_ HANDLE hFile,    _In_opt_ LPSECURITY_ATTRIBUTES lpFileMappingAttributes,    _In_ DWORD flProtect,    _In_ DWORD dwMaximumSizeHigh,    _In_ DWORD dwMaximumSizeLow,    _In_opt_ LPCWSTR lpName    );


hFile是需要映射到到进程地址空间的文件的句柄(由CreateFile返回)

psa 是一个SECURITY_ATTRIBUTES结构的指针,一般传NULL(除非要设定继承属性)


创建一个内存映射文件“相当于”先预定一块地址空间区域,然后再给区域调拨物理存储器。唯一的不同在于内存映射文件的物理存储器来自磁盘上的文件,而不是系统的页交换文件。


可是实际上,创建一个文件映射对象的时候,系统不会预定一块地址空间区域并把文件映射到该区域。(需要后续执行一些操作MapViewOfFile

系统在映射进程地址空间的时候,必须知道应该给物理存储器的页面指定何种保护属性。CreateFileMappingfdwProtect参数就是让我们指定保护属性的。

除了上面的页面保护属性,还可以把5种段属性与fdwProtect参数按位或起来。 “段”(section)只不过是内存映射的另一种叫法,这种类型的内核对象可以通过Process Explorer来查看。

第一个是:SEC_NOCACHE 它告诉系统不要对内存映射的页面进行缓存。如果把数据写入文件,系统会更频繁地更新磁盘上的文件。和PAGE_NOCACHE类似,主要是给驱动程序开发人员使用。


第二个段属性是: SEC_IMAGE 它告知系统要映射的文件是一个PE文件映像。系统把文件映射到进程地址空间的时候,系统会检查文件的内容并决定应该给各页面指定何种保护属性。例如PE文件的代码段一般用PAGE_EXECUTE_READ属性来映射。而数据段一般用PAGE_READWRITE属性来映射。指定SEC_IMAGE属性相当于告诉系统要映射文件的映像并给页面设置相应的保护属性。


SEC_RESERVESEC_COMMIT ,他们不仅是互斥的,也不适用于映射到内存的数据文件。后续会讨论。不应该使用这两个标志中的任何一个。CreateFileMapping会忽略它们。


最后一个是SEC_LARGE_PAGES 它告诉windows要为内存映射文件使用大页面内存。只有当用于PE映像文件或内存映射文件的时候。当我们把自己的数据文件映射到内存时,是无法使用该属性的。

正如之前讨论的要使用大页面必须满足以下条件.

1 在调用CreateFileMapping的时候,必须同时制定SEC_COMMIT属性来调拨内存。

2 映射的大小必须大于GetLargePageMinimum函数的返回值(参考dwMaximumSizeHigh和dwMaximum这两个参数)

3 必须用PAGE_READWRITE保护属性定义映射

4 用户必须具有启用内存中锁定页面的用户权限,否则CreateFileMapping函数调用会失败。


dwMaximumSizeHigh 和 dwMaximumSizeLow CreateFileMapping函数主要是为了确保有足够的物理存储器可供文件映像对象使用。这两个参数告知系统内存映射文件的最大大小,以字节为单位。因为windows支持文件大小可以用64位整数表示,这里必须使用两个32位值。 dwMaximumSizeHigh表示高32位,dwMaximumSizeLow表示低32位。

对于小于4GB的文件,dwMaximumSizeHigh始终为0.


如果使用当前的文件大小,这两个参数传0即可。如果只是读取文件且并不改变文件大小同样传0. 如果要给文件追加数据,那么在选择文件最大大小的时候要留余量。

如果磁盘上的文件大小为0字节,就不能传两个0.这样CreateFileMapping会认为这是错误并返回NULL


查看一下代码:

int _tmain(int argc, TCHAR* argv[], TCHAR * env[]){// Before executing the line below, c:\ does not have // a file called "MMFTest.Dat"HANDLE hFile = CreateFile(TEXT("C:\\MMFTest.Dat"),GENERIC_READ | GENERIC_WRITE,FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL, NULL);// Before executing the line below, the MMFTest.Dat// file does exit but has a file size of 0 bytes.HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE,0, 100, NULL);// After executing the line above, the MMFTest.Dat// file has a size of 100 bytes.// CleanupCloseHandle(hFileMap);CloseHandle(hFile);// When the process ternimates, MMFTest.Dat remains// on the disk with a size of 100 bytes.return 0;}


如果调用CreateFileMapping并传入PAGE_READWRITE那么系统会进行检查,并确保磁盘上对应文件的大小不小于dwMaximumSizeHigh和dwMaximmSizeLow参数指定的大小。如果文件比指定的要小,那么CreateFileMapping会增大文件的大小。(为了保证后续把文件当做内存映射文件使用的时候物理存储器已经准备就绪。)

如果文件映射对象用PAGE_READONLY PAGE_WRITECOPY创建,那么CreateFileMapping指定的大小不必大于实际磁盘文件的大小,因为我们不能给文件追加任何数据。


CreateFileMapping的最后一个pszName参数用来给映射内核对象指定一个名字。用来在不同进程之间共享文件映像对象。若不需要共享则传递NULL


最后系统会创建文件映像对象并返回一个句柄给调用线程,这个句柄用来标识所创建的文件映像对象。如果系统无法创建会返回NULL。

CreateFile 失败返回INVALID_HANDLE_VALUE

CreateFileMapping 失败返回NULL


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

在创建文件映像对象以后,还需要为文件数据预定一块地址空间区域并将文件的数据作为物理存储器调拨给区域。通过MapViewOfFile

WINBASEAPI_Ret_maybenull_  __out_data_source(FILE)LPVOIDWINAPIMapViewOfFile(    _In_ HANDLE hFileMappingObject,    _In_ DWORD dwDesiredAccess,    _In_ DWORD dwFileOffsetHigh,    _In_ DWORD dwFileOffsetLow,    _In_ SIZE_T dwNumberOfBytesToMap    );

hFileMappingObject 之前创建的文件映射对象句柄(或者OpenFileMapping打开一个)

dwDesiredAccess 表示要如何访问数据。参考下表。



(作者认为windows要求一而再,再而三要求设置这些保护属性非常烦人。)


接下来的三个参数与预定地址空间区域和给区域调拨物理存储器有关。当我们把一个文件映射到进程地址空间的时候,不必一下映射整个文件。可以每次只把文件的一小部分映射到地址空间中。文件中被映射到进程地址空间的部分被称为视图(view)


把文件的一个视图映射到进程地址空间时,需要告知系统两件事。

1)必须告知系统应该把数据文件中的哪个字节映射到视图中的第一个字节。通过参数dwFileOffsetHigh 和dwFileOffsetLow来指定。(分别表示64位的高低位)

另外文件的偏移量必须是系统分配粒度的整数倍(windows系统分配粒度统一是64KB)


2)必须告诉系统要把数据文件中的多少字节映射到地址空间去。这和预定地址空间区域时需要指定区域大小是一样的道理。dwNumberOfBytesToMap用来指定大小。如果大小指定为0 系统会把文件中从偏移量开始到文件末尾的所有部分都映射到视图中。


如果在调用MapViewOfFile指定了FILE_MAP_COPY那么系统会从交换页中调拨物理存储器。调拨的物理存储器大小由dwNumberOfBytesToMap参数决定。在对文件映射视图进行操作时,只要不执行读取数据以外的任何操作,系统就不会用从页交换文件中调拨的页面。但是,一旦哪个线程写入文件视图中的任何内存地址,系统就会从页交换文件中已经调拨的页面中选择一个页面,把原始数据复制到页交换文件中的页面,然后把复制的页面映射到进程的地址空间中。此后各线程都将访问数据副本。


系统对原始数据进行复制时,系统会把页面的保护属性从PAGE_WRITECOPY改成PAGE_READWRITE例如一下代码:

(笔者对原始代码做了少量修改能够打印出虚拟地址的页属性。)

这是修改后的代码。

void testFileMapView(TCHAR * pszFileName){// Open the file that we want to map.HANDLE hFile = CreateFile(pszFileName, GENERIC_READ | GENERIC_WRITE, 0, NULL,OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);// Create a file-mapping object for the file.HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_WRITECOPY,0, 0, NULL);// Map a copy-on-write view of the file; the system will commit// enough physical storage from the paging file to accommodate// the entire file. All pages in the view will initially have// PAGE_WRITECOPY access.PBYTE pbFile = (PBYTE)MapViewOfFile(hFileMapping, FILE_MAP_COPY,0, 0, 0);// Read a byte from the mapped view.BYTE bSomeByte = pbFile[0];printAddressPageAttributes(&pbFile[0]);// When reading, the system does not touch the committed pages in// the paging file. The page keeps its PAGE_WRITECOPY attribute.// Wrie a byte to the mapped view.pbFile[0] = 0;printAddressPageAttributes(&pbFile[0]);// When writing for the first time, the system grabs a committed// page from the paging file, copies the original contents of the // page at the accessed memory address, and maps the new page// (the copy) into the process' address space. The new page has// an attribute of PAGE_READWRITE.// Write another byte to the mapped view.pbFile[1] = 0;printAddressPageAttributes(&pbFile[1]);// Because this byte is now in a PAGE_READWRITE page, the system// simply writes the byte to the page(backed by the paging file).// When finished using the file's mapped view, unmap it.// UnmapViewOfFile is discussed in the next section.UnmapViewOfFile(pbFile);// The system decommits the physical storage from the paging file.// Any writes to the page are lost.// Clean up after ourselves.CloseHandle(hFileMapping);CloseHandle(hFile);}

两个辅助函数用于打印当前地址的页面属性。

void GetProtectText(DWORD dwProtect, PTSTR szBuf, size_t inWords) {PCTSTR p = TEXT("Unknown");switch (dwProtect & ~(PAGE_GUARD | PAGE_NOCACHE | PAGE_WRITECOMBINE)) {case PAGE_READONLY:          p = TEXT("-R--"); break;case PAGE_READWRITE:         p = TEXT("-RW-"); break;case PAGE_WRITECOPY:         p = TEXT("-RWC"); break;case PAGE_EXECUTE:           p = TEXT("E---"); break;case PAGE_EXECUTE_READ:      p = TEXT("ER--"); break;case PAGE_EXECUTE_READWRITE: p = TEXT("ERW-"); break;case PAGE_EXECUTE_WRITECOPY: p = TEXT("ERWC"); break;case PAGE_NOACCESS:          p = TEXT("----"); break;}_tcscpy_s(szBuf, inWords, p);}void printAddressPageAttributes(PVOID pvAddress) {MEMORY_BASIC_INFORMATION mbi = { 0 };BOOL bOk = (VirtualQuery(pvAddress, &mbi, sizeof(mbi))== sizeof(mbi));if (!bOk)return;TCHAR szBuf[MAX_PATH] = { 0 };GetProtectText(mbi.Protect, szBuf, _countof(szBuf));_tprintf(TEXT("Address %p with the page protected :%s\n"), pvAddress, szBuf);}

运行结果:






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

不再需要把文件的数据映射到进程的地址空间中时,可以调用下面的函数来释放内存区域:

WINBASEAPIBOOLWINAPIUnmapViewOfFile(    _In_ LPCVOID lpBaseAddress    );
lpBaseAddress是区域的基地址,必须和MapViewOfFile返回值相同。

MapViewOfFile总是在进程地址空间中预定新区域,而不会释放之前预定的区域。


默认系统会对文件数据和页面进行缓存处理,可以调用FlushViewOfFile来强制写入磁盘。

WINBASEAPIBOOLWINAPIFlushViewOfFile(    _In_ LPCVOID lpBaseAddress,    _In_ SIZE_T dwNumberOfBytesToFlush    );

第一个参数是内存映射文件视图中第一个字节地址。(函数会把传入地址向下取整到页面大小的整数倍)

第二个参数是要刷新的字节数。系统会把这个数值向上取整。使总字节数称为页面大小的整数。

如果没有对文件进行过任何修改,该函数会直接返回。


如果内存映射文件的物理存储器来自网络。那么FlushViewOfFile会保证从当前工作站写入文件数据,但是无法保值远端的共享文件服务器也会把数据写入磁盘。(远端服务器可能会对其进行缓存)

为了确保服务器也会把数据写入磁盘,调用FILE_FLAG_WRITE_THROUGHCreateFile函数


调用UnmapViewOfFile有一个特性,如果最初试图采用FILE_MAP_COPY映射,那么对文件数据的修改实际是保存在页交换文件。直接调用UnmapViewOfFile不会对原始文件进行任何修改,从而导致数据丢失。


如果希望保留修改过的数据,必须自己进行额外操作。可以为同一个文件创建另一个文件映射对象,并用FILE_MAP_WRITE标志把这个性文件映射对象映射到进程地址空间中。

然后在第一个试图中查找具有PAGE_READWRITE保护属性的页面。只要找到一个具有该保护属性的页面,就可以对齐内容进行检查并决定是否需要将修改过的数据写入文件。如果不想修改也可以进行查找,直到文件尾。如果想保存修改只需调用MoveMemory把数据从第一个视图复制到第二个视图即可。由于第二个视图是用PAGE_READWRIE保护属性映射到的,MoveMemory函数会更新文件位于磁盘上的实际内容。可以用此方法来校验修改和保存数据。


17.3.5 第五步和第六步:关闭文件映射对象和文件对象。

一个正常操作文件映射和文件对象并销毁的例子(伪代码)

HANDLE hFile = CreateFile(...);HANDLE hFileMapping = CreateFileMapping(hFile, ...);PVOID pvFile = MapViewOfFile(hFileMapping, ...);// Use the memory-mapped file.UnmapViewOfFile(pvFile);CloseHandle(hFileMapping);CloseHandle(hFile);


因为调用MapViewOfFile会增加文件对象和文件映射对象的引用计数器。因此可以这样写。

HANDLE hFile = CreateFile(...);HANDLE hFileMapping = CreateFileMapping(hFile, ...);CloseHandle(hFile);PVOID pvFile = MapViewOfFile(hFileMapping, ...);CloseHandle(hFileMapping);// Use the memory-mapped file.UnmapViewOfFile(pvFile);

如果要用一个文件创建多个文件映射对象,或同一个文件映射对象要映射多个试图,那么不能过早调用CloseHandle。因为后续还需要使用这些句柄。


17.3.6 File Reserve实例程序

如何使用内存映射文件将一个ANSIUNICODE文本文件的内容颠倒过来。(但不能正确处理二进制文件)

通过调用IsTextUnicode来检测文本文件是ANSI还是Unicode编码格式


改程序首先会给原始文件创建一份副本(防止破坏原始文件不可再用)并使用CreateFile以读写的方式打开。

使用GetFileSize获得文件载入以后在末尾写入\0


最后对\r\n 颠倒以后进行修复。


由于增加了结尾的\0 会使文件增加了一个字节,这时候需要重新设置文件大小。在UnMapViewOfFile和Close FileMapping对象以后

SetFilePointer(hFile, dwFileSize, NULL, FILE_BEGIN);

SetEndOfFile(hFile);



最后FileReverse会启动记事本打开颠倒后的文件。



代码如下:

/******************************************************************************Module:  FileRev.cppNotices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre******************************************************************************/#include "..\CommonFiles\CmnHdr.h"#include <windowsx.h>#include <tchar.h>#include <commdlg.h>#include <string.h>#include "Resource.h"//////////////////////////////////////////////////////////////////////////#define FILENAME TEXT("FileRev.dat")//////////////////////////////////////////////////////////////////////////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 a the very end of the file.PSTR pchANSI = (PSTR)pvFile;pchANSI[dwFileSize / sizeof(CHAR)] = 0;// Reserve 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;}//////////////////////////////////////////////////////////////////////////BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {chSETDLGICONS(hWnd, IDI_FILEREV);// Initialize the dialog box by disabling the Reverse buttonEnableWindow(GetDlgItem(hWnd, IDC_REVERSE), FALSE);return TRUE;}//////////////////////////////////////////////////////////////////////////void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {TCHAR szPathname[MAX_PATH];switch (id) {case IDCANCEL:EndDialog(hWnd, id);break;case IDC_FILENAME:EnableWindow(GetDlgItem(hWnd, IDC_REVERSE),Edit_GetTextLength(hWndCtl) > 0);break;case IDC_REVERSE:GetDlgItemText(hWnd, IDC_FILENAME, szPathname, _countof(szPathname));// Make a copy of input file so that we don't destroy itif (!CopyFile(szPathname, FILENAME, FALSE)) {chMB("New file could not be created.");break;}BOOL bIsTextUnicode;if (FileReverse(FILENAME, &bIsTextUnicode)) {SetDlgItemText(hWnd, IDC_TEXTTYPE,bIsTextUnicode ? TEXT("Unicode") : TEXT("ANSI"));// Spawn Notepad to see the fruits of our labors.STARTUPINFO si = { sizeof(si) };PROCESS_INFORMATION pi;TCHAR sz[] = TEXT("Notepad ") FILENAME;if (CreateProcess(NULL, sz,NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {CloseHandle(pi.hThread);CloseHandle(pi.hProcess);}}break;case IDC_FILESELECT:OPENFILENAME ofn = { OPENFILENAME_SIZE_VERSION_400 };ofn.hwndOwner = hWnd;ofn.lpstrFile = szPathname;ofn.lpstrFile[0] = 0;ofn.nMaxFile = _countof(szPathname);ofn.lpstrTitle = TEXT("Select file for reversing");ofn.Flags = OFN_EXPLORER | OFN_FILEMUSTEXIST;GetOpenFileName(&ofn);SetDlgItemText(hWnd, IDC_FILENAME, ofn.lpstrFile);SetFocus(GetDlgItem(hWnd, IDC_REVERSE));break;}}//////////////////////////////////////////////////////////////////////////INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {switch (uMsg) {chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog);chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand);}return FALSE;}//////////////////////////////////////////////////////////////////////////int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR, int) {DialogBox(hInstExe, MAKEINTRESOURCE(IDD_FILEREV), NULL, Dlg_Proc);return 0;}//////////////////////////////// End of File //////////////////////////////////

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

只映射文件的一个视图,这个视图只包含文件的一小部分数据。先映射文件的开头,完成对齐访问以后,撤销对这一部分的映射,然后把文件的另一部分映射到视图中。一直重复这个过程,直到完成对整个文件的访问。


一个例子 在32位地址空间使用8GB文件的例子。 以下函数用来统计一个二进制文件中所有数值为0的字节数:

原书例子默认使用64KB的分配粒度和单线程来进行操作。

笔者自行修改了代码尝试支持增大分配粒度和支持多线程。完整测试和代码参考:

http://blog.csdn.net/sesiria/article/details/78684165


这里仅贴出扩展后的CountZeros函数:

//__int64 CountZeros(LPCTSTR szFileName, DWORD dwBlockSize, DWORD nThreads = 1, bool bUseFileMapp = true) {// Vies must always start on a multiple// of the allocation granularitySYSTEM_INFO sinf;GetSystemInfo(&sinf);// Open the data file.HANDLE hFile = CreateFile(szFileName, GENERIC_READ,FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);DWORD dwFileSizeHigh;__int64 qwFileSize = GetFileSize(hFile, &dwFileSizeHigh);qwFileSize += (((__int64)dwFileSizeHigh) << 32);// ceiling algorithmDWORD dwBlockCount = (dwBlockSize + sinf.dwAllocationGranularity - 1) / sinf.dwAllocationGranularity;dwBlockSize = sinf.dwAllocationGranularity * dwBlockCount;if (!bUseFileMapp) {LONGLONG lLastTimeStamp = stopwatch.Now();_tprintf(TEXT("Start to count the file with block size %d\n%s with size %I64dBytes\n"),dwBlockSize, szFileName, qwFileSize);__int64 qwNumOfZeros = CountWithFileMultiThread(hFile, qwFileSize, dwBlockSize, nThreads);LONGLONG lElpasedTime = stopwatch.Now() - lLastTimeStamp;_tprintf(TEXT("Count finished in %lldms\n"),lElpasedTime);CloseHandle(hFile);return qwNumOfZeros;}// Create the file-mapping object.HANDLE hFileMapping = CreateFileMapping(hFile, NULL,PAGE_READONLY, 0, 0, NULL);// We no longer need access the file object's handle.CloseHandle(hFile);__int64 qwFileOffset = 0, qwNumOfZeros = 0;LONGLONG lLastTimeStamp = stopwatch.Now();_tprintf(TEXT("Start to count the file with block size %d\n%s with size %I64dBytes\n"), dwBlockSize, szFileName, qwFileSize);if (nThreads > 1) // support multiple threads {qwNumOfZeros = CountWithFileMapMultiThreads(hFileMapping, qwFileSize, dwBlockSize, nThreads);}else {while (qwFileSize > 0) {// Determine the number of bytes to be mapped in this viewDWORD dwBytesInBlock = dwBlockSize;if (qwFileSize < dwBlockSize)dwBytesInBlock = (DWORD)qwFileSize;PBYTE pbFile = (PBYTE)MapViewOfFile(hFileMapping, FILE_MAP_READ,(DWORD)(qwFileOffset >> 32),(DWORD)(qwFileOffset & 0xFFFFFFFF),dwBytesInBlock);// Count the number of 0s in this block.for (DWORD dwByte = 0; dwByte < dwBytesInBlock; dwByte++) {if (pbFile[dwByte] == 0)qwNumOfZeros++;}// Unmap the view; we don't want multiple vies// in our address space.UnmapViewOfFile(pbFile);// Skip to the next set of bytes in the file.qwFileOffset += dwBytesInBlock;qwFileSize -= dwBytesInBlock;}}LONGLONG lElpasedTime = stopwatch.Now() - lLastTimeStamp;_tprintf(TEXT("Count finished in %lldms\n"),lElpasedTime);CloseHandle(hFileMapping);return qwNumOfZeros;}


17.5 内存映射文件和一致性

系统允许把同一个文件中的数据映射到多个视图中,例如,可以把一个文件的前10KB映射到一个视图,然后再把前4KB映射到另一个视图。

只要我们映射的是同一个文件映射对象,那么系统会确保各视图中的数据是一致的。

例如一个线程修改了视图中的内容,那么系统会更新所有其他视图以反映修改后的文件内容。(使用WRITE_COPY例外,因为会创建副本页面)

因为这些视图所对应的数据文件的每个页面在内存中只有一份。但这些内存会被映射到多个地址空间中。



如果两个应用同时打开同一个文件一个对文件进行修改,另一个是进程是一无所知的。因此若要对文件进行读写操作。最好传0给dwShareMode,表示要独占对文件的访问。

由于只读模式不存在一致性的问题,因此他非常适合用于内存映射文件。绝不应该用内存映射文件来跨网络共享写文件,因为系统无法保证数据视图的一致性。一台机器更新了文件内容,而另一台机器缺一无所知,从而导致它继续使用内存中的原始数据。


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

使用MapViewOfFileEx 代替MapViewOfFile来指定基地址。可以把文件映射到指定的地址:

WINBASEAPI_Ret_maybenull_  __out_data_source(FILE)LPVOIDWINAPIMapViewOfFileEx(    _In_ HANDLE hFileMappingObject,    _In_ DWORD dwDesiredAccess,    _In_ DWORD dwFileOffsetHigh,    _In_ DWORD dwFileOffsetLow,    _In_ SIZE_T dwNumberOfBytesToMap,    _In_opt_ LPVOID lpBaseAddress    );

增加了最后一个参数lpBaseAddress。 可以给要映射的文件制定一个目标地址。同VirtualAlloc一样,指定的目标地址必须是分配粒度(64KB)的整数倍。否则MapViewOfFileEx将返回NULL


如果系统无法将文件映射到指定地址(通常是文件太大,因此导致与其他预定的地址空间发生重叠)那么函数会返回NULL. MapViewOfFileEx并不会尝试去寻找另一个能容纳文件的地址空间。也可以传递NULL给MapViewOfFileEx这样他和MapViewOfFile功能完全一样。


使用MapViewOfFileEx在跨进程共享数据非常有用。如果两个或多个应用程序要共享一组数据结构,这些数据结构有一些指针指向其他数据结构,那么我们可能要给内存映射文件指定一个地址。例如链表。可是,链表的节点指向的其他地址未必在共享的地址空间中。这可能导致错误的内存访问。


这需要第一个链表在构造的时候确保其所有子节点所指向的地址都在将要共享的那块区域。同时第二个进程在映射地址空间的时候使用MapViewOfFileEx映射相同的基地址。这样就不会有问题。


第二种方法是链表在构造节点的时候,每个节点不直接保存地址,而是保存偏移量。可以通过偏移量来找到下一个节点。这种方法会增加代码的复杂度效率较低。也容易搞错。




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

在进程能够从自己地址空间中访问内存映射文件的数据之前,windows要求进程先调用MapViewOfFile。 然后系统在进程地址空间中为视图预定一个区域,任何其他进程都无法看到这个视图。如果另一个进程想要访问同一个文件映射对象中的数据,那么第二个进程也必须调用MapViewOfFile。这样系统就会在第二个进程的地址空间中为视图预定一块区域。


两个进程映射同一个FileMapping的时候返回的内存地址可能是不一样的。(除非用MapViewOfFileEx自行设定)。


看看另一个实现细节,以下代码实现了同一个文件映射对象的两个视图:

int _tmain(int argc, TCHAR* argv[], TCHAR * env[]){TCHAR szFileName[] = TEXT("d:\\test.pdf");// Open a existing file--it must be bigger than 64KB.HANDLE hFile = CreateFile(szFileName, GENERIC_READ | GENERIC_WRITE,0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);// Create a file-mapping object backed by the data file.HANDLE hFileMapping = CreateFileMapping(hFile, NULL,PAGE_READWRITE, 0, 0, NULL);// Map a view of the whole file into our address space.PBYTE pbFile = (PBYTE)MapViewOfFile(hFileMapping,FILE_MAP_WRITE, 0, 0, 0);// Map a view of the file (starting 64KB in) into our address spacePBYTE pbFile2 = (PBYTE)MapViewOfFile(hFileMapping,FILE_MAP_WRITE, 0, 65536, 0);// Show that the two view are not 64KB away from each other// in the address space, meaning that there is no overlap.int iDfference = int(pbFile2 - pbFile);TCHAR szMsg[100];StringCchPrintf(szMsg, _countof(szMsg),TEXT("Pointers difference = %d KB"), iDfference / 1024);MessageBox(NULL, szMsg, NULL, MB_OK);UnmapViewOfFile(pbFile2);UnmapViewOfFile(pbFile);CloseHandle(hFileMapping);CloseHandle(hFile);return 0;}





两次调用MapViewOfFile使得windows分别预定了两块不同的地址空间区域。第一个块就是文件映射对象的大小,而第二块是文件映射对象的大小将去64KB。

虽然他们是两个不同的区域,也不互相重叠。但是由于他们是同一个文件映射对象的视图,系统会保证其中的数据始终是一致的。


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

windows有许多IPC机制。RPC,COM,OLE,DDE,Windows消息,剪贴板,邮件槽,管道,套接字等。

在Windows中,在同一台机器上共享数据的最底层就是内存映射文件。如果要求低开销和高性能,内存映射文件无疑是最好的选择。

这种机制是通过两个或多个进程映射同一个文件映射对象的视图来实现的,也就是进程间共享相同的物理存储页面。当一个进程在文件映射对象的视图中写入数据的时候,其他进程会在他们的视图中立刻看到变化。

注意:多个进程共享同一个文件映射对象来说,所有进程使用的文件映射对象的名称必须完全相同。


一个例子:

启动应用程序。当一个应用程序启动时,系统会调用CreateFile来打开磁盘上的exe文件。接着系统调用CreateFileMapping来创建文件映射对象。最后系统会以新创建的进程的名义调用MapViewOfFileEx(并传入SEG_IMAGE标志)这样就把exe文件映射到了进程的地址空间中。(调用MapViewOfFileEx是为了把文件映射到指定的基地址,这个基地址保存在exe的PE文件头中)系统然后创建进程的主线程,在映射得到的视图中取得可执行代码的第一个字节的地址,把该地址放到线程的指令指针中,最后让cpu开始执行其中的代码。


如果用户启动应用程序的第二个实例,系统会发现该exe已经有一个文件映射对象,因此就不会再创建一个新的文件对象或文件映射对象。取而代之,系统会再次映射exe文件的一个视图,但这次是在新创建的进程的地址空间中。这样系统就把同一个文件同时映射到了两个地址空间中


通过句柄继承,命名和句柄复制来共享内核对象。从而达到进程间数据共享。


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

有些进程会创建临时数据并共享给其他进程,为了共享数据而必须让应用程序创建数据文件这将非常麻烦。

MS支持创建以页交换文件为后备存储器的内存映射文件,这样就不需要磁盘上专门的文件来作为后备存储器。

不需要再调用CreateFile

只需要直接调用CreateFileMapping 并传入INVALID_HANDLE_VALUE作为hFile的参数传入。所分配的存储器大小由CreateFileMappingdwMaximusSizeHighdwMaximumSizeLow参数决定。

一旦创建了文件映射对象,并把一个视图映射到进程地址空间中,就可以像使用任何内存区域一样使用它了。

如果想要和其他进程共享数据,那么可以在调用CreateFileMapping的时候传一个以0结尾的字符串给pszName

其他进程能够给以同名参数来调用CreateFileMappingOpenFileMapping


当一个进程不再需要访问文件映射对象的时候, 应该调用CloseHandle。所有句柄都关闭的时候,系统会从页交换文件中收回所以已调拨的存储器。






Memory-Mapped File Sharing 示例程序


通过创建一个名为MMFSharedData的FileMapping 对象在进程间共享数据。


源代码:

/******************************************************************************Module:  MMFShare.cppNotices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre******************************************************************************/#include "..\CommonFiles\CmnHdr.h"#include <windowsx.h>#include <tchar.h>#include "resource.h"//////////////////////////////////////////////////////////////////////////BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {chSETDLGICONS(hWnd, IDI_MMFSHARE);// Initialize the edit control with some test data.Edit_SetText(GetDlgItem(hWnd, IDC_DATA), TEXT("Some test data"));// Disable the Close button because the file can't// be closed if it was never created or opened.Button_Enable(GetDlgItem(hWnd, IDC_CLOSEFILE), FALSE);return TRUE;}//////////////////////////////////////////////////////////////////////////void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {// Handle of the open memory-mapped filestatic 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 4KB 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 button.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;}}//////////////////////////////////////////////////////////////////////////INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {switch (uMsg) {chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog);chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand);}return FALSE;}//////////////////////////////////////////////////////////////////////////int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR, int) {DialogBox(hInstExe, MAKEINTRESOURCE(IDD_MMFSHARE), NULL, Dlg_Proc);return 0;}//////////////////////////////// End of File //////////////////////////////////


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

例如需要在两个进程之间共享一个电子表格数据结构。一开始并不一定需要调拨所有表格的空间。CreateFileMapping提供了一种方法,在fdwProtect参数中指定SEC_RESERVESEC_COMMIT标志。


只有当以页交换文件为后备存储器来创建文件映射对象的时候,这些标志才有意义。

SEC_COMMIT标志让CreateFileMapping从页交换文件中调拨存储器,这和不指定任何标志具有相同的效果。


如果在调用CreateFileMapping中传入SEC_RESERVE标志,那么系统并不会从页交换文件中调拨物理存储器,只会返回映射对象的句柄。 可以用MapViewOfFileMapViewOfFileEx来给这个文件映射创建一个视图。他们会预定一块地址空间区域,但不会给区域调拨任何物理存储器。(视图访问其中的内存地址会直接引发访问违规)

同样其他进程若映射了同一个FileMapping对象的视图,视图访问所获得的地址也会导致访问违规。


为了给共享区域调拨物理存储器,这时可以直接调用

VirtualAlloc

WINBASEAPI_Ret_maybenull_ _Post_writable_byte_size_(dwSize)LPVOIDWINAPIVirtualAlloc(    _In_opt_ LPVOID lpAddress,    _In_ SIZE_T dwSize,    _In_ DWORD flAllocationType,    _In_ DWORD flProtect    );

一旦给用MapViewOfFileMapViewOfFileEx预定的区域调拨了存储器,所有映射了同一个文件映射对象的视图的其他进程就可以成功访问已调拨的页面了。


通过SEC_RESERVEVirtualAlloc,不仅能和其他进程共享电子表格的数据,还能高效使用物理存储器。


NTFS文件系统提供对稀疏(sparse file)的支持。可以用来创建和处理稀疏文件,存储器就不必总是在页交换文件中,可以在普通磁盘中。


例如有一个音频录音程序,用来录制用户的音频可能是5分钟也可能是5小时。需要一个足够大的文件来保存这些数据。


什么是稀疏文件?

稀疏文件,这是UNIX类和NTFS等文件系统的一个特性。
开始时,一个稀疏文件不包含用户数据,也没有分配到用来存储用户数据的磁盘空间。当数据被写入稀疏文件时,NTFS逐渐地为其分配磁盘空间。一个稀疏文件有可能增长得很大。稀疏文件以64KB(不同文件系统不同)为单位增量增长,因此磁盘上稀疏文件的大小总是64KB的倍数。
稀疏文件就是在文件中留有很多空余空间,留备将来插入数据使用。如果这些空余空间被ASCII码的NULL字符占据,并且这些空间相当大,那么,这个文件就被称为稀疏文件,而且,并不分配相应的磁盘块。
这样,会产生一个问题,文件已被创建了,但相应的磁盘空间并未被分配,只有在有真正的数据插入进来时,才会被分配磁盘块,如果这时文件系统被占满了,那么对该文件的写操作就会失败。为防止这种情况,有两种办法:不产生稀疏文件或为稀疏文件留够空间。
在计算机科学方面,稀疏文件是文件系统中的一种文件存储方式,在创建一个文件的时候,就预先分配了文件需要的连续存储空间,其空间内部大多都还未被数据填充现在有很多文件系统都支持稀疏文件,包括大部分的Unix和NTFS 。
稀疏文件被普遍用来磁盘镜像,数据库快照,日志文件,还有其他科学运用上。


Sparse Memory-Mapped File 示例程序

展示了如何创建一个以NTFS稀疏文件为后备存储器的内存映射文件

点击Create SparseMMF 会在NTFS卷上创建一个稀疏文件,并把他映射到进程的地址空间中。

Write写入一个字节,这个操作可能会是文件系统为文件的一部分调拨后备存储器。并更新Allocated Ranges

Free All Allocated Regions 会释放文件所占用的所有存储器,这个功能用来释放磁盘空间并给文件中的所有字节清零



MMFSparse.cpp

/******************************************************************************Module:  MMFSparse.cppNotices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre******************************************************************************/#include "..\CommonFiles\CmnHdr.h"#include <tchar.h>#include <windowsx.h>#include <strsafe.h>#include "SparseStream.h"#include "resource.h"//////////////////////////////////////////////////////////////////////////// This class make it easy to work with memory=mapped sparse filesclass CMMFSparse : public CSparseStream {private:HANDLE m_hFileMap;// File-mapping objectPVOID m_pvFile;// Address to start of mapped filepublic:// Create a Sparse MMF and maps it in the process' address space.CMMFSparse(HANDLE hStream = NULL, DWORD dwStreamSizeMAxLow = 0,DWORD dwStreamSizeMaxHigh = 0);// Closes a Sparse MMFvirtual ~CMMFSparse() { ForceClose(); }// Creates a sparse MMF and maps it in the process' address space.BOOL Initialize(HANDLE hStream, DWORD dwStreamSizeMAxLow,DWORD dwStreamSizeMaxHigh = 0);// MMF to BYTE cast operator returns address of first byte// in the memory-mapped sparse file.operator PBYTE() const { return (PBYTE)m_pvFile; };// Allows you to explicitly close the MMF without having// to wait for the destructor to be called.VOID ForceClose();};//////////////////////////////////////////////////////////////////////////CMMFSparse::CMMFSparse(HANDLE hStream, DWORD dwStreamSizeMaxLow,DWORD dwStreamSizeMaxHigh) {Initialize(hStream, dwStreamSizeMaxLow, dwStreamSizeMaxHigh);}//////////////////////////////////////////////////////////////////////////BOOL CMMFSparse::Initialize(HANDLE hStream, DWORD dwStreamSizeMaxLow,DWORD dwStreamSizeMaxHigh) {if (m_hFileMap != NULL)ForceClose();// Initialize to NULL in case something goes wrongm_hFileMap = m_pvFile = NULL;BOOL bOk = TRUE;// Assume successif (hStream != NULL) {if ((dwStreamSizeMaxLow == 0) && (dwStreamSizeMaxHigh == 0)) {DebugBreak();// Illegal stream size}CSparseStream::Initialize(hStream);bOk = MakeSparse();// Make the stream sparseif (bOk) {// Create a file-mapping objectm_hFileMap = ::CreateFileMapping(hStream, NULL, PAGE_READWRITE,dwStreamSizeMaxHigh, dwStreamSizeMaxLow, NULL);if (m_hFileMap != NULL) {// Map the stream into the process' address spacem_pvFile = ::MapViewOfFile(m_hFileMap,FILE_MAP_WRITE | FILE_MAP_READ, 0, 0, 0);}else {// Failed to map the file, cleanupCSparseStream::Initialize(NULL);ForceClose();bOk = FALSE;}}}return bOk;}//////////////////////////////////////////////////////////////////////////VOID CMMFSparse::ForceClose() {// Cleanup everything that was done successfullyif (m_pvFile != NULL) {::UnmapViewOfFile(m_pvFile);m_pvFile = NULL;}if (m_hFileMap != NULL) {::CloseHandle(m_hFileMap);m_hFileMap = NULL;}}//////////////////////////////////////////////////////////////////////////#define STREAMSIZE(1 * 1024 * 1024)// !MB (1024KB)HANDLE g_hStream = INVALID_HANDLE_VALUE;CMMFSparse g_mmf;TCHAR g_szPathname[MAX_PATH] = TEXT("\0");//////////////////////////////////////////////////////////////////////////BOOL Dlg_OnInitDialog(HWND hWnd, HWND hWndFocus, LPARAM lParam) {chSETDLGICONS(hWnd, IDI_MMFSPARSE);// Initialize the dialog box controls.EnableWindow(GetDlgItem(hWnd, IDC_OFFSET), FALSE);Edit_LimitText(GetDlgItem(hWnd, IDC_OFFSET), 4);SetDlgItemInt(hWnd, IDC_OFFSET, 1000, FALSE);EnableWindow(GetDlgItem(hWnd, IDC_BYTE), FALSE);Edit_LimitText(GetDlgItem(hWnd, IDC_BYTE), 3);SetDlgItemInt(hWnd, IDC_BYTE, 5, FALSE);EnableWindow(GetDlgItem(hWnd, IDC_WRITEBYTE), FALSE);EnableWindow(GetDlgItem(hWnd, IDC_READBYTE), FALSE);EnableWindow(GetDlgItem(hWnd, IDC_FREEALLOCATEDREGIONS), FALSE);// Store the file in a writable folderGetCurrentDirectory(_countof(g_szPathname), g_szPathname);_tcscat_s(g_szPathname, _countof(g_szPathname), TEXT("\\MMFSparse"));// Check to see if the volume supports sparse filesTCHAR szVolume[16];PTSTR pEndOfVolume = _tcschr(g_szPathname, _T('\\'));if (pEndOfVolume == NULL) {chFAIL("Impossible to find the Volume for the default document folder.");DestroyWindow(hWnd);return TRUE;}_tcsncpy_s(szVolume, _countof(szVolume),g_szPathname, pEndOfVolume - g_szPathname + 1);if (!CSparseStream::DoesFileSystemSupportSparseStreams(szVolume)) {chFAIL("Volume of default document folder does not suppoort sparse MMF.");DestroyWindow(hWnd);return TRUE;}return TRUE;}//////////////////////////////////////////////////////////////////////////void Dlg_ShowAllocatedRanges(HWND hWnd) {// Fill in the Allocated Ranges edit controlDWORD dwNumEntries;FILE_ALLOCATED_RANGE_BUFFER* pfarb =g_mmf.QueryAllocatedRanges(&dwNumEntries);if (dwNumEntries == 0) {SetDlgItemText(hWnd, IDC_FILESTATUS,TEXT("No allocated ranges in the file"));}else {TCHAR sz[4096] = { 0 };for (DWORD dwEntry = 0; dwEntry < dwNumEntries; dwEntry++) {StringCchPrintf(_tcschr(sz, _T('\0')), _countof(sz) - _tcslen(sz),TEXT("Offset: %7.7u, Length: %7.7u\r\n"),pfarb[dwEntry].FileOffset.LowPart, pfarb[dwEntry].Length.LowPart);}SetDlgItemText(hWnd, IDC_FILESTATUS, sz);}g_mmf.FreeAllocatedRanges(pfarb);}//////////////////////////////////////////////////////////////////////////void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, INT codeNotify) {switch (id) {case IDCANCEL:if (g_hStream != INVALID_HANDLE_VALUE)CloseHandle(g_hStream);EndDialog(hWnd, id);break;case IDC_CREATEMMF:{g_hStream = CreateFile(g_szPathname, GENERIC_READ | GENERIC_WRITE,0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);if (g_hStream == INVALID_HANDLE_VALUE) {chFAIL("Failed to create file.");return;}// Create a 1MB (1024 KB) MMF using the fileif (!g_mmf.Initialize(g_hStream, STREAMSIZE)) {chFAIL("Failed to initialize Sparse MMF.");CloseHandle(g_hStream);g_hStream = NULL;return;}Dlg_ShowAllocatedRanges(hWnd);// Enable/disable the other controls.EnableWindow(GetDlgItem(hWnd, IDC_CREATEMMF), FALSE);EnableWindow(GetDlgItem(hWnd, IDC_OFFSET), TRUE);EnableWindow(GetDlgItem(hWnd, IDC_BYTE), TRUE);EnableWindow(GetDlgItem(hWnd, IDC_WRITEBYTE), TRUE);EnableWindow(GetDlgItem(hWnd, IDC_READBYTE), TRUE);EnableWindow(GetDlgItem(hWnd, IDC_FREEALLOCATEDREGIONS), TRUE);// Force the Offset edit control to have the focus.SetFocus(GetDlgItem(hWnd, IDC_OFFSET));}break;case IDC_WRITEBYTE:{BOOL bTranslated;DWORD dwOffset = GetDlgItemInt(hWnd, IDC_OFFSET, &bTranslated, FALSE);if (bTranslated) {g_mmf[dwOffset * 1024] = (BYTE)GetDlgItemInt(hWnd, IDC_BYTE, NULL, FALSE);Dlg_ShowAllocatedRanges(hWnd);}}break;case IDC_READBYTE:{BOOL bTranslated;DWORD dwOffset = GetDlgItemInt(hWnd, IDC_OFFSET, &bTranslated, FALSE);if (bTranslated) {SetDlgItemInt(hWnd, IDC_BYTE, g_mmf[dwOffset * 1024], FALSE);Dlg_ShowAllocatedRanges(hWnd);}}break;case IDC_FREEALLOCATEDREGIONS:// Normally the destructor causes the file-mapping to close.// But, in this case, we want to force it so that we can reset// a portion of the file back to all zeros.g_mmf.ForceClose();// We call ForceClose above because attempting to zero a portion of// the file while it is mapped, causes DeviceIoControl to fail with// error ERROR_USER_MAPPED_FILE ("The requested operation cannot// be performed on a file with a user-mapped section open.")g_mmf.DecommitPortionOfStream(0, STREAMSIZE);// We need to close the file handle and reopen it in order to// flush the sparse state.CloseHandle(g_hStream);g_hStream = CreateFile(g_szPathname, GENERIC_READ | GENERIC_WRITE,0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);if (g_hStream == INVALID_HANDLE_VALUE) {chFAIL("Failed to create file.");return;}// Reset the MMF wrapper for the new file handle.g_mmf.Initialize(g_hStream, STREAMSIZE);// Update the UI.Dlg_ShowAllocatedRanges(hWnd);break;}}//////////////////////////////////////////////////////////////////////////INT_PTR WINAPI Dlg_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {switch (uMsg) {chHANDLE_DLGMSG(hWnd, WM_INITDIALOG, Dlg_OnInitDialog);chHANDLE_DLGMSG(hWnd, WM_COMMAND, Dlg_OnCommand);}return FALSE;}//////////////////////////////////////////////////////////////////////////int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int) {   DialogBox(hInstExe, MAKEINTRESOURCE(IDD_MMFSPARSE), NULL, Dlg_Proc);   return(0);}//////////////////////////////// End of File //////////////////////////////////

SparseStream.h

/******************************************************************************Module:  SparseStream.hNotices: Copyright (c) 2007 Jeffrey Richter & Christophe Nasarre******************************************************************************/#include "..\CommonFiles\CmnHdr.h"#include <winioctl.h>//////////////////////////////////////////////////////////////////////////#pragma  once//////////////////////////////////////////////////////////////////////////class CSparseStream {// public static method.public:static BOOL DoesFileSystemSupportSparseStreams(PCTSTR pszVolume);static BOOL DoesFileContainAnySparseStreams(PCTSTR pszPathname);// public methodpublic:CSparseStream(HANDLE hStream = INVALID_HANDLE_VALUE) {Initialize(hStream);}virtual ~CSparseStream(){}void Initialize(HANDLE hStream = INVALID_HANDLE_VALUE) {m_hStream = hStream;}public:operator HANDLE() const { return (m_hStream); }public:BOOL IsStreamSparse() const;BOOL MakeSparse();BOOL DecommitPortionOfStream(__int64 qwFileOffsetStart, __int64 qwFileOffsetEnd);FILE_ALLOCATED_RANGE_BUFFER* QueryAllocatedRanges(PDWORD pdwNumEntries);BOOL FreeAllocatedRanges(FILE_ALLOCATED_RANGE_BUFFER* pfarb);private:static BOOL AreFlagsSet(DWORD fdwFlagBits, DWORD fFlagsToCheck) {return ((fdwFlagBits & fFlagsToCheck) == fFlagsToCheck);}private:HANDLE m_hStream;};//////////////////////////////////////////////////////////////////////////inline BOOL CSparseStream::DoesFileSystemSupportSparseStreams(PCTSTR pszVolume) {DWORD dwFileSystemFlags = 0;BOOL bOk = GetVolumeInformation(pszVolume, NULL, 0, NULL, NULL,&dwFileSystemFlags, NULL, 0);bOk = bOk && AreFlagsSet(dwFileSystemFlags, FILE_SUPPORTS_SPARSE_FILES);return bOk;}//////////////////////////////////////////////////////////////////////////inline BOOL CSparseStream::IsStreamSparse() const {BY_HANDLE_FILE_INFORMATION bhfi;GetFileInformationByHandle(m_hStream, &bhfi);return (AreFlagsSet(bhfi.dwFileAttributes, FILE_ATTRIBUTE_SPARSE_FILE));}//////////////////////////////////////////////////////////////////////////inline BOOL CSparseStream::MakeSparse() {DWORD dw;return (DeviceIoControl(m_hStream, FSCTL_SET_SPARSE,NULL, 0, NULL, 0, &dw, NULL));}//////////////////////////////////////////////////////////////////////////inline BOOL CSparseStream::DecommitPortionOfStream(__int64 qwOffsetStart, __int64 qwOffsetEnd) {// Note: This function does not work if this file is memory-mapped.DWORD dw;FILE_ZERO_DATA_INFORMATION fzdi;fzdi.FileOffset.QuadPart = qwOffsetStart;fzdi.BeyondFinalZero.QuadPart = qwOffsetEnd + 1;return (DeviceIoControl(m_hStream, FSCTL_SET_ZERO_DATA, (PVOID)&fzdi,sizeof(fzdi), NULL, 0, &dw, NULL));}//////////////////////////////////////////////////////////////////////////inline BOOL CSparseStream::DoesFileContainAnySparseStreams(PCTSTR pszPathname) {DWORD dw = GetFileAttributes(pszPathname);return ((dw == 0xfffffff)? FALSE : AreFlagsSet(dw, FILE_ATTRIBUTE_SPARSE_FILE));}//////////////////////////////////////////////////////////////////////////inline FILE_ALLOCATED_RANGE_BUFFER * CSparseStream::QueryAllocatedRanges(PDWORD pdwNumEntries) {FILE_ALLOCATED_RANGE_BUFFER farb;farb.FileOffset.QuadPart = 0;farb.Length.LowPart =GetFileSize(m_hStream, (PDWORD)& farb.Length.HighPart);// There is no way to determine the correct memory block size prior to// attemping to collect this data, so I just picked 100 * sizeof(*pfarb)DWORD cb = 100 * sizeof(farb);FILE_ALLOCATED_RANGE_BUFFER* pfarb = (FILE_ALLOCATED_RANGE_BUFFER*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, cb);DeviceIoControl(m_hStream, FSCTL_QUERY_ALLOCATED_RANGES,&farb, sizeof(farb), pfarb, cb, &cb, NULL);*pdwNumEntries = cb / sizeof(*pfarb);return pfarb;}//////////////////////////////////////////////////////////////////////////inline BOOL CSparseStream::FreeAllocatedRanges(FILE_ALLOCATED_RANGE_BUFFER* pfarb) {// Free the queue entry's allocated memoryreturn HeapFree(GetProcessHeap(), 0, pfarb);}///////////////////////////////// End Of File /////////////////////////////////



阅读全文
0 0
原创粉丝点击