进程间的通信方式:管道、消息队列、共享内存、信号量、socket

文章目录

  • 为什么需要进程通信IPC
  • 进程通信实现方式
    • 总结
    • 管道(效率低)
    • 消息队列(效率高但开销大)
    • 共享内存(效率高开销小但是可能地址冲突)
    • 信号量(PV操作)
    • 信号(以上都是同步通信,信号是异步)
    • Socket(以上都是单台主机内)
      • 针对 TCP 协议通信的 socket 编程模型
      • 针对 UDP 协议通信的 socket 编程模型

为什么需要进程通信IPC

互斥导致了异步,保证同步需要进程通信。
进程间的通信方式:管道、消息队列、共享内存、信号量、socket_第1张图片

进程通信实现方式

总结

管道
Linux 内核提供了不少进程间通信的方式,其中最简单的方式就是管道,管道分为「匿名管道」和「命名管道」

不管是匿名管道还是命名管道,通信流程都是:一个进程将数据缓存在内核中,另一个进程从内核中获取数据,同时通信数据都遵循先进先出原则。

  • 匿名管道
    shell 命令中的「|」竖线就是匿名管道,通信的数据是无格式的流,并且大小受限,通信的方式是单向的,数据只能在一个方向上流动。
    如果要双向通信,需要创建两个管道
    并且匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。

  • 命名管道
    突破了匿名管道只能在亲缘关系进程间的通信限制,毫无关系的进程可以进行通信。

消息队列
消息队列克服了管道通信效率低的问题
实现原理:消息队列实际上是保存在内核的「消息链表」中,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。

自身的问题:消息队列通信的开销是很大的,因为每次数据的写入和读取都需要经过用户态与内核态之间的拷贝

共享内存(虚拟内存)
共享内存可以解决消息队列通信中开销过大的问题。
它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,是最快的进程间通信方式
但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据冲突

信号量
信号量在共享内存的基础上,规定进程之间必须互斥访问,以确保任何时刻只能有一个进程访问共享资源,避免共享内存导致数据冲突的问题。

信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作

信号
信号与信号量的名字虽然相似,但功能一点儿都不一样。

上面说的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。
信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程

信号事件的来源:

硬件来源(如键盘 Cltr+C )
软件来源(如 kill 命令)

Socket
前面说到的通信机制,都是工作于同一台主机,如果要实现不同主机的进程之间相互通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,
一个是基于 TCP 协议的通信方式,
一个是基于 UDP 协议的通信方式,
一个是本地进程间通信方式。

管道(效率低)

是什么?
所谓的管道,就是内核里面的一串缓存。从管道的一端写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。同时通信数据都遵循先进先出原则,另外,管道传输的数据是无格式的流且大小受限
特点:
管道这种通信方式效率低,不适合进程间频繁地交换数据。
管道的种类:
1、匿名管道
例如:ps auxf | grep mysql,命令行里的「|」竖线就是一个管道,它的功能是将前一个命令(ps auxf)的输出,作为后一个命令(grep mysql)的输入,从这功能描述,可以看出管道传输数据是单向的,如果想相互通信,我们需要创建两个管道才行。上面这种管道是没有名字,所以「|」表示的管道称为匿名管道,用完了就销毁。
2、命名管道(FIFO)
因为数据是先进先出的传输方式。

通信范围:

  • 对于匿名管道,它的通信范围是存在父子关系的进程。因为管道没有实体,也就是没有管道文件,只能通过 fork 来复制父进程文件描述符 fd 来达到通信的目的。
  • 对于命名管道,不相关的进程间也能相互通信。因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。

不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,

管道原理
匿名管道的创建,需要通过下面这个系统调用:

int pipe(int fd[2])

这里表示创建一个匿名管道,并返回了两个描述符,一个是管道的读取端描述符 fd[0],另一个是管道的写入端描述符 fd[1]。注意,这个匿名管道是特殊的文件,只存在于内存,不存于文件系统中。
进程间的通信方式:管道、消息队列、共享内存、信号量、socket_第2张图片
再看在 shell 里面执行 A | B命令是怎么的?如下:
进程间的通信方式:管道、消息队列、共享内存、信号量、socket_第3张图片

消息队列(效率高但开销大)

前面说到管道的通信方式是效率低的,因此管道不适合进程间频繁地交换数据。
对于这个问题,消息队列的通信模式就可以解决。比如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。

特点:
消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体,消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。
如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。

