Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

7.4 进程间通信

进程间通信(Inter-Process Communication,IPC)是指不同进程之间交换数据和信号的机制。因为进程之间的地址空间是隔离的,每个进程都有自己独立的虚拟地址空间,一个进程无法直接访问另一个进程的内存,所以需要操作系统提供专门的IPC机制来实现进程间的数据交换。

IPC的目的和分类

IPC的目的

  1. 数据传输:一个进程需要将数据发送给另一个进程
  2. 共享数据:多个进程需要共享同一份数据,一个进程修改了数据,其他进程可以看到修改
  3. 通知事件:一个进程需要向另一个或多个进程发送通知,比如进程终止、IO完成等
  4. 资源共享:多个进程之间共享有限的资源(比如内存、文件、设备等)
  5. 进程控制:一个进程需要控制另一个进程的执行(比如debugger控制目标进程)

IPC的分类

根据通信方式和使用场景,IPC可以分为以下几类:

  1. 单主机IPC:同一台主机上的进程之间通信
    • 传统IPC:管道、信号、消息队列、共享内存、信号量
    • 套接字IPC:Unix Domain Socket
  2. 跨主机IPC:不同主机上的进程之间通信,通常使用网络套接字(Socket)、RPC等

常见的IPC机制

1. 管道(Pipe)

管道是最古老也是最简单的IPC机制,通常用于父子进程之间的通信。

  • 匿名管道
    • 是半双工的,数据只能单向流动,一端读一端写
    • 只能用于有亲缘关系的进程之间(父子进程、兄弟进程)
    • 本质是内核中的一块缓冲区,数据写入缓冲区的一端,从另一端读出
    • 遵循FIFO(先进先出)的原则
    • 用法:Shell中的|就是管道,比如ps aux | grep nginx,把ps命令的输出作为grep命令的输入
  • 命名管道(FIFO)
    • 有名字的管道,在文件系统中存在一个FIFO文件
    • 可以用于任意两个进程之间的通信,不需要有亲缘关系
    • 也是半双工的
    • 可以通过文件系统路径访问
  • 优点:简单易用,适合父子进程之间传递少量数据
  • 缺点:半双工,不适合大量数据传输,只能单向传输,匿名管道只能用于亲缘进程
  • 适用场景:父子进程之间简单的数据传输,Shell命令中的管道

2. 信号(Signal)

信号是一种异步通信机制,用于通知进程某个事件发生了,是最简单的异步通知机制。

  • 常见的信号:
    • SIGINT:Ctrl+C发送的中断信号,默认终止进程
    • SIGKILL:强制杀死进程,不能被捕获和忽略
    • SIGSEGV:段错误信号,访问非法内存时发送
    • SIGCHLD:子进程终止时发送给父进程的信号
  • 进程可以设置信号的处理方式:
    • 忽略信号(SIGKILL和SIGSTOP不能忽略)
    • 捕获信号,执行自定义的信号处理函数
    • 执行默认操作
  • 优点:非常轻量,适合异步通知事件,开销极小
  • 缺点:只能传递简单的信号编号,不能传递复杂数据,信号可能会丢失,不适合可靠的数据传输
  • 适用场景:进程间的简单事件通知,异常处理,进程终止通知等

3. 消息队列(Message Queue)

消息队列是操作系统维护的一个消息链表,进程可以向队列中发送消息,也可以从队列中读取消息。

  • 特点
    • 消息是有类型的,接收进程可以按类型读取消息,不需要先进先出
    • 消息是离散的,每个消息有固定的大小上限
    • 内核维护消息队列,即使进程退出,消息队列仍然存在
    • 支持多对多通信,多个进程可以向同一个队列发消息,也可以从同一个队列读消息
  • 优点
    • 支持异步通信,发送方发完消息就可以返回,不需要等待接收方
    • 支持消息类型,灵活度高
    • 消息可以持久化,即使接收方没准备好,消息也不会丢失
  • 缺点
    • 消息有大小限制,不适合大量数据传输
    • 有用户态和内核态之间的数据拷贝开销
    • 接口比较复杂
  • 适用场景:不同进程之间传递小的结构化消息,任务队列等

4. 共享内存(Shared Memory)

