ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

【操作系统】进程间通信

2021-06-04 22:57:51  阅读:142  来源: 互联网

标签:信号量 操作系统 通信 间通信 管道 内核 进程 socket


在这里插入图片描述

每个进程的⽤户地址空间都是独⽴的,⼀般⽽⾔是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。

在这里插入图片描述

一、管道

1、管道如何创建呢,背后原理是什么?

匿名管道的创建,需要通过下⾯这个系统调⽤:

int pipe(int fd[2])

这⾥表示创建⼀个匿名管道,并返回了两个描述符,⼀个是管道的读取端描述符 fd[0] ,另⼀个是管道的写⼊端描述符 fd[1] 。注意,这个匿名管道是特殊的⽂件,只存在于内存,不存于⽂件系统中。

在这里插入图片描述

其实,所谓的管道,就是内核⾥⾯的⼀串缓存。从管道的⼀段写⼊的数据,实际上是缓存在内核中的,另⼀端读取,也就是从内核中读取这段数据。另外,管道传输的数据是⽆格式的流且⼤⼩受限。

看到这,你可能会有疑问了,这两个描述符都是在⼀个进程⾥⾯,并没有起到进程间通信的作⽤,怎么样才能使得管道是跨过两个进程的呢?

我们可以使⽤ fork 创建⼦进程,创建的⼦进程会复制⽗进程的⽂件描述符,这样就做到了两个进程各有两个「 fd[0] 与 fd[1] 」,两个进程就可以通过各⾃的 fd 写⼊和读取同⼀个管道⽂件实现跨进程通信了。

在这里插入图片描述

管道只能⼀端写⼊,另⼀端读出,所以上⾯这种模式容易造成混乱,因为⽗进程和⼦进程都可以同时写⼊,也都可以读出。那么,为了避免这种情况,通常的做法是:

  • 父进程关闭读取的 fd[0],只保留写⼊的 fd[1];
  • 子进程关闭写⼊的 fd[1],只保留读取的 fd[0];

在这里插入图片描述

所以说如果需要双向通信,则应该创建两个管道。

到这⾥,我们仅仅解析了使⽤管道进⾏⽗进程与⼦进程之间的通信,但是在我们 shell ⾥⾯并不是这样的。

在 shell ⾥⾯执⾏ A | B 命令的时候,A 进程和 B 进程都是 shell 创建出来的⼦进程,A 和 B 之间不存在⽗⼦关系,它俩的⽗进程都是 shell。

在这里插入图片描述

所以说,在 shell ⾥通过「 | 」匿名管道将多个命令连接在⼀起,实际上也就是创建了多个⼦进程,那么在我们编写 shell 脚本时,能使⽤⼀个管道搞定的事情,就不要多⽤⼀个管道,这样可以减少创建⼦进程的系统开销。

我们可以得知,对于匿名管道,它的通信范围是存在⽗⼦关系的进程。因为管道没有实体,也就是没有管道⽂件,只能通过 fork 来复制⽗进程 fd ⽂件描述符,来达到通信的⽬的。

另外,对于命名管道,它可以在不相关的进程间也能相互通信。因为命令管道,提前创建了⼀个类型为管道的设备⽂件,在进程⾥只要使⽤这个设备⽂件,就可以相互通信。

不管是匿名管道还是命名管道,进程写⼊的数据都是缓存在内核中,另⼀个进程读取数据时候⾃然也是从内核中获取,同时通信数据都遵循先进先出原则,不⽀持 lseek 之类的⽂件定位操作。

二、消息队列

前⾯说到管道的通信⽅式是效率低的,因此管道不适合进程间频繁地交换数据。

对于这个问题,消息队列的通信模式就可以解决。⽐如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。同理,B 进程要给 A 进程发送消息也是如此。

再来,消息队列是保存在内核中的消息链表,在发送数据时,会分成⼀个⼀个独⽴的数据单元,也就是消息体(数据块),消息体是⽤户⾃定义的数据类型,消息的发送⽅和接收⽅要约定好消息体的数据类型,所以每个消息体都是固定⼤⼩的存储块,不像管道是⽆格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
消息队列⽣命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会⼀直存在,⽽前⾯提到的匿名管道的⽣命周期,是随进程的创建⽽建⽴,随进程的结束⽽销毁。

消息这种模型,两个进程之间的通信就像平时发邮件⼀样,你来⼀封,我回⼀封,可以频繁沟通了。

但邮件的通信⽅式存在不⾜的地⽅有两点,⼀是通信不及时,⼆是附件也有⼤⼩限制,这同样也是消息队列通信不⾜的点。

消息队列不适合⽐较⼤数据的传输, 因为在内核中每个消息体都有⼀个最⼤⻓度的限制,同时所有队列所包含的全部消息体的总⻓度也是有上限。在 Linux 内核中,会有两个宏定义 MSGMAX 和 MSGMNB ,

它们以字节为单位,分别定义了⼀条消息的最⼤⻓度和⼀个队列的最⼤⻓度。

**消息队列通信过程中,存在⽤户态与内核态之间的数据拷⻉开销,**因为进程写⼊数据到内核中的消息队列时,会发⽣从⽤户态拷⻉数据到内核态的过程,同理另⼀进程读取内核中的消息数据时,会发⽣从内核态拷⻉数据到⽤户态的过程。

三、共享内存

消息队列的读取和写⼊的过程,都会有发⽣⽤户态与内核态之间的消息拷⻉过程。那共享内存的⽅式,就很好的解决了这⼀问题。

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

共享内存的机制,就是拿出⼀块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写⼊的东⻄,另外⼀个进程⻢上就能看到了,都不需要拷⻉来拷⻉去,传来传去,⼤⼤提⾼了进程间通信的速度。

在这里插入图片描述

四、信号量

⽤了共享内存通信⽅式,带来新的问题,那就是如果多个进程同时修改同⼀个共享内存,很有可能就冲突了。例如两个进程都同时写⼀个地址,那先写的那个进程会发现内容被别⼈覆盖了。

为了防⽌多进程竞争共享资源,⽽造成的数据错乱,所以需要保护机制,使得共享的资源,在任意时刻只能被⼀个进程访问。正好,信号量就实现了这⼀保护机制。

信号量其实是⼀个整型的计数器,主要⽤于实现进程间的互斥与同步,⽽不是⽤于缓存进程间通信的数据。

信号量表示资源的数量,控制信号量的⽅式有两种原⼦操作:

  • ⼀个是 P 操作,这个操作会把信号量减去 1,相减后如果信号量 < 0,则表明资源已被占⽤,进程需阻塞等待;相减后如果信号量 >= 0,则表明还有资源可使⽤,进程可正常继续执⾏。
  • 另⼀个是 V 操作,这个操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会将该进程唤醒运⾏;相加后如果信号量 > 0,则表明当前没有阻塞中的进程;

P 操作是⽤在进⼊共享资源之前,V 操作是⽤在离开共享资源之后,这两个操作是必须成对出现的。

举个例⼦,如果要使得两个进程互斥访问共享内存,我们可以初始化信号量为 1 。

在这里插入图片描述

具体的过程如下:

  • 进程 A 在访问共享内存前,先执⾏了 P 操作,由于信号量的初始值为 1,故在进程 A 执⾏ P 操作后信号量变为 0,表示共享资源可⽤,于是进程 A 就可以访问共享内存。
  • 若此时,进程 B 也想访问共享内存,执⾏了 P 操作,结果信号量变为了 -1,这就意味着临界资源已被占⽤,因此进程 B 被阻塞。
  • 直到进程 A 访问完共享内存,才会执⾏ V 操作,使得信号量恢复为 0,接着就会唤醒阻塞中的线程B,使得进程 B 可以访问共享内存,最后完成共享内存的访问后,执⾏ V 操作,使信号量恢复到初始值 1。

可以发现,信号初始化为 1 ,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有⼀个进程在访问,这就很好的保护了共享内存。

五、信号

上⾯说的进程间通信,都是常规状态下的⼯作模式。对于异常情况下的⼯作模式,就需要⽤「信号」的⽅式来通知进程。

