news 2026/9/26 18:25:43

【金丹·71】进程间通信:管道、共享内存、信号

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【金丹·71】进程间通信:管道、共享内存、信号

【金丹·71】进程间通信:管道、共享内存、信号

码农修仙传 · 金丹期 · 第71篇
我是玄芯散人,带你从炼气修到大乘。


境界标识

╔══════════════════════════════════╗ ║ 金丹期 · 第71篇 ║ ║ 进程间通信:管道、 ║ ║ 共享内存、信号 ║ ║ pipe/fifo/shm/signal ║ ║ 预计阅读:18分钟 ║ ╚══════════════════════════════════╝

修仙引入

上一篇把每位弟子住独立洞府的事讲透了,每位弟子只能看到自己的虚拟地址空间,互相之间隔着一道无形的墙。可修真界不是孤岛,弟子之间要传信,要约斗,要买卖,急了还得紧急求救。怎么把一段话传给另一位弟子?怎么让两位弟子共用一块灵田?怎么在半夜喊醒一位闭关的弟子?

修真界里这是传讯使的事情,操作系统里这就是 IPC(Inter-Process Communication,进程间通信)。管道用来单向流水传信,共享内存用来两人同看一卷经,信号用来紧急敲门,消息队列用来按类收件,套接字用来跨宗门传信。这些传讯使各有各的脾气,各有各的跑腿范围。这一篇把它们的脾气摸清楚,让你下次再选传讯方式时不抓瞎。


硬核主体

为什么进程间需要通信:修真界的传讯

每位弟子住在宗门划给他的独立洞府里,每位弟子有自己的虚拟地址空间,上一篇把这件事讲得很清楚。隔离带来安全与简单,但也带来代价:两位弟子没法直接"读对方的脑"。修真界里两位弟子要协作,得有规矩地交换消息。

操作系统里有五条规矩,对应五类 IPC。

第一条,管道(pipe)。父子弟子之间单向传信的竹筒。一端写、一端读,写满就阻塞,读空也阻塞。

第二条,FIFO(命名管道)。两个不相关的进程之间用一根带名字的竹筒传信。文件系统里有路径,谁打开谁用。

第三条,共享内存(shared memory)。两位弟子共看同一卷经书,速度最快,因为没有数据搬运。

第四条,信号(signal)。弟子之间紧急传信。一边给另一边发一个数字编号,另一边的接收函数被立刻打断。

第五条,消息队列与套接字。消息队列是贴标签的竹筐,跨进程收发的快递;套接字是飞剑,能跨主机送信。

修真比喻:传讯使各有分工。管道是府内信使,共享内存是公用典籍,信号是惊堂木敲一下,消息队列是带签收的快递,套接字是跨界飞剑。

管道:父子弟子的竹筒

管道是 Linux 里最简单的 IPC 机制,就是一块内核缓冲区加两个文件描述符:一个写端、一个读端。pipe(int fd[2])一次系统调用就建好这对描述符。父进程先建管道,再fork子进程。父子进程都拿到这对描述符,约定父写子读或者父读子写,另一端关掉。

// pipe_demo.c#include<unistd.h>#include<stdio.h>#include<string.h>#include<sys/wait.h>intmain(void){intfd[2];if(pipe(fd)<0){perror("pipe");return1;}pid_tpid=fork();if(pid==0){// 子进程:只读close(fd[1]);charbuf[64];intn=read(fd[0],buf,sizeof(buf));write(STDOUT_FILENO,buf,n);// 把收到的话打印出来close(fd[0]);return0;}// 父进程:只写close(fd[0]);constchar*msg="修真界里有什么\n";write(fd[1],msg,strlen(msg));close(fd[1]);wait(NULL);// 等子进程收完return0;}

跑一下输出"修真界里有什么",管道跑通。

管道有两条值得记住的属性。第一个是字节流,没有消息边界。连续写两次"abc"和"def",读端可能一次读到"abcdef",也可能两次读到"abc"和"def"。调用方要自己定边界(长度前缀或分隔符)。第二个是阻塞。缓冲区满时再写,写端阻塞;缓冲区空时再读,读端阻塞。所有写端关掉之后,读端read返回 0,这就是 EOF(End Of File,文件结束)。所有读端关掉之后,再写会触发 SIGPIPE 信号,进程默认被杀。

修真比喻:管道是父子弟子之间一根中空竹筒。父端写、子端读,竹筒有上限(65536 字节左右),写满就堵。读端读到竹筒空就等;父端把竹筒口封了,子端就知道"信没了",停止等。

命名管道 FIFO:不相关进程的带名竹筒

匿名管道只能在有亲缘关系的进程之间用,因为 fd 是 fork 出来的。FIFO(也叫命名管道)解决这个问题:在文件系统里建一个特殊文件,谁都能open它。

# mkfifo 建一个命名管道mkfifo/tmp/xianren_fifo# 一个终端写echo"剑气传书">/tmp/xianren_fifo# 另一个终端读cat/tmp/xianren_fifo

FIFO 的用法与匿名管道几乎一致。区别只在创建方式:pipe()改mkfifo()/open(),路径换了。

FIFO 也遵守字节流与阻塞这两条规矩。open一个 FIFO 时默认阻塞,直到另一端也open才返回,这也是常见的同步手段。

修真比喻:FIFO 是宗门告示板上挂出来的一根竹筒,谁都能去灌一口信,谁都能去舀一口读。竹筒还是同一根规矩,没有边界,写满堵、空了等。

共享内存:最快的 IPC,没有之一

管道要经过内核两次拷贝(用户 → 内核 → 用户)。共享内存省掉这两步。两位进程把同一段物理内存挂到各自的虚拟地址空间里,CPU 直接读那一段,写了对方立刻看到,没有数据搬运。

共享内存在 Linux 里有两种主流实现。第一种是 POSIX 共享内存,用/dev/shm下的文件加mmap。shm_open建一个共享内存对象,ftruncate设大小,mmap挂到进程地址空间。

// shm_posix.c#include<sys/mman.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>#include<stdio.h>#include<string.h>intmain(void){// 建或打开一个共享内存对象,名字 /xianren_shmintfd=shm_open("/xianren_shm",O_CREAT|O_RDWR,0666);ftruncate(fd,4096);// 设大小为 4KB// 挂到本进程地址空间char*buf=mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);if(buf==MAP_FAILED){perror("mmap");return1;}// 第一次跑:写// 后续跑:读if(fork()==0){// 子进程:读sleep(1);// 等父写完printf("子进程读到: %s\n",buf);munmap(buf,4096);close(fd);}else{// 父进程:写strcpy(buf,"灵力数据");wait(NULL);munmap(buf,4096);close(fd);shm_unlink("/xianren_shm");}return0;}

第二种是 System V 共享内存,shmget/shmat/shmdt/shmctl这一套老接口。shmget给一个 key 创建或打开共享内存段,返回一个标识符;shmat把它挂到当前进程;shmdt解除挂接;shmctl控制(删除、改权限等)。功能上和 POSIX 那套等价,只是接口老了三十年。

// shm_sysv.c (精简版)#include<sys/ipc.h>#include<sys/shm.h>#include<stdio.h>#include<string.h>intmain(void){intid=shmget(0x1234,4096,IPC_CREAT|0666);char*buf=shmat(id,NULL,0);if(fork()==0){sleep(1);printf("读到: %s\n",buf);shmdt(buf);}else{strcpy(buf,"灵力传输");wait(NULL);shmdt(buf);shmctl(id,IPC_RMID,NULL);// 删除共享内存段}return0;}

修真比喻:共享内存是宗门设在大殿里的一卷公用经书,两位弟子各派一个分身去翻同一卷。分身看到的内容永远一致,谁翻了一页,另一位的分身立刻看到新页。速度最快,因为不用传信使。

共享内存的代价:没有内建同步。两位弟子同时改同一行,分身会读到不一致的内容。必须自己加锁,POSIX 信号量或 System V 信号量都行。这是后面 077 锁家族那一篇的内容。

