半双工管道(管道 半双工)
半双工管道
本文内容来自于互联网,分享半双工管道(管道 半双工)
管 道
管道通信是最常见的通信方式之一,其是在两个进程之间实现一个数据流通的管道,该管道可以是双向或单向的。管道是一种很经典的进程之间的通信方式,其优点在于简单易用,其缺点在于功能简单,有很多限制。
管道的概念
管道是Linux/UNIX系统中比较原始的进程间通信形式,它实现数据以一种数据流的方式,在多进程间流动。在系统中其相当于文件系统上的一个文件,来缓存所要传输的数据。在某些特性上又不同于文件,例如,当数据读出后,则管道中就没有数据了,但文件没有这个特性。
匿名半双工管道在系统中是没有实名的,并不可以在文件系统中以任何方式看到该管道。它只是进程的一种资源,会随着进程的结束而被系统清除。管道通信是在UNIX系统中应用比较频繁的一种方式,例如使用grep查找。
# ls | grep ipc
上述命令中使用的是半双工管道,即grep命令的输入是ls命令的输出。管道从数据流动方向上又分全双工管道以及半双工管道,当然全双工管道现在某些系统还不支持,其在具体的实现过程中也只是在文件打开的方式上有一点区别(在操作规则上也有一些不同,全双工管道要相比半双工复杂的多)。
匿名半双工管道
匿名管道没有名字,对于管道中使用的文件描述符没有路径名,也就是不存在任何意义上的文件,它们只是在内存中跟某一个索引节点相关联的两个文件描述符。匿名半双工管道的主要特性如下:
● 数据只能在一个方向上移动。
● 只能在具有公共祖先的进程间通信,即或是父子关系进程间、或是在兄弟关系进程间通信。
尽管有如此限制,半双工管道还是最常用的通信方式。Linux环境下使用pipe函数创建一个匿名半双工管道,其函数原型如下:
#include <unistd.h>
int pipe ( int fd[2] ) ;
参数int fd[2]为一个长度为2的文件描述符数组,fd[0]是读出端,fd[1]是写入端,函数的返回值为0表示成功,–1表示失败。当函数成功返回,则自动维护了一个从fd[1]到fd[0]的数据通道。
下面实例演示了如何使用pipe函数创建管道以及关闭管道。程序中先使用函数pipe建立管道,并使用管道传输数据,在程序的结束部分,释放掉管道占用的文件资源(两个文件描述符),具体实现如下。
(1)在vi编辑器中编辑以下程序:
程序清单14-1 opro_pipe.c 管道的打开以及关闭操作
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main( void )
{
int fd[2]; /* 管道的文件描述符数组 */
char str[256];
if ( (pipe(fd)) < 0 ){
perror("pipe");
exit(1);
}
write(fd[1], "create the pipe successfully !\n", 31 );
/*向管道写入端写入数据*/
read(fd[0], str, sizeof(str) ); /*从管道读出端读出数据*/
printf ("%s", str );
printf ( " pipe file descriptors are %d, %d \n", fd[0], fd[1]) ;
close (fd[0]); /* 关闭管道的读入文件描述符*/
close (fd[1]); /* 关闭管道的读出文件描述符*/
return 0;
}
(2)在shell中编译该程序如下:
$gcc opro_pipe.c–o opro_pipe
(3)在shell中运行该程序如下:
$./ opro_pipe
create the pipe successfully !
pipe file descriptors are 4,5
程序中使用pipe函数建立了一个匿名管道fd。
%注意:文件描述符数组fd 并没有和任何有名文件相关联,之后向管道一端写入数据并从读出端读出数据,将数据输出到标准输出。在程序的最后使用close函数关闭管道的两端。
匿名半双工管道的读写操作
当对管道进行读写操作时,使用read和write函数对管道进行操作。当对一个读端已经关闭的管道进行写操作时,会产生信号SIGPIPE,说明管道读端已经关闭,并且write操作返回为–1,errno的值为EPIPE,对于SIGPIPE信号可以进行捕捉处理。如果写入进程不能捕捉或者干脆忽略SIGPIPE信号,则写入进程会中断。
%注意:在进行读写管道时,对一个管道进行读操作后,read函数返回为0,有两种意义,一种是管道中无数据并且写入端已经关闭。另一种是管道中无数据,写入端依然存活。这两种情况要根据需要分别处理。
从程序实例14.1中可以发现,单独一个进程操作管道是没有任何意义的,管道的应用一般体现在父子进程或者兄弟进程的通信。
如果要建立一个父进程到子进程的数据通道,可以先调用pipe函数紧接着调用 fork函数,由于子进程自动继承父进程的数据段,则父子进程同时拥有管道的操作权,此时管道的方向取决于用户怎么维护该管道,管道示意图如图14-3所示。
图14-3 管道示意图
当用户想要一个父进程到子进程的数据通道时,可以在父进程中关闭管道的读出端,相应的在子进程中关闭管道的输出端,如图14-3中图B所示。相反的,当维护子进程到父进程的数据通道时,在父进程中关闭输出,子进程中关闭读入即可。总之,使用pipe 及fork组合,可以构造出所有的父进程与子进程,或子进程到兄弟进程的管道。
下面实例演示了使用pipe以及fork组合实现父子进程通信。程序中先使用pipe函数建立管道,使用fork函数创建子进程。在父子进程中维护管道的数据方向,并在父进程中向子进程发送消息,在子进程中接收消息并输出到标准输出。
(1)在vi编辑器中编辑该程序如下:
程序清单14-2 fath_chil.c 管道在父子进程中的应用
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#define BUFES PIPE_BUF /* PIPE_BUF管道默认一次性读写的数据长度*/
int main ( void )
{
int fd[2];
char buf[BUFSZ];
pid_t pid;
int len;
if ( (pipe(fd)) < 0 ){ /*创建管道*/
perror ( "failed to pipe" );
exit( 1 );
}
if ( (pid = fork()) < 0 ){ /* 创建一个子进程 */
perror ( "failed to fork " );
exit( 1 );
}
else if ( pid > 0 ){
close ( fd[0] ); /*父进程中关闭管道的读出端*/
write (fd[1], "hello my son!\n", 14 ); /*父进程向管道写入数据*/
exit ( 0);
}
else {
close ( fd[1] ); /*子进程关闭管道的写入端*/
len = read (fd[0], buf, BUFS ); /*子进程从管道中读出数据*/
if ( len < 0 ){
perror ( "process failed when read a pipe " );
exit( 1 );
}
else
write(STDOUT_FILENO, buf, len); /*输出到标准输出*/
exit(0);
}
}
(2)在shell中编译该程序如下:
$gcc fath_chil.c–o fath_chil
(3)在shell中运行该程序。
$./ fath_chil
hello my son!
程序中使用pipe函数加fork组合,实现父进程到子进程的通信。程序在父进程段中关闭了管道的读出端,并相应地在子进程中关闭了管道的输入端,从而实现数据从父进程流向子进程。
管道在兄弟进程间应用时,应该先在父进程中建立管道,然后调用fork函数创建子进程,在父子进程中维护管道的数据方向。
%注意:这里的问题是维护管道的顺序,当父进程创建了管道,只有子进程已经继承了管道后,父进程才可以执行关闭管道的操作。如果在fork之前已经关闭管道,子进程将不能继承到可以使用的管道的。
下面实例演示了管道在兄弟进程间通信。下例中在父进程中创建管道,并使用fork函数创建2个子进程。在第1个子进程中发送消息到第2个子进程,第2个子进程中读出消息并处理。在父进程中,由于并不使用管道通信,所以什么都不做,直接关闭了管道的两端并退出。
(1)在vi编辑器中编辑该程序。
程序清单14-3 bro_bro.c 管道在兄弟进程间的应用
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#define BUFES PIPE_BUF
void err_quit(char * msg){
perror( msg );
exit(1);
}
int main ( void )
{
int fd[2];
char buf[BUFSZ]; /* 缓冲区 */
pid_t pid;
int len;
if ( (pipe(fd)) < 0 ) /*创建管道*/
err_quit( "pipe" );
if ( (pid = fork()) < 0 ) /*创建第一个子进程*/
err_quit("fork");
else if ( pid == 0 ){ /*子进程中*/
close ( fd[0] ); /*关闭不使用的文件描述符*/
write(fd[1], "hello brother!", 14 ); /*发送消息*/
exit(0);
}
if ( (pid = fork()) < 0 ) /*创建第二个子进程*/
err_quit("fork");
else if ( pid > 0 ){ /*父进程中*/
close ( fd[0] );
close ( fd[1] );
exit ( 0 );
}
else { /*子进程中*/
close ( fd[1] ); /*关闭不使用的文件描述符*/
len = read (fd[0], buf, BUFS ); /*读取消息*/
write(STDOUT_FILENO, buf, len);
exit(0);
}
}
(2)在shell中编译该程序如下:
$gcc bro_bro.c–o bro_bro
(3)在shell中运行该程序如下:
$./ bro_bro
hello brother!
上述程序中父进程分别建立了2个子进程,在子进程1中关闭了管道的读出端,在子进程2中关闭了管道的输入端,并在父进程中关闭了管道的两端。
%注意:程序中父进程在创建第1个子进程时并没有关闭管道两端,而是在创建第2个进程时才关闭管道。这是为了在创建第2个进程时,子进程可以继承存活的管道,而不是一个两端已经关闭的管道。
创建管道的标准库函数
从程序14-2和程序14-3中可以总结出管道操作的一个流程。父进程中先使用pipe函数创建管道,在调用fork函数创建子进程,在父子进程中维护管道的数据流向。程序退出时及时关闭管道的两端,具体流程如图14-4所示。
图14-4 匿名管道的创建流程
管道操作的基本流程为:先创建一个管道,使用fork创建子进程, 在父子进程中关闭不需要的文件描述符使用管道通信,程序结束。由于这是一个比较规范也是比较常用的管道使用模式,所以在ANSI/ISO C中将以上操作定义在两个标准的库函数popen和pclose中,它们的函数原型是:
#include <stdio.h>
FILE *popen( const char * command, const char *mode );
int pclose ( FILE *stream );
函数popen 的参数 command 是一个在shell中可运行的命令字符串的指针,参数mode 是一个字符指针,这个参数只有两种值可以使用,r或者w,分别表示popen函数的返回值是一个读打开文件指针,还是写打开文件指针。当函数失败时返回值为NULL,并设置出错变量errno。
popen函数先执行创建一个管道,然后调用fork函数创建子进程,紧接着执行一个exec函数调用, 调用/bin/sh –c来执行参数command中的命令字符串,然后函数返回一个标准的I/O文件指针。返回的文件指针类型与参数mode有关,如果参数mode是r则文件指针连接到command 命令的标准输出,如果是w则文件指针连接到command命令的标准输 入。为了关闭popen函数返回的文件指针,可以调用pclose函数。pclose函数的参数stream是一个popen打开的文件描述符,当函数失败返回–1。
下面实例演示了使用popen和pclose函数实现调用shell命令cat来打印一个文件到显示器的程序。程序中先使用popen函数为cat命令创建一条数据管道,并指定数据管道从cat命令的输出读出数据。在后续的代码中使用fgets函数读出数据,并将数据显示到标准输出中。
(1)在vi编辑器中编辑该程序如下:
程序清单14-4 recat.c 使用popen和pclose函数创建管道
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
#define BUFES PIPE_BUF
int main ( void )
{
FILE *fp;
char * cmd = "cat file1"; /*shell 命令*/
char * buf[BUFSZ];
if ((fp = popen( cmd , "r"))==NULL ) /*创建子进程到父进程的数据管道*/
{
perror ( " failed to popen " ) ;
exit ( 1 ) ;
}
while ((fgets(buf, BUFSZ, fp))!= NULL ) /*读出管道的数据*/
printf ( "%s", buf );
pclose ( fp ) ; /*关闭管道*/
exit (0) ;
}
(2)在shell中编译该程序如下:
$gcc recat.c–o recat
(3)在shell中运行该程序如下:
$./ recat
Used the popen and pclose function to create a pipe !!!
%说明:在程序14-4 recat.c中,使用popen 和pclose函数创建管道并关闭管道,使用gets函数从管道输出端读取数据并打印到标准输出中,使用popen和pclose 可以更简洁地控制管道,而无需那些繁杂的代码。当然这样做的结果是降低了程序员对管道的控制能力。
例如,popen函数返回的是文件指针,所以,在管道读写时就不能使用低级的read和write I/O调用了,只能使用基于文件指针的I/O函数,并且在popen函数中调用exec函数来复写子进程,这也是要花费一段运行时间的。