Jan
3
怎样编程实现读写大型文件
(yanlb2000, 2007.01.03, yanlb2000.blogcn.com)
在很多应用中,我们需要对文件内容进行读写等操作。一般,我们用ReadFile, WriteFile这两个API函数就可以了。
然而,在一些场合,可能需要对大量文件进行全面的读写操作。比如,对硬盘上的文件作全文检索、索引,或者编制HASH列表等。这些情况下,文件的读写操作量可能是非常大的。而且,有些文件论个头也是非常巨大,比如CD、DVD光盘镜像、高清视频节目等,动辄几百MB、甚至几个、十几个GB。这些,都对文件处理算法提出了一定的要求:要求能快速高效又准确地处理大数据量的文件。
这里,其实有两个要求:
1, 正确。显然,这是基本的要求。如果文件读写过程是错误的或有隐含错误的,那么基本功能都不能正确完成,其他就免谈了。
2, 快速。读写文件的方法不少,但怎样才能更快呢?而且是要在保证正确的前提下。
对于上面的第一点,即正确性,要补充说明一下。多数软件在多数情况下处理文件都是正确的。但是,如果要处理一个大于2GB或者4GB的文件,有些软件就不行了,或者报错,或者结果是错误的。这是因为,目前主流编程环境还是基于32位字长的CPU和操作系统环境的,一个普通指针、整数都表达2^32或其一半(当作为无符号或有符号整数时),即4GB或者2GB。然而,这些软件却没有考虑到如果文件大于4GB的情况,仍然使用普通的整数或指针来表达文件的大小或者指针,这当然就不能正确处理大于4GB的文件了。其实这时候,就必须用两个32位整数来组合表达一个64位的变量,来完成原有的单个32位整数所不能表达的大小。(不过,在64位编程环境下,这个问题就容易多了,因为一个整数或指针“自然地”就是64位的。)当然,即使是32位Windows环境下,Win32 API中这些文件读写相关的函数,也都是支持这种大数表示的,否则也就不能支持NTFS上大于4GB的文件了。
再说说第二点,要快速。这里,我就要推荐本文的主角了:MapViewOfFile。该函数可以将一个文件的内容映射到运行进程的地址空间中,以后的所有对文件的操作,就可以直接象对内存一样直接访问了。这样,我们就不需要再显示调用繁琐的文件IO操作函数了。这一切读写操作,都由系统内核帮助我们自动完成。另外,文件读写性能也会有很大提高,特别是系统内存较大、要读写的文件体积又很大的时候,性能优势会很明显。
具体的比较,我这里就不谈了,大家可以查找网上的资料。比如,这里有一个网页:
该网友对MapViewOfFile和ReadFile进行了比较,其操作是实现将一个文件进行头尾内容的倒序。小文件比较相差不大,但对于一个64MB的文件,使用MapViewOfFile和ReadFile相比,速度差别几乎有一个数量级(10倍)。那么,对于一个几百MB、几个GB的文件呢?显然会有更明显的效率差别。
不过,附带说明的是,该网页代码中,就没有应对大型文件的处理。所以,这个代码是不能处理大于4GB的文件的。
我写了一个计算文件MD5摘要的软件,其中就用了MapViewOfFile。这里将文件读写部分写在这里,供大家参考。
关于MapViewOfFile的更多讨论,我可能会准备再写篇文章来专门讨论。这里,就仅仅给出通过MapViewOfFile来读写大型文件的方法的通用流程。
//生成一个文件的MD5摘要,N是输入的文件名
void MD5File(CString *N, MD5Digest *Digest)
{
HANDLE FileHandle; //文件读写句柄
HANDLE MapHandle; //文件映射句柄
MD5Context Context; //生成MD5的算法数据结构的上下文
//生成一个文件的MD5摘要,N是输入的文件名
void MD5File(CString *N, MD5Digest *Digest)
{
HANDLE FileHandle; //文件读写句柄
HANDLE MapHandle; //文件映射句柄
MD5Context Context; //生成MD5的算法数据结构的上下文
MD5Init(&Context); //MD5上下文初始化
FileHandle = CreateFile( //打开文件,只读方式
*N,
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
0);
if(FileHandle!=INVALID_HANDLE_VALUE) //如果打开文件成功
{
//创建文件映射
MapHandle = CreateFileMapping(FileHandle, NULL, PAGE_READONLY, 0, 0, NULL);
if(MapHandle != NULL) //如果文件映射成功
{
// ULONGLONG 是系统预定义的64位无符号整数
// FileSize: 文件大小
// ptrFileMap: 文件指针,指示当前处理位置
// BufferLength: 映射视图大小
ULONGLONG FileSize, ptrFileMap, BufferLength;
// ViewPointer :当前文件映射到内存的地址指针
void *ViewPointer = NULL;
// MemInfo:获取映射内存的信息
MEMORY_BASIC_INFORMATION MemInfo;
// 定义预备申请的映射内存大小,256MB
DWORD ExpectantBufferLength = 0x10000000;
ptrFileMap = 0;
BufferLength = 0;
FileHandle = CreateFile( //打开文件,只读方式
*N,
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN,
0);
if(FileHandle!=INVALID_HANDLE_VALUE) //如果打开文件成功
{
//创建文件映射
MapHandle = CreateFileMapping(FileHandle, NULL, PAGE_READONLY, 0, 0, NULL);
if(MapHandle != NULL) //如果文件映射成功
{
// ULONGLONG 是系统预定义的64位无符号整数
// FileSize: 文件大小
// ptrFileMap: 文件指针,指示当前处理位置
// BufferLength: 映射视图大小
ULONGLONG FileSize, ptrFileMap, BufferLength;
// ViewPointer :当前文件映射到内存的地址指针
void *ViewPointer = NULL;
// MemInfo:获取映射内存的信息
MEMORY_BASIC_INFORMATION MemInfo;
// 定义预备申请的映射内存大小,256MB
DWORD ExpectantBufferLength = 0x10000000;
ptrFileMap = 0;
BufferLength = 0;
GetFileSizeEx(FileHandle, (LARGE_INTEGER *)&FileSize);
// 主循环,完成整个文件的映射和处理
do{
// 如果预定的内存大小大于文件余下(还未处理的)尺寸,则取余下的大小为申请内存大小
if( ExpectantBufferLength > (FileSize - ptrFileMap))
ExpectantBufferLength = FileSize - ptrFileMap;
// 映射内存过程,直到成功
do{
// 创建内存映射文件
ViewPointer = MapViewOfFile(
MapHandle,
FILE_MAP_READ,
ptrFileMap >> 32, // 映射到文件的起始位置,高32位
ptrFileMap & 0xffffffff, // 映射到文件的起始位置,低32位
ExpectantBufferLength); // 映射到的内存区间大小
if(ViewPointer != NULL) // 如果映射成功
{
VirtualQuery(ViewPointer, &MemInfo, sizeof(MemInfo)); // 查询该块虚拟内存的信息
BufferLength = MemInfo.RegionSize ; // 该虚拟内存的大小
// 如果该块内存大小小于0,表示申请失败,我们将申请的大小减半,再次申请
if(BufferLength <= 0) ExpectantBufferLength <<= 1;
}
// 如果申请内存的大小大于1MB但还是不成功,则继续
}while( BufferLength <= 0 && ExpectantBufferLength >= 0x100000 /* 1MB */);
// 如果申请到的尺寸大于0,成功。开始处理文件。
if(BufferLength > 0)
{
// 以下BufferLength修正为传递到MD5计算过程内存的大小,
// 不能超过文件末尾。这发生在文件最后映射的时候。
if(ptrFileMap + BufferLength > FileSize)
BufferLength = FileSize - ptrFileMap;
MD5Update(&Context, (BYTE *)ViewPointer, BufferLength);// MD5计算过程
UnmapViewOfFile(ViewPointer); // 释放映射的内存
ptrFileMap += BufferLength; // 递加文件指针为已经处理好的长度
} //
} while(ptrFileMap < FileSize); // 循环映射过程
CloseHandle(MapHandle); // 关闭内存映射句柄
} //
CloseHandle(FileHandle); // 关闭文件句柄
}
MD5Final(&Context, Digest); // MD5上下文收尾计算过程
return ;
}
// 主循环,完成整个文件的映射和处理
do{
// 如果预定的内存大小大于文件余下(还未处理的)尺寸,则取余下的大小为申请内存大小
if( ExpectantBufferLength > (FileSize - ptrFileMap))
ExpectantBufferLength = FileSize - ptrFileMap;
// 映射内存过程,直到成功
do{
// 创建内存映射文件
ViewPointer = MapViewOfFile(
MapHandle,
FILE_MAP_READ,
ptrFileMap >> 32, // 映射到文件的起始位置,高32位
ptrFileMap & 0xffffffff, // 映射到文件的起始位置,低32位
ExpectantBufferLength); // 映射到的内存区间大小
if(ViewPointer != NULL) // 如果映射成功
{
VirtualQuery(ViewPointer, &MemInfo, sizeof(MemInfo)); // 查询该块虚拟内存的信息
BufferLength = MemInfo.RegionSize ; // 该虚拟内存的大小
// 如果该块内存大小小于0,表示申请失败,我们将申请的大小减半,再次申请
if(BufferLength <= 0) ExpectantBufferLength <<= 1;
}
// 如果申请内存的大小大于1MB但还是不成功,则继续
}while( BufferLength <= 0 && ExpectantBufferLength >= 0x100000 /* 1MB */);
// 如果申请到的尺寸大于0,成功。开始处理文件。
if(BufferLength > 0)
{
// 以下BufferLength修正为传递到MD5计算过程内存的大小,
// 不能超过文件末尾。这发生在文件最后映射的时候。
if(ptrFileMap + BufferLength > FileSize)
BufferLength = FileSize - ptrFileMap;
MD5Update(&Context, (BYTE *)ViewPointer, BufferLength);// MD5计算过程
UnmapViewOfFile(ViewPointer); // 释放映射的内存
ptrFileMap += BufferLength; // 递加文件指针为已经处理好的长度
} //
} while(ptrFileMap < FileSize); // 循环映射过程
CloseHandle(MapHandle); // 关闭内存映射句柄
} //
CloseHandle(FileHandle); // 关闭文件句柄
}
MD5Final(&Context, Digest); // MD5上下文收尾计算过程
return ;
}
Tags: MapViewOfFile, 文件, 编程