进程B

进程A

同一组物理页

同一组物理页

虚拟地址空间

挂接区

虚拟地址空间

挂接区

共享物理页帧

信号:进程间的惊堂木

管道和共享内存都是"传输数据",信号只传一个整数编号,含义由收发双方约定。SIGINT是终端按 Ctrl+C 时发的,SIGSEGV是段错误,SIGKILL是强制杀死,SIGCHLD是子进程退出,SIGALRM是定时器到时。

// signal_demo.c#include<signal.h>#include<stdio.h>#include<unistd.h>voidon_sigint(intsig){// 收到 Ctrl+C 时执行write(STDOUT_FILENO,"\n弟子告辞\n",14);_exit(0);}intmain(void){signal(SIGINT,on_sigint);// 注册 SIGINT 处理函数(教学用 signal;生产推荐 sigaction)while(1){puts("弟子在修炼...");sleep(1);}return0;}

跑起来按 Ctrl+C,会打印"弟子告辞"再退出。注册的处理函数被打断后由内核调度执行。

信号有两个特别需要记住的约束。第一个是"信号安全"。信号处理函数里能调用的函数是受限的,printf这种用全局缓冲区的函数不安全,应该用write或者标志位加主循环判断。第二个是"不可重入"。同一个信号在处理函数还没返回时再次到达,标准信号默认丢弃后续到达的同号信号(只记一次),实时信号才会排队。SIGKILL和SIGSTOP不能被捕获、不能被阻塞,是内核给进程的"最后通牒"。

修真比喻:信号是惊堂木。某弟子执事敲一下,所有弟子停下手头的事去看执事写了什么数字。数字含义由宗门约定:1=集合,2=解散,9=立刻闭关被打断。敲惊堂木不传数据,只传紧急程度和约定编号。

修真界里惊堂木还有一个特点:惊堂木敲下去不能撤销。即使弟子正在闭关,一旦惊堂木敲了某条,立刻打断。这就是异步通知的意思。

消息队列:贴标签的快递

消息队列把"消息"作为单位传,每条消息有类型和数据。msgsnd往队列里塞一条,msgrcv从队列里按类型取一条。它在进程退出时会保留消息(默认),适合"留给下一个接手的人"这种情形。

// msgq_demo.c (精简)#include<sys/msg.h>#include<stdio.h>#include<string.h>structmsg{longtype;chartext[64];};intmain(void){intqid=msgget(0x5678,IPC_CREAT|0666);structmsgm={.type=1};if(fork()==0){msgrcv(qid,&m,sizeof(m.text),1,0);printf("子收到: %s\n",m.text);}else{strcpy(m.text,"修真任务");msgsnd(qid,&m,sizeof(m.text),0);wait(NULL);msgctl(qid,IPC_RMID,NULL);}return0;}

消息队列和管道比,多了"类型"这一项。读端可以"只收 type=1 的",其他留在队列里。

修真比喻:消息队列是带标签的竹筐。筐里每封信都贴了类型标签(紧急/普通/汇报),收信人按标签取走自己想看的那类。筐放在告示板上,下一个来的弟子也能取。

套接字:跨机器的飞剑

套接字(socket)才是 IPC 的终极形态,因为它能跨主机。Linux 上的 socket API 起源于 BSD 1983 年那一套,本地 IPC 用 AF_UNIX(也叫 AF_LOCAL)族,跨主机用 AF_INET(IPv4)/ AF_INET6(IPv6)。

// unix_socket_server.c (精简)#include<sys/socket.h>#include<sys/un.h>#include<stdio.h>#include<unistd.h>intmain(void){intsrv=socket(AF_UNIX,SOCK_STREAM,0);structsockaddr_unaddr={.sun_family=AF_UNIX};strcpy(addr.sun_path,"/tmp/xianren.sock");unlink("/tmp/xianren.sock");bind(srv,(structsockaddr*)&addr,sizeof(addr));listen(srv,1);intcli=accept(srv,NULL,NULL);charbuf[64];intn=read(cli,buf,sizeof(buf));write(STDOUT_FILENO,buf,n);close(cli);close(srv);unlink("/tmp/xianren.sock");return0;}

