很多时候,我们只是能列出进程间通信的方式,但是对通信方式的优缺点以及应用场景都不是很了解,这里我们来归纳一下。

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

  1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/2332713/1620400436234-30d6676f-0c96-4923-b905-251bd8d0b4a6.png#clientId=u9c22bfb1-7bdc-4&from=paste&height=269&id=ud0882c84&margin=%5Bobject%20Object%5D&name=image.png&originHeight=426&originWidth=950&originalType=binary&size=36309&status=done&style=none&taskId=u808251d7-00be-45c7-8a89-ab85a9ca841&width=600)

管道

如果我们学过linux指令。那我们肯定很熟悉“|”这个竖线

  1. ps auxf | grep mysql

上面命令行里面的”|”竖线就是一个管道,它的功能是将一个命令(ps auxf)的输出,作为后一个命令(grep mysql)的输入,从这功能描述,就可以看出管道传输数据是单向的,如果想相互通信,我们需要创建两个管道才行。

上面的这种管道是没有名字的,所以“|”表示的管道称为匿名管道,用完就销毁。

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

在使用命名管道前,先需要通过mkfifo命令来创建,并且指定管道的名字:

  1. mkfifo mypipe

其实,所谓的管道,就是内核里面的一串缓存,从管道一端写入数据,实际上是缓存在内核中,另一端读取,也就是从内核中读取这段数据,另外,管道传输的数据是无格式的流并且大小受限。

那么,怎么才可以使得管道是跨过两个进程的呢?

我们可以使用fork创建子进程,创建的子进程会复制父进程的文件描述符号,这样就做到了两个进程各有两个fd[0]和fd[1], 两个进程就可以通过各自fd写入和读取同一个管道文件,从而实现进程通信。

image.png

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

这种通信效率是非常低下的,因为管道不适合进程间频繁地交换数据。????

消息队列

对于管道效率低的问题,消息队列是不是就可以解决?。比如说,A进程要给B进程发送消息,A进程把数据放在对应的消息队列就可以正常返回了,B进程需要的时候再去读取数据就可以了。同理,B进程要给A进程发送消息也是如此。

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

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

但是邮件的通信方式存在不足的地方有两点,一是通信不及时,二是附件也有大小限制,这同样也是消息队列不足的点。

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

共享内存

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

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

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

image.png

信号量

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

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

信号

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

信号和信号量虽然名字相似度是66.66%,但是两者用途完全不一样,就好像java和javaScript的区别。在linux操作系统中,为了响应各种各样的事件,提供了几十种信号,分别代表不同的意义:

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

Socket通信

我们前面提到的进程间通信方式,都是在同一台主机上面的,如果想要做到跨主机的进程通信,那就得依靠socket通信了。