C++中使用内存映射文件处理大文件
文件操作是应用程序最为基本的功能之一,Win32 API和MFC均提供有支持文件处理的函数和类,常用的有Win32
API的CreateFile()、WriteFile()、ReadFile()和MFC提供的CFile类等。一般来说,以上这些函数可以满足大多数
场合的要求,但是对于某些特殊应用领域所需要的动辄几十GB、几百GB、乃至几TB的海量存储,再以通常的文件处理方法进行处理显然是行不通的。目前,对
于上述这种大文件的操作一般是以内存映射文件的方式来加以处理的,本文下面将针对这种Windows核心编程技术展开讨论。
内存映射文件
内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,只是内存文件映射的物理存储器来自一个
已经存在于磁盘上的文件,而非系统的页文件,而且在对该文件进行操作之前必须首先对文件进行映射,就如同将整个文件从磁盘加载到内存。由此可以看出,使用
内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均
由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要
的作用。另外,实际工程中的系统往往需要在多个进程之间共享数据,如果数据量小,处理方法是灵活多变的,如果共享数据容量巨大,那么就需要借助于内存映射
文件来进行。实际上,内存映射文件正是解决本地多个进程间数据共享的最有效方法。
内存映射文件并不是简单的文件I/O操作,实际用到
了Windows的核心编程技术--内存管理。所以,如果想对内存映射文件有更深刻的认识,必须对Windows操作系统的内存管理机制有清楚的认识,内
存管理的相关知识非常复杂,超出了本文的讨论范畴,在此就不再赘述,感兴趣的读者可以参阅其他相关书籍。下面给出使用内存映射文件的一般方法:
首先要通过CreateFile()函数来创建或打开一个文件内核对象,这个对象标识了磁盘上将要用作内存映射文件的文件。在用
CreateFile()将文件映像在物理存储器的位置通告给操作系统后,只指定了映像文件的路径,映像的长度还没有指定。为了指定文件映射对象需要多大
的物理存储空间还需要通过CreateFileMapping()函数来创建一个文件映射内核对象以告诉系统文件的尺寸以及访问文件的方式。在创建了文件
映射对象后,还必须为文件数据保留一个地址空间区域,并把文件数据作为映射到该区域的物理存储器进行提交。由MapViewOfFile()函数负责通过
系统的管理而将文件映射对象的全部或部分映射到进程地址空间。此时,对内存映射文件的使用和处理同通常加载到内存中的文件数据的处理方式基本一样,在完成
了对内存映射文件的使用时,还要通过一系列的操作完成对其的清除和使用过资源的释放。这部分相对比较简单,可以通过UnmapViewOfFile()完
成从进程的地址空间撤消文件数据的映像、通过CloseHandle()关闭前面创建的文件映射对象和文件对象。
内存映射文件相关函数
在使用内存映射文件时,所使用的API函数主要就是前面提到过的那几个函数,下面分别对其进行介绍:
HANDLE CreateFile(LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile); |
函数CreateFile()即使是在普通的文件操作时也经常用来创建、打开文件,在处理内存映射文件时,该函数来创建/打开一个文件内核对
象,并将其句柄返回,在调用该函数时需要根据是否需要数据读写和文件的共享方式来设置参数dwDesiredAccess和dwShareMode,错误
的参数设置将会导致相应操作时的失败。
HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes, DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName); |
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,几乎可以满足任何大数据量文件处理场合的要求。
LPVOID MapViewOfFile(HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, DWORD dwNumberOfBytesToMap); |
MapViewOfFile()函数负责把文件数据映射到进程的地址空间,参数hFileMappingObject为
CreateFileMapping()返回的文件映像对象句柄。参数dwDesiredAccess则再次指定了对文件数据的访问方式,而且同样要与
CreateFileMapping()函数所设置的保护属性相匹配。虽然这里一再对保护属性进行重复设置看似多余,但却可以使应用程序能更多的对数据的
保护属性实行有效控制。MapViewOfFile()函数允许全部或部分映射文件,在映射时,需要指定数据文件的偏移地址以及待映射的长度。其中,文件
的偏移地址由DWORD型的参数dwFileOffsetHigh和dwFileOffsetLow组成的64位值来指定,而且必须是操作系统的分配粒度
的整数倍,对于Windows操作系统,分配粒度固定为64KB。当然,也可以通过如下代码来动态获取当前操作系统的分配粒度:
SYSTEM_INFO sinf; GetSystemInfo(&sinf); DWORD dwAllocationGranularity = sinf.dwAllocationGranularity; |
参数dwNumberOfBytesToMap指定了数据文件的映射长度,这里需要特别指出的是,对于Windows
9x操作系统,如果MapViewOfFile()无法找到足够大的区域来存放整个文件映射对象,将返回空值(NULL);但是在Windows
2000下,MapViewOfFile()只需要为必要的视图找到足够大的一个区域即可,而无须考虑整个文件映射对象的大小。
在完成对映射到进程地址空间区域的文件处理后,需要通过函数UnmapViewOfFile()完成对文件数据映像的释放,该函数原型声明如下:
BOOL UnmapViewOfFile(LPCVOID lpBaseAddress); |
唯一的参数lpBaseAddress指定了返回区域的基地址,必须将其设定为MapViewOfFile()的返回值。在使用了函数
MapViewOfFile()之后,必须要有对应的UnmapViewOfFile()调用,否则在进程终止之前,保留的区域将无法释放。除此之外,前
面还曾由CreateFile()和CreateFileMapping()函数创建过文件内核对象和文件映射内核对象,在进程终止之前有必要通过
CloseHandle()将其释放,否则将会出现资源泄漏的问题。
除了前面这些必须的API函数之外,在使用内存映射文件时还要根
据情况来选用其他一些辅助函数。例如,在使用内存映射文件时,为了提高速度,系统将文件的数据页面进行高速缓存,而且在处理文件映射视图时不立即更新文件
的磁盘映像。为解决这个问题可以考虑使用FlushViewOfFile()函数,该函数强制系统将修改过的数据部分或全部重新写入磁盘映像,从而可以确
保所有的数据更新能及时保存到磁盘。
使用内存映射文件处理大文件应用示例
下面结合一个具体的实例来进一步讲述内存映射文件的使用方法。该实例从端口接收数据,并实时将其存放于磁盘,由于数据量大(几十GB),在此选用内存映
射文件进行处理。下面给出的是位于工作线程MainProc中的部分主要代码,该线程自程序运行时启动,当端口有数据到达时将会发出事件
hEvent[0],WaitForMultipleObjects()函数等待到该事件发生后将接收到的数据保存到磁盘,如果终止接收将发出事件
hEvent[1],事件处理过程将负责完成资源的释放和文件的关闭等工作。下面给出此线程处理函数的具体实现过程:
…… // 创建文件内核对象,其句柄保存于hFile HANDLE hFile = CreateFile("Recv1.zip", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_FLAG_SEQUENTIAL_SCAN, NULL); // 创建文件映射内核对象,句柄保存于hFileMapping // 设定大小、偏移量等参数 // 将文件数据映射到进程的地址空间 // 当数据写满60%时,为防数据溢出,需要在其后开辟一新的映射视图 // 终止事件触发 // 从进程的地址空间撤消文件数据映像 // 关闭文件映射对象 |
在终止事件触发处理过程中如果只简单的执行UnmapViewOfFile()和CloseHandle()函数将无法正确标识文件的
实际大小,即如果开辟的内存映射文件为30GB,而接收的数据只有14GB,那么上述程序执行完后,保存的文件长度仍是30GB。也就是说,在处理完成后
还要再次通过内存映射文件的形式将文件恢复到实际大小,下面是实现此要求的主要代码:
// 创建另外一个文件内核对象 hFile2 = CreateFile("Recv.zip", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_FLAG_SEQUENTIAL_SCAN, NULL); // 以实际数据长度创建另外一个文件映射内核对象 // 关闭文件内核对象 // 将文件数据映射到进程的地址空间 // 将数据从原来的内存映射文件复制到此内存映射文件 file://从进程的地址空间撤消文件数据映像 // 关闭文件映射对象 // 删除临时文件 |
结论
经实际测试,内存映射文件在处理大数据量文件时表现出了良好的性能,比通常使用CFile类和ReadFile()和WriteFile()等函数的文
件处理方式具有明显的优势。本文所述代码在Windows 98下由Microsoft Visual C++ 6.0编译通过。
-------------------------------------------------------------------------------------------------------------------------------
使用内存映射文件来提高你程序的性能[转]
本人在学习《WINDOWS核心编程》的时候对JEFFREY大师提到的一个小程序写了两个版本来比较性能,该程序的原始需求是这样的:对一个大文件进行倒序,也就是将一个文件头变成尾,尾变成头。
使用的方法有很多种,这里使用两个方法来比较,主要是突出使用内存映射文件好处;两种方法为:内存映射文件方法,I/O读写的缓存办法。
第一种办法是创建内存映射文件对象,然后将该对象映射到进程的地址空间中,再读取文件内容,然后倒序,再写入文件。
第二中方法是,将文件内容读入一个大的缓冲区,然后倒序,再写入文件,中间对原来的文件删除,然后重新写入。
程序编写如下
第一种方法,内存映射文件方式:
BOOL FileReverse(PCTSTR pszPathName)
{
HANDLE hFile = CreateFile(pszPathName,GENERIC_WRITE|GENERIC_READ,0,NULL
,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(hFile == INVALID_HANDLE_VALUE)
{
printf("File could not be opened.");
return FALSE;
}
DWORD dwFileSize = GetFileSize(hFile,NULL);
HANDLE hFileMap = CreateFileMapping(hFile,NULL,PAGE_READWRITE,0,
dwFileSize+sizeof(char),NULL);
if(hFileMap == NULL){
CloseHandle(hFile);
return FALSE;
}
PVOID pvFile = MapViewOfFile(hFileMap,FILE_MAP_WRITE,0,0,0);
if(pvFile == NULL){
CloseHandle(hFileMap);
CloseHandle(hFile);
return FALSE;
}
PSTR pchAnsi = (PSTR)pvFile;
pchAnsi[dwFileSize/sizeof(char)]=0;
_strrev(pchAnsi);
pchAnsi = strchr(pchAnsi,'');
while(pchAnsi != NULL){
*pchAnsi++ ='\r';
*pchAnsi++ ='';
pchAnsi = strchr(pchAnsi,'');
}
UnmapViewOfFile(pvFile);
CloseHandle(hFileMap);
SetFilePointer(hFile,dwFileSize,NULL,FILE_BEGIN);
SetEndOfFile(hFile);//实际上不需要写入了。
CloseHandle(hFile);
return TRUE;
}
第二中方法,使用缓存的方式:
BOOL FileReverseNoMap(PCTSTR pszPathName)
{
HANDLE hFile = CreateFile(pszPathName,GENERIC_WRITE|GENERIC_READ,0,NULL
,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
if(hFile == INVALID_HANDLE_VALUE)
{
printf("File could not be opened.");
return FALSE;
}
DWORD dwFileSize = GetFileSize(hFile,NULL);
//CloseHandle(hFile);
char *readBuf = new char[dwFileSize+1];
DWORD nRead = 0,nRet =0;
while(nRead<dwFileSize){
if(ReadFile(hFile,readBuf+nRead,dwFileSize-nRead,&nRet,NULL) ==TRUE)
{
nRead+= nRet;
}
else
{
printf("Can read the file!");
CloseHandle(hFile);
}
}
PSTR pchAnsi = (PSTR)readBuf;
pchAnsi[dwFileSize/sizeof(char)]=0;
_strrev(pchAnsi);
pchAnsi = strchr(pchAnsi,'');
while(pchAnsi != NULL){
*pchAnsi++ ='\r';
*pchAnsi++ ='';
pchAnsi = strchr(pchAnsi,'');
}
CloseHandle(hFile);
DeleteFile(pszPathName);
HANDLE hWriteFile = CreateFile(pszPathName,GENERIC_WRITE|GENERIC_READ,0,NULL
,CREATE_NEW,FILE_ATTRIBUTE_NORMAL,NULL);
WriteFile(hWriteFile,readBuf,dwFileSize,&nRet,NULL);
CloseHandle(hWriteFile);
delete readBuf;
return TRUE;
}
我运行了几次,比较结果如下:
文件大小(byte) | 1方法时间(ms) | 2方法时间(ms) |
25416 | 0 | 0 |
101664 | 0 | 0 |
406656 | 0 | 10 |
1219968 | 10 | 30 |
3202416 | 21 | 100 |
9607248 | 80 | 551 |
67250736 | 581 | 5568 |
本人测试机器的CPU是迅池1.5的笔记本,内存为712MB
通过上面的测试我们可以看到使用内存映射文件的好处,在文件内存越大这种优势就体现的越明显,其中主要的原因是:
内存映射文件直接将文件的地址映射到进程的地址空间中,那么操作文件就相当于在内存中操作一样,省去了读和写I/O的时间;第二种方式是必须这么做(READFILE,WRITEFILE),这个过程是很慢的。
C++中使用内存映射文件处理大文件的更多相关文章
- Java NIO内存映射---上G大文件处理(转)
林炳文Evankaka原创作品.转载请注明出处http://blog.csdn.net/evankaka 摘要:本文主要讲了java中内存映射的原理及过程,与传统IO进行了对比,最后,用实例说明了结果 ...
- .NET 4.0中使用内存映射文件实现进程通讯
操作系统很早就开始使用内存映射文件(Memory Mapped File)来作为进程间的共享存储区,这是一种非常高效的进程通讯手段.Win32 API中也包含有创建内存映射文件的函数,然而,这些函数都 ...
- 【转】Python之mmap内存映射模块(大文本处理)说明
[转]Python之mmap内存映射模块(大文本处理)说明 背景: 通常在UNIX下面处理文本文件的方法是sed.awk等shell命令,对于处理大文件受CPU,IO等因素影响,对服务器也有一定的压力 ...
- 居于H5的多文件、大文件、多线程上传解决方案
文件上传在web应用中是比较常见的功能,前段时间做了一个多文件.大文件.多线程文件上传的功能,使用效果还不错,总结分享下. 一. 功能性需求与非功能性需求 要求操作便利,一次选择多个文件进行上传: 支 ...
- Python之mmap内存映射模块(大文本处理)说明
背景: 通常在UNIX下面处理文本文件的方法是sed.awk等shell命令,对于处理大文件受CPU,IO等因素影响,对服务器也有一定的压力.关于sed的说明可以看了解sed的工作原理,本文将介绍通过 ...
- PHP代码中使用post参数上传大文件
今天连续碰到了两个同事向我反应上传大文件(8M)失败的事情! 都是在PHP代码中通常使用post参数进行上传文件时,当文件的大小大于8M时,上传不能不成功. 首先,我想到了nginx的client_m ...
- 使用BFG清除git仓库中的隐私文件或大文件
使用git时间不长,在调机械臂项目的时候,由于对TwinCAT3和vs的机制不太了解,没有添加很好的忽略文件(.gitignore).造成git仓库包含了很多没有用的文件,例如vs的sdf文件,Twi ...
- linux磁盘内存满了?删除大文件依然不起作用
好久没有更新博客了,但并不代表自己没有遇到技术问题了.遇到了一大堆,也解决了一大堆.只是没有记下来的欲望了,似乎大脑就这样,忘不掉.啥都忘不掉了,即使忘掉了也知道如何百度了. 查看目录大小命令 du命 ...
- HTML上传文件支持大文件上传,下载
上传 1.修改配置文件web.config,在<system.webServer>下面加入 <security> <requestFiltering > <r ...
随机推荐
- laravel when 的用法
当你在使用where语句有前提条件时,比如某值为1的时候才执行where子句,否则不执行,这个时候,laravel5.5新出了一个简便方法when($arg,fun1[,fun2]). 具体用法如下: ...
- Hadoop源码学习笔记之NameNode启动场景流程五:磁盘空间检查及安全模式检查
本篇内容关注NameNode启动之前,active状态和standby状态的一些后台服务及准备工作,即源码里的CommonServices.主要包括磁盘空间检查. 可用资源检查.安全模式等.依然分为三 ...
- 20155226 mini DC 课堂测试补交
由于电脑突然出了点问题,我没有完成mini DC这个测试,现将测试内容及结果补交 题目如下 提交测试截图和码云练习项目链接,实现Linux下dc的功能,计算后缀表达式的值 代码如下 MyDC.clas ...
- 20155305乔磊2016-2017-2《Java程序设计》第四周学习总结
20155305乔磊2016-2017-2<Java程序设计>第四周学习总结 教材学习内容总结 继承 继承就是避免多个类间重复定义共同行为. 面向对象中,子类继承父类,就是把程序中相同的代 ...
- 20155331 实验四 Android开发基础
20155331丹增旦达实验四报告 实验四 Android程序设计-1 Android Stuidio的安装测试: 参考<Java和Android开发学习指南(第二版)(EPUBIT,Java ...
- C# VS,连接到oracle 报要升级到8.多少版本的错
1:确定服务器的oracle版本 2:本地的客户端版本要和服务器一致 3:操作系统位数要一致
- 「日常训练」 Fire!(UVA-11624)
与其说是训练不如说是重温.重新写了Java版本的代码. import java.util.*; import java.math.*; import java.io.BufferedInputStre ...
- Python接口测试实战2 - 使用Python发送请求
如有任何学习问题,可以添加作者微信:lockingfree 课程目录 Python接口测试实战1(上)- 接口测试理论 Python接口测试实战1(下)- 接口测试工具的使用 Python接口测试实战 ...
- python模块的作用和说明
Python模块 如果你从Python解释器退出并再次进入,之前的定义(函数和变量)都会丢失.因此,如果你想编写一个稍长些的程序,最好使用文本编辑器为解释器准备输入并将该文件作为输入运行.这被称作编写 ...
- HDU-4055:Number String
链接:HDU-4055:Number String 题意:给你一个字符串s,s[i] = 'D'表示排列中a[i] > a[i+1],s[i] = 'I'表示排列中a[i] < a[i+1 ...