我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 pipe 函数,它用于创建匿名管道,这是一种在相关进程(如父子进程)之间进行单向数据通信的重要机制。
data-ad-format="fluid"
data-ad-layout-key="-7k+ex-4a-9w+4a">
pipe 函数介绍
1. 函数介绍
pipe 是一个 Linux 系统调用,用于创建一个匿名管道(Anonymous Pipe)。管道是一种半双工(单向)的进程间通信(IPC)机制,数据只能在一个方向上流动。
管道通常用于具有亲缘关系的进程之间通信,最常见的场景是父进程和子进程之间的数据传递。创建管道后,会得到两个文件描述符:一个用于读取(read end),一个用于写入(write end)。写入端写入的数据会被内核缓冲,然后可以从读取端读取出来。
管道是 Unix/Linux “一切皆文件” 哲学的体现,管道的两端都可以像普通文件一样使用 read() 和 write() 系统调用进行操作。
重要特性:
2. 函数原型
1 2 3 4
| #include <unistd.h> // 必需
int pipe(int pipefd[2]);
|
3. 功能
返回文件描述符: 通过 pipefd 数组返回两个文件描述符:
4. 参数
int pipefd[2]: 一个包含两个整数的数组,用于接收返回的文件描述符。
pipefd[0]: 管道的读取端文件描述符。进程可以使用 read(pipefd[0], buffer, size) 从此端读取数据。
pipefd[1]: 管道的写入端文件描述符。进程可以使用 write(pipefd[1], buffer, size) 向此端写入数据。
注意: 创建管道后,通常会使用 fork() 创建子进程,然后父子进程分别关闭不需要的端。例如,父进程负责写入,则应关闭 pipefd[0];子进程负责读取,则应关闭 pipefd[1]。
5. 返回值
失败时:
返回 -1,并设置全局变量 errno 来指示具体的错误原因:
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:基本的父子进程管道通信
这个例子演示了最经典的管道使用场景:父进程向子进程发送数据。
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
| #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() 创建子进程。此时,父子进程都拥有管道两端的文件描述符副本。
在子进程中:
在父进程中:
示例 2:双向管道通信
这个例子演示如何使用两个管道实现父子进程之间的双向通信。
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 105 106 107
| #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; }
|
代码解释:
- 创建两个管道:一个用于父进程向子进程发送数据,另一个用于子进程向父进程发送数据。2. 在父子进程中分别关闭不需要的管道端。3. 通过协调读写操作,实现双向通信。
示例 3:管道与错误处理
这个例子重点演示管道的错误处理和一些特殊情况。
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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
| #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; }
|
代码解释:
- demonstrate_pipe_errors 演示了传递无效参数给 pipe() 的错误处理。2. demonstrate_pipe_characteristics 演示了管道的基本读写行为和文件结束条件。3. demonstrate_pipe_capacity 演示了管道的容量限制和阻塞行为(注释掉了,因为可能运行时间较长)。4. demonstrate_pipe2_and_nonblocking 演示了 pipe2 函数和非阻塞模式的使用。5. main 函数协调各个演示部分,并在最后总结关键知识点。
编译和运行:
1 2 3 4 5 6 7 8 9 10
| # 编译示例 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
|
总结:
pipe 函数是 Linux 进程间通信的基础工具之一。它简单高效,特别适合父子进程间的数据传递。理解其单向性、阻塞性和缓冲机制对于正确使用管道至关重要。在实际编程中,要注意及时关闭不需要的文件描述符,正确处理各种返回值和错误情况,并根据需要考虑使用 pipe2 或其他更高级的 IPC 机制。