这两天大概学习了一下Linux的管道。进程间通信有很多种方法,例如信号量、消息、管道等等。一般来说,对于简单的进程间为了运行同步而进行的通信,信号量,甚至是互斥量(单值的信号量)就足够了;对于格式化的通信,消息很好地满足了要求,由于消息的意义以及处理对于操作系统来说都是规格化的,因此风险也比较小。但是,上面两种进程间的通信方式都过于简单,在需要灵活控制的,且数据量比较大的情况下,这两种方式就无法胜任了,而管道则正好满足了这方面的需求。
管道表现得像普通的文件描述符一样。如不进行通信,你无法知道一个文件描述符是一个文件还是一个管道。这是一个特征;读标准输入和写标准输出的程序不需要知道或关心它们是否在与其他的进程进行通信。如果你想知道,比较规范的检查方式是对文件描述符调用‘lseek(fd,0,SEEK_CUR)’;这个调用试图由当前位置搜索0个字节,也就是说,这是一个什么也没做的操作。通常,这样的操作叫做no-op,即“no
operation”的缩写。但是,什么也没做,不等于什么结果也不产生(不然,我们这么的意义何在?),对于管道,该操作会失败,而对于其他文件也不会造成任何损坏。这似乎是聪明的程序员经常玩耍的一个把戏,还记得在写Intel
X86上的汇编吗?AND AX,AX
同样看似什么也没做,可是它却能够把标志位给置起来,从而可以获得AX这个寄存器中的数的很多特征。
系统调用pipe()用于创建管道:
#include
int pipe(int
filedes[2]);
其中的参数值是一个2元素的整数数组的地址。pipe()成功时返回0,和其他绝大多数标准POSIX一样,出错时返回-1.
如果调用成功,则进程此时有了两个额外的打开文件描述符。filedes[0]中的值是管道的读取端(read
end),而filedes[1]是管道的写入端(writer
end)(一种方便的记忆方式是读取端使用索引0,类似于标准输入的文件描述符0(stdin),而写入端使用索引1,同样类似于标准输出的文件描述符1(stdout))。
正如前面所提到的,写入写入端的数据能够在读取端进行读取。处理完管道的时候,调用close()将两端关闭。下面这个简单的程序演示了如何创建管道、向其中写入数据并从中读取数据:
#include
#include
#include
int main(int
argc,char**argv)
{
static const char mesg[]="Jimmy Chen";
char buf[BUFSIZE];
ssize_t rcount,wcount;
int pipefd[2];
size_t l;
if(pipe(pipefd)<0){
fprintf(stderr,"%s:pipe
failed:%s\n",argv[0],strerror(errno));
exit(1);
}
printf("Read end = fd %d, write end = fd
%d\n",pipefd[0],pipefd[1]);
l=strlen(mesg);
if((wcount=write(pipefd[1],mesg,1))!=1){
fprintf(stderr,"%s:write
failed:%s\n",argv[0],strerror(errno));
exit(1);
}
if((rcount=read(pipefd[0],buf,BUFSIZE))!=wcount){
fprintf(stderr,"%s:read
failed:%s\n",argv[0],strerror(errno));
exit(1);
}
buf[rcount]='\0';
printf("Read
<%s> from pipe\n",buf);
close(pipefd[0]);close(pipefd[1]);
return 0;
}
首先声明了一些局部变量,最令人感兴趣的是mesg,它是经管道传输的文本。接下去创建了一个管道,同时进行错误检查。再接下去就是对管道的读与写了,同时进行错误检查。
这个程序没有有用的功能,但是它确实演示了基本的概念。注意没有调用open()或者creat(),而程序也没有使用它所继承的3个文件描述符。但是,write()和read()依然能够调用成功,这就证明了文件描述符是有效的,而进入管道的数据确实能够被输出。当然,如果消息太大,则我们的程序就会无法正常运行。这是因为管道的空间就是那么小。
与其它的文件描述符相同,调用fork()之后,如果管道未关闭,则管道的文件描述符可以由子进程继承,而且调用exec()之后该描述符依然可用。
下面再介绍一下缓冲管道。
缓冲无处不在。和谐并不是事物的自然状态,而缓冲则使得和谐成为可能。
管道缓冲其数据,这意味着写入管道的数据由内核保存直到这些数据被读取。但是,一个管道仅能够保存适量的写入且仍未读取的数据。我们可以将写入进程成为生产者(producer),将读取进程成为消费者(consumer)。系统如何管理空闲和充满的管道呢?
当管道充满时,系统自动阻塞生产者下次试图使用write()写入数据到管道中。一旦管道清空,系统将数据拷入管道,并允许系统调用write()返回给生产者。
类似的,如果管道空闲,则消费者在调用read()的时候就会阻塞,直到管道中有更多可读取的数据(阻塞机制可以关闭,这个我们以后再谈)。
当生产者对管道的写入端调用close()时,消费者仍能够成功地读取依然缓冲在管道中的数据。完成之后再调用read()时返回0,表明文件结束。
相反,如果消费者关闭读取端,则对写入端的write()调用将会失败——彻底失败。特别的,内核给生产者发送“中断的管道(broken
pipe)”信号,其默认动作是关闭该进程。
本文主要参考了Arnold Robbins的《Linux Programming by Example》
阅读(740) | 评论(0) | 转发(1) |