面试了这么多场,“ 进程间的通信 ” 真是从不缺席,小伙伴们赶快重视起来!!

Posted 小乔不掉发

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试了这么多场,“ 进程间的通信 ” 真是从不缺席,小伙伴们赶快重视起来!!相关的知识,希望对你有一定的参考价值。

每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为 进程间通信


1、管道:

匿名管道:

  • 管道是单向的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
  • 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
  • 单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
  • 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
  • 效率低,不适合频繁交换数据

管道的实质:

  • 管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序的将数据写入缓冲区,另一端的进程则顺序的读出数据。
  • 该缓冲区可以看做是一个循环队列,读和写的位置都是自动增长的,不能随意改变,一个数据只能被读一次,读出来以后在缓冲区就不复存在了。
  • 当缓冲区读空或者写满时,有一定的规则控制相应的读进程或者写进程进入等待队列,当空的缓冲区有新数据写入或者满的缓冲区有数据读出来时,就唤醒等待队列中的进程继续读写。

管道的局限:

  • 只支持单向数据流;
  • 只能用于具有亲缘关系的进程之间;
  • 没有名字;
  • 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);
  • 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;

有名管道:

匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道

  • 有名管道 不同于匿名管道之处在于它 提供了一个路径名 与之关联,以有名管道的文件形式存在于文件系统中,这样,即使与有名管道的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过有名管道相互通信,因此,通过有名管道不相关的进程也能交换数据。值的注意的是,有名管道严格遵循 先进先出 ,对匿名管道及有名管道的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。有名管道的名字存在于文件系统中,内容存放在内存中。

总结:

  • 在 shell 里通过「|」匿名管道将多个命令连接在一起,实际上也就是创建了多个子进程,那么在我们编写 shell 脚本时,能使用一个管道搞定的事情,就不要多用一个管道,这样可以减少创建子进程的系统开销。
  • 对于匿名管道,它的通信范围是存在父子关系的进程。因为管道没有实体,也就是没有管道文件,只能通过 fork 来复制父进程 fd 文件描述符,来达到通信的目的。
  • 对于命名管道,它可以在不相关的进程间也能相互通信。因为命令管道,提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以相互通信。
  • 不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中,另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作

2、消息队列:

A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。同理,B 进程要给 A 进程发送消息也是如此。(就像平时发邮件一样,你来一封,我回一封,可以频繁沟通了。)

  • 消息队列是存放在内核中的 消息链表,每个消息队列由消息队列标识符表示。
  • 消息队列允许一个或多个进程向它写入与读取消息。
  • 管道和消息队列的通信数据都是先进先出的原则。
  • 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比FIFO更有优势。
  • 消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
  • 目前主要有两种类型的消息队列:POSIX消息队列以及System V消息队列,系统V消息队列目前被大量使用。系统V消息队列是随内核持续的,只有在内核重起或者人工删除时,该消息队列才会被删除。

缺点:

  • 消息队列 不适合大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。在 Linux 内核中,会有两个宏定义 MSGMAX 和 MSGMNB,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。
  • 消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。

与管道的比较:

管道消息队列
无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统存放在内核中
用完就被删除有在内核重启或者显示地删除一个消息队列时,该消息队列才会被真正的删除。
随进程的创建而建立,随进程的结束而销毁在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达。

3、共享内存:

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

现代操作系统,对于内存管理:采用的是 虚拟内存技术。也就是每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。所以,即使进程 A 和 进程 B 的虚拟地址是一样的,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响。

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。


4、信号量:

① 用了 共享内存通信方式,带来新的问题:那就是如果 多个进程同时修改同一个共享内存,很有可能就冲突了。例如两个进程都同时写一个地址,那先写的那个进程会发现内容被别人覆盖了。
为了防止多进程竞争共享资源,而造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被一个进程访问。正好,信号量就实现了这一保护机制。

信号量 (表示资源的数量):其实是一个 整型的计数器,主要用于 实现进程间的互斥与同步,而不是用于缓存进程间通信的数据。

② 控制信号量的方式有两种原子操作:(两个操作是必须成对出现)

  • P 操作:信号量 减去 1,相减后如果信号量 < 0,则表明资源已被占用,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使用,进程可正常继续执行。(用在进入共享资源之前)
  • V 操作:信号量 加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;(用在离开共享资源之后)
  • 信号初始化为 1,就代表着是 互斥信号量,它可以保证共享内存在 任何时刻只有一个进程在访问
  • 信号初始化为 0,就代表着是 同步信号量,它可以保证 访问者对资源的有序访问

③ 信号量与互斥量之间的区别:
(在大多数情况下,同步已经实现了互斥)

互斥量信号量
用于线程的 互斥用于线程的 同步
互斥:某一资源同时只允许一个访问者对其进行访问(无序的)同步:在互斥的基础上,通过其它机制实现访问者对资源的有序访问
互斥量值只能为 0/1信号量值可以为非负整数
一个互斥量只能用于一个资源的互斥访问信号量可以实现多个同类资源的多线程互斥和同步
互斥量的加锁和解锁必须由同一线程分别对应使用信号量可以由一个线程释放,另一个线程得到

5、信号:

① 上面的进程间通信,都是常规状态下的工作模式。对于 异常情况 下的工作模式,就需要用「信号」的方式来通知进程。

  • 信号可以在 任何时候 发给某一进程,而无需知道该进程的状态(唯一的异步通信机制)
  • 如果该进程当前并未处于执行状态,则该信号就由内核保存起来,直到该进程回复执行并传递给它为止。
  • 如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消是才被传递给进程

② Linux系统中 常用信号

  • Ctrl+C 产生 SIGINT 信号,表示终止该进程;
  • Ctrl+Z 产生 SIGTSTP 信号,表示停止该进程,但还未结束;
  • Ctrl+\\ \\ 产生 SIGQUIT信号,表示程序退出

③ 信号来源:

信号是 软件层次 上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件主要有两个来源:

  • 硬件来源:用户按键输入Ctrl+C退出、硬件异常如无效的存储访问等。
  • 软件终止:终止进程信号、其他进程调用kill函数、软件异常产生信号。

6、Socket(套接字):

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

Socket是什么呢?

  • Socket是 应用层 与 TCP/IP 协议族通信的 中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。


套接字的特性由3个属性确定,它们分别是:域、端口号、协议类型

根据创建 socket 类型的不同,通信的方式也就不同:
1、针对 TCP 协议通信的 socket 编程模型:

先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

注意:

  • 服务端调用 accept 时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。
  • 监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。
  • 成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样

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

  • UDP 是没有连接的,所以不需要三次握手,也就不需要像 TCP 调用 listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端口号,因此也需要 bind。
  • 对于 UDP 来说,不需要要维护连接,那么也就没有所谓的发送方和接收方,甚至都不存在客户端和服务端的概念,只要有一个 socket 多台机器就可以任意通信,因此每一个 UDP 的 socket 都需要 bind。
  • 每次通信时,调用 sendto 和 recvfrom,都要传入目标主机的 IP 地址和端口

3、针对 本地进程间 通信的 socket 编程模型:

  • 本地 socket 的编程接口和 IPv4 、IPv6 套接字编程接口是一致的,可以支持「字节流」和「数据报」两种协议;
  • 本地 socket 的实现效率大大高于 IPv4 和 IPv6 的字节流、数据报 socket 实现;
    对于本地字节流 socket,其 socket 类型是 AF_LOCAL 和 SOCK_STREAM。

对于本地数据报 socket,其 socket 类型是 AF_LOCAL 和 SOCK_DGRAM。

本地字节流 socket 和 本地数据报 socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端口,而是 绑定一个本地文件,这也就是它们之间的最大区别。

参考引用:
https://www.jianshu.com/p/c1015f5ffa74
https://blog.csdn.net/qq_34827674/article/details/107678226

以上是关于面试了这么多场,“ 进程间的通信 ” 真是从不缺席,小伙伴们赶快重视起来!!的主要内容,如果未能解决你的问题,请参考以下文章

面试了这么多场,“ 进程间的通信 ” 真是从不缺席,小伙伴们赶快重视起来!!

面试了这么多场,“ 进程间的通信 ” 真是从不缺席,小伙伴们赶快重视起来!!

2022-Java常问面试题总结2

linux面试

2021最新Java开发面试解答

作为字节跳动面试官,javaweb项目用到的技术