ipc - signal
09. 进程间通信与信号
0. 本章先解决什么问题
进程之间默认隔离,这保证安全,但也带来问题:
两个进程如何交换数据?
一个进程如何通知另一个进程发生了事件?
父进程如何管理子进程?
多个进程如何共享状态但不互相破坏?
进程间通信 IPC 和信号解决这些问题。
本章讲:
- 管道、消息队列、共享内存、Socket 的差异。
- 信号是什么,为什么它是异步事件。
- 子进程退出、僵尸进程是什么。
- 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 归还给空闲池 |
| 崩溃怎么办 | 持有者死掉后状态如何恢复 |
所以共享内存的正确模型不是“大家都能读写一块内存”,而是“用同步协议管理一块共享缓冲区的所有权和可见性”。
反例:信号不是可靠消息队列
信号适合通知“发生了某类事件”,但不适合承载复杂消息流。
原因是:
代码块收起展开
同类信号可能合并;
处理时机是异步的;处理函数能安全做的事很少。
如果你把每个任务都编码成一次信号:
发送 100 次信号 = 100 个任务
接收方未必能按 100 条独立消息处理。更稳的设计是:
信号只负责唤醒;
真正任务放在管道、队列、共享内存或文件描述符事件里。
这也是异步控制流的通用原则:信号做通知,状态和数据走可排队、可确认、可恢复的通道。
12. 学完本章你能解决什么问题
学完这一章,你应该能解决或开始分析这些问题:
- 为什么进程间通信需要 OS 参与?
- 管道和消息队列有什么区别?
- 共享内存为什么快,又为什么危险?
- Socket 为什么也能用于本机 IPC?
- 信号为什么是异步控制流?
- 信号处理为什么不能随便写复杂逻辑?
- 僵尸进程是什么,为什么需要 wait?
- 如何根据数据量、消息边界、复制成本、跨机器需求选择 IPC?
IPC 的核心是:在隔离和通信之间找到合适边界。