进程间通信
之内存映射文件
摘要:我们实际的开发中经常可能遇到我们的后台系统需要通过回前台UI进程通知用户一些消息,这样两个进程之间就不可避免的产生的通信的需求。幸运的是Windows给我们提供了很多进程间能通信的方法,如:剪贴板、窗口消息、共享内存、管道、套接字等,我们这里讨论一下共享内存方式进程间通信。
关键词:进程间通信,内存映射文件,VC++,WindowsAPI
进程通常被定义为一个正在运行的程序的实例,它由两个部分组成:一个是操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。 另一个是地址空间,它包含所有的可执行模块或DLL模块的代码和数据。它还包含动态分配的空间。他们工作在不同的地址空间中这也就限制他们之的通信,而我们实际的开发中经常可能遇到我们的后台系统需要通过回前台UI进程通知用户一些消息,这样两个进程之间就不可避免的产生的通信的需求。幸运的是Windows给我们提供了很多进程间能通信的方法,如:剪贴板、窗口消息、共享内存、管道、套接字等,我们这里只讨论共享内存方式进程间通信。
共享内存方式的进程间通信大体如下流程:
1.首先通过CreateFileMapping设定一块共享内存区,下面其函数原型
HANDLE CreateFileMapping(
HANDLE hFile, // handle to file to map
LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
// optional security attributes
DWORD flProtect, // protection for mapping object
DWORD dwMaximumSizeHigh, // high-order 32 bits of object size
DWORD dwMaximumSizeLow, // low-order 32 bits of object size
LPCTSTR lpName // name of file-mapping object
);
通过CreateFileMapping产生一个file-mapping核心对象,再通过
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAcess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap
);得到共享内存的指针
2.用共享内存指针指向的内存在进程间传递数据
3.清理(Cleaning up)
BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);
下面我们就来一起看一个通过共享内存实现进程间通信的例子。
根据我们要实现的功能这里我们先设计一个管理内存映射文件的类。把对内存映射文件的基本操作封闭一下,这样可以方便于我们实现进程间复杂的数据交换。现在我们来看一下这个类里面要实现的方法,这里列出类的声明:
class CJDMapFile
{
public:
BOOL IsOpen(); // 文件是否打开
DWORD GetSpaceLength(); // 得过还能写入多字节的数据
BOOL SetFlag(DWORD code); // 设置头标记
BOOL ClearFile(); // 清空文件内容
BOOL IsEmpty(); // 文件是否为空
VOID CloseFile(); // 关闭文件
// 从文件中读取指定字节的数据
BOOL ReadFile(LPVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten);
// 向文件写入指定字节的数据
BOOL WriteFile(LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten);
// 打来文件
BOOL OpenFile(char * szFileName);
// 创建并打开文件
BOOL CreateFile(char * szFileName);
CJDMapFile();
virtual ~CJDMapFile();
private:
HANDLE hMapFile;
LPVOID pMapFile;
DWORD* pFlags; // 文件标记
LPVOID pContent; // 文件内容开始地址
DWORD dwRPostion; // 读文件的位置
DWORD dwWPostion; // 写文件的位置
BOOL m_IsOpen; // 文件是否打开
};
这里我们为自己的内存映射文件设计了一个简单的文件格式:
4字节 |
…… |
标记 |
文件内容 |
文件的前四个字节很简单,就是文件内存放消息的个数,这样我们能知道要读取多少个消息。上面列出来类的功能方法接着我们再看几个关建方法的实现:
1.创建并打开文件(CreateFile)
/*
函数功能:
创建内存映射文件
修改说明:
参数列表:
char *szFileName 内存映射文件名
返回值列表:
TRUE 创建成功 FALSE 创建失败
*/
BOOL CJDMapFile::CreateFile(char *szFileName)
{
hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE,
NULL,
PAGE_READWRITE,
0,
FILESIZE,
szFileName);
if (!hMapFile)
{
return FALSE;
}
pMapFile = MapViewOfFile(hMapFile,
FILE_MAP_ALL_ACCESS,
0,
0,
0);
if( pMapFile == NULL ) {
return FALSE;
}
pFlags = (DWORD*)pMapFile;
*pFlags = 0;
pContent = (LPVOID)((long)pMapFile+(long)(sizeof(DWORD)));
m_IsOpen = TRUE;
return TRUE;
}
2. 向文件写入指定字节的数据
/*
函数功能:
写文件
修改说明:
参数列表:
LPCVOID lpBuffer 写入的数据
DWORD nNumberOfBytesToWrite 写入数据的长度
LPDWORD lpNumberOfBytesWritten 真实写入数据的长度
返回值列表:
TRUE 成功 FALSE 失败
*/
BOOL CJDMapFile::WriteFile(LPCVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten)
{
if (lpBuffer==NULL||nNumberOfBytesToWrite==0)
{
return FALSE;
}
if (this->pMapFile == NULL || dwWPostion > (FILESIZE-4))
{
return FALSE;
}
DWORD dwSize = 0;
dwSize = dwRPostion + nNumberOfBytesToWrite;
nNumberOfBytesToWrite = dwSize > (FILESIZE-4)? (FILESIZE-4)-dwWPostion:nNumberOfBytesToWrite;
memcpy((LPVOID)((LONG)pContent+dwWPostion),lpBuffer,nNumberOfBytesToWrite);
dwWPostion += nNumberOfBytesToWrite;
*lpNumberOfBytesWritten = nNumberOfBytesToWrite;
return TRUE;
}
3. 从文件中读取指定字节的数据
/*
函数功能:
写文件
修改说明:
参数列表:
LPCVOID lpBuffer 读取的数据
DWORD nNumberOfBytesToWrite 读取数据的长度
LPDWORD lpNumberOfBytesWritten 真实读取数据的长度
返回值列表:
TRUE 成功 FALSE 失败
*/
BOOL CJDMapFile::ReadFile(LPVOID lpBuffer,
DWORD nNumberOfBytesToWrite,
LPDWORD lpNumberOfBytesWritten)
{
if (lpBuffer==NULL||nNumberOfBytesToWrite==0)
{
return FALSE;
}
if (this->pMapFile == NULL || dwRPostion > (FILESIZE-4))
{
return FALSE;
}
DWORD dwSize = 0;
dwSize = dwRPostion + nNumberOfBytesToWrite;
nNumberOfBytesToWrite = dwSize > (FILESIZE-4)? (FILESIZE-4)-dwRPostion:nNumberOfBytesToWrite;
memcpy(lpBuffer,(LPVOID)((LONG)pContent+dwRPostion),nNumberOfBytesToWrite);
*lpNumberOfBytesWritten = nNumberOfBytesToWrite;
dwRPostion += *lpNumberOfBytesWritten;
return TRUE;
}
4. 关闭文件
/*
函数功能:
关闭内存映射文件
修改说明:
参数列表:
返回值列表:
*/
VOID CJDMapFile::CloseFile()
{
if (this->pMapFile == NULL)
{
return;
}
UnmapViewOfFile(this->pMapFile);
CloseHandle(hMapFile);
pFlags = NULL;
pMapFile = NULL;
pContent = NULL;
m_IsOpen = FALSE;
}
有了这个自定义类的帮助我们在下面消息的接收上将会很方便,现在我们再看看消息的接收控制类设计。
消息控制类用来控制我们消息的发送接收,设计它的目的是为了将发送消息时与具体的进程间通信的方式隔离开,能让我们平滑的过渡到其它方式上去。我们的消息的属性包括标题、内容、发送时间、序号、类型,由此需要先定义一个结构用于存放我们的消息,下面让我们来看一下类的具体声明与消息结构定义,具体声明如下:
// ESMSMsgHandl.h: interface for the CESMSMsgHandl class.
//
//////////////////////////////////////////////////////////////////////
#if !defined(AFX_ESMSMSGHANDL_H__BC0A1503_01C3_4BE7_AFB1_F7EF03D83C4C__INCLUDED_)
#define AFX_ESMSMSGHANDL_H__BC0A1503_01C3_4BE7_AFB1_F7EF03D83C4C__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#include "JDMapFile.h"
//消息的类型
enum{
TP_NOTIFY=4001,
TP_AFFIRM
};
#pragma pack(push, 1)
//消息头
typedef struct _tagMsgHeader
{
WORD wType;//消息的类型
LONG lTime;
LONG lSerial;
LONG lState;
char szTitle[33];
LONG lLength;
}MSG_HEADER,*LPMSG_HEADER;
//服务器消息结构体
typedef struct _tagSystemMsg
{
union{
MSG_HEADER header;
};
LPVOID pContent;
//消息体的内容
}SYSTEMMSG,*LPSYSTEMMSG;
#pragma pack(pop)
#ifdef PROCESS1
#define RECVMSG "PROCESS_MEMORY_FILE_F7EF03D83C4C"
#define SENDMSG "PROCESS_MEMORY_FILE_F7EF03D83C4D"
#define RECVMSGMUTEX "PROCESS_MEMORY_FILE_F7EF03D83C4C"
#define SENDMSGMUTEX "PROCESS_MEMORY_FILE_F7EF03D83C4D"
#else
#define RECVMSG "PROCESS_MEMORY_FILE_F7EF03D83C4D"
#define SENDMSG "PROCESS_MEMORY_FILE_F7EF03D83C4C"
#define RECVMSGMUTEX "PROCESS_MEMORY_FILE_F7EF03D83C4D"
#define SENDMSGMUTEX "PROCESS_MEMORY_FILE_F7EF03D83C4C"
#endif
class CMsgHandle
{
public:
CMsgHandle();
virtual ~CMsgHandle();
public:
BOOL UnLockWrite();
BOOL LockWrite(DWORD dwTimeOut);
BOOL UnLockRead();
BOOL LockRead(DWORD dwTimeOut);
VOID Close();
BOOL SetWriteNum(DWORD Code);
BOOL ClearMsgNum(DWORD Code);
DWORD IsWrite();
DWORD IsRead();
BOOL RecvMsg(LPSYSTEMMSG* pMsg);
BOOL SendMsg(LPSYSTEMMSG pMsg);
BOOL Init();
private:
CJDMapFile recvFile;
CJDMapFile sendFile;
HANDLE hReadMutex;
HANDLE hWriteMutex;
};
#endif // !defined(AFX_ESMSMSGHANDL_H__BC0A1503_01C3_4BE7_AFB1_F7EF03D83C4C__INCLUDED_)
同样我们这里也列出几个关键的类方法实现,让我们具体看一下这些代码:
1. 对象初始化
/*
函数功能:
消息控制对像始化,完成接收内存映像文件与发送内存映像文件的创建
修改说明:
参数列表:
无
返回值列表:
TRUE 成功 FALSE 失败
*/
BOOL CMsgHandle::Init()
{
if (!recvFile.CreateFile(RECVMSG))
{
return FALSE;
}
if (!sendFile.CreateFile(SENDMSG))
{
return FALSE;
}
return TRUE;
}
2. 接收客户端服务进程序的消息
/*
函数功能:
接收客户端服务进程序的消息
修改说明:
参数列表:
LPSYSTEMMSG:指向消息内容的指针
返回值列表:
TRUE:成功 FALSE:失败
*/
BOOL CMsgHandle::RecvMsg(LPSYSTEMMSG* pMsg)
{
MSG_HEADER header;
LPVOID pBuf;
DWORD dwSize = sizeof(header);
if (!recvFile.ReadFile(&header,dwSize,&dwSize))
{
return FALSE;
}
pBuf = malloc(sizeof(SYSTEMMSG)+header.lLength+1);
if (pBuf == NULL)
{
return FALSE;
}
ZeroMemory(pBuf,sizeof(SYSTEMMSG)+header.lLength+1);
*pMsg = (LPSYSTEMMSG)pBuf;
(*pMsg)->header = header;
(*pMsg)->pContent = NULL;
if (header.lLength>0)
{
(*pMsg)->pContent = (LPVOID)((LONG)pBuf+(LONG)sizeof(SYSTEMMSG));
dwSize = header.lLength;
if (!recvFile.ReadFile((*pMsg)->pContent,dwSize,&dwSize))
{
free(pBuf);
*pMsg = NULL;
return FALSE;
}
}
return TRUE;
}
3. 向客户端的服务进程序发送消息
/*
函数功能:
向客户端的服务进程序发送消息
修改说明:
参数列表:
LPSYSTEMMSG:指向消息内容的指针
返回值列表:
TRUE:成功 FALSE:失败
*/
BOOL CMsgHandle::SendMsg(LPSYSTEMMSG pMsg)
{
MSG_HEADER header;
DWORD dwSize = sizeof(header);
header = pMsg->header;
if (sizeof(MSG_HEADER)+header.lLength>sendFile.GetSpaceLength())
{
return FALSE;
}
if (!sendFile.WriteFile(&header,dwSize,&dwSize))
{
return FALSE;
}
if (pMsg->header.lLength >0)
{
dwSize = pMsg->header.lLength;
if (!sendFile.WriteFile(pMsg->pContent,dwSize,&dwSize))
{
return FALSE;
}
}
return TRUE;
}
上面是消息控制类的声明和部分实现它主要控制消息的接收与发送,为我们提供一个统一的接收发送消息的接口,同样我们也可在其内部实现其它方法的进程间通信。
这节来我们来具体看一下,怎么应用这个两个类。我们这里新建了一个简单的对话框工程,在对话框面版上添加发送与接收数据的按钮这样我们可以利用这个两按钮向另一进程发送或接受另一进程发送的数据,再添加两个与之对应的EDIT控件这就完成了我们测试程序的用户界面,接下来我们看发送数据按钮事件里的代码:
void CIPCDlg::OnSendData()
{
UpdateData(TRUE);
DWORD write=0;
LPSYSTEMMSG pMsg=NULL;
pMsg = AllocMsgMemery(m_SendContent.GetLength()+1);
strcpy((char*)pMsg->pContent,m_SendContent.GetBuffer(0));
// 向服务器发送消息
if (handl.LockWrite(INFINITE))
{
write = handl.IsWrite();
if (write==0)
{
handl.SetWriteNum(0);
}
if (handl.SendMsg(pMsg))
{
write++;
// 写入发送的消息数
handl.SetWriteNum(write);
}
else
{
AfxMessageBox("写入出错!");
}
}
handl.UnLockWrite();
free(pMsg);
}
再让我们看一下接受数据按钮事件处理函数代码:
void CIPCDlg::OnRecvData()
{
DWORD read;
LPSYSTEMMSG pMsg=NULL;
CString strMsg;
// 接收服务器消息
if (handl.LockRead(INFINITE))
{
read = handl.IsRead();
for (DWORD i=0;i<read;i++)
{
if (handl.RecvMsg(&pMsg))
{
strMsg.Format("%s",(char*)pMsg->pContent);
m_RecvContent = m_RecvContent+"/n/r"+strMsg;
free(pMsg);
}
else
{
break;
}
}
handl.ClearMsgNum(0);
}
handl.UnLockRead();
UpdateData(FALSE);
}
除了将我们刚才设计的两个类的文件加入工程,同时还一个地方需要添加一些代码,那就是在StdAfx.h中添加
#define PROCESS1
#include "include/ExMapFile.h"
#include "include/MsgHandle.h"
我们需要注意一下#define PROCESS1,这个预定义用来控制编译出两个程序。我们先把它注释掉编译出来一个RELEASE版本,再把它放开来编译出一个DEBUG版本,然后我们用这两个版本的程序来测试我们的这两个类。
上面给大家介绍了用内存映射文件来完成两个进程间通信的功能,希望能大家有所帮助,如有谬误望大家多多指教。