ipc - signal

09. 进程间通信与信号

0. 本章先解决什么问题

进程之间默认隔离,这保证安全,但也带来问题:

两个进程如何交换数据?
一个进程如何通知另一个进程发生了事件?
父进程如何管理子进程?
多个进程如何共享状态但不互相破坏?

进程间通信 IPC 和信号解决这些问题。

本章讲:

  • 管道、消息队列、共享内存、Socket 的差异。
  • 信号是什么,为什么它是异步事件。
  • 子进程退出、僵尸进程是什么。
  • IPC 如何在性能、复杂度、隔离之间取舍。

IPC 与信号进程关系

这张图怎么读

这张图把进程通信的关键边界画出来:进程地址空间彼此隔离,字节流、消息、共享内存、Socket、信号和 wait 都要经过 OS 管理。共享内存快,但必须另外解决同步和生命周期;信号是异步控制流,不是普通函数回调。

1. 为什么进程通信比线程通信麻烦

线程共享同一进程地址空间,通信可以直接读写共享内存。

进程有独立地址空间:

进程 A 的地址 0x1000
进程 B 的地址 0x1000
不是同一块物理内存

所以进程通信需要 OS 参与。

OS 提供受控通道,既让数据能过去,又保持隔离和权限。

2. 管道

管道是单向字节流通道。

进程 A 写 -> 管道 -> 进程 B 读

特点:

  • 适合父子进程或相关进程。
  • 字节流,不保留高级消息结构。
  • 缓冲区满时写可能阻塞。
  • 缓冲区空时读可能阻塞。

命令行中的 | 就是管道思想:

producer | consumer

3. 消息队列

消息队列按消息传递。

send message
receive message

比字节流更像“一个个包”。

优点:

  • 保留消息边界。
  • 可以排队。
  • 可以按类型或优先级处理。

缺点:

  • 内核管理成本。
  • 消息大小和队列容量有限。
  • 仍然需要复制或序列化。

4. 共享内存

共享内存让多个进程映射同一块物理内存。

进程 A 虚拟地址 -> 共享物理页
进程 B 虚拟地址 -> 同一共享物理页

优点:

  • 数据不必在进程间复制。
  • 适合大块数据共享。
  • 性能高。

缺点:

  • 需要同步机制。
  • 容易数据竞争。
  • 生命周期和权限要管理。

共享内存常和信号量、锁、事件通知一起使用。

机制深挖:IPC 的三个隐藏成本

IPC 不是“两个进程能说话”这么简单。每种 IPC 都在三个成本之间取舍:

成本含义哪些方式更敏感
复制成本数据是否要从一个地址空间复制到另一个地址空间管道、消息队列、Socket
同步成本发送方和接收方如何协调谁先读写共享内存、mmap、消息队列
背压成本接收方处理不过来时,发送方会阻塞、丢弃还是堆积管道、队列、Socket

举一个典型问题:生产者写得很快,消费者处理得很慢。

producer -> IPC buffer -> consumer

不同 IPC 的表现不一样:

IPC缓冲满时可能发生什么排障重点
管道写阻塞或失败消费者是否还在读,管道容量是否被填满
消息队列队列堆积、发送阻塞、消息被拒绝队列长度、消息大小、消费速率
共享内存数据被覆盖、读写竞态ring buffer 指针、锁、序号、内存可见性
Socket发送缓冲区满、连接变慢send buffer、接收端读取、协议背压

所以 IPC 设计必须回答:

接收方慢时,系统应该等待、丢弃、降级,还是扩容?

这不是语法问题,而是系统行为问题。

5. Socket 作为 IPC

Socket 不只用于跨机器,也可以用于同一台机器内进程通信。

优点:

  • 接口统一。
  • 可本机也可跨机器。
  • 支持流式或数据报式通信。
  • 适合服务化架构。

缺点:

  • 比共享内存复制更多。
  • 协议和序列化成本更明显。

如果未来需要跨机器,Socket 通常更容易扩展。

6. 文件和 mmap

进程也可以通过文件通信:

进程 A 写文件
进程 B 读文件

或者 mmap 同一文件:

多个进程映射同一文件区域

适合:

  • 持久化共享数据。
  • 大文件读写。
  • 简单数据交换。

但要处理:

  • 文件锁。
  • 刷盘。
  • 一致性。
  • 并发写。

7. 信号是什么

信号是 OS 发送给进程的异步通知。

它表示:

某个事件发生了

例如:

  • 终止请求。
  • 子进程退出。
  • 定时器到期。
  • 非法内存访问。
  • 用户中断。

信号像打断正常控制流的事件。

进程可以:

  • 使用默认处理。
  • 忽略某些信号。
  • 注册处理函数。

8. 信号处理的危险

信号是异步到来的,可能打断程序正在执行的任意位置。

所以信号处理要小心:

  • 不能做复杂、不安全的操作。
  • 不能随便访问共享状态。
  • 要避免和主流程产生竞态。
  • 通常只设置标志或写入安全通道。

