【Linux指南】进程通信系列(一):为什么进程不能直接说话 —— IPC 的必要性与 Linux IPC 全景
在 Linux 进程控制系统中我们花了大量篇幅讲进程的独立性每个进程拥有独立的虚拟地址空间、独立的页表、独立的内核数据结构。独立性是操作系统的 “护城河” —— 它保证一个进程崩溃不会波及别人一个进程的数据不会被随意窥探。但硬币的另一面很快就会显现当两个进程真的需要协作时独立性反而成了最大的障碍。小到 Shell 里一句ls | grep txt大到 Nginx 主进程向工作进程派发连接背后都要解决同一个问题 —— 进程间通信IPCInter-Process Communication。本篇文章是整个进程通信系列的开篇不急着上接口、写代码而是先把三个根本问题讲透为什么进程不能直接通信通信的本质是什么Linux 提供了哪些通信方案把这条主线立住后面管道、共享内存、消息队列、信号量的所有细节都会变得顺理成章。文章目录一、先搞懂为什么进程之间不能直接 说话1.1 一个看似简单的需求1.2 现象背后虚拟地址空间与写时拷贝1.3 独立性是缺点吗—— 是设计不是缺陷二、进程间通信的四大目的2.1 数据传输2.2 资源共享2.3 通知事件2.4 进程控制三、IPC 的本质让进程看到同一份 公共资源3.1 推导通信的前提是 看见3.2 谁提供通信能力—— 只能是操作系统3.3 使用方式系统调用是唯一的官方通道四、热身其实你每天都在用 IPC五、Linux IPC 的发展史与全景分类5.1 一条演进时间线5.2 全景分类图5.3 本系列怎么讲六、几个容易混淆的问题6.1 两个进程读写同一个磁盘文件算 IPC 吗6.2 IPC 和网络通信是一回事吗6.3 线程之间需要 IPC 吗七、总结与下一篇预告7.1 本篇核心结论7.2 下一篇预告一、先搞懂为什么进程之间不能直接 “说话”1.1 一个看似简单的需求先看一个再自然不过的需求父进程创建了一个子进程去下载文件父进程想知道 “下载的进度怎么样了”。按照写普通程序的直觉你可能会想定义一个全局变量int g_val 0;子进程更新它父进程读它不就完了吗我们来试一下// independence.c —— 父子进程各改各的变量#includestdio.h#includestdlib.h#includeunistd.h#includesys/wait.hintg_val100;// 全局变量父子进程看起来共享intmain(){pid_tidfork();if(id0){// 子进程把 g_val 改成 200g_val200;printf(子进程: 我把 g_val 改成了 %d地址是 %p\n,g_val,g_val);exit(0);}// 父进程等待子进程退出后读一下 g_valwaitpid(id,NULL,0);printf(父进程: 我看到的 g_val 还是 %d地址是 %p\n,g_val,g_val);return0;}编译运行gcc independence.c -o independence ./independence结果类似子进程: 我把 g_val 改成了 200地址是 0x5591a8c01014 父进程: 我看到的 g_val 还是 100地址是 0x5591a8c01014一个非常有意思的现象两边的地址一模一样值却不一样。子进程明明把变量改成了 200父进程读到的依然是 100。1.2 现象背后虚拟地址空间与写时拷贝这个现象在[《进程深度解析五程序地址空间》](Linux 进程深度解析五程序地址空间 —— 进程的独立内存王国-CSDN博客)和[《进程控制系列一fork 与写时拷贝》](【Linux指南】进程控制系列一进程创建 ——fork 函数与写时拷贝COW深度解析_linux 进程创建 cow-CSDN博客)里已经深入分析过这里只做简要回顾因为它正是 IPC 存在的根源进程看到的 “地址” 是虚拟地址。父子进程打印的0x5591...都只是各自虚拟地址空间里的地址需要经过各自进程的页表翻译才能落到物理内存。fork 之后触发写时拷贝COW。fork 创建子进程时父子先共享同一批物理页只读一旦任何一方尝试写入比如子进程执行g_val 200内核就会立刻为写入方复制一份新的物理页再改写它的页表映射。于是从写入那一刻起父子进程的同名变量已经指向两块不同的物理内存——虚拟地址相同只是各自页表翻译的终点不同。用一张图看清楚这个 “分家” 过程这就是进程独立性的落地表现操作系统刻意让每个进程活在自己的 “内存王国” 里国王换了多少金币邻国是看不见的。1.3 独立性是缺点吗—— 是设计不是缺陷你可能会想既然这么麻烦为什么不干脆让所有进程共享内存通信不就简单了恰恰相反隔离是操作系统稳定性的基石一个进程野指针越界、内存写崩只污染自己的地址空间其他进程和内核毫发无伤恶意程序无法直接读取别的进程的密码、密钥等敏感数据出问题的进程可以被单独终止、单独回收不影响整个系统。所以正确的理解是进程隔离是 “默认状态”通信是 “需要额外申请的特权服务”。当进程确实需要协作时不能靠 “拆墙”而要走操作系统提供的 “官方通道”—— 这就是 IPC。二、进程间通信的四大目的在动手学任何一种 IPC 之前先明确 “我们要用 IPC 干什么”。进程间通信的目的归纳为四类每一类都对应着真实系统中随处可见的场景2.1 数据传输一个进程需要把它的数据发送给另一个进程。Shell 里ls | grep txtls进程把自己的输出 “传” 给grep进程日志进程把日志内容交给落盘进程统一写入爬虫主进程把解析好的 URL 交给下载进程。这是最直观的一类数据本身要从进程 A 流动到进程 B。2.2 资源共享多个进程之间访问、使用同一份资源。多个业务进程共享一份只读配置表多个进程共同维护一张 “在线用户表”谁都可以读写更新。注意它和数据传输的区别数据传输是 “我发给你我自己就不留了”资源共享是 “这份资源摆在公共区域大家都可以反复访问”。对应到实现上共享内存是这一类的代表。2.3 通知事件一个进程需要向另一个或一组进程发送消息通知它它们发生了某种事件。子进程退出时内核通知父进程 “你的孩子结束了”这正是进程控制系列里waitpid存在的前提一个进程崩溃时内核通知它的父进程用户按下CtrlC前台进程收到 “终止” 通知。这一类的特点是不关心传多少数据只关心 “事件发生了”这一个信号。信号Signal是这类通信的代表。2.4 进程控制有些进程希望完全控制另一个进程的执行。最典型的就是调试器如 GDBDebug 进程要能拦截目标进程的所有陷入和异常要能随时知道目标进程的状态变化运行、暂停、退出甚至能改写目标进程的执行流程断点就是改写目标进程的指令实现的。进程控制往往是前三种能力的综合运用既要传输控制命令数据传输又要感知状态变化通知事件。小结四种目的按 “传的东西” 排个序 —— 数据传输传 “内容”通知事件传 “信号”资源共享传 “访问权”进程控制传 “控制权”。后面你会看到不同的 IPC 方式天然擅长其中一两类。三、IPC 的本质让进程看到同一份 “公共资源”3.1 推导通信的前提是 “看见”有了第一节的结论 —— 进程之间天然隔离 —— 我们可以做一条严谨的推导进程 A 的数据在 A 的地址空间里进程 B 想拿到它必须有一块A 和 B 都能访问到的区域而进程地址空间相互隔离任何一方都无法单方面 “闯入” 对方领地所以这块 “公共区域” 只能由站在所有进程之上的管理者来开辟和维护 —— 它就是操作系统OS。于是得到 IPC 的核心结论也是贯穿整个系列的一句话进程通信的本质让互相通信的多个进程看到同一份由操作系统提供的公共资源。记住图中这个三层结构后面每一种 IPC 都能套进来只是 “公共资源” 的形态不同IPC 方式公共资源是什么匿名管道 / 命名管道内核维护的一块内存缓冲区借助文件框架组织共享内存一块被同时映射进多个进程地址空间的物理内存消息队列内核维护的一个带类型的消息链表信号量内核中的一个资源计数器 等待队列3.2 谁提供通信能力—— 只能是操作系统有没有可能不由 OS 提供让用户进程自己搞定答案是不能原因有二进程隔离是内核维护的。想让两个进程看到同一份物理内存只有内核有能力操作页表、开辟物理页 —— 这属于内核的特权指令范畴用户态程序没有这个权限。通信需要仲裁。两个进程同时写一块缓冲区谁先谁后缓冲区满了怎么办这些并发问题必须有一个中立裁判这个裁判同样只能是 OS。3.3 使用方式系统调用是唯一的官方通道OS 向外暴露系统调用作为统一的通信接口pipe()、shmget()、msgget()、semop()…… 程序员调用这些接口向内核 “申请” 公共资源、往里读写数据、最后释放资源。所以学习任何一种 IPC都可以遵循同一个三段式框架这个框架也是本系列每篇文章的行文主线① 看到同一份资源 —— 这份资源是什么怎么让多个进程都 看见 它 ② 提供者与管理者 —— OS 如何创建、组织、维护这份资源 ③ 使用方式 —— 暴露了哪些系统调用怎么用代码完成一次完整的通信知识拓展面试视角如果面试官问 “进程间通信的本质是什么”最好的回答不是背 “有管道、共享内存、消息队列……”而是先给出这条主线进程具有独立性通信的前提是让不同进程看到同一份公共资源这份资源由 OS 提供通过系统调用使用。然后再展开具体方式层次立刻就出来了。四、热身其实你每天都在用 IPC在正式接触系统调用之前先在命令行里 “玩” 一次通信感受一下 IPC 离我们有多近。$who|wc-l3这条命令统计当前登录了几个用户。who进程把用户列表写进了一条管道wc -l进程从管道里读出来数行数 —— 两个独立进程一次完整的数据传输这就是最古老的 IPC管道。|的本质是把who的标准输出接到wc的标准输入中间那条 “管子” 就是 OS 在内核里开的一段缓冲区。再比如$psaux|grepsshd|grep-vgrep|wc-l四个进程串联成一条 “流水线”数据像水一样从左往右流过三级管道 —— Shell 里的多级管道本质上就是多个 IPC 通道的接力。日常开发中你早已被 IPC 包围浏览器主进程与渲染进程之间的消息传递、Redis 主进程把数据写入 AOF 落盘进程、Docker 容器进程与宿主机之间的控制通道…… 全部建立在今天要学的这些机制之上。五、Linux IPC 的发展史与全景分类5.1 一条演进时间线Linux 的 IPC 体系不是一天设计出来的而是几十年 Unix 历史 “层积” 的结果。了解这条时间线能帮你理解为什么今天会有 “好几套长得不一样的接口” 并存分阶段展开早期 Unix最先诞生的是管道匿名管道 pipe简单高效但只能用于有亲缘关系的进程之间单向通信随后出现命名管道 FIFO用 “一个有名字的特殊文件” 突破了亲缘关系的限制。System V 时代ATT推出了 System V IPC 三件套 ——消息队列、共享内存、信号量广泛用于早期服务器。它们的共同特点功能强大但生命周期随内核进程退了资源还在、缺乏完善的通知机制。BSD 分支推出了Socket最初为网络跨主机通信而生后来拓展出本地域套接字Unix 域 Socket既能做本机 IPC又兼容网络套接字的编程范式 —— 一套 API 从单机写到分布式。POSIX 标准化POSIX 对消息队列、共享内存、信号量做了标准化重设计接口更友好并补充了互斥量、条件变量、读写锁等线程同步设施信号Signal作为简易的异步通知机制也一直沿用至今。现代 Linux以上全部兼容并蓄。日常大量使用的是管道、Unix 域 Socket 和共享内存。一句话总结演进路线匿名管道 → 命名管道 → System V IPC → Socket → POSIX IPC。5.2 全景分类图把上面的发展史落成一张分类图这就是 Linux IPC 的 “全家福”也是本系列的行军地图5.3 本系列怎么讲对着地图规划路线 —— 本系列 9 篇文章的安排篇目主题覆盖的分类本篇IPC 必要性与全景分类全局认知第二篇匿名管道pipe 的本质、fork 继承与四种读写规则Unix 原生第三篇匿名管道实战多进程任务分发进程池 命令行管道原理Unix 原生第四篇命名管道 FIFO让任意进程之间建立管道Unix 原生第五篇System V 共享内存最快的 IPC 与它的同步难题System V第六篇System V 消息队列自带边界的 “块级” 通信System V第七篇System V 信号量共享内存的 “交通信号灯”System V第八篇内核视角System V IPC 如何被统一管理System V第九篇横向对比与选型管道、共享内存、消息队列、信号量怎么选全局收束其中管道家族是 “最常用”System V 三兄弟是 “原理核心 面试高频”内核视角篇会顺便看到一份精彩的 “C 语言如何实现泛型多态” 的内核代码。六、几个容易混淆的问题在进入第二篇之前先澄清几个初学者最容易走偏的问题6.1 两个进程读写同一个磁盘文件算 IPC 吗直觉上 “能看到同一份文件内容” 好像就算通信了。严格来说普通文件不是为通信设计的。两个进程同时打开/tmp/log.txtOS 确实只会加载一份 inode 和文件内容到内存这一点第二篇会从内核角度详细分析理论上确实 “看到了同一份资源”。但用它通信有三个致命问题速度慢读写要经过完整文件系统路径还涉及刷盘等 IO 开销时序失控谁先写谁先读完全没法控制没有同步机制语义缺失OS 不会为 “文件里出现新数据” 提供任何通知。所以进程通信不用普通文件而是用 OS专门设计的通信机制。不过这个思路本身非常有价值 —— 第二篇你会看到命名管道 FIFO 恰恰就是 “给管道起一个文件路径名”的设计它借了文件的 “壳”内核里走的依然是专属的内存通道。6.2 IPC 和网络通信是一回事吗不是一回事但是一套思想。本系列讲的 IPC 指的是同一台主机上的进程通信管道、System V IPC 等跨主机的进程通信靠Socket网络编程。可以把 Socket 理解为 IPC 的 “广域版”把 “让两个进程看到同一份资源” 的范围从 “一块内存” 扩展到了 “一条网络连接”。两者的编程范式高度相似学完本系列再进入网络系列会非常丝滑。6.3 线程之间需要 IPC 吗严格来说不需要 “进程间” 通信 —— 因为同一进程内的多个线程本来就共享地址空间全局变量、堆内存天然就是 “同一份资源”不需要 OS 帮忙开辟公共区域。但线程之间依然存在同步与互斥问题两个线程同时改一个变量照样打架这对应 POSIX 的互斥量、条件变量、信号量等同步设施。所以可以这样划分认知进程之间先解决 看见IPC 的公共资源再解决 秩序同步互斥 线程之间天然能 看见只需要解决 秩序同步互斥本系列第七篇讲信号量时你会发现它的思想同时适用于进程和线程 —— 这也是为什么学好多线程之前先把 System V 信号量的原理吃透。七、总结与下一篇预告7.1 本篇核心结论进程具有独立性独立地址空间 写时拷贝隔离是操作系统稳定性的基石通信是 “额外申请的特权”IPC 的四大目的数据传输、资源共享、通知事件、进程控制IPC 的本质让互相通信的进程看到同一份由 OS 提供的公共资源通过系统调用使用—— 这个三段式框架看到资源 → OS 提供 → 系统调用使用适用于所有 IPC 方式Linux IPC 全景Unix 原生管道家族、信号→ System V IPC消息队列、共享内存、信号量→ Socket → POSIX IPC演进路线是 “匿名管道 → 命名管道 → System V IPC → Socket → POSIX IPC”。7.2 下一篇预告下一篇进入管道家族《进程通信系列二匿名管道 —— pipe 的本质、fork 继承与四种读写规则》。我们将会看到匿名管道 “披着文件外皮的内存” 到底长什么样struct file inode 缓冲区为什么pipe()必须在fork()之前调用fd 又是如何被子进程 “继承” 的管道四种读写行为逐个实验验证阻塞读、阻塞写、EOF、SIGPIPE。管道是 Unix 最古老的通信方式也是理解后面所有 IPC 的最佳起点 —— 我们第二篇见。