ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

【金丹·71】进程间通信:管道、共享内存、信号 【金丹·71】进程间通信管道、共享内存、信号码农修仙传 · 金丹期 · 第71篇我是玄芯散人带你从炼气修到大乘。境界标识╔══════════════════════════════════╗ ║ 金丹期 · 第71篇 ║ ║ 进程间通信管道、 ║ ║ 共享内存、信号 ║ ║ pipe/fifo/shm/signal ║ ║ 预计阅读18分钟 ║ ╚══════════════════════════════════╝修仙引入上一篇把每位弟子住独立洞府的事讲透了每位弟子只能看到自己的虚拟地址空间互相之间隔着一道无形的墙。可修真界不是孤岛弟子之间要传信要约斗要买卖急了还得紧急求救。怎么把一段话传给另一位弟子怎么让两位弟子共用一块灵田怎么在半夜喊醒一位闭关的弟子修真界里这是传讯使的事情操作系统里这就是 IPCInter-Process Communication进程间通信。管道用来单向流水传信共享内存用来两人同看一卷经信号用来紧急敲门消息队列用来按类收件套接字用来跨宗门传信。这些传讯使各有各的脾气各有各的跑腿范围。这一篇把它们的脾气摸清楚让你下次再选传讯方式时不抓瞎。硬核主体为什么进程间需要通信修真界的传讯每位弟子住在宗门划给他的独立洞府里每位弟子有自己的虚拟地址空间上一篇把这件事讲得很清楚。隔离带来安全与简单但也带来代价两位弟子没法直接读对方的脑。修真界里两位弟子要协作得有规矩地交换消息。操作系统里有五条规矩对应五类 IPC。第一条管道pipe。父子弟子之间单向传信的竹筒。一端写、一端读写满就阻塞读空也阻塞。第二条FIFO命名管道。两个不相关的进程之间用一根带名字的竹筒传信。文件系统里有路径谁打开谁用。第三条共享内存shared memory。两位弟子共看同一卷经书速度最快因为没有数据搬运。第四条信号signal。弟子之间紧急传信。一边给另一边发一个数字编号另一边的接收函数被立刻打断。第五条消息队列与套接字。消息队列是贴标签的竹筐跨进程收发的快递套接字是飞剑能跨主机送信。修真比喻传讯使各有分工。管道是府内信使共享内存是公用典籍信号是惊堂木敲一下消息队列是带签收的快递套接字是跨界飞剑。管道父子弟子的竹筒管道是 Linux 里最简单的 IPC 机制就是一块内核缓冲区加两个文件描述符一个写端、一个读端。pipe(int fd[2])一次系统调用就建好这对描述符。父进程先建管道再fork子进程。父子进程都拿到这对描述符约定父写子读或者父读子写另一端关掉。// pipe_demo.c#includeunistd.h#includestdio.h#includestring.h#includesys/wait.hintmain(void){intfd[2];if(pipe(fd)0){perror(pipe);return1;}pid_tpidfork();if(pid0){// 子进程只读close(fd[1]);charbuf[64];intnread(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这就是 EOFEnd Of File文件结束。所有读端关掉之后再写会触发 SIGPIPE 信号进程默认被杀。修真比喻管道是父子弟子之间一根中空竹筒。父端写、子端读竹筒有上限65536 字节左右写满就堵。读端读到竹筒空就等父端把竹筒口封了子端就知道信没了停止等。命名管道 FIFO不相关进程的带名竹筒匿名管道只能在有亲缘关系的进程之间用因为 fd 是 fork 出来的。FIFO也叫命名管道解决这个问题在文件系统里建一个特殊文件谁都能open它。# mkfifo 建一个命名管道mkfifo/tmp/xianren_fifo# 一个终端写echo剑气传书/tmp/xianren_fifo# 另一个终端读cat/tmp/xianren_fifoFIFO 的用法与匿名管道几乎一致。区别只在创建方式pipe()改mkfifo()/open()路径换了。FIFO 也遵守字节流与阻塞这两条规矩。open一个 FIFO 时默认阻塞直到另一端也open才返回这也是常见的同步手段。修真比喻FIFO 是宗门告示板上挂出来的一根竹筒谁都能去灌一口信谁都能去舀一口读。竹筒还是同一根规矩没有边界写满堵、空了等。共享内存最快的 IPC没有之一管道要经过内核两次拷贝用户 → 内核 → 用户。共享内存省掉这两步。两位进程把同一段物理内存挂到各自的虚拟地址空间里CPU 直接读那一段写了对方立刻看到没有数据搬运。共享内存在 Linux 里有两种主流实现。第一种是 POSIX 共享内存用/dev/shm下的文件加mmap。shm_open建一个共享内存对象ftruncate设大小mmap挂到进程地址空间。// shm_posix.c#includesys/mman.h#includesys/stat.h#includefcntl.h#includeunistd.h#includestdio.h#includestring.hintmain(void){// 建或打开一个共享内存对象名字 /xianren_shmintfdshm_open(/xianren_shm,O_CREAT|O_RDWR,0666);ftruncate(fd,4096);// 设大小为 4KB// 挂到本进程地址空间char*bufmmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);if(bufMAP_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 (精简版)#includesys/ipc.h#includesys/shm.h#includestdio.h#includestring.hintmain(void){intidshmget(0x1234,4096,IPC_CREAT|0666);char*bufshmat(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是终端按 CtrlC 时发的SIGSEGV是段错误SIGKILL是强制杀死SIGCHLD是子进程退出SIGALRM是定时器到时。// signal_demo.c#includesignal.h#includestdio.h#includeunistd.hvoidon_sigint(intsig){// 收到 CtrlC 时执行write(STDOUT_FILENO,\n弟子告辞\n,14);_exit(0);}intmain(void){signal(SIGINT,on_sigint);// 注册 SIGINT 处理函数教学用 signal生产推荐 sigactionwhile(1){puts(弟子在修炼...);sleep(1);}return0;}跑起来按 CtrlC会打印弟子告辞再退出。注册的处理函数被打断后由内核调度执行。信号有两个特别需要记住的约束。第一个是信号安全。信号处理函数里能调用的函数是受限的printf这种用全局缓冲区的函数不安全应该用write或者标志位加主循环判断。第二个是不可重入。同一个信号在处理函数还没返回时再次到达标准信号默认丢弃后续到达的同号信号只记一次实时信号才会排队。SIGKILL和SIGSTOP不能被捕获、不能被阻塞是内核给进程的最后通牒。修真比喻信号是惊堂木。某弟子执事敲一下所有弟子停下手头的事去看执事写了什么数字。数字含义由宗门约定1集合2解散9立刻闭关被打断。敲惊堂木不传数据只传紧急程度和约定编号。修真界里惊堂木还有一个特点惊堂木敲下去不能撤销。即使弟子正在闭关一旦惊堂木敲了某条立刻打断。这就是异步通知的意思。消息队列贴标签的快递消息队列把消息作为单位传每条消息有类型和数据。msgsnd往队列里塞一条msgrcv从队列里按类型取一条。它在进程退出时会保留消息默认适合留给下一个接手的人这种情形。// msgq_demo.c (精简)#includesys/msg.h#includestdio.h#includestring.hstructmsg{longtype;chartext[64];};intmain(void){intqidmsgget(0x5678,IPC_CREAT|0666);structmsgm{.type1};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;}消息队列和管道比多了类型这一项。读端可以只收 type1 的其他留在队列里。修真比喻消息队列是带标签的竹筐。筐里每封信都贴了类型标签紧急/普通/汇报收信人按标签取走自己想看的那类。筐放在告示板上下一个来的弟子也能取。套接字跨机器的飞剑套接字socket才是 IPC 的终极形态因为它能跨主机。Linux 上的 socket API 起源于 BSD 1983 年那一套本地 IPC 用 AF_UNIX也叫 AF_LOCAL族跨主机用 AF_INETIPv4/ AF_INET6IPv6。// unix_socket_server.c (精简)#includesys/socket.h#includesys/un.h#includestdio.h#includeunistd.hintmain(void){intsrvsocket(AF_UNIX,SOCK_STREAM,0);structsockaddr_unaddr{.sun_familyAF_UNIX};strcpy(addr.sun_path,/tmp/xianren.sock);unlink(/tmp/xianren.sock);bind(srv,(structsockaddr*)addr,sizeof(addr));listen(srv,1);intcliaccept(srv,NULL,NULL);charbuf[64];intnread(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 重新读配置按 CtrlC 终止进程这些是信号的强项。要给消息分类、留待后续处理消息队列合适。它的类型字段让消费端可以只拿我关心的消息也不会因为发送方立刻退出而丢。跨主机情形只有套接字能搞定。分布式服务基于 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
返回列表