Linux 进程间通信之管道基础解析 —— 匿名管道的原理与实现
🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:
文章目录
前言:
在 Linux 系统中,进程是资源分配的基本单位,各个进程拥有独立的地址空间,进程间的隔离性让数据无法直接互通,而进程间通信(IPC)就是打破这种隔离、实现进程数据交互和协同工作的核心技术。管道作为 Unix 系统中最古老的 IPC 方式,也是 Linux 下最基础、最常用的进程间通信手段,其设计贴合 Linux一切皆文件的核心思想,简单易用且能满足亲缘进程间的通信需求。本文将从管道的基础概念出发,深入解析匿名管道的创建、工作原理,从文件描述符和内核视角带你吃透匿名管道的底层逻辑。
声明:看到这里博主就准备更换一下自己的配置了,首先是语言主要会用cpp了,然后云服务器从Centos到Ubuntu,文本编辑器选择的vscode和远端进行连接使用。后续要是有时间的话博主会出一期关于配置切换的文章然后链接放在这里。大家可以自己先去查查怎么做,其实也是很简单的,并不复杂。
一. 进程间通信基础认知
在学习管道之前,我们需要先明确进程间通信的核心目的和分类,建立对 IPC 技术的整体认知,这能帮助我们更好地理解管道的设计初衷和应用场景。
1.1 进程间通信的核心目的
进程间通信的本质是实现进程间的数据交互、资源共享和事件协同,具体可分为四个方面:
- 数据传输:一个进程将自身数据发送给另一个进程,是最基础的 IPC 需求;
- 资源共享:多个进程共享同一份系统资源(如文件、内存),提高资源利用率;
- 通知事件:进程向其他进程发送事件通知,如子进程退出时通知父进程、进程完成任务后通知调度进程;
- 进程控制:一个进程对另一个进程进行执行控制,如调试进程拦截目标进程的异常和陷入,实时获取其状态。

1.2 进程间通信的发展与分类
Linux 的 IPC 技术从 Unix 继承并不断发展,整体可分为三大类,管道是其中最基础的一类:
- 管道:包括
匿名管道(pipe)和命名管道(FIFO),是最基础的 IPC 方式,基于文件系统实现; - System V IPC:包括共享内存、消息队列、信号量,由 System V 系统引入,基于内核的 IPC 资源管理实现;
- POSIX IPC:遵循 POSIX 标准的 IPC 方式,是对 System V IPC 的改进,包括 POSIX 共享内存、消息队列、信号量等。
管道作为最原始的 IPC 方式,虽然功能简单,但却是理解 Linux 进程间通信和文件系统的关键,也是实现其他复杂 IPC 的基础。

二. 管道的基础概念
2.1 管道的定义
管道是一种半双工的数据流通信方式,本质是内核中的一块缓冲区,它将一个进程的标准输出与另一个进程的标准输入相连,形成一条单向的数据流通道。我们可以把管道理解为进程间的 “一根水管”,数据从一端写入,从另一端读出,实现单向的通信。
在 Linux 命令行中,我们经常使用的管道符|就是管道的典型应用,例如who | wc -l:
who进程的标准输出被重定向到管道的写端;wc -l进程的标准输入被重定向到管道的读端;
内核中的管道缓冲区作为中间介质,完成两个进程间的数据传递。
我们还可以使用下图中这样的实验来看一下管道

- 从图中我们可以看出最后他们三个指令的父进程都是bash的,他们之间是具有血缘关系的进程
2.2 管道的核心特性(最后总结部分的图片里更全点,可以着重看那个)
管道的设计贴合 Linux一切皆文件的思想,其核心特性可总结为(在文章的最后我们还会有更全面点的总结,这里可以简单看下):
- 半双工通信:数据只能沿一个方向流动,若需双向通信,需创建两个管道;
- 基于缓冲区:管道的实质是内核缓冲区,数据写入后暂存于内核,直到被另一个进程读取;
- 文件式操作:管道通过文件描述符操作,读写接口与文件一致(read/write),符合 Linux 文件操作规范;
- 亲缘进程专属:匿名管道仅支持具有共同祖先的亲缘进程(父进程与子进程、兄弟进程)间通信。
引入:
关于匿名管道的两个初步理解图,可以看完再继续往下细节的学习



三. 匿名管道的创建与 API
匿名管道是最基础的管道类型,通过系统调用pipe创建,其核心 API 简单且易用,是实现亲缘进程间通信的首选。
3.1 匿名管道的创建函数
#include<unistd.h>intpipe(int pipefd[2]);函数参数
pipefd:整型数组,是输出型参数,用于保存管道的读、写文件描述符:pipefd[0]:管道的读端,仅用于读取管道中的数据;- pipefd[1]:管道的写端,仅用于向管道中写入数据。
返回值
- 成功:返回 0;
- 失败:返回 - 1,并设置
errno表示错误原因。
注意:调用pipe函数的进程会同时持有管道的读端和写端,若要实现两个进程间的单向通信,需要在进程创建后关闭各自无用的文件描述符,避免数据读写异常