在 Linux 操作系统中, 为了响应各种各样的事件,提供了⼏⼗种信号,分别代表不同的意义。

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

  • Ctrl+C 产⽣ SIGINT 信号,表示终⽌该进程;
  • Ctrl+Z 产⽣ SIGTSTP 信号,表示停⽌该进程,但还未结束;

如果进程在后台运⾏,可以通过 kill 命令的⽅式给进程发送信号,但前提需要知道运⾏中的进程 PID号,例如:

  • kill -9 1050 ,表示给 PID 为 1050 的进程发送 SIGKILL 信号,⽤来⽴即结束该进程;

信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)。

**信号是进程间通信机制中唯⼀的异步通信机制,**因为可以在任何时候发送信号给某⼀进程,⼀旦有信号产⽣,我们就有下⾯这⼏种,⽤户进程对信号的处理⽅式。

  • 执⾏默认操作。 Linux 对每种信号都规定了默认操作,例如,上⾯列表中的 SIGTERM 信号,就是终⽌进程的意思。
  • 捕捉信号。 我们可以为信号定义⼀个信号处理函数。当信号发⽣时,我们就执⾏相应的信号处理函数。
  • 忽略信号。 当我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。有两个信号是应⽤进程⽆法捕捉和忽略的,即 SIGKILL 和 SEGSTOP ,它们⽤于在任何时候中断或结束某⼀进程。

六、Socket

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

实际上,Socket 通信不仅可以跨⽹络与不同主机的进程间通信,还可以在同主机上进程间通信。

创建 socket 的系统调⽤:

int socket(int domain, int type, int protocal)

三个参数分别代表:

  • domain 参数⽤来指定协议族,⽐如 AF_INET ⽤于 IPV4、AF_INET6 ⽤于 IPV6、
    AF_LOCAL/AF_UNIX ⽤于本机;
  • type 参数⽤来指定通信特性,⽐如 SOCK_STREAM 表示的是字节流,对应 TCP、SOCK_DGRAM 表示的是数据报,对应 UDP、SOCK_RAW 表示的是原始套接字;
  • protocal 参数原本是⽤来指定通信协议的,但现在基本废弃。因为协议已经通过前⾯两个参数指定完成,protocol ⽬前⼀般写成 0 即可;

根据创建 socket 类型的不同,通信的⽅式也就不同:

  • 实现 TCP 字节流通信: socket 类型是 AF_INET 和 SOCK_STREAM;
  • 实现 UDP 数据报通信:socket 类型是 AF_INET 和 SOCK_DGRAM;
  • 实现本地进程间通信: 「本地字节流 socket 」类型是 AF_LOCAL和SOCK_STREAM,「本地数据报 socket 」类型是 AF_LOCAL 和 SOCK_DGRAM。另外,AF_UNIX 和AF_LOCAL 是等价的,所以AF_UNIX 也属于本地 socket;

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

在这里插入图片描述

  • 服务端和客户端初始化 socket ,得到⽂件描述符;
  • 服务端调⽤ bind ,将绑定在 IP 地址和端⼝;
  • 服务端调⽤ listen ,进⾏监听;
  • 服务端调⽤ accept ,等待客户端连接;
  • 客户端调⽤ connect ,向服务器端的地址和端⼝发起连接请求;
  • 服务端 accept 返回⽤于传输的 socket 的⽂件描述符;
  • 客户端调⽤ write 写⼊数据;服务端调⽤ read 读取数据;
  • 客户端断开连接时,会调⽤ close ,那么服务端 read 读取数据的时候,就会读取到了 EOF ,待处理完数据后,服务端调⽤ close ,表示连接关闭。

这⾥需要注意的是,服务端调⽤ 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 被⽤于在同⼀台主机上进程间通信的场景:

  • 本地 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 地址和端⼝,⽽是绑定⼀个本地⽂件,这也就是它们之间的最⼤区别。

学自小林coding所著的《图解系统》,仅做学习用,侵删

标签:信号量,操作系统,通信,间通信,管道,内核,进程,socket
来源: https://blog.csdn.net/flyersboy/article/details/117574342

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有