浅析进程通信


首先我们要知道,进程通信需要操作系统支持,因为各进程拥有的内存地址空间相互独立,为了保证安全,一个进程不能直接访问另一个进程的地址空间安全。

进程间通信的方式有三种:管道通信,共享存储,消息队列。

管道通信

管道又分为匿名管道和命名管道。

匿名管道本质上是内存中一片固定大小的缓冲区,当我们通过系统调用创建了匿名管道,会返回一个写入段描述符fd[0]和读取端描述符fd[1]。我们可以使用 fork 创建子进程,创建的子进程会复制父进程的文件描述符,两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了。

命名管道本质上是在文件系统创建了一个设备文件,不同的进程就可以通过这个文件实现相互通信。

管道是一种经典的BIO方式,是半双工的,某一时间段内只能单向传输。如果要实现双向同时传输,则需要设置两个管道。当管道写满时,写进程会阻塞,直到读进程将管道内的数据取走。当管道读空时,读进程会阻塞,直到写进程向管道中写入数据,即可唤醒写进程。

这种方式效率很低,不适合进程间的频繁数据交换,好处就是简单


消息队列

消息队列是经典的消息传递的方式,其本质上是生产者-消费者的模型

在Linux中,消息队列是保存在内核中的消息链表,在发送数据的时候,会分成一个个的数据单元,消息的发送方和接收方要固定好消息体的格式,所以每个消息都是固定大小的数据块,而不像是无序的字节。所以消息队列不适合传输比较大的数据,比如在Linux中定义了MSGMAX和MAGMNB,他们分别定义了消息最大长度和消息队列的最大长度。

如果消息队列中的数据被读取了,那么内核就会把这个消息体给删除

消息队列的生命周期是随内核的,当内核不释放消息队列时,那么直到系统关闭或者宕机的时候消息队列才会被释放。

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


共享内存

由于消息队列存在用户态和内存态之间的数据拷贝开销,所以原生的消息队列的性能是比较差的。

共享内存技术:简单来说,就是基于虚拟内存机制,将不同虚拟地址空间,映射到同一块物理内存,这样的话两个进程对自己的虚拟内存进行操作,实际上改变的是共享区域中的内存数据。


信号量

注意到共享内存的读写,它实际上涉及到了一个临界资源的访问问题,所谓临界资源就是多个进程/线程都能够读取到的一段共享数据,那么在并发的情况下,就会导致并发下的数据安全问题

为了防止多进程竞争共享资源,一般有悲观锁和乐观锁的实现机制,乐观锁是基于一种机器原子指令CAS实现的,而悲观锁的实现方式通常有信号量

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

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

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

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

基于信号量实现的互斥,初始化为1

在对资源进行访问前,进行加锁

  • 如果信号量为1,执行P操作后变为0,表示资源可用
  • 如果执行P操作后信号量<0,那么这个线程将被阻塞,并且被注册到监视这个信号量的监视器所对应的同步队列中

当退出对资源的访问的时候,进行解锁

  • 如果执行V操作后信号量<=0,那么就唤醒同步队列阻塞中一个线程

基于信号量实现的同步,初始化为0

同步,比如说要保证A进程在B进程之前执行,我们把P操作放在B进程,V操作放在A进程。

B要取数据,那么就会执行P操作,使信号量变为-1,那么B被阻塞,。只有当A生产数据后,执行V操作使信号量变为0,B就被唤醒。


文章作者: Fuchanglai
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Fuchanglai !
赏
  目录