进程间通信的方式
进程间通信的方式一共有六种,分别是:管道、消息队列、共享内存、信号量、信号和socket。
1、管道
(1)我们创建管道实际上就是在内核中创建一块缓冲区,发送方拷贝数据到这块内核缓冲区中,接收方从这块内核缓冲区中拷贝数据到自己的用户空间。
(2)管道内传输的数据都是字节流,并且都是单向的,如果我们想进行进程间的双向通信,就必须创建两个管道。
(3)管道分为匿名管道和有名管道,匿名管道只能在具有“父子关系”的进程间通信,因为管道没有实体,也就是没有管道文件,只能通过fork()来复制父进程fd文件描述符,来达到通信的目的。而有名管道可以在没有“亲缘关系”的进程间通信,因为它提前创建了一个类型为管道的设备文件,在进程里只要使用这个设备文件,就可以 相互通信。
(4)因为发送方向缓冲区发送数据之后,管道中的数据没有被接收方读取的话,那么发送方进程会一直阻塞,直到管道中的数据被读取,因此管道数据传输效率比较低,不适合在进程间频繁地数据传输。
2、消息队列
管道的通信方式是效率比较低的,这一问题我们可以使用消息队列的通信模式可以解决。
(1)比如,A进程要给B进程发送消息,A进程把数据放在对应的消息队列后就可以正常返回了,B进程需要的时候再去读取数据就可以了。同理,B进程要给A进程发送消息也是如此。
(2)再来,消息队列是保存在内核中的消息链表,消息链表中每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。
(3)消息队列的声明周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁的。
(4)消息队列的缺点:
① 消息队列不适合比较大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限的。在 Linux 内核中,会有两个宏定义MSGMAX和MSGMNB,他们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。
②消息队列通信过程中,存在用户态与内核之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程。同理,另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。
3、共享内存
消息队列的读取和写入过程,都会有用户态与内核态之间的消息拷贝过程。那共享内存的方式,就很好的解决了这一问题。
共享内存的机制,就是从要通信的进程中各拿出一块虚拟地址空间来,映射到相同的物理内存中。这样A进程写入数据到共享内存,B进程就可以立刻读取到,不需要在用户态与内核态之间的数据拷贝,大大提高了进程间通信的速度。
4、信号量
(1)信号量其实是一个整型的计数器,主要用于实现进程间的互斥和同步,而不是用于缓存进程间通信的数据。
(2)信号量表示资源的数量,控制信号量的方式有两种原子操作:
① 一个是P操作,这个操作会把信号量减去1,相减后如果信号量 < 0,则表明没有资源可用,进程需要阻塞等待;相减后如果信号量 >= 0,则表明还有资源可用,进程可正常执行。
② 另一个是V操作,这个操作会把信号量加上1, 相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会选择一个阻塞进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程。
(3) 信号量初始化为1,就代表是互斥信号量,它可以保证共享内存在任何时刻只有一个进程在访问,这就很好的保护了共享内存。
(4)信号量初始化为0,就代表是同步信号量,它可以保证不同进程间的执行顺序。
5、信号
上面说的进程间通信,都是常规状态下的工作模式,对于异常情况下的工作模式,就需要用信号的方式来通知进程。
信号是进程间通信机制中唯一的异步通信机制,因为可以在任何时候发送信号给某一进程,一旦有信号产生,我们就有下面这几种,用户进程对信号的处理方式:
(1)执行默认操作。Linux 对每种信号都规定了默认操作。
(2)捕捉信号。我么可以为信号定义一个信号处理函数,当信号发生时,我们就执行相应的信号处理函数。
(3)忽略信号。我们不希望处理某些信号的时候,就可以忽略该信号,不做任何处理。不过有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SIGSTOP,它们用于在任何时候中断或结束某一进程。
6、Socket
前面提到的管道、消息队列、共享内存、信号量和信号都是在同一主机上进行进程间通信的方式,想要跨网络与不同主机上的进程之间通信,就需要Socket通信了。实际上,Socket不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信。根据创建的Socket的类型不同,分为三种常见的通行方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。