关于单一管道不能双向通信的理解

前言

本文关于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子进程有很多,不止图中的一个。所以会造成如下情况。

图五


关于单一管道不能双向通信的理解
https://llc-zh.github.io/2022/08/18/关于单一管道不能双向通信的理解/
作者
野风掠原
发布于
2022年8月18日
许可协议