高频考点,六大进程通讯机制总结

 

🎓 尽人事,听天命。博主东南大学硕士在读,热爱健身和篮球,乐于分享技术相关的所见所得,关注公众号 @ 飞天小牛肉,第一时间获取文章更新,成长的路上咱们一块儿进步git

🎁 本文已收录于 「CS-Wiki」Gitee 官方推荐项目,现已累计 1.4k+ star,致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习面试

🍉 若是各位小伙伴春招秋招没有拿得出手的项目的话,能够参考我写的一个项目「开源社区系统 Echo」Gitee 官方推荐项目,目前已累计 250+ star,基于 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 并提供详细的开发文档和配套教程。公众号后台回复 Echo 能够获取配套教程,目前尚在更新中算法

 

初学操做系统的时候,我就一直懵逼,为啥进程同步与互斥机制里有信号量机制,进程通讯里又有信号量机制,而后你再看网络上的各类面试题汇总或者博客,你会发现不少都是千篇一概的进程通讯机制有哪些?进程同步与互斥机制鲜有人问津。看多了我都想把 CSDN 屏了.....,最后知道真相的我只想说为啥不能一篇博客把东西写清楚,没头没尾真的浪费时间。数据库

但愿这篇文章可以拯救某段时间和我同样被绕晕的小伙伴。上篇文章我已经讲过进程间的同步与互斥机制,各位小伙伴看完这个再来看进程通讯比较好。编程

全文脉络思惟导图以下:后端

1. 什么是进程通讯

顾名思义,进程通讯( InterProcess Communication,IPC)就是指进程之间的信息交换。实际上,进程的同步与互斥本质上也是一种进程通讯(这也就是待会咱们会在进程通讯机制中看见信号量和 PV 操做的缘由了),只不过它传输的仅仅是信号量,经过修改信号量,使得进程之间创建联系,相互协调和协同工做,可是它缺少传递数据的能力数组

虽然存在某些状况,进程之间交换的信息量不多,好比仅仅交换某个状态信息,这样进程的同步与互斥机制彻底能够胜任这项工做。可是大多数状况下,进程之间须要交换大批数据,好比传送一批信息或整个文件,这就须要经过一种新的通讯机制来完成,也就是所谓的进程通讯。安全

再来从操做系统层面直观的看一些进程通讯:咱们知道,为了保证安全,每一个进程的用户地址空间都是独立的,通常而言一个进程不能直接访问另外一个进程的地址空间,不过内核空间是每一个进程都共享的,因此进程之间想要进行信息交换就必须经过内核网络

下面就来咱们来列举一下 Linux 内核提供的常见的进程通讯机制:数据结构

  • 管道(也称做共享文件)

  • 消息队列(也称做消息传递)

  • 共享内存(也称做共享存储)

  • 信号量和 PV 操做

  • 信号

  • 套接字(Socket)

2. 管道

匿名管道

各位若是学过 Linux 命令,那对管道确定不陌生,Linux 管道使用竖线 | 链接多个命令,这被称为管道符。

$ command1 | command2

以上这行代码就组成了一个管道,它的功能是将前一个命令(command1)的输出,做为后一个命令(command2)的输入,从这个功能描述中,咱们能够看出管道中的数据只能单向流动,也就是半双工通讯,若是想实现相互通讯(全双工通讯),咱们须要建立两个管道才行。

另外,经过管道符 | 建立的管道是匿名管道,用完了就会被自动销毁。而且,匿名管道只能在具备亲缘关系(父子进程)的进程间使用,。也就是说,匿名管道只能用于父子进程之间的通讯

在 Linux 的实际编码中,是经过 pipe 函数来建立匿名管道的,若建立成功则返回 0,建立失败就返回 -1:

int pipe (int fd[2]);

该函数拥有一个存储空间为 2 的文件描述符数组:

  • fd[0] 指向管道的读端,fd[1] 指向管道的写端

  • fd[1] 的输出是 fd[0] 的输入

