现在的位置: 首页 > 综合 > 正文

Mapping File

2013年08月07日 ⁄ 综合 ⁄ 共 15736字 ⁄ 字号 评论关闭
 

内存映射文件之剖析
                                                                              作者:xrbeck
 
内存映射文件(Mapping File)是Windows内存管理中的重要一环,也是编程
技术中比较高级的一个话题。目前关于这方面的资料比较少,而其实内存映射
文件其实对我们的对于Windows的内存了解很重要,在这里把笔者的心得写
出来,和大家一起讨论。
 
                          内存空间及映射
    相信大家都已经知道,在WIN32中和16位Windows的最大不同就是WIN32
引入了面向进程的独立虚拟地址,这个地址的寻址空间达到了4GB(2^32),当然
这个地址是虚拟的。每个进程拥有自己的独立空间,进程A的地址0X10000000
和进程B的地址0X10000000没有丝毫的联系(只是在用户进程地址空间,不包括其他
范围)。说到这个地方可能大家会奇怪了,我的机器中只有64M(或者128M等)内存呀,怎么会有这么大的地址空间呢?而进程A和进程B的同样的地址又会如何识别使得不冲突呢?
   这里先让我们来看看Windows的内存空间(注:这里我们都以Win9X来讨论,
当然Win2K或者WinNT和9X在某些方面会不大一样)
                    
                     0x00000000----0x003FFFFF     4M      属于系统保留区域
 
            0x00400000---0x7FFFFFFF    2G-4M   面向进程独立的地址空间
 
            0x80000000--0xBFFFFFFF     1G       Win32共享的空间,用来存放
                                                  内存映射文件等
         
            0xC0000000---0xFFFFFFFF    1G       用来存放Vxd等
 
有上面的列表可知,用户的程序运行在第二个地址范围中,而我们用来讨论的映射文件则放在了第三个地址范围中.而我们调试程序的时候经常有看到某个指针变量的值
为多少,这个值就指的是虚拟地址空间中的地址.
    那么Windows是如何将这个虚拟地址空间转化为实际的PC上的RAM的地址呢?
这就牵涉到映射的问题,也就是以页(page)为基本单位实现两个地址的对应.这个相信
在操作系统这门课里已经学习过,这里就不再重复了.在上面这个问题中,地址情况
可能如下:
           进程A                   RAM                    进程B
 

 
 
 
0x10000000

1页
………
 
1页          
 

 
 
 
0x10000000

 

 
 
 
 
 
 


这样,相信大家都已经清楚了程序访问中的地址空间和具体的OS访问的地址之间的关系了吧(关于页的大小,不同的平台有不同的值,Windows的是4K,我们可以用GetSystemInfo这个API返回的SYSTEM_INFO中的dwPageSize得知道)
(备注2:实际的映射过程中,是以64K为边界对齐的)
   
虚拟内存
然而,我们知道RAM是宝贵而稀少的,早在16位的Windows时代已经推出
了硬盘交换文件以提供虚拟内存,Win32则是提供了硬盘上的页面文件来继续
支持虚拟内存.根据Richter的说法:”系统页面的大小是决定应用程序能使用多少
物理内存的最重要的因素,RAM只是很小的影响”.在实际的内存访问过程中,系统
先会上RAM中寻找需要的资料,如果找不到,就会提供一个页面错误让OS上页面
文件上去找,如果找到则把页面文件的内容加载到RAM继续访问,否则就报错
提示”无效的页面错误”(也是我们最常碰到的程序错误).在这里,我们不妨把页面
文件理解为后备的RAM”.(Windows提供给用户控制虚拟内存的方法是在
控制面板中的系统选项).所以在这种情况下,RAM的主要作用只是起到了
和硬盘上的页面文件做数据的交换,所以才有了Richter的说法.
      如果用户程序要自己使用虚拟内存,那么首先的第一步是在进程地址
