关于进程间通信的一点点知识

前言

不知道你们有没有被问到过进程间是如何通信的,反正我是在刚毕业的时候被多次问到过,而当时我就知道个共享内存和管道,所以接受进程通信的恐惧吧哈哈哈哈。

概述

什么是进程通信?

由于每个进程都是相对独立运行的,且每个用户请求都可能导致多个进程在操作系统中运行,如果多个进程之间需要协作完成任务,那么进程间可能就需要进行相互通信获取数据,这种通信方式就称作为进程间通信(Inter-process communication IPC)。

维基百科定义

我们再看下维基百科是如何定义 IPC 的

Inter process communication (IPC) is used for exchanging data between multiple threads in one or more processes or programs. The Processes may be running on single or multiple computers connected by a network. The full form of IPC is Inter-process communication.

从定义上可以主要分为两类即本地进程和远程进程,如果是本地进程那么我们直接交换数据就可以了,如果是远程进程的话,那建立网络连接再交换不就行了吗,是不是很简单,其实事实并非如此。

为什么进程间需要通信

为啥要通信?因为只要进程涉及到协作,那么两个进程必然需要进行通信,因为不通信的话,那么基本不能完整的完成一个任务。

不知道大家刷 B 站,如果经常刷的话,可能就会看到这两个梗

在秀恩爱(撒狗粮)的视频下面大概率都有这样类似的评论:”男主什么时候搬出去,我好搬进去“ 的梗
还有大部分的视频都会有”下次一定“的弹幕

这两个例子不够典型,但是可以帮助理解进程协作的两个典型场景,一个是”互斥“,一个是”同步“。

第一个”互斥“,就是一个进程/线程要独占资源,哈哈哈,我不知道第一个例子你们能理解不,如果理解不了,那就把男主的资源想象为房子(或者女朋友哈哈哈哈),只有男主搬出去,另一个进程才能搬进来(或者成为该女朋友的对象哈哈哈哈),这个就称为”互斥“。

第二个例子是”下次一定“,即我安排了我下次的点赞、投币行为的时间,这种叫做”同步“,就是安排好接下来的进程/线程执行的顺序。

进程通信(IPC)

在类 Unix 系统中可以使用的 IPC 方法有很多种,从处理机制的角度看,它们可以分为三大类:基于通信的 IPC 方法、基于信号的 IPC 方法以及基于同步的 IPC 方法。其中,基于通信的 IPC 方法又分为以数据传输为手段的 IPC 方法和以共享内存为手段的 IPC 方法,前者包括了管道、命名管道、消息队列、 SOCKET以及 RPC。以共享内存为手段的 IPC 方法主要以共享内存为代表(它是最快的 IPC 方法)。基于信号的 IPC 方法就是我们常说的操作系统的信号机制,它是唯一的一种异步 IPC 方法。在基于同步的 IPC 方法中,最重要的就是信号量。

管道(PIPE)

管道是一种半双工(单向)的通信方式,只能用于父进程与子进程以及同祖先的子进程之间的通信。举一个常用的例子,我们常常会通过 grep 命令过滤我们想要的看的数据,一般会这么用:

image.png

管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序将进程数据写入缓冲区,另一端进程则顺序读取数据,该缓冲区可以看作一个循环队列,读和写的位置都是自动增加的,一个数据只能被读取一次,读出以后数据将不存在于缓冲区中。管道也可以看成一个文件,但不是普通的文件,也不属于任何的文件系统,管道自己构成一个文件系统,存在于内存中。

image.png

虽然管道很简单,一端写入数据另一端读出数据,但是也有一定的局限性:

  • 半双工(单向),数据只能向一个方向流动
  • 管道只能在具有共同的父进程之间的两个进程使用
  • 需要双方通信时,需要建立两个管道

因为管道是半双工的,也就是说进程 A 可以将数据传输到进程 B,而进程 B 要将数据传输到进程 A,那就需要建立另外一个管道来实现。在默认情况下,所有管道都是未命名的,这就意味着当没有进程使用管道时,管道则被移除(从内存中删除)。

命名管道(FIFO)

在默认管道中只能是两个具有血缘关系的进程才能进行使用,而命名管道则突破这个限制。FIFO 不同于管道之处在于它提供一个路径与之关联,以 FIFO 的文件形式存在于系统中,只要可以访问该路径的权限,那么就能够彼此通过 FIFO 进行通信,所以,通过 FIFO 不相关的进程也能交换数据。

若没有进程使用该命名管道时,该命名管道则会被从内存/缓冲区中移除,但并不会永久消失,因为命名管道存在的形式是被持久化存在磁盘系统中的,这一点与管道是有一定的区别。

命名管道同管道相同也是半双工的通信方式,也就是两个进行若需要相互传输数据需要建立两个命名管道进行传输。

消息队列

消息队列是在两个进程之间传递数据的一种简单有效的方式,每个数据块都有一个特定的类型,接收方可以根据类型来有选择地接收数据,有点类似于邮箱,当我接收到数据时就保存起来,当某个进程需要一种类型的数据时就去消息队列中获取即可。

消息队列的优点

  • 可以通过发送消息来避免命名管道的同步和阻塞问题
  • 可以用一些方法来提前查看紧急消息

消息队列的缺点

  • 与管道一样,每个数据块都有一个最大长度的限制
  • 系统中所有队列包含的数据块的总长度也有一个上限

image.png

SOCKET

