目录
[System V](#System V)
我们之前学习的进程,都是就单个进程而言,独立性很强,如果进程之间,要进行协同工作呢 ?
我们都知道父子进程数据共享,父进程的数据子进程可以看到,但是因为有写时拷贝的问题,父子之间无法相互传递数据,所以在Linux下就有了进程间通信,通过进程间通信来互相传递数据,并且进程间通信的方式有多种,比如管道 / 命名管道 ,消息队列 ,共享内存 ,信号量 ,信号等方式,下面我们一一来看:
一、进程间通信(IPC)
进程间通信的目的
进程间通信 ,Inter-Process Communication 简称 IPC
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)
- 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
狭义上的进程间通信就是对数据进行交互,互相传数据、交换信息,比如说管道、消息队列、共享内存、socket就是进程 A 把数据发给 进程 B,进程 B 进行处理,这是传输数据。
广义上的进程间通信还包括进程间的 控制 / 同步 / 互斥,这些不一定是传数据,而是协调、打招呼、上锁,比如信号量几乎不传数据,主要做锁、做互斥、做同步。
进程间通信的发展
管道
管道是最古老的进程间通信的(IPC)方式之一。起源于1970 年代的早期 UNIX,是 UNIX 最原生的 IPC 方式。是 shell 中 | 管道符的底层实现,设计思想就是 "把一个进程的输出连到另一个进程的输入"。管道有匿名管道和命名管道等。并且管道是 UNIX "一切皆文件" 思想的典型体现:把进程间的数据传输抽象成文件读写,接口简单、易用。
System V
System V IPC 诞生于 1980 年代的 System V UNIX,比管道晚了约 10 年。是为了弥补管道只能单向、半双工、只能在亲缘进程间通信的不足,新增了消息队列、信号量、共享内存三种更通用的 IPC。优点就是System V兼容性极强,几乎所有类 UNIX 系统都支持。但是System V IPC也相对较老,所以系统调用接口比较老旧。
POSIX
POSIX 是 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 共享同一个管道缓冲区,只是操作权限不同。
- 为什么要把管道抽象成文件?
- 因为 Linux 希望所有 I/O 设备、资源,接口都统一,这样进程就不用关心底层是文件、键盘、屏幕、管道、socket,从而只用一套系统调用比如 open/read/write/close 来进行操作。这就是 Linux 最经典的设计思想:一切皆文件。而管道的本质是内核级内存缓冲区,为了统一管理,内核就把它包装成看起来像文件,操作起来像文件的形式,进而管道就可以用 struct file 描述并用文件描述符fd 访问。
- struct file 到底是什么?
- struct file 不是文件本身,而是"打开的文件描述对象"。它记录了这个打开的资源是什么(文件?管道?socket?),操作函数(read/write 指针)及引用计数(多少个 fd 指向它),任何被打开的资源,内核都会创建一个 struct file。所以打开普通文件会产生 struct file,打开管道也会产生 struct file 文件描述对象。
- 管道如何对应 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 匿名管道从创建到父子进程单向通信的完整生命周期,是站在文件描述符的角度:
-
父进程创建管道 : 父进程调用 pipe(int fd[2]) 系统调用,向内核发起创建管道的请求。内核在内部完成管道的实际创建,分配一块内核内存缓冲区(即管道本体,用于暂存进程间通信数据)。并为管道生成两个 struct file 对象,一个代表读端(仅支持 read 操作),一个代表写端(仅支持 write 操作),二者共享同一个管道缓冲区。内核在父进程的文件描述符表(fd_array)中找到两个最小的空闲下标(通常为 3 和 4),将其分别指向读端和写端的 struct file,并将这两个文件描述符返回给父进程: fd[0] = 3(读端)、fd[1] = 4(写端)。此时,父进程同时持有管道的读、写两端,可对管道进行读写操作。
-
父进程 fork 出子进程 : 父进程调用 fork() 创建子进程,子进程会完整复制父进程的文件描述符表。复制完成后,子进程的 fd[0] = 3 和 fd[1] = 4 与父进程指向完全相同的管道读端和写端 struct file ,即父子进程共享同一个管道资源。此时,父子进程都拥有管道的读写权限,通信方向尚未明确,数据流向可能混乱。
-
关闭冗余文件描述符,建立单向通信 : 为实现父进程向子进程单向通信,需要关闭冗余的文件描述符,父进程调用 close(fd[0]),关闭管道读端,仅保留写端 fd[1] = 4,只能向管道写入数据。子进程调用 close(fd[1]),关闭管道写端,仅保留读端 fd[0] = 3,只能从管道读取数据。最终形成父进程只写、子进程只读的单向数据流通道:父进程通过 write(4, ...) 向管道写入数据,子进程通过 read(3, ...) 从管道读取数据,同时保证了通信语义的清晰与安全。
从内核视角理解匿名管道

下面我们再从内核视角理解匿名管道的本质:
-
首先管道并不是磁盘上的文件,它的本质是内核态的一块内存缓冲区(通常由若干物理内存页构成,图中表现为"数据页"),用于临时存放进程间通信的数据。这个缓冲区由内核统一管理,用户态进程无法直接访问,只能通过系统调用间接读写。它在内部被组织为环形缓冲区,实现高效的流式数据读写。
-
为了遵循"一切皆文件"的设计,内核将管道封装进标准的文件模型,内核将管扫抽象为文件,用 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,这是它们属于同一个管道的核心标志。
-
父进程调用 pipe() 时,内核将这两个 struct file 挂载到父进程的文件描述符表(fd_array)中,分配两个文件描述符(如 3 和 4)。父进程 fork() 子进程时,子进程会完整复制父进程的文件描述符表,因此子进程的文件描述符也指向这两个struct file,从而与父进程共享同一个管道缓冲区。这就是父子进程能通过管道通信的底层基础:它们通过各自的 struct file 入口,操作同一个内核缓冲区。
-
最后一步单向通信,管道的单向通信特性是由内核在 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内核如何通过文件抽象和引用计数管理管道资源。
谢谢大家的观看

