
做 Linux 系统编程这些年最常被问到的一个问题就是两个进程到底怎么传数据。很多同学把 fork 用得很熟进程一出来就各干各的一旦需要协作就卡住了——这背后的原因是进程之间的地址空间是相互隔离的一个进程的变量另一个进程根本看不到。今天我把管道、信号量、共享内存、消息队列这四个最核心的 Linux IPC 机制串起来写一篇完整的实操笔记每个机制都附上我跑过的 C 代码也会重点讲清楚“为什么这样用”以及我踩过的坑。内容从原理到动手从 API 到调试适合正在学系统编程的学生、做多进程架构的工程师以及准备面试想快速复习 IPC 的开发者。1. 为什么需要IPC进程间通信的整体思路1.1 进程天生隔离但架构需要协作每个进程都有自己的虚拟地址空间这是一个基础安全设计。进程 A 里定义一个变量并赋值进程 B 完全感知不到因为 A 修改的是自己地址空间里的内容B 连 A 的地址都访问不了。这个隔离机制的好处是显而易见的一个进程崩溃了不会顺手把别人的内存改坏整个系统不会因为单个进程的问题而崩溃。但坏处也在于此现实里的工程需求几乎不可能靠单个进程完成。Nginx 的 master 进程要调度多个 worker 进程处理并发连接Redis 做 AOF 持久化时要 fork 出子进程来写文件业务系统里生产者进程不断产生数据、消费者进程持续处理数据。这些场景里进程之间必须“对话”。于是内核提供了几种跨进程的数据通道统称进程间通信也就是 IPCInter-Process Communication。你可以把它们理解成给进程开的几扇窗口管道是水管单向流水共享内存是共享黑板大家公用一块区域消息队列是带标签的信箱投递的是有边界的消息信号量则是一把控制进出的闸门它本身不传数据专门管次序和互斥。把这四样东西看成一个体系再去学 API 就有方向了。1.2 四大机制先按“有没有数据内容”分类我在实践里的习惯是先做一次粗分把四个机制分成两组。管道、共享内存、消息队列这仨传输的都是实际数据它们解决的是“怎么把一段内容从一个进程搬到另一个进程”。信号量是另一类它运行时的数据只是一个整数计数器不搬运业务数据回答的是“当前资源能不能用”的问题互斥锁、读者写者限制、限流器这些都是信号量的舞台。分完这两组再在传输数据的那一组里做二次筛选。要看你想传的数据是什么形态是连续的字节流就考虑管道是大块需要频繁读写的数据就考虑共享内存是需要按消息边界、按类型异步投递的就考虑消息队列。这个筛选方法后文第 6 节我会给出一张完整的对照表先记住这个粗分框架就够了。2. 管道与命名管道父子与任意进程间的单向运输线2.1 匿名管道四行代码看懂单向数据流匿名管道是最古老的 IPC 机制之一Shell 里的cmd1 | cmd2用的就是它。它的本质是内核里的一块缓冲区通过pipe()系统调用拿到两个文件描述符pipefd[0]负责读pipefd[1]负责写。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/wait.h int main(void) { int pipefd[2]; pid_t pid; char buf[128]; if (pipe(pipefd) -1) { perror(pipe); exit(EXIT_FAILURE); } pid fork(); if (pid -1) { perror(fork); exit(EXIT_FAILURE); } if (pid 0) { // 子进程负责写先关掉不用的读端 close(pipefd[0]); const char *msg hello from child; write(pipefd[1], msg, strlen(msg) 1); close(pipefd[1]); _exit(0); } else { // 父进程负责读先关掉不用的写端 close(pipefd[1]); ssize_t n read(pipefd[0], buf, sizeof(buf)); if (n 0) { printf(parent received: %s\n, buf); } close(pipefd[0]); wait(NULL); } return 0; }关键的细节在于 fork 之后文件描述符会被子进程继承所以父子进程手里同时握着 pipefd[0] 和 pipefd[1]。如果两边都不关掉自己不需要的那一端读写就会陷入悬念——比如父进程如果不关写端那么即使子进程再也没有数据可写父进程的 read 也不会等到 EOF因为“写端还开着”这个事实本身就足够让 read 继续等待。所以代码里那句close(pipefd[0])、close(pipefd[1])不是仪式感是必须的。管道是单向的这是它的另一个硬约束。数据流只能从写端流向读端。如果业务上父子进程需要双向频繁通信正确的做法是创建两根管道各管一个方向。我见过有人试图在一个管道里来回写最后因为缓冲区和调度原因数据混乱到没法排查那真是自找的麻烦。2.2 命名管道把通信通道公开到文件系统匿名管道有个天生的限制只适用于有亲缘关系的进程之间。因为它没有名字大家只靠 fork 时复制文件描述符来接管没有血缘关系的两个进程根本拿不到那对描述符。命名管道也叫 FIFO解决的就是这个问题。它通过mkfifo()在文件系统里创建一个特殊文件任何进程只要能访问这个路径就可以用open()打开它参与通信。一个典型场景是日志采集进程把日志写进 FIFO另一个分析进程从 FIFO 里读取并做实时筛选两个进程独立启动互不相识但通过路径名完成了数据交换。// 写入端 #include stdio.h #include stdlib.h #include fcntl.h #include sys/stat.h #include unistd.h int main(void) { const char *path /tmp/my_fifo; mkfifo(path, 0666); // 已存在时返回 EEXIST可忽略 int fd open(path, O_WRONLY); if (fd 0) { perror(open); exit(EXIT_FAILURE); } write(fd, data from writer, 16); close(fd); return 0; }// 读取端 #include stdio.h #include stdlib.h #include fcntl.h #include sys/stat.h #include unistd.h int main(void) { const char *path /tmp/my_fifo; mkfifo(path, 0666); int fd open(path, O_RDONLY); if (fd 0) { perror(open); exit(EXIT_FAILURE); } char buf[128]; ssize_t n read(fd, buf, sizeof(buf)); if (n 0) { printf(reader received: %.*s\n, (int)n, buf); } close(fd); unlink(path); // 用完清理特殊文件 return 0; }这个示例里最容易踩的坑就是 open 的阻塞行为。先启动读取端时open(path, O_RDONLY)会一直阻塞直到某个写入端真正打开了这个 FIFO反过来先启动写入端时open(path, O_WRONLY)也会等待读取端出现。这听起来好像没什么大不了但在脚本化部署或自动化测试里经常会出现“进程明明启动了却不往下走”的假死现象排查半天才发现是 open 在等待对端。解决的方法有两种。一种是对 FIFO 使用O_NONBLOCK标志让 open 立即返回后续读写时再根据 EAGAIN 这类错误处理数据未就绪的情况另一种是保证程序启动顺序先起写入端再起读取端或者反过来用 while 循环检测路径可用。生产环境我更倾向于前者代码清晰也不依赖外部启动顺序。2.3 管道实操中我踩过的三个坑第一管道缓冲区是有限的。Linux 管道默认缓冲区的实际大小通常也就在 64KB 量级可以用fpathconf(fd, _PC_PIPE_BUF)查询。如果写端持续写入大量数据而读端迟迟不读写进程就会阻塞在 write 调用上。这不是 bug是背压机制但很多第一次接触的人会误以为程序卡死了。第二写端退出和读退出带来的信号。读端关闭后写端再 write 就会收到 SIGPIPE 信号默认动作是直接终止进程。所以写端如果是循环写入的模式一定要考虑对 SIGPIPE 做捕获处理否则某个时刻读方退出写方也会跟着“神秘死亡”查日志时常常找不到头绪。第三管道传数据是字节流语义是流式的没有消息边界。你 write 了三次读端可能一次就读完了;你 write 一次 100 字节读端也可能分两次读走。如果业务消息本身有固定格式接收端就要自己做协议解析比如加长度前缀、加分隔符。这个问题在“消息队列”一节我会做对比因为那正是消息队列存在的价值之一。3. 信号量不传输数据的闸门控制3.1 信号量的计数器模型与 P/V 操作前面的管道负责搬运数据信号量却是另一个维度的机制。它维护一个整数计数器只回答一个问题当前还能不能进入临界区。信号量有两个原子操作P 操作把计数器减一如果减完后小于零说明资源被占满调用者就阻塞等待;V 操作把计数器加一如果之前有进程在等待则唤醒其中一个。很多人把信号量理解成“锁”这不完全对。值为 1 的信号量确实可以当互斥锁用但信号量的本质是计数器你可以把初值设成 3允许最多 3 个进程同时访问某个连接池或某种有限资源这就是“计数信号量”。想清楚你要的是互斥还是限流再去设初值就不会搞错。Linux 里有两套信号量 APISystem V 信号量用semget/semop/semctlPOSIX 信号量用sem_open/sem_wait/sem_post。System V 版本语法繁琐但功能完整也常见于经典教材;POSIX 版本更像“文件系统里的锁文件”语义直观。这一节我先用 System V 版本讲原理因为它那几步操作更能解释信号量的生命周期。3.2 System V信号量四步实现从semget到semopSystem V 信号量的使用流程可以概括为四步创建或获取信号量集合初始化初值执行 P/V 操作最后清理。#include stdio.h #include stdlib.h #include sys/sem.h #include sys/ipc.h #include unistd.h #include sys/wait.h union semun { int val; struct semid_ds *buf; unsigned short *array; }; static int semid; void sem_p(void) { struct sembuf sb {0, -1, 0}; if (semop(semid, sb, 1) -1) { perror(semop P); exit(EXIT_FAILURE); } } void sem_v(void) { struct sembuf sb {0, 1, 0}; if (semop(semid, sb, 1) -1) { perror(semop V); exit(EXIT_FAILURE); } } int main(void) { key_t key ftok(., 42); semid semget(key, 1, IPC_CREAT | IPC_EXCL | 0666); if (semid -1) { perror(semget); exit(EXIT_FAILURE); } union semun su; su.val 1; // 初值设为 1就是互斥锁 if (semctl(semid, 0, SETVAL, su) -1) { perror(semctl SETVAL); exit(EXIT_FAILURE); } pid_t pid fork(); if (pid 0) { for (int i 0; i 5; i) { sem_p(); printf(child enters critical section, i%d\n, i); usleep(100000); sem_v(); } _exit(0); } else { for (int i 0; i 5; i) { sem_p(); printf(parent enters critical section, i%d\n, i); usleep(100000); sem_v(); } wait(NULL); semctl(semid, 0, IPC_RMID); // 清理信号量 } return 0; }注意这段代码里我加了IPC_EXCL意图是确保创建一个全新的信号量避免连接到别人遗留的旧对象上导致初值不对。生产环境里如果多个进程要共享同一个信号量通常的做法是第一个进程用IPC_CREAT | IPC_EXCL创建并初始化其他进程只用semget获取已有的信号量不做重复创建。这样职责清楚不会因为重复初始化把计数器的值弄乱。8.3 关于 SEM_UNDO 和死锁的经验体会信号量用起来虽然不复杂但实战里的问题往往出在两个地方。第一个是 SEM_UNDO 的“好意”可能变成“坏事”。semop 里可以给 sem_flg 加上 SEM_UNDO作用是如果进程异常退出内核自动把这进程做过的操作对计数器做逆操作防止资源被永远锁死。听着很贴心但实际使用时要非常小心。对于互斥锁来说进程挂了锁被自动释放确实合理;但如果你的业务流程里“进程挂了也别让锁自动消失”恰恰是保护数据库数据一致性的关键那 SEM_UNDO 反而会破坏设计。我的习惯是除非有明确需求否则不用 SEM_UNDO由业务代码自己管理锁的释放和异常处理。第二个是死锁。比如有两把锁 A 和 B进程 1 先拿 A 再拿 B进程 2 先拿 B 再拿 A总有一个瞬间两边各持有一把锁同时等待对方释放另外一把。信号量的 P 操作会阻塞等待所以它并不能自动避免死锁只是把死锁的问题留给了使用者。排查的时候用ipcs -s看信号量集合状态再配合日志确认每个进程卡在哪个 semop 调用上基本就能定位。这也是我反复强调“临界区要尽量短”的原因锁的持有时间越短出问题的概率越低。4. 共享内存速度接近内存的IPC通路4.1 共享内存原理把同一块物理页映射到两个地址空间管道是要把数据从用户态拷到内核缓冲区再从内核缓冲区拷出去两次拷贝。共享内存的思路完全不同内核把同一块物理内存页面同时映射到多个进程的虚拟地址空间里。进程 A 往某个地址写数据进程 B 从自己的地址空间读同一个地址看到的自然是同样的内容整个过程完全没有内核参与数据拷贝。打个比方这就像一间会议室里挂了一块公共白板多个小组的人都能在自己的座位上看同一块板子谁写了字别人立刻能看到。物理层面省掉了搬运所以共享内存是四种 IPC 里性能最高的一种高吞吐场景基本全用它。System V 共享内存的核心流程是shmget创建或获取一块共享内存shmat把它挂接到进程地址空间进程像操作普通指针一样读写shmdt解除映射最后shmctl做清理。C 项目里常见的 boost 共享内存本质也是把 mmap 或 shm_open 这类接口封装了一层底层思路和我这里讲的 System V 版本一致概念上没有任何额外魔法。4.2 实战共享内存信号量组合数据交换共享内存不包含任何同步机制两个进程同时写同一块区域数据就可能损坏。所以标准姿势一定是“共享内存 信号量”组合使用信号量管次序共享内存管数据。我把两者合在一起写一个父子进程交换数据的完整例子。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include sys/ipc.h #include sys/shm.h #include sys/sem.h #include sys/wait.h #define SHM_SIZE 1024 union semun { int val; struct semid_ds *buf; unsigned short *array; }; static int semid; void sem_p(void) { struct sembuf sb {0, -1, 0}; if (semop(semid, sb, 1) -1) { perror(semop P); exit(EXIT_FAILURE); } } void sem_v(void) { struct sembuf sb {0, 1, 0}; if (semop(semid, sb, 1) -1) { perror(semop V); exit(EXIT_FAILURE); } } int main(void) { key_t key ftok(., 101); int shmid shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666); if (shmid -1) { perror(shmget); exit(EXIT_FAILURE); } semid semget(key, 1, IPC_CREAT | IPC_EXCL | 0666); if (semid -1) { perror(semget); exit(EXIT_FAILURE); } union semun su; su.val 1; semctl(semid, 0, SETVAL, su); char *shm (char *)shmat(shmid, NULL, 0); if (shm (char *)-1) { perror(shmat); exit(EXIT_FAILURE); } pid_t pid fork(); if (pid 0) { // 子进程写共享内存 sem_p(); strcpy(shm, hello from shared memory); sem_v(); _exit(0); } else { wait(NULL); // 父进程读共享内存 sem_p(); printf(parent read: %s\n, shm); sem_v(); shmdt(shm); shmctl(shmid, IPC_RMID, NULL); semctl(semid, 0, IPC_RMID); } return 0; }这个例子里如果没有信号量子进程的strcpy和父进程的printf之间就没有 happens-before 关系父进程很可能在子进程还没写完时就抢着读读到的是半个字符串或者乱码。加了信号量之后写操作和读操作被隔离在两个临界区里虽然父子之间仍有前后顺序的要求但信号量保证的是临界区的互斥访问这在多进程、多生产者消费者的真实场景中更有意义。4.3 原子性与同步问题有一点要单独说清楚共享内存本身不保证操作的原子性。哪怕进程 A 写一个 int进程 B 在另一侧读你也不能想当然地认为一定安全。CPU 指令可能是原子的但编译器和 CPU 的乱序执行、缓存一致性等问题会让事情复杂化。更别说写一个结构体或一个长字符串那必然是多条指令中间状态完全可能被别的进程观察到。所以前面组合例子里的信号量不是摆设。在真正的高并发场景里我还会进一步考虑使用原子变量或内存屏障来替代信号量比如用 C11 的stdatomic接口配合mmap映射的共享内存实现无锁读。但这属于进阶话题基础阶段的思路仍然是“信号量管同步共享内存管数据”。如果你的程序追求极致吞吐无锁共享内存绝对值得深入研究但前提是你已经能说清楚内存序的问题否则还是老老实实加锁更安全。5. 消息队列带类型标签的异步消息5.1 System V消息队列四步流程消息队列跟前面几种机制都不一样。它把数据包装成一条一条的“消息”每条消息有自己的类型标签进程之间按类型读取自己关心的内容。这有点像把快递放进一组货架每个货架按编号分区收货人按区号取件而不是把所有包裹混在一起倒出来自己找。System V 消息队列的四个核心调用分别是msgget创建或获取队列、msgsnd发送消息、msgrcv接收消息、msgctl控制与清理。消息结构体长这样struct msg_buf { long mtype; // 消息类型必须是正数 char mtext[128]; // 消息正文大小自定义 };发送和接收的代码示例如下#include stdio.h #include stdlib.h #include string.h #include sys/ipc.h #include sys/msg.h #include sys/types.h #include unistd.h #include sys/wait.h struct msg_buf { long mtype; char mtext[128]; }; int main(void) { key_t key ftok(., 88); int msgid msgget(key, IPC_CREAT | IPC_EXCL | 0666); if (msgid -1) { perror(msgget); exit(EXIT_FAILURE); } pid_t pid fork(); if (pid 0) { struct msg_buf msg; msg.mtype 1; strcpy(msg.mtext, hello from sender); // 第三个参数是正文长度不含 mtype if (msgsnd(msgid, msg, strlen(msg.mtext) 1, 0) -1) { perror(msgsnd); exit(EXIT_FAILURE); } _exit(0); } else { struct msg_buf msg; ssize_t n msgrcv(msgid, msg, sizeof(msg.mtext), 1, 0); if (n -1) { perror(msgrcv); exit(EXIT_FAILURE); } printf(parent received: %s\n, msg.mtext); wait(NULL); msgctl(msgid, IPC_RMID, NULL); } return 0; }这里有个最容易犯的错msgsnd的第三个参数是mtext的长度不是整个结构体的大小。如果你误传成sizeof(struct msg_buf)内核会把你 mtype 后面的内容当成正文的一部分接收端再按mtext读出来就会带上多余的字节。我调试线上问题时亲眼见过一次这种错误现象是消息尾部冒出随机内容。5.2 消息类型与阻塞模式是消息队列的灵魂消息队列最强大的地方就在于消息类型。msgrcv的第四个参数控制读取规则如果传 0就读取队列里最早的一条消息不区分类型;如果传正数 N就读取类型值等于 N 且最早进入队列的那条;如果传负数 -N就读取类型值小于等于 N 的最早一条。这套规则让你可以实现多消费者按需取件不同业务进程监听不同的消息类型互相不抢消息。再把阻塞模式和IPC_NOWAIT拎出来说。msgsnd在队列已满时会阻塞msgrcv在队列为空或没有匹配类型的消息时也会阻塞如果给最后一个参数加上IPC_NOWAIT这两个调用在无法立即执行时会返回 -1errno 依次为 EAGAIN让调用者去做别的事。非阻塞模式在需要“轮询一下有没有消息”的场景里非常实用但要注意别把 CPU 空转变成新的负担更好的是结合多路复用或条件等待机制。还有一个值得说的点消息队列天然有消息边界不会像管道那样出现“两次发送粘成一次读取”的问题。这是它跟管道最本质的区别。代价是每个消息结构固定消息大小限制在系统规定的上限内一般也够用吞吐量不如共享内存。5.3 内核消息队列和分布式MQ的边界这里额外说一句容易混淆的事。今天大家在业务系统里常说的“消息队列”往往指的是 RabbitMQ、Kafka 这些分布式消息中间件它们解决的是跨机器、跨服务、高可用、海量消息的问题。而本文讲的系统调用级消息队列是内核提供的本地 IPC 手段只服务于同一台机器上的进程。两者不是一回事虽然名字都叫消息队列。在本地多进程架构里内核消息队列有它的价值异步解耦、按类型分发、天然消息边界而且没有网络开销。但它也确实有局限比如队列容量有限、不支持集群扩展、没有持久化机制机器重启后队列消息就丢失。所以我的使用习惯是单机内部模块之间传递有明确类型的短消息用内核消息队列;跨机器、需要可靠投递和海量吞吐的场景才把分布式消息中间件请出来。理解了这条边界你就不会在面试里把两者搞混也能更合理地做技术选型。6. 四种IPC怎么选我的选型方法论6.1 一张表看清四种机制到了该做决策的时候。我在实际工作里会把四个机制放在一张表里做对比机制数据形态通信方向性能进程范围同步能力典型场景匿名管道字节流单向中含内核拷贝父子等亲缘进程无命令管道、进程输出重定向命名管道字节流单向中含内核拷贝任意进程无本地日志流、模块间流式数据信号量计数器无数据快任意进程有互斥锁、限流、读者写者控制共享内存自定义结构双向最高无内核拷贝任意进程需配合信号量高吞吐共享数据、缓存、状态同步消息队列带类型消息单向逻辑中含内核拷贝任意进程无按类型异步分发、解耦顺序上我是把信号量和共享内存分开列的因为它本身不传数据。看表也能发现管道和消息队列都经过内核缓冲区丢进缓冲区再取出来所以性能天然不如共享内存。共享内存是唯一不走内核拷贝通道的代价是你要自己处理同步等于性能和复杂度总是成正比的。6.2 选型判断的几条经验依赖表格之外我还有几条经验法则遇到具体项目时直接套用。第一如果只是把一个进程的连续输出传给另一个进程去处理首选管道。它简单、可靠还自带背压机制数据多了写端自然等待不需要额外设计。日志采集、命令执行的流式处理基本都是这个套路。第二如果多个进程要读写同一个数据结构比如全局任务列表、配置版本号、统计数据首选共享内存。吞吐高延迟低但一定要同步。我通常直接配一个信号量前者管数据、后者管互斥分工明确。第三如果交互双方是同一台机器上的模块消息是明显分条的而且发送方不关心接收方此刻是否就绪就用消息队列。比如任务分发器把订单任务按类型发给不同处理进程这种场景天然适合消息队列。第四如果只是需要保证某段代码同时只能有一个进程执行或者限制并发访问数那就信号量单独出场。它不复杂复杂的是你要把临界区设计的足够小否则锁粒度变大以后性能就变差了。7. 常见问题与调试技巧7.1 高频坑位记录Linux IPC 的坑不少很多还是隐蔽的。我自己在实战和帮别人排查问题时反复遇到下面几类整理成速查表问题现象直接原因解决方案管道 read 阻塞不返回写端没关闭EOF 永远不会到达确保每条链路的另一端都被正确 close写端信号量持有进程 killed共享资源被看护只有用 SEM_UNDO 自动补偿设计时决定是否需要 SEM_UNDOmsgrcv 读到乱码msgsnd 的第三个参数错误参数应为 mtext 长度而不是结构体 sizeof共享内存读取到半写状态缺少同步机制用信号量或原子操作保证读写互斥FIFO open 阻塞open 等待对端出现使用 O_NONBLOCK 或控制启动顺序同一 IPC 对象误用旧数据key 相同导致 attach 到旧资源创建时用 IPC_EXCL传 ke 结束还有一类高频问题来自与fork的交互。子进程里如果使用了之前父进程printf过的缓冲区子进程里再 exit 会导致缓冲区被 flush 两次输出重复。我的习惯是子进程一律用_exit()结束避开标准库的清理流程这也是我在前面的代码里一直用_exit(0)的原因。看起来是个小细节但能让你少调试很久。7.2 调试与清理工具排查 System V IPC 问题时ipcs命令是必须优先掌握的。ipcs -m查看共享内存ipcs -q查看消息队列ipcs -s查看信号量。这些命令会列出 key、ID、权限、发送接收次数等信息能让你一眼看出哪些 IPC 对象被创建了但没被清理。如果发现僵尸对象堆积比如程序异常退出后共享内存还在再用ipcrm -m shmid、ipcrm -q msqid、ipcrm -s semid手动清理。这类资源一旦泄漏重启程序也不会自动消失因为它们生命周期跟随内核不跟随进程。我在开发环境就不止一次见到过同一套 key 反复创建共享内存最后把所有内存段挤爆的场景。所以写代码时务必把shmctl(...IPC_RMID)、msgctl(...IPC_RMID)、semctl(...IPC_RMID)放进资源清理路径里养成随手清理的习惯。调试系统调用层行为时strace -f也很有用。它能跟踪 fork 出来的所有进程把每个pipe、write、shmget的返回值和 errno 打在眼前。曾经有个“消息读取超时”的问题所有人盯着业务代码看半天最后我用 strace 一看发现某个进程的msgrcv一直阻塞在类型不匹配的消息上因为业务代码里类型值传错了正负号。这种问题靠日志往往要绕很远strace 直接就能定位到具体那行系统调用。最后我还想多分享一个小经验设计 IPC 方案时先把“谁创建、谁清理、谁释放锁”这三件事明确写下来再写代码。很多出问题的项目API 用的没错死就死在资源归属和清理责任不清上。像共享内存这种内核级资源如果每个进程都认为自己该负责清理就可能出现一个进程正常干活、另一个进程把共享内存段删掉的诡异事故。每个 IPC 对象指定一个管理者其他人只负责使用这个约定能帮你省掉大量线上故障排查的时间。