使用内存映射文件的共有以下几步:
1) 创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件。
2) 创建一个文件映射内核对象,告诉系统该文件的大小和你打算如何访问该文件。
3) 让系统将文件映射对象的全部或一部分映射到你的进程地址空间中。
当完成对内存映射文件的使用时,必须执行下面这些步骤将它清除:
1) 告诉系统从你的进程的地址空间中撤消文件映射内核对象的映像。
2) 关闭文件映射内核对象。
3) 关闭文件内核对象。
步骤1:
:创建或打开一个文件内核对象,都要调用C r e a t e F i l e函数:
HANDLE CreateFile(
PCSTR pszFileName, //文件路径
DWORD dwDesiredAccess, //G E N E R I C _ R E A D 参数 可以从文件中读取数据
DWORD dwShareMode, //FILE_SHARE_READ|FILE_SHARE_WRITE 打开文件均成功
PSECURITY_ATTRIBUTES psa,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile);
步骤2::创建一个mapping文件对象,
HANDLE CreateFileMapping(
HANDLE hFile, //前面第一步的返回值
PSECURITY_ATTRIBUTES psa, //null
DWORD fdwProtect,//部分保护属性 PAGE_READONLY
DWORD dwMaximumSizeHigh, //两个最重要的参数,这两个参数将告诉系统该文件的最大字节数,对于4 GB 或小于4 GB的文件来说, H i g h的值将始终是0。
DWORD dwMaximumSizeLow, //低32位文件大小,单位为byte,512*1024*1024,500m
PCTSTR pszName);//内存映射数据文件通常并不需要被共享,因此这个参数通常是N U L L
步骤3:将文件数据映射到进程的地址空间
PVOID MapViewOfFile(
HANDLE hFileMappingObject,//第二步返回值
DWORD dwDesiredAccess,//F I L E _ M A P _ R E A D可以读取
DWORD dwFileOffsetHigh,//0
DWORD dwFileOffsetLow,//512*10248*1024
SIZE_T dwNumberOfBytesToMap);//多少数据映射到地址空间 0
步骤4:从进程的地址空间中撤消文件数据的映像
当不再需要保留映射到你的进程地址空间区域中的文件数据时,可以通过调用下面的函数将它释放:
BOOL UnmapViewOfFile(PVOID pvBaseAddress);
该函数的唯一的参数p v B a s e A d d r e s s用于设定返回区域的基地址。该值必须与调用M a p Vi e w O f F i l e函数返回的值相同。必须记住要调用U n m a p Vi e w O f F i l e函数。如果没有调用这个函数,那么在你的进程终止运行前,保留的区域就不会被释放。每当你调用M a p Vi e w O f F i l e时,系统总是在你的进程地址空间中保留一个新区域,而以前保留的所有区域将不被释放。
为了提高速度,系统将文件的数据页面进行高速缓存,并且在对文件的映射视图进行操作时不立即更新文件的磁盘映像。如果需要确保你的更新被写入磁盘,可以强制系统将修改过的数据的一部分或全部重新写入磁盘映像中,方法是调用F l u s h Vi e w O f F i l e函数:
BOOL FlushViewOfFile( PVOID pvAddress, SIZE_T dwNumberOfBytesToFlush);
第一个参数是包含在内存映射文件中的视图的一个字节的地址。该函数将你在这里传递的地址圆整为一个页面边界值。第二个参数用于指明你想要刷新的字节数。系统将把这个数字向上圆整,使得字节总数是页面的整数。如果你调用F l u s h Vi e w O f F i l e函数并且不修改任何数据,那么该函数只是返回,而不将任何信息写入磁盘。
17.3.5 步骤5和步骤6:关闭文件映射对象和文件对象
不用说,你总是要关闭你打开了的内核对象。如果忘记关闭,在你的进程继续运行时会出现资源泄漏的问题。当然,当你的进程终止运行时,系统会自动关闭你的进程已经打开但是忘记关闭的任何对象。但是如果你的进程暂时没有终止运行,你将会积累许多资源句柄。因此你始终都应该编写清楚而又“正确的”代码,以便关闭你已经打开的任何对象。若要关闭文件映射对象和文件对象,只需要两次调用C l o s e H a n d l e函数,每个句柄调用一次:
让我们更加仔细地观察一下这个进程。下面的伪代码显示了一个内存映射文件的例子:
HANDLE hFile = CreateFile(...); HANDLE hFileMapping = CreateFileMapping(hFile, ...); PVOID pvFile = MapViewOfFile(hFileMapping, ...); // Use the memory-mapped file. UnmapViewOfFile(pvFile); CloseHandle(hFileMapping); CloseHandle(hFile);
上面的代码显示了对内存映射文件进行操作所用的“预期”方法。但是,它没有显示,当你调用M a p Vi e w O f F i l e时系统对文件对象和文件映射对象的使用计数的递增情况。这个副作用是很大的,因为它意味着我们可以将上面的代码段重新编写成下面的样子:
HANDLE hFile = CreateFile(...); HANDLE hFileMapping = CreateFileMapping(hFile, ...); CloseHandle(hFile); PVOID pvFile = MapViewOfFile(hFileMapping, ...); CloseHandle(hFileMapping); // Use the memory-mapped file. UnmapViewOfFile(pvFile);
当对内存映射文件进行操作时,通常要打开文件,创建文件映射对象,然后使用文件映射对象将文件的数据视图映射到进程的地址空间。由于系统递增了文件对象和文件映射对象的内部使用计数,因此可以在你的代码开始运行时关闭这些对象,以消除资源泄漏的可能性。
如果用同一个文件来创建更多的文件映射对象,或者映射同一个文件映射对象的多个视图,那么就不能较早地调用C l o s e H a n d l e函数——以后你可能还需要使用它们的句柄,以便分别对C r e a t e F i l e M a p p i n g和M a p Vi e w O f F i l e函数进行更多的调用。
上一节讲过我要告诉你如何将一个16 EB的文件映射到一个较小的地址空间中。当然,你是无法做到这一点的。你必须映射一个只包含一小部分文件数据的文件视图。首先映射一个文件的开头的视图。当完成对文件的第一个视图的访问时,可以取消它的映像,然后映射一个从文件中的一个更深的位移开始的新视图。必须重复这一操作,直到访问了整个文件。这使得大型内存映射文件的处理不太方便,但是,幸好大多数文件都比较小,因此不会出现这个问题。
让我们看一个例子,它使用一个8 GB的文件和一个3 2位的地址空间。下面是一个例程,它使用若干个步骤来计算一个二进制数据文件中的所有0字节的数目:
__int64 Count0s(void) { //Views must always start on a multiple //of the allocation granularity SYSTEM_INFO sinf; GetSystemInfo(&sinf); //Open the data file. HANDLE hFile = CreateFile("C://HugeFile.Big", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); //Create the file-mapping object. HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); DWORD dwFileSizeHigh; __int64 qwFileSize = GetFileSize(hFile, &dwFileSizeHigh); qwFileSize += (((__int64) dwFileSizeHigh) << 32); //We no longer need access to the file object's handle. CloseHandle(hFile); __int64 qwFileOffset = 0, qwNumOf0s = 0; while (qwFileSize > 0) { // Determine the number of bytes to be mapped in this view DWORD dwBytesInBlock = sinf.dwAllocationGranularity; if(qwFileSize < sinf.dwAllocationGranularity) dwBytesInBlock =(DWORD) qwFileSize; PBYTE pbFile = (PBYTE) MapViewOfFile(hFileMapping, FILE_MAP_READ, (DWORD)(qwFileOffset >> 32), // Starting byte (DWORD)(qwFileOffset & 0xFFFFFFFF), // in file dwBytesInBlock); // # of bytes to map // Count the number of Js in this block. for(DWORD dwByte = 0; dwByte < dwBytesInBlock; dwByte++) { if(pbFile[dwByte] == 0) qwNumOf0s++; } // Unmap the view; we don't want multiple views // in our address space. UnmapViewOfFile(pbFile); // Skip to the next set of bytes in the file. qwFileOffset += dwBytesInBlock; qwFileSize -= dwBytesInBlock; } CloseHandle(hFileMapping); return(qwNumOf0s); }
17.5 内存映射文件与数据视图的相关性
系统允许你映射一个文件的相同数据的多个视图。例如,你可以将文件开头的10 KB映射到一个视图,然后将同一个文件的头4 KB映射到另一个视图。只要你是映射相同的文件映射对象,系统就会确保映射的视图数据的相关性。例如,如果你的应用程序改变了一个视图中的文件内容,那么所有其他视图均被更新以反映这个变化。这是因为尽管页面多次被映射到进程的虚拟地址空间,但是系统只将数据放在单个R A M页面上。如果多个进程映射单个数据文件的视图,那么数据仍然是相关的,因为在数据文件中,每个R A M页面只有一个实例——正是这个R A M页面被映射到多个进程的地址空间。
注意Wi n d o w s允许创建若干个由单个数据文件支持的文件映射对象。Wi n d o w s不能保证这些不同的文件映射对象的视图具有相关性。它只能保证单个文件映射对象的多个视图具有相关性。
然而,当对文件进行操作时,没有理由使另一个应用程序无法调用C r e a t e F i l e函数以打开由另一个进程映射的同一个文件。这个新进程可以使用R e a d F i l e和Wr i t e F i l e函数来读取该文件的数据和将数据写入该文件。当然,每当一个进程调用这些函数时,它必须从内存缓冲区读取文件数据或者将文件数据写入内存缓冲区。该内存缓冲区必须是进程自己创建的一个缓冲区,而不是映射文件使用的内存缓冲区。当两个应用程序打开同一个文件时,问题就可能产生:一个进程可以调用R e a d F i l e函数来读取文件的一个部分,并修改它的数据,然后使用Wr i t e F i l e函数将数据重新写入文件,而第二个进程的文件映射对象却不知道第一个进程执行的这些操作。由于这个原因,当你为将被内存映射的文件调用C r e a t e F i l e函数时,最好将d w S h a r e M o d e参数的值设置为0。这样就可以告诉系统,你想要单独访问这个文件,而其他进程都不能打开它。
只读文件不存在相关性问题,因此它们可以作为很好的内存映射文件。内存映射文件决不应该用于共享网络上的可写入文件,因为系统无法保证数据视图的相关性。如果某个人的计算机更新了文件的内容,其他内存中含有原始数据的计算机将不知道它的信息已经被修改。
17.6 设定内存映射文件的基地址
正如你可以使用Vi r t u a l A l l o c函数来确定对地址空间进行倒序所用的初始地址一样,你也可以使用M a p Vi e w O f F i l e E x函数而不是使用M a p Vi e w O f F i l e函数来确定一个文件被映射到某个特定的地址。请看下面的代码:
PVOID MapViewOfFileEx( // (WinCE不支持)
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
SIZE_T dwNumberOfBytesToMap,
PVOID pvBaseAddress);
该函数的所有参数和返回值均与M a p Vi e w O f F i l e函数相同,唯一的差别是最后一个参数p v B a s e A d d r e s s有所不同。在这个参数中,你为要映射的文件设定一个目标地址。与Vi r t u a l A l l o c一样,你设定的目标地址应该是分配粒度边界( 64 KB)的倍数,否则M a p Vi e w O f F i l e E x将返回N U L L,表示出现了错误。
17.7 实现内存映射文件的具体方法
让我们再来观察另一个实现代码的差别。下面是一个小程序,它映射了单个文件映射对象的两个视图:
#include int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int nCmdShow) { //Open an existing file-it must be bigger than 64 KB. HANDLE hFile = CreateFile(pszCmdLine, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, 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 64 KB in) into our address space PBYTE pbFile2 = (PBYTE) MapViewOfFile(hFileMapping, FILE_MAP_WRITE, 0, 65536, 0); if((pbFile + 65536) == pbFile2) { // If the addresses overlap, there is one address // space region for both views: this must be Windows 98. MessageBox(NULL, "We are running under Windows 98", NULL, MB_OK); } else { // If the addresses do not overlap, each view has its own // address space region: this must be Windows 2000. MessageBox(NULL, "We are running under Windows 2000", NULL, MB_OK); } UnmapViewOfFile(pbFile2); UnmapViewOfFile(pbFile); CloseHandle(hFileMapping); CloseHandle(hFile); return(0); }
在Windows 98中,当文件映射对象的视图被映射时,系统将为整个文件映射对象保留足够的地址空间。即使调用M a p Vi e w O f F i l e函数时它的参数指明你想要系统只映射文件映射对象的一小部分,系统也会为它保留足够的地址空间。这意味着即使你规定只映射文件映射对象的一个64 KB的部分,也不能将一个1 GB的文件映射对象映射到一个视图中。
每当进程调用M a p Vi e w O f F i l e时,该函数将返回一个为整个文件映射对象保留的地址空间区域中的地址。因此,在上面的代码段中,第一次调用M a p Vi e w O f F i l e函数时返回包含整个映射文件的区域的基地址,第二次调用M a p Vi e w O f F i l e函数时返回离同一个地址空间区域64 KB位置上的地址。
Windows 2000的实现代码在这里同样存在很大的差别。在上面的代码段中,两次调用M a p Vi e w O f F i l e函数将导致Windows 2000保留两个不同的地址空间区域。第一个区域的大小是文件映射对象的大小,第二个区域的大小是文件映射对象的大小减去64 KB。尽管存在两个不同的区域,但是它们的数据能够保证其相关性,因为两个视图都是从相同的文件映射对象映射而来的。在Windows 98下,各个视图具有相关性,因为它们位于同一个内存中。
17.8 使用内存映射文件在进程之间共享数据
数据共享方法是通过让两个或多个进程映射同一个文件映射对象的视图来实现的,这意味着它们将共享物理存储器的同一个页面。因此,当一个进程将数据写入一个共享文件映射对象的视图时,其他进程可以立即看到它们视图中的数据变更情况。注意,如果多个进程共享单个文件映射对象,那么所有进程必须使用相同的名字来表示该文件映射对象。