内存映射主要有两方面的作用,其一是对大文件的内存映射处理,其二是用于进程间共享。
(1)内存映射文件处理过程
首先,通过 CreateFile() 函数来创建或打开一个文件内核对象,这个对象标识了磁盘上将要用作内存映射文件的文件。
其次,通过 CreateFileMapping() 函数来为刚才创建的文件内核对象创建一个文件映射并告诉系统文件的尺寸以及访问文件的方式。
再次,通过 MapViewOfFile() 函数将文件内核映射对象添加到进程中。
接着,程序就可以通过指针进行常规的文件读取了,这里的操作就和文件操作一样。
最后,用完之后还得回收,先用 UnmapViewOfFile() 将释放映射内核对象指针,然后通过 CloseHandle 关闭之前创建的文件映射内核对象句柄和文件内核对象句柄。
(2)函数说明
A,CreateFile
CreateFile()即使是在普通的文件操作时也经常用来创建、打开文件,在处理内存映射文件时,该函数来创建/打开一个文件内核对象,并将其句柄返回,在调用该函数时需要根据是否需要数据读写和文件的共享方式来设置参数dwDesiredAccess和dwShareMode,错误的参数设置将会导致相应操作时的失败。
B,CreateFileMapping
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,几乎可以满足任何大数据量文件处理场合的要求。
C,MapViewOfFile
LPVOID MapViewOfFile(HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap);
MapViewOfFile()函数负责把文件数据映射到进程的地址空间,参数hFileMappingObject为CreateFileMapping()返回的文件映像对象句柄。参数dwDesiredAccess则再次指定了对文件数据的访问方式,而且同样要与CreateFileMapping()函数所设置的保护属性相匹配。MapViewOfFile()函数允许全部或部分映射文件,在映射时,需要指定数据文件的偏移地址以及待映射的长度。其中,文件的偏移地址由DWORD型的参数dwFileOffsetHigh和dwFileOffsetLow组成的64位值来指定,而且必须是操作系统的分配粒度的整数倍,对于Windows操作系统,分配粒度固定为64KB。参数dwNumberOfBytesToMap指定了数据文件的映射长度。
D,UnmapViewOfFile
BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);
在完成对映射到进程地址空间区域的文件处理后,需要通过函数UnmapViewOfFile()完成对文件数据映像的释放。唯一的参数lpBaseAddress指定了返回区域的基地址,必须将其设定为MapViewOfFile()的返回值。在使用了函数MapViewOfFile()之后,必须要有对应的UnmapViewOfFile()调用,否则在进程终止之前,保留的区域将无法释放。除此之外,前面还曾由CreateFile()和CreateFileMapping()函数创建过文件内核对象和文件映射内核对象,在进程终止之前有必要通过CloseHandle()将其释放,否则将会出现资源泄漏的问题。
E,除了前面这些必须的API函数之外,在使用内存映射文件时还要根据情况选用其他一些辅助函数。例如,在使用内存映射文件时,为了提高速度,系统将文件的数据页面进行高速缓存,而且在处理文件映射视图时不立即更新文件的磁盘映像。为解决这个问题可以考虑使用FlushViewOfFile()函数,该函数强制系统将修改过的数据部分或全部重新写入磁盘映像,从而可以确保所有的数据更新能及时保存到磁盘。
(3)大文件实例
HANDLE hFile = CreateFile("Recv1.zip",GENERIC_WRITE | GENERIC_READ,FILE_SHARE_READ,
NULL,CREATE_ALWAYS, FILE_FLAG_SEQUENTIAL_SCAN, NULL); // 创建文件内核对象
HANDLE hFileMapping = CreateFileMapping(hFile,NULL,PAGE_READWRITE, 0, 0x4000000, NULL);// 创建文件映射内核对象
CloseHandle(hFile);// 释放文件内核对象
PBYTE pbFile = (PBYTE)MapViewOfFile(hFileMapping, // 将文件数据映射到进程的地址空间
FILE_MAP_ALL_ACCESS,(DWORD)(qwFileOffset>>32), (DWORD)(qwFileOffset&0xFFFFFFFF), dwBytesInBlock);
while(bLoop)
{
DWORD ret = WaitForMultipleObjects(2, hEvent, FALSE, INFINITE); // 捕获事件hEvent[0]和事件hEvent[1]
ret -= WAIT_OBJECT_0;
switch (ret)
{
case 0: // 接收数据事件触发
nReadLen=syio_Read(port[1], pbFile + qwFileOffset, QueueLen); // 从端口接收数据并保存到内存映射文件
qwFileOffset += nReadLen;
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); // 将数据从原来的内存映射文件复制到此内存映射文件
UnmapViewOfFile(pbFile); //从进程的地址空间撤消文件数据映像
UnmapViewOfFile(pbFile2);
CloseHandle(hFileMapping); // 关闭文件映射对象
CloseHandle(hFileMapping2);
DeleteFile("Recv1.zip"); // 删除临时文件
(4)如果是小批量数据,采用以上函数即可同样实现进程共享。
参考原文:http://www.cppblog.com/franksunny/archive/2007/03/30/20974.html
======================================================================================================================
VC中关于GlobalAlloc,GlobalLock,GlobalUnLock
(1)GlobalAlloc函数分配一块内存,该函数会返回分配的内存句柄。GlobalLock函数锁定内存块,该函数接受一个内存句柄作为参数,然后返回一个指向被锁定的内存块的指针,您可以用该指针来读写内存。GlobalUnlock函数来解锁先前被锁定的内存,该函数使得指向内存块的指针无效。GlobalFree函数来释放内存块,您必须传给该函数一个内存句柄。
(2)函数说明
A,GlobalAlloc,分配一个全局内存块,函数返回全局内存句柄。参数类型及说明:wFlags Long,对分配的内存类型进行定义的常数标志,如下所示:GMEM_FIXED 分配一个固定内存块;GMEM_MOVEABLE 分配一个可移动内存块等。Bytes Long,要分配的字符数。
注解:如指定了 GMEM_FIXED,那么返回值就是要使用的实际内存地址即指针(GlobalLock 会返回同样的值),所以在使用固定内存块的时候不需要执行一个 GlobalLock/GlobalUnlock 操作。
关于GlobalAlloc的问题。问:在使用 GlobalAlloc 分配一个全局内存块时,使用GMEM_FIXED分配一个固定内存块与使用GMEM_MOVEABLE分配一个可移动内存块到底有什么不同?其效率上是否也存在差异?
答:GMEM_MOVEABLE是允许操作系统(或者应用程序)实施对内存堆的管理,在必要时,操作系统可以移动内存块获取更大的块,或者合并一些空闲的内存块,也称“垃圾回收”,它可以提高内存的利用率。一般情况下,内存堆空间是由用户来管理的,windows操作系统不干预。如果存在下列情况,即堆中有10个1K的空闲块,这时如果直接申请一个5K的内存空间,会得到不成功的信息。但如果其它已经被占用的内存块是movable,这时系统就可以移动这些内存块,合并出一个5k的内存块,并成功分配给用户使用。它的空间效率是以时间效率为代价的。
B,GlobalLock,锁定一个全局的内存对象,返回指向该对象的第一个字节的指针。函数原型:LPVOID GlobalLock( HGLOBAL hMem ),
参数:hMem:全局内存对象的句柄。这个句柄是通过GlobalAlloc或GlobalReAlloc来得到的。返回值:调用成功返回指向该对象的第一个字节的指针;调用失败返回NULL,可以用GetLastError来获得出错信息。
C,GlobalUnlock,解除被锁定的全局内存对象。函数原型:BOOL GlobalUnlock( HGLOBAL hMem ),参数:hMem:全局内存对象句柄,
返回值:非零值,指定的内存对象仍处于被锁定状态;0,函数执行出错,可以用GetLastError来获得出错信息,如果返回NO_ERROR,则表示内存对象已经解锁了。注意:这个函数实际上是将内存对象的锁定计数器减一,如果计数器不为0,则表示执行过多个GlobalLock函数来对这个内存对象加锁,需要对应数目的GlobalUnlock函数来解锁。
(3)示例:
// Malloc memory
hMem = GlobalAlloc(GMEM_MOVEABLE, nSize);
// Lock memory
pMem = (BYTE *) GlobalLock(hMem);
..................
// Unlock memory
GlobalUnlock(hMem);
参考原文:http://www.cnblogs.com/howareyou586/archive/2008/11/06/1328353.html