在Windows环境下的IPC
1进程与内存保护
在Windows 16位环境下(Windows 3.1/3.2),所有Windows应用程序共享单一地址,任何进程都能够对这一空间中的内容(包括属于其他进程的内存)进行读写操作,甚至可存取操作系统本身的数据,这样就可能破坏其他程序的数据段代码。
出于安全的考虑,Windows 32环境下每个进程都有自己的地址空间,一个WIN32进程不能存取另一个进程的内存数据。两个进程可以用具有相同值的指针寻址,但将被映射到不同的物理地址。所读写的只是它们各自的数据,这样就减少了进程之间的相互干扰。比如:在进程A声明了一个指针P,这个指针只能被本进程A使用,在其他进程B中使用P,将会导致致命的问题,Windows禁止其操作。这样实现了对每个进程执行环境进行保护和隔离。这也是操作系统要实现的基本功能。
在WIN32环境下,每个WIN32进程拥有4GB的地址空间,但并不代表它真正拥有4GB的实际物理内存,而只是操作系统利用CPU的内存分配功能提供的虚拟地址空间。在一般情况下,绝大多数虚拟地址并没有物理内存于它对应,在真正可以使用这些地址空间之前,还要由操作系统提供实际的物理内存(这个过程叫“提交”commit)。在不同的情况下,系统提交的物理内存是不同的,可能是RAM,也可能是硬盘模拟的虚拟内存。
2进程间通信(IPC)
专业术语:IPC,Inter-Process Communication,进程间通信(通讯)
但在实际应用过程中,Windows各个进程之间常常需要交换数据,进行数据通讯。WIN32 API提供了许多函数使我们能够方便高效的进行进程间的通讯,通过这些函数我们可以控制不同进程间的数据交换,就如同在WIN16中对本地进程进行读写操作一样。
3在Windows 16位环境下的进程间通信
典型的WIN16两进程可以通过共享内存来进行数据交换:
(1) 进程A将GlobalAlloc(GMEM_SHARE...)API分配一定长度的内存;
(2) 进程A将GlobalAlloc函数返回的句柄传递给进程B(通过一个登录消息);
(3) 进程B对这个句柄调用GlobalLock函数,并返回的指针访问数据。
这种方法在WIN32中可能失败,这是因为GlobalLock函数返回指向的是进程A的内存,由于进程使用的是虚拟地址而非实际物理地址,因此这一指针仅与A进程有关,而于B进程无关。WIN32环境下,各进程间不能相互访问内存。每个进程的代码段都访问内存的虚拟地址,由操作系统完成到物理地址的映射。代码能处理的同一个虚拟内存地址将被Windows映射到不同的物理地址上。
4 在Windows 32位环境下的进程间通信
下面描述了WIN32下进程之间通信的几种实现方法,SSIPCall只是选择了其中一种作为实现方法。了解各类通信方法,有助于全面了解Windows 的各类IPC机制。
在WIN32环境下,为实现进程间平等的数据交换,用户可以有如下几种选择:
n 使用剪贴板
在16位时代常使用的方式,CWnd中提供支持。
因为剪贴板使用的场合很多,容易出现冲突的情况,且效率不高。
n 动态数据交换(DDE)
其方式在一块全局内存中手工放置大量的数据,然后使用窗口消息传递内存指针。这是16位WIN时代使用的方式,因为在WIN32下已经没有全局和局部内存了,现在的内存只有一种就是虚存。所以,这种方法在WIN32下无效。
n 消息管道(匿名管道Anonymous Pipes、命名管道Named Pipes)
用于设置应用程序间的一条永久通讯通道,通过该通道可以象自己的应用程序 访问一个平面文件一样读写数据。
² 匿名管道(Anonymous Pipes)
单向流动,并且只能够在同一电脑上的各个进程之间流动。
² 命名管道(Named Pipes)
双向,跨网络,任何进程都可以轻易的抓住,放进管道的数据有固定的格式,而使用ReadFile()只能读取该大小的倍数。
该方式缺陷:服务端只能运行在基于Windows NT 内核的Windows系统中。
n 邮件槽(Mailslots)
广播式通信,在WIN32系统中提供的新方法,可以在不同主机间交换数据,实现了跨网络,单在WIN9X下只支持邮件槽客户。服务端必须运行在Windows NT/2000/XP。
n Windows套接字(Windows Socket)、TCP/IP方式
它具备消息管道所有的功能,但遵守一套通信标准使的不同操作系统之上的应用程序之间可以互相通信。这种方式用于网络方面比较好,但用于本地进程间的通信,感觉有点浪费,效率方面没有SSIPCall的实现高。
n COM/DCOM
通过COM系统的代理存根方式进行进程间数据交换,但只能够表现在对接口函数的调用时传送数据,通过DCOM可以在不同主机间传送数据。
n RPC
远程过程调用,调用方法比较复杂。很少使用,因其与UNIX的RPC不兼容。
n 串行/并行通信(Serial/Parallel Communication)
它允许应用程序通过串行或并行端口与其他的应用程序通信。
n 使用内存映射文件
1) 设定一块共享内存区域
产生一个file-mapping核心对象:
HANDLE CreateFileMapping(HANDLE,LPSECURITY_ATTRIBUTES, DWORD, DWORD, DWORD,LPCSTR)
得到共享内存的指针:
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, DWORD dwDesiredAcess,
DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap );
2) 找出共享内存
每个客户端进程使用先获得句柄:
HANDLE OpenFileMapping(DWORD dwDesiredAccess,
BOOL bInheritHandle, LPCTSTR lpName);
再调用MapViewOfFile(),取得共享内存的指针
3) 同步处理(Mutex)
4) 清理(Cleaning up)
BOOL UnmapViewOfFile(LPCVOID lpBaseAddress);
CloseHandle();
内存映射文件机制的本质,是Windows操作在系统核心内存区域开了块内存,然后每个进程把这块内存映射到自己可以访问的虚内存地址中。对每个进程来说,似乎在操作各自的内存区域,而实际上所有的操作被映射到Windows核心的共享的内存区域。
Win核心内存区域 |
进程1
内存地址 p1 |
进程2
内存地址 p2 |
映象机制 |
图3:内存映象机制模型图
n 通过共享内存DLL共享内存
共享数据DLL允许进程以类似于Windows 3.1 DLL共享数据的方式访问读写数据,多个进程都可以对该共享数据DLL进行数据操作,达到共享数据的目的。在WIN32中为建立共享内存,必须执行以下步骤:
1)首先创建一个有名的数据区。这在Visual C++中是使用data_seg pragma宏。使用data_seg pragma宏必须注意数据的初始化:
#pragma data_seg("MYSEC")
char MySharedData[4096]={0};
#pragma data_seg()
2)然后在用户的DEF文件中为有名的数据区设定共享属性。
LIBRARY TEST
DATA READ WRITE
SECTIONS
.MYSEC READ WRITE SHARED
完成这两步,每个附属于DLL的进程都将接受到属于自己的数据拷贝,一个进程的数据变化并不会反映到其他进程的数据中。所以,还要做下面的一步。
3)在DEF文件中适当地输出数据。
以下的DEF文件项说明了如何以常数变量的形式输出MySharedData。
EXPORTS
MySharedData CONSTANT
4)最后在应用程序(进程)按外部变量引用共享数据。
extern _export "C"{char * MySharedData[]};
5)进程中使用该变量应注意间接引用。
m_pStatic=(CEdit*)GetDlgItem(IDC_SHARED);
m_pStatic->GetLine(0,*MySharedData,80);
n 向另一进程发送WM_COPYDATA消息
传输只读数据可用Win32中的WM_COPYDATA消息。该消息允许在进程间传递只读数据,但不能处理返回,可用共享内存弥补回传数据。SDK文档推荐用户使用SendMessage函数,接受方在数据拷贝完成前不返回,这样发送方就不可能删除和修改数据。
SendMessage(hwnd,WM_COPYDATA,wParam,lParam);
其中wParam设置为包含数据的窗口(也既数据源窗口)的句柄。lParam指向一个COPYDATASTRUCT的结构:
typedef struct tagCOPYDATASTRUCT{
DWORD dwData;//用户定义数据
DWORD cbData;//数据大小
PVOID lpData;//指向数据的指针
} COPYDATASTRUCT;
该结构用来定义用户数据。
在接受端的窗口过程函数里面,做处理即可。下面是样板代码:
LRESULT CALLBACK MyWndProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM lParam)
{
switch(message)
{
case WM_COPYDATA:
{
HWND hWndCall = (HWND) wParam;
PCOPYDATASTRUCT pCopy = (PCOPYDATASTRUCT)lParam;
if (pCopy && ( pCopy->dwData == SSIPC_COPYDATA_ID ) )
{
LPBYTE pBuff = (LPBYTE) (pCopy->lpData) ;
DWORD len = pCopy->cbData;
...... // 具体的业务代码在此
}
break;
}
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
n 直接调用ReadProcessMemory和WriteProcessMemory函数实现进程间通讯
通过调用ReadProcessMemory以及WriteProcessMemory函数用户可以按类似与Windows 3.1的方法实现进程间通讯,在发送进程中分配一块内存存放数据,可以调用GlobalAlloc或者VirtualAlloc函数实现:
m_hGlobalHandle = GlobalAlloc(GMEM_SHARE,1024);
可以得到指针地址:
mpszGlobalHandlePtr=(LPSTR)GlobalLock(m_hGlobalHandle);
在接收进程中要用到用户希望影响的进程的打开句柄。为了读写另一进程,应按如下方式调用OpenProcess函数:
HANDLE hTargetProcess=OpenProcess(
STANDARD_RIGHTS_REQUIRED|
PROCESS_VM_REDA| PROCESS_VM_WRITE|
PROCESS_VM_OPERATION,//访问权限
FALSE,//继承关系
dwProcessID);//进程ID
为保证OpenProcess函数调用成功,用户所影响的进程必须由上述标志创建。
用户获得进程的有效句柄,就可用ReadProcessMemory函数读取该进程的内存:
BOOL ReadProcessMemory(
HANDLE hProcess, // 进程指针
LPCVOID lpBaseAddress, // 数据块的首地址
LPVOID lpBuffer, // 读取数据所需缓冲区
DWORD cbRead, // 要读取的字节数
LPDWORD lpNumberOfBytesRead );
使用同样的句柄也可以写入该进程的内存:
BOOL WriteProcessMemory(
HANDLE hProcess, // 进程指针
LPVOID lpBaseAddress, // 要写入的首地址
LPVOID lpBuffer, // 缓冲区地址
DWORD cbWrite, // 要写的字节数
LPDWORD lpNumberOfBytesWritten );
如下所示是读写另一进程的共享内存中的数据:
ReadProcessMemory((HANDLE)hTargetProcess,
(LPSTR)lpsz,m_strGlobal.GetBuffer(_MAX_FIELD),
_MAX_FIELD,&cb);
WriteProcessMemory((HANDLE)hTargetProcess,
(LPSTR)lpsz,(LPSTR)STARS,
m_strGlobal.GetLength(),&cb);