本文介绍利用内存映射文件修改大文件:在大文件内存前加入一段数据,若要使用内存映射文件,必须执行下列操作步骤:
当完成对内存映射文件的使用时,必须执行下面这些步骤将它清除:
一、我们打开关于A文件内核对象,并创建一个关于B文件的内核对象
若要创建或打开一个文件内核对象,总是要调用CreateFile函数:
HANDLE CreateFile( PCSTR pszFileName,//用于指明要创建或打开的文件的名字 DWORD dwDesiredAccess,//用于设定如何访问该文件的内容 DWORD dwShareMode,//如何共享该文件 PSECURITY_ATTRIBUTES psa,//指向安全属性的指针 DWORD dwCreationDisposition,//如何创建 DWORD dwFlagsAndAttributes,//文件属性 HANDLE hTemplateFile);//用于复制文件句柄
第二个参数dwDesiredAccess用于设定如何访问该文件的内容
值 | 含义 |
0 | 不能读取或写入文件的内容,当只想获得文件的属性时,请设定0 |
GENERIC_READ | 可以从文件中读取数据 |
GENERIC_WRITE | 可以将数据写入文件 |
GENERIC_READ|GENERIC_WRITE | 可以从文件中读取数据,也可以将数据写入文件 |
当创建或打开一个文件,将它作为一个内存映射文件来使用时,请选定最有意义的一个或多个访问标志,以说明你打算如何
访问文件的数据,对内存映射文件来说,必须打开用于只读访问或读写访问的文件,因此,可以分别设定GENERIC_READ
或GENERIC_READ|GENERIC_WRITE,
第三个参数dwShareMode告诉系统你想如何共享该文件,可以为dwShareMode设定下表所列的4个值之一:
值 | 含义 |
0 | 打开文件的任何尝试均将失败 |
FILE_SHARE_READ | 使用GENERIC_WRITE打开文件的其他尝试将会失败 |
FILE_SHARE_WRITE | 使用GENERIC_READ打开文件的其他尝试将会失败 |
FILE_SHARE_READFILE_SHARE_WRITE | 打开文件的其他尝试将会取得成功 |
如果CreateFile函数成功地创建或打开指定的文件,便返回一个文件内核对象的句柄,否则返回
INVALID_HANDLE_VALUE,
注意能够返回句柄的大多数Windows函数如果运行失败,那么就会返回NULL,但是,CreateFile函数将返回
INVALID_HANDLE_VALUE,它定义为((HANDLE)-1),
CreateFile的涵义是创建File这个内核对象,而不是创建物理磁盘上的“文件”。在Win32 API中有一系列
操作内核对象的函数,创建内核对象的函数大多命名为CreateXxxx型。
二、我们要分别创建两个文件映射内核对象
调用CreateFile函数,就可以将文件映像的物理存储器的位置告诉操作系统,你传递的路径名用于指明支持
文件映像的物理存储器在磁盘(或网络或光盘)上的确切位置,这时,必须告诉系统,文件映射对象需要多少物理存
储器,若要进行这项操作,可以调用CreateFileMapping函数:
HANDLE CreateFileMapping( HANDLE hFile,//想要映射到进程地址空间中的文件句柄,该句柄由前面调用的CreateFile函数返回 PSECURITY_ATTRIBUTES psa,//指向安全属性的指针 DWORD fdwProtect,//保护设置 DWORD dwMaximumSizeHigh,//高位文件大小 DWORD dwMaximumSizeLow,//低位文件大小 PCTSTR pszName//共享内存名称
);
第一个参数hFile用于标识你想要映射到进程地址空间中的文件句柄,该句柄由前面调用的CreateFile函数返回,
任何可以获得的物理文件句柄, 如果你需要创建一个物理文件无关的内存映射也无妨, 将它设置成为
0xFFFFFFFF(INVALID_HANDLE_VALUE)就可以了.
如果需要和物理文件关联, 要确保你的物理文件创建的时候的访问模式和"保护设置"匹配, 比如: 物理文件只读,
内存映射需要读写就会发生错误. 推荐你的物理文件使用独占方式创建.
如果使用 INVALID_HANDLE_VALUE, 也需要设置需要申请的内存空间的大小, 无论物理文件句柄参数是否
有效, 这样 CreateFileMapping 就可以创建一个和物理文件大小无关的内存空间给你, 甚至超过实际文件大
小, 如果你的物理文件有效, 而大小参数为0, 则返回给你的是一个和物理文件大小一样的内存空间地址范围.
返回给你的文件映射地址空间是可以通过复制, 集成或者命名得到, 初始内容为0.当高地位表示的这个值大于实
际的文件大小时,系统会扩大文件到这个值,因为系统需要保证进程空间能完全被映射。值为0默认为文件的大
小,这时候如果文件大小为0,创建失败。
psa参数是指向文件映射内核对象的SECURITY_ATTRIBUTES结构的指针,通常传递的值是NULL(它提供默认
的安全特性,返回的句柄是不能继承的)。
本章开头讲过,创建内存映射文件就像保留一个地址空间区域然后将物理存储器提交给该区域一样,因为内存映射
文件的物理存储器来自磁盘上的一个文件,而不是来自从系统的页文件中分配的空间,当创建一个文件映射对象时,
系统并不为它保留地址空间区域,也不将文件的存储器映射到该区域(下一节将介绍如何进行这项操作),但是,当系
统将存储器映射到进程的地址空间中去时,系统必须知道应该将什么保护属性赋予物理存储器的页面,
CreateFileMapping函数的fdwProtect参数使你能够设定这些保护属性,大多数情况下,可以设定下表中列出
的3个保护属性之一。
保护属性
含义
PAGE_READONLY
当文件映射对象被映射时,可以读取文件的数据,必须已经将GENERIC_READ传递给CreateFile函数。
PAGE_READWRITE
当文件映射对象被映射时,可以读取和写入文件的数据,必须已经将GENERIC_READ|GENERIC_WRITE传递给CreateFile。
PAGE_WRITECOPY
当文件映射对象被映射时,可以读取和写入文件的数据,如果写入数据,会导致页面的私有拷贝得以创建,必须已经将GENERIC_READ或GENERIC_WRITE传递给CreateFile。
CreateFileMapping的另外两个参数是dwMaximumSizeHigh和dwMaximumSizeLow,它们是两个最重
要的参数,CreateFileMapping函数的主要作用是保证文件映射对象能够得到足够的物理存储器,这两个参数
将告诉系统该文件的最大字节数,它需要两个32位的值,因为Windows支持的文件大小可以用64位的值来表
示,dwMaximumSizeHigh参数用于设定较高的32位,而dwMaximumSizeLow参数则用于设定较低的32
位值,对于4GB或小于4GB的文件来说,dwMaximumSizeHigh的值将始终是0。
高位文件大小,我想目前我们的机器都是32位的东东, 不可能得到超过32位进程所能寻址的私有32位地址空
间, 一般还是设置0吧, 我没有也不想尝试将它设置超过0的情况.
低位文件大小,这个还是可以进行设置的, 不过为了让其他共享用户知道你申请的文件映射的相关信息, 我使
用的时候是在获得的地址空间头部添加一个结构化描述信息, 记录内存映射的大小, 名称等, 这样实际申请的
空间就比输入的增加了一个头信息结构大小了, 我认为这样类似BSTR的方式应该是比较合理的.
int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int nCmdShow) { //Before executing the line below, C:/ does not have //a file called "MMFTest.Dat." HANDLE hfile = CreateFile("C://MMFTest.dat", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); //Before executing the line below, the MMFTest.Dat //file does exist but has a file size of 0 bytes. HANDLE hfilemap = CreateFileMapping(hfile, NULL, PAGE_READWRITE, 0, 100, NULL); //After executing the line above, the MMFTest.Dat //file has a size of 100 bytes. //Cleanup CloseHandle(hfilemap); CloseHandle(hfile); //When the process terminates, MMFTest.Dat remains //on the disk with a size of 100 bytes. return(0); }
如果调用CreateFileMapping函数,传递PAGE_READWRITE标志,那么系统将设法确保磁盘上的相关数据
文件的大小至少与dwMaximumSizeHigh和dwMaximumSizeLow参数中设定的大小相同,如果该文件小
于设定的大小,CreateFileMapping函数将扩展该文件的大小,使磁盘上的文件变大,这种扩展是必要的,这样,
当以后将该文件作为内存映射文件使用时,物理存储器就已经存在了,如果正在用PAGE_READONLY或
PAGE_WRITECOPY标志创建该文件映射对象,那么CreateFileMapping特定的文件大小不得大于磁盘文
件的物理大小,这是因为你无法将任何数据附加给该文件。
CreateFileMapping函数的最后一个参数是pszName,它是个以0结尾的字符串,用于给该文件映射对
象赋予一个名字,该名字用于与其他进程共享文件映射对象(本章后面展示了它的一个例子,第3章详细介绍了
内核对象的共享操作),内存映射数据文件通常并不需要被共享,因此这个参数通常是NULL。
系统创建文件映射对象,并将用于标识该对象的句柄返回该调用线程,如果系统无法创建文件映射对象,便
返回一个NULL句柄值,记住,当CreateFile运行失败时,它将返回INVALID_HANDLE_VALUE(定义为-1),
当CreateFileMapping运行失败时,它返回NULL,请不要混淆这些错误值。
在本实例中创建文件映射内核对象代码如下:
HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); DWORD dwFileSizeHigh; __int64 qwFileSize = GetFileSize(hFile, &dwFileSizeHigh); qwFileSize += (((__int64) dwFileSizeHigh) << 32); char AddMsg[]="Girl,you love me?, I love you very much!"; //加入的文件内容 __int64 myFilesize=qwFileSize+sinf.dwAllocationGranularity; //合并后的文件大小 HANDLE hmyfilemap = CreateFileMapping(hmyfile, NULL, PAGE_READWRITE, //合并文件大小的内存映射对象 (DWORD)(myFilesize>>32), (DWORD)(myFilesize& 0xFFFFFFFF), NULL);
三、将文件数据映射到地址空间
当创建了一个文件映射对象后,仍然必须让系统为文件的数据保留一个地址空间区域,并将文件的数据作
为映射到该区域的物理存储器进行提交,可以通过调用MapViewOfFile函数来进行这项操作:
PVOID MapViewOfFile( HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, SIZE_T dwNumberOfBytesToMap,
);
参数hFileMappingObject用于标识文件映射对象的句柄,该句柄是前面调用CreateFileMapping或
OpenFileMapping(本章后面介绍)函数返回的,参数dwDesiredAccess用于标识如何访问该数据,不错,必
须再次设定如何访问文件的数据,可以设定下表所列的4个值中的一个。
值
含义
FILE_MAP_WRITE
可以读取和写入文件数据,CreateFileMapping函数必须通过传递PAGE_READWRITE标志来调用
FILE_MAP_READ
可以读取文件数据,CreateFileMapping函数可以通过传递下列任何一个保护属性来调用:PAGE_READONLY,PAGE_READWRITE或PAGE_WRITECOPY。
FILE_MAP_ALL_ACCESS
与FILE_MAP_WRITE相同
FILE_MAP_COPY
可以读取和写入文件数据,如果写入文件数据,可以创建一个页面的私有拷贝,在Windows2000中,CreateFileMapping函数可以用PAGE_READONLY,PAGE_READWRITE或PAGE_WRITECOPY等保护属性中的任何一个来调用,在Windows98中,CreateFileMapping必须用PAGE_WRITECOPY来调用。
如果在调用MapViewOfFile函数时设定了FILE_MAP_COPY标志,系统就会从系统的页文件中提交物理存储
器,提交的地址空间数量由dwNumberOfBytesToMap参数决定,只要你不进行其他操作,只是从文件的映像视
图中读取数据,那么系统将决不会使用页文件中的这些提交的页面,但是,如果进程中的任何线程将数据写入文件
的映像视图中的任何内存地址,那么系统将从页文件中抓取已提交页面中的一个页面,将原始数据页面拷贝到该
页交换文件中,然后将该拷贝的页面映射到你的进程的地址空间,从这时起,你的进程中的线程就要访问数据的本
地拷贝,不能读取或修改原始数据。
当系统制作原始页面的拷贝时,系统将把页面的保护属性从PAGE_WRITECOPY改为PAGE_READWRITE,下
面这个代码段就说明了这个情况:
// Open the file that we want to map. HANDLE hFile = CreateFile(pszFileName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); // Create a file-mapping object for the file. HANDLE hFileMapping = CreateFileMapping(hFile, NULL, PAGE_WRITECOPY, 0, 0, NULL); // Map a copy-on-write view of the file; the system will commit // enough physical storage from the paging file to accommodate // the entire file. All pages in the view will initially have // PAGE_WRITECOPY access. PBYTE pbFile = (PBYTE) MapViewOfFile(hFileMapping, FILE_MAP_COPY, 0, 0, 0); // Read a byte from the mapped view. BYTE bSomeByte = pbFile[0]; // When reading, the system does not touch the committed pages in // the paging file. The page keeps its PAGE_WRITECOPY attribute. // Write a byte to the mapped view. pbFile[0] = 0; // When writing for the first time, the system grabs a committed // page from the paging file, copies the original contents of the // page at the accessed memory address, and maps the new page // (the copy) into the process's address space. The new page has // an attribute of PAGE_READWRITE. // Write another byte to the mapped view. pbFile[1] = 0; // Because this byte is now in a PAGE_READWRITE page, the system // simply writes the byte to the page (backed by the paging file). // When finished using the file's mapped view, unmap it. // UnmapViewOfFile is discussed in the next section. UnmapViewOfFile(pbFile); // The system decommits the physical storage from the paging file. // Any writes to the pages are lost. // Clean up after ourselves. CloseHandle(hFileMapping); CloseHandle(hFile);
四、从进程的地址空间撤消文件数据的映射
当不再需要保留映射到你的进程地址空间区域中的文件数据时,可以通过调用下面的函数将它释放:
BOOLUnmapViewOfFile(PVOIDpvBaseAddress);
该函数的唯一的参数pvBaseAddress用于设定返回区域的基地址,该值必须与调用MapViewOfFile函数
返回的值相同,必须记住要调用UnmapViewOfFile函数,如果没有调用这个函数,那么在你的进程终止运行前,保
留的区域就不会被释放,每当你调用MapViewOfFile时,系统总是在你的进程地址空间中保留一个新区域,而以
前保留的所有区域将不被释放。
五、关闭文件映射对象和文件对象
不用说,你总是要关闭你打开了的内核对象,如果忘记关闭,在你的进程继续运行时会出现资源泄漏的问题,当
然,当你的进程终止运行时,系统会自动关闭你的进程已经打开但是忘记关闭的任何对象,但是如果你的进程暂时
没有终止运行,你将会积累许多资源句柄,因此你始终都应该编写清楚而又“正确的”代码,以便关闭你已经打开的
任何对象,若要关闭文件映射对象和文件对象,只需要两次调用CloseHandle函数,每个句柄调用一次:
让我们更加仔细地观察一下这个进程,下面的伪代码显示了一个内存映射文件的例子:
HANDLEhFile=CreateFile(...); HANDLEhFileMapping=CreateFileMapping(hFile,...); PVOIDpvFile=MapViewOfFile(hFileMapping,...); //Usethememory-mappedfile. UnmapViewOfFile(pvFile); CloseHandle(hFileMapping); CloseHandle(hFile);
上面的代码显示了对内存映射文件进行操作所用的“预期”方法,但是,它没有显示,当你调用MapViewOfFile
时系统对文件对象和文件映射对象的使用计数的递增情况,这个副作用是很大的,因为它意味着我们可以将上面的代
码段重新编写成下面的样子:
HANDLEhFile=CreateFile(...); HANDLEhFileMapping=CreateFileMapping(hFile,...); CloseHandle(hFile); PVOIDpvFile=MapViewOfFile(hFileMapping,...); CloseHandle(hFileMapping); //Usethememory-mappedfile. UnmapViewOfFile(pvFile);
当对内存映射文件进行操作时,通常要打开文件,创建文件映射对象,然后使用文件映射对象将文件的数据视图
映射到进程的地址空间,由于系统递增了文件对象和文件映射对象的内部使用计数,因此可以在你的代码开始运行
时关闭这些对象,以消除资源泄漏的可能性,
如果用同一个文件来创建更多的文件映射对象,或者映射同一个文件映射对象的多个视图,那么就不能较早地调用
CloseHandle函数——以后你可能还需要使用它们的句柄,以便分别对CreateFileMapping和MapViewOfFile
函数进行更多的调用,
本实例中第三到第六步代码如下:
CloseHandle(hFile);//Wenolongerneedaccesstothefileobject'shandle. CloseHandle(hmyfile); PBYTEpbmyFile=(PBYTE)MapViewOfFile(hmyfilemap,FILE_MAP_WRITE,//内存映射视图 0,//Startingbyte 0,//infile sizeof(AddMsg)); memcpy(pbmyFile,AddMsg,sizeof(AddMsg));//加入内容 UnmapViewOfFile(pbmyFile); __int64qwFileOffset=0;//A文件视图的偏移量 __int64qwmyFileOffset=sinf.dwAllocationGranularity;//合并文件视图的遍移量 while(qwFileSize>0) { //Determinethenumberofbytestobemappedinthisview DWORDdwBytesInBlock=sinf.dwAllocationGranularity; if(qwFileSize<sinf.dwAllocationGranularity)//文件小于系统分配粒度 dwBytesInBlock=(DWORD)qwFileSize;//偏移量为文件大小 PBYTEpbFile=(PBYTE)MapViewOfFile(hFileMapping,FILE_MAP_READ, (DWORD)(qwFileOffset>>32),//Startingbyte (DWORD)(qwFileOffset&0xFFFFFFFF),//infile dwBytesInBlock);//#ofbytestomap PBYTEpbmyFile=(PBYTE)MapViewOfFile(hmyfilemap,FILE_MAP_WRITE, (DWORD)(qwmyFileOffset>>32),//Startingbyte (DWORD)(qwmyFileOffset&0xFFFFFFFF),//infile dwBytesInBlock); memcpy(pbmyFile,pbFile,dwBytesInBlock); //Unmaptheview;wedon'twantmultipleviews //inouraddressspace. UnmapViewOfFile(pbFile); UnmapViewOfFile(pbmyFile); //Skiptothenextsetofbytesinthefile. qwmyFileOffset+=dwBytesInBlock; qwFileOffset+=dwBytesInBlock; qwFileSize-=dwBytesInBlock; } CloseHandle(hFileMapping); CloseHandle(hmyfilemap);
续:
在软件开发过程中,也经常碰到进程间共享数据的需求,比如A进程创建计算数据,B进程进行显示数据的图形。这样的开发方式可以把一个大
程序开发成独立的小程序,提高软件的成功率,也可以更加适合团队一起开发,加快软件开发速度。
下面就来使用文件映射的方式进行共享数据:先要使用函数CreateFileMapping来创建一个想共享的文件数据句柄,然后使用
MapViewOfFile来获取共享的内存地址,然后使用OpenFileMapping函数在另一个进程里打开共享文件的名称,这样就可以实现不同的进程
共享数据了。
调用函数的例子如下:
#001 //文件共享。
#002 //蔡军生 2007/10/27 QQ:9073204 深圳
#003 void FileMapping(void)
#004 {
#005 //打开共享的文件对象。
#006 m_hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS,
#007 FALSE,_T("TestFileMap"));
#008 if (m_hMapFile)
#009 {
#010 //显示共享的文件数据。
#011 LPTSTR lpMapAddr = (LPTSTR)MapViewOfFile(m_hMapFile,FILE_MAP_ALL_ACCESS,
#012 0,0,0);
#013 OutputDebugString(lpMapAddr);
#014 }
#015 else
#016 {
#017 //创建共享文件。
#018 m_hMapFile = CreateFileMapping( (HANDLE)0xFFFFFFFF,NULL,
#019 PAGE_READWRITE,0,1024,_T("TestFileMap"));
#020
#021 //拷贝数据到共享文件里。
#022 LPTSTR lpMapAddr = (LPTSTR)MapViewOfFile(m_hMapFile,FILE_MAP_ALL_ACCESS,
#023 0,0,0);
#024 std::wstring strTest(_T("TestFileMap"));
#025 wcscpy(lpMapAddr,strTest.c_str());
#026
#027 FlushViewOfFile(lpMapAddr,strTest.length()+1);
#028 }
#029 }