socket 一般我们称为套接字(计算机专业的小伙伴应该对这个词很熟悉吧),socket 也是一种 IPC 方法,但是与其他 IPC 机制不同的是,socket 通信机制不需要两个进程必须在同一个计算机系统中,它则是以网络连接的形式让多个进程建立通信并相互传递数据。

socket 会设置到大量的 TCP/IP 协议栈的知识,而本篇文章只介绍进程通信,如果有小伙伴对 socket 感兴趣的话,后面我可以单独写一篇关于 socket 的文章(或者可以自行查找相关的资料哈哈哈)

共享内存

共享内存是最高效的 IPC 机制,因为它不涉及到进程之间的任何数据传输,因为它共享一个物理内存的位置,多个进程可以在该位置进行读写操作,这些进程通过将本地进程映射到共享物理内存位置(通过指针或者其他方法)来实现。这种高效率带来的问题是,我们必须用其他辅助手段来同步进程对共享内存的访问,否则就会产生竞态条件,因此,共享内存通常和其他进程通信方式一起使用。

竞态条件:两个进程竞争同一资源时,如果对资源的访问顺序敏感,就被称存在竞态条件

虽然共享内存是最高效的 IPC 机制,但还是有一定的局限性:使用共享内存的通信方式的进程必须处于同一计算机系统,有物理内存可以共享才行。

image.png

共享内存和消息队列,管道通信的区别:

消息队列,管道数据传递方式一般为:

  • 获取输入
  • 通过消息队列,管道写入数据,通常需要从进程拷贝到内核
  • 从内核拷贝到进程
  • 进程在拷贝到输出文件

共享内存数据传递方式一般为:

  • 将数据从文件中输入到共享内存中
  • 从共享内存中输出到文件中

所以共享内存不涉及到内核操作,而且只需要两步即可完成数据的传输,所以相对来说共享内存是最高效的 IPC 机制。

信号

操作系统信号(signal)是 IPC 中唯一一种异步的通信方法,它的本质是用软件来模拟硬件的终端机制,信号用来通知某个进程有某个事件发生了。例如,在终端中按下 cmd+c 则会停止正在运行的程序,以及我们常用的 kill 命令同样有信号的参与,如:kill -9 pid,( 不知道大家在用的时候知不知道这个 -9 是什么意思,等下会有介绍到)。

与其他 IPC 方式不同的是,信号属于不精确通信,信号只能告诉进程大概发生了什么事情,但是不能准确的告诉进程详细的细节信息,还有就是,进程间的通信并不完全是为了数据交换才进行 IPC,信号就是只会告诉对方需要作出相应的事件即可。

在 Linux 系统中每一个信号都有一个 SIG 为前缀的名字,例如 SIGINT、SIGKILL 等等,但是在操作系统内部,这些信号都是由正整数来表示,这些正整数被称为信号编号,例如一个进程不能正常退出,那我们就通过进程ID(PID)进行杀掉,则用到了 kill -9 pid 这个命令,而 9 就是信号编号,对应的信号就是 SIGKILL(在 MacOS 中对应的则是 KILL)。

image.png

信号量

当多个进程同时访问系统上的某个资源时,比如同时写一个数据库的某条记录,或者同时修改一个文件时,就需要考虑进程的同步问题,以确保任一时刻只有一个进程可以拥有对资源的独占式访问/操作。通常,程序对共享资源的访问代码只是很短的一段,但就是这一段代码引发了进程之间的竞态条件。这段代码则被称为关键代码段,或者临界区。对进程同步,也就是确保任一时刻只有一个进程能进入关键代码段。

信号量与信号完全是两种概念,信号量是一种特殊的变量,它只能取自然数值并且只支持两种操作:等待和信号。在类 Unix 系统中,等待和信号都已经具有特殊含义了,所以对信号量的这两种操作更常用P、V操作来称呼。这两个字母来自荷兰语单词 passeren(传递,进入临界区)和 vrijgeven(释放,退出临界区)。假设有信号量 SV,则对它的 P、V操作如下:

  • P(SV):如果SV的值大于 0,就将它减去1;如果 SV 的值为 0,则挂起进程的执行
  • V(SV):如果有其他进程因等待 SV 而挂起,那么就唤醒;如果没有,则将 SV 加 1信号量的取值可以是任何自然数,但常用的、最简单的信号量是二进制信号量,它只能取 0 和 1 这两个值,使用二进制信号量同步两个进程,以确保关键代码段的独占式访问的一个典型例子:

img

在图中,当关键代码可用时,二进制信号量 SV 的值为 1,进程 A 和 B 都有机会进入关键代码段。如果此时进程 A 执行了 P(SV)操作将 SV 减 1,则进程 B 若在执行 P(SV)操作就会被挂起,直到进程 A 离开关键代码段,并执行 V(SV)操作将 SV 加 1,关键代码段才重新变得可用。如果此时进程 B 因为等待 SV 而处于挂起状态,则它将被唤醒,并进入关键代码段。

信号量,有点类似于锁的概念

总结

这篇文章主要介绍了为什么需要 IPC,以及 IPC 的几种方式,理论知识较多,已经尽可能的通过一些图来辅助理解这些知识了,其实也不是很难理解吧哈哈哈。

如果我们把进程的底层原理理解明白(知道进程的大概概念也行),那么我们在编程中就有可能写出高可用的的并发编程代码(啊,理想状态),而且还是现在这个张口闭口都是高并发的时代,不谈并发都不是”合格的程序员“的时代(这是一个”合格程序员“对我说的。。挺讽刺的),进程则是支撑并发编程的基础啦。

所以总的来说,学习进程的底层原理会大大的帮助我们对并发编程的理解。

参考