3.2 匿名管道的简单使用示例
下面的示例实现了一个基础的匿名管道通信:从键盘读取数据写入管道,再从管道读取数据输出到屏幕,直观展示管道的读写操作。
#include<cstdio>#include<cstdlib>#include<cstring>#include<unistd.h>intmain(){int fds[2];char buf[100];int len;if(pipe(fds)==-1){ std::perror("make pipe"); std::exit(1);}while(std::fgets(buf,100,stdin)){ len = std::strlen(buf);if(write(fds[1], buf, len)!= len){ std::perror("write to pipe");break;} std::memset(buf,0x00,sizeof(buf));if((len =read(fds[0], buf,100))==-1){ std::perror("read from pipe");break;}if(write(1, buf, len)!= len){ std::perror("write to stdout");break;}}return0;}该示例中,进程自身同时完成管道的写和读操作,虽然未实现跨进程通信,但清晰展示了管道的基本读写流程:通过fd[1]写,通过fd[0]读,操作接口与普通文件完全一致。
四. 基于 fork 的匿名管道跨进程通信
匿名管道本身由单个进程创建,要实现跨进程通信,需要借助fork函数创建子进程 —— 子进程会继承父进程的文件描述符表,从而与父进程共享同一个管道的读、写端,这是匿名管道实现亲缘进程通信的核心原理。

4.1 fork 共享管道的核心原理
fork函数创建的子进程会复制父进程的文件描述符表,包括父进程创建的管道读、写端文件描述符,因此父子进程会共享同一个内核管道缓冲区,实现数据互通。其核心步骤分为三步:
- 父进程创建管道:父进程调用pipe创建管道,持有fd[0](读)和fd[1](写)两个文件描述符;
- 父进程 fork 创建子进程:子进程继承父进程的文件描述符表,同样持有管道的fd[0]和fd[1];
- 关闭无用的文件描述符:根据通信方向,父、子进程分别关闭无用的读 / 写端,实现单向通信。
例如要实现父进程读、子进程写,则:
- 父进程关闭写端
fd[1],仅保留读端fd[0]; - 子进程关闭读端
fd[0],仅保留写端fd[1]。 注意下面图中的是父写子读,不过基本逻辑都一样

4.2 从文件描述符视角理解管道通信
从文件描述符的角度,我们可以更清晰地看到父子进程共享管道的过程,以父读子写为例:
✅️ 步骤 1:父进程创建管道
父进程的文件描述符表中,0、1、2 分别为标准输入、标准输出、标准错误,pipe创建的管道分配到 3(读端fd[0])和 4(写端fd[1])。
父进程:0(tty)1(tty)2(tty)3(pipe读)4(pipe写)✅️ 步骤 2:父进程 fork 创建子进程
子进程复制父进程的文件描述符表,此时父子进程的文件描述符 3、4 均指向同一个内核管道缓冲区。
父进程:0(tty)1(tty)2(tty)3(pipe读)4(pipe写) 子进程:0(tty)1(tty)2(tty)3(pipe读)4(pipe写)核心关键点:父子进程的文件描述符指向同一个内核管道缓冲区,这是进程间能通过管道通信的根本原因;关闭无用描述符则是为了保证通信的单向性,避免出现数据读写的混乱。
✅️ 步骤 3:关闭无用文件描述符
父进程关闭写端 4,子进程关闭读端 3,此时管道形成单向的 “子写父读” 通道,数据只能从子进程写入,父进程读出。
父进程:0(tty)1(tty)2(tty)3(pipe读)- 子进程:0(tty)1(tty)2(tty)-4(pipe写)核心关键点:父子进程的文件描述符指向同一个内核管道缓冲区,这是进程间能通过管道通信的根本原因;关闭无用描述符则是为了保证通信的单向性,避免出现数据读写的混乱。
4.3 子写父读的完整实战示例以及四个场景分析(场景分析挺重要的)
下面的示例实现了子进程向管道写入字符串,父进程从管道读取并打印的功能,是 “子写父读” 的标准实现:
#include<iostream>#include<string>#include<unistd.h>// 子进程 wvoidWriteData(int wfd){int cnt =1; pid_t id =getpid();while(true){sleep(1); std::string message ="hello father process, "; message +="cnt: "+ std::to_string(cnt++)+", my pid is: "+ std::to_string(id);write(wfd, message.c_str(), message.size());}}// 父进程:rvoidReadData(int rfd){char inbuffer[1024];while(true){ ssize_t n =read(rfd, inbuffer,sizeof(inbuffer)-1);if(n >0){sleep(5); inbuffer[n]='\0'; std::cout <<getpid()<<"# "<< inbuffer << std::endl;}}}intmain(){// 1. 创建管道成功int pipefd[2]={0};int n =pipe(pipefd);(void)n;// 2. 创建子进程 pid_t id =fork();if(id ==0){// 3.形成单向通信的信道// 子进程: wclose(pipefd[0]);// 关掉我不用的这个读WriteData(pipefd[1]);close(pipefd[1]);// 可以关掉exit(0);}else{// 3.形成单向通信的信道// 父进程: rclose(pipefd[1]);// 关掉我不用的这个写ReadData(pipefd[0]);close(pipefd[0]);// 可以关掉}// 0->read 1->wirte// 0->嘴巴->读 1->笔->写// std::cout << "pipefd[0]: " << pipefd[0] << std::endl;// std::cout << "pipefd[1]: " << pipefd[1] << std::endl;return0;}通过对上述案例进行一定程度上的修改,有一想4种情况,大家注意看一下,也可以根据我的截图自己改了去试一下。