把AF_UNIX换成AF_INET再加上 IP + 端口,就能跨主机通信。这正是 084 网络编程那一篇要展开的内容,本篇先提一句:套接字是修真界的飞剑,能在宗门之间传信。

修真比喻:套接字是修真界的飞剑。本地用竹筒(AF_UNIX 文件路径当地址),跨宗用罗盘定位(AF_INET IP+端口)。想发给哪台机器的哪个弟子,把信绑在飞剑上指定坐标即可。

各 IPC 机制对比:什么情形用什么

修真界传讯使有五位。怎么挑?给一张速查表。

机制传输粒度跨主机速度适用范围
匿名管道 pipe字节流否中父子进程简单传数据
命名管道 FIFO字节流否中不相关进程单向传数据
共享内存 shm内存页否极快大数据量传输、低延迟场合
信号 signal整数编号否快异步通知、强制控制
消息队列 msgq结构化消息否中带类型的消息、跨进程持久
套接字 socket字节流/消息是中/慢跨主机、本地复杂通信

挑选思路。

要传的数据量大、追求最低延迟,选共享内存。视频处理可以用,内存数据库可以用,要做零拷贝日志还是用。

只是简单的命令触发或父子进程传一小段配置,管道最省事。bash的cmd1 | cmd2就是匿名管道。

要"远程控制"或"紧急打断",用信号。守护进程收到 SIGHUP 重新读配置,按 Ctrl+C 终止进程,这些是信号的强项。

要给消息分类、留待后续处理,消息队列合适。它的类型字段让消费端可以"只拿我关心的",消息也不会因为发送方立刻退出而丢。

跨主机情形只有套接字能搞定。分布式服务基于 socket,远程 RPC 基于 socket,跨数据中心通信也是 socket。

修真比喻:选传讯使的法门有三步。第一步看距离:府内传信是管道,跨府传信是套接字。第二步看时效:传大宗货物(共享内存)一站到底,递紧急口令(信号)一敲即达。第三步看规矩:消息要贴标签上筐(消息队列),裸传一串字节就用管道。

修真界里这一条规则最为常用:"数据量大就共享内存,简单通信用管道,紧急打断用信号,跨主机用套接字。"四句话记牢,面试遇到 IPC 选择题基本稳。


修仙术语对照表

修仙术语技术现实本篇位置
传讯使IPC 进程间通信为什么需要通信
父子弟子竹筒匿名管道 pipe管道
告示板挂的竹筒命名管道 FIFO命名管道
写满堵、空了等管道阻塞行为管道
信没了的标志读端 EOF管道
共用一卷经书共享内存 shared memory共享内存
分身翻经书mmap 挂接共享内存共享内存
System V 老规矩shmget/shmat 旧接口共享内存
惊堂木信号 signal信号
最后通牒SIGKILL/SIGSTOP 不可捕获信号
贴标签的竹筐消息队列 msgq消息队列
跨宗飞剑套接字 socket套接字
府内传信AF_UNIX 本地套接字套接字
罗盘定位AF_INET IP+端口套接字
选传讯使三步法IPC 选择口诀对比

进阶条件

看完这一篇到能向别人讲清"五种 IPC 各用在什么情形",差这几条:

  • 能讲清匿名管道为什么只能在有亲缘关系的进程间用(fd 是 fork 出来的)
  • 能讲清管道字节流特性的实际后果(调用方要自己定边界,长度前缀或分隔符)
  • 能讲清管道什么时候触发 SIGPIPE(所有读端都关掉再写)
  • 能说出共享内存为什么是最快的 IPC(省掉内核两次拷贝)
  • 能讲清共享内存为什么必须自己加同步(多进程并发改同一段会不一致)
  • 能列出 mmap 共享内存与 shmget 共享内存的接口差异
  • 能说出 SIGKILL 和 SIGSTOP 为什么不能被捕获(内核的最后通牒)
  • 能讲清信号处理函数为什么不能调 printf(不可重入 + 全局缓冲区)
  • 能说出消息队列与管道比多出来的一项能力(消息类型 + 持久)
  • 能讲清套接字为什么是 IPC 的终极形态(AF_UNIX 本地 + AF_INET 跨主机)

