我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 pipe
函数,它用于创建匿名管道,这是一种在相关进程(如父子进程)之间进行单向数据通信的重要机制。
pipe 函数介绍
1. 函数介绍
pipe
是一个 Linux 系统调用,用于创建一个匿名管道(Anonymous Pipe)。管道是一种半双工(单向)的进程间通信(IPC)机制,数据只能在一个方向上流动。
管道是 Unix/Linux “一切皆文件” 哲学的体现,管道的两端都可以像普通文件一样使用 read()
和 write()
系统调用进行操作。
重要特性:
- 匿名性: 管道没有名字,不能通过文件系统路径访问。
- 单向性: 数据只能从写端流向读端。
- 亲缘性: 通常用于有亲缘关系的进程间通信。
- 阻塞性: 默认情况下,读写操作可能会阻塞。
- 缓冲性: 内核提供缓冲区来存储管道中的数据。
2. 函数原型
#include <unistd.h> // 必需
int pipe(int pipefd[2]);
3. 功能
- 创建管道: 在内核中创建一个管道缓冲区。
- 返回文件描述符: 通过
pipefd
数组返回两个文件描述符:pipefd[0]
: 管道的读取端(read end)。pipefd[1]
: 管道的写入端(write end)。
4. 参数
int pipefd[2]
: 一个包含两个整数的数组,用于接收返回的文件描述符。pipefd[0]
: 管道的读取端文件描述符。进程可以使用read(pipefd[0], buffer, size)
从此端读取数据。pipefd[1]
: 管道的写入端文件描述符。进程可以使用write(pipefd[1], buffer, size)
向此端写入数据。- 注意: 创建管道后,通常会使用
fork()
创建子进程,然后父子进程分别关闭不需要的端。例如,父进程负责写入,则应关闭pipefd[0]
;子进程负责读取,则应关闭pipefd[1]
。
5. 返回值
- 成功时: 返回 0。
- 失败时:
- 返回 -1,并设置全局变量
errno
来指示具体的错误原因:EFAULT
:pipefd
数组指针无效。EMFILE
: 进程已打开的文件描述符数量达到上限 (RLIMIT_NOFILE
)。ENFILE
: 系统范围内已打开的文件数量达到上限。
- 返回 -1,并设置全局变量
6. 相似函数,或关联函数
pipe2(int pipefd[2], int flags)
:pipe
的扩展版本,允许设置额外的标志,如O_CLOEXEC
(执行时关闭)或O_NONBLOCK
(非阻塞模式)。mkfifo(const char *pathname, mode_t mode)
: 创建命名管道(FIFO),它是一个存在于文件系统中的特殊文件,可以让无亲缘关系的进程通信。socketpair(int domain, int type, int protocol, int sv[2])
: 创建一对相互连接的套接字,可以实现全双工通信。popen(const char *command, const char *type)
,pclose(FILE *stream)
: 高级函数,创建一个管道并启动一个 shell 来执行命令,方便地实现程序间的数据交换。
7. 示例代码
示例 1:基本的父子进程管道通信
这个例子演示了最经典的管道使用场景:父进程向子进程发送数据。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
#define BUFFER_SIZE 256
int main() {
int pipefd[2];
pid_t pid;
char buffer[BUFFER_SIZE];
printf("=== 基本父子进程管道通信 ===\n");
// 1. 创建管道
if (pipe(pipefd) == -1) {
perror("pipe 创建失败");
exit(EXIT_FAILURE);
}
printf("管道创建成功: 读端=%d, 写端=%d\n", pipefd[0], pipefd[1]);
// 2. 创建子进程
pid = fork();
if (pid == -1) {
perror("fork 失败");
// 清理管道文件描述符
close(pipefd[0]);
close(pipefd[1]);
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程:读取数据
printf("子进程 (PID: %d) 开始读取数据...\n", getpid());
// 关闭写端(子进程不需要写入)
close(pipefd[1]);
// 从管道读取数据
ssize_t bytes_read = read(pipefd[0], buffer, BUFFER_SIZE - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0'; // 确保字符串以 null 结尾
printf("子进程读取到数据: %s", buffer);
} else if (bytes_read == 0) {
printf("子进程读取结束 (写端已关闭)\n");
} else {
perror("子进程读取失败");
}
// 关闭读端
close(pipefd[0]);
printf("子进程结束\n");
exit(EXIT_SUCCESS);
} else {
// 父进程:写入数据
printf("父进程 (PID: %d) 开始写入数据...\n", getpid());
// 关闭读端(父进程不需要读取)
close(pipefd[0]);
// 向管道写入数据
const char *message = "Hello from parent process through pipe!\n";
ssize_t bytes_written = write(pipefd[1], message, strlen(message));
if (bytes_written == -1) {
perror("父进程写入失败");
} else {
printf("父进程写入 %ld 字节数据\n", bytes_written);
}
// 关闭写端,这会通知读端数据已写完
close(pipefd[1]);
// 等待子进程结束
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("子进程正常退出,退出码: %d\n", WEXITSTATUS(status));
} else {
printf("子进程异常退出\n");
}
printf("父进程结束\n");
}
return 0;
}
代码解释:
- 首先调用
pipe(pipefd)
创建管道,得到读端pipefd[0]
和写端pipefd[1]
。 - 调用
fork()
创建子进程。此时,父子进程都拥有管道两端的文件描述符副本。 - 在子进程中:
- 关闭不需要的写端
pipefd[1]
。 - 使用
read(pipefd[0], ...)
从管道读取数据。 - 读取完成后关闭读端
pipefd[0]
。
- 关闭不需要的写端
- 在父进程中:
- 关闭不需要的读端
pipefd[0]
。 - 使用
write(pipefd[1], ...)
向管道写入数据。 - 写入完成后关闭写端
pipefd[1]
,这会通知读端没有更多数据。 - 使用
waitpid()
等待子进程结束。
- 关闭不需要的读端
示例 2:双向管道通信
这个例子演示如何使用两个管道实现父子进程之间的双向通信。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
#define BUFFER_SIZE 256
int main() {
int parent_to_child_pipe[2]; // 父进程向子进程发送数据
int child_to_parent_pipe[2]; // 子进程向父进程发送数据
pid_t pid;
char buffer[BUFFER_SIZE];
printf("=== 双向管道通信 ===\n");
// 1. 创建两个管道
if (pipe(parent_to_child_pipe) == -1 || pipe(child_to_parent_pipe) == -1) {
perror("管道创建失败");
exit(EXIT_FAILURE);
}
printf("管道创建成功\n");
printf("父到子管道: 读端=%d, 写端=%d\n", parent_to_child_pipe[0], parent_to_child_pipe[1]);
printf("子到父管道: 读端=%d, 写端=%d\n", child_to_parent_pipe[0], child_to_parent_pipe[1]);
// 2. 创建子进程
pid = fork();
if (pid == -1) {
perror("fork 失败");
close(parent_to_child_pipe[0]);
close(parent_to_child_pipe[1]);
close(child_to_parent_pipe[0]);
close(child_to_parent_pipe[1]);
exit(EXIT_FAILURE);
}
if (pid == 0) {
// 子进程
printf("子进程 (PID: %d) 启动\n", getpid());
// 关闭不需要的文件描述符
close(parent_to_child_pipe[1]); // 子进程不写入父到子管道
close(child_to_parent_pipe[0]); // 子进程不读取子到父管道
// 1. 从父进程接收消息
printf("子进程等待接收父进程消息...\n");
ssize_t bytes_read = read(parent_to_child_pipe[0], buffer, BUFFER_SIZE - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
printf("子进程收到消息: %s", buffer);
// 2. 向父进程发送回复
const char *reply = "Hello parent! I'm child process.\n";
ssize_t bytes_written = write(child_to_parent_pipe[1], reply, strlen(reply));
if (bytes_written == -1) {
perror("子进程发送回复失败");
} else {
printf("子进程发送回复 (%ld 字节)\n", bytes_written);
}
}
// 关闭管道
close(parent_to_child_pipe[0]);
close(child_to_parent_pipe[1]);
printf("子进程结束\n");
exit(EXIT_SUCCESS);
} else {
// 父进程
printf("父进程 (PID: %d) 启动\n", getpid());
// 关闭不需要的文件描述符
close(parent_to_child_pipe[0]); // 父进程不读取父到子管道
close(child_to_parent_pipe[1]); // 父进程不写入子到父管道
// 1. 向子进程发送消息
const char *message = "Hello child! I'm parent process.\n";
printf("父进程向子进程发送消息...\n");
ssize_t bytes_written = write(parent_to_child_pipe[1], message, strlen(message));
if (bytes_written == -1) {
perror("父进程发送消息失败");
} else {
printf("父进程发送消息 (%ld 字节)\n", bytes_written);
}
// 2. 等待并接收子进程的回复
printf("父进程等待子进程回复...\n");
ssize_t bytes_read = read(child_to_parent_pipe[0], buffer, BUFFER_SIZE - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
printf("父进程收到回复: %s", buffer);
}
// 关闭管道
close(parent_to_child_pipe[1]);
close(child_to_parent_pipe[0]);
// 等待子进程结束
int status;
waitpid(pid, &status, 0);
printf("父进程结束\n");
}
return 0;
}
代码解释:
1. 创建两个管道:一个用于父进程向子进程发送数据,另一个用于子进程向父进程发送数据。
2. 在父子进程中分别关闭不需要的管道端。
3. 通过协调读写操作,实现双向通信。
示例 3:管道与错误处理
这个例子重点演示管道的错误处理和一些特殊情况。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
#include <fcntl.h>
#define BUFFER_SIZE 256
// 演示管道错误处理
void demonstrate_pipe_errors() {
printf("=== 管道错误处理演示 ===\n");
// 1. 传递无效指针
printf("1. 传递无效指针给 pipe()...\n");
if (pipe(NULL) == -1) {
printf(" 错误: %s\n", strerror(errno));
if (errno == EFAULT) {
printf(" 说明: pipe() 参数不能为 NULL\n");
}
}
printf("\n");
}
// 演示管道读写特性
void demonstrate_pipe_characteristics() {
printf("=== 管道特性演示 ===\n");
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe 创建失败");
return;
}
pid_t pid = fork();
if (pid == -1) {
perror("fork 失败");
close(pipefd[0]);
close(pipefd[1]);
return;
}
if (pid == 0) {
// 子进程
close(pipefd[1]); // 关闭写端
char buffer[BUFFER_SIZE];
printf("子进程: 尝试从空管道读取 (会阻塞)...\n");
// 读取数据
ssize_t bytes_read = read(pipefd[0], buffer, BUFFER_SIZE - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
printf("子进程: 读取到数据: %s", buffer);
} else if (bytes_read == 0) {
printf("子进程: 读取到文件结束 (所有写端都已关闭)\n");
} else {
perror("子进程: 读取失败");
}
close(pipefd[0]);
exit(EXIT_SUCCESS);
} else {
// 父进程
close(pipefd[0]); // 关闭读端
// 写入一些数据
const char *message = "Message from parent\n";
printf("父进程: 向管道写入数据...\n");
write(pipefd[1], message, strlen(message));
// 关闭写端,通知子进程结束
printf("父进程: 关闭写端,通知子进程结束...\n");
close(pipefd[1]);
wait(NULL);
}
printf("\n");
}
// 演示管道容量和阻塞行为
void demonstrate_pipe_capacity() {
printf("=== 管道容量和阻塞行为演示 ===\n");
printf("注意: 这个演示可能需要较长时间\n");
int pipefd[2];
if (pipe(pipefd) == -1) {
perror("pipe 创建失败");
return;
}
// 获取管道容量(Linux 特定)
int pipe_capacity = fcntl(pipefd[1], F_GETPIPE_SZ);
if (pipe_capacity != -1) {
printf("管道容量: %d 字节\n", pipe_capacity);
}
pid_t pid = fork();
if (pid == -1) {
perror("fork 失败");
close(pipefd[0]);
close(pipefd[1]);
return;
}
if (pid == 0) {
// 子进程:读取端
close(pipefd[1]); // 关闭写端
sleep(2); // 让父进程先填满管道
char buffer[1024];
int total_read = 0;
ssize_t bytes_read;
printf("子进程: 开始读取数据...\n");
while ((bytes_read = read(pipefd[0], buffer, sizeof(buffer))) > 0) {
total_read += bytes_read;
printf("子进程: 读取 %ld 字节,总计 %d 字节\n", bytes_read, total_read);
}
printf("子进程: 读取完成,总计 %d 字节\n", total_read);
close(pipefd[0]);
exit(EXIT_SUCCESS);
} else {
// 父进程:写入端
close(pipefd[0]); // 关闭读端
char data[1024];
memset(data, 'A', sizeof(data) - 1);
data[sizeof(data) - 1] = '\0';
int total_written = 0;
ssize_t bytes_written;
printf("父进程: 开始写入大量数据...\n");
// 写入数据直到管道满(会阻塞)
for (int i = 0; i < 100; i++) {
bytes_written = write(pipefd[1], data, strlen(data));
if (bytes_written == -1) {
perror("父进程: 写入失败");
break;
}
total_written += bytes_written;
printf("父进程: 写入 %ld 字节,总计 %d 字节\n", bytes_written, total_written);
}
printf("父进程: 写入完成,总计 %d 字节\n", total_written);
close(pipefd[1]);
wait(NULL);
}
printf("\n");
}
// 演示 pipe2 和非阻塞管道
void demonstrate_pipe2_and_nonblocking() {
printf("=== pipe2 和非阻塞管道演示 ===\n");
#ifdef __linux__
int pipefd[2];
// 使用 pipe2 创建非阻塞管道
if (pipe2(pipefd, O_NONBLOCK) == -1) {
perror("pipe2 创建失败");
printf("可能是因为系统不支持 pipe2\n");
return;
}
printf("使用 pipe2 创建了非阻塞管道: 读端=%d, 写端=%d\n", pipefd[0], pipefd[1]);
// 尝试从空的非阻塞管道读取
char buffer[10];
ssize_t bytes_read = read(pipefd[0], buffer, sizeof(buffer));
if (bytes_read == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
printf("从空的非阻塞管道读取: %s\n", strerror(errno));
printf("说明: 非阻塞模式下,没有数据时立即返回错误\n");
} else {
perror("读取失败");
}
}
close(pipefd[0]);
close(pipefd[1]);
#else
printf("pipe2 在此系统上不可用\n");
#endif
printf("\n");
}
int main() {
printf("管道 (pipe) 函数演示程序\n");
printf("当前进程 PID: %d\n\n", getpid());
demonstrate_pipe_errors();
demonstrate_pipe_characteristics();
// demonstrate_pipe_capacity(); // 这个演示可能需要较长时间,可选择性运行
demonstrate_pipe2_and_nonblocking();
printf("=== 总结 ===\n");
printf("管道 (pipe) 关键知识点:\n");
printf("1. 单向通信: 数据只能从写端流向读端\n");
printf("2. 亲缘进程: 通常用于父子进程通信\n");
printf("3. 阻塞特性: 默认情况下读写可能阻塞\n");
printf("4. 文件结束: 当所有写端关闭时,读端返回 0\n");
printf("5. 缓冲机制: 内核提供缓冲区存储数据\n");
printf("6. 错误处理: 注意 EFAULT, EMFILE, ENFILE 等错误\n");
printf("7. 资源清理: 使用完后必须关闭文件描述符\n");
printf("8. 双向通信: 需要创建两个管道\n\n");
printf("最佳实践:\n");
printf("- 及时关闭不需要的管道端\n");
printf("- 正确处理读写返回值\n");
printf("- 考虑使用 pipe2 设置 O_CLOEXEC 标志\n");
printf("- 对于复杂通信,考虑使用命名管道 (FIFO) 或套接字\n");
return 0;
}
代码解释:
编译和运行:
# 编译示例
gcc -o pipe_example1 pipe_example1.c
gcc -o pipe_example2 pipe_example2.c
gcc -o pipe_example3 pipe_example3.c
# 运行示例
./pipe_example1
./pipe_example2
./pipe_example3
总结: