内存映射文件使用方法资料.docx
《内存映射文件使用方法资料.docx》由会员分享,可在线阅读,更多相关《内存映射文件使用方法资料.docx(21页珍藏版)》请在冰豆网上搜索。
内存映射文件使用方法资料
内存映射文件
要点:
MapViewOfFile函数中映射文件的偏移地址必须为系统分配粒度的整数倍,但一次映射的文件长度则不受此限制。
摘要:
本文给出了一种方便实用的解决大文件的读取、存储等处理的方法,并结合相关程序代码对具体的实现过程进行了介绍。
引言
文件操作是应用程序最为基本的功能之一,Win32API和MFC均提供有支持文件处理的函数和类,常用的有Win32API的CreateFile()、WriteFile()、ReadFile()和MFC提供的CFile类等。
一般来说,以上这些函数可以满足大多数场合的要求,但是对于某些特殊应用领域所需要的动辄几十GB、几百GB、乃至几TB的海量存储,再以通常的文件处理方法进行处理显然是行不通的。
目前,对于上述这种大文件的操作一般是以内存映射文件的方式来加以处理的,本文下面将针对这种Windows核心编程技术展开讨论。
内存映射文件
内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,只是内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而非系统的页文件,而且在对该文件进行操作之前必须首先对文件进行映射,就如同将整个文件从磁盘加载到内存。
由此可以看出,使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。
另外,实际工程中的系统往往需要在多个进程之间共享数据,如果数据量小,处理方法是灵活多变的,如果共享数据容量巨大,那么就需要借助于内存映射文件来进行。
实际上,内存映射文件正是解决本地多个进程间数据共享的最有效方法。
内存映射文件并不是简单的文件I/O操作,实际用到了Windows的核心编程技术--内存管理。
所以,如果想对内存映射文件有更深刻的认识,必须对Windows操作系统的内存管理机制有清楚的认识,内存管理的相关知识非常复杂,超出了本文的讨论范畴,在此就不再赘述,感兴趣的读者可以参阅其他相关书籍。
下面给出使用内存映射文件的一般方法:
首先要通过CreateFile()函数来创建或打开一个文件内核对象,这个对象标识了磁盘上将要用作内存映射文件的文件。
在用CreateFile()将文件映像在物理存储器的位置通告给操作系统后,只指定了映像文件的路径,映像的长度还没有指定。
为了指定文件映射对象需要多大的物理存储空间还需要通过CreateFileMapping()函数来创建一个文件映射内核对象以告诉系统文件的尺寸以及访问文件的方式。
在创建了文件映射对象后,还必须为文件数据保留一个地址空间区域,并把文件数据作为映射到该区域的物理存储器进行提交。
由MapViewOfFile()函数负责通过系统的管理而将文件映射对象的全部或部分映射到进程地址空间。
此时,对内存映射文件的使用和处理同通常加载到内存中的文件数据的处理方式基本一样,在完成了对内存映射文件的使用时,还要通过一系列的操作完成对其的清除和使用过资源的释放。
这部分相对比较简单,可以通过UnmapViewOfFile()完成从进程的地址空间撤消文件数据的映像、通过CloseHandle()关闭前面创建的文件映射对象和文件对象。
内存映射文件相关函数
在使用内存映射文件时,所使用的API函数主要就是前面提到过的那几个函数,下面分别对其进行介绍:
HANDLECreateFile(LPCTSTRlpFileName,
DWORDdwDesiredAccess,
DWORDdwShareMode,
LPSECURITY_ATTRIBUTESlpSecurityAttributes,
DWORDdwCreationDisposition,
DWORDdwFlagsAndAttributes,
HANDLEhTemplateFile);
函数CreateFile()即使是在普通的文件操作时也经常用来创建、打开文件,在处理内存映射文件时,该函数来创建/打开一个文件内核对象,并将其句柄返回,在调用该函数时需要根据是否需要数据读写和文件的共享方式来设置参数dwDesiredAccess和dwShareMode,错误的参数设置将会导致相应操作时的失败。
HANDLECreateFileMapping(HANDLEhFile,
LPSECURITY_ATTRIBUTESlpFileMappingAttributes,
DWORDflProtect,
DWORDdwMaximumSizeHigh,
DWORDdwMaximumSizeLow,
LPCTSTRlpName);
CreateFileMapping()函数创建一个文件映射内核对象,通过参数hFile指定待映射到进程地址空间的文件句柄(该句柄由CreateFile()函数的返回值获取)。
由于内存映射文件的物理存储器实际是存储于磁盘上的一个文件,而不是从系统的页文件中分配的内存,所以系统不会主动为其保留地址空间区域,也不会自动将文件的存储空间映射到该区域,为了让系统能够确定对页面采取何种保护属性,需要通过参数flProtect来设定,保护属性PAGE_READONLY、PAGE_READWRITE和PAGE_WRITECOPY分别表示文件映射对象被映射后,可以读取、读写文件数据。
在使用PAGE_READONLY时,必须确保CreateFile()采用的是GENERIC_READ参数;PAGE_READWRITE则要求CreateFile()采用的是GENERIC_READ|GENERIC_WRITE参数;至于属性PAGE_WRITECOPY则只需要确保CreateFile()采用了GENERIC_READ和GENERIC_WRITE其中之一即可。
DWORD型的参数dwMaximumSizeHigh和dwMaximumSizeLow也是相当重要的,指定了文件的最大字节数,由于这两个参数共64位,因此所支持的最大文件长度为16EB,几乎可以满足任何大数据量文件处理场合的要求。
LPVOIDMapViewOfFile(HANDLEhFileMappingObject,
DWORDdwDesiredAccess,
DWORDdwFileOffsetHigh,
DWORDdwFileOffsetLow,
DWORDdwNumberOfBytesToMap);
MapViewOfFile()函数负责把文件数据映射到进程的地址空间,参数hFileMappingObject为CreateFileMapping()返回的文件映像对象句柄。
参数dwDesiredAccess则再次指定了对文件数据的访问方式,而且同样要与CreateFileMapping()函数所设置的保护属性相匹配。
虽然这里一再对保护属性进行重复设置看似多余,但却可以使应用程序能更多的对数据的保护属性实行有效控制。
MapViewOfFile()函数允许全部或部分映射文件,在映射时,需要指定数据文件的偏移地址以及待映射的长度。
其中,文件的偏移地址由DWORD型的参数dwFileOffsetHigh和dwFileOffsetLow组成的64位值来指定,而且必须是操作系统的分配粒度的整数倍,对于Windows操作系统,分配粒度固定为64KB。
当然,也可以通过如下代码来动态获取当前操作系统的分配粒度:
SYSTEM_INFOsinf;
GetSystemInfo(&sinf);
DWORDdwAllocationGranularity=sinf.dwAllocationGranularity;
参数dwNumberOfBytesToMap指定了数据文件的映射长度,这里需要特别指出的是,对于Windows9x操作系统,如果MapViewOfFile()无法找到足够大的区域来存放整个文件映射对象,将返回空值(NULL);但是在Windows2000下,MapViewOfFile()只需要为必要的视图找到足够大的一个区域即可,而无须考虑整个文件映射对象的大小。
在完成对映射到进程地址空间区域的文件处理后,需要通过函数UnmapViewOfFile()完成对文件数据映像的释放,该函数原型声明如下:
BOOLUnmapViewOfFile(LPCVOIDlpBaseAddress);
唯一的参数lpBaseAddress指定了返回区域的基地址,必须将其设定为MapViewOfFile()的返回值。
在使用了函数MapViewOfFile()之后,必须要有对应的UnmapViewOfFile()调用,否则在进程终止之前,保留的区域将无法释放。
除此之外,前面还曾由CreateFile()和CreateFileMapping()函数创建过文件内核对象和文件映射内核对象,在进程终止之前有必要通过CloseHandle()将其释放,否则将会出现资源泄漏的问题。
除了前面这些必须的API函数之外,在使用内存映射文件时还要根据情况来选用其他一些辅助函数。
例如,在使用内存映射文件时,为了提高速度,系统将文件的数据页面进行高速缓存,而且在处理文件映射视图时不立即更新文件的磁盘映像。
为解决这个问题可以考虑使用FlushViewOfFile()函数,该函数强制系统将修改过的数据部分或全部重新写入磁盘映像,从而可以确保所有的数据更新能及时保存到磁盘。
使用内存映射文件处理大文件应用示例
下面结合一个具体的实例来进一步讲述内存映射文件的使用方法。
该实例从端口接收数据,并实时将其存放于磁盘,由于数据量大(几十GB),在此选用内存映射文件进行处理。
下面给出的是位于工作线程MainProc中的部分主要代码,该线程自程序运行时启动,当端口有数据到达时将会发出事件hEvent[0],WaitForMultipleObjects()函数等待到该事件发生后将接收到的数据保存到磁盘,如果终止接收将发出事件hEvent[1],事件处理过程将负责完成资源的释放和文件的关闭等工作。
下面给出此线程处理函数的具体实现过程:
……
//创建文件内核对象,其句柄保存于hFile
HANDLEhFile=CreateFile("Recv1.zip",
GENERIC_WRITE|GENERIC_READ,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_FLAG_SEQUENTIAL_SCAN,
NULL);
//创建文件映射内核对象,句柄保存于hFileMapping
HANDLEhFileMapping=CreateFileMapping(hFile,NULL,PAGE_READWRITE,
0,0x4000000,NULL);
//释放文件内核对象
CloseHandle(hFile);
//设定大小、偏移量等参数
__int64qwFileSize=0x4000000;
__int64qwFileOffset=0;
__int64T=600*sinf.dwAllocationGranularity;
DWORDdwBytesInBlock=1000*sinf.dwAllocationGranularity;
//将文件数据映射到进程的地址空间
PBYTEpbFile=(PBYTE)MapViewOfFile(hFileMapping,
FILE_MAP_ALL_ACCESS,
(DWORD)(qwFileOffset>>32),(DWORD)(qwFileOffset&0xFFFFFFFF),dwBytesInBlock);
while(bLoop)
{
//捕获事件hEvent[0]和事件hEvent[1]
DWORDret=WaitForMultipleObjects(2,hEvent,FALSE,INFINITE);
ret-=WAIT_OBJECT_0;
switch(ret)
{
//接收数据事件触发
case0:
//从端口接收数据并保存到内存映射文件
nReadLen=syio_Read(port[1],pbFile+qwFileOffset,QueueLen);
qwFileOffset+=nReadLen;
//当数据写满60%时,为防数据溢出,需要在其后开辟一新的映射视图
if(qwFileOffset>T)
{
T=qwFileOffset+600*sinf.dwAllocationGranularity;
UnmapViewOfFile(pbFile);
pbFile=(PBYTE)MapViewOfFile(hFileMapping,
FILE_MAP_ALL_ACCESS,
(DWORD)(qwFileOffset>>32),(DWORD)(qwFileOffset&0xFFFFFFFF),dwBytesInBlock);
}
break;
//终止事件触发
case1:
bLoop=FALSE;
//从进程的地址空间撤消文件数据映像
UnmapViewOfFile(pbFile);
//关闭文件映射对象
CloseHandle(hFileMapping);
break;
}
}
…
在终止事件触发处理过程中如果只简单的执行UnmapViewOfFile()和CloseHandle()函数将无法正确标识文件的实际大小,即如果开辟的内存映射文件为30GB,而接收的数据只有14GB,那么上述程序执行完后,保存的文件长度仍是30GB。
也就是说,在处理完成后还要再次通过内存映射文件的形式将文件恢复到实际大小,下面是实现此要求的主要代码:
//创建另外一个文件内核对象
hFile2=CreateFile("Recv.zip",
GENERIC_WRITE|GENERIC_READ,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_FLAG_SEQUENTIAL_SCAN,
NULL);
//以实际数据长度创建另外一个文件映射内核对象
hFileMapping2=CreateFileMapping(hFile2,
NULL,
PAGE_READWRITE,
0,
(DWORD)(qwFileOffset&0xFFFFFFFF),
NULL);
//关闭文件内核对象
CloseHandle(hFile2);
//将文件数据映射到进程的地址空间
pbFile2=(PBYTE)MapViewOfFile(hFileMapping2,
FILE_MAP_ALL_ACCESS,
0,0,qwFileOffset);
//将数据从原来的内存映射文件复制到此内存映射文件
memcpy(pbFile2,pbFile,qwFileOffset);
file:
//从进程的地址空间撤消文件数据映像
UnmapViewOfFile(pbFile);
UnmapViewOfFile(pbFile2);
//关闭文件映射对象
CloseHandle(hFileMapping);
CloseHandle(hFileMapping2);
//删除临时文件
DeleteFile("Recv1.zip");
结论
经实际测试,内存映射文件在处理大数据量文件时表现出了良好的性能,比通常使用CFile类和ReadFile()和WriteFile()等函数的文件处理方式具有明显的优势。
本文所述代码在Windows98下由MicrosoftVisualC++6.0编译通过。
摘要:
本文通过内存映射文件的使用来对大尺寸文件进行访问操作,同时也对内存映射文件的相关概念和一般编程过程作了较为详细的介绍。
关键词:
内存映射文件;大文件处理;分配粒度
引言
文件操作是应用程序最为基本的功能之一,Win32API和MFC均提供有支持文件处理的函数和类,常用的有Win32API的CreateFile()、WriteFile()、ReadFile()和MFC提供的CFile类等。
一般来说,以上这些函数可以满足大多数场合的要求,但是对于某些特殊应用领域所需要的动辄几十GB、几百GB、乃至几TB的海量存储,再以通常的文件处理方法进行处理显然是行不通的。
目前,对于上述这种大文件的操作一般是以内存映射文件的方式来加以处理的,本文下面将针对这种Windows核心编程技术展开讨论。
内存映射文件概述
内存文件映射也是Windows的一种内存管理方法,提供了一个统一的内存管理特征,使应用程序可以通过内存指针对磁盘上的文件进行访问,其过程就如同对加载了文件的内存的访问。
通过文件映射这种使磁盘文件的全部或部分内容与进程虚拟地址空间的某个区域建立映射关联的能力,可以直接对被映射的文件进行访问,而不必执行文件I/O操作也无需对文件内容进行缓冲处理。
内存文件映射的这种特性是非常适合于用来管理大尺寸文件的。
在使用内存映射文件进行I/O处理时,系统对数据的传输按页面来进行。
至于内部的所有内存页面则是由虚拟内存管理器来负责管理,由其来决定内存页面何时被分页到磁盘,哪些页面应该被释放以便为其它进程提供空闲空间,以及每个进程可以拥有超出实际分配物理内存之外的多少个页面空间等等。
由于虚拟内存管理器是以一种统一的方式来处理所有磁盘I/O的(以页面为单位对内存数据进行读写),因此这种优化使其有能力以足够快的速度来处理内存操作。
使用内存映射文件时所进行的任何实际I/O交互都是在内存中进行并以标准的内存地址形式来访问。
磁盘的周期性分页也是由操作系统在后台隐蔽实现的,对应用程序而言是完全透明的。
内存映射文件的这种特性在进行大文件的磁盘事务操作时将获得很高的效益。
需要说明的是,在系统的正常的分页操作过程中,内存映射文件并非一成不变的,它将被定期更新。
如果系统要使用的页面目前正被某个内存映射文件所占用,系统将释放此页面,如果页面数据尚未保存,系统将在释放页面之前自动完成页面数据到磁盘的写入。
对于使用页虚拟存储管理的Windows操作系统,内存映射文件是其内部已有的内存管理组件的一个扩充。
由可执行代码页面和数据页面组成的应用程序可根据需要由操作系统来将这些页面换进或换出内存。
如果内存中的某个页面不再需要,操作系统将撤消此页面原拥用者对它的控制权,并释放该页面以供其它进程使用。
只有在该页面再次成为需求页面时,才会从磁盘上的可执行文件重新读入内存。
同样地,当一个进程初始化启动时,内存的页面将用来存储该应用程序的静态、动态数据,一旦对它们的操作被提交,这些页面也将被备份至系统的页面文件,这与可执行文件被用来备份执行代码页面的过程是很类似的。
图1展示了代码页面和数据页面在磁盘存储器上的备份过程:
图1进程的代码页、数据页在磁盘存储器上的备份
显然,如果可以采取同一种方式来处理代码和数据页面,无疑将会提高程序的执行效率,而内存映射文件的使用恰恰可以满足此需求。
对大文件的管理
内存映射文件对象在关闭对象之前并没有必要撤销内存映射文件的所有视图。
在对象被释放之前,所有的脏页面将自动写入磁盘。
通过CloseHandle()关闭内存映射文件对象,只是释放该对象,如果内存映射文件代表的是磁盘文件,那么还需要调用标准文件I/O函数来将其关闭。
在处理大文件处理时,内存映射文件将表示出卓越的优势,只需要消耗极少的物理资源,对系统的影响微乎其微。
下面先给出内存映射文件的一般编程流程框图:
图2使用内存映射文件的一般流程
而在某些特殊行业,经常要面对十几GB乃至几十GB容量的巨型文件,而一个32位进程所拥有的虚拟地址空间只有232=4GB,显然不能一次将文件映像全部映射进来。
对于这种情况只能依次将大文件的各个部分映射到进程中的一个较小的地址空间。
这需要对上面的一般流程进行适当的更改:
1)映射文件开头的映像。
2)对该映像进行访问。
3)取消此映像
4)映射一个从文件中的一个更深的位移开始的新映像。
5)重复步骤2,直到访问完全部的文件数据。
下面给出一段根据此描述而写出的对大于4GB的文件的处理代码:
//选择文件
CFileDialogfileDlg(TRUE,"*.txt","*.txt",NULL,"文本文件(*.txt)|*.txt||",this);
fileDlg.m_ofn.Flags|=OFN_FILEMUSTEXIST;
fileDlg.m_ofn.lpstrTitle="通过内存映射文件读取数据";
if(fileDlg.DoModal()==IDOK)
{
//创建文件对象
HANDLEhFile=CreateFile(fileDlg.GetPathName(),GENERIC_READ|GENERIC_WRITE,
0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(hFile==INVALID_HANDLE_VALUE)
{
TRACE("创建文件对象失败,错误代码:
%d\r\n",GetLastError());
return;
}
//创建文件映射对象
HANDLEhFileMap=CreateFileMapping(hFile,NULL,PAGE_READWRITE,0,0,NULL);
if(hFileMap==NULL)
{
TRACE("创建文件映射对象失败,错误代码:
%d\r\n",GetLastError());
return;
}
//得到系统分配粒度
SYSTEM_INFOSysInfo;
GetSystemInfo(&SysInfo);
DWORDdwGran=SysInfo.dwAllocationGranularity;
//得到文件尺寸
DWORDdwFileSizeHigh;
__int64qwFileSize=GetFileSize(hFile,&dwFileSizeHigh);
qwFileSize|=(((__int64)dwFileSizeHigh)<<32);
//关闭文件对象
CloseHandle(hFile);
//偏移地址
__int64qwFileOffset=0;
//块大小
DWORDdwBlockBytes=1000*dwGran;
if(qwFileSize<1000*dwGran)
dwBlockBytes=(DWORD)qwFileSize;
while(qwFileOffset