2.1 文件映射
文件映射(Memory-Mapped Files)能使进程把文件内容当作进程地址区间一块内存那样来对待。因此,进程不必使用文件I/O操作,只需简单的指针操作就可读取和修改文件的内容。
Win32 API允许多个进程访问同一文件映射对象,各个进程在它自己的地址空间里接收内存的指针。通过使用这些指针,不同进程就可以读或修改文件的内容,实现了对文件中数据的共享。
应用程序有三种方法来使多个进程共享一个文件映射对象。
(1)继承:第一个进程建立文件映射对象,它的子进程继承该对象的句柄。
(2)命名文件映射:第一个进程在建立文件映射对象时可以给该对象指定一个名字(可与文件名不同)。第二个进程可通过这个名字打开此文件映射对象。另外,第一个进程也可以通过一些其它IPC机制(有名管道、邮件槽等)把名字传给第二个进程。
(3)句柄复制:第一个进程建立文件映射对象,然后通过其它IPC机制(有名管道、邮件槽等)把对象句柄传递给第二个进程。第二个进程复制该句柄就取得对该文件映射对象的访问权限。
文件映射是在多个进程间共享数据的非常有效方法,有较好的安全性。但文件映射只能用于本地机器的进程之间,不能用于网络中,而开发者还必须控制进程间的同步。
2.2 共享内存
Win32 API中共享内存(Shared Memory)实际就是文件映射的一种特殊情况。进程在创建文件映射对象时用0xFFFFFFFF来代替文件句柄(HANDLE),就表示了对应的文件映射对象是从操作系统页面文件访问内存,其它进程打开该文件映射对象就可以访问该内存块。由于共享内存是用文件映射实现的,所以它也有较好的安全性,也只能运行于同一计算机上的进程之间。
2.3 匿名管道
管道(Pipe)是一种具有两个端点的通信通道:有一端句柄的进程可以和有另一端句柄的进程通信。管道可以是单向-一端是只读的,另一端点是只写的;也可以是双向的一管道的两端点既可读也可写。
匿名管道(Anonymous Pipe)是 在父进程和子进程之间,或同一父进程的两个子进程之间传输数据的无名字的单向管道。通常由父进程创建管道,然后由要通信的子进程继承通道的读端点句柄或写 端点句柄,然后实现通信。父进程还可以建立两个或更多个继承匿名管道读和写句柄的子进程。这些子进程可以使用管道直接通信,不需要通过父进程。
匿名管道是单机上实现子进程标准I/O重定向的有效方法,它不能在网上使用,也不能用于两个不相关的进程之间。
2.4 命名管道
命名管道(Named Pipe)是服务器进程和一个或多个客户进程之间通信的单向或双向管道。不同于匿名管道的是命名管道可以在不相关的进程之间和不同计算机之间使用,服务器建立命名管道时给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,根据给定的权限和服务器进程通信。
命名管道提供了相对简单的编程接口,使通过网络传输数据并不比同一计算机上两进程之间通信更困难,不过如果要同时和多个进程通信它就力不从心了。
2.5 邮件槽
邮件槽(Mailslots)提 供进程间单向通信能力,任何进程都能建立邮件槽成为邮件槽服务器。其它进程,称为邮件槽客户,可以通过邮件槽的名字给邮件槽服务器进程发送消息。进来的消 息一直放在邮件槽中,直到服务器进程读取它为止。一个进程既可以是邮件槽服务器也可以是邮件槽客户,因此可建立多个邮件槽实现进程间的双向通信。
通过邮件槽可以给本地计算机上的邮件槽、其它计算机上的邮件槽或指定网络区域中所有计算机上有同样名字的邮件槽发送消息。广播通信的消息长度不能超过400字节,非广播消息的长度则受邮件槽服务器指定的最大消息长度的限制。
邮件槽与命名管道相似,不过它传输数据是通过不可靠的数据报(如TCP/IP协议中的UDP包)完成的,一旦网络发生错误则无法保证消息正确地接收,而命名管道传输数据则是建立在可靠连接基础上的。不过邮件槽有简化的编程接口和给指定网络区域内的所有计算机广播消息的能力,所以邮件槽不失为应用程序发送和接收消息的另一种选择。
2.6 剪贴板
剪贴板(Clipped Board)实质是Win32 API中一组用来传输数据的函数和消息,为Windows应用程序之间进行数据共享提供了一个中介,Windows已建立的剪切(复制)-粘贴的机制为不同应用程序之间共享不同格式数据提供了一条捷径。当用户在应用程序中执行剪切或复制操作时,应用程序把选取的数据用一种或多种格式放在剪贴板上。然后任何其它应用程序都可以从剪贴板上拾取数据,从给定格式中选择适合自己的格式。
剪贴板是一个非常松散的交换媒介,可以支持任何数据格式,每一格式由一无符号整数标识,对标准(预定义)剪贴板格式,该值是Win32 API定义的常量;对非标准格式可以使用Register Clipboard Format函数注册为新的剪贴板格式。利用剪贴板进行交换的数据只需在数据格式上一致或都可以转化为某种格式就行。但剪贴板只能在基于Windows的程序中使用,不能在网络上使用。
2.7 动态数据交换
动态数据交换(DDE)是使用共享内存在应用程序之间进行数据交换的一种进程间通信形式。应用程序可以使用DDE进行一次性数据传输,也可以当出现新数据时,通过发送更新值在应用程序间动态交换数据。
DDE和剪贴板一样既支持标准数据格式(如文本、位图等),又可以支持自己定义的数据格式。但它们的数据传输机制却不同,一个明显区别是剪贴板操作几乎总是用作对用户指定操作的一次性应答-如从菜单中选择Paste命令。尽管DDE也可以由用户启动,但它继续发挥作用一般不必用户进一步干预。DDE有三种数据交换方式:
(1) 冷链:数据交换是一次性数据传输,与剪贴板相同。
(2) 温链:当数据交换时服务器通知客户,然后客户必须请求新的数据。
(3) 热链:当数据交换时服务器自动给客户发送数据。
DDE交换可以发生在单机或网络中不同计算机的应用程序之间。开发者还可以定义定制的DDE数据格式进行应用程序之间特别目的IPC,它们有更紧密耦合的通信要求。大多数基于Windows的应用程序都支持DDE。
2.8 对象连接与嵌入
应用程序利用对象连接与嵌入(OLE)技术管理复合文档(由多种数据格式组成的文档),OLE提供使某应用程序更容易调用其它应用程序进行数据编辑的服务。例如,OLE支持的字处理器可以嵌套电子表格,当用户要编辑电子表格时OLE库可自动启动电子表格编辑器。当用户退出电子表格编辑器时,该表格已在原始字处理器文档中得到更新。在这里电子表格编辑器变成了字处理器的扩展,而如果使用DDE,用户要显式地启动电子表格编辑器。
同DDE技术相同,大多数基于Windows的应用程序都支持OLE技术。
2.9 动态连接库
Win32动态连接库(DLL)中的全局数据可以被调用DLL的所有进程共享,这就又给进程间通信开辟了一条新的途径,当然访问时要注意同步问题。
虽然可以通过DLL进行进程间数据共享,但从数据安全的角度考虑,我们并不提倡这种方法,使用带有访问权限控制的共享内存的方法更好一些。
2.10 远程过程调用
Win32 API提供的远程过程调用(RPC)使应用程序可以使用远程调用函数,这使在网络上用RPC进行进程通信就像函数调用那样简单。RPC既可以在单机不同进程间使用也可以在网络中使用。
由于Win32 API提供的RPC服从OSF-DCE(Open Software Foundation Distributed Computing Environment)标准。所以通过Win32 API编写的RPC应用程序能与其它操作系统上支持DEC的RPC应用程序通信。使用RPC开发者可以建立高性能、紧密耦合的分布式应用程序。
2.11 NetBios函数
Win32 API提供NetBios函数用于处理低级网络控制,这主要是为IBM NetBios系统编写与Windows的接口。除非那些有特殊低级网络功能要求的应用程序,其它应用程序最好不要使用NetBios函数来进行进程间通信。
2.12 Sockets
Windows Sockets规范是以U.C.Berkeley大学BSD UNIX中流行的Socket接口为范例定义的一套Windows下的网络编程接口。除了Berkeley Socket原有的库函数以外,还扩展了一组针对Windows的函数,使程序员可以充分利用Windows的消息机制进行编程。
现在通过Sockets实现进程通信的网络应用越来越多,这主要的原因是Sockets的跨平台性要比其它IPC机制好得多,另外WinSock 2.0不仅支持TCP/IP协议,而且还支持其它协议(如IPX)。Sockets的唯一缺点是它支持的是底层通信操作,这使得在单机的进程间进行简单数据传递不太方便,这时使用下面将介绍的WM_COPYDATA消息将更合适些。
2.13 WM_COPYDATA消息
WM_COPYDATA是一种非常强大却鲜为人知的消息。当一个应用向另一个应用传送数据时,发送方只需使用调用SendMessage函数,参数是目的窗口的句柄、传递数据的起始地址、WM_COPYDATA消息。接收方只需像处理其它消息那样处理WM_COPY DATA消息,这样收发双方就实现了数据共享。
WM_COPYDATA是一种非常简单的方法,它在底层实际上是通过文件映射来实现的。它的缺点是灵活性不高,并且它只能用于Windows平台的单机环境下。
Windows系统编程之进程间通信
作者:北极星2003
来源:看雪论坛(www.pediy.com)
附件:windowipc.rar
Windows 的IPC(进程间通信)机制主要是异步管道和命名管道。(至于其他的IPC方式,例如内存映射、邮槽等这里就不介绍了)
管道(pipe)是用于进程间通信的共享内存区域。创建管道的进程称为管道服务器,而连接到这个管道的进程称为管道客户端。一个进程向管道写入信息,而另外一个进程从管道读取信息。
异步管道是基于字符和半双工的(即单向),一般用于程序输入输出的重定向;命名管道则强大地多,它们是面向消息和全双工的,同时还允许网络通信,用于创建客户端/服务器系统。
一、异步管道(实现比较简单,直接通过实例来讲解)
实验目标:当前有sample.cpp, sample.exe, sample.in这三个文件,sample.exe为sample.cpp的执行程序,sample.cpp只是一个简单的程序示例(简单求和),如下:
代码:
#include <iostream.h> int main() { int a, b ; while ( cin >> a >> b && ( a || b ) ) cout << a + b << endl ; return 0; }
Sample.in文件是输入文件,内容:
32 433
542 657
0 0
要求根据sample.exe和它的输入数据,把输出数据重定向到sample.out
流程分析:实际这个实验中包含两个部分,把输入数据重定向到sample.exe 和把输出数据重定向到sample.out。在命令行下可以很简单的实现这个功能“sample <sample.in >sample.out”,这个命令也是利用管道特性实现的,现在我们就根据异步管道的实现原理自己来实现这个功能。
管道是基于半双工(单向)的,这里有两个重定向的过程,显然需要创建两个管道,下面给出流程图:
异步管道实现的流程图说明:
1)。父进程是我们需要实现的,其中需要创建管道A,管道B,和子进程,整个实现流程分为4个操作。
2)。管道A:输入管道
3)。管道B:输出管道
4)。操作A:把输入文件sample.in的数据写入输入管道(管道A)
5)。操作B:子进程从输入管道中读取数据,作为该进程的加工原料。通常,程序的输入数据由标准的输入设备输入,这里实现输入重定向,即把输入管道作为输入设备。
6)。操作C:子进程把加工后的成品(输出数据)输出到输出管道。通常,程序的输出数据会输出到标准的输出设备,一般为屏幕,这里实现输出重定向,即把输出管道作为输出设备。
7)。操作D:把输出管道的数据写入输出文件
需要注意的是,管道的本质只是一个共享的内存区域。这个实验中,管道区域处于父进程的地址空间中,父进程的作用是提供环境和资源,并协调子进程进行加工。
程序源码:
代码:
#include <windows.h> #include <iostream.h> const int BUFSIZE = 4096 ; HANDLE hChildStdinRd, hChildStdinWr, hChildStdinWrDup, hChildStdoutRd,hChildStdoutWr,hChildStdoutRdDup, hSaveStdin, hSaveStdout; BOOL CreateChildProcess(LPTSTR); VOID WriteToPipe(LPTSTR); VOID ReadFromPipe(LPTSTR); VOID ErrorExit(LPTSTR); VOID ErrMsg(LPTSTR, BOOL); void main( int argc, char *argv[] ) { // 处理输入参数 if ( argc != 4 ) return ; // 分别用来保存命令行,输入文件名(CPP/C),输出文件名(保存编译信息) LPTSTR lpProgram = new char[ strlen(argv[1]) ] ; strcpy ( lpProgram, argv[1] ) ; LPTSTR lpInputFile = new char[ strlen(argv[2]) ]; strcpy ( lpInputFile, argv[2] ) ; LPTSTR lpOutputFile = new char[ strlen(argv[3]) ] ; strcpy ( lpOutputFile, argv[3] ) ; SECURITY_ATTRIBUTES saAttr; saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); saAttr.bInheritHandle = TRUE; saAttr.lpSecurityDescriptor = NULL; /************************************************ * redirecting child process's STDOUT * ************************************************/ hSaveStdout = GetStdHandle(STD_OUTPUT_HANDLE); if (! CreatePipe(&hChildStdoutRd, &hChildStdoutWr, &saAttr, 0)) ErrorExit("Stdout pipe creation failed\n"); if (! SetStdHandle(STD_OUTPUT_HANDLE, hChildStdoutWr)) ErrorExit("Redirecting STDOUT failed"); BOOL fSuccess = DuplicateHandle( GetCurrentProcess(), hChildStdoutRd, GetCurrentProcess(), &hChildStdoutRdDup , 0, FALSE, DUPLICATE_SAME_ACCESS); if( !fSuccess ) ErrorExit("DuplicateHandle failed"); CloseHandle(hChildStdoutRd); /************************************************ * redirecting child process's STDIN * ************************************************/ hSaveStdin = GetStdHandle(STD_INPUT_HANDLE); if (! CreatePipe(&hChildStdinRd, &hChildStdinWr, &saAttr, 0)) ErrorExit("Stdin pipe creation failed\n"); if (! SetStdHandle(STD_INPUT_HANDLE, hChildStdinRd)) ErrorExit("Redirecting Stdin failed"); fSuccess = DuplicateHandle( GetCurrentProcess(), hChildStdinWr, GetCurrentProcess(), &hChildStdinWrDup, 0, FALSE, DUPLICATE_SAME_ACCESS); if (! fSuccess) ErrorExit("DuplicateHandle failed"); CloseHandle(hChildStdinWr); /************************************************ * 创建子进程(即启动SAMPLE.EXE) * ************************************************/ fSuccess = CreateChildProcess( lpProgram ); if ( !fSuccess ) ErrorExit("Create process failed"); // 父进程输入输出流的还原设置 if (! SetStdHandle(STD_INPUT_HANDLE, hSaveStdin)) ErrorExit("Re-redirecting Stdin failed\n"); if (! SetStdHandle(STD_OUTPUT_HANDLE, hSaveStdout)) ErrorExit("Re-redirecting Stdout failed\n"); WriteToPipe( lpInputFile ) ; ReadFromPipe( lpOutputFile ); delete lpProgram ; delete lpInputFile ; delete lpOutputFile ; } BOOL CreateChildProcess( LPTSTR lpProgram ) { PROCESS_INFORMATION piProcInfo; STARTUPINFO siStartInfo; BOOL bFuncRetn = FALSE; ZeroMemory( &piProcInfo, sizeof(PROCESS_INFORMATION) ); ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) ); siStartInfo.cb = sizeof(STARTUPINFO); bFuncRetn = CreateProcess ( NULL, lpProgram, NULL, NULL, TRUE, \ 0, NULL, NULL, &siStartInfo, &piProcInfo); if (bFuncRetn == 0) { ErrorExit("CreateProcess failed\n"); return 0; } else { CloseHandle(piProcInfo.hProcess); CloseHandle(piProcInfo.hThread); return bFuncRetn; } } VOID WriteToPipe( LPTSTR lpInputFile ) { HANDLE hInputFile = CreateFile(lpInputFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_READONLY, NULL); if (hInputFile == INVALID_HANDLE_VALUE) return ; BOOL fSuccess ; DWORD dwRead, dwWritten; CHAR chBuf[BUFSIZE] = {0} ; for (;;) { fSuccess = ReadFile( hInputFile, chBuf, BUFSIZE, &dwRead, NULL) ; if ( !fSuccess || dwRead == 0) break; fSuccess = WriteFile( hChildStdinWrDup, chBuf, dwRead, &dwWritten, NULL) ; if ( !fSuccess ) break; } if (! CloseHandle(hChildStdinWrDup)) ErrorExit("Close pipe failed\n"); CloseHandle ( hInputFile ) ; } VOID ReadFromPipe( LPTSTR lpOutputFile ) { HANDLE hOutputFile = CreateFile( lpOutputFile, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hOutputFile == INVALID_HANDLE_VALUE) return ; BOOL fSuccess ; DWORD dwRead, dwWritten; CHAR chBuf[BUFSIZE] = { 0 }; if (!CloseHandle(hChildStdoutWr)) ErrorExit("Closing handle failed"); for (;;) { fSuccess = ReadFile( hChildStdoutRdDup, chBuf, BUFSIZE, &dwRead, NULL) ; if( !fSuccess || dwRead == 0) { break; } fSuccess = WriteFile( hOutputFile, chBuf, dwRead, &dwWritten, NULL) ; if ( !fSuccess ) break; } CloseHandle ( hOutputFile ) ; } VOID ErrorExit (LPTSTR lpszMessage) { MessageBox( 0, lpszMessage, 0, 0 ); }
二、命名管道
命名管道具有以下几个特征:
(1)命名管道是双向的,所以两个进程可以通过同一管道进行交互。
(2)命名管道不但可以面向字节流,还可以面向消息,所以读取进程可以读取写进程发送的不同长度的消息。
(3)多个独立的管道实例可以用一个名称来命名。例如几个客户端可以使用名称相同的管道与同一个服务器进行并发通信。
(4)命名管道可以用于网络间两个进程的通信,而其实现的过程与本地进程通信完全一致。
实验目标:在客户端输入数据a和b,然后发送到服务器并计算a+b,然后把计算结果发送到客户端。可以多个客户端与同一个服务器并行通信。
界面设计:
难点所在:
实现的过程比较简单,但有一个难点。原本当服务端使用ConnectNamedPipe函数后,如果有客户端连接,就可以直接进行交互。原来我在实现过程中,当管道空闲时,管道的线程函数会无限(INFINITE)阻塞。若现在需要停止服务,就必须结束所有的线程,TernimateThread可以作为一个结束线程的方法,但我基本不用这个函数。一旦使用这个函数之后,目标线程就会立即结束,但如果此时的目标线程正在操作互斥资源、内核调用、或者是操作共享DLL的全局变量,可能会出现互斥资源无法释放、内核异常等现象。这里我用重叠I/0来解决这个问题,在创建PIPE时使用FILE_FLAG_OVERLAPPED标志,这样使用ConnectNamedPipe后会立即返回,但线程的阻塞由等待函数WaitForSingleObject来实现,等待OVERLAPPED结构的事件对象被设置。
客户端主要代码:
代码:
void CMyDlg::OnSubmit() { // 打开管道 HANDLE hPipe = CreateFile("\\\\.\\Pipe\\NamedPipe", GENERIC_READ | GENERIC_WRITE, \ 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL) ; if ( hPipe == INVALID_HANDLE_VALUE ) { this->MessageBox ( "打开管道失败,服务器尚未启动,或者客户端数量过多" ) ; return ; } DWORD nReadByte, nWriteByte ; char szBuf[1024] = {0} ; // 把两个整数(a,b)格式化为字符串 sprintf ( szBuf, "%d %d", this->nFirst, this->nSecond ) ; // 把数据写入管道 WriteFile ( hPipe, szBuf, strlen(szBuf), &nWriteByte, NULL ) ; memset ( szBuf, 0, sizeof(szBuf) ) ; // 读取服务器的反馈信息 ReadFile ( hPipe, szBuf, 1024, &nReadByte, NULL ) ; // 把返回信息格式化为整数 sscanf ( szBuf, "%d", &(this->nResValue) ) ; this->UpdateData ( false ) ; CloseHandle ( hPipe ) ; }
服务端主要代码:
代码:
// 启动服务 void CMyDlg::OnStart() { CString lpPipeName = "\\\\.\\Pipe\\NamedPipe" ; for ( UINT i = 0; i < nMaxConn; i++ ) { // 创建管道实例 PipeInst[i].hPipe = CreateNamedPipe ( lpPipeName, PIPE_ACCESS_DUPLEX|FILE_FLAG_OVERLAPPED, \ PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT, nMaxConn, 0, 0, 1000, NULL ) ; if ( PipeInst[i].hPipe == INVALID_HANDLE_VALUE ) { DWORD dwErrorCode = GetLastError () ; this->MessageBox ( "创建管道错误!" ) ; return ; } // 为每个管道实例创建一个事件对象,用于实现重叠IO PipeInst[i].hEvent = CreateEvent ( NULL, false, false, false ) ; // 为每个管道实例分配一个线程,用于响应客户端的请求 PipeInst[i].hTread = AfxBeginThread ( ServerThread, &PipeInst[i], THREAD_PRIORITY_NORMAL ) ; } this->SetWindowText ( "命名管道实例之服务器(运行)" ) ; this->MessageBox ( "服务启动成功" ) ; } // 停止服务 void CMyDlg::OnStop() { DWORD dwNewMode = PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_NOWAIT ; for ( UINT i = 0; i < nMaxConn; i++ ) { SetEvent ( PipeInst[i].hEvent ) ; CloseHandle ( PipeInst[i].hTread ) ; CloseHandle ( PipeInst[i].hPipe ) ; } this->SetWindowText ( "命名管道实例之服务器" ) ; this->MessageBox ( "停止启动成功" ) ; } // 线程服务函数 UINT ServerThread ( LPVOID lpParameter ) { DWORD nReadByte = 0, nWriteByte = 0, dwByte = 0 ; char szBuf[MAX_BUFFER_SIZE] = {0} ; PIPE_INSTRUCT CurPipeInst = *(PIPE_INSTRUCT*)lpParameter ; OVERLAPPED OverLapStruct = { 0, 0, 0, 0, CurPipeInst.hEvent } ; while ( true ) { memset ( szBuf, 0, sizeof(szBuf) ) ; // 命名管道的连接函数,等待客户端的连接(只针对NT) ConnectNamedPipe ( CurPipeInst.hPipe, &OverLapStruct ) ; // 实现重叠I/0,等待OVERLAPPED结构的事件对象 WaitForSingleObject ( CurPipeInst.hEvent, INFINITE ) ; // 检测I/0是否已经完成,如果未完成,意味着该事件对象是人工设置,即服务需要停止 if ( !GetOverlappedResult ( CurPipeInst.hPipe, &OverLapStruct, &dwByte, true ) ) break ; // 从管道中读取客户端的请求信息 if ( !ReadFile ( CurPipeInst.hPipe, szBuf, MAX_BUFFER_SIZE, &nReadByte, NULL ) ) { MessageBox ( 0, "读取管道错误!", 0, 0 ) ; break ; } int a, b ; sscanf ( szBuf, "%d %d", &a, &b ) ; pMyDlg->nFirst = a ; pMyDlg->nSecond = b ; pMyDlg->nResValue = a + b ; memset ( szBuf, 0, sizeof(szBuf) ) ; sprintf ( szBuf, "%d", pMyDlg->nResValue ) ; // 把反馈信息写入管道 WriteFile ( CurPipeInst.hPipe, szBuf, strlen(szBuf), &nWriteByte, NULL ) ; pMyDlg->SetDlgItemInt ( IDC_FIRST, a, true ) ; pMyDlg->SetDlgItemInt ( IDC_SECOND, b, true ) ; pMyDlg->SetDlgItemInt ( IDC_RESULT, pMyDlg->nResValue, true ) ; // 断开客户端的连接,以便等待下一客户的到来 DisconnectNamedPipe ( CurPipeInst.hPipe ) ; } return 0 ; }