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