粗略的解释一下经过匿名管道实现进程间通讯的步骤:

1)父进程建立两个匿名管道,管道 1(fd1[0]fd1[1])和管道 2(fd2[0]fd2[1]);

由于管道的数据是单向流动的,因此要想实现数据双向通讯,就须要两个管道,每一个方向一个。

2)父进程 fork 出子进程,因而对于这两个匿名管道,子进程也分别有两个文件描述符指向匿名管道的读写两端;

3)父进程关闭管道 1 的读端 fd1[0] 和 管道 2 的写端 fd2[1],子进程关闭管道 1 的写端 fd1[1] 和 管道 2 的读端 fd2[0],这样,管道 1 只能用于父进程写、子进程读;管道 2 只能用于父进程读、子进程写。管道是用环形队列实现的,数据从写端流入从读端流出,这就实现了父子进程之间的双向通讯。

看完上面这些讲述,咱们来理解下管道的本质是什么:对于管道两端的进程而言,管道就是一个文件(这也就是为啥管道也被称为共享文件机制的缘由了),但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,而且只存在于内存中。

简单来讲,管道的本质就是内核在内存中开辟了一个缓冲区,这个缓冲区与管道文件相关联,对管道文件的操做,被内核转换成对这块缓冲区的操做

有名管道

匿名管道因为没有名字,只能用于父子进程间的通讯。为了克服这个缺点,提出了有名管道,也称作 FIFO,由于数据是先进先出的传输方式。

所谓有名管道也就是提供一个路径名与之关联,这样,即便与建立有名管道的进程不存在亲缘关系的进程,只要能够访问该路径,就可以经过这个有名管道进行相互通讯。

使用 Linux 命令 mkfifo 来建立有名管道:

$ mkfifo myPipe

myPipe 就是这个管道的名称,接下来,咱们往 myPipe 这个有名管道中写入数据:

$ echo "hello" > myPipe

执行这行命令后,你会发现它就停在这了,这是由于管道里的内容没有被读取,只有当管道里的数据被读完后,命令才能够正常退出。因而,咱们执行另一个命令来读取这个有名管道里的数据:

$ cat < myPipe
hello

3. 消息队列

能够看出,管道这种进程通讯方式虽然使用简单,可是效率比较低,不适合进程间频繁地交换数据,而且管道只能传输无格式的字节流。为此,消息传递机制(Linux 中称消息队列)应用而生。好比,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就能够正常返回了,B 进程在须要的时候自行去消息队列中读取数据就能够了。一样的,B 进程要给 A 进程发送消息也是如此。

消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构。若是进程从消息队列中读取了某个消息,这个消息就会被从消息队列中删除。对比一下管道机制:

  • 消息队列容许一个或多个进程向它写入或读取消息。

  • 消息队列能够实现消息的随机查询,不必定非要以先进先出的次序读取消息,也能够按消息的类型读取。比有名管道的先进先出原则更有优点。

  • 对于消息队列来讲,在某个进程往一个队列写入消息以前,并不须要另外一个进程在该消息队列上等待消息的到达。而对于管道来讲,除非读进程已存在,不然先有写进程进行写入操做是没有意义的。

  • 消息队列的生命周期随内核,若是没有释放消息队列或者没有关闭操做系统,消息队列就会一直存在。而匿名管道随进程的建立而创建,随进程的结束而销毁。

须要注意的是,消息队列对于交换较少数量的数据颇有用,由于无需避免冲突。可是,因为用户进程写入数据到内存中的消息队列时,会发生从用户态拷贝数据到内核态的过程;一样的,另外一个用户进程读取内存中的消息数据时,会发生从内核态拷贝数据到用户态的过程。所以,若是数据量较大,使用消息队列就会形成频繁的系统调用,也就是须要消耗更多的时间以便内核介入

4. 共享内存

