程序员必须让拥有依赖关系的进程集协调,这样才能达到进程的共同目标。可以使用两种技术来达到协调。第一种技术在具有通信依赖关系的两个进程间传递信息。这种技术称做进程间通信( interprocess communication )。第二种技术是同步,当进程间相互具有合作依赖时使用。这两种类型的依赖关系可以同时存在。
一般而言,进程有单独的地址空间。我们可以了解下可执行程序被装载到内存后建立的一系列映射等理解这一点。如此以来意味着如果我们有两个进程(进程 A 和进程 B ),那么,在进程 A 中声明的数据对于进程 B 是不可用的。而且,进程 B 看不到进程 A 中发生的事件,反之亦然。如果进程 A 和 B 一起工作来完成某个任务,必须有一个在两个进程间通信信息和时间的方法。我们这里可以去看看基本的进程组件。注意进程有一个文本、数据以及堆栈片断。进程可能也有从自由存储空间中分配的其它内存。进程所占有的数据一般位于数据片断、堆栈片断或进程的动态分配内存中。数据对于其它进程来说是受保护的。为了让一个进程访问另外一个进程的数据,必须最终使用操作系统调用。与之类似,为了让一个进程知道另一个进程中文本片断中发生的事件,必须在进程间建立一种通信方式。这也需要来自操作系统 API 的帮助。当进程将数据发送到另一进程时,称做 IPC ( interprocess communication, 进程间通信)。下面先列举几种不同类型的进程间通信方式:
进程间通信 描述
环境变量 / 文件描述符 子进程接受父进程环境数据的拷贝以及所有文件描述符。父进程可以在它的数据片断或环境中设置一定的变量,同时子进程接收这些值。父进程可以打开文件,同时推进读 / 写指针的位置,而且子进程使用相同的偏移访问该文件。
命令行参数 在调用 exec 或派生函数期间,命令行参数可以传递给子进程。
管道 用于相关和无关进程间的通信,而且形成两个进程间的一个通信通道,通常使用文件读写程序访问。
共享内存 两个进程之外的内存块,两个进程均可以访问它。
DDE (动态数据交换, 使用客户机 / 服务器模型( C/S ),服务器对客户的数据
Dynamic data exchange ) 或动作请求作出反应。
一、环境变量、文件描述符:
当创建一个子进程时,它接受了父进程许多资源的拷贝。子进程接受了父进程的文本、堆栈
以及数据片断的拷贝。子进程也接受了父进程的环境数据以及所有文件描述符的拷贝。子进
程从父进程继承资源的过程创造了进程间通信的一个机会。父进程可以在它的数据片断或环
境中设置一定的变量,子进程于是接受这些值。同样,父进程也可以打开一个文件,推进到
文件内的期望位置,子进程接着就可以在父进程离开读 / 写指针的准确位置访问该文件。
这类通信的缺陷在于它是单向的、一次性的通信。也就是说,除了文件描述外,如果子进程
继承了任何其它数据,也仅仅是父进程拷贝的所有数据。 一旦创建了子进程,由子进程对
这些变量的任何改变都不会反映到父进程的数据中。同样,创建子进程后,对父进程数据的
任何改变也不会反映到子进程中。所以,这种类型的进程间通信更像指挥棒传递。一旦父进
程传递了某些资源的拷贝,子进程对它的使用就是独立的,必须使用原始传递资源。
二、命令行参数:
通过命令行参数( 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;
}
命名管道不仅可用于无关联进程间、位于不同机器上的两进程间的通信,而且可用于多对一通信,可以建立服务器进程,允许同时通过多个客户访问命名管道。命名管道常常用于多线程服务器。累死了。。(待续)