在 AIX 应用开发中会遇到进程间通讯的需求,进程间通讯的方法有很多,例如通过共享内存、信号灯、内存映射文件、数据管道、文件、Socket 等等。这里主要介绍一种通过数据管道和系统标准输入输出文件描述符相结合的方式来实现进程间通讯和数据交互。本文面向 AIX 或其他 UNIX 平台 C 语言的开发者,读者需要具备一定进程间匿名管道通讯的知识,并且对文件描述符、基本的 I/O 操作有一定了解。
什么是匿名管道
管道是进程间协同工作的一种方式,单独构成一种独立的文件系统,管道是半双工的。而匿名管道数据只能向一个方向流动,双方通信时,需要建立起两个管道;只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。
文件指针与管道
当我们要读写一个文件,需要用到文件指针,它是一个指向结构体的指针。我们对管道进行读写操作时也需要用到文件指针,通过文件指针来对管道一端进行写,而另一端的进程则通过文件指针进行读。如果文件指针指向的是标准输入,那么该进程则是从标准输入中读取数据,本文利用文件指针进行匿名管道的读写操作。
通过匿名管道实现进程间通讯
通信场景如下:现在有两个进程 A、进程 B( 是进程 A 的子进程 ),进程 A 从数据库中读取一条待处理数据 M,数据 M 中存储了进程 A 需要调用的可执行程序名称及需要传递给可执行程序的参数。由于参数很多,并且参数长度及个数以及类型都是变化的,所以这里不采取参数传递方法,采用了匿名管道进程间通信方法。
图1. 匿名管道通信
进程 A 和进程 B 的标准输出都输出到指定的日志文件 logfile 中,进程 A 实现通过调用 popen 会另外 fork 一个进程 B, 而进程 A 和进程 B 之间建立起了一个数据管道(这里进程 A 写入,进程 B 读出)。这里要做一下说明,进程 B 本身就是 a.out的可执行程序,popen 原理是 fork 后再执行 exec 家族函数。我们知道 exec 家族函数特点就是调用进程的实体,包括代码段,数据段和堆栈等都已经被新的内容取代。另外,子进程(进程 B)的标准输入就是数据管道的”读端”,而管道”写端”在父进程以文件指针形式存在。这样父进程(进程 A)可以通过对 popen 返回的文件指针操作进行写操作,子进程(进程 B)可以通过读取标准输入来获取数据管道传递过来的数据。并且子进程的标准输出与父进程是相同的,由于调用 popen 时( FILE * popen( const char * command , const char * type) ),command 参数执行了重定向标准输出到 logfile,所以在子进程 ( 进程 B) 中调用任何 printf 标准输出函数都会将数据写到 logfile 中去。
清单 1. 进程 A 简要代码(fileA.sqc)
fileA.sqc #include #include #include #include … static int npWrite( FILE *fp , const char * tovalue, FILE *fp1 ) { int iLen = 0, iRetLen = 0; iLen = strlen( tovalue ); … . iRetLen = fwrite( tovalue , sizeof( char ) , iLen , fp ); … return 0; } int request() { FILE * fp = 0x00; FILE * fpw =0x00; char logfile[100]; char batfile[100]; char binpath[100]; char execfile[100]; … .. (1) sprintf( execfile , "%s/%s %s %s %s %s 1>>%s", binpath,”a.out”,“123456”,”2011-01-01”,”test”,“A0001”,logfile); fpw = 0x00; (2) /* 通过 popen 当前进程 ( 进程 A) 会通过 fork 和 exec 系统调用来启动一个子进程,( 子进程代 ** 码见 fileB.sqc), 子进程也就是我们前面讲述到的进程 B。那么进程 A 是进程 B 的父进程 . */ fpw = popen( execfile ,"w"); if ( fpw == 0x00 ) { fprintf( fp , " popen error %s\n.", strerror( errno ) ); fclose( fp ); return -1; } … . if ( npWrite( fpw , “hello world” , fp ) ) return -1; … . pclose( fpw ); fflush( fp ); … . fclose( fp ); return 0; }
源文件 fileA.sqc中调用 popen 函数来创建匿名管道,该函数需要注意以下几点:
1) popen 函数用创建管道的方式启动一个进程 ( 进程 B,即调用 popen 函数的进程的子进程 ) 并调用 Shell。管道是单向的,所以只能定义成只读或者只写。
2) popen 函数的返回值是一个普通的标准 I/O 流,它只能用 pclose 而不是 fclose 函数来关闭。向这个流的写入被转化为对 command 命令的标准输入;而 command 命令的标准输出则和调用 popen 函数的进程相同,除非这个被 command 命令自己改变。相反的 , 读取一个“被 popen 了的”流,就相当于读取 command 命令的标准输出,而 command 的标准输入则是和调用 popen 函数的进程相同。
3) popen 函数的输出流默认是被全缓冲的,poepn 函数等待相关的进程结束并返回一个 command 命令的退出状态 , 就像 wait4 函数 一样。
在 fileA.sqc中(1)标示的 popen 将执行命令的标准输出定向到了 logfile 中,由于 popen 会 fork 一个进程去执行 popen 参数 command 指定的程序,一般子进程标准输出不重定向的情况下,子进程的标准输出与父进程相同,子进程可以从标准输入中得到父进程传递的数据流(通过管道),并且也可通过子进程输出到标准输出,在 logfile 中父进程可以知道子进程的输出结果。request 函数实现读取数据调用处理程序并将数据写入管道的功能。
清单 2. 进程 B 部分源代码(fileB.sqc)
static int npRead(FILE * fp , int i ) { int iLen = 3; int iRetLen = 0; int ireadLen =0 ; char buflen[3+1]; char buffer[100]; memset( buffer , 0x00 , sizeof( buffer )); memset( buflen , 0x00 , sizeof( buflen )); ireadLen = … .; … ireadLen = fread( buffer , sizeof( char ) , iRetLen , stdin ); … buffer[iRetLen] = 0x00; … // 输出到与父进程相同的 LOG 文件中 printf( "recv[%d][%s]\n" ,iRetLen ,buffer ); … . return 0; } void nGetNetData() { int i = 0; for ( i = 0; i < n ; i++ ) { npRead( stdin , i ); } } 进程 B 调用 nGetNetData 函数就可以从标准输入到进程 A 传递数据了,并且在 npRead 中通过调用 printf 将日志信息输出到了在进程 A 中指定的文件 logfile 中。
结束语
1、使用匿名管道并结合我们常用标准输入输出函数实现进程间通讯很方便,但不适用于无亲缘关系的进程。
2、对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是单独构成一种文件系统,并且只存在内存中,通信双方的进程通过标准输入输出 API 进行通信写入和读取。
3、管道也是一种文件类型,理解其原理更有利于我们使用它。
4、标准输入输出和普通的文件描述符相同,我们可以根据需要利用 shell 或者 dup 函数都可以实现重定向。这样我们就可以更好的利用标准 I/O 库为我们工作了。
原文:
http://www.ibm.com/developerworks/cn/aix/library/1106_chenye_commbypipe/index.html?ca=drs-
【编辑推荐】