共享内存是最快的IPC机制,允许多个进程共享同一块物理内存区域,多个进程可以直接读写这块内存,不需要内核介入,几乎没有额外开销。

  • 原理:多个进程的虚拟地址空间映射到同一块物理内存页,一个进程修改了共享内存的内容,其他进程可以立即看到
  • 注意:共享内存本身没有同步机制,多个进程同时读写共享内存会出现竞态条件,需要配合信号量、互斥锁等同步机制使用
  • 优点:速度最快,没有数据拷贝开销,适合大量数据传输
  • 缺点:没有同步机制,需要自己实现同步,使用复杂,容易出现并发问题
  • 适用场景:大量数据的高效传输,比如视频处理、大数据计算等需要在进程间传递大量数据的场景

5. 信号量(Semaphore)

信号量主要用于进程间的同步和互斥,保护共享资源,不是用来传输数据的。

  • 原理:信号量是一个计数器,用来表示可用资源的数量
    • P操作(wait):信号量减1,如果减到小于0,进程阻塞等待
    • V操作(post):信号量加1,如果有等待的进程,唤醒一个
  • 二值信号量:信号量的值只能是0或1,相当于互斥锁,用来保护临界区
  • 计数信号量:信号量的值可以大于1,用来表示可用资源的数量
  • 优点:轻量,高效,适合进程间的同步和互斥
  • 缺点:只能用来同步,不能传输数据,使用不当容易出现死锁
  • 适用场景:保护共享资源,实现进程间的同步和互斥,配合共享内存使用

6. Unix域套接字(Unix Domain Socket)

Unix域套接字是专门用于同一主机上进程间通信的套接字,和网络套接字接口类似,但不需要经过网络协议栈,效率更高。

  • 特点
    • 支持TCP(流套接字)和UDP(数据报套接字)两种模式
    • 接口和网络Socket几乎一样,容易使用,代码可以和网络Socket复用
    • 比网络Socket快很多,不需要网络协议栈处理,没有校验和计算、序号确认等开销
    • 支持在进程之间传递文件描述符
  • 优点
    • 功能强大,支持可靠的字节流和数据报传输,不需要处理分片、重传等问题
    • 接口通用,和网络编程接口一致,学习成本低
    • 效率比管道、消息队列更高
    • 可以跨平台使用(Windows也有类似的实现)
  • 缺点:比共享内存慢,有数据拷贝开销
  • 适用场景:同一主机上不同进程之间的通用通信,比如服务和客户端之间的通信,现在很多服务都用Unix域套接字作为本地通信的首选

7. 网络套接字(Socket)

网络套接字用于不同主机之间的进程通信,当然也可以用于同一主机内的进程通信。

  • 常见的有TCP和UDP套接字
  • 优点:跨网络,支持不同主机之间的通信,功能强大
  • 缺点:同一主机内通信的话,效率比Unix域套接字低,有网络协议栈开销
  • 适用场景:跨主机的分布式通信

各种IPC机制的对比

IPC机制有无数据拷贝通信方式适用场景优点缺点
匿名管道半双工,亲缘进程父子进程简单通信简单易用只能单向,亲缘进程,数据量小
命名管道半双工,任意进程无亲缘进程简单通信简单,任意进程半双工,数据量小
信号异步事件通知轻量,开销小不能传复杂数据,不可靠
消息队列全双工,任意进程小结构化消息传递异步,可靠,支持消息类型有大小限制,拷贝开销
共享内存全双工,任意进程大量数据高速传输速度最快,无拷贝无同步,需要自己加锁
信号量-同步互斥高效,轻量不能传数据
Unix域套接字全双工,任意进程通用本地通信功能强大,接口通用,高效有拷贝开销,比共享内存慢
网络套接字全双工,跨主机分布式跨网络通信跨网络,通用效率低,协议开销大

IPC选择建议

  1. 简单事件通知:用信号
  2. 父子进程简单数据传输:用匿名管道
  3. 大量数据高速传输:用共享内存 + 信号量/互斥锁
  4. 本地通用服务通信:优先用Unix域套接字,接口通用,效率高
  5. 小的结构化消息传递:可以用消息队列,现在更常用的是Unix域套接字或者消息队列中间件
  6. 跨主机通信:用网络套接字或者RPC框架

思考问题

  1. 为什么需要进程间通信机制?进程之间为什么不能直接访问对方的内存?
  2. 共享内存是最快的IPC机制,为什么不是所有场景都用共享内存?
  3. Unix域套接字和网络套接字有什么区别?为什么同一主机内的通信优先用Unix域套接字?
  4. 如果你要实现一个本地的RPC框架,你会选择哪种IPC机制?为什么?