【Linux】进程间通信之匿名管道

👦个人主页:Weraphael

✍🏻作者简介:目前正在学习c++和算法

✈️专栏:Linux

🐋 希望大家多多支持,咱一起进步!😁

如果文章有啥瑕疵,希望大佬指点一二

如果文章对你有帮助的话

欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍


目录

  • 一、进程间通信介绍
      • [1.1 是什么](#1.1 是什么)
      • [1.2 为什么(目的)](#1.2 为什么(目的))
      • [1.3 怎么做到的(浅聊)](#1.3 怎么做到的(浅聊))
      • [1.4 进程间通信的实现方式](#1.4 进程间通信的实现方式)
  • 二、什么是管道
  • 三、匿名管道
      • [3.1 匿名管道的工作原理](#3.1 匿名管道的工作原理)
      • [3.2 站在文件描述符角度-深度理解管道](#3.2 站在文件描述符角度-深度理解管道)
      • [3.3 pipe系统调用接口](#3.3 pipe系统调用接口)
      • [3.4 匿名管道的特征](#3.4 匿名管道的特征)
      • [3.5 匿名管道的四种情况](#3.5 匿名管道的四种情况)
  • 四、简单模拟进程池(匿名管道的应用场景)
  • 五、相关代码

一、进程间通信介绍

1.1 是什么

进程间通信IPCInter-Process Communication)就是两个或多个进程实现数据层面的交互

1.2 为什么(目的)

  • 数据传输:一个进程需要将它的数据发送给另一个进程。

  • 资源共享:多个进程之间共享同样的资源。

  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

  • ...

1.3 怎么做到的(浅聊)

  • 进程间通信的本质:让不同的进程能共享同一份"资源"。这个"资源"可以理解为特定形式的内存空间。当一个进程将数据写入共享内存后,另一个进程就可以从同一份内存空间中读取这些数据,从而实现了进程之间的数据传递和共享。

  • 这个资源(内存)一般由操作系统提供。那为什么不是两个进程中的其中一个提供这个"资源"呢?可以假想一下,假设这个资源真的由其中一个进程提供,而进程是具有独立性的,各自拥有自己的虚拟地址空间和资源,那么它就会暴露自己的内部状态和数据给其他进程,如果提供资源的进程出现故障或意外终止,其他进程可能会受到影响,因为它们依赖于这个进程提供的资源。

  • 相比之下,如果资源由操作系统提供,而进程相当于用户,如果想使用资源,那么操作系统必定会提供统一接口

  • 而我们知道,因为系统中不止一对进程在进行通信,可能会存在多个,那么操作系统就要提供多份"资源",因此操作系统就必须对"资源"进行管理,这又得搬出管理的六字真言:先描述,再组织

1.4 进程间通信的实现方式

二、什么是管道

Linux中,管道可以被视为一种特殊类型的文件,它是基于文件级别的通信方式 。它使得一个进程的输出可以直接成为另一个进程的输入,从而实现了进程之间的数据传输和协作。在Linux中,你可以使用管道符号 |将一个进程的输出发送到另一个进程的输入。

  • 比方说你想要统计一个文件中包含的单词数量。

其中,当cat命令和wc命令运行起来后就是两个进程,cat进程通过标准输出将数据传输到管道当中,wc进程再通过标准输入从管道当中读取数据,至此便完成了两个进程间通信。

Linux中,有两种类型的管道,一种是匿名管道(Anonymous Pipe),另一种是命名管道(Named Pipe)。因此,这篇博客重点是要解释匿名的工作原理等方面。(命名管道在下一篇博客讲解)

三、匿名管道

3.1 匿名管道的工作原理

  • Linux中,管道可以被视为一种特殊类型的文件,它是基于文件级别的通信方式。为什么这样说呢?管道的使用方式类似于文件的读写,它是通过文件描述符进行访问,管道的读取端和写入端分别对应着文件的读取和写入。在管道中写入的数据会被暂存在内核的缓冲区中,然后读取进程从缓冲区中读取,因为它在文件系统中没有具体的文件路径或名字,因此被称为匿名管道。它其实是操作系统在内存中创建的一段缓冲区。(后面会介绍)

  • Linux中,使用管道符号 | 本质上就是创建了一个匿名管道,它常用于父子进程(或者有血缘关系)之间的通信。

不知道大家有没有好奇:进程通信时每次都要向文件中读取数据,而文件是存储在磁盘上的,这不会导致效率非常低吗?如果效率非常低的话,Linux为什么还要开发出管道符号|呢?因此,我们必须要知道匿名管道的工作原理。

进程间通信的本质:让不同的进程看到同一份资源 。因此,使用匿名管道实现父子进程间通信的原理就是:让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。

学习到现在,我们都清楚(复习):

  • 当一个程序运行起来(加载到内存),就是一个进程,操作系统会管理该进程并且其创建task_struct结构体对象。

  • 当进程打开文件时,操作系统会在内核中创建数据结构来描述这个已打开的文件对象struct file。它包含了inode(文件的所有属性)、指向文件操作函数表的指针(文件操作方法)、内核缓冲区等等。

  • 而进程可以打开多个文件。所以,操作系统还需要对打开的文件进行管理,所以进程task_struct对象里有一个指针struct files_struct* files,这个指针指向一个结构体files_struct,而这个结构体包含一个指针数组struct file* fd_array[],这个数组我们可以称之为文件描述符表。数组中的每个元素都是指向当前进程所打开文件的指针(地址)!默认情况下,当一个进程启动时,操作系统会打开三个标准流(文件):stdin(键盘文件)、stdout(显示器文件)、stderr(显示器文件)。

回归正题:

此时fork创建子进程,操作系统会以父进程为模板,为子进程创建新的进程控制块task_structfiles_struct等。但并不会复制父进程打开的文件对象 struct file,因为文件操作通常是在用户空间的,与进程无关;而子进程会拷贝父进程的文件描述符表 files_struct,这是因为文件描述符表是进程的一部分,它记录了进程打开的文件描述符和相应的文件对象。因此,子进程的 struct file* fd_array[] 中的元素会指向父进程打开的文件对象,即父子进程(不同的进程)可以看到同一份资源(打开的文件),这不就是通信的本质吗?!

又因为文件对象struct file是由操作系统管理的,如果操作系统识别到是普通文件,就通过内核缓冲区刷新策略往磁盘上读写;而如果识别到时管道文件,操作系统不会将数据刷新到磁盘,而是直接在内核缓冲区中进行数据传输。因此,这种操作方式使得管道的数据传输更加高效。

注意:匿名管道在相关进程的生命周期内有效,但是并不是一旦相关进程结束就会自动关闭并释放资源。而是会依靠文件对象的引用计数机制来管理资源的释放。只有当所有相关进程都关闭了对管道的引用时,文件对象的引用计数变为零,才会触发操作系统对管道资源的释放。

3.2 站在文件描述符角度-深度理解管道

如果父进程以只读方式打开管道,并且在创建子进程时,子进程也会以只读方式打开管道。那么进程间该如何通信呢?

显然以上这种方式是无法进行通信的。如果要实现通信,一种常见的方法(步骤)是:

  1. 父进程在打开管道的时候,以读和写的方式打开
  1. 接下来父进程fork创建子进程,子进程会拷贝父进程的文件描述符表,所以子进程也会以读写的方式打开管道文件
  1. 接下来根据实际需求,可以通过关闭相应的管道端口来实现单向通信。比方说,你想让父进程向管道写入数据,而子进程从管道读取数据。那么你就需要关闭父进程的读取端,关闭子进程的写入端。

接下来可能有人会想:为什么管道两端不设计成既可读,也可写呢?

  1. 竞态条件:如果管道的两端都可以读写,那么可能会出现竞态条件。两个进程同时尝试在管道中读取和写入数据,可能导致数据的不一致性和混乱。

  2. 死锁:读写管道的两端同时进行读写操作时,可能会导致死锁。例如,一个进程在等待从管道中读取数据,而另一个进程在等待向管道中写入数据,这种情况下可能会导致两个进程相互等待,最终造成死锁。

  3. 数据丢失:如果管道的两端同时读写,可能会导致数据丢失。例如,一个进程正在向管道中写入数据,同时另一个进程正在从管道中读取数据,这种情况下可能会导致部分数据被丢失。

  4. 混乱的通信:同时读写管道可能会导致通信的混乱和不可预测性。由于数据的读写是同时进行的,可能会导致数据的顺序混乱或部分数据丢失,使通信变得不可靠。

因此,为了避免这些问题,一般建议使用管道时一端只负责写入数据,另一端只负责读取数据。这样可以确保通信的可靠性和一致性。

3.3 pipe系统调用接口

Linux中,除了使用管道符号|创建和使用匿名管道之外,也可以在程序中使用 pipe 系统调用进行创建和使用。

【函数原型】

cpp 复制代码
#include <unistd.h>
int pipe(pipefd);

其中:

  • pipefd:做输出型参数,传一个整型数组,用于存放管道文件的两个文件描述符。pipefd[0] 是管道的读取端,pipefd[1] 是管道的写入端。

  • 返回值

    • 成功时,返回值为0,并且在 pipefd 数组中存放管道的两个文件描述符。

    • 失败时,返回值为-1,错误码保存在 errno 中。

在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,我们设置父进程只能向管道读取数据,而子进程从管道写入数据,代码如下(配代码注释)

【程序结果】

3.4 匿名管道的特征

  1. 匿名管道常用于具有血缘关系的进程。

  2. 管道只能单向通信。

  3. 父子进程是会协同的,同步与互斥的,是为了保护管道文件的数据安全(信号量及多线程再谈)。意思就是说:在管道通信过程中,如果父进程负责从管道读取数据,而子进程负责向管道写入数据,要确保进程在通信时以正确的顺序发送和接收消息,避免出现数据竞争和不确定行为。

  4. 管道的生命周期随进程。因为管道在操作系统中被看作是一种文件,但是它其实是是通过内存缓冲区来传输数据。当所有打开管道的进程(读取端和写入端)都关闭了管道,管道所占用的资源就会被操作系统释放。

  5. 管道是面向字节流的。(网络再谈)

3.5 匿名管道的四种情况

  1. 读写端正常,管道如果为空,读端就会被阻塞,等待写端的数据。

比方说,写端只向管道写三次数据,那么对应的读端就会接收三次数据,后续读端就会被阻塞,等待写端数据

【程序结果】

  1. 读写端正常,管道如果被写满,写端就要被阻塞。这说明管道是有限大小的缓冲区,此大小由操作系统决定。

比方说:子进程不断写入,父进程不读取(死循环啥也不干)

【程序结果】

  1. 读端正常读,写端关闭,那么读取端会在读取完管道中的所有数据后得到一个特殊的信号,表明已经到达了管道的末尾。这样读取操作就会返回值为 0,也就是read函数会返回0,而不会再被阻塞(不会再等待管道中会有新的数据)。因此,当写端关闭后,读端就知道不会再有新的数据写入管道,可以安全地关闭管道,结束通信。

【程序结果】

因此,当写端关闭后,读端就知道不会再有新的数据写入管道,可以安全地关闭管道,结束通信。

  1. 写端正常,读端关闭。那么这两个进程之间通信就没有意义了,因此,操作系统就要杀掉正在写入的进程。那如何杀掉呢?--- 当操作系统希望终止一个正在运行的进程时,通常会发送一个特定的信号 给该进程来杀掉。

    (该图片来自于往期博客)

那么写端进程退出时究竟是收到了什么信号,我们可以来验证一下

【程序结果】

运行结果显示,子进程(写端)退出时收到的是13号信号

通过kill -l命令可以查看13对应的具体信号。

由此可知,当发生情况四时,操作系统向子进程发送的是SIGPIPE信号将写端进程终止的。

四、简单模拟进程池(匿名管道的应用场景)

在另一篇博客中,博主正在加工中~

五、相关代码

本篇博客的相关代码:点击跳转

相关推荐
zhd15306915625ff22 分钟前
化工厂主要涉及的自动化备件有哪些?
运维·自动化·化工厂
Jason-河山22 分钟前
利用API返回值实现商品信息自动化更新:技术与实践
运维·自动化
wowocpp1 小时前
查看 linux ubuntu 分区 和 挂载 情况 lsblk
linux·运维·ubuntu
wowocpp1 小时前
查看 磁盘文件系统格式 linux ubuntu blkid ext4
linux·数据库·ubuntu
久绊A2 小时前
网络信息系统的整个生命周期
网络
并不会2 小时前
常见 CSS 选择器用法
前端·css·学习·html·前端开发·css选择器
_PowerShell2 小时前
[ DOS 命令基础 3 ] DOS 命令详解-文件操作相关命令
网络·dos命令入门到精通·dos命令基础·dos命令之文件操作命令详解·文件复制命令详解·文件对比命令详解·文件删除命令详解·文件查找命令详解
龙鸣丿2 小时前
Linux基础学习笔记
linux·笔记·学习
耶啵奶膘4 小时前
uniapp-是否删除
linux·前端·uni-app
奋斗的小花生4 小时前
c++ 多态性
开发语言·c++