五. 从内核视角看管道的本质
从文件描述符视角,我们理解了管道的使用流程,而从内核视角,我们能看透管道的底层实现 —— 管道的本质是内核中的一块缓冲区,由两个file结构体指向同一个inode,贴合 Linux “一切皆文件” 的设计思想。
5.1 管道的内核数据结构
在 Linux 内核中,管道的底层实现涉及三个核心数据结构:
file结构体:进程的文件描述符表中的每个项都指向一个file结构体,记录文件的操作方式、当前偏移量等信息;inode结构体:用于描述文件的物理属性,管道的inode中保存了管道缓冲区的地址、大小、读写位置等核心信息;- 管道缓冲区:内核中的一块连续内存,是管道实际存储数据的地方。
对于匿名管道,父子进程的fd[0]和fd[1]会分别指向不同的file结构体,但这两个file结构体最终会指向同一个inode结构体,而该inode指向内核中的管道缓冲区。

5.2 管道的内核实现逻辑
当进程对管道执行read/write操作时,内核的处理逻辑如下:
- 写操作write
(fd[1], data, len):内核将数据从进程地址空间复制到管道缓冲区,并更新inode中的写位置; - 读操作
read(fd[0], buf, len):内核将管道缓冲区中的数据复制到进程地址空间,并更新inode中的读位置; - 缓冲区同步:内核会保证管道缓冲区的读写同步,若缓冲区为空,读操作会阻塞;若缓冲区满,写操作会阻塞。这个上面也有分析到一点。
简单来说,管道的读写操作本质是进程地址空间与内核缓冲区之间的数据拷贝,而两个进程共享同一个内核缓冲区,就实现了数据的跨进程传递。
补充:读写规则的一些了解,可以简单看看

5.3 管道与普通文件的异同
管道的操作接口与普通文件一致,但二者在底层实现和使用上有明显区别,核心对比如下:
| 特性 | 管道 | 普通文件 |
|---|---|---|
| 存储介质 | 内核缓冲区 | 磁盘 / 块设备 |
| 生命周期 | 随进程(进程退出释放) | 随文件系统(需手动删除) |
| 数据读写 | 流式读写,不可随机访问 | 支持随机访问(lseek) |
| 共享方式 | 仅亲缘进程通过文件描述符共享 | 所有进程可通过路径 / 文件描述符共享 |
| 数据持久化 | 不持久化(读出即删除) | 持久化(数据保存在磁盘) |
但二者的核心共性是都遵循 Linux 的文件操作模型,通过文件描述符、file结构体、inode结构体实现操作,这也是管道能复用文件读写接口的根本原因。
六. 核心要点总结(含管道5个特点总结图)
本文从进程间通信的基础出发,详细解析了 Linux 匿名管道的核心概念、创建 API、基于 fork 的跨进程通信实现,并从文件描述符和内核两个视角深入剖析了匿名管道的底层逻辑,核心要点可总结为:
- 管道是 Linux 最基础的 IPC 方式,本质是内核中的一块缓冲区,贴合 “一切皆文件” 的设计思想,通过
read/write接口操作; - 匿名管道通过
pipe函数创建,返回读、写两个文件描述符,需借助fork实现亲缘进程间通信,核心是子进程继承父进程的文件描述符表,共享同一个内核管道缓冲区; - 匿名管道通信的标准流程是:
创管道→fork 子进程→关闭无用文件描述符→读写通信→关闭描述符,关闭无用描述符是保证单向通信的关键; - 从文件描述符视角,父子进程的文件描述符指向同一个内核管道缓冲区;从内核视角,管道是由两个
file结构体指向同一个inode的内核缓冲区,读写操作是进程与内核缓冲区之间的数据拷贝; - 匿名管道是半双工的,仅支持亲缘进程间的单向通信,若需双向通信需创建两个管道,且其生命周期随进程,数据不持久化。

结尾:
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 结语:匿名管道作为最原始的 IPC 方式,虽然功能有限,但却是理解 Linux 进程间通信、文件系统和内核机制的关键。后面的文章中,我们将继续深入讲解管道的读写规则、命名管道(FIFO)的实现,以及管道在进程池中的实际应用,带你进一步吃透 Linux 管道技术。创作不易,觉得本文有帮助的话,欢迎点赞、收藏、关注三连~ 后续会持续更新 Linux 进程间通信的其他核心技术(共享内存、消息队列、信号量等),带你从底层吃透 Linux IPC。
✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど