乐趣区

关于后端:凉了张三同学没答好进程间通信被面试官挂了


前言

收场小故事

炎炎夏日,张三骑着单车去面试花了 1 小时,一路上挥汗如雨。

后果面试过程只花了 5 分钟就完结了,面完的时候,天还是仍然是亮的,还得在烈日下奔走 1 小时回去。

面试五分钟,骑车两小时。

你看,张三因面试没筹备好,吹空调的工夫只有 5 分钟,来回路上花了 2 小时晒太阳,你说惨不惨?

所以啊,炎炎夏日,为了能缩短吹空调的工夫,咱们应该在面试前筹备得更充沛些,吹空调工夫是要本人争取的。

很显著,在这一场面试中,张三在 过程间通信 这一块没温习好,尽管列出了过程间通信的形式,但这只是外表功夫,应该须要进一步理解每种通信形式的优缺点及利用场景。

说真的,咱们这次一起帮张三一起温习下,加深他对过程间通信的了解,好让他下次吹空调的工夫能长一点。


注释

每个过程的用户地址空间都是独立的,一般而言是不能相互拜访的,但内核空间是每个过程都共享的,所以过程之间要通信必须通过内核。

Linux 内核提供了不少过程间通信的机制,咱们来一起瞧瞧有哪些?

管道

如果你学过 Linux 命令,那你必定很相熟「|」这个竖线。

$ ps auxf | grep mysql

下面命令行里的「|」竖线就是一个 管道 ,它的性能是将前一个命令(ps auxf)的输入,作为后一个命令(grep mysql)的输出,从这性能形容,能够看出 管道传输数据是单向的,如果想互相通信,咱们须要创立两个管道才行。

同时,咱们得悉下面这种管道是没有名字,所以「|」示意的管道称为 匿名管道,用完了就销毁。

管道还有另外一个类型是 命名管道,也被叫做 FIFO,因为数据是先进先出的传输方式。

在应用命名管道前,先须要通过 mkfifo 命令来创立,并且指定管道名字:

$ mkfifo myPipe

myPipe 就是这个管道的名称,基于 Linux 所有皆文件的理念,所以管道也是以文件的形式存在,咱们能够用 ls 看一下,这个文件的类型是 p,也就是 pipe(管道)的意思:

$ ls -l
prw-r--r--. 1 root    root         0 Jul 17 02:45 myPipe

接下来,咱们往 myPipe 这个管道写入数据:

$ echo "hello" > myPipe  // 将数据写进管道
                         // 停住了 ...

你操作了后,你会发现命令执行后就停在这了,这是因为管道里的内容没有被读取,只有当管道里的数据被读完后,命令才能够失常退出。

于是,咱们执行另外一个命令来读取这个管道里的数据:

$ cat < myPipe  // 读取管道里的数据
hello

能够看到,管道里的内容被读取进去了,并打印在了终端上,另外一方面,echo 那个命令也失常退出了。

咱们能够看出,管道这种通信形式效率低,不适宜过程间频繁地替换数据。当然,它的益处,天然就是简略,同时也咱们很容易得悉管道里的数据曾经被另一个过程读取了。

那管道如何创立呢,背地原理是什么?

匿名管道的创立,须要通过上面这个零碎调用:

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 内核中,会有两个宏定义 MSGMAXMSGMNB,它们以字节为单位,别离定义了一条音讯的最大长度和一个队列的最大长度。

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


共享内存

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

古代操作系统,对于内存治理,采纳的是虚拟内存技术,也就是每个过程都有本人独立的虚拟内存空间,不同过程的虚拟内存映射到不同的物理内存中。所以,即便过程 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,就代表着是 互斥信号量,它能够保障共享内存在任何时刻只有一个过程在拜访,这就很好的爱护了共享内存。

另外,在多过程里,每个过程并不一定是程序执行的,它们根本是以各自独立的、不可预知的速度向前推动,但有时候咱们又心愿多个过程能密切合作,以实现一个独特的工作。

