好的,我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 pipe
函数,它是实现进程间通信 (IPC - Inter-Process Communication) 的基础机制之一,尤其适用于具有亲缘关系的进程(如父子进程、兄弟进程)之间进行单向数据传输。
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;
}
代码解释:
- 调用
pipe(pipefd)
创建管道,成功后pipefd[0]
是读端,pipefd[1]
是写端。 - 调用
fork()
创建子进程。fork
之后,父子进程都拥有管道两端的文件描述符副本。 - 子进程 (cpid == 0):
- 关闭不需要的写端
pipefd[1]
。 - 进入循环,调用
read(pipefd[0], &buf, 1)
从管道读取数据(一次读一个字节)。 - 将读到的字节写入标准输出。
- 当
read
返回 0 时,表示已到达 EOF(因为父进程关闭了写端),循环结束。 - 关闭读端
pipefd[0]
。 - 使用
_exit()
退出(在子进程中通常推荐使用_exit
而非exit
,以避免刷新 stdio 缓冲区可能带来的问题)。
- 关闭不需要的写端
- 父进程 (cpid > 0):
- 关闭不需要的读端
pipefd[0]
。 - 定义要发送的消息。
- 调用
write(pipefd[1], message, ...)
将消息写入管道。 - 关闭写端
pipefd[1]
。这一步很重要,它会通知子进程数据已发送完毕(读端read
会返回 0)。 - 调用
wait()
等待子进程结束,并检查其退出状态。
- 关闭不需要的读端
示例 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;
}
代码解释:
- 调用
pipe(pipefd)
创建管道。 - 第一次
fork()
创建子进程pid1
。 - 在
pid1
子进程中:- 关闭管道读端。
- 使用
dup2(pipefd[1], STDOUT_FILENO)
将子进程的标准输出 (STDOUT_FILENO
,即文件描述符 1) 重定向到管道的写端。这意味着ls
命令的所有输出都会被写入管道。 - 关闭原始的管道写端文件描述符
pipefd[1]
。 - 调用
execlp("ls", "ls", NULL)
执行ls
命令。因为标准输出已被重定向,ls
的输出会进入管道。
- 第二次
fork()
创建子进程pid2
。 - 在
pid2
子进程中:- 关闭管道写端。
- 使用
dup2(pipefd[0], STDIN_FILENO)
将子进程的标准输入 (STDIN_FILENO
,即文件描述符 0) 重定向到管道的读端。这意味着wc
命令会从管道读取输入。 - 关闭原始的管道读端文件描述符
pipefd[0]
。 - 调用
execvp("wc", cmd)
执行wc -l
命令。因为标准输入已被重定向,wc
会从管道读取数据并统计行数,结果输出到标准输出(通常是屏幕)。
- 父进程:
- 关闭自己的管道文件描述符(不再需要)。
- 调用
waitpid
等待两个子进程结束。
这个例子很好地展示了管道如何连接两个进程的标准输入和输出,从而实现数据流的传递,就像在 shell 中使用 |
一样。
总结:
pipe
函数是 Linux 进程间通信的基础工具之一。它创建的匿名管道简单高效,特别适合于有亲缘关系的进程之间的单向数据传输。理解其与 fork
、dup2
、read
、write
等函数的配合使用是掌握 Linux IPC 的关键。