我们通常把一个进程连接到另一个进程的一个数据流称为一个“管道”,通常是用作把一个进程的输出通过管道连接到另一个进程的输入。管道本质上是内核的一块缓存。内核使用环形队列机制,借助内核缓冲区(4k)实现。
- 管道是Unix中最古老的进程间通信的形式;
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”;
- 管道的实质就是操作系统所提供的一块内存;
每个管道只有一个页面作为缓冲区,该页面是按照环形缓冲区的方式来使用的。这种访问方式是典型的“生产者——消费者”模型。当“生产者”进程有大量的数据需要写时,而且每当写满一个页面就需要进行睡眠等待,等待“消费者”从管道中读走一些数据,为其腾出一些空间。相应的,如果管道中没有可读数据,“消费者” 进程就要睡眠等待,具体过程如下图所示:
管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大(内核缓冲区(4k)),它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。
匿名管道
匿名管道是基于文件描述符的通信方式。实现两个进程间的通信时必须通过fork创建子进程,实现父子进程之间的通信。
单独创建一个无名管道,并没有任何实际的意义。我们一般是在一个进程在由pipe()创建管道后,一般再由fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。
- 1. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。
- 2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
- 3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
匿名管道读写规则:
- 当没有数据可读时:O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止;O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN;(基于不同版本linux内核返回值可能存在差异,Linux5.4.6返回值-EAGAIN)
- 当管道满的时候:O_NONBLOCK disable:write 调用阻塞,直到有进程读走数据;O_NONBLOCK enable:调用返回 -1,error 值为EAGAIN;(基于不同版本linux内核返回值可能存在差异,Linux5.4.6返回值-EAGAIN)
- 如果所有的管道写端对应的文件描述符被关闭,则read返回0;(基于不同版本linux内核返回值可能存在差异,Linux5.4.6返回当前已读取的字节大小)
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出;(基于不同版本linux内核返回值可能存在差异,Linux5.4.6返回值-EPIPE)
- 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性;
- 当要写入的数据量大于PIPE_BUF时,linux将不在保证写入的原子性;
匿名管道特点:
- 只能用于具有公共祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
- 管道提供流式服务(管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等)。
- 写入管道中的数据遵循先入先出的规则。
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程。
- 一般而言,内核会对管道操作进行同步与互斥(写满就不写,读完就不读)。
- 管道是半双工的(一边进一遍出),数据只能向一个方向流动;需要双方通信时,需要建立两个管道。