匿名管道(pipe):
匿名管道(pipe)是Linux支持的最初Unix IPC形式之一
匿名管道进行父子进程之间通信需要用到的函数:
int pipe(int fildes[2]);
一般都是指创建匿名管道,其中传出 int fildes[2] 参数是固定的;
fildes[0] 代表读端,fildes[1] 代表写端。
适用于有血缘关系的进程。
通常父子进程之间是不要使用 sleep() 函数的,因为管道默认就是堵塞的。
虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内存空间。
-
本质:
是内核缓冲区。也是伪文件(不占用磁盘空间) -
特点:
- 其读端和写端,对应两个文件描述符;数据写端流入,读端流出
- 操作管道的进程被销毁之后,管道自动被释放了
- 管道默认是阻塞的(读写端)
-
实现原理:
- 内部实现方式:环形队列。只有一次读写的机会。先进先出。
- 其缓冲区默认是 4K 大小;但是大小会随着实际情况适当 调整。
-
局限性:
- 队列:数据只能够读取一次,不能够重复读取
- 半全工方式工作
设置管道为非阻塞
管道默认是阻塞的,那么如何设置管道为非阻塞的呢?
通过下面代码给需要修改阻塞状态的文件描述符进行增加flag的操作。
int flags = fcntl(fd[0], F_GETFL);
flags |= O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);
2 匿名管道(pipe)
2.1 什么是匿名管道?
匿名管道(pipe)是Linux支持的最初Unix IPC形式之一,具有以下特点:
- 匿名管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立两个管道;
- 只能作用于父子进程或者兄弟进程之间(具有亲缘关系的进程)
- 单独构成的一种独立的文件系统:匿名管道对于管道两端的进程而言,就是一个文件,但它不是普通文件,它不属于某种文件系统,而是自理门户,单独构成一种文件系统,去并且只存在于内存中。
- 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓存区的头部读出数据。
2.2 匿名管道的实现机制
- 匿名管道是右内核管理的一个缓冲区,相当于我们放入内存的中一个纸条。匿名管道的一端连接一个进程的输出。这个进程会向管道中放入信息。
- 匿名管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。
- 一个缓存区不需要很大,它被设计成为唤醒的数据结构,以便管道可以被循环利用。
- 当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。
- 当管道被放满信息的时候,尝试放入信息的进程就会等待,直到另一端的进程取出信息。
-
两个进程都终结的时候,管道也会自动消失。
- 从原理上,匿名管道利用fork机制建立,从而让两个进程可以连接到同一个PIPE上。
- 最开始的时候,上面的两个箭头都连接到同一个进程Process 1上(连接在Process 1上的两个箭头)。
- 当fork复制进程的时候,会将这两个连接也复制到新的进程(Process 2)。
- 随后,每个进程关闭在自己不需要的一个连接(两个黑色的箭头被关闭;Process 1关闭从PIPE来的输入连接,Process 2关闭输出到PIPE的连接),这样,剩下的红色连接就构成了上图的PIPE。
设想
通过pipe函数创建匿名管道之后。用fork函数进行子进程的创建。创建之后,在那一瞬间,父进程和子进程具有相同的文件描述符表。因此,在子进程中也存在一个文件描述符指向管道缓冲区。
关闭父进程的写端,关闭子进程的读端。就形成了下面的这个通信图。
2.3 匿名管道实现细节
- 在Linux中,匿名管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构,和VFS的索引节点inode。
- 通过将两个file结构指向同一个临时的VFS节点,而这个VFS索引节点又指向了一个物理页面而实现的。
2.4 关于匿名管道的读写
- 匿名管道的实现的源代码在fs/pipe.c中,在pipe.c中有很多函数,其中有两个函数比较重要,即匿名管道pipe_read()读函数和匿名管道写函数pipe_write()。
- 匿名管道写函数通过将字节复制到VFS索引节点指向物理内存而写入数据,而匿名管道读函数则通过复制物理内存而读出数据。
当然,内核必须利用一定的同步机制对管道的访问,为此内核使用了锁、等待队列、和信号。 - 当写入进程向匿名管道中写入时,它利用标准的库函数write(),系统根据库函数传递的文件描述符,可找到该文件的file结构。
- file结构中制定了用来进行写操作的函数(即写入函数)地址,于是,内核调用该函数完成写操作。
- 写入函数在向内存中写入数据之前,必须首先检查VFS索引节点中的信息,同时满足如下条件时,才能进行实际的内存复制工作
- 内存中有足够的空间可以容纳所有要写入的数据。
- 内存没有被读程序锁定。
- 如果同时满足上述条件,写入函数首先会锁定内存,然后从写进程的地址空间中复制数据到内存。
- 否则,写进程就休眠在VFS索引节点的等待队列中,接下来,内核将调用调度程序,而调度程序会选择其他进程运行。
- 写进程实际处于可中断的等待状态,当内存中有足够的空间可以容纳写入数据,或内存被解锁时,读取进程会唤醒写入进程,这时,写入进程将接受到信号。当数据写入内存之后,内存被解锁,而所有休眠在索引节点的读取进程会被唤醒。
- 进程可以在没有数据或者内存被锁定时立即返回错误信息,而不是阻塞该进程,这一来于文件或管道的打开模式。
- 进程可以休眠在索引节点的等待队列中等待写入进程写入数据。当所有的进程完成了管道操作之后,管道的索引节点被丢弃,而共享数据页被释放。
- VFS
VFS(virtual File System/虚拟文件系统):是Linux文件系统对外的接口。任何要使用文件系统的程序都必须经由这层接口来使用它。它是采用标准的Unix系统调用读写位于不同物理介质上的不同文件系统。VFS是一个可以让open()、read()、write()等系统调用不用关系底层的存储介质和文件系统类型就可以工作的粘合层。在Linux中,VFS采用的是面向对象的编程方法。
参考
Linux中父子进程、兄弟子进程之间通信方式--匿名管道pipe(适用于有血缘关系的进程)
进程之间通信之匿名管道(PIPE)和有名管道(FIFO)