为了不像消息队列那样频繁的拷贝消息、进行系统调用,共享内存机制出现了。

顾名思义,共享内存就是容许不相干的进程将同一段物理内存链接到它们各自的地址空间中,使得这些进程能够访问同一个物理内存,这个物理内存就成为共享内存。若是某个进程向共享内存写入数据,所作的改动将当即影响到能够访问同一段共享内存的任何其余进程。

集合内存管理的内容,咱们来深刻理解下共享内存的原理。首先,每一个进程都有属于本身的进程控制块(PCB)和逻辑地址空间(Addr Space),而且都有一个与之对应的页表,负责将进程的逻辑地址(虚拟地址)与物理地址进行映射,经过内存管理单元(MMU)进行管理。两个不一样进程的逻辑地址经过页表映射到物理空间的同一区域,它们所共同指向的这块区域就是共享内存

不一样于消息队列频繁的系统调用,对于共享内存机制来讲,仅在创建共享内存区域时须要系统调用,一旦创建共享内存,全部的访问均可做为常规内存访问,无需借助内核。这样,数据就不须要在进程之间来回拷贝,因此这是最快的一种进程通讯方式。

5. 信号量和 PV 操做

实际上,对具备多 CPU 系统的最新研究代表,在这类系统上,消息传递的性能实际上是要优于共享内存的,由于消息队列无需避免冲突,而共享内存机制可能会发生冲突。也就是说若是多个进程同时修改同一个共享内存,先来的那个进程写的内容就会被后来的覆盖。

而且,在多道批处理系统中,多个进程是能够并发执行的,但因为系统的资源有限,进程的执行不是一向到底的, 而是走走停停,以不可预知的速度向前推动(异步性)。但有时候咱们又但愿多个进程能密切合做,按照某个特定的顺序依次执行,以实现一个共同的任务。

举个例子,若是有 A、B 两个进程分别负责读和写数据的操做,这两个线程是相互合做、相互依赖的。那么写数据应该发生在读数据以前。而实际上,因为异步性的存在,可能会发生先读后写的状况,而此时因为缓冲区尚未被写入数据,读进程 A 没有数据可读,所以读进程 A 被阻塞。

所以,为了解决上述这两个问题,保证共享内存在任什么时候刻只有一个进程在访问(互斥),而且使得进程们可以按照某个特定顺序访问共享内存(同步),咱们就可使用进程的同步与互斥机制,常见的好比信号量与 PV 操做。

进程的同步与互斥实际上是一种对进程通讯的保护机制,并非用来传输进程之间真正通讯的内容的,可是因为它们会传输信号量,因此也被归入进程通讯的范畴,称为低级通讯

下面的内容和上篇文章【看完了进程同步与互斥机制,我终于完全理解了 PV 操做】中所讲的差很少,看过的小伙伴可直接跳到下一标题。

信号量其实就是一个变量 ,咱们能够用一个信号量来表示系统中某种资源的数量,好比:系统中只有一台打印机,就能够设置一个初值为 1 的信号量。

用户进程能够经过使用操做系统提供的一对原语来对信号量进行操做,从而很方便的实现进程互斥或同步。这一对原语就是 PV 操做:

1)P 操做:将信号量值减 1,表示申请占用一个资源。若是结果小于 0,表示已经没有可用资源,则执行 P 操做的进程被阻塞。若是结果大于等于 0,表示现有的资源足够你使用,则执行 P 操做的进程继续执行。

能够这么理解,当信号量的值为 2 的时候,表示有 2 个资源可使用,当信号量的值为 -2 的时候,表示有两个进程正在等待使用这个资源。不看这句话真的没法理解 V 操做,看完顿时如梦初醒。

2)V 操做:将信号量值加 1,表示释放一个资源,即便用完资源后归还资源。若加完后信号量的值小于等于 0,表示有某些进程正在等待该资源,因为咱们已经释放出一个资源了,所以须要唤醒一个等待使用该资源(就绪态)的进程,使之运行下去。

