个人主页Cx330❄️个人专栏《C语言》《LeetCode刷题集》《数据结构-初阶》《C知识分享》《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔《Git深度解析》:版本管理实战全解心向往之行必能至Cx330的简介目录前言一. 进程间通信介绍1.1 进程间通信目的1.2 进程间通信的发展与分类二、先搞懂什么是管道匿名管道有何特殊性2.1 管道的本质2.2 管道的核心特性三、匿名管道的创建3.1 匿名管道的创建流程3.2 匿名管道的使用示例四. 核心深挖匿名管道的底层原理4.1 fork 共享管道的核心原理4.2 从文件描述符视角理解管道通信4.3 实战示例分析四个场景案例五. 站在内核较低——管道的本质5.1 管道的内核数据结构5.2 管道的内核实现逻辑5.3 管道特点总结六、总结匿名管道的适用场景前言在Linux系统中进程之间的资源相互独立、地址空间隔离想要实现数据交互就必须依靠进程间通信IPC机制。而匿名管道作为最基础、最经典的IPC方式不仅是shell命令中“|”管道符的底层实现更是理解Linux进程协作、内核缓存机制的入门钥匙。这篇博客将从底层原理、核心特性、代码实现、常见坑点四个维度彻底拆解匿名管道带你从原理到实战掌握匿名管道通信的全流程。声明往后的学习中博主就要更换配置了语言更换为C,云服务器从Centos更改为Ubuntu文本编辑器改为VsCode与Xshell远程链接使用一. 进程间通信介绍在学习管道之前我们需要先明确进程间通信的核心目的和分类建立对 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 -lwho进程的标准输出被重定向到管道的写端wc -l进程的标准输入被重定向到管道的读端内核中的管道缓冲区作为中间介质完成两个进程间的数据传递。我们还可以使用下图中这样的实验来看一下管道从图中我们可以看出最后他们三个指令的父进程都是bash的他们之间是具有血缘关系的进程2.2 管道的核心特性管道的设计贴合 Linux一切皆文件的思想其核心特性可总结为半双工通信数据只能沿一个方向流动若需双向通信需创建两个管道基于缓冲区管道的实质是内核缓冲区数据写入后暂存于内核直到被另一个进程读取文件式操作管道通过文件描述符操作读写接口与文件一致read/write符合 Linux 文件操作规范亲缘进程专属匿名管道仅支持具有共同祖先的亲缘进程父进程与子进程、兄弟进程间通信。三、匿名管道的创建3.1 匿名管道的创建流程匿名管道依靠pipe()系统调用创建内核会完成三件事开辟一段环形缓冲区默认大小一般为4096字节/页可通过proc文件系统调整作为数据中转站分配两个文件描述符fd[0] 读端、fd[1] 写端严格单向通信返回文件描述符由调用进程持有fork()子进程会完整继承这两个文件描述符。关键特性匿名管道是半双工通信数据只能从写端流入、读端流出不支持双向传输且数据遵循先进先出FIFO原则无消息边界是字节流通信。匿名管道的创建函数#include unistd.h int pipe(int pipefd[2]);函数参数pipefd整型数组是输出型参数用于保存管道的读、写文件描述符pipefd[0]管道的读端仅用于读取管道中的数据pipefd[1]管道的写端仅用于向管道中写入数据。返回值成功返回 0失败返回 - 1并设置errno表示错误原因。注意调用pipe函数的进程会同时持有管道的读端和写端若要实现两个进程间的单向通信需要在进程创建后关闭各自无用的文件描述符避免数据读写异常3.2 匿名管道的使用示例下面的示例实现了一个基础的匿名管道通信从键盘读取数据写入管道再从管道读取数据输出到屏幕直观展示管道的读写操作。#include cstdio #include cstdlib #include cstring #include unistd.h int main() { 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; } } return 0; }该示例中进程自身同时完成管道的写和读操作虽然未实现跨进程通信但清晰展示了管道的基本读写流程通过fd[1]写通过fd[0]读操作接口与普通文件完全一致。四. 核心深挖匿名管道的底层原理匿名管道本身由单个进程创建要实现跨进程通信需要借助fork函数创建子进程 —— 子进程会继承父进程的文件描述符表从而与父进程共享同一个管道的读、写端这是匿名管道实现亲缘进程通信的核心原理。4.1 fork 共享管道的核心原理fork函数创建的子进程会复制父进程的文件描述符表包括父进程创建的管道读、写端文件描述符因此父子进程会共享同一个内核管道缓冲区实现数据互通。其核心步骤分为三步父进程创建管道父进程调用pipe创建管道持有fd[0]读和fd[1]写两个文件描述符父进程 fork 创建子进程子进程继承父进程的文件描述符表同样持有管道的fd[0]和fd[1]关闭无用的文件描述符根据通信方向父、子进程分别关闭无用的读 / 写端实现单向通信。例如要实现父进程写、子进程读则父进程关闭读端fd[0]仅保留写端fd[1]子进程关闭写端fd[1]仅保留读端fd[0]。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 // 子进程:w void WriteData(int wfd) { int cnt1; pid_t idgetpid(); while(true) { std::string messagehello father process, ; messagecnt: std::to_string(cnt) , my pid is: std::to_string(id); write(wfd,message.c_str(),message.size()); sleep(1); } } // 父进程:r void ReadData(int rfd) { char inbuffer[1024]; while(true) { ssize_t nread(rfd,inbuffer,sizeof(inbuffer)-1); if(n0) { inbuffer[n]\0; std::coutgetpid()# inbufferstd::endl; } } } int main() { // 1.创建管道成功 int pipefd[2]{0}; int npipe(pipefd);//系统调用 (void)n; // 2.创建子进程 pid_t idfork(); if(id0) { // 3.形成单向通信的信道 // 子进程:w close(pipefd[0]); WriteData(pipefd[1]); close(pipefd[1]); exit(0); } else { // 3.形成单向通信的信道 // 父进程:r close(pipefd[1]); ReadData(pipefd[0]); close(pipefd[0]); } // 0-read fd, 1-write fd // 0-嘴巴-读, 1-笔-写 // std::coutpipefd[0]:pipefd[0]std::endl; // std::coutpipefd[1]:pipefd[1]std::endl; return 0; }通过对上述案例进行一定程度上的修改有一想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 管道特点总结六、总结匿名管道的适用场景匿名管道的优势在于实现简单、效率高、内核原生支持适合以下场景shell命令行的管道符实现父子/兄弟进程间的简单数据流转小批量、单向的字节流传输。如果需要双向通信、无亲缘进程通信、大数据持久化传输就需要进阶学习命名管道、共享内存、消息队列等更强大的IPC机制。
Linux匿名管道通信:原理深挖+代码实现,一篇吃透进程间数据流转
个人主页Cx330❄️个人专栏《C语言》《LeetCode刷题集》《数据结构-初阶》《C知识分享》《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔《Git深度解析》:版本管理实战全解心向往之行必能至Cx330的简介目录前言一. 进程间通信介绍1.1 进程间通信目的1.2 进程间通信的发展与分类二、先搞懂什么是管道匿名管道有何特殊性2.1 管道的本质2.2 管道的核心特性三、匿名管道的创建3.1 匿名管道的创建流程3.2 匿名管道的使用示例四. 核心深挖匿名管道的底层原理4.1 fork 共享管道的核心原理4.2 从文件描述符视角理解管道通信4.3 实战示例分析四个场景案例五. 站在内核较低——管道的本质5.1 管道的内核数据结构5.2 管道的内核实现逻辑5.3 管道特点总结六、总结匿名管道的适用场景前言在Linux系统中进程之间的资源相互独立、地址空间隔离想要实现数据交互就必须依靠进程间通信IPC机制。而匿名管道作为最基础、最经典的IPC方式不仅是shell命令中“|”管道符的底层实现更是理解Linux进程协作、内核缓存机制的入门钥匙。这篇博客将从底层原理、核心特性、代码实现、常见坑点四个维度彻底拆解匿名管道带你从原理到实战掌握匿名管道通信的全流程。声明往后的学习中博主就要更换配置了语言更换为C,云服务器从Centos更改为Ubuntu文本编辑器改为VsCode与Xshell远程链接使用一. 进程间通信介绍在学习管道之前我们需要先明确进程间通信的核心目的和分类建立对 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 -lwho进程的标准输出被重定向到管道的写端wc -l进程的标准输入被重定向到管道的读端内核中的管道缓冲区作为中间介质完成两个进程间的数据传递。我们还可以使用下图中这样的实验来看一下管道从图中我们可以看出最后他们三个指令的父进程都是bash的他们之间是具有血缘关系的进程2.2 管道的核心特性管道的设计贴合 Linux一切皆文件的思想其核心特性可总结为半双工通信数据只能沿一个方向流动若需双向通信需创建两个管道基于缓冲区管道的实质是内核缓冲区数据写入后暂存于内核直到被另一个进程读取文件式操作管道通过文件描述符操作读写接口与文件一致read/write符合 Linux 文件操作规范亲缘进程专属匿名管道仅支持具有共同祖先的亲缘进程父进程与子进程、兄弟进程间通信。三、匿名管道的创建3.1 匿名管道的创建流程匿名管道依靠pipe()系统调用创建内核会完成三件事开辟一段环形缓冲区默认大小一般为4096字节/页可通过proc文件系统调整作为数据中转站分配两个文件描述符fd[0] 读端、fd[1] 写端严格单向通信返回文件描述符由调用进程持有fork()子进程会完整继承这两个文件描述符。关键特性匿名管道是半双工通信数据只能从写端流入、读端流出不支持双向传输且数据遵循先进先出FIFO原则无消息边界是字节流通信。匿名管道的创建函数#include unistd.h int pipe(int pipefd[2]);函数参数pipefd整型数组是输出型参数用于保存管道的读、写文件描述符pipefd[0]管道的读端仅用于读取管道中的数据pipefd[1]管道的写端仅用于向管道中写入数据。返回值成功返回 0失败返回 - 1并设置errno表示错误原因。注意调用pipe函数的进程会同时持有管道的读端和写端若要实现两个进程间的单向通信需要在进程创建后关闭各自无用的文件描述符避免数据读写异常3.2 匿名管道的使用示例下面的示例实现了一个基础的匿名管道通信从键盘读取数据写入管道再从管道读取数据输出到屏幕直观展示管道的读写操作。#include cstdio #include cstdlib #include cstring #include unistd.h int main() { 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; } } return 0; }该示例中进程自身同时完成管道的写和读操作虽然未实现跨进程通信但清晰展示了管道的基本读写流程通过fd[1]写通过fd[0]读操作接口与普通文件完全一致。四. 核心深挖匿名管道的底层原理匿名管道本身由单个进程创建要实现跨进程通信需要借助fork函数创建子进程 —— 子进程会继承父进程的文件描述符表从而与父进程共享同一个管道的读、写端这是匿名管道实现亲缘进程通信的核心原理。4.1 fork 共享管道的核心原理fork函数创建的子进程会复制父进程的文件描述符表包括父进程创建的管道读、写端文件描述符因此父子进程会共享同一个内核管道缓冲区实现数据互通。其核心步骤分为三步父进程创建管道父进程调用pipe创建管道持有fd[0]读和fd[1]写两个文件描述符父进程 fork 创建子进程子进程继承父进程的文件描述符表同样持有管道的fd[0]和fd[1]关闭无用的文件描述符根据通信方向父、子进程分别关闭无用的读 / 写端实现单向通信。例如要实现父进程写、子进程读则父进程关闭读端fd[0]仅保留写端fd[1]子进程关闭写端fd[1]仅保留读端fd[0]。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 // 子进程:w void WriteData(int wfd) { int cnt1; pid_t idgetpid(); while(true) { std::string messagehello father process, ; messagecnt: std::to_string(cnt) , my pid is: std::to_string(id); write(wfd,message.c_str(),message.size()); sleep(1); } } // 父进程:r void ReadData(int rfd) { char inbuffer[1024]; while(true) { ssize_t nread(rfd,inbuffer,sizeof(inbuffer)-1); if(n0) { inbuffer[n]\0; std::coutgetpid()# inbufferstd::endl; } } } int main() { // 1.创建管道成功 int pipefd[2]{0}; int npipe(pipefd);//系统调用 (void)n; // 2.创建子进程 pid_t idfork(); if(id0) { // 3.形成单向通信的信道 // 子进程:w close(pipefd[0]); WriteData(pipefd[1]); close(pipefd[1]); exit(0); } else { // 3.形成单向通信的信道 // 父进程:r close(pipefd[1]); ReadData(pipefd[0]); close(pipefd[0]); } // 0-read fd, 1-write fd // 0-嘴巴-读, 1-笔-写 // std::coutpipefd[0]:pipefd[0]std::endl; // std::coutpipefd[1]:pipefd[1]std::endl; return 0; }通过对上述案例进行一定程度上的修改有一想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 管道特点总结六、总结匿名管道的适用场景匿名管道的优势在于实现简单、效率高、内核原生支持适合以下场景shell命令行的管道符实现父子/兄弟进程间的简单数据流转小批量、单向的字节流传输。如果需要双向通信、无亲缘进程通信、大数据持久化传输就需要进阶学习命名管道、共享内存、消息队列等更强大的IPC机制。