进程间通信——内存共享
内存映射文件
内存映射文件是由一个文件到一块内存的映射。Win32提供了允许应用程序把文件映射到一个进程的函数(CreateFileMapping)。这样,文件内的数据就可以用内存读/写指令来访问,而不是用ReadFile和WriteFile这样的I/O系统函数,从而提高了文件存取速度。
这种函数最适用于需要读取文件并且对文件内包含的信息做语法分析的应用程序,如对输入文件进行语法分析的彩色语法编辑器,编译器等。把文件映射后进行读和分析,能让应用程序使用内存操作来操纵文件,而不必在文件里来回地读、写、移动文件指针。
有些操作,如放弃“读”一个字符,在以前是相当复杂的,用户需要处理缓冲区的刷新问题。在引入了映射文件之后,就简单的多了。应用程序要做的只是使指针减少一个值。
映射文件的另一个重要应用就是用来支持永久命名的共享内存。要在两个应用程序之间共享内存,可以在一个应用程序中创建一个文件并映射之,然后另一个应用程序可以通过打开和映射此文件把它作为共享的内存来使用。
内存映射文件:
内存映射文件有三种,第一种是可执行文件的映射,第二种是数据文件的映射,第三种是借助页面交换文件的内存映射.应用程序本身可以使用后两种内存映射.
1.可执行文件映射:
Windows在执行一个Win32应用程序时使用的是内存映射文件技术.系统先在进程地址空间的0x00400000以上保留一个足够大的虚拟地址空间(0x00400000以下是由系统管理的),然后把应用程序所在的磁盘空间作为虚拟内存提交到这个保留的地址空间中去(我的理解也就是说,虚拟内存是由物理内存和磁盘上的页面文件组成的,现在应用程序所在的磁盘空间就成了虚拟地址的页面文件).做好这些准备后,系统开始执行这个应用程序,由于这个应用程序的代码不在内存中(在页面文件中),所以在执行第一条指令的时候会产生一个页面错误(页面错误也就是说,系统所访问的数据不在内存中),系统分配一块内存把它映射到0x00400000处,把实际的代码或数据读入其中(系统分配一块内存区域,把它要访问的在页面文件中的数据读入到这块内存中,需在注意是系统读入代码或数据是一页一页读入的),然后可以继续执行了.当以后要访问的数据不在内存中时,就可以通过前面的机制访问数据.对于Win32DLL的映射也是同样,不过DLL文件应该是被Win32进程共享的(我想应该被映射到x80000000以后,因为0x80000000-0xBFFFFFFF是被共享的空间).
当系统在另一个进程中执行这个应用程序时,系统知道这个程序已经有了一个实例,程序的代码和数据已被读到内存中,所以系统只需把这块内存在映射到新进程的地址空间即可,这样不就实现了在多个进程间共享数据了吗!然而这种共享数据只是针对只读数据,如果进程改写了其中的代码和数据,操作系统就会把修改的数据所在的页面复制一份到改写的进程中(我的理解也就是说共享的数据没有改变,进程改写的数据只是共享数据的一份拷贝,其它进程在需要共享数据时还是共享没有改写的数据),这样就可以避免多个进程之间的相互干扰.
目的:系统使用内存映射文件,以便加载和执行exe 和dll文件,这可以大大节省页文件空间和应用程序启动运行所需的时间。
2.数据文件的内存映射:
数据文件的内存映射原理与可执行文件内存映射原理一样.先把数据文件的一部分映射到虚拟地址空间的0x80000000 - 0xBFFFFFFF,但没有提交实际内存(也就是说作为页面文件),当有指令要存取这段内存时同样会产生页面错误异常.操作系统捕获到这个异常后,分配一页内存,映射内存到发生异常的位置,然后把要访问的数据读入到这块内存,继续执行刚才产生异常的指令(这里我理解的意思是把刚才产生异常的指令在执行一次,这次由于数据已经映射到内存中,指令就可以顺利执行过去),由上面的分析可知,应用程序访问虚拟地址空间时由操作系统管理数据在读入等内容,应用程序本身不需要调用文件的I/O函数(这点我觉得很重要,也就是为什么使用内存映射文件技术对内存的访问就象是对磁盘上的文件访问一样).
目的:可以使用内存映射文件来访问磁盘上的数据文件,这使你可以不必对文件执行I/O操作,并且可以不必对文件内容进行缓存。
3.基于页面交换文件的内存映射:
内存映射的第三种情况是基于页面交换文件的.一个Win32进程利用内存映射文件可以在进程共享的地址空间保留一块区域(0x8000000 - 0xBFFFFFFF),这块区域与系统的页面交换文件相联系.我们可以用这块区域来存储临时数据,但更常见的做法是利用这块区域与其他进程通信(因为0x80000000以上是系统空间,进程切换只是私有地址空间,系统空间是所有进程共同使用的),这样多进程间就可以实现通信了.事实上Win32多进程间通信都是使用的内存映射文件技术,如PostMessage(),SentMessage()函数,在内部都使用内存映射文件技术.
目的:可以使用内存映射文件,使同一台计算机上运行的多个进程能够相互之间共享数据,Windows确实提供了其他一些方法,以便在进程之间进行数据通信,但是这些方法都是使用内存映射文件来实现的,这使得内存映射文件成为单个计算机上多个进程互相进行同行的最有效的方法。
内存映射文件的函数:
CreateFileMapping , OpenFileMapping, MapViewOfFile, UnmapViewOfFile 和 FlushViewOfFile 。
用法如下:
1 . HANDLE CreateFileMapping(
HANDLE hFile, // 一个文件句柄
LPSECURITY_ATTRIBUTE lpAttributes, // 定义内存映射文件对象是否可以被承
DWORD flProtect, // 该内存映射文件的保护类型
DWORD dwMaximumSizeHigh,// 内存映射文件的长度
DWORD dwMaximumSizeLow, //
LPCTSTR lpName // 内存映射文件的名字
)
hFile 指定要映射的文件的句柄,如果这是一个已经打开的文件的句柄( CreateFile 函数的返回值),那么将建立这个文件的内存映射文件,如果这个参数为 -1(0xFFFFFFFF) ,则建立共享内存。
lpAttribute 安全属性,一般设为 NULL
flProtect 指定映射文件的保护类型,它的取值可以是 PAGE_READONLY (内存页面只读) 或 PAGE_READWRITE (内存页面可读写)。
dwMaximumSizeHigh 和 dwMaximumSizeLow 参数组合指定了一个 64 位的内存映射文件的长度。一种简单的方法是将这两个参数全部设置为 0 ,那么内存映射文件的大小将与磁盘文件大小一致。
2 . HANDLE OpenFileMapping( DWORD dwDesiredAccess, // 指定保护类型
BOOL bIsInheritHandle, // 返回的句柄是否可以被继承
LPCSTR lpName // 创建对象时使用的名字
)
如果创建的是共享内存,其他进程不能再使用 CreateFileMapping 函数去创建同名的内存映射文件对象,而要使用 OpenFileMapping 函数打开已创建好的对象。
dwDesiredAcess 指定保护类型有 FILE_MAP_WRITE 或 FILE_MAP_READ
3 . LPVOID MapViewOfFile(
HANDLE hFileMappingObject, // 前两个函数返回的内存映射文件的句柄
DWORD dwDesiredAcess, // 保护类型 FILE_MAP_READ ,FILE_MAP_WRIT
DWORD dwFileOffsetHight, // 从文件的那个地址开始映射
DWORD dwFileOffsetLow,
SIZE_T dwNumberOfBytesToMap // 要映射的字节数,为 0 则映射整个文件
)
4 . BOOL UnmapViewOfFile( LPCVOID lpBaseAddress )
当不再使用内存映射文件时,可以通过 UmmapViewOfFile 函数撤销映射并使用 CloseHandle 函数关闭内存映射文件的句柄。
5 . BOOL FlushViewOfFile(
LPCVOID lpBaseAddress, // 开始的地址
SIZE_T dwNumberOfBytesToFlush // 数据块的大小
)
如果修改了映射视图中的内存,系统会在试图撤销映射或文件映射对象被删除时自动将数据写到磁盘上,但程序也可以根据需要将视图中的数据立即写到磁盘上。
使用步骤
1. 使用 CreateFileMapping 创建一个内存映射文件内核对象,告诉操作系统内存映射文件需要的物理内存大小,这个步骤决定了内存映射文件的用途――究竟是为磁盘上的文件建立内存映射还是为多个进程共享数据建立共享内存。或者使用 OpenFileMapping 打开映射文件内核对象。
2. 映射文件映射对象的全部或一部分到进程的地址空间,可以认为该操作是为文件中的内容分配线型地址空间,并将线型地址和文件内容对应起来,完成该操作的函数是 MapViewOfFile 。
使用内存映射文件读文件的具体过程可以这样:
(1) 调用 CreateFile 函数打开想要映射的文件,得到文件句柄 hFile 。
(2) 调用 CreateFileMapping 函数,并传入文件句柄 hFile ,为该文件创建一个内存映射内核对象,得到内存映射文件的句柄 hMap 。
(3) 调用 MapViewOfFile 函数映射整个文件或一部分到进程的虚拟地址空间。该函数返回文件映射到内存后的起始地址。使用指向这个地址的指针就可以读取文件的内容了。
(4) 调用 UnmapViewOfFile 函数来解除文件映射。
(5) 调用 CloseHandle 函数关闭文件对象,必须传入内存映射文件句柄 hMap(6) 调用 CloseHandle 函数关闭文件对象,必须传入文件句柄 hFile 。
进程间共享内存:
共享内存主要是通过映射机制实现的。 Windows 下进程的地址空间是相互隔离的,但在物理上却是重叠的。所谓的重叠是指同一块内存区域可能被多个进程同时使用。当调用 CreateFileMapping 创建命名的内存映射文件对象时, Windows 即在物理内存中申请了一块指定大小的内存区域,返回文件映射对象的新句柄 hMap 。为了能够访问这块区域必须调 MapViewOfiFile 函数,促使 Windows 将此内存空间映射到进程的地址空间中。当在其他进程中访问这块区域时,则必须使用 OpenFileMapping 函数来取得对象句柄 hMap ,并调用 MapViewOfFile 函数得到此内存空间的一个映射。这样一来,系统就把同一块内存区域映射到了不同进程的地址空间中,从而达到共享内存的目的。
使用内存映射文件的方法:
1.利用内存映射文件进行文件I/O操作:
CreateFile()-->CreateFileMapping()-->MapViewOfFile()......
2.利用内存映射文件实现Win32进程间通信:
我只介绍两种常用的方法:
第一种方法:两个进程使用同一个文件映射核心对象,打开各自的视图,或者父进程把自己创建的文件映射核心对象继承给子进程使用.这种方法比较安全有效.
第二种方法:基于页面交换文件的内存映射对象.在调用CreateFileMapping()函数时,传递的文件句柄为0xFFFFFFFF,系统就从页面交换文件中提交物理内存,然后进程之间按照第一种方法进程通信.这种方法不用事先准备一个特殊的文件(也就是说不用事先调用CreateFile()返回一个文件的句柄),非常方便.
与虚拟内存一样,内存映射文件可以用来保留一个地址空间的区域,一旦该文件被映射,就可以访问它,就像整个文件已经加载内存一样。
代码:
// SharedMem.h: interface for the CSharedMem class.
//
//
#if !defined(AFX_SHAREDMEM_H__CAC00683_0770_4CB6_A356_176AF5FA8A00__INCLUDED_)
#define AFX_SHAREDMEM_H__CAC00683_0770_4CB6_A356_176AF5FA8A00__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
class CSharedMem
{
public:
char* Receive();
int Open( const int size );
void Close();
int Send( const char* data );
int Create( const int size );
CSharedMem(const char* name);
virtual ~CSharedMem();
private:
HANDLE mo_handle;
char* mc_pData;
char mc_pName[1024];//create shared memory name
int mi_size;//create size
};
#endif // !defined(AFX_SHAREDMEM_H__CAC00683_0770_4CB6_A356_176AF5FA8A00__INCLUDED_)
// SharedMem.cpp: implementation of the CSharedMem class.
//
//
#include "stdafx.h"
#include "SharedMem.h"
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif
//
// Construction/Destruction
//
CSharedMem::CSharedMem(const char *name)
{
mc_pData = NULL;
mo_handle = NULL;
memset( mc_pName, 0x00, sizeof( mc_pName) );
strcpy( mc_pName, name );
mi_size = 0;
}
CSharedMem::~CSharedMem()
{
}
int CSharedMem::Create( const int size )
{
if (mo_handle != NULL) return -1;
mi_size = size;
if ( NULL != ( mo_handle = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE | SEC_COMMIT, 0, size , mc_pName ) ) )
{
if ( NULL == ( mc_pData = (char*) MapViewOfFile(mo_handle, FILE_MAP_WRITE, 0, 0, 0) ) )
{
CloseHandle(mo_handle);
mo_handle = NULL;
return -1;
}
return 0;
}
return -1;
}
int CSharedMem::Send(const char *data)
{
if (mc_pData == NULL || mo_handle == NULL ) return -1;
if (strlen( data ) > mi_size) return -1;
memcpy(mc_pData, data, mi_size);
return 0;
}
void CSharedMem::Close()
{
if ( mc_pData != NULL )
{
UnmapViewOfFile( mc_pData );
mc_pData= NULL;
}
if ( mo_handle != NULL )
{
CloseHandle( mo_handle );
mo_handle= NULL;
}
}
int CSharedMem::Open( const int size )
{
if (mo_handle != NULL) return -1;
mi_size = size;
if ( NULL != ( mo_handle = OpenFileMapping( FILE_MAP_READ, false, mc_pName ) ) )
{
if ( NULL == ( mc_pData = (char*) MapViewOfFile(mo_handle, FILE_MAP_READ, 0, 0, mi_size) ) )
{
CloseHandle(mo_handle);
mo_handle = NULL;
return -1;
}
return 0;
}
return -1;
}
char* CSharedMem::Receive()
{
return mc_pData;
}