空间中保留(rerserve)一块地址(在4M—2G中),然后再把这块空间提交(commit)
给真正的内存.Windows提供给我们的对虚拟内存的操作界面是VirtualAlloc和
VirtualFree这一组API,这样我们就可以利用虚拟内存的庞大的特性来处理
一般程序难以解决的问题.比如假设有个二维结构Item[300][256];里面每个单位
为200个字节, 现在要修改里面的某个单元,要实际的分配这么大的内存几乎是
不可能的,而分匹处理又难以体现二维结构直接用下标访问的简洁性,这样我们
就可以先保留下这个庞大的结构,然后只提交要修改的部分给实际的内存,使得
最后的操作简洁而有效.
Windows提供的保留和提交虚拟内存的函数只是一个:VirtualAlloc,不过是里面
的参数不一样,拿上面的例子而言,我们可以这样处理:
 
LPVOID pStart = NULL;      
LPVOID pItem = NULL;
DWORD Offset=0;
 // 在系统默认位置保留整个数据结构,返回保留的首地址 
pStart =VirtualAlloc(NULL,300*256*200, MEM_RESERVE,PAGE_READWRITE);
 …
 // 计算出要存取的偏移位置
 ….
// 只提交其中的一部分给内存
pItem =VirtualAlloc(pStart+Offset,200,MEM_RESERVE,PAGE_READWRITE);
 // 直接修改
pItem=….  
VirtualFree(pStart, 300*256*200, PAGE_READWRITE); 
 
     可是虚拟内存也会带来不方便的地方,假设我们要运行一个程序,按照前面
的做法是要使用到页面文件来作为虚拟内存而访问,这样系统必然是先保留
程序的地址空间,然后提交物理内存,接下来把数据和代码从硬盘上的程序文件
拷贝到系统的页面文件,最后加载运行.这样的结果必然是使得加载一个应用
程序的时间变的很长,所以系统真正的做法是把程序文件(.exe)直接当作是内存
文件而使用,这样就不再从页面文件中分配空间使得加载的时间大大增加.
     这个特性无疑是非常诱人的,居然可以直接拿文件当内存,那不是很方便吗?
是的.在系统加载exe文件和dll文件的时候,系统是自动这么处理的.那么如果
是一般的数据文件要使用这种特性可以吗?答案是肯定的,也就是我们绕了一个
大圈子最终要说的今天的话题:内存映射文件.
内存映射文件
前面已经提到:内存映射文件是拿文件直接当作系统的内存使用,那么它主要
的用途是什么呢?主要有以下两点:
1.       直接用内存映射文件来访问磁盘上的数据文件,无需再进行文件
的I/0操作.
2.       用来在多个进程之间共享数据.进程间共享数据有很多种方法,比如
发送消息WM_COPYDATA,匿名管道等等,但他们的低层都毫无例外
的使用到了Mapping File.然而因为WM_COPYDATA一定需要使用
同步函数SendMessage,所以在实时性方面表现的不是很好.
(至于同步和异步的区别可以参考笔者的另一篇文章:
http://www.csdn.net/Develop/read_article.asp?id=14204)
 
前面已经提到过,内存映射文件的位置在3G—4G的空间中,这部分是Win32
所有进程都看的到并且共享的,自然可以用来传输数据,另外各个进程所
共享的DLL等也是映射在这个空间范围.
   内存映射文件的使用可以分为以下三步:
1.CreateFileMapping    创建一个文件映射内核对象
2.MapViewOfFile       将文件数据映射进进程地址空间
3.UnmapViewOfFile     从进程地址空间解除这个映射
 
   下面以Mapping File的两个主要作用分别给出两个简单的例子:
 
A 直接用内存映射文件访问文件.
    首先在C盘下创建一个Mapping.txt里面输入1234567
    HANDLE hFile=CreateFile("c://mapping.txt",
                                GENERIC_READ|GENERIC_WRITE,
                                FILE_SHARE_READ|FILE_SHARE_WRITE,
                                NULL,
                                OPEN_EXISTING,
                                FILE_ATTRIBUTE_NORMAL,
                                NULL);
    HANDLE hFilemap = CreateFileMapping(hFile..);
                                          NULL,
                                          PAGE_READWRITE,
                                          0,
                                          100,     // 只是开辟100个
                                          NULL);