信号不是普通函数回调,它是异步控制流。

信号的第一原则:处理函数越小越好

信号可能在程序执行到任意位置时插进来。比如主流程正在更新一个复杂结构,信号处理函数也去读写同一结构,就可能看到半更新状态。

更稳的模式是:

信号处理函数:
只设置一个标志
或写入一个安全的通知 fd/管道

主循环:
在安全位置检查标志
执行真正的清理、退出、重载配置

这背后的原则是:异步事件只负责唤醒或标记,复杂逻辑回到普通控制流里做。

做法风险
在信号处理里分配内存、加锁、写复杂日志可能死锁或破坏内部状态
在信号处理里只设置原子标志风险较低
用管道/eventfd 类通道把信号转成主循环事件更容易纳入统一事件模型

9. 父子进程和僵尸进程

子进程结束后,OS 会保留它的一些退出信息,等待父进程读取。

如果父进程一直不 wait,子进程可能成为僵尸进程。

僵尸进程不是还在运行,而是:

进程已结束
但退出状态还没被父进程回收

父进程应当正确等待子进程,或者让系统接管回收。

10. IPC 选型

需求候选
简单单向字节流管道
保留消息边界消息队列、数据报 Socket
大块数据高性能共享共享内存
本机/跨机器统一Socket
持久化交换文件、mmap 文件
事件通知信号、eventfd 类机制

选型要看:

  • 数据量。
  • 是否需要消息边界。
  • 是否跨机器。
  • 是否需要持久化。
  • 是否能接受复制。
  • 同步复杂度。

11. 联系实际:多进程服务如何通信

一个多进程服务可能:

  • 主进程监听连接。
  • worker 进程处理请求。
  • 进程间通过 Socket 或管道传任务。
  • 大块数据用共享内存。
  • 主进程通过信号通知优雅退出。
  • 父进程 wait 子进程,防止僵尸。

这说明 IPC 不是单独知识点,而是进程隔离、资源管理、同步和生命周期管理的组合。

设计卡:按通信形态选择 IPC

选 IPC 之前,先把需求写清楚:

需求形态更自然的候选关键风险
一方连续生产字节,另一方消费管道、流式 Socket缓冲区满/空导致阻塞
一条条独立任务或事件消息队列、数据报 Socket队列容量、消息边界、序列化
大块数据频繁共享共享内存 + 同步原语竞态、生命周期、权限
本机未来可能扩展到跨机器Socket协议、错误处理、连接管理
只通知“发生了某事”信号/eventfd 类机制异步处理安全、丢事件语义
子进程生命周期管理wait / 信号僵尸进程、退出状态回收

实践上常见组合是:

共享内存传大数据
消息队列或事件通知传“哪块数据可用”
父进程通过信号和 wait 管理 worker 生命周期

这能避免两个极端:所有数据都复制导致慢,或者只用共享内存却没有同步和通知,最后变成竞态。

边界条件:共享内存快,但同步协议错了会读到半成品

共享内存省掉了进程间复制,但它不会自动定义数据何时有效。

危险时间线:

进程 A 写 header
进程 A 写 body 的前半段
进程 B 看到 ready 标志
进程 B 开始读取
进程 A 继续写 body 后半段

如果 ready 标志写得太早,B 会读到半成品。共享内存必须额外定义:

问题必须回答
谁拥有这块区域当前是生产者写,还是消费者读
数据何时完整用长度、版本、状态位还是序号表示
写入顺序如何保证ready 标志必须在数据写完后可见
消费后如何回收谁把 buffer 归还给空闲池
崩溃怎么办持有者死掉后状态如何恢复

所以共享内存的正确模型不是“大家都能读写一块内存”,而是“用同步协议管理一块共享缓冲区的所有权和可见性”。

反例:信号不是可靠消息队列

信号适合通知“发生了某类事件”,但不适合承载复杂消息流。

原因是:

代码块JAVA · 2 行收起展开
同类信号可能合并;
处理时机是异步的;

处理函数能安全做的事很少。

如果你把每个任务都编码成一次信号:

发送 100 次信号 = 100 个任务

接收方未必能按 100 条独立消息处理。更稳的设计是:

信号只负责唤醒;
真正任务放在管道、队列、共享内存或文件描述符事件里。

这也是异步控制流的通用原则:信号做通知,状态和数据走可排队、可确认、可恢复的通道。

12. 学完本章你能解决什么问题

学完这一章,你应该能解决或开始分析这些问题:

  1. 为什么进程间通信需要 OS 参与?
  2. 管道和消息队列有什么区别?
  3. 共享内存为什么快,又为什么危险?
  4. Socket 为什么也能用于本机 IPC?
  5. 信号为什么是异步控制流?
  6. 信号处理为什么不能随便写复杂逻辑?
  7. 僵尸进程是什么,为什么需要 wait?
  8. 如何根据数据量、消息边界、复制成本、跨机器需求选择 IPC?

IPC 的核心是:在隔离和通信之间找到合适边界。

延伸阅读