管道
pipe() 和 fork() 通常一起使用,目的是实现 父子进程间的单向通信,这种通信方式是通过一个叫做 管道 的特殊文件来实现的。

(图片来源网络,侵删)
管道 的核心特点:
- 单向数据流:数据只能从一个方向流向另一个方向,就像一根真正的管道,水只能从一头流进,从另一头流出。
- 匿名管道:我们通常说的
pipe()创建的是匿名管道,它没有在文件系统中对应的文件名,存在于内存中,它的生命周期随进程的结束而结束。 - 基于文件描述符:在 Linux/Unix 中,一切皆文件,管道在程序中表现为两个文件描述符:
- 一个用于 读取 (
fd[0]) - 一个用于 写入 (
fd[1])
- 一个用于 读取 (
- 同步与阻塞:当管道为空时,读操作会阻塞,直到有数据写入,当管道已满时,写操作会阻塞,直到有数据被读出,这天然地起到了同步作用。
pipe() 系统调用
pipe() 函数用于创建一个管道。
函数原型
#include <unistd.h> int pipe(int pipefd[2]);
参数
pipefd: 一个包含两个整数的数组,函数执行成功后,pipefd[0]将代表管道的 读端,pipefd[1]将代表管道的 写端。
返回值
- 成功:返回
0。 - 失败:返回
-1,并设置errno。
关键规则
- 数据从
pipefd[1]写入,从pipefd[0]读出。 - 进程必须关闭它不使用的端点! 这是避免死锁的关键,如果一个进程只写不读,它就必须关闭读端;如果只读不写,就必须关闭写端。
fork() 系统调用
fork() 函数用于创建一个与当前进程(父进程)几乎完全相同的副本(子进程)。
函数原型
#include <unistd.h> pid_t fork(void);
返回值
这是 fork() 最独特的地方:

(图片来源网络,侵删)
- 在父进程中:
fork()返回 子进程的 PID (Process ID),这是一个大于 0 的整数。 - 在子进程中:
fork()返回0。 - 失败:返回
-1。
关键点
- 写时复制:
fork()后,父进程和子进程共享相同的物理内存空间,但只有在其中一个进程试图修改内存时,系统才会为该进程复制一份副本,这提高了效率。 - 文件描述符的继承:子进程会继承父进程的所有打开的文件描述符,包括
pipe()创建的管道的两个端点,这是父子进程能够通过管道通信的基础。
pipe() 和 fork() 协同工作流程
让我们把这两者结合起来,看看如何实现父子进程通信,最常见的场景是:父进程写入数据,子进程读取数据。
标准步骤:
-
创建管道:在
fork()之前,父进程先调用pipe()创建一个管道,父进程同时拥有了管道的读端 (fd[0]) 和写端 (fd[1])。 -
创建子进程:父进程调用
fork(),会发生两件事:- 子进程被创建。
- 子进程继承了父进程的文件描述符表,所以子进程也同时拥有了管道的读端 (
fd[0]) 和写端 (fd[1])。
-
关闭不必要的端点:这是最关键的一步,必须严格遵守。
(图片来源网络,侵删)- 父进程:如果父进程只负责写入,它必须调用
close(fd[0])来关闭读端,如果它不关闭,它自己可能会意外地从管道中读取数据,或者更严重,可能导致子进程的read()永远阻塞(如果父进程一直不写)。 - 子进程:如果子进程只负责读取,它必须调用
close(fd[1])来关闭写端,如果它不关闭,它自己可能会意外地向管道中写入数据,或者更严重,可能导致父进程的write()永远阻塞(如果管道已满,而子进程一直不读)。
- 父进程:如果父进程只负责写入,它必须调用
-
进行通信:
- 父进程使用
write(fd[1], ...)向管道中写入数据。 - 子进程使用
read(fd[0], ...)从管道中读取数据。
- 父进程使用
-
关闭所有端点:通信结束后,父进程和子进程都应该关闭它们各自使用的端点。
代码示例:父进程写,子进程读
这是一个经典的例子,父进程向管道写入一条消息,子进程读取并打印。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#define BUFFER_SIZE 100
int main() {
int pipefd[2]; // 用于存储管道的文件描述符
pid_t pid;
char write_buf[] = "Hello from parent process!";
char read_buf[BUFFER_SIZE];
// 1. 创建管道
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
// 2. 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid > 0) {
// --- 父进程代码 ---
printf("Parent process (PID: %d) started.\n", getpid());
// 3. 关闭父进程的读端,因为父进程只写
close(pipefd[0]);
// 4. 向管道写入数据
write(pipefd[1], write_buf, strlen(write_buf));
printf("Parent: Wrote message to pipe.\n");
// 关闭写端,表示写入完成
close(pipefd[1]);
// 等待子进程结束,避免子进程成为僵尸进程
wait(NULL);
printf("Parent process finished.\n");
} else if (pid == 0) {
// --- 子进程代码 ---
printf("Child process (PID: %d) started.\n", getpid());
// 3. 关闭子进程的写端,因为子进程只读
close(pipefd[1]);
// 4. 从管道读取数据
// read 会阻塞,直到有数据可读
int nbytes = read(pipefd[0], read_buf, BUFFER_SIZE - 1);
if (nbytes > 0) {
read_buf[nbytes] = '\0'; // 确保字符串正确终止
printf("Child: Read message from pipe: %s\n", read_buf);
}
// 关闭读端
close(pipefd[0]);
printf("Child process finished.\n");
}
return 0;
}
代码解析:
pipe(pipefd):创建管道,pipefd[0]是读端,pipefd[1]是写端。fork():创建子进程,父子进程现在都拥有pipefd[0]和pipefd[1]。- 父进程 (
pid > 0):close(pipefd[0]):父进程关闭读端,因为它只负责写。write(pipefd[1], ...):向管道写入字符串。close(pipefd[1]):写入完成后关闭写端。wait(NULL):等待子进程执行完毕。
- 子进程 (
pid == 0):close(pipefd[1]):子进程关闭写端,因为它只负责读。read(pipefd[0], ...):从管道读取数据,这个调用会阻塞,直到父进程写入数据。close(pipefd[0]):读取完成后关闭读端。
输出结果:
Parent process (PID: 1234) started.
Child process (PID: 1235) started.
Parent: Wrote message to pipe.
Child: Read message from pipe: Hello from parent process!
Child process finished.
Parent process finished.
(注意:PID 1234 和 1235 是示例,每次运行都会不同)
进阶应用:双向通信
上面的例子是单向通信,要实现双向通信(父进程和子进程互相发送消息),需要创建两个管道。
- 管道1:用于父进程 -> 子进程通信。
- 管道2:用于子进程 -> 父进程通信。
基本思路:
- 创建两个管道:
pipe1和pipe2。 fork()创建子进程。- 父进程:
- 关闭
pipe1的读端 (pipe1[0]) 和pipe2的写端 (pipe2[1])。 - 使用
pipe1[1]写入,用pipe2[0]读取。
- 关闭
- 子进程:
- 关闭
pipe1的写端 (pipe1[1]) 和pipe2的读端 (pipe2[0])。 - 使用
pipe1[0]读取,用pipe2[1]写入。
- 关闭
| 特性 | 描述 |
|---|---|
pipe() |
创建一个匿名管道,返回两个文件描述符,一个用于读,一个用于写。 |
fork() |
创建一个子进程,子进程继承父进程的文件描述符。 |
| 协同作用 | fork() 让子进程继承父进程的管道端点,然后通过关闭不用的端点,建立起单向或双向的通信通道。 |
| 核心原则 | “谁用谁开,不用谁关”,明确每个进程的角色(写者或读者),并立即关闭不用的端,以避免死锁和意外行为。 |
理解 pipe() 和 fork() 的组合是迈向高级 Unix/Linux C 编程的重要一步,它们是构建复杂系统、实现进程同步和通信的基础。
