内存映射文件
和虚拟内存一样,内存映射文件可以用来保留一个进程地址区域;但是,与虚拟内存不同,它提交的不是物理内存或是虚拟页文件,而是硬盘上的文件。将文件映射成内存,我们可以像使用内存一样使用文件.
使用场合
它有三个主要用途:
系统加载EXE和DLL文件
操作系统就是用它来加载exe和dll文件建立进程,运行exe。这样可以节省页文件和启动时间。
访问大数据文件
如果文件太大,比如超过了进程用户区2G,用fopen是不能对文件进行操作的。这时,可用内存映射文件。对于大数据文件可以不必对文件执行I/O操作,不必对所有文件内容进行缓存。
进程共享机制
内存映射文件是多个进程共享数据的一种较高性能的有效方式,它也是操作系统进程通信机制的底层实现方法。RPC、COM、OLE、DDE、窗口消息、剪贴板、管道、Socket等都是使用内存映射文件实现的。
相关函数介绍
CreateFile(文件名,访问属性,共享模式,…)
作用:创建文件内核对象API
其中,访问属性有:
0 不能读写 (用它可以访问文件属性)
GENERIC_READ
GENERIC_WRITE
GENERIC_READ|GENERIC_WRITE;
共享模式:
0 独享文件,其他应用程序无法打开
FILE_SHARE_WRITE
FILE_SHARE_READ|FILE_SHARE_WRITE
这个属性依赖于访问属性,必须和访问属性不冲突。
当创建失败时,返回INVALID_HANDLE_VALUE。
HANDLE CreateFileMapping(Handle 文件,PSECURITY_ATTRIBUTES 安全属性,DWORD 保护属性,DWORD 文件大小高32位,DWORD 文件大小低32位,PCTSTR 映射名称)
作用:创建文件映射内核对象
“文件”是上面创建的句柄;
“安全属性”是内核对象需要的,NULL表示使用系统默认的安全属性;“保护属性”是当将存储器提交给进程空间时,需要的页面属性:PAGE_READONLY, PAGE_READWRITE和PAGE_WRITECOPY。这个属性不能和文件对象的访问属性冲突。除了这三个外,还有两个属性可以和它们连接使用(|)。当更新文件内容时,不提供缓存,直接写入文件,可用SEC_NOCACHE;当文件是可执行文件时,系统会根据节赋予不同的页面属性,可用SEC_IMAGE。另外,SEC_RESERVE和SEC_COMMIT用于稀疏提交的文件映射,详细介绍请参考下文。
“文件大小高32位”和“文件大小低32位”联合起来告诉系统,这个映射所能支持的文件大小(操作系统支持264B文件大小);当这个值大于实际的文件大小时,系统会扩大文件到这个值,因为系统需要保证进程空间能完全被映射。值为0默认为文件的大小,这时候如果文件大小为0,创建失败。
“映射名称”是给用户标识此内核对象,供各进程共享,如果为NULL,则不能共享。
对象创建失败时返回NULL。
创建成功后,系统仍未为文件保留进程空间。
PVOID MAPViewOfFile(HANDLE 映射对象,DWORD访问属性,DWORD 偏移量高32位,DWORD 偏移量低32位,SIZE_T 字节数)
作用:文件映射内核对象映射到进程空间
“映射对象”是前面建立的对象;
“访问属性”可以是下面的值:FILE_MAP_WRITE(读和写)、FILE_MAP_READ、FILE_MAP_ALL_ACCESS(读和写)、FILE_MAP_COPY。当使用FILE_MAP_COPY时,系统分配虚拟页文件,当有写操作时,系统会拷贝数据到这些页面,并赋予PAGE_READWRITE属性。
可以看到,每一步都需要设置这类属性,是为了可以多点控制,试想,如果在这一步想有多种不同的属性操作文件的不同部分,就比较有用。
“偏移高32位”和“偏移低32位”联合起来标识映射的开始字节(地址是分配粒度的倍数);
“字节数”指映射的字节数,默认0为到文件尾。
当你需要指定映射到哪里时,你可以使用:
PVOID MAPViewOfFile(HANDLE 映射对象,DWORD访问属性,DWORD 偏移量高32位,DWORD 偏移量低32位,SIZE_T 字节数,PVOID 基地址)
“基地址”是映射到进程空间的首地址,必须是分配粒度的倍数。
BOOL FlushViewOfFile(PVOID 进程空间地址,SIZE_T 字节数)
作用:保存文件修改, 为了提高速度,更改文件时可能只更改到了系统缓存,这时,需要强制保存更改到硬盘,特别是撤销映射前。
“进程空间地址”指的是需要更改的第一个字节地址,系统会变成页面的地址;
“字节数”,系统会变成页面大小的倍数。
写入磁盘后,函数返回,对于网络硬盘,如果希望写入网络硬盘后才返回的话,需要将FILE_FLAG_WRITE_THROUGH参数传给CreateFile。
当使用FILE_MAP_COPY建立映射时,由于对数据的更改只是对虚拟页文件的修改而不是硬盘文件的修改,当撤销映射时,会丢失所做的修改。如果要保存,怎么办?
你可以用FILE_MAP_WRITE建立另外一个映射,它映射到进程的另外一段空间;扫描第一个映射的PAGE_READWRITE页面(因为属性被更改),如果页面改变,用MoveMemory或其他拷贝函数将页面内容拷贝到第二次映射的空间里,然后再调用FlushViewOfFile。当然,你要记录哪个页面被更改。
BOOL UnmapViewOfFile(PVOID pvBaseAddress)
作用:撤销映射, pvBaseAddress 这个地址必须与MapViewOfFile返回值相同。
CloseHandle(HANDLE)
作用:关闭内核对象
在不需要内核对象时,尽早将其释放,防止内存泄露。由于它们是内核对象,调用CloseHandle(HANDLE)就可以了。
在CreateFileMapping后马上关闭文件句柄;
在MapViewOfFile后马上关闭内存映射句柄;
最后再撤销映射。
内存映射文件的使用步骤
2.1 创建或打开一个文件
CreateFile
2.2 创建内存映射文件
HANDLE CreateFileMapping(
HANDLE hFile, //文件句柄
LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
//安全属性
DWORD flProtect, //保护模式
DWORD dwMaximumSizeHigh,//大小的高32位
DWORD dwMaximumSizeLow, //大小的低32位
LPCTSTR lpName ); //文件映射内核对象的名称
2.3 映射成内存地址
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, //文件映射句柄
DWORD dwDesiredAccess, //访问模式
DWORD dwFileOffsetHigh, //地址偏移高32位
DWORD dwFileOffsetLow,//地址偏移低32位
DWORD dwNumberOfBytesToMap ); //要映射的字节数
2.4 使用内存
对于大文件,可以用多次映射的方法达到访问的目的。有点像AWE技术。
Windows只保证同一文件映射内核对象的多次映射的数据一致性,比如,当有两次映射同一对象到二个进程空间时,一个进程空间的数据改变后,另一个进程空间的数据也会跟着改变;不保证不同映射内核对象的多次映射的一致性。所以,使用文件映射时,最好在CreateFile时将共享模型设置为0独享,当然,对于只读文件没这个必要。
2.5 卸载映射
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress //卸载的地址
);
2.6 关闭内存映射文件
CloseHandle
2.7 文件关闭
CloseHandle
示例代码:
#include "stdafx.h"
#include "windows.h"
void Map( )
{
//创建文件
HANDLE hFile = CreateFile( "C:\\map.dat",
GENERIC_READ|GENERIC_WRITE,
0, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, NULL );
//创建文件映射
HANDLE hMap = CreateFileMapping( hFile, NULL,
PAGE_READWRITE, 0, 1024 * 1024, NULL );
//映射地址
CHAR * pszText = (CHAR *)MapViewOfFile(
hMap, FILE_MAP_ALL_ACCESS,
0, 0, 1024 * 1024 );
//使用内存
strcpy( pszText, "Hello File Mapping" );
printf( "%s\n", pszText );
//卸载地址
UnmapViewOfFile( pszText );
//关闭文件映射
CloseHandle( hMap );
//关闭文件
CloseHandle( hFile );
}
int main(int argc, char* argv[])
{
Map( );
return 0;
}