【Linux 系统】进程间通信(匿名管道 & 命名管道)-- 详解(上)

简介: 【Linux 系统】进程间通信(匿名管道 & 命名管道)-- 详解(上)

⚪前言

如何理解进程间通信?

进程具有独立性,所以进程想要通信难度是比较大的,成本高。

在日常生活中,通信的本质是传递信息,但站在程序员角度来看,进程间通信的本质:让不同的进程看到同一份资源(内存空间)

进程间通信就是进程之间互相传递数据,那么进程间能直接相互传递数据吗?

不能,因为进程具有独立性,所有的数据操作都会发生写时拷贝,父子进程都不能传递,更不要说两个进程毫无关系还想直接相互传递数据。

所以两个进程如果想要通信就一定要通过中间媒介的方式来进行通信,那么就必须先想办法让不同的进程看到同一份公共的资源,这里所谓公共的资源就是系统通过某种方式提供的系统内存。 这块空间通常是由操作系统提供的,可以被两个不同的进程都看到,然后它们才能实现通信。

传递数据就是由一个进程拷到对应的内存里,这块内存另一个进程当然也能看到,所以也自然能从内存里拷到自己的进程中。

综上所述,我们就知道了进程间通信要学的就是如何通过系统,让不同的进程看到同一份资源。操作系统提供的通信方案有很多种,这句话的含义就是操作系统让不同进程看到同一份资源的方式有很多种,最典型的有管道、消息队列、共享内存、信号量等等。下面主要谈管道和共享内存,而信号量会在后面多线程的部分再展开,这里主要以概念为主。

所以进程间通信的本质就是让不同的进程,能看到同一份系统资源,而这份资源就是系统通过某种方式提供的系统内存,因为方式是有差别的,所以通信策略也是有差别的。


一、进程间通信介绍

1、进程间通信目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程(可以理解为一个进程将数据加工成半成品通过某种通信方式给到另一个进程,另一个进程再做加工)。
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
为什么要进行进程间通信?

往往是出于交互数据、控制、通知等目的。


2、进程间通信发展

在进程间通信发展的过程主要有两种流派,一种是只在主机上通信,就是 System V,另一种是可以在主机上的进程跨网络通信,就是 POSIX。下面主要学习 System V,等到后面网络部分再学习 POSIX。

管道是操作系统本身提供的,所以这里能接触到的是管道和 System V 进程间的通信方式。

  • 管道
  • System V 进程间通信
  • POSIX 进程间通信

3、进程间通信的分类

(1)管道

  • 匿名管道 pipe
  • 命名管道

(2)System V IPC

