1. 函数介绍
pipe
是一个 Linux 系统调用,用于创建一个匿名管道 (anonymous pipe)。管道是一种半双工(单向)的通信通道,具有固定的读端和写端。
你可以把管道想象成一个单向的水管或传送带:
- 一端是写入端 (write end):数据被“放入”管道。
- 另一端是读取端 (read end):数据从管道中被“取出”。
- 数据在管道内部按照先进先出 (FIFO) 的顺序流动。
- 管道有有限的容量(通常由
PIPE_BUF
常量定义,Linux 上通常是 65536 字节)。如果管道满了,写入操作会阻塞;如果管道空了,读取操作会阻塞。
匿名管道最常见的用途是在相关进程(通过 fork
创建的父子进程或兄弟进程)之间传递数据。
2. 函数原型
#include <unistd.h> // 必需
int pipe(int pipefd[2]);
3. 功能
- 创建管道: 请求内核创建一个新的匿名管道。
- 返回文件描述符: 在成功创建后,将两个关联的文件描述符通过
pipefd
数组返回给调用者:pipefd[0]
: 读端 (read end) 的文件描述符。- `pipefd[1]**: 写端 (write end) 的文件描述符。
- 初始化状态: 刚创建时,管道是空的。
4. 参数
int pipefd[2]
: 这是一个包含两个整数的数组,用于接收pipe
调用返回的文件描述符。pipefd[0]
: 管道的读取端。进程可以对此文件描述符调用read
来获取数据。pipefd[1]
: 管道的写入端。进程可以对此文件描述符调用write
来放入数据。
5. 返回值
- 成功时: 返回 0。同时,
pipefd[0]
和pipefd[1]
被填充为有效的文件描述符。 - 失败时: 返回 -1,并设置全局变量
errno
来指示具体的错误原因(例如EMFILE
进程打开的文件描述符已达上限,ENFILE
系统打开的文件总数已达上限等)。
6. 相似函数,或关联函数
socketpair
: 创建一对相互连接的匿名套接字,可以实现双向进程间通信。- 命名管道 (FIFO): 通过
mkfifo
或mknod
创建的特殊文件,允许无亲缘关系的进程进行通信。 read
,write
: 用于对管道的读端和写端进行实际的数据传输。close
: 用于关闭管道的读端或写端。关闭写端会使读端在数据读完后read
返回 0(EOF);关闭读端会使写端write
产生SIGPIPE
信号(默认终止进程)。fork
: 通常与pipe
结合使用,子进程和父进程通过继承的管道文件描述符进行通信。
7. 示例代码
示例 1:父子进程通过管道通信
这个经典的例子演示了如何使用 pipe
在父进程和子进程之间传递数据。
#include <unistd.h> // pipe, fork, read, write, close
#include <sys/wait.h> // wait
#include <stdio.h> // perror, printf
#include <stdlib.h> // exit
#include <string.h> // strlen
int main() {
int pipefd[2]; // 用于存储管道的两个文件描述符
pid_t cpid; // 子进程 ID
char buf; // 用于逐字节读取的缓冲区
// 1. 创建管道
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
// 2. 创建子进程
cpid = fork();
if (cpid == -1) {
perror("fork");
// 创建子进程失败,需要关闭已创建的管道
close(pipefd[0]);
close(pipefd[1]);
exit(EXIT_FAILURE);
}
// 3. 根据进程 ID 执行不同代码
if (cpid == 0) { // 子进程执行代码
// --- 子进程 ---
// 关闭不需要的写端
if (close(pipefd[1]) == -1) {
perror("child: close write end");
_exit(EXIT_FAILURE); // 子进程中使用 _exit
}
printf("Child process (PID %d): Reading from pipe...\n", getpid());
// 从管道读端读取数据,直到遇到 EOF
while (read(pipefd[0], &buf, 1) > 0) {
write(STDOUT_FILENO, &buf, 1); // 写入到标准输出 (屏幕)
}
// 检查 read 是否因错误而失败
if (read(pipefd[0], &buf, 1) == -1) {
perror("child: read");
_exit(EXIT_FAILURE);
}
printf("Child process: Finished reading. Exiting.\n");
// 关闭读端
if (close(pipefd[0]) == -1) {
perror("child: close read end");
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS); // 子进程成功退出
} else { // 父进程执行代码
// --- 父进程 ---
// 关闭不需要的读端
if (close(pipefd[0]) == -1) {
perror("parent: close read end");
// 清理子进程?
exit(EXIT_FAILURE);
}
const char *message = "Message from parent to child through pipe!\n";
printf("Parent process (PID %d): Writing to pipe...\n", getpid());
// 向管道写端写入数据
if (write(pipefd[1], message, strlen(message)) != (ssize_t)strlen(message)) {
perror("parent: write");
// 可能需要 kill 子进程
exit(EXIT_FAILURE);
}
printf("Parent process: Message sent. Closing write end.\n");
// 关闭写端,这会使子进程的 read() 在读完数据后返回 0 (EOF)
if (close(pipefd[1]) == -1) {
perror("parent: close write end");
exit(EXIT_FAILURE);
}
// 等待子进程结束
int status;
if (wait(&status) == -1) {
perror("parent: wait");
exit(EXIT_FAILURE);
}
if (WIFEXITED(status)) {
printf("Parent process: Child exited with status %d.\n", WEXITSTATUS(status));
} else {
printf("Parent process: Child did not exit normally.\n");
}
}
return 0;
}
代码解释:
示例 2:使用管道实现简单的命令行管道 (ls | wc -l
)
这个例子模拟了 shell 中 ls | wc -l
的功能,即列出当前目录内容并统计行数。
#include <unistd.h> // pipe, fork, dup2, execvp, close
#include <sys/wait.h> // wait
#include <stdio.h> // perror, fprintf, stderr
#include <stdlib.h> // exit
int main() {
int pipefd[2];
pid_t pid1, pid2;
// 1. 创建管道
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
// 2. 创建第一个子进程来执行 'ls'
pid1 = fork();
if (pid1 == -1) {
perror("fork ls");
close(pipefd[0]);
close(pipefd[1]);
exit(EXIT_FAILURE);
}
if (pid1 == 0) { // 第一个子进程
// --- 'ls' 进程 ---
// 关闭不需要的读端
close(pipefd[0]);
// 将标准输出重定向到管道的写端
// dup2(oldfd, newfd): 关闭 newfd, 然后使 newfd 成为 oldfd 的副本
if (dup2(pipefd[1], STDOUT_FILENO) == -1) {
perror("dup2 ls");
_exit(EXIT_FAILURE);
}
// 关闭原始的管道写端文件描述符 (因为已经复制到 STDOUT_FILENO)
close(pipefd[1]);
// 执行 'ls' 命令
// execlp 在 PATH 中查找程序
execlp("ls", "ls", (char *)NULL);
// 如果 execlp 返回,说明执行失败
perror("execlp ls failed");
_exit(EXIT_FAILURE);
}
// 3. 创建第二个子进程来执行 'wc -l'
pid2 = fork();
if (pid2 == -1) {
perror("fork wc");
// 可能需要 kill pid1?
close(pipefd[0]);
close(pipefd[1]);
exit(EXIT_FAILURE);
}
if (pid2 == 0) { // 第二个子进程
// --- 'wc -l' 进程 ---
// 关闭不需要的写端
close(pipefd[1]);
// 将标准输入重定向到管道的读端
if (dup2(pipefd[0], STDIN_FILENO) == -1) {
perror("dup2 wc");
_exit(EXIT_FAILURE);
}
// 关闭原始的管道读端文件描述符
close(pipefd[0]);
// 执行 'wc -l' 命令
char *cmd[] = {"wc", "-l", NULL};
execvp(cmd[0], cmd); // execvp 需要 char *const argv[]
// 如果 execvp 返回,说明执行失败
perror("execvp wc failed");
_exit(EXIT_FAILURE);
}
// 4. 父进程
// 父进程不需要使用管道,所以关闭两端
close(pipefd[0]);
close(pipefd[1]);
// 等待两个子进程结束
// 注意:waitpid 可能更精确地等待特定子进程
int status;
if (waitpid(pid1, &status, 0) == -1) {
perror("waitpid ls");
}
if (waitpid(pid2, &status, 0) == -1) {
perror("waitpid wc");
}
printf("Parent process: Both 'ls' and 'wc -l' have finished.\n");
return 0;
}
代码解释:
这个例子很好地展示了管道如何连接两个进程的标准输入和输出,从而实现数据流的传递,就像在 shell 中使用 |
一样。
总结: