进程之间的通信方式有管道、消息队列、共享内存、信号量和Socket五种方式。
管道
来看一条Linux的指令:
netstat -tulnp | grep 8080
学过Linux命名的估计都懂得这条指令的含义,其中的【|】就是管道的意思,作用是把前一条命令的输出作为后一条命令的输入。在这里就是把netstat -tulnp的输出结果作为grep 8080这条指令的输入。如果两个进程要进行通信的话,就可以用这种管道进行通信了,并且这种通信方式是单向的,只能把第一个命令的输出作为第二个命名的输入,如果进程之间想要互相通信的话,就需要创建两个管道。
另外我们可以知道,这条竖线是没有名字的,我们把这种通信方式称之为匿名管道。既然有匿名管道,那就意味着有命名管道,下面我们来创建一个命名管道:
mkfifo test
这条命令创建了一个名字为test的命名管道。
接下来我们用一个进程向这个管道里面写数据,然后会有另外一个进程把里面的数据读出来。
echo "this is a pipe" > test // 写入数据
这个时候管道的内容没有被读出来的话,那么这个命令就会一直停在这里,只有当另一个进程把test里面的内容读出来的时候,这条命令才会结束。
cat < test // 读数据
我们可以看到,test里面的数据被读出来了,上一条命令也就执行结束了。
从上面的例子可以看出,管道的通知机制类似于缓存,就像是一个进程把数据放在某个缓存区域,然后等着另外一个进程去拿,并且是单向传输的。
这种通信方式的缺点是效率低下。比如a进程给b进程传输数据,只能等待b进程读取了数据之后a进程才能返回(同步阻塞)。因此管道不适合频繁通信的进程。
管道的优点则是比较简单,而且能够保证我们的数据已经真的被其他进程拿走了。
消息队列
知道了管道这种通信方式的缺点之后,有的人就想了,能不能把进程的数据放在某个内存之后就马上让进程返回,而无需等待其他进程来取呢?
答案是可以的,有人想出了消息队列的通信方式来解决这个问题。比如a进程要给b进程发送消息,只需要把消息放在对应的消息队列里面就可以了,b进程需要的时候再去对应的消息队列里面取出来。同理,b进程要给a进程发送消息也是一样的。
消息队列这种通信方式同样类似于缓存,存在读写大缓存性能可能很差的缺点。如果a进程发送的数据占的内存比较大,并且进程之间的通信特别频繁的话,发送消息(拷贝)这个过程可能就需要花很多的时间来读内存中的数据。
共享内存
共享内存这个通信方式就很好地解决了拷贝数据消耗的时间。
通常来讲,每个进程是有自己的独立内存的,共享内存则是通过特定的机制实现的。我们都知道,系统在加载一个进程的时候,分配给进程的内存并不是实际的物理内存,而是虚拟的内存空间。我们可以让两个进程各自拿出一块虚拟地址空间来,然后映射到相同的物理内存中,这样两个进程虽然有着独立的虚拟内存空间,却会有一部分是映射到相同的物理内存,也就实现了内存共享。
信号量(共享内存的进阶)
共享内存最大的问题就是多进程竞争内存的问题,也就是线程安全的问题。解决这个问题的方法就是信号量。
信号量的本质是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是1,当进程a访问内存1的时候,我们会把信号量的值设为0,然后当进程b也要访问内存1的时候,看到信号量的值为0就知道已经有进程在访问内存1了,这个时候进程b就访问不了内存1了,相当于锁的机制。
因为进程之间读取共享内存之前要先读取信号量,因此信号量也可以看作是进程之间的一种通信方式。
Socket
上面所说的管道、消息队列、共享内存和信号量都是多个进程在一台主机之间的通信,而不同主机之间的进程则是通过Socket进行通信的。比如通过浏览器发起的HTTP请求和服务器返回的响应就是通过Socket的通信方式实现的。
总结
了解进程之间的通信方式是十分必要的,比如JVM就是用得共享内存的方式,对于帮助了解JVM的底层机制起了重要作用。
"你的晚安,是下意识的恻隐。"