进程间通信的研究(一)
<!--参照教辅>
一.进程通信
程序员必须让拥有依赖关系的进程集协调,这样才能达到进程的共同目标。可以使用两种技术来达到协调。第一种技术在具有通信依赖关系的两个进程间传递信息。这种技术称做进程间通信(interprocess communication)。第二种技术是同步,当进程间相互具有合作依赖时使用。这两种类型的依赖关系可以同时存在。
一般而言,进程有单独的地址空间。我们可以了解下可执行程序被装载到内存后建立的一系列映射等理解这一点。如此以来意味着如果我们有两个进程(进程A和进程B),那么,在进程A中声明的数据对于进程B是不可用的。而且,进程B看不到进程A中发生的事件,反之亦然。如果进程A和B一起工作来完成某个任务,必须有一个在两个进程间通信信息和时间的方法。我们这里可以去看看基本的进程组件。注意进程有一个文本、数据以及堆栈片断。进程可能也有从自由存储空间中分配的其它内存。进程所占有的数据一般位于数据片断、堆栈片断或进程的动态分配内存中。数据对于其它进程来说是受保护的。为了让一个进程访问另外一个进程的数据,必须最终使用操作系统调用。与之类似,为了让一个进程知道另一个进程中文本片断中发生的事件,必须在进程间建立一种通信方式。这也需要来自操作系统API的帮助。当进程将数据发送到另一进程时,称做IPC(interprocess communication,进程间通信)。
二.进程间通信的方式,已知的有以下8种:
1.无名管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
2.高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。
3.有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
4.信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
5.消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
6.信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
7.共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
8.套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。
三.环境变量、文件描述符:
当创建一个子进程时,它接受了父进程许多资源的拷贝。子进程接受了父进程的文本、堆栈以及数据片断的拷贝。子进程也接受了父进程的环境数据以及所有文件描述符的拷贝。子进程从父进程继承资源的过程创造了进程间通信的一个机会。父进程可以在它的数据片断或环境中设置一定的变量,子进程于是接受这些值。同样,父进程也可以打开一个文件,推进到文件内的期望位置,子进程接着就可以在父进程离开读/写指针的准确位置访问该文件。这类通信的缺陷在于它是单向的、一次性的通信。也就是说,除了文件描述外,如果子进程继承了任何其它数据,也仅仅是父进程拷贝的所有数据。 一旦创建了子进程,由子进程对这些变量的任何改变都不会反映到父进程的数据中。同样,创建子进程后,对父进程数据的任何改变也不会反映到子进程中。所以,这种类型的进程间通信更像指挥棒传递。一旦父进程传递了某些资源的拷贝,子进程对它的使用就是独立的,必须使用原始传递资源。
四.命令行参数:
通过命令行参数(command-line argument)可以完成另一种单向、一次性的进程间通信我前面的文章已经提到过使用命令行参数。命令行参数在调用一个exec或派生调用操作系统时传递给子进程。命令行参数通常在其中一个参数中作为NULL终止字符串传递给exec或派生函数调用。这些函数可以按单向、一次性方式给子进程传递值。WINDOWS有调用执行exe程序的API。大家可以去参考一下ShellExecuteA函数相关。
五.管道通信:
继承资源以及命令行参数是最简单形式的进程间通信。它们同时有两个主要限制。除了文件描述符外,继承资源是IPC的单向、一次性形式。传递命令参数也是单向、一次性的IPC方法。这些方法也只有限制于关联进程,如果不关联,命令行参数和继承资源不能使用。还有另一种结构,称做管道(Pipe),它可以用于在关联进程间以及无关联进程间进行通信。管道是一种数据结构,像一个序列化文件一样访问。它形成了两个进程间的一种通信渠道。管道结构通过使用文本和写方式来访问。如果进程A希望通过管道发送数据给进程B,那么进程A向管道写入数据。为了让进程B接收此数据,进程B必须读取管道,与命令行参数的IPC形式不一样。管道可以双向通信。两进程间的数据流是双向通信的。管道可以在程序的整个执行期间使用,在进程间发送和接收数据。所以,管道充当可访问管道的进程间的一种可活链接,有两种基本管道类型:
1. 匿名管道
2. 命名管道
上面的图可以看出在没有管道时,两进程是不能互写的。
建立管道后就可以相互通信了。只有关联进程可以使用匿名管道来通信。无关联进程必须使用命名管道。
匿名管道:通过文件描述符或文件句柄提供对匿名管道的访问。对系统API的调用创建一个管道,并返回一个文件描述符。这个文件描述符是用作read()或write()函数的一个参数。当通过文件描述符调用read()或write()时,数据的源和目标就是管道。例如,在OS/2环境中使用操作系统函数DosCreatePipe()创建匿名管道:
int mian( void )
{
PFHILE readHandle;
PFHILE writeHandle;
DosCreatePipe( readHandle, writeHandle, size );
…
…
…
}
在WINDOWS下例如我写的ASM集成环境通过管道与DOS命令行通信的MFC下代码块:
void CIDEManager::Commond( CString cmd, char* buf, unsigned int bufsize )
{
SECURITY_ATTRIBUTES sa;
HANDLE hRead, hWrite;
sa.nLength = sizeof( SECURITY_ATTRIBUTES );
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
if ( !CreatePipe( &hRead, &hWrite, &sa, 0 ) ) // 创建管道
{
return;
}
STARTUPINFO si;
PROCESS_INFORMATION pi;
si.cb = sizeof( STARTUPINFO );
GetStartupInfo( &si );
si.hStdError = hWrite;
si.hStdOutput = hWrite;
si.wShowWindow = SW_HIDE;
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
if ( !CreateProcess( NULL, ( LPTSTR )( LPCTSTR )cmd, NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi ) )
{
return;
}
CloseHandle( hWrite );
DWORD bytesRead;
while ( TRUE )
{
memset( buf, 0, bufsize );
if ( ReadFile( hRead, buf, bufsize, &bytesRead, NULL ) != NULL )
{
break;
}
Sleep( 200 );
}
CloseHandle( hRead );
return;
}
命名管道:将管道用作两个无关联进程间的通信渠道,程序员必须使用命名管道,它可以看作一种具有某名字的特殊类型文件。进程可以根据它的名字访问这个管道。通过匿名管道,父和子进程可以单独使用文件描述符来访问他们所共享的管道,因为子进程继承了父进程的文件描述符,同时文件描述符用read()或write()函数的参数。因为无关进程不能访问彼此的文件描述符,所以不能使用匿名管道。由于命名管道提供该管道的一个等价文件名,任何知道此管道名字的进程都可以访问它。下面是命名管道相对于匿名管道的优点:
命名管道可以被无关联进程使用。
命名管道可以持久。创建它的程序退出后,它们仍然可以存在。
命名管道可以在网络或分布环境中使用。
命名管道容易用于多对一关系中。
与访问匿名管道一样,命名管道也是通过read()或write()函数来访问。两者之间的主要区别在于命名管道的创建方式以及谁可以反问它们。命名管道可以建立一个进程间通信的C/S模型。访问命名管道的进程可能都位于同一台机器上,或位于通过网络通信的不同机器上。由于管道的名字可以通过管道所在服务器的逻辑名,所以能够跨网络访问管道。例如,////ServerName//Pipe//MyPipe(不区分大小写)可以作为一个管道名字。假如Server1是网络服务器的名字。当打开或访问这个管道的调用解析文件名时,首先应该定位Server1,然后访问MyPipe。例子如下:
服务器端:
int main( void )
{
HANDLE pipehandle;
char buf[ 256 ];
DWORD bytesRead;
if( ( pipehandle = CreateNamedPipe( "////.//Pipe//cao", PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, 1, 0, 0, 5000, NULL ) ) == INVALID_HANDLE_VALUE )
{
printf( "CreateNamedPipe failed with error %d/n", GetLastError() );
system( "pause" );
return 0;
}
printf( "server is running/n" );
if( ConnectNamedPipe( pipehandle, NULL ) == 0 )
{
printf( "connectNamedPipe failed with error %d/n", GetLastError() );
CloseHandle( pipehandle );
system( "pause" );
return 0;
}
if( ReadFile( pipehandle, buf, sizeof( buf ), &bytesRead, NULL ) == 0 )
{
printf( "ReadFile failed with error %d/n", GetLastError() );
CloseHandle( pipehandle );
system( "pause" );
return 0;
}
printf( "%s/n", buf );
if ( DisconnectNamedPipe( pipehandle ) == 0 )
{
printf( "DisconnectNamedPipe failed with error %d/n", GetLastError() );
CloseHandle( pipehandle );
system( "pause" );
return 0;
}
system( "pause" );
return 0;
}
客户端:
int main( void )
{
HANDLE pipehandle;
DWORD writesbytes;
char buff[ 256 ];
if( WaitNamedPipe( "////.//Pipe//cao", NMPWAIT_WAIT_FOREVER ) == 0 )
{
printf( "WaitNamedPipe failed with error %d/n", GetLastError() );
system( "pause" );
return 0;
}
if( ( pipehandle = CreateFile( "////.//Pipe//cao", GENERIC_READ | GENERIC_WRITE, 0, ( LPSECURITY_ATTRIBUTES )NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, ( HANDLE )NULL ) ) == INVALID_HANDLE_VALUE )
{
printf( "CreateFile failed with error %d/n", GetLastError() );
system( "pause" );
return 0;
}
ZeroMemory( &buff, sizeof( buff ) );
gets( buff );
if( WriteFile( pipehandle, buff, sizeof( buff ), &writesbytes, NULL ) == 0 )
{
printf( "WriteFile failed with error %d/n", GetLastError() );
CloseHandle( pipehandle );
system( "pause" );
return 0;
}
printf( "write %d bytes", writesbytes );
CloseHandle( pipehandle );
system( "pause" );
return 0;
}
命名管道不仅可用于无关联进程间、位于不同机器上的两进程间的通信,而且可用于多对一通信,可以建立服务器进程,允许同时通过多个客户访问命名管道。命名管道常常用于多线程服务器。
六.共享内存
共享内存也可以实现进程间的通信。进程需要可以被其他进程浏览的内存块。希望访问这个内存块的其他进程请求对它的访问,或由创建它的进程授予访问内存块的权限。可以访问特定内存块的所有进程对它具有即时可见性。共享内存被映射到使用它的每个进程的地址空间。所以,它看起来像是另一个在进程内声明的变量。当一个进程写共享内存,所有的进程都立即知道写入的内容,而且可以访问。
进程间共享内存的关系与函数间全局变量的关系相似。程序中的所有函数都可以使用全局变量的值。同样,共享内存块可以被正在执行的所有进程访问。内存块可能共享一个逻辑地址,进程也可以共享某些物理地址。
共享内存块的创建必须由一个系统API调用来完成。在WIN32环境中,使用CreateFileMapping()、MapViewOfFile()以及MapViewOfFileEx() API能很好地完成。
共享内存分配位于WIN32系统中2~3GB地址范围内。一旦调用MapViewOfFile()和MapViewOfFileEx(),共享文件映射对象的所有进程都可以立即访问此内存块,而且在需要时,可以读写此内存块。
七.动态数据交换
动态数据交换( dynamic data exchange ) 是当今可用的进程间通信最强大和完善的形式之一。动态数据交换使用消息传递、共享内存、事务协议、客户/服务器范畴、同步规则以及会话协议来让数据和控制信息在进程间流动。动态数据交换对话( dynamic data exchange session, DDE )的基本模型是客户、服务器。服务器对来自客户的数据或动作作出反应。客户和服务器可以以多种关系来通信。
一个服务器可以与任意数量的客户通信。一个客户也可以与任意数量的服务器通信。单个DDE代理既可以是客户,也可以是服务器。也就是说,进程可以从一个正为另一个进程执行服务的DDE代理请求服务。