最后一条是金丹期对"IPC 选择"的"分水岭"。面试里被问"什么情形用什么 IPC",能直接说出"数据量大用共享内存,简单通信用管道,紧急打断用信号,跨主机用套接字"这套四句口诀,这一关就过了。


下期预告 + 互动

上一篇把虚拟内存与这一篇把 IPC 都讲完了。可操作系统还要管一样东西:弟子辛辛苦苦攒的灵石怎么存到宗门仓库里?宗门仓库怎么组织?下一篇讲文件系统,把数据落到硬盘上的整套规矩拆开看。inode、目录树这些都会展开,VFS与ext4也都会讲到,看完之后再有人问"文件是怎么存到硬盘上的",你能直接答出来。

现在问你:

🔍 跑一下ls -l /proc/self/fd/,看看你自己的进程打开了哪些 fd。除了 0、1、2 这三件套之外还有没有别的 fd?跑一个命令管道(比如ls / | grep xren),用pidof拿到grep的 pid,再ls -l /proc/<pid>/fd/看看 pipe 的两端是谁。

⚙️ 你有没有遇到过"父子进程通信数据丢失"或者"管道读端卡死"的诡异问题?通常是某端没关掉对的 fd,导致read永远不返回。下次遇到先用lsof -p <pid>看 fd 表,逐个排查。

评论区聊聊你跟进程间通信打过的交道。

我是玄芯散人,带你从炼气修到大乘。


本文是「码农修仙传」系列第71篇。系列导航见 xren.ren

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 18:25:11

区块链钱包核心解析:从私钥助记词到冷热钱包的安全实践

1. 钱包里没有“币”&#xff1a;先把这个最核心的认知建立起来很多人第一次接触区块链钱包时&#xff0c;脑子里装的是物理钱包的画面——一个皮夹子&#xff0c;里面插着几张钞票、几枚硬币。这个类比在区块链世界里完全是误导。区块链钱包里根本不存在任何“币”&#xff0c…

作者头像 李华
网站建设 2026/9/26 18:25:04

SpringBoot3整合FastJSON2:configureMessageConverters配置实战与避坑指南

SpringBoot 3 项目升级后&#xff0c;原来在 SpringBoot 2 里用得挺顺的 FastJson 突然不干活了&#xff0c;接口要么返回一串 Jackson 的默认格式&#xff0c;要么直接报错&#xff0c;排查几圈下来发现核心问题就出在configureMessageConverters这个配置方法上。这篇内容就围…

作者头像 李华
网站建设 2026/9/26 18:22:56

Kimi为何把8+9算成18?大语言模型的数学盲点与避坑指南

1. 一次让人怀疑人生的Kimi计算翻车1.1 现场还原&#xff1a;我问了什么&#xff0c;它回了什么前两天我在电脑前整理一份数据表&#xff0c;顺手想用Kimi验证一个非常简单的算式&#xff1a;8加9到底等于多少。问题刚敲出去我就觉得自己有点多余&#xff0c;这种题连小学生都能…

作者头像 李华
网站建设 2026/9/26 18:22:00

S7-1500编程语言选型指南:LAD、FBD、SCL如何选择与混用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 18:21:56

SQL 2000.zip 处理指南:识别、安装、恢复与迁移

简介&#xff1a;该压缩包为SQL Server 2000相关资源包&#xff0c;面向需要维护旧系统或学习早期数据库技术的IT人员、DBA及数据库初学者。包体约400.83MB&#xff0c;采用zip格式封装&#xff0c;便于离线保存与查阅。目前已有140人学习。资源内容聚焦SQL 2000的核心功能体系…

作者头像 李华