前言
本文关于Linux单一管道不能双向通信的理解基于这篇文章,解读其中的代码同时附带部分理解。
第一部分:管道通信不会阻塞的例子
我们先来看一看这段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| #include<stdio.h> #include<sys/types.h> #include<sys/wait.h> #include<unistd.h> #include<stdlib.h> #include<string.h> int main(void) { pid_t pid; int fp[2]; int ret=pipe(fp); int i; char *s[5]; char *t[5]; s[0]="ls"; s[1]="-l"; s[2]=NULL; t[0]="wc"; t[1]=NULL; for(i=0;i<2;i++) { pid=fork(); if(pid==-1) { perror("pid error:"); exit(1); } if(pid == 0){ break; } } if(i == 0) { close(fp[0]); dup2(fp[1],STDOUT_FILENO); execvp(s[0],s); } else if(i == 1){ close(fp[1]); dup2(fp[0],STDIN_FILENO); execvp(t[0],t); } int set; if(pid > 0){ do{ set = waitpid(-1,NULL,WNOHANG); }while(set == 0); } return 0; }
|
解读这段代码,可以知道它的作用是fork出两个子进程,其中一个子进程(称其为sub1)的管道读端关闭,而且sub1的标准输出从原先的指向tty(终端)变为指向管道写端,将输出的ls -l命令执行结果写入到管道缓冲区,另一个子进程(称其为sub2)的管道写端关闭,而且sub2的标准输入也从原先的指向tty(终端)变为指向管道读端,也就是说sub2的输入是从管道缓冲区读入的,执行wc命令统计上述ls -l执行结果的行数,单词数和字节数,将统计的结果输出到终端。
下面我画一幅图来详细描述这个过程。
这是fork返回时三个进程的文件描述符状态,子进程会继承父进程的文件描述符。
正如前文所说,sub1子进程的标准输出会指向管道写入端,同时关闭管道读端,将ls -l命令执行的结果输出到管道写端,写入管道缓冲区。sub1终止,sub1的文件描述符关闭。
注意此时父进程的do while循环打破了,因为waitpid返回了sub1的进程号,父进程这时也终止了,父进程的文件描述符自然就关闭了。子进程sub2读入ls -l的结果,作为wc的输入,因为此时写端没有文件描述符引用,所以sub2读操作不会阻塞。
第二部分:管道通信阻塞的例子
我们再看这段修改过的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #include<stdio.h> #include<sys/types.h> #include<sys/wait.h> #include<unistd.h> #include<stdlib.h> #include<string.h> int main(void) { while(1) { pid_t pid; int fp[2]; int ret=pipe(fp); int i; char *s[5]; char *t[5]; s[0]="ls"; s[1]="-l"; s[2]=NULL; t[0]="wc"; t[1]=NULL; for(i=0;i<2;i++) { pid=fork(); if(pid==-1) { perror("pid error:"); exit(1); } if(pid == 0){ break; } } if(i == 0) { close(fp[0]); dup2(fp[1],STDOUT_FILENO); execvp(s[0],s); } else if(i == 1){ close(fp[1]); dup2(fp[0],STDIN_FILENO); execvp(t[0],t); } int set; if(pid > 0){ do{ set = waitpid(-1,NULL,WNOHANG); }while(set == 0); } } return 0; }
|
这段修改过的代码添加了一个while死循环,这意味着父进程不会终止,而且还会不断的fork子进程,非常的恐怖。如果运行了这段代码,可以使用kill命令终止父进程。
那么这段代码为什么会阻塞进程?请看下图
管道写端引用>0, Read读完管道缓冲区后,如果缓冲区内没有数据写入,会阻塞等待写入。当然类似sub2的wc子进程有很多,不止图中的一个。所以会造成如下情况。