
管道Pipe是 Linux 系统中最古老、最常用的进程间通信IPC方式之一。它基于先进先出的数据流模型为进程之间提供了一条单向或双向的数据传输通道。本文将从概念到代码系统讲解匿名管道和命名管道的原理、区别以及popen()、pclose()、pipe()、mkfifo()四个核心函数的用法。以下是我在职坐标的学习记录一、管道的分类Linux 中的管道分为两种匿名管道Anonymous Pipe和命名管道Named Pipe / FIFO。1.1 匿名管道匿名管道是一种半双工的通信机制数据只能沿一个方向流动从写端流向读端。它没有名字不存在于文件系统中只能在具有亲缘关系的进程之间使用——通常是父子进程或兄弟进程。核心特点半双工通信数据只能单向流动一端写、另一端读。无名字不占用文件系统路径通过文件描述符fd引用。亲缘关系限制只能用于有血缘关系的进程间通过fork()创建的子进程继承了父进程的管道描述符。基于内存管道内部维护一个内核缓冲区Linux 默认大小为 64KB数据写入后存储在缓冲区中读取后自动清除。生命周期随进程消亡而自动释放不需要手动清理。典型使用场景父进程通过管道向子进程传递数据或者在 shell 中使用|符号将两个命令串联起来如ls | grep .c本质上就是匿名管道。1.2 命名管道FIFO命名管道也叫 FIFOFirst In, First Out它在文件系统中拥有一个路径名任何知道该路径的进程都可以打开它进行通信因此不要求进程之间存在亲缘关系。核心特点存在于文件系统通过mkfifo()创建后可以用ls看到它文件类型为ppipe。无亲缘限制任意两个进程只要知道 FIFO 的路径就可以通过它进行通信。半双工或全双工传统用法是半双工的一个进程读、另一个写但如果双方都以读写模式O_RDWR打开某些系统上也支持全双工。阻塞行为默认以阻塞方式打开——读端会阻塞直到有写端打开写端也会阻塞直到有读端打开。可以设置O_NONBLOCK标志改变这一行为。持久性FIFO 文件存在于文件系统中不会随进程退出自动消失需要手动删除unlink()或rm。1.3 两者的对比对比维度匿名管道pipe命名管道FIFO文件系统可见不可见仅通过 fd 访问可见有文件路径使用范围仅限有亲缘关系的进程任意进程之间创建方式pipe()mkfifo()生命周期随进程自动销毁持久存在需手动删除通信方向半双工单向半双工也可全双工取决于实现阻塞行为通常不阻塞除非缓冲区满或空打开时默认阻塞直到对端也打开典型场景父子进程通信、shell 管道符无关系进程间的协调通信二、popen() 函数2.1 函数原型#include stdio.h FILE *popen(const char *command, const char *type);2.2 函数作用popen()会创建一个管道然后fork()出一个子进程在子进程中通过exec()执行command所指定的 shell 命令。父进程可以通过返回的FILE*流来读取命令的输出或者向命令写入数据。简单来说popen()封装了创建管道 fork exec这一整套流程让开发者可以用一行代码就实现启动一个命令并读取其输出或向一个命令写入数据的功能。2.3 参数详解参数类型说明commandconst char *要执行的 shell 命令字符串。它会作为参数传递给/bin/sh -c因此支持 shell 的通配符、管道符等语法。例如ls -l或cat /etc/passwd \| grep root。typeconst char *管道的工作模式只能取r或w。type参数深入解释rread 模式父进程以读方式打开管道子进程执行command后的标准输出会连接到管道的写端。父进程通过返回的FILE*可以读取命令的输出结果。FILE *fp popen(ls -l, r); // 父进程可以通过 fp 读取 ls -l 的输出wwrite 模式父进程以写方式打开管道父进程写入的数据会成为子进程执行command时的标准输入。FILE *fp popen(grep error result.txt, w); // 父进程可以向 fp 写入数据这些数据会作为 grep 的标准输入2.4 返回值成功返回一个指向FILE的指针标准 I/O 流可以用fgets()、fread()、fprintf()、fwrite()等标准库函数来操作。失败返回NULL可能的原因包括无法fork子进程、无法创建管道等。2.5 示例代码#include stdio.h #include stdlib.h int main() { FILE *fp; char buf[1024]; // 以读模式打开管道执行 ls -l 命令 fp popen(ls -l, r); if (fp NULL) { perror(popen failed); exit(EXIT_FAILURE); } // 逐行读取命令输出 while (fgets(buf, sizeof(buf), fp) ! NULL) { printf(%s, buf); } pclose(fp); return 0; }三、pclose() 函数3.1 函数原型#include stdio.h int pclose(FILE *stream);3.2 函数作用pclose()用于关闭由popen()创建的管道流。它不仅会关闭底层的管道文件描述符还会调用waitpid()等待子进程终止并回收子进程的资源避免产生僵尸进程。这一点非常重要如果popen()之后不调用pclose()子进程退出后会变成僵尸进程占用系统资源。3.3 参数详解参数类型说明streamFILE *由popen()返回的文件流指针。必须是popen()的返回值不能传入其他来源的FILE*。3.4 返回值成功返回子进程的终止状态与waitpid()的返回格式相同。可以使用WIFEXITED()、WEXITSTATUS()等宏来解析退出码。失败返回-1并设置errno。常见错误包括传入无效的stream指针。3.5 获取子进程退出码#include stdio.h #include stdlib.h #include sys/wait.h int main() { FILE *fp popen(ls /nonexistent_dir, r); if (fp NULL) { perror(popen failed); exit(EXIT_FAILURE); } int status pclose(fp); if (WIFEXITED(status)) { printf(子进程正常退出退出码: %d\n, WEXITSTATUS(status)); } else { printf(子进程异常终止\n); } return 0; }四、pipe() 函数4.1 函数原型#include unistd.h int pipe(int pipefd[2]);4.2 函数作用pipe()创建一个匿名管道并通过pipefd数组返回两个文件描述符pipefd[0]是读端pipefd[1]是写端。数据从写端流入从读端流出。pipe()通常与fork()配合使用父进程创建管道后调用fork()子进程继承管道的文件描述符父子进程之间就可以通过管道通信了。4.3 参数详解参数类型说明pipefdint[2]一个长度为 2 的整型数组用于接收返回的两个文件描述符。pipefd[0]为读端 fdpipefd[1]为写端 fd。4.4 返回值成功返回0pipefd[0]和pipefd[1]被填充为有效的文件描述符。失败返回-1并设置errno。常见原因包括进程打开的文件描述符数量已达上限EMFILE、系统内存不足ENOMEM等。4.5 使用模式创建管道后通常的做法是父进程调用pipe()创建管道。父进程调用fork()创建子进程。父进程关闭不需要的端口例如关闭读端子进程关闭不需要的端口例如关闭写端。父子进程分别通过write()/read()进行通信。通信结束后双方各自close()自己持有的 fd。为什么要关闭不需要的端口如果写端没有全部关闭读端的read()永远不会收到 EOF返回 0会一直阻塞等待数据如果读端没有全部关闭可能会导致数据被错误的进程读走。4.6 示例代码#include stdio.h #include stdlib.h #include unistd.h #include string.h #include sys/wait.h int main() { int pipefd[2]; pid_t pid; char buf[1024]; // 1. 创建管道 if (pipe(pipefd) -1) { perror(pipe failed); exit(EXIT_FAILURE); } // 2. fork 子进程 pid fork(); if (pid -1) { perror(fork failed); exit(EXIT_FAILURE); } if (pid 0) { // ---- 子进程作为写端 ---- close(pipefd[0]); // 关闭读端 const char *msg Hello from child!; write(pipefd[1], msg, strlen(msg)); close(pipefd[1]); // 写完关闭 exit(EXIT_SUCCESS); } else { // ---- 父进程作为读端 ---- close(pipefd[1]); // 关闭写端 int n read(pipefd[0], buf, sizeof(buf) - 1); if (n 0) { buf[n] \0; printf(父进程收到: %s\n, buf); } close(pipefd[0]); // 读完关闭 waitpid(pid, NULL, 0); // 等待子进程结束 } return 0; }4.7 pipe2() 扩展Linux 特有Linux 还提供了一个扩展版本pipe2()可以在创建管道时直接设置标志位#define _GNU_SOURCE #include fcntl.h #include unistd.h int pipe2(int pipefd[2], int flags);flags 值说明O_NONBLOCK将管道的两个 fd 设置为非阻塞模式O_CLOEXEC设置 close-on-exec 标志exec时自动关闭这两个 fd五、mkfifo() 函数5.1 函数原型#include sys/types.h #include sys/stat.h int mkfifo(const char *pathname, mode_t mode);5.2 函数作用mkfifo()在文件系统中创建一个命名管道FIFO 文件。创建完成后任何进程都可以通过open()、read()、write()、close()等系统调用来操作这个 FIFO实现无亲缘关系的进程间通信。5.3 参数详解参数类型说明pathnameconst char *FIFO 文件的路径名。可以是绝对路径如/tmp/myfifo也可以是相对路径。如果该路径已经存在一个同名文件mkfifo()会失败并设置errno为EEXIST。modemode_tFIFO 文件的权限位与open()的mode参数含义相同如0666所有用户可读写。注意最终权限还会受到进程的umask值影响。例如umask为022时0666实际生效的权限为0644。常见 mode 值说明mode 值含义0666所有用户可读可写0644所有者可读写其他人只读0600仅所有者可读写5.4 返回值成功返回0。失败返回-1并设置errno。常见错误包括EEXIST路径已存在同名文件。ENOENT路径中某个目录不存在。EACCES对路径中的某个目录没有权限。5.5 FIFO 的阻塞行为FIFO 的默认阻塞打开方式有一个重要的行为特征以只读方式打开O_RDONLY会阻塞直到有进程以写方式打开该 FIFO。以只写方式打开O_WRONLY会阻塞直到有进程以读方式打开该 FIFO。这意味着读端和写端必须配对打开否则会一直等待。可以使用O_NONBLOCK标志来避免阻塞但需要注意处理错误如写入时没有读端会返回ENXIO。5.6 示例代码以下展示两个独立的程序——一个读端、一个写端通过 FIFO 进行通信写端fifo_writer.c#include stdio.h #include stdlib.h #include fcntl.h #include sys/stat.h #include unistd.h #include string.h #define FIFO_PATH /tmp/myfifo int main() { // 创建 FIFO如果已存在则忽略错误 if (mkfifo(FIFO_PATH, 0666) -1) { perror(mkfifo); // 如果已存在不退出继续打开使用 } // 以写方式打开 FIFO会阻塞直到有读端打开 int fd open(FIFO_PATH, O_WRONLY); if (fd -1) { perror(open); exit(EXIT_FAILURE); } const char *msg Hello from FIFO writer!; write(fd, msg, strlen(msg)); printf(已写入: %s\n, msg); close(fd); // 注意FIFO 文件不会自动删除如需清理可手动 unlink // unlink(FIFO_PATH); return 0; }读端fifo_reader.c#include stdio.h #include stdlib.h #include fcntl.h #include sys/stat.h #include unistd.h #define FIFO_PATH /tmp/myfifo int main() { char buf[1024]; // 以读方式打开 FIFO会阻塞直到有写端打开 int fd open(FIFO_PATH, O_RDONLY); if (fd -1) { perror(open); exit(EXIT_FAILURE); } int n read(fd, buf, sizeof(buf) - 1); if (n 0) { buf[n] \0; printf(读取到: %s\n, buf); } close(fd); return 0; }编译运行gcc fifo_writer.c -o fifo_writer gcc fifo_reader.c -o fifo_reader # 先运行写端会阻塞等待读端 ./fifo_writer # 再运行读端 ./fifo_reader六、总结与选型建议需求场景推荐方案父子进程间快速传递数据pipe()fork()执行一个 shell 命令并读取/写入数据popen()pclose()无亲缘关系的进程间通信mkfifo()创建命名管道需要在 shell 脚本或命令行中使用直接使用\|匿名管道或mkfifo命令命名管道管道是 Linux IPC 的基础设施理解它的原理和行为对于编写健壮的多进程程序至关重要。实际开发中还需要注意管道的容量限制默认 64KB、SIGPIPE 信号的处理向已关闭读端的管道写入时触发以及多读者/多写者场景下的数据交错问题。