使用内存映射文件读写大文件
使用内存映射文件读写大文件
文件操作是应用程序最为基本的功能之一,Win32 API和MFC均提供有支持文件处理的函数和类。一般来说,这些函数可以满足大多数场合的要求,但是对于某些特殊应用领域所需要的动辄几十GB、几百GB、乃至几TB的海量存储,再以通常的文件处理方法进行处理显然是行不通的。使用字符串变量的方法不仅会加重内存的负担,而且会Unicode和ASCII码的转换会把你弄得焦头烂额。目前,对于上述这种大文件的操作一般是以内存映射文件的方式来加以处理的,比I/O读写要快20倍,所谓I/O操作不是对外围设备直接进行操作,而是对设备与cpu连接的接口电路的操作。而映射文件的方法就是对磁盘直接进行操作。
内存映射文件无非就是那些文件中的数据被直接映射到进程地址空间中去的文件,与虚拟内存有类似的地方是,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,只是内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而非系统的页文件,而且在对该文件进行操作之前必须首先对文件进行映射,就如同将整个文件从磁盘加载到内存。由此可以看出,使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,这意味着在对文件进行处理时将不必再为文件申请并分配缓存,所有的文件缓存操作均由系统直接管理,由于取消了将文件数据加载到内存、数据从内存到文件的回写以及释放内存块等步骤,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。另外,实际工程中的系统往往需要在多个进程之间共享数据,如果数据量小,处理方法是灵活多变的,如果共享数据容量巨大,那么就需要借助于内存映射文件来进行。实际上,内存映射文件正是解决本地多个进程间数据共享的最有效方法。
首先要通过CreateFile()函数来创建或打开一个文件内核对象,这个对象标识了磁盘上将要用作内存映射文件的文件。在用CreateFile()将文件映像在物理存储器的位置通告给操作系统后,只指定了映像文件的路径,映像的长度还没有指定。为了指定文件映射对象需要多大的物理存储空间还需要通过CreateFileMapping()函数来创建一个文件映射内核对象以告诉系统文件的尺寸以及访问文件的方式。
CreateFileMapping()在创建了文件映射对象后,还必须为文件数据保留一个地址空间区域,并把文件数据作为映射到该区域的物理存储器进行提交。由MapViewOfFile()函数负责通过系统的管理而将文件映射对象的全部或部分映射到进程地址空间,实际上相当于加载文件中指定的数据到内存中。此时,对内存映射文件的使用和处理同通常加载到内存中的文件数据的处理方式基本一样,在完成了对内存映射文件的使用时,还要通过一系列的操作完成对其的清除和使用过资源的释放。这部分相对比较简单,可以通过UnmapViewOfFile()完成从进程的地址空间撤消文件数据的映像、通过CloseHandle()关闭前面创建的文件映射对象和文件对象。
实际上操作文件映射对象就相当于操作VC++文件读写方式下的文件内部指针。
而在某些特殊行业,经常要面对十几GB乃至几十GB容量的巨型文件,而一个32位进程所拥有的虚拟地址空间只有2^32 = 4GB,显然不能一次将文件映像全部映射进来。对于这种情况只能依次将大文件的各个部分映射到进程中的一个较小的地址空间。这需要对上面的一般流程进行适当的更改:
1)映射从文件开头的映像;
2)对该映像进行访问;
3)取消此映像;
4)映射一个从文件中的一个更深的位移开始的新映像;
5)重复步骤2,直到访问完全部的文件数据。
示例代码:
在本例中,首先通过GetFileSize()得到被处理文件长度(64位)的高32位和低32位值。然后在映射过程中设定每次映射的块大小为1000倍的分配粒度(系统的数据分块大小),如果文件长度小于1000倍的分配粒度时则将块大小设置为文件的实际长度。在处理过程中由映射、访问、撤消映射构成了一个循环处理。其中,每处理完一个文件块后都通过关闭文件映射对象来对每个文件块进行整理。CreateFileMapping()、MapViewOfFile()等函数是专门用来进行内存文件映射处理用的。
// 创建文件对象 HANDLE hFile = ::CreateFile(strFile, GENERIC_READ,FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_RANDOM_ACCESS, NULL); if (hFile == INVALID_HANDLE_VALUE) { TRACE("创建文件对象失败,错误代码:%d\r\n", GetLastError()); return; } // 创建文件映射对象 HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); if (hFileMap == NULL) { TRACE("创建文件映射对象失败,错误代码:%d\r\n", GetLastError()); return; } // 得到系统分配粒度 SYSTEM_INFO SysInfo; GetSystemInfo(&SysInfo); DWORD dwGran = SysInfo.dwAllocationGranularity; // 得到文件尺寸 DWORD dwFileSizeHigh; __int64 qwFileSize = GetFileSize(hFile, &dwFileSizeHigh); qwFileSize |= (((__int64)dwFileSizeHigh) << 32);///MSDN // 偏移地址 __int64 qwFileOffset = 0; __int64 T_newmap = 900 * dwGran; // 块大小 DWORD dwBlockBytes = 1000 * dwGran;//文件数据分段大小 if (qwFileSize - qwFileOffset < dwBlockBytes) dwBlockBytes = (DWORD)qwFileSize; // 映射视图 char *lpbMapAddress = (char *)MapViewOfFile(hFileMap,FILE_MAP_READ, (DWORD)(qwFileOffset >> 32), (DWORD)(qwFileOffset & 0xFFFFFFFF),dwBlockBytes); if (lpbMapAddress == NULL) { TRACE("映射文件映射失败,错误代码:%d ", GetLastError()); return; } // 关闭文件对象 CloseHandle(hFile); ///////////读文件数据 while(qwFileOffset < qwFileSize) { /******************** 读文件 ***************************/ //read_eh(&lpbMapAddress)读取已映射到内存的数据,并将文件指针作相应后移(lpbMapAddress++),返回指针偏移量 qwFileOffset = qwFileOffset + read_eh(&lpbMapAddress); //修改偏移量 if (qwFileOffset > T_newmap) {//当数据读到90%时,为防数据溢出,需要映射在其后的数据 T_newmap UnmapViewOfFile(lpbMapAddress);//释放当前映射 if ((DWORD)(qwFileSize - T_newmap) < dwBlockBytes) dwBlockBytes = (DWORD)(qwFileSize - T_newmap); lpbMapAddress = (char *)MapViewOfFile(hFileMap,FILE_MAP_READ, (DWORD)(T_newmap >> 32), (DWORD)(T_newmap & 0xFFFFFFFF),dwBlockBytes); // 修正参数 lpbMapAddress = lpbMapAddress + qwFileOffset - T_newmap; T_newmap =T_newmap + 900 * dwGran; if (lpbMapAddress == NULL) { TRACE("映射文件映射失败,错误代码:%d ", GetLastError()); return; } } } //释放最后数据块映射 UnmapViewOfFile(lpbMapAddress); // 关闭文件映射对象句柄 CloseHandle(hFileMap);
内存映射对于大文件的使用
http://blog.csdn.net/bdmh/article/details/6369250
平时很少使用大文件的内存映射,碰巧遇到了这样的要求,所以把过程记录下来,当给各位一个引子吧,因为应用不算复杂,可能有考虑不到的地方,欢迎交流。
对于一些小文件,用普通的文件流就可以很好的解决,可是对于超大文件,比如2G或者更多,文件流就不行了,所以要使用API的内存映射的相关方法,即使是内存映射,也不能一次映射全部文件的大小,所以必须采取分块映射,每次处理一小部分。
先来看几个函数
CreateFile :打开文件
GetFileSize : 获取文件尺寸
CreateFileMapping :创建映射
MapViewOfFile :映射文件
看MapViewOfFile的帮助,他的最后两个参数都需要是页面粒度的整数倍,一般机器的页面粒度为64k(65536字节),而我们实际操作中,一般都不是这样规矩的,任意位置,任意长度都是可能的,所以就要做一些处理。
本例的任务是从一个长度列表中(FInfoList),依次读取长度值,然后到另外一个大文件(FSourceFileName)中去顺序读取指定长度的数据,如果是小文件,这个就好办了,一次读到文件流中,然后依次读取就是了,大数对于大文件,就需要不断改变映射的位置,来取得我们想要的数据。
本例中显示先通过GetSystemInfo来获取页面粒度,然后以10倍的页面粒度为一个映射数据块,在for循环中,会判断已经读取的长度(totallen)加上即将读取的长度,是否在本次映射范围之内(10倍的页面粒度),如果在就继续读取,如果超出了,就要记下剩下的数据,然后重新映射下一块内存,并将记录下的剩余数据合并到新读取的数据中,有点绕啊(可能是我的想法太绕了),下面列出代码。
procedure TGetDataThread.DoGetData; var FFile_Handle:THandle; FFile_Map:THandle; list:TStringList; p:PChar; i,interval:Integer; begin try totallen := 0; offset := 0; tstream := TMemoryStream.Create; stream := TMemoryStream.Create; list := TStringList.Create; //获取系统信息 GetSystemInfo(sysinfo); //页面分配粒度大小 blocksize := sysinfo.dwAllocationGranularity; //打开文件 FFile_Handle := CreateFile(PChar(FSourceFileName),GENERIC_READ,FILE_SHARE_READ,nil,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0); if FFile_Handle = INVALID_HANDLE_VALUE then Exit; //获取文件尺寸 filesize := GetFileSize(FFile_Handle,nil); //创建映射 FFile_Map := CreateFileMapping(FFile_Handle,nil,PAGE_READONLY,0,0,nil); if FFile_Map = 0 then Exit; //此处我们已10倍blocksize为一个数据块来映射,如果文件尺寸小于10倍blocksize,则直接映射整个文件长度 if filesize div blocksize > 10 then readlen := 10*blocksize else readlen := filesize; for i := 0 to FInfoList.Count - 1 do begin list.Delimiter := ‘:‘; list.DelimitedText := FInfoList.Strings[i]; //取得长度,我这里做了解析,因为我存储的信息为 a:b:c 这种类型,所以以:号分隔 len := StrToInt(list.Strings[1]); interval := StrToInt(list.Strings[2]); if (i = 0) or (totallen+len >=readlen) then begin //如果已读取的长度加上即将要读取的长度大于 10倍blocksize,那么我们要保留之前映射末尾的内容,以便和新映射的内容合并 if i > 0 then begin offset := offset + readlen; //写入临时流 tstream.Write(p^,readlen-totallen); tstream.Position := 0; end; //如果未读取的数据长度已经不够一个分配粒度,那么就直接映射剩下的长度 if filesize-offset < blocksize then readlen := filesize-offset; //映射,p是指向映射区域的指针 //注意这里第三个参数,一直设为0,这个值要根据实际情况设置 p := PChar(MapViewOfFile(FFile_Map,FILE_MAP_READ,0,offset,readlen)); end; //如果临时流中有数据,需要合并 if tstream.Size > 0 then begin //把临时流数据copy过来 stream.CopyFrom(tstream,tstream.Size); //然后在末尾写入新数据,合并完成 stream.Write(p^,len-tstream.Size); totallen := len-tstream.Size; //移动指针的位置,指向下一个数据的开始 Inc(p,len-tstream.Size); tstream.Clear; end else begin stream.Write(p^,len); totallen := totallen + len; Inc(p,len); end; stream.Position := 0; //将流保存成文件 stream.SaveToFile(IntToStr(i)+‘.txt‘); stream.Clear; end; finally stream.Free; tstream.Free; CloseHandle(FFile_Handle); CloseHandle(FFile_Map); end; end;
如何将一整个文件读入内存,文件大小有64M
function FastReadFile(FileName: string): Integer; const PAGE_SIZE = 4 * 1024; //映射块大小不易过大,尽量以4k对齐 var hFile: THandle; szHigh,szLow: DWORD; szFile,ps: Int64; hMap: THandle; hData: Pointer; dwSize: Cardinal; begin Result := -1; hFile := 0; hMap := 0; hData := nil; szHigh := 0; try //打开已存在的文件,获得文件句柄 hFile := CreateFile(PChar(FileName),GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ, nil,OPEN_EXISTING,FILE_FLAG_SEQUENTIAL_SCAN,0); if hFile = 0 then begin Result := GetLastError; Exit; end; //获取文件大小 hMap := 0; hData := nil; szHigh := 0; szLow := GetFileSize(hFile,@szHigh); szFile := szLow or (szHigh shl 32); //创建映射句柄 hMap := CreateFileMapping(hFile, nil, PAGE_READWRITE, szHigh, szLow, nil); if hMap = 0 then begin Result := GetLastError; Exit; end; ps := 0; //文件可能比较大,分块进行映射 while ps < szFile do begin //计算映射大小及位置 if szFile - ps > PAGE_SIZE then dwSize := PAGE_SIZE else dwSize := szFile - ps; szLow := ps and $FFFFFFFF; szHigh := ps shr 32; //进行映射 hData := MapViewOfFile(hMap,FILE_MAP_ALL_ACCESS,szHigh,szLow,dwSize); if hData = nil then Break; try //此时文件偏移ps处的数据通过hData即可读取到,块大小为dwSize //以下加上你读取的代码,可以做一个回调函数 //比如你要当前位置的数据(取文件数)拷到指定内存处 CopyMemory(目标地址指针,hData,dwSize); // finally //移动文件偏移位置 ps := ps + dwSize; //释放映射块 UnmapViewOfFile(hData); hData := nil; end; end; finally //释放必要资源 if hData <> nil then UnmapViewOfFile(hData); if hMap <> 0 then CloseHandle(hMap); if hFile <> 0 then CloseHandle(hFile); end; end;