LPVOID pVoid=MapViewOfFile(hFilemap,FILE_MAP_ALL_ACCESS,0,0,0);
Char *Buf=(char *)pVoid;
Buf[0]=”T”
CloseHandle(hFile);
CloseHandle(hFilemap);
UnmapViewOfFile(pVoid);
(注意:没有考虑异常情况)
 
       这样,当我们再打开Mapping.txt的文件的时候,就发现第一个字节”1”
    已经被改为了’T’.
   也许有些读者会提问:干吗这么麻烦呢?直接用fopen或者CreateFile
不就OK了?是的,小文件是,可是如果这个文件有上百兆呢?Mapping
File为我们提供了一种直接映射存取的方便之道.
   这里有个小小的地方要注意,创建映射对象的时候有个保护属性
fdwProtect可以选择PAGE_WRITECOPY,顾名思义是用来写拷贝的,
系统在收到这个参数后,将会从页面文件中额外的提交物理内存
(前面已经提到过,映射对象不使用页面文件).当发生读操作的时候,系统
仍旧使用映射文件,当发生写操作的时候,系统从页面文件中分配页面,
从映射文件中拷贝到该页进行访问,这样使得原先的写操作被丢弃.
读者可以试着照上面的例子把CreateFileMappingMapViewOfFile
里面的两个对应字节改为PAGE_WRITECOPY和FILE_MAP_COPY,
这样原文件即使有写操作也不会被改动.
 
B 在不同的进程间共享数据
    要进行共享如果每次都要在硬盘上创建一个文件该是多么的麻烦啊,
 Windows提供了这样一种机制:当在创建映射对象的时候如果hFile
 填上(HANDLE)0xFFFFFFFF,系统会自动从页面文件中创建文件对象.
    另外有书上提到共享方式是以p2p的方式还是c/s的架构来进行,
 我想不过是打开的方式不同吧,没有别的差别,(一个用CreateFileMapping
打开看是否为已经存在,另一个用OpenFileMapping打开)
 
来看个例子;
 
 # define WM_DATACOMING    WM_USER+100
 
进程A:
 HANDLE hFilemap=CreateFileMapping((HANDLE)0xFFFFFFFF,
                                          NULL,
                                          PAGE_READWRITE,
                                          0,
                                          100,
                                          "SHARED");
 
 LPVOID pVoid=MapViewOfFile(hFilemap,FILE_MAP_ALL_ACCESS,0,0,0);
 memset(pVoid,0,100);
 strcpy((char *)pVoid,"this is a mapping file test");
HANDLE hDes=FindWindow(NULL,"MAPPING");   // 对象窗口的名称
SendMessage(hDes, WM_DATACOMING,0,0);
CloseHandle(hFilemap);
UnmapViewOfFile(pVoid);
 
进程B(拥有窗口名称为MAPPING)
 
//  WM_DATACOMING消息捕捉函数
HANDLE hFilemap=OpenFileMapping(NULL,NULL,"SHARED");
LPVOID pVoid=MapViewOfFile(hFilemap,FILE_MAP_ALL_ACCESS,0,0,0);
Label1->Caption=(char *)pVoid;
 
可以看到数据已经被正确的传送过来.
可能有些读者已经注意到,在这种情况下需要给映射对象取个名字(例子
中为SHARED),是的,在这种用途下需要给它取个名字,而在第一种应用
中这个地方可以被忽略.这里可能会引起打架的地方就是这个名字了,
如果多个进程创建了多个映射对象,根据名字来不是比较容易冲突了吗?
是的,这是个问题,笔者建议可以采用窗体的名称(MAPPING)或者别的
唯一的ID来使得不引起混淆.
   请注意这个函数:MapViewOfFile,注意到里面有个单词:Viewà
