目录一、进程间通信(IPC)进程间通信的目的进程间通信的发展管道System VPOSIX进程间通信的本质二、管道匿名管道什么是匿名管道?理解匿名管道pipe接口从文件描述符角度理解匿名管道​编辑从内核视角理解匿名管道读写方法代码的实现匿名管道的五种特性:特性一:特性二:特性三:特性四:特性五:同步性互斥性匿名管道的4种情况:三、总结我们之前学习的进程都是就单个进程而言独立性很强如果进程之间要进行协同工作呢 ?我们都知道父子进程数据共享父进程的数据子进程可以看到但是因为有写时拷贝的问题父子之间无法相互传递数据所以在Linux下就有了进程间通信通过进程间通信来互相传递数据并且进程间通信的方式有多种比如管道 / 命名管道消息队列共享内存信号量信号等方式下面我们一一来看:一、进程间通信(IPC)进程间通信的目的进程间通信Inter-Process Communication 简称 IPC数据传输一个进程需要将它的数据发送给另一个进程资源共享多个进程之间共享同样的资源通知事件一个进程需要向另一个或一组进程发送消息通知它它们发生了某种事件如进程终止时要通知父进程进程控制有些进程希望完全控制另一个进程的执行如 Debug 进程此时控制进程希望能够拦截另一个进程的所有陷入和异常并能够及时知道它的状态改变狭义上的进程间通信就是对数据进行交互互相传数据、交换信息比如说管道、消息队列、共享内存、socket就是进程 A 把数据发给 进程 B进程 B 进行处理这是传输数据。广义上的进程间通信还包括进程间的 控制 / 同步 / 互斥这些不一定是传数据而是协调、打招呼、上锁比如信号量几乎不传数据主要做锁、做互斥、做同步。进程间通信的发展管道管道是最古老的进程间通信的(IPC)方式之一。起源于1970 年代的早期 UNIX是 UNIX 最原生的 IPC 方式。是 shell 中|管道符的底层实现设计思想就是 “把一个进程的输出连到另一个进程的输入”。管道有匿名管道和命名管道等。并且管道是 UNIX “一切皆文件” 思想的典型体现把进程间的数据传输抽象成文件读写接口简单、易用。System VSystem V IPC 诞生于 1980 年代的 System V UNIX比管道晚了约 10 年。是为了弥补管道只能单向、半双工、只能在亲缘进程间通信的不足新增了消息队列、信号量、共享内存三种更通用的 IPC。优点就是System V兼容性极强几乎所有类 UNIX 系统都支持。但是System V IPC也相对较老所以系统调用接口比较老旧。POSIXPOSIX 是 POSIX 标准委员会制定的标准化 IPC 规范目的是统一不同 UNIX 变种的 IPC 接口提升跨平台可移植性。它在 System V IPC 的基础上做了简化和改进接口更贴近文件操作风格更易用、更现代。现代类 UNIX 系统Linux、FreeBSD、macOS等都必须实现这套接口。System V 和 POSIX 都是通信标准可以理解为 System V IPC 多用于老系统或旧软件而POSIX IPC是新式、标准化、现代通信的标准。所谓的“IPC 标准”就是 Linux/UNIX 内核规定的“进程间通信的接口规范”。就是告诉进程怎么传数据、怎么同步、怎么共享内存。标准不是文件、不是库是一套“接口 规则”。进程间通信的本质进程间通信的本质其实就是让两个互相隔离的进程看到同一份不属于任何进程、只属于操作系统的公共资源一块内核 / 共享内存资源。这块资源不属于进程 A也不属于进程 B。而是由操作系统内核管理进程不能直接访问必须通过系统调用进行读写、操作。所以不同的通信方式(管道、System V、POSIX)就有属于各自的系统调用本质就是使用各自的系统调用规范来操作这块公共资源。二、管道匿名管道什么是匿名管道?匿名管道是从一个进程连接到另一个进程的数据流是 UNIX 中最古老的 IPC 形式。本质上是由内核管理的一块缓冲区作为进程间数据传输的 “中转站”不属于任何一个进程只能通过系统调用访问。以上面这副图为例:who 进程将标准输出重定向到管道的写入端把登录用户信息写入内核缓冲区。wc -l 进程将标准输入重定向到管道的读取端从缓冲区中读取数据并统计行数。内核负责管理管道的读写同步、数据缓存保证数据有序传递。理解匿名管道匿名管道是单向通信的也就是说它只支持管道的一端读另一端写换句话说也就是数据只能单向流动。管道是内核维护的内存缓冲区Linux 把它抽象成了“文件”用 struct file 表示但它不是磁盘上的文件只是遵循“一切皆文件”的设计思想方便用文件接口来操作。为了实现单向读写分离内核给管道创建了两个 struct file 对象一个代表读端只能读不能写)一个代表写端只能写不能读)这两个 struct file 共享同一个管道缓冲区只是操作权限不同。1. 为什么要把管道抽象成文件因为 Linux 希望所有 I/O 设备、资源接口都统一这样进程就不用关心底层是文件、键盘、屏幕、管道、socket从而只用一套系统调用比如 open/read/write/close 来进行操作。这就是 Linux 最经典的设计思想一切皆文件。而管道的本质是内核级内存缓冲区为了统一管理内核就把它包装成看起来像文件操作起来像文件的形式进而管道就可以用 struct file 描述并用文件描述符fd 访问。2. struct file 到底是什么struct file 不是文件本身而是“打开的文件描述对象”。它记录了这个打开的资源是什么文件管道socket?)操作函数read/write 指针及引用计数多少个 fd 指向它)任何被打开的资源内核都会创建一个 struct file。所以打开普通文件会产生 struct file打开管道也会产生 struct file 文件描述对象。3. 管道如何对应 struct file一个匿名管道 1 个内核缓冲区 2 个 struct file。读端表示一个 struct file (只支持 read)写端表示一个 struct file只支持 write)两个 struct file 指向同一个管道缓冲区。它们不是两个文件是同一个管道的两个入口。pipe接口pipe的功能是创建一个匿名管道用于亲缘进程间的单向通信。参数是一个输出型参数 pipefd[2]一个长度为 2 的 int 数组用于存储管道的两个文件描述符pipefd[0]是读端只能从管道读取数据pipefd[1]是写端只能向管道写入数据。返回值成功时返回 0并在 pipefd 数组中填充两个有效的文件描述符。失败则返回 -1并设置 errno 错误码。下面我们用一下这个函数:进程的文件描述符表 fd_array 是一个指针数组其中每一个元素都是 struct file* 类型的指针这意味着进程能操作的所有 I/O 对象普通文件、设备、管道、socket 等在内核中统一由 struct file 来表示。管道本身并不是磁盘上的真实文件但 Linux 为了遵循一切皆文件的设计思想将管道这种内核内存缓冲区也抽象成文件体系中的一员因此必须用 struct file 来描述它。而匿名管道设计上是单向数据流必须严格区分只能读的一端和只能写的一端这两端在行为、权限、操作函数上完全不同读端不支持写写端不支持读。Linux 不会把“可读可写”混在同一个 struct file 里破坏语义因此内核为同一个管道缓冲区创建了两个独立的 struct file一个专门表示读端一个专门表示写端。这两个 struct file 共享底层同一个管道缓冲区但各自拥有独立的操作模式正好可以被分别放入进程 fd_array 的两个下标 3 和 4(因为标准输入0标准输出1标准错误2这三个是进程启动时默认就被占用)形成一对读、写文件描述符让用户态可以用统一的 read / write 接口操作管道同时又保证管道单向通信的语义。为什么读端和写端分别要用一个 struct file 表示?在 Linux 中之所以管道的读端和写端必须分别单独用一个 struct file 来表示根本原因不在于它是不是“文件”而在于 Linux 的统一 I/O 模型规定任何一个独立、具有明确操作权限的 I/O 入口都必须对应一个 struct file。 管道的读端是一个只能执行 read 操作的入口写端是一个只能执行 write 操作的入口这两者在权限、操作函数集、文件模式f_mode上完全不同且无法被兼容地塞进同一个 struct file 内。为了保证管道单向通信的语义正确实现同时让读端和写端都能通过文件描述符fd被进程统一访问Linux 内核必须为同一个管道缓冲区创建两个独立的 struct file 对象一个专门封装读端的只读语义与操作函数另一个专门封装写端的只写语义与操作函数。正是基于这一统一的 I/O 抽象规则读端才表现为一个 struct file而进程则通过 fd 数组中的下标分别指向这两个 struct file从而实现对管道的安全、单向访问。从文件描述符角度理解匿名管道上图展示了Linux 匿名管道从创建到父子进程单向通信的完整生命周期是站在文件描述符的角度:1. 父进程创建管道 : 父进程调用 pipe(int fd[2]) 系统调用向内核发起创建管道的请求。内核在内部完成管道的实际创建分配一块内核内存缓冲区即管道本体用于暂存进程间通信数据。并为管道生成两个 struct file 对象一个代表读端(仅支持 read 操作)一个代表写端(仅支持 write 操作)二者共享同一个管道缓冲区。内核在父进程的文件描述符表(fd_array)中找到两个最小的空闲下标(通常为 3 和 4)将其分别指向读端和写端的 struct file并将这两个文件描述符返回给父进程 fd[0] 3(读端)、fd[1] 4(写端)。此时父进程同时持有管道的读、写两端可对管道进行读写操作。2. 父进程 fork 出子进程 : 父进程调用 fork() 创建子进程子进程会完整复制父进程的文件描述符表。复制完成后子进程的 fd[0] 3 和 fd[1] 4 与父进程指向完全相同的管道读端和写端 struct file 即父子进程共享同一个管道资源。此时父子进程都拥有管道的读写权限通信方向尚未明确数据流向可能混乱。3. 关闭冗余文件描述符建立单向通信 : 为实现父进程向子进程单向通信需要关闭冗余的文件描述符父进程调用 close(fd[0])关闭管道读端仅保留写端 fd[1] 4只能向管道写入数据。子进程调用 close(fd[1])关闭管道写端仅保留读端 fd[0] 3只能从管道读取数据。最终形成父进程只写、子进程只读的单向数据流通道父进程通过 write(4, ...) 向管道写入数据子进程通过 read(3, ...) 从管道读取数据同时保证了通信语义的清晰与安全。从内核视角理解匿名管道下面我们再从内核视角理解匿名管道的本质:1. 首先管道并不是磁盘上的文件它的本质是内核态的一块内存缓冲区(通常由若干物理内存页构成图中表现为“数据页”)用于临时存放进程间通信的数据。这个缓冲区由内核统一管理用户态进程无法直接访问只能通过系统调用间接读写。它在内部被组织为环形缓冲区实现高效的流式数据读写。2. 为了遵循“一切皆文件”的设计内核将管道封装进标准的文件模型内核将管扫抽象为文件用 struct file inode 封装inode节点代表管道的内核实体它不对应磁盘文件而是管理管道缓冲区的元数据(如等待队列、锁、缓冲区状态)是两个操作入口的共同“根”。内核再为管道创建两个独立的 struct file分别代表读端和写端读端 struct file的 f_mode 标记为只读f_op 指向管道读操作函数集仅允许 read 操作。写端 struct file 的 f_mode 标记为只写f_op 指向管道写操作函数集仅允许 write 操作。两个 struct file 的 f_inode 指针指向同一个管道 inode这是它们属于同一个管道的核心标志。3. 父进程调用 pipe() 时内核将这两个 struct file 挂载到父进程的文件描述符表(fd_array)中分配两个文件描述符(如 3 和 4)。父进程 fork() 子进程时子进程会完整复制父进程的文件描述符表因此子进程的文件描述符也指向这两个struct file从而与父进程共享同一个管道缓冲区。这就是父子进程能通过管道通信的底层基础它们通过各自的 struct file 入口操作同一个内核缓冲区。4. 最后一步单向通信管道的单向通信特性是由内核在 struct file 层面强制实现的写端 struct file 不提供读操作读端 struct file 不提供写操作。任何尝试在写端读、读端写的行为都会被内核直接拒绝从根本上保证了数据流只能单向流动。关闭冗余文件描述符如父进程关读端、子进程关写端)是为了进一步明确通信方向并正确触发管道的 EOF 机制(当所有写端关闭时读端 read()返回 0。上面这幅图是 Linux 匿名管道结构图 : 整个大框代表 Linux 内核空间所有管道的内核对象都驻留在这里用户态进程无法直接访问。左侧方框表示写端 struct file这是进程持有管道写端的操作句柄。struct file 里的 f_inode 指向内核中管道的索引节点inode。f_mode 标记为只写 (O_WRONLY)。f_op 指向管道写操作函数集(只实现 write 。右侧方框表示读端 struct file这是进程持有管道读端的操作句柄。struct file 里的 f_inode 指向内核中管道的索引节点inode。f_mode 标记为只读 (O_RDONLY)。f_op指向管道读操作函数集(只实现 read 。中间红色框是管道内核实体就是管道的本质由以下两部分物理绑定组成1. struct inode (索引节点)代表管道在 Linux 通用文件模型中的身份。不对应磁盘文件而是内核中的特殊 inode。包含管道的元数据、等待队列、锁机制。2. struct pipe_inode_info (管道私有数据)包含指向内存数据页 (Page Cache) 的指针。管理环形缓冲区的读写指针、数据长度。真正存储通信数据的地方。称通常将二者合称为管道的 inode 对象它是连接所有 struct file 的核心根节点。读端 f_inode → 管道 inode表示读端的 struct file 通过 f_inode 指针关联到管道的内核实体。写端 f_inode → 管道 inode表示写端的 struct file 通过 f_inode 指针关联到同一个管道内核实体。两个 struct file 虽然独立但指向同一个 inode这标志着它们属于同一个管道。内核通过这种指针关联强制实现了同一个内核缓冲区的双向单向数据流访问。读写方法代码的实现对于父进程执行的写方法要写入数据前需准备一段缓冲区 buffer 用于承载待写入内容。写入的数据通常是动态变化的比如可以先写入一段固定提示字符串再拼接当前进程的 PID最后附上一个自增的计数变量以此模拟实际进程通信中多变的业务数据。采用循环间隔 1 秒的方式写入信息使用 C 语言的安全格式化函数 snprintf 将动态内容拼接成完整字符串。snprintf 会自动在字符串末尾添加 \0 作为 C 语言字符串结束标记但文件 / 管道本身并不识别 \0它们只存储字节流。因此调用 write 时只需使用 strlen 获取有效字符长度即可无需额外 1 携带 \0避免将无意义的结束符写入通信通道。对于子进程的读方法需先准备接收数据的缓冲区 buffer通过循环调用 read 系统调用从管道读端读取数据。read 的返回值有明确含义返回值 0成功读取到对应字节数的数据可将缓冲区内容打印或处理返回值 0表示所有写端已关闭管道中无剩余数据可退出循环结束通信返回值 0表示读取发生错误如文件描述符无效、信号中断等需打印错误信息后退出。需要注意的是在这段代码中变量 cnt 由父进程初始化fork() 创建子进程时子进程会共享父进程的内存空间(包括 cnt 所在的内存页内核会将该页标记为只读以避免立即拷贝。当子进程执行 cnt-- 时这一写操作会触发硬件异常内核随即执行写时拷贝Copy-On-Write为子进程分配新的物理内存页将原 cnt 的值拷贝到新页再让子进程在新页上完成 cnt-- 修改最终实现父子进程拥有各自独立的 cnt 变量彼此修改互不影响。运行结果:运行结果也直观验证了匿名管道的单向通信机制与写时拷贝特性子进程通过管道向父进程传递格式化数据输出中计数器从 10 递减到 1、PID 保持不变既体现了管道的同步阻塞与有序传输特性也证明子进程对继承自父进程的变量cnt的修改触发了写时拷贝使得父子进程数据相互独立最终完成了高效的亲缘进程间通信。匿名管道的五种特性:特性一:1. 匿名管道只能进行单向通信特性二:2. 匿名管道只能用来进行具有血缘关系的进程之间的通信包括父子进程兄弟进程爷孙进程即同一个祖先 fork 出来的所有进程因为这些进程中都是同一份文件描述符表因为匿名管道没有名字只能靠 fork 时继承文件描述符来共享。只要两个进程来自同一个祖宗祖宗创建管道 → 子孙们全部继承就能通信。证明:如图所示执行命令 sleep 10000 | sleep 3000 | sleep 4000 后通过 ps 命令查看到三个 sleep 进程PID 分别为1684403,1684404,1684405。它们的 PPID父进程ID均为 1677237即 Bash 进程说明它们是兄弟进程属于有血缘关系的进程。这三个兄弟进程能够成功通过竖线 | 内核实现为匿名管道进行通信。由此证明匿名管道只能用于具有血缘关系的进程间通信如父子、兄弟、爷孙等由同一个祖先 fork 出来的进程。特性三:3. 匿名管道是面向字节流的管道里的数据是连续的字节序列没有“消息边界”。比如子进程分10次写管道父进程可能一次读完所有字节也可能分多次读内核不保证“一次写对应一次读”。之前上面的代码里子进程循环写 hello bit父进程读到的是一整串字节流而不是10条独立消息。并且数据严格按照写入顺序被读取先写的字节先被读到不会乱序。特性四:4. 匿名管道的生命周期随进程匿名管道在Linux内核中以 struct file 结构体形式存在其生命周期由引用计数与进程文件描述符表共同决定匿名管道是内核内存中的动态对象由 struct file 结构体描述该结构体核心着维护读端和写端的引用计数。struct file 中的引用计数记录当前有多少个文件描述符fd指向该管道对象。每新增一个fd(如pipe()创建、fork()继承)引用计数1每关闭一个fd (close())或进程退出释放fd引用计数-1。进程通过文件描述符表持有管道fd这是管道存活的唯一依据。 fork() 时子进程会继承父进程的文件描述符表从而共享管道引用进程退出时会自动关闭所有持有的fd减少引用计数。当读端引用计数和写端引用计数均归0时无任何进程持有管道读写端内核判定管道无引用立即销毁 struct file 对象释放内存。特性五:5. 管道通对于多进程而言是自带同步与互斥机制的同步性同步读进程尝试读空管道时会阻塞写进程尝试写满管道时会阻塞直到对方完成操作。父进程读得快子进程写得慢就会造成父进程阻塞因为管道缓冲区被读空了没有数据可读父进程调用 read() 时会被内核阻塞挂起直到子进程写入新数据才会被唤醒继续读子进程写得快父进程读得慢就会造成子进程阻塞因为管道缓冲区被写满了没有空间可写子进程调用 write() 时会被内核阻塞挂起直到父进程读出数据腾出空间才会被唤醒继续写缓冲区为空时read() 阻塞等待写入。缓冲区满时write() 阻塞等待读取。这种“你慢我等你快我停”的机制天然实现了读写双方的同步保证数据不会丢失、不会乱序。互斥性互斥同一时刻对管道的关键操作(如读写缓冲区)是互斥的只有一个进程能操作管道缓冲区不会出现多个进程同时修改导致数据混乱。当多个进程同时往管道写数据时内核会让它们排队一个写完另一个才能写保证字节流的完整性。当多个进程同时从管道读数据时内核也会保证数据不会被重复读取每个字节只会被一个进程读到。原子写保证对于小于 PIPE_BUF 通常是 4096 字节的写操作内核保证是原子的要么全部写入要么完全不写不会被其他进程打断。和同步性的区别:同步性解决的是快等慢的问题读空阻塞、写满阻塞保证数据不丢失、顺序不乱。互斥性解决的是并发冲突的问题保证多个进程同时操作时数据不会被破坏、不会出现混乱。匿名管道的4种情况:1. 子进程写的慢父进程就要阻塞等等管道有数据父进程才能读2. 子进程写的快父进程不读管道一旦被写满子进程就必须阻塞了3. 读端在读写端写完就关闭读端再读的话就会读完从而会读到空字符串 此时read的返回值就是0表明读端已读到文件结尾证明:当子进程完成 10 次数据写入后主动关闭管道写端父进程先读完管道缓冲区中剩余的数据之后继续调用read()时因所有写端已关闭read()会返回0标志读到管道 “文件结尾”直观验证了管道作为单向字节流的特性 —— 当所有写端关闭后读端会收到 EOF 信号内核自动完成通信收尾保证了进程间数据传输的完整性与有序性。4. 写端一直写读端fd已关导致不会读取时此时操作系统就会杀掉写端的进程上面的代码展示了匿名管道的典型保护机制父进程读取一次数据后关闭管道读端子进程仍在死循环中持续向管道写入数据内核检测到读端已全部关闭、写入操作无效后向子进程发送编号为 13 的SIGPIPE信号直接终止子进程最终父进程通过waitpid回收子进程资源输出signal: 13直观验证了 “读端关闭后写端持续写入会被系统杀死” 的管道通信规则这是 Linux 内核为避免资源浪费而设计的标准、正常的安全行为。子进程的退出状态显示 13不是退出码而是代表子进程被操作系统发送的 13 号信号SIGPIPE强行终止这属于异常退出不是正常的 return 0 退出。因为匿名管道的读端全部关闭后写端仍然持续写入数据内核会判定为无效操作并发送 SIGPIPE 信号杀死进程这种由信号终止的进程都属于异常退出不会有正常的退出码。下面我们再来看一下匿名管道较为标准规范的正常退出流程:上面的代码这张图完整展示了匿名管道正常结束通信的完整流程子进程将写入次数设为 5 次循环写完后主动关闭管道写端并打印write endpoint quit!父进程持续读取数据当子进程关闭写端后父进程读完缓冲区剩余数据再次调用read()时会得到返回值0代表读到管道 “文件结尾”随后打印read pipe end of file并退出循环最终通过waitpid()回收子进程资源。这一过程直观验证了管道的字节流特性与内核同步机制当所有写端关闭后读端会收到EOF信号read()返回0标志通信结束保证了数据传输的完整性与进程资源的安全回收。三、总结本文介绍了Linux进程间通信(IPC)的基本概念与匿名管道的实现机制。进程间通信主要包括数据传输、资源共享、事件通知和进程控制四种目的。匿名管道是最古老的IPC方式用于亲缘进程间的单向通信。文章详细阐述了匿名管道的创建过程、内核数据结构、读写机制以及五种特性单向通信、血缘关系限制、字节流传输、随进程的生命周期以及自带的同步互斥机制。通过代码示例验证了管道的读写行为、写时拷贝特性以及异常处理流程展示了Linux内核如何通过文件抽象和引用计数管理管道资源。谢谢大家的观看