学习Linux(36)管道

管道的基本概念
在进入正式的学习之前,想一想管道为什么叫管道,也想一想生活中有什么跟管道相关的?比如水管,水通过水管从一端流向另一端,那么进程间通信是不是可以模仿这种“流向”的关系呢,很显然是可以的,数据可以从一个进程流向另一个进程,那么一个进程产生数据,然后通过管道发送给另一个进程,另一个进程读取到数据,这样一来就实现了进程间的通信了。
什么是管道呢?当从一个进程连接数据流到另一个进程时,这就是一个管道(pipe)。我们通常是把一个进程的输出通过管道连接到另一个进程的输入。对于shell命令来说,命令的连接是通过管道字符来完成的,正如“ ps -aux | grep root ”命令一样,只需要使用“|”字符进行连接即可。
那么我们对这个”ps -aux | grep root”命令进行详细的分析,它实际上就是执行以下过程:
shell负责安排两个命令的标准输入和标准输出。
ps的标准输入来自终端鼠标、键盘等。
ps的标准输出传递给grep,作为它的标准输入。
grep的标准输出连接到终端显示器屏幕时。
shell所做的工作实际上是对标准输入和标准输出流进行了重新连接,使数据流从键盘输入通过两个命令最终输出到屏幕上,
在这里插入图片描述

其实,管道本质上是一个文件,过程可以看做是ps进程将输出的内容写入管道中,grep进程从管道中读取数据,这样子就是一个可读可写的文件,这其实也遵循了Linux中”一切皆文件”的设计思想,因此Linux系统直接把管道实现成了一种文件系统,借助VFS给应用程序提供操作接口。不过还是要注意的是:虽然管道的实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间,它占用的是内存空间,因此Linux上的管道就是一个操作方式为文件的内存缓冲区。

管道的分类
Linux系统上的管道分两种类型:
匿名管道
命名管道
这两种管道也叫做无名或有名管道。匿名管道最常见的形态就是我们在shell操作中最常用的”|”。它的特点是只能在父子进程中使用,父进程在产生子进程前必须打开一个管道文件,然后fork产生子进程,这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符,以达到使用同一个管道通信的目的。此时除了父子进程外,没人知道这个管道文件的描述符,所以通过这个管道中的信息无法传递给其他进程。这保证了传输数据的安全性,当然也降低了管道了通用性,于是系统还提供了命名管道,它本质是一个文件,位于文件系统中,命名管道可以让多个无相关的进程进行通讯。

匿名管道PIPE
匿名管道(PIPE)是一种特殊的文件,但虽然它是一种文件,却没有名字,因此一般进程无法使用open()来获取他的描述符,它只能在一个进程中被创建出来,然后通过继承的方式将他的文件描述符传递给子进程,这就是为什么匿名管道只能用于亲缘关系进程间通信的原因。另外,匿名管道不同于一般文件的显著之处是:它有两个文件描述符,而不是一个,一个只能用来读,另一个只能用来写,这就是所谓的”半双工”通信方式。而且它对写操作不做任何保护,即:假如有多个进程或线程同时对匿名管道进行写操作,那么这些数据很有可能会相互践踏,因此一个简单的结论是:匿名管道只能用于一对一的亲缘进程通信。最后, 匿名管道不能使用lseek()来进行所谓的定位,因为他们的数据不像普通文件那样按块的方式存放在诸如硬盘、flash 等块设备上。
总结来说,匿名管道有以下的特征:
1.没有名字,因此不能使用open()函数打开,但可以使用close()函数关闭。
2.只提供单向通信(半双工),也就是说,两个进程都能访问这个文件,假设进程1往文件内写东西,那么进程2 就只能读取文件的内容。
3.只能用于具有血缘关系的进程间通信,通常用于父子进程建通信 。
4.管道是基于字节流来通信的 。
5.依赖于文件系统,它的生命周期随进程的结束而结束。
6.写入操作不具有原子性,因此只能用于一对一的简单通信情形。
7.管道也可以看成是一种特殊的文件,对于它的读写也可以使用普通的read()和write()等函数。但是它又不是普通的文件,并不属于其他任何文件系统,并且只存在于内核的内存空间中,因此不能使用lseek()来定位。
命名管道FIFO
命名管道(FIFO)与匿名管道(PIPE)是不同的,命名管道可以在多个无关的进程中交换数据(通信)。我们知道,匿名管道的通信方式通常都由一个共同的祖先进程启动,只能在”有血缘关系”的进程中交互数据,这给我们在不相关的的进程之间交换数据带来了不方便,因此产生了命名管道,来解决不相关进程间的通信问题。
命名管道不同于无名管道之处在于它提供了一个路径名与之关联,以一个文件形式存在于文件系统中,这样,即使与命名管道的创建进程不存在”血缘关系”的进程,只要可以访问该命名管道文件的路径,就能够彼此通过命名管道相互通信,因为可以通过文件的形式,那么就可以调用系统中对文件的操作,如打开(open)、读(read)、写(write)、关闭(close)等函数,虽然命名管道文件存储在文件系统中,但数据却是存在于内存中的,这点要区分开。
总结来说,命名管道有以下的特征:
1.有名字,存储于普通文件系统之中。
2.任何具有相应权限的进程都可以使用 open()来获取命名管道的文件描述符。
3.跟普通文件一样:使用统一的 read()/write()来读写。
4.跟普通文件不同:不能使用 lseek()来定位,原因是数据存储于内存中。
5.具有写入原子性,支持多写者同时进行写操作而数据不会互相践踏。
6.遵循先进先出(First In First Out)原则,最先被写入 FIFO 的数据,最先被读出来。

