进程间通信入门:匿名管道的使用、阻塞场景与避坑指南
一、什么是进程间通信我们知道操作系统中每个进程都拥有独立的内存空间彼此相互隔离。但实际业务中多个进程往往需要配合完成任务必须交换数据、同步运行状态。由于进程不能直接访问对方内存操作系统便设计了专门的交互方案这就是进程间通信Inter-Process Communication简称 IPC。二、进程通信的目的进程间通信IPC核心目的数据传输进程间互相传递业务数据、文件、指令等信息。资源共享多个进程共用同一文件、内存等系统资源避免重复创建。进程同步协调执行顺序防止竞争冲突保证运行逻辑有序。事件通知一个进程向其他进程发送状态、信号触发对应动作。进程控制管控其他进程的启停、挂起、终止等行为。三、通信行为本质两大类行为本质数据拷贝中转主流方式操作系统在进程地址空间之外开辟中间缓冲区。数据从发送进程拷贝到缓冲区再从缓冲区拷贝到接收进程。代表管道、命名管道、消息队列、Socket。内存区域共享特殊方式操作系统划出一块物理内存映射到多个进程的虚拟地址空间。进程直接读写同一片内存无额外数据拷贝。代表共享内存需搭配信号量做同步。四、通信方法1. 匿名管道1核心原理匿名管道是操作系统在内核空间开辟的一块固定大小的环形缓冲区通过一对文件描述符fd[0]读端、fd[1]写端实现父子进程间的半双工通信。父子之间最常用有血缘关系也能通信单工固定单向永远只能一方发、一方收如广播半双工可切换方向但同一时刻只能单向如对讲机、普通管道全双工双向同时传输收发互不干扰如手机通话2创建管道// 创建管道if(pipe(intfd[2])-1){perror(pipe创建失败);return1;}内核动作创建两个struct file对象都位于内核内存中分配并初始化共享的管道缓冲区配置第一个struct file读端f_op-read→ 指向管道的读。private_data→ 指向上面那个共享的struct inode。不支持写操作不支持lseek。配置第二个struct file写端f_op-write→ 指向管道的写。private_data→ 指向同一个struct inode。不支持读操作不支持lseek。在当前进程的文件描述符表中分配两个槽位第一个空槽位存入读端struct file的指针 →fd[0]。第二个空槽位存入写端struct file的指针 →fd[1]。对比打开文件时创建的struct file:相同点无论是open()还是pipe()内核创建的都是同一类数据结构——struct file对象。它们都遵循“一切皆文件”的设计哲学都有文件操作函数表 (f_op)、私有数据指针等通用字段。不同点它们的作用和内部实现完全不同open()创建的struct file背后连接的是磁盘上的文件或设备、目录等主要功能是读写磁盘数据维护文件偏移量 (f_pos)。pipe()创建的两个struct file读端和写端背后连接的是内核中的管道环形缓冲区(pipe_inode_info)主要功能是进程间通信没有文件偏移量的概念读写行为基于缓冲区而非磁盘。3管道中的环形缓冲区为什么匿名管道要用环形缓冲区因为管道是 “先进先出FIFO” 的字节流用环形缓冲区能最高效、无浪费、无移动地实现 “读一点、写一点、循环利用空间”。其实说是环形本质还是线性只是逻辑结构变成了环形。实现逻辑环形就是当读写指针走到开辟空间的末尾时重新回到空间起始位置。工作流程有固定大小管道默认 64KB写数据 → 写指针往后移读数据 → 读指针往后移读过的数据就“释放”了指针走到末尾 → 回到 0 位置继续读写指针相遇 缓冲区空 / 满为线性时的缺点当读指针往后移之后已经读过的空间就浪费掉了4管道的使用管道的使用只能在父子进程之间。其使用类似于对文件的读写之不过管道有两个struct file一个是读一个是写当创建子进程之后首先要关闭一个struct file只保留一个用来读或写。fd[0]/fd[1]就是他们的文件描述符。charbuf[1024];// 读写缓冲区// 创建子进程pid_tpidfork();if(pid-1){perror(fork失败);return1;}// 子进程读数据if(pid0){// 子进程只需要读关闭写端close(fd[1]);// 从管道读取数据intlenread(fd[0],buf,strlen(buf)1);printf(子进程读取到数据%s\n,buf);// 关闭读端close(fd[0]);return0;}// 父进程写数据else{// 父进程只需要写关闭读端close(fd[0]);// 向管道写入数据char*msgHello, Pipe!;write(fd[1],msg,sizeof(msg));printf(父进程写入数据%s\n,msg);// 关闭写端close(fd[1]);// 等待子进程结束wait(NULL);}5字节流概念核心结论面向字节流 没有边界、没有格式、没有消息头就是一串连续的字节。读多少、写多少、怎么分块完全由进程自己决定。1. 最简单的比喻管道就像一根水管流的是 “水”不是 “一包一包的东西”。你写 100 字节 → 变成连续水流进去你读 20 字节 → 取出前面 20 字节剩下 80 字节还在流里没有分割、没有边界、没有编号、没有结构这就叫字节流。2. 对比一下面向字节流管道写ABCDEFGHIJK读先读5个 → ABCDE再读3个 → FGH剩下 → IJK没有边界怎么读都行连续不断。面向数据报消息队列写[第一条ABC] [第二条DEF]读必须一次读完整一条不能读一半有边界、有格式、有消息头。3. 管道字节流的 3 个特点① 无边界一次写 100 字节对方可以分 10 次读每次读 10 字节。怎么拆、怎么拼管道不管。② 连续、有序像水流一样先进先出顺序不乱。③ 无格式管道只认0101 二进制字节不管是字符串、整数、结构体。4. 最关键的一句话面向字节流 数据没有 “包” 的概念只有 “流” 的概念。写进去是一串读出来可以任意切分管道不负责划分消息。6若进程结束没关闭管道在文件的struct file中有着引用计数f_count专门用来记有几个进程打开了这个文件多一个文件就少一个文件就–。那么管道的struct file自然也有这个引用计数当进程结束管道的f_count--管道的生命周期也就结束了。7管道的阻塞1. 读端阻塞管道为空无数据现象read()调用卡住不返回进程挂起。触发管道缓冲区里没有任何数据且所有写端文件描述符都未关闭。逻辑内核认为还有进程会继续写数据于是读进程休眠等待数据。2. 读端不阻塞正常退出管道为空但所有写端 fd 全部 closeread()直接返回0表示读到文件末尾读进程结束读取。3. 写端阻塞管道缓冲区已满现象write()调用卡住写进程挂起。触发管道环形缓冲区被写满且所有读端 fd 都未关闭。管道默认缓冲区大小常见4096 字节PAGE_SIZE。逻辑缓冲区装不下新数据内核等待读进程取走数据。4. 写端异常所有读端已关闭管道破裂致命场景管道所有读端都close此时再执行write()写进程会收到SIGPIPE 信号默认行为进程直接终止。若捕获 / 忽略 SIGPIPEwrite()返回-1errno EPIPE。俗称管道断裂broken pipe。读全关写操作的杀死情况#includestdio.h#includesys/wait.h#includeunistd.h#includestdlib.hintmain(){intfd[2];charbuffer[1024];if(pipe(fd)-1){perror(pipe);return1;}pid_tpidfork();if(pid-1){perror(fork);return1;}if(pid0){// Child processclose(fd[0]);sleep(1);write(fd[1],child_write,11);exit(0);}else{// Parent processclose(fd[0]);intwstatus;waitpid(pid,wstatus,0);if(WIFEXITED(wstatus)){printf(子进程正常退出\n);}elseif(WIFSIGNALED(wstatus)){// 被信号杀死printf(子进程被信号杀死信号编号%d (SIGPIPE)\n,WTERMSIG(wstatus));}}return0;}