使用管道需要注意以下4种特殊情况(默认都是阻塞I/O操作,没有设置O_NONBLOCK标志):
1. 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
2. 如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
3. 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。
4. 如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。
总结:
读管道:1.管道中有数据,read返回实际读到的字节数。2.管道中无数据:管道写端被全部关闭,read返回0(好像读到文件结尾);写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)。
写管道:1.管道读端全部被关闭,进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)。2. 管道读端没有全部关闭:管道已满,write阻塞;管道未满,write将数据写入,并返回实际写入的字节数。
重点注意:
如果写入的数据大小n<=PIPE_BUF时,linux保证写入的原子性,即要么不写,要么全写入。如果没有足够的空间供n个字节全部写入,则会阻塞直到有足够空间供n个字节全部写入;如果写入的数据大小n>PIPE_BUF时,写入不再具有原子性,可能中间有其它进程穿插写入,其自身也会阻塞,直到将n字节全部写入在才返回写入的字节数,否则阻塞等待。
读数据时,如果请求读取的数据(read函数的缓冲区)大小>=PIPE_BUF,则直接返回管道中现有的数据字节数(即将管道中的数据全部读出);如果< PIPE_BUF,则返回管道中现有的数据字节数(此时管道中的实际数据量<=请求的数据量大小),或者返回请求数据量的大小。
练习1:父子进程使用管道通信,父写入字符串,子进程读出并打印到屏幕。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int main(void)
{int ret,fd1;char *p="zhangshuxiong\n";int fd[2];ret = pipe(fd);if(ret == -1){perror("pipe");exit(1);}fd1 = fork( );if(fd1 == -1){perror("fork");exit(1);}else if(fd1 == 0) {sleep(3); //子进程睡3秒close(fd[1]); //子进程关闭写端char buff[1024]={0};ret = read(fd[0],buff,1024); //子进程读数据if(ret == -1){perror("read");exit(1);}else if(ret == 0) {printf("父进程没有向管道里写入数据\n");}else {int res= write(STDOUT_FILENO,buff,ret); //将读出的数据输出到屏幕if(res == -1){perror("write");exit(1);}}close(fd[0]); //子进程结束前关闭掉文件描述符}else {close(fd[0]);int rer = write(fd[1],p,strlen(p)); //父进程写入数据if(rer == -1){perror("write");exit(1);}close(fd[1]); //父进程结束前关闭掉文件描述符wait( NULL ); //父进程回收(阻塞等待)}return 0;
}
[root@localhost pipe]# ./pip
zhangshuxiong
[root@localhost pipe]# //可见,如果没有wait,则父进程会先结束,正因为有了wait,父进程会等待子进程结束,最后shell进程才会收回前台,等待与用户交互。注意,即使没有sleep函数,依然能保证子进程运行时一定会读到数据,因为是阻塞读。
练习2:使用管道实现父子进程间通信,完成:ls | wc –l。假定父进程实现ls,子进程实现wc。
[root@localhost pipe]# ls
makefile pip pip.c pipe pipe1 pipe1.c pipe2 pipe2.c pipe3 pipe3.c pipe.c pipe_test pipe_test.c test
[root@localhost pipe]# ls | wc –l //统计文件的字数
14
其实 ls | wc –l命令执行后,shell进程会创建两个子进程,并创建一个管道,用于两子进程通信,下面给出详细实现过程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main(void)
{int ret,fd1;int fd[2];ret = pipe(fd);if(ret == -1){perror("pipe");exit(1);}fd1 = fork( );if(fd1 == -1){perror("fork");exit(1);}else if(fd1 == 0) {close(fd[1]);int as = dup2(fd[0],STDIN_FILENO); //将标准输入重定向到管道读端if(as == -1){perror("dup2");exit(1);}close(fd[0]); //只是关了fd[0],不关也可以,进程结束会自动关闭execlp("wc","wc","-l",NULL); //该命令从标准输入读取文本}else {close(fd[0]);int as = dup2(fd[1],STDOUT_FILENO); //将标准输出重定向到管道写端if(as == -1){perror("dup2");exit(1);}execlp("ls","ls",NULL); ///该命令结果会写到标准输出}return 0;
}
[root@localhost pipe]# ./pip
14 //可见,跟ls | wc –l的结果一样
注意,上述程序并没有考虑到子进程的回收问题,如果父进程比子进程先结束,子进程会被init进程回收;后结束,子进程会先变为僵尸进程,等父进程结束了,再被init进程回收。
ls命令正常会将结果集写出到stdout,但现在会写入管道的写端;wc –l 正常应该从stdin读取数据,但此时会从管道的读端读。
也有可能会出现这种情况:程序执行,发现程序执行结束,shell还在阻塞等待用户输入。这是因为,shell → fork → ./pipe1, 程序pipe1的子进程将stdin重定向给管道,父进程执行的ls会将结果集通过管道写给子进程。若父进程在子进程打印wc的结果到屏幕之前被shell调用wait回收,shell就会先输出$提示符。
练习3:使用管道实现兄弟进程间通信。 兄:ls 弟: wc -l 父:等待回收子进程。要求,使用“循环创建N个子进程”模型创建兄弟进程,使用循环因子i标示。注意管道读写行为。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>int main(void)
{int i,ret,fd1;int n=2;int fd[2];ret = pipe(fd);if(ret == -1){perror("pipe");exit(1);}for(i=0;i<n;i++){fd1 = fork( );if(fd1 == -1){perror("fork");exit(1);}else if(fd1 == 0)break;}if(i == n){close(fd[0]);close(fd[1]); //特别强调,父进程不用管道,必须要关掉,否则运行出错(为了维护管道的单向通信)int status;do {pid_t pid=waitpid(-1,&status,0);if(pid > 0)n--;if(pid == -1){perror("waitpid");exit(1);}if(WIFEXITED(status))printf("the child process of exit with %d\n",WEXITSTATUS(status));else if(WIFSIGNALED(status))printf("the child process was killed by %dth signal\n",WTERMSIG(status));}while(n>0);}else if(i == 1) {close(fd[1]);int as = dup2(fd[0],STDIN_FILENO);if(as == -1){perror("dup2");exit(1);}close(fd[0]);execlp("wc","wc","-l",NULL);}else {close(fd[0]);int as = dup2(fd[1],STDOUT_FILENO);if(as == -1){perror("dup2");exit(1);}execlp("ls","ls",NULL);}return 0;
}
[root@localhost pipe]# ./pip
14
the child process of exit with 0
the child process of exit with 0
强调一点:在使用管道传递数据之前,不用的管道读或写端都必须要关闭,这是为了维护管道的正常运行(单向通信)。
测试:是否允许,一个pipe有一个写端,多个读端呢?是否允许有一个读端多个写端呢?
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>int main(void)
{pid_t pid;int fd[2], i, n;char buf[1024];int ret = pipe(fd);if(ret == -1){perror("pipe error");exit(1);}for(i = 0; i < 2; i++){if((pid = fork()) == 0)break;else if(pid == -1){perror("pipe error");exit(1);}}if (i == 0) {close(fd[0]);write(fd[1], "1.hello\n", strlen("1.hello\n"));} else if(i == 1) {close(fd[0]);write(fd[1], "2.world\n", strlen("2.world\n"));} else {close(fd[1]); //父进程关闭写端,留读端读取数据 //sleep(1); //这条语句是很关键的n = read(fd[0], buf, 1024); //从管道中读数据write(STDOUT_FILENO, buf, n);for(i = 0; i < 2; i++) //两个儿子wait两次wait(NULL);}return 0;
}
如果父进程不睡眠:
[root@localhost pipe]# ./pipe3
2.world
1.hello
[root@localhost pipe]# ./pipe3
1.hello
[root@localhost pipe]# ./pipe3
2.world
可见:三个进程的执行顺序是随机的,如果两个子进程在父进程读之前,都先写入,那么两个都会读出。为了确保两个都读出,可以使用读两次的方法,也可以让父进程先睡眠一会,如下:
如果父进程睡眠:
[root@localhost pipe]# ./pipe3
1.hello
2.world
[root@localhost pipe]# ./pipe3
1.hello
2.world
最终练习:统计当前系统中进程ID大于10000的进程个数。
提示: 采用awk命令,可以统计文本中符合条件列的个数及和。运用ps aux和管道。