pipe()函数
头文件
#include <unistd.h>
函数原型
int pipe(int pipefd[2]);
函数原型非常简单,没有任何的传入参数,注意:数组pipefd是用于返回两个引用管道末端的文件描述符,它是一个由两个整数类型的文件描述符组成的数组的指针。pipefd [0] 指管道的读取端, pipefd[1]指向管道的写端。向管道的写入端写入数据将会由内核缓冲,即写入内存中,直到从管道的读取端读取数据为止,而且数据遵循先进先出原则。pipe()函数还会返回一个int类型的变量,如果为0则表示创建匿名管道成功,如果为-1则表示创建失败,并且设置errno。
匿名管道创建成功以后,创建该匿名管道的进程(父进程)同时掌握着管道的读取端和写入端,但是想要父子进程间有数据交互,则需要以下操作:
1.父进程调用pipe()函数创建匿名管道,得到两个文件描述符pipefd[0]、pipefd [1],分别指向管道的读取端和写入端。
2.父进程调用fork()函数启动(创建)一个子进程,那么子进程将从父进程中继承这两个文件描述符pipefd[0]、pipefd [1],它们指向同一匿名管道的读取端与写入端。
3.由于匿名管道是利用环形队列实现的,数据将从写入端流入管道,从读取端流出,这样子就实现了进程间通信,但是这个匿名管道此时有两个读取端与两个写入端。
在这里插入图片描述

4.如果想要从父进程将数据传递给子进程,则父进程需要关闭读取端,子进程关闭写入端。
在这里插入图片描述

5.如果想要从子进程将数据传递给父进程,则父进程需要关闭写入端,子进程关闭读取端。
在这里插入图片描述

6.当不需要管道的时候,就在进程中将未关闭的一端关闭即可。

演示代码
1、父进程pipe无名管道
2、fork子进程
3、close无名管道
4、write/read 读写端口
5、close 关闭端口
在这里插入图片描述

父进程读出子进程写入无名管道的数据。
在这里插入图片描述

我们再深入学习一下pipe管道的一些知识吧,比如:当没有数据可读时,调用read()函数读取数据时通常会阻塞,即它将暂停进程来等待直到有数据到达为止。但如果管道的另一端已被关闭,也就是说,已经没有进程打开这个管道并向它写数据了,这时调用read()函数如果会阻塞的话,就没有意义,因为这个进程永远不会等待到数据,这也是匿名管道的一个特性,它只能在创建时返回对应的文件描述符,而无法在关闭文件描述符后后再通过open()这类函数打开,因此对一个已关闭写数据的管道做read()调用将返回0而不是阻塞。这就使读进程能够像检测文件结束一样,对管道进行检测并作出相应的动作。注意,这与read()函数读取一个无效的文件描述符不同,read()函数会把无效的文件描述符看作一个错误并返回-1。

mkfifo()函数
至此,我们还只能在有”血缘关系”的程序之间传递数据,即这些程序是由一个共同的祖先进程启动的。但如果我们想在不相关的进程之间交换数据,这还不是很方便,我们可以用FIFO文件来完成这项工作,或者称之为命名管道。命名管道是一种特殊类型的文件,它在文件系统中以文件名的形式存在,但它的的数据却是存储在内存中的。我们可以在终端(命令行)上创建命名管道,也可以在程序中创建它。
比如使用mkfifo命令去创建一个命名管道,此时会创建一个命名管道文件test(Linux一切皆文件):
在这里插入图片描述