这个函数是把创建好的映射对象真正提交到地址空间去,这就产生了
一个视.Windows中允许映射统一数据文件的多个视,比如说可以将
一个文件的全部映射到一个视,然后将他的前10K单独映射为一个视.
那么系统是不是真正区别这多个视呢?答案是要看是什么系统,
如果是Win9x,系统并没有额外再映射一个新的地址给它,而只是
原先的基地址加上一个偏移量做为新的视的地址而返回,换句话
说地址空间只有一份,而WinNT则是真正的新产生了一个地址空间
返回来.
   看看下面这个小例子:
HANDLE hFilemap=CreateFileMapping((HANDLE)0xFFFFFFFF,
                                          NULL,
                                          PAGE_READWRITE,
                                          0,
                                          100,
                                          "SHARED");
// 提交整个地址给空间
LPVOID pVoid1=MapViewOfFile(hFilemap,FILE_MAP_ALL_ACCESS,0,0,0);
// 从偏移40产生一个新视
LPVOID pVoid2= MapViewOfFile(hFilemap,FILE_MAP_ALL_ACCESS,0,40,0);
If(pVoid1+40==pVoid2)
       MessageBox(“Run On Win95”);
else
 MessageBox(“Run On NT”);
 
 可以注意到返回的值为0x8…这符合地址列表中MappingFile的位置.必然在
“Server”中开启的映射对象的地址和”Client”中利用MapViewOfFile返回的地址
是一致的(9x环境).这也是因为这个部分的地址空间是大家共享的.
   那么既然是一样的,能不能直接使用这个值呢?比如上面的进程间共享数据
的例子:如果进程A的发送语句改为:
   // 把指针值作为参数传递
   SendMessage(hDes, WM_DATACOMING,(WPARAM) pVoid,0);
 
进程B的接受消息部分改为:
   LPVOID pVoid=(LPVOID)MSG.WPARAM;
   Label1->Caption=(char *)pVoid;
 
可以看到可以正确的显示出来,因为指针所指的地方的确是有这么一笔数据,
那么是不是意味着我们就能这么使用呢?答案是否定的,首先这个值相等
只是在Win9x的环境下,在NT环境下是不相等的,另外NT下访问这个地址
空间的时候要求一定要先使用MapViewOfFile函数.这是第一个原因,更加
重要的是内存映射对象属于内核对象(Kernal Object),这种对象的最大不同就
在于它是系统维护的一块数据结构,用户只能通过相应的接口函数进行间接
的访问.每访问一次就增加一个引用记数(reference count),当计数器变为0的
时候,系统自动释放这个内核对象.在上面的例子中,尽管Server端和Client
的值是一样的,但是如果Server端执行UnmapViewOfFile释放内核对象的时候,
这部分数据将会被系统释放掉,因为它的引用计数只是1,只有我们在Client
端使用MapViewOfFile增加这个对象的计数的时候,才不会被系统释放掉.
 
                                                   
 Win32的堆位于进程私有空间内,属于自由分配区,比如大家在C++中常
使用的new操作符就是在这个地方分配的,关于堆的操作有HeapCreate
和HeapAlloc等,这里就不再继续讨论了.
 
后记
   Mapping File一直是个比较难以讨论的问题,在CSDN上也看到不少网友
讨论的比较模糊,最后不了了之.笔者对这个问题也一直想搞个明白,在看
Richter的大作<Advanced Windows>的时候,因为内存这个部分是连在一起
的好几章,理论也比较抽象和繁杂,看的很是头痛.写出这篇文章也是希望
帮助大家更好的理解这个部分,对内存有着进一步的了解以便更好的开发程序.
有兴趣进一步研究者可联系 QQ:33854303
 
 
                                                                                xrbeck写于2002/7/3
参考资料:
1.       Windows 高级编程指南 Jeffery Richter
2.       Windows程序设计      Charles Petzold
3.       Win32多线程程序设计 侯捷译
 

什么是AV错误?我该如何调试它?

当你运行程式得到了一个AV(Access Violation)错误的时候,这意味着你的程式正在试图访问一块

