几乎所有的应用程序都离不开对文件的操作,一般的步骤是打开文件,读写文件,关闭文件。但是,频繁的读写操作效率会很低,并且如果文件很大的情况,全部读入缓冲区也不现实,微软提供了一个叫映射文件的技术,可以完美解决上面的问题。我暂时的理解就是文件映射后得到一个指针,对这个指针进行任何操作(添加,修改数据)都是直接修改的文件。
用途1:使用内存映射文件加载和执行exe和dll,可以大大节省页面文件空间(暂未研究)
用途2:使用内存映射文件访问磁盘数据,不必IO操作,不必缓存
用途3:在同一台计算机上的多个进程来共享数据。windows下提供的其他进程间通信的方法基本都是基于内存映射文件实现的。
使用内存映射文件需要以下步骤:
1)创建或打开一个文件内核对象,用于告知磁盘上需要用作内存映射文件
2)创建文件映射内核对象,告诉系统文件的大小以及访问文件的方式
3)将文件的全部或部分映射到进程的地址空间中
映射完成后需要按照以下步骤清除
1)撤销文件映射内核对象的映像
2)关闭文件映射内核对象
3)关闭文件内核对象
创建或打开文件对象
HANDLE CreateFileA(
_In_ LPCSTR lpFileName,//文件名
_In_ DWORD dwDesiredAccess,//如何访问 (GENERIC_READ|GENERIC_WRITE或组合)
_In_ DWORD dwShareMode,//文件共享方式(FILE_SHARED_READ|FILE_SHARED_WRITE或组合或0)
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,//安全属性,内核继承相关
_In_ DWORD dwCreationDisposition,//
_In_ DWORD dwFlagsAndAttributes,
_In_opt_ HANDLE hTemplateFile
);
dwCreationDispositionLong,下述常数之一:
CREATE_NEW 创建文件;如文件存在则会出错
CREATE_ALWAYS 创建文件,会改写前一个文件
OPEN_EXISTING 文件必须已经存在。由设备提出要求
OPEN_ALWAYS 如文件不存在则创建它
TRUNCATE_EXISTING 将现有文件清空
参数见:https://baike.baidu.com/item/CreateFile/9621657?fr=aladdin
注:CreateFile失败会返回INVALID_HANDLE_VALUE
创建文件映射对象,失败返回NULL
LPVOID CreateFileMappingA(
_In_ HANDLE hFile,//上面CreateFile的返回值
_In_opt_ LPSECURITY_ATTRIBUTES lpFileMappingAttributes,//安全属性
_In_ DWORD flProtect,//保护模式。PAGE_READONLY,PAGE_READWRITE,PAGE_WRITECOPY
_In_ DWORD dwMaximumSizeHigh,//映射文件长度的高位
_In_ DWORD dwMaximumSizeLow,//映射文件长度的低位,两个组合成映射文件的长度
_In_opt_ LPCSTR lpName//内核名字,即命名对象
);
将文件数据映射到进城地址空间
MapViewOfFile(
_In_ HANDLE hFileMappingObject,//上面CreateFileMapping的返回值
_In_ DWORD dwDesiredAccess,//权限,FILE_MAP_WRITE,FILE_MAP_READ,FILE_MAP_ALL_ACCESS(与FILE_MAP_WRITE相同),FILE_MAP_COPY
_In_ DWORD dwFileOffsetHigh,//映射文件的起始位置高位
_In_ DWORD dwFileOffsetLow,//映射文件的起始位置低位,组合起来则是起始位置
_In_ SIZE_T dwNumberOfBytesToMap//此次需要映射的长度,设为0则全部映射
);
注:偏移量需要是Windows分配粒度的整数倍,(目前一直都是64K)
撤销就比较简单了,就是UnMapViewOfFile与CloseHandle
两个进程通过内存文件映射共享内存的例子,简单拖了个界面,open则是进行初始化的一些创建操作,close则是清除文件映射对象,read获取值,write写入值
初始化部分代码:
if (mapView)
{
UnmapViewOfFile(mapView);
mapView = NULL;
}
hFile = ::CreateFileA("D:/1.txt", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
MessageBox("CreateFile error");
return ;
}
HANDLE hFileMapping = ::CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 1024,"FILEMAP");
if (::GetLastError() == ERROR_ALREADY_EXISTS)
{
MessageBox("filemapping exists");
//CloseHandle(hFileMapping);
}
if (!hFileMapping)
{
MessageBox("CreateFileMapping error");
return ;
}
else
{
mapView = MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
CloseHandle(hFileMapping);
}
清除部分
UnmapViewOfFile(mapView);
mapView = NULL;
CloseHandle(hFile);
读取部分:
if (!mapView)
{
MessageBox("mapview not open");
}
char *p = (char*)mapView;
GetDlgItem(IDC_EDIT1)->SetWindowTextA(p);
由于MapViewOfFile的返回值是一个void*类型,所以可以自己进行转化,对得到的这个指针进行操作就像是在操作内存一般简单,不过实际上对它的所有操作都是在修改了文件
写入部分:
if (!mapView)
{
MessageBox("mapview closed");
return;
}
CString p;
GetDlgItemText(IDC_EDIT1,p);
memset(mapView, 0, 1024);
//SetEndOfFile必须在撤销视图的映像并关闭了文件映射对象之后才能调用,不然就会返回FALSE
//使用GetLastError返回ERROR_USER_MAPPED_FILE,表明不能在与文件映射对象相关联的对象上执行此操作
//此处只是简单的进行清0处理
//SetFilePointer(hFile, p.GetLength(),NULL, FILE_BEGIN);
//BOOL bRet = SetEndOfFile(hFile);
memcpy(mapView, p.GetBuffer(0), p.GetLength());
p.ReleaseBuffer();
上面清0的原因是如果一开始文件内容比较长,然后写入了一个短一点的字符串,再次读取后会读取到后面的未被修改的数据
例:源字符串 abc123,之后一个进程写入123,另一个进程再读时会得到123123