父子进程通过 pipe() 管道通信时一方需关闭读端另一方需关闭写端

前言

前两天在看游双的《Linux高性能服务器编程》时注意到下面这段话:

管道能在父、子进程间传递数据,利用的是fork调用之后两个管道文件描述符(fd[0]和fd[1])都保持打开。一对这样的文件描述符只能保证父、子进程间一个方向的数据传输,父进程和子进程必须有一个关闭fd[0],另一个关闭fd[1]。比如,我们要使用管道实现从父进程向子进程写数据,就应该按照图13-1所示来操作。

父进程通过管道向子进程写数据

有一些思考:

  • Q1: 为什么一方需关闭读端另一方需关闭写端?不关闭会有什么问题?

  • Q2: 双方都让读写端同时打开,能否实现双向通信?

文件在内核中的表示

文件在内核中由一些数据结构表示,主要是以下三个表:

  • 描述符表(descriptor table)。每个进程都有它独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
  • 文件表(file table)。打开文件的集合是由一张文件表来表示的,所有的进程共享这张表。每个文件表的表项组成(针对我们的目的)包括当前的文件位置、引用计数(reference count)(即当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。关闭一个描述符会减少相应的文件表表项中的引用计数。内核不会删除这个文件表表项,直到它的引用计数为零。
  • v-node表(v-node table)。同文件表一样,所有的进程共享这张v-node表。每个表项包含stat结构中的大多数信息,包括st_mode和 st_size成员。

打开文件的内核数据结构表示

pipe()

pipe在内核中创建一个匿名管道(内核内存中维护的缓冲器),借助这个匿名管道读写数据实现两个进程通信。

关于匿名管道与有名管道:

  • 匿名管道没有文件实体,没有名字,创建后只能让有血缘关系进程获得;有名管道(FIFO)有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。
  • FIFO 在文件系统中作为一个特殊文件存在,但 FIFO 中的内容却存放在内存中。有名管道(FIFO)不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中。FIFO 有名字,不相关的进程可以通过打开有名管道进行通信。

父进程通过 pipe() 开辟出匿名管道后调用 fork() 创建出子进程,由于子进程共享父进程打开的文件所以子进程也持有这条管道。

子进程如何继承父进程打开的文件

父子进程共享匿名管道

不关闭会有什么问题?

管道的读写是是符合生产者–消费者模型。写入内容对应于生产内容;读取管道对应于消费内容。当所有的生产者都退场以后,消费者应当有方法判断这种情况,而不是傻傻等待已经不存在的生产者继续生产,以至于陷入永久的阻塞。

当对管道的读取端调用read函数(默认是阻塞的)返回0 时,就意味着所有的生产者都已经退场了,作为消费者的读取进程,就不需要继续等待新的内容了。什么情况下对管道读取端的描述符调用read会返回0呢?同时满足下面两个条件,对管道读取端描述符调用read返回值就是0

  1. 所有的相关进程都已经关闭了管道的写入端描述符
  2. 管道中的已有内容已经被全部读取
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 <cerrno>
#include <cstdlib>
#include <ostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
#include <iostream>

using std::cout;
using std::endl;

int main ()
{
int fd[2];
int r = pipe(fd);
if (r == -1){
perror("创建管道失败");
exit(1);
}

if (fork()){ // 父进程
int val = 0;
close(fd[0]); // 关闭读端
int times = 3;
while (times--){
sleep(1);
++val;
write(fd[1], &val, sizeof(val));
cout << "父进程发送数据: " << val << endl;
}
close(fd[1]); // 关闭写端
wait(nullptr); // 阻塞等待子进程退出
cout << "父进程结束" << endl;
} else { // 子进程
int val;
close(fd[1]); // 关闭写端
while (1){
cout << "子进程阻塞在 read() 等待接收数据" << endl;
int result = read(fd[0], &val, sizeof(val));
if (result == 0) {
cout << "子进程发现无数据可读且没有进程持有写端的 fd,退出 read" << endl;
break;
}
else cout << "子进程成功接收" << result << "字节数据: " << val << endl;
}
cout << "子进程退出循环" << endl;
}
}

注释掉第 39 行,程序运行结果:看到子进程一直阻塞在 read (),父进程的 wait() 由于子进程一直阻塞也不能完成。最后需要 ctrl + c 手动关闭

打开第 39 行,程序运行结果:子进程发现无数据可读且没有进程持有写端的 fd,退出 read(),父进程的 wait() 成功回收子进程后退出

父子进程同时打开读写端,能否实现双向通信?

不能,会有抢数据问题。进程被调度是无规律的,双方都读写的话无法保证一个写进程写完数据后立即调度读进程来接收,所以自己写的数据很可能会被自己读取。

参考


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!