不再有效的内存,请注意我所提到的“不再”有效。大多数的情况下,出现这个错误要么是因为你试图访问一块已经被释放的内存,要么是想使用一个还未创建对象的指针。

幸运的是:Win32的内存体系在不同的进程(Process)间使用了独立的地址空间。所以我们可以不必担心会访问到其他的进程中的地址空间而造成破坏,(在Win 16下就有这种潜在的危险).这也就意味着我们能够正确的利用错误对话框中的信息。

当我们得到一个AV错误对话框的时候,将会出现例如:Av at ddress ????的字样,如图:

这个时候把这个地址写下来(如图为:0X4006A620),回到程序中并且打开调试用的CPU窗口,右键选择"Goto Address",你将会发现出错的信息结构。

当然CPU窗口都是以汇编语言(Assembly)出现的.你可能对此不大熟悉。于是你就可以在窗口中滚动看看到底是哪个函数(funtion)调用了它。这样你就可以在这个地方设置断点了(breakpoint).

不幸的是,不是每个错误都是这么容易的捕捉到的。相对而言。指针问题是很难调试的。这里有个常规的法则就是:在删除指针指向的对象以后。请将它置为NULL。因此在调用的时候你可以先看看这个指针是否为NULL,如果是NULL,你可以在这个地方输出一些调试信息以方便你在发生AV错误的时候能精确的找到这个地方。

// ==================================================

译者注:比如这样:

if(pName==NULL)

MessageBox("Pointer pName is NULL","Hint",MB_OK);

这样在弹出这个对话框的时候就可以知道是这个地方错了

// =====================================================

最后一个比较保险的方式就是尽可能的在使用指针的地方设置断点看看是否它为NULL。

 

译者注:

这篇文章告诉了我们什么是AV,一般产生的原因和如何捕捉等。我觉得上面谈到的只是一些比较通用的法则。实际未必如此。比如大家知道由系统自动销毁的对象其指针未必就是NULL,

而想通过检测指针的值来判断AV的话,这种情况就不适合。还有一个方面就是出现AV的另外一种情况:指针未能初始化(Initalization)并未提到,当然这都要靠写程式的自己防止了。

另外文章中提到的删除对象后把指针置为NULL应该是大力倡导的,这点也是Scott Meyers所提到的,毕竟删除一个NULL指针是安全的,而删除一个已经删除的指针“其结果是不可预测的”.

 

内存映射文件
本文给出了一种方便实用的解决大文件的读取、存储等处理的方法,并结合相关程序代码对具体的实现过程进行了介绍。 

   引言 

   文件操作是应用程序最为基本的功能之一,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 
HANDLE hFileMapping = CreateFileMapping(hFile,NULL,PAGE_READWRITE, 
0, 0x4000000, NULL); 
// 释放文件内核对象 
CloseHandle(hFile); 

// 设定大小、偏移量等参数 
__int64 qwFileSize = 0x4000000; 
__int64 qwFileOffset = 0; 
__int64 T = 600 * sinf.dwAllocationGranularity; 
DWORD dwBytesInBlock = 1000 * sinf.dwAllocationGranularity; 

// 将文件数据映射到进程的地址空间 
PBYTE pbFile = (PBYTE)MapViewOfFile(hFileMapping, 
FILE_MAP_ALL_ACCESS, 
(DWORD)(qwFileOffset>>32), (DWORD)(qwFileOffset&0xFFFFFFFF), dwBytesInBlock); 
while(bLoop) 

// 捕获事件hEvent[0]和事件hEvent[1] 
DWORD ret = WaitForMultipleObjects(2, hEvent, FALSE, INFINITE); 
ret -= WAIT_OBJECT_0; 
switch (ret) 

// 接收数据事件触发 
case 0: 
// 从端口接收数据并保存到内存映射文件 
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; 

// 终止事件触发 
case 1: 
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()等函数的文件处理方式具有明显的优势。本文所述代码在Windows 98下由Microsoft Visual C++ 6.0编译通过。

 

抱歉!评论已关闭.