本篇要回答的问题:进程与进程之间如何协同(互斥、同步、资源共享、通讯)?为什么 iOS 敢于大刀阔斧砍掉绝大部分进程间协同机制——它是对的吗?
上一讲讲了进程内执行体的协同。今天接着聊进程间协同。一个贯穿全篇的追问:有了线程和协程这些进程内多任务设施后,进程的边界发生了怎样的变化?
启动进程
在一个进程中启动另一个进程,两种方法:创建子进程,或让 Shell 配合执行。
| 系统 | 创建子进程 | 评价 |
|---|---|---|
| UNIX/Linux | fork | 使用简洁,但架构上是糟糕设计 |
| Windows | CreateProcess | 参数很多,但清晰 |
| iOS | 不支持创建子进程 | 做了两个颠覆性改变 |
关键判断:iOS 的两个变化——①软件永远是单例(不创建多实例);②调用另一进程能力不是去创建它,而是基于 URL Scheme 去打开它。
URL Scheme:如 https、ftp 代表协议规范。iOS 下软件可声明实现某 Scheme(如微信注册 weixin),UIApplication.openURL("weixin://...") 即跳转到微信——支付宝/微信支付的对接即基于此。它并非 iOS 发明,源于浏览器;Windows 对应 ShellExecute,Linux 桌面亦有类似能力。
同步与互斥
进程内同步互斥有锁、读写锁、信号量、等待组、条件变量;进程间主流只支持锁(Mutex)和信号量(Semaphore)(Windows 还有 Event)。
进程间锁的语义同进程内,区别在标识互斥资源的方法:
| 系统 | 标识资源 | 接口合理性 |
|---|---|---|
| Windows | 名称(Name) | 更合理 |
| iOS | 路径(Path) | 更合理 |
| Linux | 共享内存 | 暴露了实现机理 |
信号量(Semaphore) 由 Dijkstra 提出,是代表共享资源数量的整型 S,操作界面为 PV:
| 操作 | 含义 | 行为 |
|---|---|---|
| P(S) | 请求/等待资源 | S 减 1,若 S<0 则无资源可用、等待 |
| V(S) | 释放资源并唤醒 | S 加 1,若 S<=0 说明有人在等、唤醒其一 |
关键判断:条件变量的设计灵感正是从信号量 PV 进一步抽象而来,只不过信号量中变量与条件都是确定的。
进程间同步互斥不如进程内丰富也没那么牢靠(无 WaitGroup、无 Cond)。根因:进程可能异常挂掉导致状态异常——如进程拿锁后挂掉致锁不释放、其他进程永久饥饿。锁还能把释放责任交给内核兜底,信号量更麻烦(OS 不知道 S 该是多少才合理)。
资源共享
两个进程再隔离,只要有共同中间人就能对话。中间人 = 共享资源。
典型共享存储型资源:文件系统与剪贴板。文件系统因存储设备天然共享,故是天然的进程间通讯中间人;很多 OS 里"一切皆文件",命名管道就只是特殊"文件"。
与文件系统相关的协同机制:文件、文件锁、管道(匿名/命名)、共享内存。
关键判断:共享内存是虚拟内存机制的自然结果——虚拟内存本就在内存页与磁盘文件间建映射,只要让两个进程的内存页关联到同一文件句柄即可共享数据,可能是性能最高的进程间数据通讯手段。
Linux 接口:Map(addr, len, prot, flags, fd, off) 把文件 fd 的 [off,off+len) 映射到 [addr,addr+len) 虚拟地址(addr 传 nil 自动选空闲地址);Unmap 取消映射,之后再访问会缺页异常。Windows 语义大同小异。
真正值得注意的是 iOS:基于文件系统的进程间通讯一律不支持。因为 iOS 把软件装进了沙箱(Sandbox)——
| 存储 | 隔离方式 |
|---|---|
| 内存 | 虚拟内存机制跨进程隔离 |
| 外存 | iOS 更进一步:文件系统也相互独立,软件 A 的文件软件 B 默认不可访问 |
剪贴板是文件系统之外仅剩的共享存储型资源,但限制大(只有一个、会互相覆盖),实践中是用户跨进程交互手段而非进程间通讯,反而更可能被木马利用(监听剪贴板窃取痕迹)。
收发消息
不用共享资源,还能基于网络——关键事实:这些进程同在一台机器、同一局域网。
套接字是最强大的通讯方式,没有之一。 进程间基于套接字通讯极其自然。
| 机制 | 说明 |
|---|---|
| UNIX 域 | UNIX 发明的本地通讯套接字,用 name 作访问地址(而非 ip:port) |
| 命名管道(NamedPipe) | Windows 不支持 UNIX 域,但其命名管道更像管道服务器,能力上与 UNIX 域等价 |
架构思维上我们学到什么?
进程间协同机制五花八门,但 iOS 把其中绝大部分堵死了——创新系统往往有颠覆性,做的是大大的减法。
关键判断:iOS 告诉我们——①软件只需启动一个进程实例;②大部分进程间协同机制是多余的,只需能调用其他软件能力(URL Scheme) + 能互斥 + 能收发消息就够了。至少从桌面 OS 视角,进程间协同大部分属于过度设计。
为什么以前会"过度设计"?并非前辈喜欢,而是有了线程和协程后,进程的边界已极大变化。回到架构方法论:
| 概要设计 | 内容 |
|---|---|
| 子系统职责 | 定义职责范围 |
| 子系统规格 | 接口、子系统间边界 |
| 需求分解组合 | 如何满足需求、变化点应对策略 |
关键判断:进程至少应是子系统级别的边界。子系统间应是规格级(稳定的契约)协同,而非实现框架级(不稳定的技巧)协同。所以站在子系统边界看进程边界——进程间协同只需"调用另一进程能力",无需复杂、高频、高度耦合的配合。iOS 大改的另一核心原因是安全(后续安全管理章再谈)。
总结
进程间协同机制(互斥/同步/资源共享/通讯)以"剧烈变动"形容毫不过分。许式伟认同 iOS 的判断:大刀阔斧砍掉惯例功能后,进程相比线程和协程才有了更清晰的分工。本讲也是单机软件内容的收尾,下一讲进入互联网世界。
先把"是什么"回答清楚
| 概念 | 一句话说明 |
|---|---|
| URL Scheme | 声明并打开另一软件能力的机制(如 weixin://),iOS 用它取代创建子进程 |
| 信号量(PV) | Dijkstra 提出,代表资源数量的整型,P 请求/V 释放;条件变量由它抽象而来 |
| 共享内存 | 让两进程内存页映射同一文件句柄,性能最高的进程间数据通讯 |
| 沙箱(Sandbox) | iOS 让进程的内存与外存文件系统都相互隔离 |
| 套接字 / UNIX 域 | 最强大的通讯方式;UNIX 域用 name 做本地通讯地址 |
| 子系统边界 | 进程至少是子系统级边界,应规格级协同而非实现框架级 |
一句话速记
进程间协同机制五花八门,但 iOS 做减法证明大半是过度设计——有了线程/协程后进程退回"子系统级边界",只需 URL Scheme 调用 + 互斥 + 收发消息(套接字) 三样,规格级协同即可,安全是另一大动因。
几条值得记住的判断
- 进程应是子系统级边界,子系统间用稳定的规格契约协同。
- 共享内存是虚拟内存的自然结果,性能最高(同文件句柄映射)。
- 套接字是最强通讯方式;UNIX 域与 Windows 命名管道能力等价。
- iOS 大减法是对的:进程间复杂高频耦合协同多属过度设计,且关乎安全。
思考题
有了线程和协程这些进程内多任务设施后,进程内协同与进程间协同的需求边界发生了怎样的变化?回到你自己的系统:你的"进程边界"是否真的对齐到了"子系统边界",还是塞进了太多本应在进程内完成的高频耦合协作?