生命周期:
消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而前面提到的匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。

缺点:

  • 一是通信不及时
  • 二是数据大小有限制,消息队列不适合大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限

开销:
消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。

消息队列面对的问题

消息传递系统现在面临着许多信号量和管程所未涉及的问题和设计难点,尤其对那些在网络中不同机器上的通信状况。

例如:

1、消息丢失
2、确认消息丢失
3、身份验证
身份验证也是一个问题,比如客户端怎么知道它是在与一个真正的文件服务器通信,从发送方到接收方的信息有可能被中间人所篡改

共享内存(效率高开销小但是可能地址冲突)

消息队列的读取和写入的过程,都会有发生用户态与内核态之间的消息拷贝的资源消耗,开销大。而共享内存的方式就很好的解决了这一问题。

现代操作系统,对于内存管理,采用的是虚拟内存技术,也就是每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。所以,即使进程 A 和 进程 B 的虚拟地址是一样的,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响。
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样进程A写入的东西,进程B马上就能看到了,不需要拷贝来拷贝去,大大提高了进程间通信的速度。

信号量(PV操作)

用了共享内存通信方式可以解决消息队列开销大的问题,带来新的问题,那就是如果多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。

信号量在共享内存的基础上,设置进程之间必须互斥访问,达到保护共享资源,以确保任何时刻只能有一个进程访问共享资源,避免共享内存导致数据冲突的问题。

信号量就是一个整型计数器,记录资源的数量,主要用于实现进程间的互斥与同步。

PV操作:控制信号量的两种原子操作

P操作是用在进入共享资源之前对资源数-1
V 操作是用在离开共享资源之后对资源数+1
这两个操作是必须成对出现的

  • 一个是 P 操作,这个操作会把信号量减去 -1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。

  • 另一个是 V 操作,这个操作会把信号量加上 +1,相加后如果信号量 <= 0,则表明当前仍有阻塞中的进程,于是会挑一个阻塞进程将其唤醒;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;

信号(以上都是同步通信,信号是异步)

上面说的进程间通信,都是常规状态下的工作模式。对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。
信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程

信号事件的来源:

  • 硬件来源(如键盘 Cltr+C )
  • 软件来源(如 kill 命令)
    在 Linux 操作系统中, 为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义。我们可以通过 kill -l 命令,查看所有的信号:
    进程间的通信方式:管道、消息队列、共享内存、信号量、socket_第4张图片

运行在 shell 终端的进程,我们可以通过键盘输入某些组合键的时候,给进程发送信号。例如

  • Ctrl+C 产生 SIGINT 信号,表示终止该进程;

  • Ctrl+Z 产生 SIGTSTP 信号,表示停止该进程,但还未结束;

如果进程在后台运行,可以通过 kill 命令的方式给进程发送信号,但前提需要知道进程 PID 号
例如:kill -9 1050 ,表示给 PID 为 1050 的进程发送 SIGKILL 信号

Socket(以上都是单台主机内)

前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。

针对 TCP 协议通信的 socket 编程模型

进程间的通信方式:管道、消息队列、共享内存、信号量、socket_第5张图片
服务端和客户端初始化 socket,得到文件描述符;

服务端调用 bind,将绑定在 IP 地址和端口;

服务端调用 listen,进行监听;

服务端调用 accept,等待客户端连接;

客户端调用 connect,向服务器端的地址和端口发起连接请求;

服务端 accept 返回用于传输的 socket 的文件描述符;

客户端调用 write 写入数据;服务端调用 read 读取数据;

客户端断开连接时,会调用 close,那么服务端 read 读取数据的时候,就会读取到了 EOF,待处理完数据后,服务端调用 close,表示连接关闭。

这里需要注意的是,服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。

所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。

成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。

针对 UDP 协议通信的 socket 编程模型

进程间的通信方式:管道、消息队列、共享内存、信号量、socket_第6张图片
UDP 是没有连接的,所以不需要三次握手,也就不需要像 TCP 调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口号,因此也需要 bind。

对于 UDP 来说,不需要要维护连接,那么也就没有所谓的发送方和接收方,甚至都不存在客户端和服务端的概念,只要有一个 socket 多台机器就可以任意通信,因此每一个 UDP 的 socket 都需要 bind。

另外,每次通信时,调用 sendto 和 recvfrom,都要传入目标主机的 IP 地址和端口。

你可能感兴趣的:(Linux操作系统,java,linux,服务器)