上面说的都是终端的命令——mkfifo,当然还有系统调用函数,很巧的是,这个函数也叫这个名字——mkfifo,这个函数的作用就是创建一个命名管道,其实就类似于创建一个文件,只不过这个文件的类型是命名管道的类型。
mkfifo()会根据参数pathname建立特殊的FIFO文件,而参数mode为该文件的模式与权限。mkfifo()创建的FIFO文件其他进程都可以进行读写操作,可以使用读写一般文件的方式操作它,如open,read,write,close等。
一个进程对管道进行读操作时: - 若该管道是阻塞打开,且当前 FIFO 内没有数据,则对读进程而言将一直阻塞到有数据写入。 - 若该管道是非阻塞打开,则不论 FIFO 内是否有数据,读进程都会立即执行读操作。即如果 FIFO内没有数据,则读函数将立刻返回 0。
一个进程对管道进行写操作时: - 若该管道是阻塞打开,则写操作将一直阻塞到数据可以被写入。 - 若该管道是非阻塞打开而不能写入全部数据,则写操作进行部分写入或者调用失败

头文件
#include <sys/types.h>
#inlcude <sys/state.h>
函数原型
int mkfifo(const char * pathname,mode_t mode);
函数传入值 mode: - O_RDONLY:读管道。 - O_WRONLY:写管道。 - O_RDWR:读写管道。 - O_NONBLOCK:非阻塞。 - O_CREAT:如果该文件不存在,那么就创建一个新的文件,并用第三个参数为其设置权限。 - O_EXCL:如果使用 O_CREAT 时文件存在,那么可返回错误消息。这一参数可测试文件是否存在。
函数返回值: - 0:成功 - EACCESS:参数 filename 所指定的目录路径无可执行的权限。 - EEXIST:参数 filename 所指定的文件已存在。 - ENAMETOOLONG:参数 filename 的路径名称太长。 - ENOENT:参数 filename 包含的目录不存在。 - ENOSPC:文件系统的剩余空间不足。 - ENOTDIR:参数 filename 路径中的目录存在但却非真正的目录。 - EROFS:参数 filename 指定的文件存在于只读文件系统内。

演示代码
1、第一个进程使用mkfifo创建有名管道
2、open有名管道,write/read数据
3、close有名管道
4、第二进程使用open有名管道,read/write数据
5、close有名管道

fifo_read 程序
在这里插入图片描述

fifo_write 程序
在这里插入图片描述

使用第一个终端运行 test_fifo_read 一直监听有名管道的数据,并持续输出先
使用第二个终端运行 test_fifo_write ,每次调用输入不同是参数。
在这里插入图片描述

大家可以试想这样一个问题,只使用一个FIFO文件,如果有多个进程同时向同一个FIFO文件写数据,而只有一个读FIFO进程在同一个FIFO文件中读取数据时,会发生怎么样的情况呢?大家是不是会觉得数据相互交错混乱?如果不做任何处理,的确会这样子,但FIFO 跟 PIPE 区别的还有一个最大的不同点在于: FIFO 是具有写原子特性的,就是让写操作的原子化,怎样才能使写操作原子化呢?答案很简单,系统规定:在一个以O_WRONLY(即阻塞方式)打开的FIFO中, 如果写入的数据长度小于等待PIPE_BUF,那么或者写入全部字节,或者一个字节都不写入。如果所有的写请求都是发往一个阻塞的FIFO的,并且每个写记请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据决不会交错在一起。这种特性使得我们可以同时对 FIFO 进行写操作而不怕数据遭受破坏。
说了那么多,FIFO的应用场景是什么呢?一个典型应用是Linux 的日志系统。系统的日志信息被统一安排存放在/var/log目录下,这些日志文件都是一些普通的文本文件,在Linux系统中普通的文件可以被一个或多个进程重读多次打开,每次打开都有一个独立的位置偏移量,如果多个进程或线程同时写文件,那么除非他们之间能相互协调好,否则必然导致混乱。可惜需要写日志的进程根本不可能”协调好”,由于写日志的进程是毫无关联的,因此常用的互斥手段(比如什么互斥锁、信号量等)是无法起作用的,就像你无法试图通过交通法规来杜绝有人乱闯红灯一样,因为总有人可以故意无视规则,肆意践踏规则,如何使得毫不相干的不同进程的日志信息都能完整地输送到日志文件中而不相互破坏,是一个必须要解决的问题,一个简单高效的方案是:使用 FIFO 来接收各个不相干进程的日志信息,然后让一个进程专门将 FIFO 中的数据写到相应的日志文件当中。这样做的好处是,任何进程无需对日志信息的互斥编写出任何额外的代码,只管往 FIFO 里面写入即可。后台默默耕耘的日志系统服务例程会将这些信息一一地拿出来再写入日志文件,FIFO 的写入原子性保证了数据的完整无缺。

Hankin
2020.07.22

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值