前言
每个进程的用户地址空间都是独立的,一般而言是不能互相访问的,但内核空间是每个进程都共享的,所以进程之间要通信必须通过内核。
Linux提供了以下进程通信方式:
一、管道
所谓的管道,就是内核里面的一串缓存。从管道的一段写入的数据,实际上是缓存在内核中的,另一端读取,也就是从内核中读取这段数据。
而利用管道进行进程之间的通信,就是利用fork()函数创造一个子进程,创建的子进程会复制父进程的文件描述符(见下图)
接着,为了明确写读端,我们只允许输出方写、接收方读(见下图)。
- 管道这种通信方式效率低,不适合进程间频繁地交换数据
- 其实无非就是,在内核空间中缓存,然后两个进程从这个内核空间中读和写。
二、消息队列
消息队列通过内核维护的一个队列,允许多个进程以一种松耦合的方式进行通信,消息在进程间以队列的形式存在。
消息发送者可以将消息放入队列后立即返回,不需要等待接收者处理完消息。接收者可以在方便的时候从队列中读取消息。以此来达到异步通信的效果。
消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。
缺点:
- 通信不及时
- 附件也有大小限制
三、共享内存
在操作系统中我们学过,每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中,如下图。这样两个进程直接共享了这一块物理内存,都可以读写。
缺点:
- 多个进程容易读写冲突
四、信号量
信号量是一种用于进程间通信的同步机制,主要用于管理进程间的资源访问,避免竞态条件。信号量的核心思想是通过信号(通常是整数)来控制对共享资源的访问。它在多进程或多线程环境中非常常用,尤其是需要协调进程之间的资源使用时。
本质上是一个计数器,这个计数器可以递增(表示释放资源)或递减(表示占用资源)。
有两个操作:
• P操作(等待或减操作,Proberen):尝试将信号量减1,如果信号量为0,则阻塞进程,直到信号量大于0为止。
• V操作(释放或加操作,Verhogen):将信号量加1,如果有进程在等待信号量,则唤醒这些进程。
缺点:
- 容易死锁
五、信号
以上都是常规工作状态下的工作模式,对于异常情况下的工作模式,就需要用「信号」的方式来通知进程。
信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件。
工作原理:
- 1. 发送信号:信号可以由操作系统、用户或其他进程发送给某个进程。比如,当用户按下Ctrl+C,系统会发送一个中断信号给正在运行的进程。进程也可以通过特定的系统调用(比如kill())发送信号给自己或其他进程。
- 2. 接收与处理信号:当进程接收到信号时,操作系统会暂停该进程的当前操作,并立即处理这个信号。如果进程有专门的处理函数,系统会调用这个函数来处理信号。处理完毕后,进程会继续执行之前中断的任务。
- 3. 信号的阻塞与解除阻塞:进程可以暂时阻塞某些信号,这意味着这些信号在被阻塞时不会立即处理。等到关键操作完成后,再解除阻塞并处理这些信号。这样可以避免在关键时刻被打断。
- 4. 信号的忽略与默认处理:如果进程不希望处理某些信号,可以选择忽略它们。但某些重要的信号(如SIGKILL)不能被忽略,操作系统会强制执行这些信号的默认操作(通常是终止进程)。
缺点:
- 传达有限
- 非实时
- 信号丢失
六、Socket
前面提到的管道、消息队列、共享内存、信号量和信号都是在同一台主机上进行进程间通信,那要想跨网络与不同主机上的进程之间通信,就需要 Socket 通信了。
实际上,Socket通信不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信。
参考:
5.2 进程间有哪些通信方式? | 小林coding