pipe系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 pipe 函数,它是实现进程间通信 (IPC - Inter-Process Communication) 的基础机制之一,尤其适用于具有亲缘关系的进程(如父子进程、兄弟进程)之间进行单向数据传输。

1. 函数介绍

pipe 是一个 Linux 系统调用,用于创建一个匿名管道 (anonymous pipe)。管道是一种半双工(单向)的通信通道,具有固定的读端和写端。

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

你可以把管道想象成一个单向的水管或传送带:

  • 一端是写入端 (write end):数据被“放入”管道。

  • 另一端是读取端 (read end):数据从管道中被“取出”。

  • 数据在管道内部按照先进先出 (FIFO) 的顺序流动。

  • 管道有有限的容量(通常由 PIPE_BUF 常量定义,Linux 上通常是 65536 字节)。如果管道满了,写入操作会阻塞;如果管道空了,读取操作会阻塞。

匿名管道最常见的用途是在相关进程(通过 fork 创建的父子进程或兄弟进程)之间传递数据。

2. 函数原型

1
2
3
4
#include <unistd.h> // 必需

int pipe(int pipefd&#91;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 在父进程和子进程之间传递数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#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&#91;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&#91;0]);
close(pipefd&#91;1]);
exit(EXIT_FAILURE);
}

// 3. 根据进程 ID 执行不同代码
if (cpid == 0) { // 子进程执行代码
// --- 子进程 ---
// 关闭不需要的写端
if (close(pipefd&#91;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&#91;0], &buf, 1) > 0) {
write(STDOUT_FILENO, &buf, 1); // 写入到标准输出 (屏幕)
}

// 检查 read 是否因错误而失败
if (read(pipefd&#91;0], &buf, 1) == -1) {
perror("child: read");
_exit(EXIT_FAILURE);
}

printf("Child process: Finished reading. Exiting.\n");

// 关闭读端
if (close(pipefd&#91;0]) == -1) {
perror("child: close read end");
_exit(EXIT_FAILURE);
}

_exit(EXIT_SUCCESS); // 子进程成功退出

} else { // 父进程执行代码
// --- 父进程 ---
// 关闭不需要的读端
if (close(pipefd&#91;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&#91;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&#91;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;
}

代码解释:

  1. 调用 pipe(pipefd) 创建管道,成功后 pipefd[0] 是读端,pipefd[1] 是写端。2. 调用 fork() 创建子进程。fork 之后,父子进程都拥有管道两端的文件描述符副本。3. 子进程 (cpid == 0):* 关闭不需要的写端 pipefd[1]。* 进入循环,调用 read(pipefd[0], &buf, 1) 从管道读取数据(一次读一个字节)。* 将读到的字节写入标准输出。* 当 read 返回 0 时,表示已到达 EOF(因为父进程关闭了写端),循环结束。* 关闭读端 pipefd[0]。* 使用 _exit() 退出(在子进程中通常推荐使用 _exit 而非 exit,以避免刷新 stdio 缓冲区可能带来的问题)。4. 父进程 (cpid > 0):* 关闭不需要的读端 pipefd[0]。* 定义要发送的消息。* 调用 write(pipefd[1], message, …) 将消息写入管道。* 关闭写端 pipefd[1]。这一步很重要,它会通知子进程数据已发送完毕(读端 read 会返回 0)。* 调用 wait() 等待子进程结束,并检查其退出状态。

示例 2:使用管道实现简单的命令行管道 (ls | wc -l)

这个例子模拟了 shell 中 ls | wc -l 的功能,即列出当前目录内容并统计行数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#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&#91;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&#91;0]);
close(pipefd&#91;1]);
exit(EXIT_FAILURE);
}

if (pid1 == 0) { // 第一个子进程
// --- 'ls' 进程 ---
// 关闭不需要的读端
close(pipefd&#91;0]);

// 将标准输出重定向到管道的写端
// dup2(oldfd, newfd): 关闭 newfd, 然后使 newfd 成为 oldfd 的副本
if (dup2(pipefd&#91;1], STDOUT_FILENO) == -1) {
perror("dup2 ls");
_exit(EXIT_FAILURE);
}

// 关闭原始的管道写端文件描述符 (因为已经复制到 STDOUT_FILENO)
close(pipefd&#91;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&#91;0]);
close(pipefd&#91;1]);
exit(EXIT_FAILURE);
}

if (pid2 == 0) { // 第二个子进程
// --- 'wc -l' 进程 ---
// 关闭不需要的写端
close(pipefd&#91;1]);

// 将标准输入重定向到管道的读端
if (dup2(pipefd&#91;0], STDIN_FILENO) == -1) {
perror("dup2 wc");
_exit(EXIT_FAILURE);
}

// 关闭原始的管道读端文件描述符
close(pipefd&#91;0]);

// 执行 'wc -l' 命令
char *cmd&#91;] = {"wc", "-l", NULL};
execvp(cmd&#91;0], cmd); // execvp 需要 char *const argv&#91;]

// 如果 execvp 返回,说明执行失败
perror("execvp wc failed");
_exit(EXIT_FAILURE);
}

// 4. 父进程
// 父进程不需要使用管道,所以关闭两端
close(pipefd&#91;0]);
close(pipefd&#91;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;
}

代码解释:

  1. 调用 pipe(pipefd) 创建管道。2. 第一次 fork() 创建子进程 pid1。3. 在 pid1 子进程中:* 关闭管道读端。* 使用 dup2(pipefd[1], STDOUT_FILENO) 将子进程的标准输出 (STDOUT_FILENO,即文件描述符 1) 重定向到管道的写端。这意味着 ls 命令的所有输出都会被写入管道。* 关闭原始的管道写端文件描述符 pipefd[1]。* 调用 execlp(“ls”, “ls”, NULL) 执行 ls 命令。因为标准输出已被重定向,ls 的输出会进入管道。4. 第二次 fork() 创建子进程 pid2。5. 在 pid2 子进程中:* 关闭管道写端。* 使用 dup2(pipefd[0], STDIN_FILENO) 将子进程的标准输入 (STDIN_FILENO,即文件描述符 0) 重定向到管道的读端。这意味着 wc 命令会从管道读取输入。* 关闭原始的管道读端文件描述符 pipefd[0]。* 调用 execvp(“wc”, cmd) 执行 wc -l 命令。因为标准输入已被重定向,wc 会从管道读取数据并统计行数,结果输出到标准输出(通常是屏幕)。6. 父进程:* 关闭自己的管道文件描述符(不再需要)。* 调用 waitpid 等待两个子进程结束。

这个例子很好地展示了管道如何连接两个进程的标准输入和输出,从而实现数据流的传递,就像在 shell 中使用 | 一样。

总结:

pipe 函数是 Linux 进程间通信的基础工具之一。它创建的匿名管道简单高效,特别适合于有亲缘关系的进程之间的单向数据传输。理解其与 fork、dup2、read、write 等函数的配合使用是掌握 Linux IPC 的关键。

data-ad-format="auto" data-full-width-responsive="true">