
在 Linux 系统中进程是资源分配的基本单位各进程拥有独立的地址空间这保证了进程的安全性但也造成了进程间数据隔离的问题。为了让不同进程能够交换信息、协同工作操作系统提供了多种进程间通信IPCInterprocess Communication机制。本文作为进程间通信系列的上半篇将聚焦最基础且经典的管道通信包括匿名管道和命名管道的原理、实现、特性及实战案例帮助读者彻底掌握这两种 IPC 方式。1. 进程间通信基础认知1.1 进程间通信的核心目的进程间通信的本质是让多个进程能够 “看到” 同一份公共资源如内核维护的内存区域、文件缓冲区等从而实现数据交互与协同。其核心目的主要有 4 点数据传输一个进程将数据传递给另一个进程例如客户端向服务端发送请求数据。资源共享多个进程共享同一资源如文件、内存区域避免资源冗余存储。通知事件一个进程向其他进程发送事件通知例如子进程终止时通知父进程回收资源。进程控制一个进程控制另一个进程的执行例如调试器Debug 进程监控目标进程的运行状态。1.2 进程间通信的发展与分类Linux 系统的 IPC 机制经历了三个主要发展阶段1. 传统 IPC以管道为代表是 Unix 系统中最古老的 IPC 形式2. System V IPC包括消息队列、共享内存、信号量后续篇章详解3. POSIX IPC兼容多平台包括消息队列、共享内存、信号量、互斥量等。本文重点讲解传统 IPC 中的管道通信分为匿名管道pipe 和命名管道FIFO 两类二者核心语义一致仅创建与访问方式不同。2. 匿名管道2.1 匿名管道的核心概念什么是管道我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。匿名管道是一种单向通信的 IPC 机制仅适用于具有亲缘关系的进程如父子进程、兄弟进程。它本质是内核维护的一块内存缓冲区形似 “管道”—— 数据从一端写入从另一端读出遵循 “先进先出FIFO” 的原则。我们日常使用的命令行管道 | 就是匿名管道的典型应用例如 who | wc -l 命令• who 进程的标准输出stdout通过管道传递给 wc -l 进程的标准输入stdin• 内核为两个进程创建匿名管道who 进程写入数据wc -l 进程读取数据最终实现用户数量统计。2.2 匿名管道的实现原理2.2.1 管道的内核结构匿名管道由内核管理其本质是一个 “伪文件”仅存在于内存不刷新到磁盘内核通过 pipe_inode_info 结构体维护管道的读写状态、缓冲区等信息• 管道缓冲区默认大小为 64KBLinux 2.6.11 及以上版本用于临时存储写入的数据• 读写指针记录当前读写位置保证数据 FIFO 顺序• 引用计数跟踪当前打开管道的进程数所有进程关闭后管道自动释放。2.2.2 亲缘进程共享管道的原理匿名管道的创建与共享依赖于进程的 fork 机制核心步骤如下1. 父进程创建管道调用 pipe 函数创建管道得到两个文件描述符 fd[0]读端和 fd[1]写端此时父进程同时拥有读写权限2. 父进程 fork 子进程fork 后子进程会复制父进程的文件描述符表因此子进程也拥有 fd[0] 和 fd[1]指向同一个内核管道缓冲区3. 关闭多余端口为实现单向通信父进程关闭读端close(fd[0])仅保留写端子进程关闭写端close(fd[1])仅保留读端形成单向数据通道。关键说明进程的文件描述符表是私有的但指向的内核文件对象管道是共享的且管道缓冲区的数据不会发生 “写时拷贝”确保通信效率。光是直接说步骤我们肯定还是有点不清楚的接下来我来进行详细说明前面我们也说了进程间通信的本质是让多个进程能够 “看到” 同一份公共资源那么这个管道其实就是这个公共资源让我们的父子进程都能访问到这个管道是一个文件但却是经过系统处理过的特殊文件因为我们想要的仅是两个进程间进行通信不想也不需要涉及到磁盘的刷新操作而正常文件都是会涉及到把数据刷新到磁盘操作的所以我们的管道文件是经过系统特殊处理的仅在内存中进行数据传输操作但管道文件仍是文件所以创建时它会在进程的文件描述符表中出现这点和普通文件一样优先使用文件描述符表中空的位置如 不了解请移步Linux文件一-CSDN博客那么我们如何创建出这个匿名管道呢我们来继续看。2.3 pipe函数Linux系统中为我们提供了创建匿名管道的函数——pipe函数函数原型与参数如下#include unistd.h int pipe(int pipefd[2]);参数pipefd 是输出型参数数组大小为 2pipefd[0]管道读端的文件描述符仅支持读操作pipefd[1]管道写端的文件描述符仅支持写操作。返回值成功返回 0失败返回 - 1且 errno 会设置对应错误码如 EMFILE 表示文件描述符耗尽。其实就是pipe函数可以帮我们以读、写方式创建并打开一个管道文件并将两个文件描述符存入pipefd数组中创建成功返回0失败返回-1。接下来我们就可以看到匿名管道连接父子进程的具体流程了1. 父进程创建匿名管道2. 父进程 fork创建子进程在这一步中父进程通过 fork创建出了子进程而子进程会拷贝父进程的文件描述符表那么自然就也拥有了管道的读、写文件描述符。但是我们注意父子进程的文件描述符表都是各自私有的但文件描述表中的文件却是在内存中共享的实际也在共享区所以父子进程对文件进行修改不会发生写实拷贝。如父进程打开了文件A子进程的文件描述符表继承了文件A的文件描述符此时子进程对文件A进行修改那么父进程访问文件A看到的就是修改过后文件A。3. 父进程关闭fd[0]子进程关闭fd[1]我们现在是想要父进程写入信息给管道子进程从管道中读取信息所以可以关闭各自不需要的端口父进程关闭读端fd[0]子进程关闭写端fd[1]这样我们就实现了单向通信。示例#include stdio.h #include stdlib.h #include unistd.h #include sys/types.h #include sys/wait.h #include string.h #define BUF_SIZE 1024 // 父进程写数据 void writer(int wfd) { const char* msg_template Hello child, Im father (count: %d); char buf[BUF_SIZE]; int count 0; while (1) { // 构造消息 snprintf(buf, BUF_SIZE, msg_template, count); // 写入管道 ssize_t write_len write(wfd, buf, strlen(buf)); if (write_len -1) { perror(write pipe error); break; } printf(Father write: %s\n, buf); sleep(1); // 每隔1秒写一次 } } // 子进程读数据 void reader(int rfd) { char buf[BUF_SIZE]; while (1) { // 清空缓冲区 memset(buf, 0, BUF_SIZE); // 从管道读取 ssize_t read_len read(rfd, buf, BUF_SIZE - 1); if (read_len 0) { printf(Child read: %s\n, buf); } else if (read_len 0) { // 写端关闭读取到EOF printf(Father closed write end, child exit\n); break; } else { perror(read pipe error); break; } } } int main() { int pipefd[2]; // 1. 创建管道 if (pipe(pipefd) -1) { perror(pipe create error); exit(EXIT_FAILURE); } // 2. 创建子进程 pid_t pid fork(); if (pid -1) { perror(fork error); exit(EXIT_FAILURE); } if (pid 0) { // 子进程关闭写端只读 close(pipefd[1]); reader(pipefd[0]); close(pipefd[0]); exit(EXIT_SUCCESS); } else { // 父进程关闭读端只写 close(pipefd[0]); writer(pipefd[1]); close(pipefd[1]); // 等待子进程退出 waitpid(pid, NULL, 0); printf(Child exited, father exit\n); } return 0; }运行结果2.4 匿名管道的特性总结匿名管道的特性总结如下• 通信范围仅支持亲缘进程父子、兄弟进程• 通信方向半双工单向通信若需双向通信需创建两个管道• 生命周期随进程所有打开管道的进程退出后管道自动释放• 服务类型流式服务数据无边界按字节流传输• 同步互斥内核自带同步与互斥机制避免同时读写、数据不一致• 数据存储仅存于内存不刷新到磁盘效率较高。3. 命名管道3.1 命名管道的核心概念匿名管道的局限性在于仅能用于亲缘进程而命名管道FIFOFirst In First Out通过 “文件名” 标识管道突破了亲缘关系的限制允许任意两个进程甚至无关联的进程通信。命名管道的本质仍是内核维护的内存缓冲区与匿名管道的核心差异在于•匿名管道通过文件描述符隐式关联•命名管道通过文件系统中的 “特殊文件”FIFO 文件显式关联进程通过打开该文件即可连接管道。也就是说匿名管道与命名管道的核心差异点是匿名管道是通过父子进程看到同一份资源从而进行链接而命名管道是通过路径与文件名的方式找到同一份文件资源因为文件的路径具有唯一性。同时匿名管道与命名管道都是在内存中进行数据传递不涉及磁盘的刷新数据。3.2 命名管道的创建方式命名管道有两种创建方式命令行创建和代码创建。3.2.1 命令行创建mkfifo 命令mkfifo 管道文件名示例3.2.2 代码创建mkfifo 函数#include sys/stat.h #include sys/types.h int mkfifo(const char *pathname, mode_t mode);参数• pathname管道文件的路径如 ./myfifo• mode管道文件的权限如 0666 表示读写权限受 umask 影响实际权限 mode ~umask我们创建文件时需要赋予文件权限返回值成功返回 0失败返回 - 1如文件已存在时 errno 设为 EEXIST。3.3 命名管道的打开方式命名管道的通信双方需通过 open 函数打开 FIFO 文件打开规则直接影响进程阻塞行为1. 以读方式打开O_RDONLY阻塞直到有进程以写方式打开该 FIFO2. 以写方式打开O_WRONLY阻塞直到有进程以读方式打开该 FIFO3. 以读写方式打开O_RDWR立即返回成功避免阻塞但破坏半双工特性不推荐。3.4 实战案例下面通过命名管道实现一个简单的 C/S 架构服务端持续读取管道数据客户端向管道写入数据实现跨进程通信。3.4.1 公共头文件comm.h定义管道路径、头文件依赖#pragma once #include stdio.h #include sys/stat.h #include sys/types.h #include fcntl.h #include unistd.h #include string.h #include stdlib.h // 命名管道路径全局唯一确保客户端和服务端能找到 #define FIFO_PATH ./server_fifo // 缓冲区大小 #define BUF_SIZE 10243.4.2 服务端代码server.c服务端职责创建 FIFO 文件 → 以读方式打开 → 持续读取客户端数据 → 关闭管道#include comm.h int main() { // 1. 设置umask确保管道权限为0666 umask(0); // 2. 创建命名管道 if (mkfifo(FIFO_PATH, 0666) -1) { perror(mkfifo create error); exit(EXIT_FAILURE); } printf(Server: FIFO created successfully\n); // 3. 以读方式打开管道默认阻塞等待客户端连接 int fd open(FIFO_PATH, O_RDONLY); if (fd -1) { perror(open FIFO error); exit(EXIT_FAILURE); } printf(Server: Client connected, start reading...\n); // 4. 持续读取客户端数据 char buf[BUF_SIZE]; while (1) { memset(buf, 0, BUF_SIZE); ssize_t read_len read(fd, buf, BUF_SIZE - 1); if (read_len 0) { // 读取成功打印数据 printf(Client message: %s\n, buf); } else if (read_len 0) { // 客户端关闭写端服务端退出 printf(Client disconnected, server exit\n); break; } else { perror(read FIFO error); break; } } // 5. 关闭管道、删除FIFO文件 close(fd); unlink(FIFO_PATH); // 移除文件系统中的FIFO节点 printf(Server: FIFO closed and deleted\n); return 0; }3.4.3 客户端代码client.c客户端职责以写方式打开 FIFO → 从键盘读取输入 → 写入管道 → 关闭管道#include comm.h int main() { // 1. 以写方式打开管道默认阻塞等待服务端启动 int fd open(FIFO_PATH, O_WRONLY); if (fd -1) { perror(open FIFO error); exit(EXIT_FAILURE); } printf(Client: Connected to server, start sending...\n); // 2. 从键盘读取数据写入管道 char buf[BUF_SIZE]; while (1) { memset(buf, 0, BUF_SIZE); // 提示用户输入 printf(Please enter message (enter quit to exit): ); fflush(stdout); // 刷新缓冲区避免输入提示不显示 // 从键盘读取输入 ssize_t read_len read(STDIN_FILENO, buf, BUF_SIZE - 1); if (read_len 0) { // 去除换行符read会读取回车符\n buf[read_len - 1] \0; // 若输入quit退出客户端 if (strcmp(buf, quit) 0) { printf(Client: Exit\n); break; } // 写入管道 write(fd, buf, strlen(buf)); } else if (read_len 0) { printf(Client: Input closed\n); break; } else { perror(read stdin error); break; } } // 3. 关闭管道 close(fd); return 0; }运行结果3.5 命名管道与匿名管道的区别4. 管道通信的核心特性无论是匿名管道还是命名管道都基于内核 FIFO 缓冲区实现因此共享以下 5 个核心特性这些特性也决定了管道的适用场景与使用约束。特性 1自带同步与互斥机制管道是临界资源同一时刻只能被一个进程操作内核会自动实现同步与互斥无需开发者手动加锁互斥性保证同一时刻只有一个进程能对管道执行读 / 写操作避免多个进程同时操作导致的数据错乱同步性强制进程按 “写→读→写” 的顺序交互比如写端满了会阻塞读端空了会等待确保数据生产与消费的节奏匹配。特性 2生命周期随进程管道的存在依赖于 “打开它的进程”匿名管道通过进程的文件描述符表关联当所有打开管道的亲缘进程退出后内核自动释放管道缓冲区命名管道虽然在文件系统中存在 FIFO 节点但管道的内存缓冲区会在所有打开它的进程退出后被释放仅保留文件系统中的空节点需手动unlink删除。简言之管道的 “有效生命周期” 与使用它的进程绑定。特性 3提供流式服务管道是面向字节流的通信方式无数据边界、不分报文段写端进程写入的数据是连续的字节流读端进程可以任意指定每次读取的字节数比如写端写了 100 字节读端可以分 5 次读 20 字节与 “数据报服务”如 UDP不同流式服务没有 “报文” 概念需开发者自行约定数据分隔符如换行符来区分消息。特性 4半双工通信模式管道默认是半双工通信数据传输规则如下单工只能单向传输如广播半双工可以双向传输但同一时刻只能一个方向传输管道默认是单向通道若需双向通信需创建两个管道全双工可以同时双向传输如 Socket。因此管道通常用于 “单向数据流转” 的场景如命令行A | B的输出转输入。特性 5容量与原子写限制管道的缓冲区大小与写入原子性有明确约束以 Linux 系统为例管道容量Linux 2.6.11 及之后版本管道缓冲区默认大小为65536 字节64KB若写端写入数据超过缓冲区容量会阻塞直到读端消费数据。原子写限制当单次写入数据量≤PIPE_BUFLinux 下为 4096 字节时内核保证写入的原子性数据不会被其他进程的写入打断若超过PIPE_BUF则不保证原子性可能出现数据交织。5. 管道的特殊场景无论是匿名管道还是命名管道其底层都是内核维护的 FIFO 内存缓冲区因此二者会共享完全一致的读写规则与特殊场景。下面详细解析管道通信中最常见的 4 种特殊情况场景 1读端等待写端数据若写端进程未写入数据但读端进程持续调用read读取管道•管道中无数据可供读取读端进程会被内核阻塞挂起处于等待状态•直到写端进程向管道写入数据后读端进程才会被唤醒继续执行读取操作。场景 2写端等待读端读取若读端进程未读取数据但写端进程持续调用write写入管道•当管道缓冲区被写满后写端进程会被内核阻塞挂起•直到读端进程读取数据、释放管道缓冲区空间后写端进程才会被唤醒继续执行写入操作。场景 3写端关闭后的读端处理当写端进程完成数据写入并主动关闭写端调用close关闭写端文件描述符•读端进程会继续读取管道中剩余的数据•当管道内数据被全部读完后读端调用read会返回0类似读取文件到 EOF 的状态此时读端可以据此判断 “写端已结束通信”执行后续逻辑。场景 4读端关闭后的写端终止若读端进程主动关闭读端调用close关闭读端文件描述符但写端进程仍持续向管道写入数据•操作系统会向写端进程发送SIGPIPE信号•该信号的默认处理行为是终止写端进程可通过捕获信号自定义处理逻辑但通常需避免这种场景。结语好好学习天天向上有任何问题请指正谢谢观看