例如,过程 A 是负责生产数据,而过程 B 是负责读取数据,这两个过程是相互合作、相互依赖的,过程 A 必须先生产了数据,过程 B 能力读取到数据,所以执行是有前后程序的。

那么这时候,就能够用信号量来实现多进程同步的形式,咱们能够初始化信号量为 0

具体过程:

  • 如果过程 B 比过程 A 先执行了,那么执行到 P 操作时,因为信号量初始值为 0,故信号量会变为 -1,示意过程 A 还没生产数据,于是过程 B 就阻塞期待;
  • 接着,当过程 A 生产完数据后,执行了 V 操作,就会使得信号量变为 0,于是就会唤醒阻塞在 P 操作的过程 B;
  • 最初,过程 B 被唤醒后,意味着过程 A 曾经生产了数据,于是过程 B 就能够失常读取数据了。

能够发现,信号初始化为 0,就代表着是 同步信号量,它能够保障过程 A 应在过程 B 之前执行。


信号

下面说的过程间通信,都是惯例状态下的工作模式。对于异常情况下的工作模式,就须要用「信号」的形式来告诉过程。

信号跟信号量尽管名字类似度 66.66%,但两者用处齐全不一样,就如同 Java 和 JavaScript 的区别。

在 Linux 操作系统中,为了响应各种各样的事件,提供了几十种信号,别离代表不同的意义。咱们能够通过 kill -l 命令,查看所有的信号:

$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

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

  • Ctrl+C 产生 SIGINT 信号,示意终止该过程;
  • Ctrl+Z 产生 SIGTSTP 信号,示意进行该过程,但还未完结;

如果过程在后盾运行,能够通过 kill 命令的形式给过程发送信号,但前提须要晓得运行中的过程 PID 号,例如:

  • kill -9 1050,示意给 PID 为 1050 的过程发送 SIGKILL 信号,用来立刻完结该过程;

所以,信号事件的起源次要有硬件起源(如键盘 Cltr+C)和软件起源(如 kill 命令)。

信号是过程间通信机制中 惟一的异步通信机制,因为能够在任何时候发送信号给某一过程,一旦有信号产生,咱们就有上面这几种,用户过程对信号的解决形式。

1. 执行默认操作。Linux 对每种信号都规定了默认操作,例如,下面列表中的 SIGTERM 信号,就是终止过程的意思。Core 的意思是 Core Dump,也即终止过程后,通过 Core Dump 将以后过程的运行状态保留在文件外面,不便程序员预先进行剖析问题在哪里。

2. 捕获信号。咱们能够为信号定义一个信号处理函数。当信号产生时,咱们就执行相应的信号处理函数。

3. 疏忽信号。当咱们不心愿解决某些信号的时候,就能够疏忽该信号,不做任何解决。有两个信号是利用过程无奈捕获和疏忽的,即 SIGKILLSEGSTOP,它们用于在任何时候中断或完结某一过程。


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;

接下来,简略说一下这三种通信的编程模式。

针对 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 函数来读写数据,就像往一个文件流外面写货色一样。

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

UDP 是没有连贯的,所以不须要三次握手,也就不须要像 TCP 调用 listen 和 connect,然而 UDP 的交互依然须要 IP 地址和端口号,因而也须要 bind。

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

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

针对本地过程间通信的 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 地址和端口,而是 绑定一个本地文件,这也就是它们之间的最大区别。


总结

因为每个过程的用户空间都是独立的,不能互相拜访,这时就须要借助内核空间来实现过程间通信,起因很简略,每个过程都是共享一个内核空间。

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

匿名管道 顾名思义,它没有名字标识,匿名管道是非凡文件只存在于内存,没有存在于文件系统中,shell 命令中的「|」竖线就是匿名管道,通信的数据是 无格局的流并且大小受限 ,通信的形式是 单向 的,数据只能在一个方向上流动,如果要双向通信,须要创立两个管道,再来 匿名管道是只能用于存在父子关系的过程间通信,匿名管道的生命周期随着过程创立而建设,随着过程终止而隐没。

命名管道 冲破了匿名管道只能在亲缘关系过程间的通信限度,因为应用命名管道的前提,须要在文件系统创立一个类型为 p 的设施文件,那么毫无关系的过程就能够通过这个设施文件进行通信。另外,不论是匿名管道还是命名管道,过程写入的数据都是 缓存在内核 中,另一个过程读取数据时候天然也是从内核中获取,同时通信数据都遵循 先进先出 准则,不反对 lseek 之类的文件定位操作。

音讯队列 克服了管道通信的数据是无格局的字节流的问题,音讯队列实际上是保留在内核的「音讯链表」,音讯队列的音讯体是能够用户自定义的数据类型,发送数据时,会被分成一个一个独立的音讯体,当然接收数据时,也要与发送方发送的音讯体的数据类型保持一致,这样能力保障读取的数据是正确的。音讯队列通信的速度不是最及时的,毕竟 每次数据的写入和读取都须要通过用户态与内核态之间的拷贝过程。

共享内存 能够解决音讯队列通信中用户态与内核态之间数据拷贝过程带来的开销,它间接调配一个共享空间,每个过程都能够间接拜访 ,就像拜访过程本人的空间一样快捷不便,不须要陷入内核态或者零碎调用,大大提高了通信的速度,享有 最快 的过程间通信形式之名。然而便捷高效的共享内存通信,带来新的问题,多过程竞争同个共享资源会造成数据的错乱。

那么,就须要 信号量 来爱护共享资源,以确保任何时刻只能有一个过程访问共享资源,这种形式就是互斥拜访。信号量不仅能够实现拜访的互斥性,还能够实现过程间的同步,信号量其实是一个计数器,示意的是资源个数,其值能够通过两个原子操作来管制,别离是 P 操作和 V 操作

与信号量名字很类似的叫 信号 ,它俩名字尽管类似,但性能一点儿都不一样。信号是过程间通信机制中 惟一的异步通信机制 ,信号能够在利用过程和内核之间间接交互,内核也能够利用信号来告诉用户空间的过程产生了哪些零碎事件,信号事件的起源次要有硬件起源(如键盘 Cltr+C)和软件起源(如 kill 命令),一旦有信号产生, 过程有三种形式响应信号 1. 执行默认操作、2. 捕获信号、3. 疏忽信号。有两个信号是利用过程无奈捕获和疏忽的,即 SIGKILLSEGSTOP,这是为了不便咱们能在任何时候完结或进行某个过程。

后面说到的通信机制,都是工作于同一台主机,如果 要与不同主机的过程间通信,那么就须要 Socket 通信了。Socket 实际上不仅用于不同的主机过程间通信,还能够用于本地主机过程间通信,可依据创立 Socket 的类型不同,分为三种常见的通信形式,一个是基于 TCP 协定的通信形式,一个是基于 UDP 协定的通信形式,一个是本地过程间通信形式。

以上,就是过程间通信的次要机制了。你可能会问了,那线程通信间的形式呢?

同个过程下的线程之间都是共享过程的资源,只有是共享变量都能够做到线程间通信,比方全局变量,所以对于线程间关注的不是通信形式,而是关注多线程竞争共享资源的问题,信号量也同样能够在线程间实现互斥与同步:

  • 互斥的形式,可保障任意时刻只有一个线程访问共享资源;
  • 同步的形式,可保障线程 A 应在线程 B 之前执行;

好了,今日帮张三同学温习就到这了,心愿张三同学早日收到情意的 offer,给夏天划上充斥汗水的句号。


好文举荐

「过程和线程」基础知识全家桶,30 张图一套带走

20 张图揭开「内存治理」的迷雾,霎时恍然大悟

30 张图带你走进操作系统的「互斥与同步」


退出移动版