主要用于单机通信。

  • System V 消息队列
  • System V 共享内存(不常用)
  • System V 信号量(了解原理

(3)POSIX IPC

主要用于网络通信。  

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

上面这些分类的标准在我们使用者看来,都是接口上具有一定的规律。


4、进程间通信的必要性

单进程无法使用并发能力,也无法实现多进程协同。

进程间通信有很多目的,比如:传输数据、同步执行流、消息通知等,就是为了实现多进程协同。

进程间通信不是目的,只是一种手段。


5、进程间通信的技术背景

  • 进程是具有独立性的。进程是通过虚拟地址空间 + 页表来保证进程运行的独立性(进程内核数据结构 + 进程的代码和数据)。
  • 通信成本较高,进程本身就已经具有独立性了,这时要让不同进程看到同一份资源,肯定不容易。

6、进程间通信的本质理解

进程间通信的前提是:首先要让不同的进程看到同一块“内存”(特定的结构组织的)。

那么我们所谓的进程看到同一块 “内存”是属于哪一个进程呢?—— 不能隶属于任何一个进程,而应该更强调共享。


二、管道

1、什么是管道

现实生活中也存在着很多管道,它们的共同点是:都有一个入口和一个出口(最典型的特点:只能单向通信),在这其中就传送着人们所需要的自来水、石油资源等。

互联网中的管道传送的是数据资源,所以计算机就模拟出一条管道。数据资源一定是有人想传入,并且有人想获取,那么这里的有人就分别对应发送进程和接受进程。

现实中构建管道所使用的材料是钢铁,而计算机中构建管道缓冲区所使用的材料是系统内存,而这里的系统内存就是让不同进程所看到的同一块系统资源。上面所说的概念只是一种感性的理解,还没有涉及到任何的系统概念,归根结底是想让大家明白不同角色的定位。

  • 管道是 Unix 中最古老的进程间通信的形式。
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个 “管道”

管道不能是进程 A 或进程 B 提供的,一定是操作系统提供的,只是两个进程恰好利用某种方式通过管道来进行通信。任何的中间资源不能隶属于某一个进程,因为进程具有独立性,一旦某种中间通信资源隶属于某个进程,那么其它进程一定不能看到。

管道一共有两种通信方案:匿名管道命名管道它们的底层原理基本上是一样的,区别在于它们各自的侧重点不同。


2、匿名管道 pipe

匿名管道是供具有血缘关系的进程进行进程间通信,常用于父子进程之间。即便是父子,它们的数据也不是共享的,而是私有的,凡是共享的都是因为双方都不写入罢了。

所有的通信方式,特别是进程间通信,首先是得保证不同的进程看到同一份资源。匿名管道就是这个管道没有名字,它也不需要,匿名管道是由子进程继承父进程的文件描述符中的内容来的


sleep 在系统中也是一条命令,这里就是想让这三个 sleep 进程别立马退出,然后让它们 & 在后台运行。ps 后我们可以看到这三个进程是兄弟进程,而 13079 一定是 bash。

系统就是 pipe 管道 1 和 管道 2,通过 for 循环 fork() 三个进程(如果是父进程就继续 fork),它们都能打开之前的两个管道文件,然后它们三个进程再关闭对应的读写端形成一条单身的数据流。通过 | 就可以实现 sleep 10000 到 sleep 20000 或者其它命令之间进程间通信。换而言之,我们曾经使用到的 | 就是匿名管道。

补充:进程退出,那么曾经打开的文件也会被关闭(因为进程中保存着打开文件的相关数据结构,而进程退出后,文件就自然会被关闭)。同样,管道也是文件,所以管道的生命周期就是进程的生命周期。


怎么保证父子进程看到同一份资源呢?

我们已经对文件描述符很熟悉了,它和管道强相关的,这里要强调的是:在 struct file 之后是提供文件的方法和缓冲区的。

管道的原理:先让父进程以读和写的方式打开同一个文件(可以理解成以读方式打开一次,再以写方式打开一次)。(注意这里只是为了好理解才这样表述,实际上创建管道,它有自己独立的接口。)相当于父进程以读又以写打开一个 pipe_file 文件(把同一个文件打开两次就得到不同的文件描述符,比如说默认的 1 和 2,所以对于一个文件来说,可以以读的方式打开一次又以写的方式打开一次,不过一般是读方式或写方式其中一种。即便同时打开,用的也是同一个接口。这个过程就称为创建管道的过程。

那么管道有了,下面就需要通信数据,所以父进程 fork() 创建子进程(强调一下,子进程是一个独立的进程,有自己独立的地址空间、页表、文件描述符表,代码共享,数据各自私有,但是结构中的大部分数据都是以父进程为模板)(与进程强相关的都会被拷贝,与文件相关的不变),所以子进程文件描述符表中写入的内容和父进程是一样的,最重要的是,曾经父进程对应打开的 pipe_file 文件,现在子进程中的 3 号 4 号文件描述符也指向 pipe_file 文件。也就能说明,为什么父子进程都 printf,结果都是向显示屏打印。


这就是进程间通信的第一步:保证不同的进程看到同一份资源,这份资源就是系统提供的一段内存区域,那么我们就可以理解父进程通过 3 或 4 号文件描述符往管道中对应读写的数据就在这个文件对应的缓冲区中,而子进程也可以通过 3 或 4 号文件描述符往管道中读写数据。

那么对于地址空间、文件描述符表等数据结构而言,虽然父子进程不共享,但是文件描述符表中的内容是一样的,那么也就意味着父子进程能够指向同一份文件。

管道的本质就是文件,当然,管道和文件也有差别,比如说文件是需要刷新到磁盘上的,而管道通信的临时文件不需要刷新到磁盘上。


管道只能进行单向数据通信

这也就意味着要么是父进程写、子进程读,要么是子进程写、父进程读。总之一个管道只能进行单向数据通信,如果要双向通信就只能建立多个管道。

如果想让父进程写、子进程读,就关闭父进程的读、子进程的写;如果想让子进程写、父进程读,就关闭子进程的读、父进程的写。父子进程各自关闭不需要的文件描述符就可以达到构建单向通信信道的目的。

在构建单向信道时,父子进程到最后都要关闭一个文件描述符,那为什么曾经还要打开呢?

根本原因:如果父进程只以读或只以写的方式打开这个文件,那么 fork() 创建子进程后只有对应的读或者只有写,那么就会造成父子进程要么都是读,要么都是写,这样就不能完成管道的单向通信。

还有一个原因:我们需要灵活的控制父子进程来完成读写通信,所以最终是父进程写、子进程读,还是子进程写、父进程读,这完全取决于我们的应用场景。

对应的一组写和读可以不关闭吗?

虽然这样也没错,不关闭也可以达到管道的单向通信。不过一般建议还是要关闭其中一个,因为一方面证明了管道的单向通信这样的特性,另一方面主要是为了防止我们误操作。当然,我们也无法确定各种操作系统对于管道的支持情况,所以最好还是按照标准规范。

为什么管道在设计时只支持单向通信?

这是与文件系统强相关的,如果能设计双向通信的话人家早就这么做了。无法支持双向通信的原因大概率跟文件的读写位置有关,一个文件的读写位置只有一个,如果要实现管道双向通信就一定要让双方既能读又能写,所以读写位置必须是两对,那么就需要修改文件系统,这样反而更麻烦,不如直接创建两个管道即可。

注意:并不是所有文件都可以被当作管道,但是管道确实又是一种文件。比如:touch 一个 test.txt,然后让两个没有任何关系的进程一个以写方式打开,一个以读方式打开。这样显然是比较困难的,虽然两进程可以看到同一个文件,但这样就需要写进程把数据刷新到磁盘,读进程再从磁盘中读取,这并不是系统想支持的通信方案。通信一定要考虑成熟、稳定且高效。


3、实现进程间通信(图 + demo代码 —— 站在文件描述符角度深度理解管道

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

谁调用就让谁以读写的方式打开一个文件,不需要指定文件名。

pipe 是我们要认识的一 个创建匿名管道的系统调用接口

pipe 的参数是一个具有 2 个参数的数组,我们都知道数组传参会降维成指针。这里 pipe 的参数是一个输出型参数,说白了就是不需要传入什么参数,而是在需要调用你的时候再拿回什么。我们可以通过这个参数拿到打开的管道文件的 fd,这个数组有两个参数,这意味着它会拿到 2 个 fd,分别是 read、write。不妨思考一下,它在底层无非就是让父进程以读和写方式分别打开一个文件,然后得到两个文件描述符。据经验判断,默认会拿到的 fd 是 3 和 4。


(1)父进程创建管道


(2)父进程 fork 子进程


(3)父进程写,子进程读,通常 fd[0] 对应 read,fd[1] 对应 write,子进程关闭 fd[1],父进程关闭 fd[0],再让父进程等待子进程


(4)父子进程实现通信

  1. '\0' 是 C 语言中的规定,不是文件的规定,管道也是文件,不需要给文件描述符写入 '\0',所以子进程在 write 的时候不要写 '\0',父进程在往 buffer 里读入数据的时候需要预留一个位置给 '\0'。
  2. 我们必然不可能把 '\0' 写入文件中,也不可能从文件中读取 '\0'。
  3. read 的返回值 read 成功就返回它读到了多少个字节,0 表示读到文件结尾,-1 表示出错。写端不仅仅写,在写完后还会把写的文件描述符关闭,此时另一端再读就会读到 0。如果返回值大于 0,则读取成功,并追加 '\0'。如果返回值等于 0,则子进程不再继续写入了,而是关闭写文件描述符并退出。如果是其它情况,那么 read 失败,这里暂且不做处理。此时子进程不断的往管道写入数据,父进程不断的往管道读入数据到 buffer 并打印,每次循环都把 buffer 中的内容清空,以验证父进程的打印数据一定是从子进程中来的(读端从管道中成功读取数据之后,管道中的数据就会被置为无效,下次再写就会覆盖,后面会讲生产者消费模型)。
  4. 建议在父子进程通信完之后关闭文件对应的文件描述符。
为什么不选择定义全局 buffer 来进行通信呢?

因为有写时拷贝的存在,父子进程需要保证各自数据的私有性,再怎么样也无法更改通信。


【Linux 系统】进程间通信(匿名管道 & 命名管道)-- 详解(下)https://developer.aliyun.com/article/1515603?spm=a2c6h.13148508.setting.18.11104f0e63xoTy

相关文章
|
21天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
47 1
|
9天前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
62 13
|
16天前
|
SQL 运维 监控
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
|
24天前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
29天前
|
缓存 算法 Linux
Linux内核的心脏:深入理解进程调度器
本文探讨了Linux操作系统中至关重要的组成部分——进程调度器。通过分析其工作原理、调度算法以及在不同场景下的表现,揭示它是如何高效管理CPU资源,确保系统响应性和公平性的。本文旨在为读者提供一个清晰的视图,了解在多任务环境下,Linux是如何智能地分配处理器时间给各个进程的。
|
1月前
|
网络协议 Linux 虚拟化
如何在 Linux 系统中查看进程的详细信息?
如何在 Linux 系统中查看进程的详细信息?
93 1
|
1月前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
5月前
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能
|
5月前
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
192 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
|
4月前
|
算法 Linux 调度
探索进程调度:Linux内核中的完全公平调度器
【8月更文挑战第2天】在操作系统的心脏——内核中,进程调度算法扮演着至关重要的角色。本文将深入探讨Linux内核中的完全公平调度器(Completely Fair Scheduler, CFS),一个旨在提供公平时间分配给所有进程的调度器。我们将通过代码示例,理解CFS如何管理运行队列、选择下一个运行进程以及如何对实时负载进行响应。文章将揭示CFS的设计哲学,并展示其如何在现代多任务计算环境中实现高效的资源分配。