内存映射文件不同于文件I/O操作,内存映射实际用到了Windows的核心编程技术–内存管理。
使用内存映射文件的一般方法:
首先要通过CreateFile()函数来创建或打开一个文件内核对象,这个对象标识了磁盘上将要用作内存映射文件的文件。
在用CreateFile()将文件映像在物理存储器的位置通告给操作系统后,只指定了映像文件的路径,映像的长度还没有指定。
为了指定文件映射对象需要多大的物理存储空间还需要通过CreateFileMapping()函数来创建一个文件映射内核对象以告诉
系统文件的尺寸以及访问文件的方式。
在创建了文件映射对象后,还必须为文件数据保留一个地址空间区域,并把文件数据作为映射到该区域的物理存储器进行提交。
由MapViewOfFile()函数负责通过系统的管理而将文件映射对象的全部或部分映射到进程地址空间。此时,对内存映射文件的使用
和处理同通常加载到内存中的文件数据的处理方式基本一样,在完成了对内存映射文件的使用时,还要通过一系列的操作完成对其
清除和使用过资源的释放,可以通过UnmapViewOfFile()完成从进程的地址空间撤消文件数据的映像、
通过CloseHandle()关闭前面创建的文件映射对象和文件对象。
一:
CreateFileMapping---创建有名或无名的共享内存:
HANDLE CreateFileMapping(
HANDLE hFile, //物理文件句柄,设为0xFFFFFFFF以创建一个进程间共享的对象
LPSECURITY_ATTRIBUTES lpAttributes, //安全设置
DWORD flProtect, //保护方式
DWORD dwMaximumSizeHigh, //高位文件大小
DWORD dwMaximumSizeLow, //低位文件大小
LPCTSTR lpName //共享内存名称
);
参数:
1.物理文件句柄:
1)需要创建一个物理文件无关的内存映射, 将它设置为 0xFFFFFFFF(INVALID_HANDLE_VALUE).
2)需要和物理文件关联, 要确保物理文件创建时访问模式和"保护方式"匹配,
如: 物理文件只读, 内存映射需要读写就会发生错误. 推荐物理文件使用独占方式创建.
3)使用 INVALID_HANDLE_VALUE, 则需要设置申请内存空间的大小, 无论物理文件句柄参数是否有效,
都可以创建一个和物理文件大小无关的内存空间, 甚至超过实际文件大小。
如果物理文件有效, 而大小参数为0, 则返回的是一个和物理文件大小
一样的内存空间地址范围. 返回的文件映射地址空间是可以通过复制, 集成或者命名得到, 初始内容为0.
注意:
hFile:文件打开模式必须与flProtect参数指定相一致;如果这个参数值为0xFFFFFFFF,那么必须在dwMaximumSizeHigh和
dwMaximumSizeLow参数中指定映射对象的大小。并且将在操作系统虚拟内存页面替换文件中创建文件映射对象,而不是使用磁盘文件,同时必须给
出这个映射对象的大小。文件映射对象通过副本,遗传或名字来共享。
2.安全设置(安全描述符指针):
决定返回句柄是否能被子进程继承,如果是NULL,那么子进程不能继承。
WinNt中,如果是NULL,那么文件映射对象得到一个默认的安全描述符。
所以:一般设置NULL(默认)即可.
3.保护方式:
在win2k下如果需要进行限制, 这是针对那些将内存文件映射共享给整个网络上面的应用进程使用时, 可以考虑进行限制.
如果多进程都对同一共享内存进行写访问,则需要考虑同步。
PAGE_READONLY :只读属性,并且hFile对应的文件必须以GENERIC_READ形式打开。
PAGE_READWRITE:可读可写属性,并且hFile对应的文件必须以GENERIC_READ 和 GENERIC_WRITE形式打开。
PAGE_WRITECOPY:对可写区域复制后操作,并且hFile对应的文件必须以GENERIC_READ 和 GENERIC_WRITE形式打开。
指定为PAGE_WRITECOPY,可以保证其原始数据不会遭到破坏,同时允许其他进程在必要时对数据进行拷贝。
4.位文件大小:dwMaximumSizeHigh
32位机器,不可能得到超过32位进程所能寻址的私有32位地址空间, 一般设置0.
5.位文件大小:dwMaximumSizeLow
为了让其他共享用户知道申请的文件映射的相关信息, 使用时在获得的地址空间头部添加一个结构化描述信息,记录内存映射的大小, 名称等,
这样实际申请的空间就比输入的增加了一个头信息结构大小, 类似于BSTR的方式.
注意:如果dwMaximumSizeHigh,dwMaximumSizeLow 这两个参数为0,则文件映射对象的最大长度等于hFile指定的文件长度。
6.共享内存名称:lpName
为了对内存进行互斥访问, 设置了一个互斥句柄, 而当名称选择和命名共享内存同名时, 因为他们使用共同的namespace导致了错误.
如果这个名字已存在,则按照flProtect指定的来处理映射对象。
如果此参数为空,则创建一个无名字的文件映射对象。
如果此参数的名字与系统事件的名字相同,则函数执行失败,GetLastError返回 ERROR_INVALID_HANDLE;
用CreateFileMapping时使用GetLastError可以获得对应错误:
ERROR_FILE_INVALID 企图创建一个零长度的文件映射
ERROR_INVALID_HANDLE 发现命名内存空间和现有的内存映射, 互斥量, 信号量, 临界区同名
ERROR_ALREADY_EXISTS 表示内存空间命名已经存在
7.返回值:HANDLE
函数调用成功返回文件映射对象的句柄,
如果文件映射对象已经存在则返回原有映射对象的句柄,GetLastError返回ERROR_ALREADY_EXISTS。
函数执行失败返回Null。
简述CreateFileMapping():
该函数创建一个文件映射内核对象,通过参数hFile指定待映射到进程地址空间的文件句柄(该句柄由CreateFile()函数
的返回值获取)。由于内存映射文件的物理存储器实际是存储于磁盘上的一个文件,而不是从系统的页文件中分配的内存,所以系统不会
主动为其保留地址空间区域,也不会自动将文件的存储空间映射到该区域,为了让系统能够确定对页面采取何种保护属性,需要通过参数
flProtect来设定,保护属性PAGE_READONLY、PAGE_READWRITE和PAGE_WRITECOPY分别表示文件映射对象被映射后,可以读取、读写文件数据。
在使用PAGE_READONLY时,必须确保CreateFile()采用的是GENERIC_READ参数;
PAGE_READWRITE则要求CreateFile()采用的是GENERIC_READ|GENERIC_WRITE参数;
至于属性PAGE_WRITECOPY则只需要确保CreateFile()采用了GENERIC_READ和GENERIC_WRITE其中之一即可。
DWORD型的参数dwMaximumSizeHigh和dwMaximumSizeLow也是相当重要的,指定了文件的最大字节数,由于这两个参数共64位,因此所支持
的最大文件长度为16EB,几乎可以满足任何大数据量文件处理场合的要求。
二:MapViewOfFile—创建文件视图(共享内存对象:在调用进程的地址空间映射一个文件视图)
创建文件映射对象后,要将文件中的数据映射到进程的虚拟内存中,必须创建一个文件的视图。
调用MapViewOfFile函数映射到本进程的地址空间内。
LPVOID WINAPI MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap);
);
参数:
hFileMappingObject: 由CreateFileMapping 或 OpenFileMapping 返回的文件映射对象句柄。
dwDesiredAccess:映射视图的访问模式,与创建文件映射对象的保护模式flProtect有关:
FILE_MAP_WRITE:一个可读写属性的文件视图被创建,保护模式为PAGE_READWRITE
FILE_MAP_READ :一个只读属性的文件视图被创建,保护模式为PAGE_READWRITE 或 PAGE_READONLY
FILE_MAP_ALL_ACCESS:与FILE_MAP_WRITE模式相同
FILE_MAP_COPY:保护模式为PAGE_WRITECOPY时,得到一个视图文件,当你对视图文件写操作时,页面自动交换,并且你所做的修改不会损坏原始数据资料。
dwNumberOfBytesToMap:映射文件部分的大小,如果为0,则映射整个文件。
返回值:
如果成功返回返回映射视图的起始地址,如果失败返回NULL。
MapViewOfFile和MapViewOfFileEx函数使用CreateFileMapping返回的文件映射对象句柄来在进程的虚拟地址空间里建立文件的视图,
或者文件的某个部分。如果这些函数指定的权限标志和CreateFileMapping中的权限标志不一致,则会执行失败。
1)MapViewOfFile函数返回一个指向文件视图的指针。利用MapViewOfFile中声明的地址指针,可以从文件中读或向文件中写入数据。
向文件视图中写入数据会导致文件映射对象改变。真正将数据写入到磁盘上的文件,由系统负责处理。数据并不是马上就被写到磁盘上,
很多文件的输入输出都被缓存起来,以改善系统的性能。程序可以调用FlushViewOfFile函数来越过这个方式,强迫系统马上将数据写入到
磁盘中去。
2)MapViewOfFileEx函数和MapViewOfFile函数的工作是一样的,只不过可以利用MapViewOfFileEx函数的lpvBase参数,来指定文件视图
在进程虚拟地址空间中的基础地址。如果在指定的地址处没有足够的空间,则调用失败。
lpvBase参数必须是系统内存最小单位的整数倍,否则调用会失败。
要得到系统内存的最小单位,使用GetSystemInfo函数,将信息写到SYSTEM_INFO结构的成员中。
3)程序可以从同一个文件映射对象中创建多个文件视图。文件视图可以是不同的大小,但他们必须小于文件映射对象。
注意:MapViewOfFile函数的dwOffsetHigh和dwOffsetLow参数必须是系统内存最小单位的整数倍。
内存映射文件的读写和一般的文件读写不同, 是直接面对申请的地址空间, 为此需要使用MapViewOfFile得到相关的地址LPVOID类型的指针。
如果需要进行文件写入, 可以通过类型转换直接对于内存地址进行赋值,
如:
memcpy( lpAddress, lpBuf, …)
但要注意:防止内存溢出的情况。
简述MapViewOfFile:
函数负责把文件数据映射到进程的地址空间,参数hFileMappingObject为CreateFileMapping()返回的文件映像对象句柄。
参数dwDesiredAccess则再次指定了对文件数据的访问方式,而且同样要与CreateFileMapping()函数所设置的保护属性相匹配。虽然这里
一再对保护属性进行重复设置看似多余,但却可以使应用程序能更多的对数据的保护属性实行有效控制。MapViewOfFile()函数允许全部或
部分映射文件,在映射时,需要指定数据文件的偏移地址以及待映射的长度。其中,文件的偏移地址由DWORD型的
参数dwFileOffsetHigh和dwFileOffsetLow组成的64位值来指定,而且必须是操作系统的分配粒度的整数倍,对于Windows操作系统,分配
粒度固定为64KB。当然,也可以通过如下代码来动态获取当前操作系统的分配粒度:
SYSTEM_INFO sinf;
GetSystemInfo(&sinf);
DWORD dwAllocationGranularity = sinf.dwAllocationGranularity;
参数dwNumberOfBytesToMap指定了数据文件的映射长度,
特别指出:对于Windows9x操作系统,如果MapViewOfFile()无法找到足够大的区域来存放整个文件映射对象,将返回空值(NULL);
但在Windows2000下,MapViewOfFile()只需要为必要的视图找到足够大的一个区域即可,而无须考虑整个文件映射对象的大小。
在完成对映射到进程地址空间区域的文件处理后,需要通过函数UnmapViewOfFile()完成对文件数据映像的释放,该函数原型声明如下:
BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);
参数lpBaseAddress指定了返回区域的基地址,必须将其设定为MapViewOfFile()的返回值。在使用了函数MapViewOfFile()之后,必须要
有对应的UnmapViewOfFile()调用,否则在进程终止之前,保留的区域将无法释放。除此之外,前面还曾由CreateFile()和CreateFileMapping()
函数创建过文件内核对象和文件映射内核对象,在进程终止之前有必要通过CloseHandle()将其释放,否则将会出现资源泄漏的问题。
三:OpenFileMapping—打开命名共享内存(打开一个已命名的文件映射对象)
HANDLE OpenFileMapping(
DWORD dwDesiredAccess, // 访问模式
BOOL bInheritHandle, // 继承标志
LPCTSTR lpName // 文件映射对象名指针
);
注意:
dwDesiredAccess:访问模式与MapViewOfFile中的访问模式相同。
bInheritHandle:继承标志,是否可以被一个新的进程继承使用,如果为TRUE,可以继承。
返回值:
成功返回一个已命名的文件映射对象,失败返回NULL。
四:关闭内存映射文件: CloseHandle
内存映射文件相关函数除了上面列出的主要API函数,还有:
HANDLE CreateFile(
LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
函数CreateFile()即使是在普通的文件操作时也经常用来创建、打开文件,在处理内存映射文件时,该函数来创建/打开一个文件内核对象,
并将其句柄返回,在调用该函数时需要根据是否需要数据读写和文件的共享方式来设置参数dwDesiredAccess和dwShareMode,错误的参数
设置将会导致相应操作时的失败。
MapViewOfFileEx 在调用进程的地址空间映射一个文件视图,并且允许调用进程为映射视图指定特殊的内存地址
LPVOID MapViewOfFileEx(
HANDLE hFileMappingObject, // 文件映射对象的句柄
DWORD dwDesiredAccess, // 访问模式
DWORD dwFileOffsetHigh, // 文件偏移的高32位
DWORD dwFileOffsetLow, // 文件偏移的低32位
DWORD dwNumberOfBytesToMap, // 映射视图的大小
LPVOID lpBaseAddress // 指定映射视图的其实内存地址
);
注意:
与MapViewOfFile用法相同,但是如果指定的内存地址空间大小不够,则函数执行失败。
将内存复制到所映射的物理文件上:
FlushMapViewOfFile:将内存里面的内容DUMP到物理磁盘上面
FlushViewOfFile :把文件映射视图中的修改的内容或全部写回到磁盘文件中
BOOL FlushViewOfFile
(
LPCVOID lpBaseAddress, // 修改内容的起始地址
DWORD dwNumberOfBytesToFlush // 修改的字节数目
);
函数执行成功返回非零。
卸载内存映射文件地址指针
UnmapViewOffFile 卸载
UnmapViewOfFile 删除文件的映射视图
BOOL UnmapViewOfFile
(
LPCVOID lpBaseAddress // 映射视图起始地址,由 MapViewOfFile 函数 MapViewOfFileEx产生。
);
返回值:
如果调用成功返回非零,并且所有指定地址内的脏页面会被写入硬盘。调用失败返回零。
例子:通过MFC3个按钮分别实现内存共享的创建,映射,打开和关闭功能
1.新建MFC工程,添加3个按钮,并为每个按钮添加如下代码:
2.创建按钮:
HANDLE m_hFileMapping;
void CMappingDlg::OnBtnCreate()
{
LPCTSTR lpFileMappingName = _T("LaserShare");
m_hFileMapping = CreateFileMapping(
INVALID_HANDLE_VALUE,
NULL,
PAGE_READWRITE,
0,
4 * 1024,
lpFileMappingName
);
DWORD dwErr = GetLastError();
if (NULL == m_hFileMapping )
{
AfxMessageBox(_T("无法创建该内存映射文件"));
return;
}
if (dwErr == ERROR_ALREADY_EXISTS)
{
AfxMessageBox(_T("存在同名内存映射文件"));
CloseHandle(m_hFileMapping);
return;
}
PVOID pMapOfView = MapViewOfFile(m_hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0 /*4 * 1024*/);
if (NULL == pMapOfView)
{
AfxMessageBox(_T("映射该文件错误"));
CloseHandle(m_hFileMapping);
return;
}
ZeroMemory(pMapOfView, 4 * 1024);
CString strText;
GetDlgItemText(IDC_EDIT_DATA, strText);
memcpy(pMapOfView, strText.GetBuffer(sizeof(strText)),(strText.GetLength() + 1) * sizeof(TCHAR));
UnmapViewOfFile(pMapOfView);
//CloseHandle(m_hFileMapping); //此处不能关闭,否则其他进程不能打开该内存映射文件
}
3.打开按钮:
void CMappingDlg::OnBtnOpen()
{
LPCTSTR lpFileMappingName = _T("LaserShare");
//HANDLE hFileMapping = OpenFileMapping(PAGE_READWRITE, FALSE, lpFileMappingName);//error
//OpenFileMapping的第一个参数一定不能是PAGE_*, 区别于CreateFileMapping
HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ | FILE_MAP_WRITE, FALSE, lpFileMappingName);
if (NULL == hFileMapping)
{
AfxMessageBox(_T("打不开该内存映射文件"));
SetDlgItemText(IDC_EDIT_DATA, _T(""));//置空
return;
}
PVOID pMapOfFile = MapViewOfFile(hFileMapping, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0 /*4 * 1024 */);
if (NULL == pMapOfFile)
{
AfxMessageBox(_T("映射该文件错误"));
CloseHandle(hFileMapping);
return;
}
TCHAR tchArr[256];
ZeroMemory(tchArr, sizeof(tchArr));
memcpy(tchArr, pMapOfFile, sizeof(tchArr));
SetDlgItemText(IDC_EDIT_DATA, tchArr);
UnmapViewOfFile(pMapOfFile);
CloseHandle(hFileMapping);
}
4.关闭按钮:
void CMappingDlg::OnBtnClose()
{
CloseHandle(m_hFileMapping);
}
运行:
运行2次,打开2个实例程序,
在编辑框里输入内容后点击创建,在另一个程序里点击打开,即可看到在前一个共享的内容: