Windows系统中CreateFileMapping实现的共享内存及用法

在32位的Windows系统中,每一个进程都有权访问他自己的4GB(232=4294967296)平面地址空间,没有段,没有选择符,没有near和far指针,没有near和far函数调用,也没有内存模式。

每个进程都有独立的4GB逻辑地址空间,32位的Windows系统允许每一个进程独立访问自己的内存,即独立于其它进程,也即它自己的32位逻辑地址空间。操作系统将把每一个进程的逻辑地址转换成实际的物理地址,独立的地址空间可以使其他已经出错的进程之间相互隔离,入阁一个进程通过他自己的内存空间处理数据,其他的进程就比在DOS中安全,在DOS中的所有应用程序共享相同的物理内存空间,虽然这带来了许多好处,但在不同进程之间转的指针,就会出现一些麻烦。在一个进程中,一个给定的逻辑地址将与另一进程的指针不会有相同的逻辑地址。

那么怎么样才能在32位的Windows系统中达到共享内存的目的呢?

随着进程的分离内存空间的出现,进程不能简单地使用GlobalAlloc()函数来分配内存,并把它转递给另一个进程来共享,一个进程检查有另一个进程分配的指针,他只是指向随机地址。然而,在32位的Windows系统支持在进程间共享内存映象文件,可以通过内存映象文件来达到内存共享的目的。

32位的Windows系统的虚拟内存系统具有把实际内存映象到页面文件或交换文件的能力.通过把内存映象到任何想映象的文件,包括系统页面本身,应用程序就可以拓展这种能力。文件影响能用来提供更快更简捷的文件访问方式,并提供内存共享。

把文件映像到内存,首先必须调用CreateFileMapping()函数,然后再调用MapViewOfFile函数,把文件视映像到进程地址空间上。

C:专家点评
要把文件映像到内存,首先必须调用CreateFileMapping()函数,它需要用一个由CreateFile()函数打开并返回的文件句柄,对大多数共享内存应用程序。必须把此句柄设置为0xFFFFFFFF,用来指定系统页面文件。通过使用上面的特殊句柄,可以不调用CreateFile函数,当然在完成时,也不必有一个内存的磁盘文件拷贝。

CreateFileMapping()函数的第二个参数是一个指向SECURITY_ATTRIBUTES结构的指针,它指明返回的句柄是否可以被子进程所继承。另外,在SECURITY_ATTRIBUTES结构中,也包括一个安全性描述的子指针,它由WinNT支持它的安全机制。

第三个参数允许指定内存块的访问权限,权限值有PAGE_READONLY、PAGE_READWRITE和PAGE_WRITECOPY,PAGE_WRITECOPY可以在指定页面上拷贝并写访问。这意味着当一个进程映像此内存并写入时,它将得到自己修改数据的拷贝,面不是写到共享内存空间,另外,可以把几个标志一起使用来指定其他部分的属性。

其他参数允许指定内存块的最大尺寸,如果内存块的尺寸比第一个 参数中指定的文件尺寸还要大,这个文件就增大。

最后一个参数为内存映像对象指定名字,通过调用CreateFileMapping函数和OpenFileMapping函数,其他进程可用这个名字来访问相同的文件映像。

一旦一个内存映像对象由CreateFileMapping()创建成功,可以调用MapViewOfFile函数把文件视映像到进程地址空间上,这个函数需要用一个由CreateFileMapping()函数或OpenFileMapping()函数返回的句柄,并允许指定访问模式和映像的字节数,以及文件映像对象中的偏移量。另外,还可以使用MapViewOfFileEx()函数来实现同样的功能,只是此函数还允许指定映像对象的起始地址。

当用完映像文件后,可以通过调用UnmapViewOfFile函数,释放映像内存并把一些映像数据写入文件(除非它是交换文件),如果想立即把数据写回磁盘文件,那就需要调用FlushViewOfFile()函数把映像内存写入文件。

为了便于读者对以上程序的理解和进行下一步的学习,这里再简单介绍一下Win32内存模式。在开始学习Win32内存管理之前,最好先了解一些16位Windows与32位Windows的区别,它们之间的主要区别是把寄存器和指针长度变为32位。

Win32 API允许应用程序访问虚拟内存,如果想保存大量的数据,这十分有用,尽管大部分空间从来没有用于物理存储。虚拟内存页有四种状态,提交页面被分配一个物理存储单元,不管它是在是在交换文件中,提交页面被锁定后就强行留在内存中,直到解锁为止。保留页面存储一块保留地址国,它不被分配任何存储单元,*页面都没有用。

VirtualAlloc()函数可用来分配从指定逻辑内存开始的一定范围内的地址,该函数的另一个参数允许指定访问保护标志,并指定内存是否被保存或被用于物理存储单元,也可以调用此函数来调配前面保存的虚拟内存页。

可以通过调用GloballLock()函数来强制使内存块留在物理内存中,应该谨慎使用此函数,因为它阻止系统管理这块内存,直到调用GlobalUnlock()函数为止。如果一个进程锁定了一大块内存,其他进程就会终止,不管还有多少内存。

函数VirtualProtect()用于改变一块内存的访问权限,VirtualQuery()函数返回内存页的住处。如果想访问其他进程的页面,可以通过调用VirtualProtecteEx和VirutalQueryEx两个函数来实现。

D:测试创建和打开文件映射的时候老是得到"句柄无效"的错误, 仔细看了MSDN以后才发觉是函数认识不透, 这里把相关的解释翻译出来

HANDLE CreateFileMapping(
HANDLE hFile,                       //物理文件句柄
LPSECURITY_ATTRIBUTES lpAttributes, //安全设置
DWORD flProtect,                    //保护设置
DWORD dwMaximumSizeHigh,            //高位文件大小
DWORD dwMaximumSizeLow,             //低位文件大小
LPCTSTR lpName                      //共享内存名称
);

1) 物理文件句柄
   任何可以获得的物理文件句柄, 如果你需要创建一个物理文件无关的内存映射也无妨, 将它设置成为 0xFFFFFFFF(INVALID_HANDLE_VALUE)就可以了.

如果需要和物理文件关联, 要确保你的物理文件创建的时候的访问模式和"保护设置"匹配, 比如: 物理文件只读, 内存映射需要读写就会发生错误. 推荐你的物理文件使用独占方式创建.

如果使用 INVALID_HANDLE_VALUE, 也需要设置需要申请的内存空间的大小, 无论物理文件句柄参数是否有效, 这样 CreateFileMapping 就可以创建一个和物理文件大小无关的内存空间给你, 甚至超过实际文件大小, 如果你的物理文件有效, 而大小参数为0, 则返回给你的是一个和物理文件大小一样的内存空间地址范围. 返回给你的文件映射地址空间是可以通过复制, 集成或者命名得到, 初始内容为0.

2) 保护设置
   就是安全设置, 不过一般设置NULL就可以了, 使用默认的安全配置. 在win2k下如果需要进行限制, 这是针对那些将内存文件映射共享给整个网络上面的应用进程使用是, 可以考虑进行限制.

3) 高位文件大小
   弟兄们, 我想目前我们的机器都是32位的东东, 不可能得到超过32位进程所能寻址的私有32位地址空间, 一般还是设置0吧, 我没有也不想尝试将它设置超过0的情况.
4) 低位文件大小
   这个还是可以进行设置的, 不过为了让其他共享用户知道你申请的文件映射的相关信息, 我使用的时候是在获得的地址空间头部添加一个结构化描述信息, 记录内存映射的大小, 名称等, 这样实际申请的空间就比输入的增加了一个头信息结构大小了, 我认为这样类似BSTR的方式应该是比较合理的.

5) 共享内存名称
   这个就是我今天测试的时候碰壁的祸根, 因为为了对于内存进行互斥访问, 我设置了一个互斥句柄, 而名称我选择和命名共享内存同名, 之下就是因为他们使用共同的namespace导致了错误, 呵呵.

7) 调用CreateFileMapping的时候GetLastError的对应错误
   ERROR_FILE_INVALID     如果企图创建一个零长度的文件映射, 应有此报
   ERROR_INVALID_HANDLE   如果发现你的命名内存空间和现有的内存映射, 互斥量, 信号量, 临界区同名就麻烦了
   ERROR_ALREADY_EXISTS   表示内存空间命名已经存在

8) 相关服务或者平台的命名保留
   Terminal Services:
   命名可以包含 "Global" 或者 "Local" 前缀在全局或者会话名空间初级文件映射. 其他部分可以包含任何除了()以外的字符, 可以参考 Kernel Object Name Spaces.

Windows 2000 or later:
   如果 Terminal Services 没有运行 "Global" 和 "Local" 前缀的特殊含义就被忽略了

2

在开发软件过程里,也经常碰到进程间共享数据的需求。比如A进程创建计算数据,B进程进行显示数据的图形。这样的开发方式可以把一个大程序分开成独立的小程序,提高软件的成功率,也可以更加适合团队一起开发,加快软件的开发速度。下面就来使用文件映射的方式进行共享数据。先要使用函数CreateFileMapping来创建一个想共享的文件数据句柄,然后使用MapViewOfFile来获取共享的内存地址,然后使用OpenFileMapping函数在另一个进程里打开共享文件的名称,这样就可以实现不同的进程共享数据。
函数CreateFileMapping、MapViewOfFile声明如下:
WINBASEAPI
__out
HANDLE
WINAPI
CreateFileMappingA(
__in HANDLE hFile,
__in_opt LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
__in DWORD flProtect,
__in DWORD dwMaximumSizeHigh,
__in DWORD dwMaximumSizeLow,
__in_opt LPCSTR lpName
);
WINBASEAPI
__out
HANDLE
WINAPI
CreateFileMappingW(
__in HANDLE hFile,
__in_opt LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
__in DWORD flProtect,
__in DWORD dwMaximumSizeHigh,
__in DWORD dwMaximumSizeLow,
__in_opt LPCWSTR lpName
);
#ifdef UNICODE
#define CreateFileMapping CreateFileMappingW
#else
#define CreateFileMapping CreateFileMappingA
#endif // !UNICODE
WINBASEAPI
__out
LPVOID
WINAPI
MapViewOfFile(
__in HANDLE hFileMappingObject,
__in DWORD dwDesiredAccess,
__in DWORD dwFileOffsetHigh,
__in DWORD dwFileOffsetLow,
__in SIZE_T dwNumberOfBytesToMap
);
hFile是创建共享文件的句柄。
lpFileMappingAttributes是文件共享的属性。
flProtect是当文件映射时读写文件的属性。
dwMaximumSizeHigh是文件共享的大小高位字节。
dwMaximumSizeLow是文件共享的大小低位字节。
lpName是共享文件对象名称。
hFileMappingObject是共享文件对象。
dwDesiredAccess是文件共享属性。
dwFileOffsetHigh是文件共享区的偏移地址。
dwFileOffsetLow是文件共享区的偏移地址。
dwNumberOfBytesToMap是共享数据长度。
调用函数的例子如下:
# //文件共享。
# //蔡军生 2007/10/27 QQ:9073204 深圳
# void FileMapping(void)
# {
# //打开共享的文件对象。
# m_hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS,
# FALSE,_T("TestFileMap"));
# if (m_hMapFile)
# {
# //显示共享的文件数据。
# LPTSTR lpMapAddr = (LPTSTR)MapViewOfFile(m_hMapFile,FILE_MAP_ALL_ACCESS,
# ,,);
# OutputDebugString(lpMapAddr);
# }
# else
# {
# //创建共享文件。
# m_hMapFile = CreateFileMapping( (HANDLE)0xFFFFFFFF,NULL,
# PAGE_READWRITE,,,_T("TestFileMap"));
#
# //拷贝数据到共享文件里。
# LPTSTR lpMapAddr = (LPTSTR)MapViewOfFile(m_hMapFile,FILE_MAP_ALL_ACCESS,
# ,,);
# std::wstring strTest(_T("TestFileMap"));
# wcscpy(lpMapAddr,strTest.c_str());
#
# FlushViewOfFile(lpMapAddr,strTest.length()+);
# }
# }

内存映射API函数CreateFileMapping创建一个有名的共享内存:
HANDLE CreateFileMapping(
HANDLE hFile, // 映射文件的句柄,
//设为0xFFFFFFFF以创建一个进程间共享的对象
LPSECURITY_ATTRIBUTES lpFileMappingAttributes, // 安全属性
DWORD flProtect, // 保护方式
DWORD dwMaximumSizeHigh, //对象的大小
DWORD dwMaximumSizeLow,
LPCTSTR lpName // 必须为映射文件命名
);

与虚拟内存类似,保护方式可以是PAGE_READONLY或是PAGE_READWRITE。如果多进程都对同一共享内存进行写访问,则必须保持相互间同步。映射文件还可以指定PAGE_WRITECOPY标志,可以保证其原始数据不会遭到破坏,同时允许其他进程在必要时*的操作数据的拷贝。

在创建文件映射对象后使用可以调用MapViewOfFile函数映射到本进程的地址空间内。

下面说明创建一个名为MySharedMem的长度为4096字节的有名映射文件:
HANDLE hMySharedMapFile=CreateFileMapping((HANDLE)0xFFFFFFFF),
NULL,PAGE_READWRITE,0,0x1000,"MySharedMem");
并映射缓存区视图:
LPSTR pszMySharedMapView=(LPSTR)MapViewOfFile(hMySharedMapFile,
FILE_MAP_READ|FILE_MAP_WRITE,0,0,0);

其他进程访问共享对象,需要获得对象名并调用OpenFileMapping函数。
HANDLE hMySharedMapFile=OpenFileMapping(FILE_MAP_WRITE,
FALSE,"MySharedMem");

一旦其他进程获得映射对象的句柄,可以象创建进程那样调用MapViewOfFile函数来映射对象视图。用户可以使用该对象视图来进行数据读写操作,以达到数据通讯的目的。

当用户进程结束使用共享内存后,调用UnmapViewOfFile函数以取消其地址空间内的视图:
if (!UnmapViewOfFile(pszMySharedMapView))
{

AfxMessageBox("could not unmap view of file");

}

上一篇:decorator(修饰器)的业务应用


下一篇:C# 中 string.Empty、""、null的差别