一、概述
内存映射文件是将磁盘文件部分或全部映射到物理内存的一块地址空间,通过此地址空间,实现应用程序像访问内存一样便捷地访问磁盘文件。通常应用程序要访问磁盘文件首先是打开文件,读文件,最后再关闭文件,这是一个非常烦琐的操作过程,尤其是在频繁访问大文件时,应用程序的复杂性和运行效率是无法容忍的,通过使用内存映射文件可以很好的解决这一问题,如图1所示。
图1 内存映射文件
二、过程介绍
要正确使用内存映射文件必需执行六个过程:一是文件打开或创建;二是创建文件映射;三是将文件数据映射到址址空间;四是解除文件数据映射;五是关闭映射文件;六是关闭文件。上述过程主要用到四个API函数,下面对上述过程进行详细介绍:
文件打开或创建使用CreateFile函数。调用成功返回一个文件句柄,这个句柄在后面创建映射文件时要用到。CreatFile函数原型如下:
HANDLE CreateFile(
LPCTSTR lpFileName, // 文件名
DWORD dwDesiredAccess, // 访问模式
DWORD dwShareMode, // 共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全属性
DWORD dwCreationDistribution, // 创建方式
DWORD dwFlagsAndAttributes, // 文件属性
HANDLE hTemplateFile
);
参数lpFileName为要打开的文件名,dwDesiredAccess参数描述打开文件访问方式,此参数直接影响内存映射文件的访问方式。
创建文件映射对象CreatFileMapping函数,是为打开的文件指定一块磁盘空间,此空间大小要大于或等于已打开文件大小,否则不能够完整访问文件,即能够将整个文件完整的装入该地址空间内。CreatFileMapping函数原型如下:
HANDLE CreateFileMapping(
HANDLE hFile, // 文件句柄
LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // 安全属性
DWORD flProtect, // 保护属性
DWORD dwMaximumSizeHigh, // 映射对象大小高32位
DWORD dwMaximumSizeLow, // 映射对象大小低32位
LPCTSTR lpName // 文件映射对象名字
);
参数hFile为已打开或创建的文件句柄;flProtect 参数类似CreateFile函数中dwDesiredAccess参数,用于指定保护属性,但是在这里指定的保护属性要与CreateFile函数中相对应,如在CreateFile中指定GENERIC_READ,在CreateFileMapping中只能指定PAGE_READONLY;dwMaximumSizeHigh和 dwMaximumSizeLow分别是要创建内存映射文件大小的高、低32位值,即内存映射文件大小最大可达180亿GB,当然如要创建成功,必须要有180亿GB物理磁盘空间,实际上这种情况几乎用不到。
将文件数据映射到进程地址空间MapViewOfFile函数,是从已打开的文件映射对象中指定起始位置,并映射指定大小数据到进程空间,更通俗地讲就是将磁盘文件的某一部分读入内存。MapViewOfFile函数原型如下:
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, // 文件映射对象
DWORD dwDesiredAccess, // 访问模式
DWORD dwFileOffsetHigh, // 文件位置偏移高32位
DWORD dwFileOffsetLow, // 文件位置偏移低32位
DWORD dwNumberOfBytesToMap // 字节数
);
参数hFileMappingObject是CreateFileMapping函数返回的文件映射句柄;dwDesiredAccess用于指定访问方式;dwFileOffsetHigh和dwFileOffsetLow用于指定映射文件偏移位置高、低32位地址,此值小于等于文件大小,如此值为0,则系统试图将从偏移地址到文件末尾全部映射,另外特别需要注意的是此偏移值的大小要是64KB的整数倍,系统默认64K;dwNumberOfBytesToMap参数指定映射数据字节数。函数调用成功返回进程地址空间映射数据首地址,用户程序根据此值进行数据访问。
在执行完上述三步后就可以在指定的地址空间范围内对文件进行数据操作,当操作完成后再调用UnMapViewOfFile关闭映射,通过调用UnMapViewOfFile系统将内存中的数据回写到磁盘,调用CloseHandle函数关闭映射文件和文件句柄。
三、文件操作类示例
在应用程序中频繁地调用上述API函数会使程序冗长且不易理解,如将其封装为文件操作类可以使用程序更清晰,简洁。下面通过一个文件操作类示例详细介绍内存映射文件的使用过程。类定义如下:
const FILE_CACHE_SIZE = 200*64*1024;
// = 6.4MB 表示可CACHE文件大小,即文件大小小于此值文件可以打开
FILE_MAPVIEW_SIZE = 64*1024;
// = 64KB 表示将文件映射到地址区域大小,此值为64K的整数倍,且应小于等于 FILE_CACHE_SIZE
FILE_READONLY = 0;
FILE_WRITE = 1;
Type
TFileCache = class(TObject)
private
mMappingViewSize : INT64;
mWriteMappingOffset : INT64;
mWriteBufferOffset : INT64;
mReadMappingOffset : INT64;
FfileHandle : integer;
FmappingHandle : integer;
mWriteBuffer : pointer;
mReadBuffer : pointer;
dwWriteFizeSize :Dword;
dwReadFileSize :Dword;
dwShareMode :Dword;
CacheActive :Boolean;
public
{ Public declarations }
constructor Create;
destructor Destroy; override;
function OpenFileCache(const AFileName:String;ShareMode:Dword;WriteFileSize:INT64): boolean;
function CloseFileCache(): boolean;
function WriteData(WriteAddressOffset:INT64;pData:Dword; pDataLength: integer): boolean;
function ReadData(pAddressOffset: INT64; var pData; pDataLength: integer): boolean;
property ReadFileSize : Dword read dwReadFileSize;
property WriteFileSize : Dword read dwWriteFizeSize Write dwWriteFizeSize;
property Active : boolean read CacheActive ;
end;
TFileCache类中主定义了打开文件缓冲OpenFileCache、关闭文件缓冲CloseFileCache、写数据WriteData、读数据ReadData四个函数。通过OpenFileCache函数可以完成打开或创建文件并打开或创建映射文件,参数AfileName用于指定文件名;ShareMode用于指定访问方式,0代表读,大于等于1为写;WriteFileSize用于指定创建文件大小,只有在写方式下起作用。返回TRUE则打开成功,否则返回FALSE。实现的核心代码如下:
function TFileCache.OpenFileCache(const AFileName: String;ShareMode:Dword;WriteFileSize:INT64): boolean;
begin
if (WriteFileSize >FILE_CACHE_SIZE) and (ShareMode=1)
then
begin
raise Exception.Create('WriteFileSize Too much to Cache');
result:=false;
exit;
end
else
dwWriteFizeSize:= WriteFileSize;
//打开或创建文件
case ShareMode of
0: begin
FFileHandle := CreateFile(PChar(AFileName), GENERIC_READ, FILE_SHARE_READ, nil, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
dwShareMode:=0;
end;
1: begin
FFileHandle := CreateFile(PChar(AFileName), GENERIC_READ or GENERIC_WRITE, FILE_SHARE_READ, nil, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
dwShareMode:=1;
end;
else
FFileHandle := CreateFile(PChar(AFileName), GENERIC_READ or GENERIC_WRITE, FILE_SHARE_READ, nil, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
raise Exception.Create('FileCache ShareMode=0 is Read else is write');
result:=false;
exit;
end; {end 打开或创建文件}
if FFileHandle = INVALID_HANDLE_VALUE then
begin
raise Exception.Create('Error when open file');
result:=false;
exit;
end
else
begin
dwReadFileSize:=GetFileSize(FFileHandle,nil);
if dwReadFileSize > FILE_CACHE_SIZE
then
begin
raise Exception.Create('FileSize Too much to Cache');
result:=false;
exit;
end;
case ShareMode of
0: FMappingHandle := CreateFileMapping(FFileHandle, nil, PAGE_READONLY, 0, dwReadFileSize, nil); //DWORD(FILE_CACHE_SIZE shr 32)
1: FMappingHandle := CreateFileMapping(FFileHandle, nil, PAGE_READWRITE, 0, DWORD(FILE_CACHE_SIZE and $FFFFFFFF), nil);
else
FMappingHandle := CreateFileMapping(FFileHandle, nil, PAGE_READWRITE, 0, DWORD(FILE_CACHE_SIZE and $FFFFFFFF), nil);
raise Exception.Create('FileCache ShareMode=0 is Read else is write');
result:=false;
exit;
end;{end case}
end; {end 创建文件映射}
if FMappingHandle=0 then
begin
raise Exception.Create('Error when mapping file');
result:=false;
exit;
end;
mWriteMappingOffset := 0;
mWriteBuffer := nil;
mWriteBufferOffset := 0;
mReadMappingOffset := 0;
mReadBuffer := nil;
CacheActive:=true;
Result := true;
end;
CloseFileCache用于关闭文件映射文件以及文件句柄,并恢复类属性值,代码如下:
function TFileCache.CloseFileCache(): boolean;
begin
if (mWriteBuffer <> nil)
then
UnmapViewOfFile(mWriteBuffer);
if (mReadBuffer <> nil)
then
UnmapViewOfFile(mReadBuffer);
if (FMappingHandle <> 0)
then
CloseHandle(FMappingHandle);
if (FFileHandle <> INVALID_HANDLE_VALUE) then
begin
if dwShareMode=0
then
SetFilePointer(FFileHandle,dwReadFileSize,nil,FILE_BEGIN)
else
SetFilePointer(FFileHandle,dwWriteFizeSize,nil,FILE_BEGIN);
SetEndofFile(FFileHandle);
CloseHandle(FFileHandle);
end;
mMappingViewSize := FILE_MAPVIEW_SIZE;
FFileHandle := INVALID_HANDLE_VALUE;
FMappingHandle := 0;
mWriteMappingOffset := 0;
mWriteBuffer := nil;
mWriteBufferOffset := 0;
mReadMappingOffset := 0;
mReadBuffer := nil;
dwReadFileSize:=0;
dwWriteFizeSize:=0;
dwShareMode:=0;
CacheActive:=false;
end;
WriteData函数实现在文件指定位置写入指定大小数据。参数WriteAddressOffset指定文件偏移置;pData要写入数据的地址指针;pDataLength写入数据长度,其代码如下:
function TFileCache.WriteData(WriteAddressOffset:INT64;pData:Dword; pDataLength: integer): boolean;
var
datawrote: integer;
datacanwrite: integer;
datatowrite: integer;
actualdatatowrite: integer;
MapViewOffset,WriteMapBufferOffset:int64;
begin
datawrote := 0;
MapViewOffset:= trunc(WriteAddressOffset/mMappingViewSize) * mMappingViewSize;
WriteMapBufferOffset:=WriteAddressOffset mod mMappingViewSize;
while (datawrote < pDataLength) do
begin
datacanwrite := mMappingViewSize - WriteMapBufferOffset;
if (mWriteBuffer<>nil) and (datacanwrite <= 0) then
begin
UnmapViewOfFile(mWriteBuffer);
mWriteBuffer := nil;
MapViewOffset := MapViewOffset + mMappingViewSize;
WriteMapBufferOffset:=0;
end;
if (mWriteBuffer = nil) then
begin
mWriteBuffer := MapViewOfFile(FMappingHandle, FILE_MAP_WRITE, DWORD(mWriteMappingOffset shr 32), DWORD(MapViewOffset and $FFFFFFFF), mMappingViewSize);
datacanwrite := mMappingViewSize - WriteMapBufferOffset;
if (mWriteBuffer = nil) then
begin
raise Exception.Create('Error when map view of file.');
result:=false;
Exit;
end;
end;
datatowrite := pDataLength - datawrote;
if (datacanwrite >= datatowrite)
then
actualdatatowrite := datatowrite
else
actualdatatowrite := datacanwrite;
CopyMemory(Pointer(Longint(mWriteBuffer) + WriteMapBufferOffset), Pointer(Longint(Pointer(pData)) + datawrote), actualdatatowrite);
WriteMapBufferOffset := WriteMapBufferOffset + actualdatatowrite;
datawrote := datawrote + actualdatatowrite;
end; {while end}
if mWriteBuffer<>nil
then
begin
UnmapViewOfFile(mWriteBuffer);
mWriteBuffer := nil;
end;
Result := true;
end;
ReadData函数实现从文件指定位置读取指定大小数据。参数pAddressOffset指定文件偏移置;pData为存放读取数据的地址指针;pDataLength读取数据长度,实现的核心代码如下:
function TFileCache.ReadData(pAddressOffset: INT64; var pData; pDataLength: integer): boolean;
var
datareaded: integer; //已读数据大小
datacanread: integer; //能读数据大小
datatoread: integer; //读数据大小
actualdatatoread: integer; //事实读数据大小
high:Dword;
LastMapViewSize:Dword; //最后一个文件块大小,此块值当<=文件映像大小mMappingViewSize
actualMapViewSize:Dword;
begin
datareaded := 0;
while (datareaded < pDataLength) do
begin
datacanread := mReadMappingOffset + mMappingViewSize - pAddressOffset - datareaded;
if (mReadBuffer<>nil) and ((datacanread <= 0) or (datacanread > mMappingViewSize)) then
begin
UnmapViewOfFile(mReadBuffer);
mReadBuffer := nil;
end;
if (mReadBuffer = nil) then
begin
mReadMappingOffset := (pAddressOffset + datareaded) div mMappingViewSize * mMappingViewSize;
LastMapViewSize:=dwReadFileSize-mReadMappingOffset; //求所剩文件大小
if LastMapViewSize<= mMappingViewSize
then
actualMapViewSize:=LastMapViewSize
else
actualMapViewSize:=mMappingViewSize;
high:=DWORD(mReadMappingOffset shr 32);
mReadBuffer := MapViewOfFile(FMappingHandle, FILE_MAP_READ, high, DWORD(mReadMappingOffset and $FFFFFFFF), actualMapViewSize);
datacanread := mReadMappingOffset + mMappingViewSize - pAddressOffset - datareaded;
if (mReadBuffer = nil) then
begin
raise Exception.Create('Error when map view of file.');
Result := false;
end;
end;
datatoread := pDataLength - datareaded;
if (datacanread >= datatoread) then
actualdatatoread := datatoread
else
actualdatatoread := datacanread;
CopyMemory(Pointer(Longint(@pData) + datareaded), Pointer(LongInt(mReadBuffer) + pAddressOffset - mReadMappingOffset + datareaded), actualdatatoread);
datareaded := datareaded + actualdatatoread;
end; {while end}
if mReadBuffer<>nil
then
begin
UnmapViewOfFile(mReadBuffer);
mReadBuffer := nil;
end;
Result := true;
end;
WriteData函数与ReadWData函数在流程上基本相同,如图2所示,如要从一文件偏移置为50K的位置读取40K数据时要首先将文件从起始到64K-1位置的数据映射到进程地址空间,然后从50K的位置读取14K数据后(图中数据框虚线所示),再关闭映射,并从文件64K位置映射下一个64K数据到进程地址空间,然后再读取余下的26K数据。文件操作类调用示例程序界面如图3所示,读文件关键代码如下:
图2 文件读写过
图3 示例程序界面
procedure TForm1.Button2Click(Sender: TObject);
var
s: string;
fileoffset,charsize:Dword;
begin
if FR.Active
then
begin
fileoffset:=strtoint(trim(edit1.Text));
charsize:=strtoint(trim(edit2.Text));
if FR.ReadFileSize >= fileoffset
then
begin
if FR.ReadFileSize>=(fileoffset+charsize)
then
begin
SetString(S, nil, charsize);
FR.ReadData(fileoffset, Pointer(S)^, charsize);
Memo1.Lines.Add(S);
end
else
showmessage('字节数超出文件大小');
end
else
showmessage('读位置超出文件大小');
end
else
showmessage('文件未打开');
end;
写文件的关键代码如下:
procedure TForm1.Button6Click(Sender: TObject);
var
fileoffset,charsize:Dword;
begin
if FW.Active
then
begin
fileoffset:=strtoint(trim(edit3.Text));
charsize:=strtoint(trim(edit4.Text));
if fileoffset <= FILE_CACHE_SIZE
then
begin
if fileoffset+charsize <=FILE_CACHE_SIZE
then
begin
FW.WriteData(fileoffset,Dword(memo1.Text),charsize);
end
else
showmessage('写入字节数大于映射文件大小');
end
else
showmessage('写入位置超出映射文件大小');
end
else
showmessage('文件未打开');
end;
四、结语
基于内存映射文件编写的文件操作类可以广泛用于文件读写操作,其具有占用系统资源少,运行效率高,结构清晰等特点,特别适合用于大文件操作,数据采集等系统应用中。读者可以根据应用需要在此文件操作类基础上不断丰富类函数,构建自己的应用系统。
|