我以为已经讲的足够通俗了,不过对于 V 操做你们可能仍然有困惑,下面再来看两个关于 V 操做的问答:

问:信号量的值 大于 0 表示有共享资源可供使用,这个时候为何不须要唤醒进程

答:所谓唤醒进程是从就绪队列(阻塞队列)中唤醒进程,而信号量的值大于 0 表示有共享资源可供使用,也就是说这个时候没有进程被阻塞在这个资源上,因此不须要唤醒,正常运行便可。

问:信号量的值 等于 0 的时候表示没有共享资源可供使用,为何还要唤醒进程

答:V 操做是先执行信号量值加 1 的,也就是说,把信号量的值加 1 后才变成了 0,在此以前,信号量的值是 -1,即有一个进程正在等待这个共享资源,咱们须要唤醒它。

信号量和 PV 操做具体的定义以下:

互斥访问共享内存

两步走便可实现不一样进程对共享内存的互斥访问:

  • 定义一个互斥信号量,并初始化为 1

  • 把对共享内存的访问置于 P 操做和 V 操做之间

P 操做和 V 操做必须成对出现。缺乏 P 操做就不能保证对共享内存的互斥访问,缺乏 V 操做就会致使共享内存永远得不到释放、处于等待态的进程永远得不到唤醒。

实现进程同步

回顾一下进程同步,就是要各并发进程按要求有序地运行。

举个例子,如下两个进程 P一、P2 并发执行,因为存在异步性,所以两者交替推动的次序是不肯定的。假设 P2 的 “代码4” 要基于 P1 的 “代码1” 和 “代码2” 的运行结果才能执行,那么咱们就必须保证 “代码4” 必定是在 “代码2” 以后才会执行。

若是 P2 的 “代码4” 要基于 P1 的 “代码1” 和 “代码2” 的运行结果才能执行,那么咱们就必须保证 “代码4” 必定是在 “代码2” 以后才会执行。

使用信号量和 PV 操做实现进程的同步也很是方便,三步走:

  • 定义一个同步信号量,并初始化为当前可用资源的数量

  • 在优先级较的操做的面执行 V 操做,释放资源

  • 在优先级较的操做的面执行 P 操做,申请占用资源

配合下面这张图直观理解下:

6. 信号

注意!信号和信号量是彻底不一样的两个概念

信号是进程通讯机制中惟一的异步通讯机制,它能够在任什么时候候发送信号给某个进程。经过发送指定信号来通知进程某个异步事件的发送,以迫使进程执行信号处理程序。信号处理完毕后,被中断进程将恢复执行。用户、内核和进程都能生成和发送信号。

信号事件的来源主要有硬件来源和软件来源。所谓硬件来源就是说咱们能够经过键盘输入某些组合键给进程发送信号,好比常见的组合键 Ctrl+C 产生 SIGINT 信号,表示终止该进程;而软件来源就是经过 kill 系列的命令给进程发送信号,好比 kill -9 1111 ,表示给 PID 为 1111 的进程发送 SIGKILL 信号,让其当即结束。咱们来查看一下 Linux 中有哪些信号:

7. Socket

至此,上面介绍的 5 种方法都是用于同一台主机上的进程之间进行通讯的,若是想要跨网络与不一样主机上的进程进行通讯,那该怎么作呢?这就是 Socket 通讯作的事情了(固然,Socket 也能完成同主机上的进程通讯)。

Socket 起源于 Unix,原意是插座,在计算机通讯领域,Socket 被翻译为套接字,它是计算机之间进行通讯的一种约定或一种方式。经过 Socket 这种约定,一台计算机能够接收其余计算机的数据,也能够向其余计算机发送数据。

从计算机网络层面来讲,Socket 套接字是网络通讯的基石,是支持 TCP/IP 协议的网络通讯的基本操做单元。它是网络通讯过程当中端点的抽象表示,包含进行网络通讯必须的五种信息:链接使用的协议,本地主机的 IP 地址,本地进程的协议端口,远地主机的 IP 地址,远地进程的协议端口

Socket 的本质实际上是一个编程接口(API),是应用层与 TCP/IP 协议族通讯的中间软件抽象层,它对 TCP/IP 进行了封装。它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面。对用户来讲,只要经过一组简单的 API 就能够实现网络的链接。

8. 总结

简单总结一下上面六种 Linux 内核提供的进程通讯机制:

1)首先,最简单的方式就是管道,管道的本质是存放在内存中的特殊的文件。也就是说,内核在内存中开辟了一个缓冲区,这个缓冲区与管道文件相关联,对管道文件的操做,被内核转换成对这块缓冲区的操做。管道分为匿名管道和有名管道,匿名管道只能在父子进程之间进行通讯,而有名管道没有限制。

2)虽然管道使用简单,可是效率比较低,不适合进程间频繁地交换数据,而且管道只能传输无格式的字节流。为此消息队列应用而生。消息队列的本质就是存放在内存中的消息的链表,而消息本质上是用户自定义的数据结构。若是进程从消息队列中读取了某个消息,这个消息就会被从消息队列中删除。

3)消息队列的速度比较慢,由于每次数据的写入和读取都须要通过用户态与内核态之间数据的拷贝过程,共享内存能够解决这个问题。所谓共享内存就是:两个不一样进程的逻辑地址经过页表映射到物理空间的同一区域,它们所共同指向的这块区域就是共享内存。若是某个进程向共享内存写入数据,所作的改动将当即影响到能够访问同一段共享内存的任何其余进程。

对于共享内存机制来讲,仅在创建共享内存区域时须要系统调用,一旦创建共享内存,全部的访问均可做为常规内存访问,无需借助内核。这样,数据就不须要在进程之间来回拷贝,因此这是最快的一种进程通讯方式。

4)共享内存速度虽然很是快,可是存在冲突问题,为此,咱们可使用信号量和 PV 操做来实现对共享内存的互斥访问,而且还能够实现进程同步。

5)信号和信号量是彻底不一样的两个概念!信号是进程通讯机制中惟一的异步通讯机制,它能够在任什么时候候发送信号给某个进程。经过发送指定信号来通知进程某个异步事件的发送,以迫使进程执行信号处理程序。信号处理完毕后,被中断进程将恢复执行。用户、内核和进程都能生成和发送信号。

6)上面介绍的 5 种方法都是用于同一台主机上的进程之间进行通讯的,若是想要跨网络与不一样主机上的进程进行通讯,就须要使用 Socket 通讯。另外,Socket 也能完成同主机上的进程通讯。

总结完毕!

 

🎉 关注公众号 | 飞天小牛肉,即时获取更新

  • 博主东南大学硕士在读,利用课余时间运营一个公众号『 飞天小牛肉 』,2020/12/29 日开通,专一分享计算机基础(数据结构 + 算法 + 计算机网络 + 数据库 + 操做系统 + Linux)、Java 基础和面试指南的相关原创技术好文。本公众号的目的就是让你们能够快速掌握重点知识,有的放矢。但愿你们多多支持哦,和小牛肉一块儿成长 😃

  • 并推荐我的维护的开源教程类项目: CS-Wiki(Gitee 推荐项目,现已累计 1.4k+ star), 致力打造完善的后端知识体系,在技术的路上少走弯路,欢迎各位小伙伴前来交流学习 ~ 😊

  • 若是各位小伙伴春招秋招没有拿得出手的项目的话,能够参考我写的一个项目「开源社区系统 Echo」Gitee 官方推荐项目,目前已累计 250+ star,基于 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 并提供详细的开发文档和配套教程。公众号后台回复 Echo 能够获取配套教程,目前尚在更新中。

相关文章
相关标签/搜索