Linux编程:4、进程通信-管道(匿名管道)
一、管道的基本概念
- 定义:
管道是 Linux 中一种半双工(单向)的进程间通信方式,本质是内核中的一块缓冲区,类似特殊文件,数据遵循 “先进先出(FIFO)” 原则,读取后自动删除。 - 分类:
- 匿名管道:无文件名,仅用于有血缘关系的进程(如父子进程)通信。
- 命名管道(FIFO):有文件名,可用于无血缘关系的进程通信
- 为什么使用管道:
信号可以实现进程之间的通信,但是不能传递更多的数据。 解决方案:使用管道。
二、匿名管道的创建与使用
1. 创建管道 ——pipe
系统调用
#include <unistd.h>
int pipe(int pipefd[2]); // pipefd[0]为读端,pipefd[1]为写端
- 成功返回 0,失败返回 - 1。
- 管道缓冲区大小为
PIPE_BUF
(通常 4096 字节),写入超过该大小时会被分割。
2. 管道的特性
- 单向通信:只能从读端读、写端写,若需双向通信需创建两个管道。
- 阻塞机制:
- 读端:无数据时
read
阻塞,直至有数据写入。 - 写端:缓冲区满时
write
阻塞,直至有数据被读取。
- 读端:无数据时
- 文件描述符关闭影响:
- 写端全关闭时,读端
read
返回 0。 - 读端全关闭时,写端
write
触发SIGPIPE
信号(默认终止进程)。
- 写端全关闭时,读端
三、管道在进程间的通信场景
1. 同一进程内的管道(无实际用途)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>int main(void)
{int fd[2];int ret = pipe(fd); // 创建一个匿名管道if (ret < 0) {perror("pipe failed.");exit(1);}char msg[] = "hello world";int count = write(fd[1], msg, strlen(msg) + 1);printf("向管道写入 %d 个字节:%s\n", count, msg);// BUFSIZ 缓冲区默认大小为 BUFSIZ,具体大小与系统定义有关char buff[BUFSIZ];count = read(fd[0], buff, BUFSIZ);printf("从管道读到 %d 个字节: %s\n", count, buff);return 0;
}
2. 父子进程间的管道通信
- 原理:
fork
后子进程复制父进程的文件描述符,从而父子进程可通过同一管道读写。 - 实例代码:
-
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/wait.h> #include <unistd.h> int main(void) {int fd[2];int ret = pipe(fd); // 创建一个匿名管道if (ret < 0) {perror("pipe failed.");exit(1);}// 创建一个进程// 此时管道的读端 fd[0]和写端 fd[1]被复制到子进程中ret = fork();if (ret == -1) {perror("fork failed.");exit(1);}if (ret == 0) { // 在子进程中char buff[BUFSIZ];printf("子进程正在从管道读取数据...\n");// 如果父进程还没有向管道写入数据,此时读管道就会导致阻塞int count = read(fd[0], buff, BUFSIZ);printf("子进程从管道读到了%d 个字节:%s\n", count, buff);exit(EXIT_SUCCESS);}else { // 在父进程中char msg[] = "hello world";sleep(3);int count = write(fd[1], msg, strlen(msg) + 1);printf("父进程向管道写入%d 个字节:%s\n", count, msg);int status;wait(&status); // 等待任意一个子进程结束}return 0; }
3. 结合exec
的管道通信
- 场景:父进程创建管道后,子进程通过
execl
执行其他程序,利用管道传递数据。 - 实现步骤:
- 父进程创建管道并
fork
子进程。 - 子进程通过命令行参数接收管道读端文件描述符,或通过
dup
重定向标准输入 / 输出。
- 父进程创建管道并
- 代码示例(父进程向子进程传递数据):
#include <cstdlib> #include <cstring> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h>int main() {// 创建一个管道int fd[2];int ret = pipe(fd);if (ret == -1) {perror("pipe failed.");exit(-1);}int pid = fork();if (pid < 0) {perror("fork failed.");exit(-2);}else if (pid == 0) {// 子进程读取数据char arg[8];sprintf(arg, "%d", fd[0]);printf("正在调用 test2进程...\n");execl("test2", "test2", arg, NULL);perror("execl failed");exit(EXIT_FAILURE);}else {// 父进程写入数据sleep(3);char msg[] = "hello world";int count = write(fd[1], msg, strlen(msg) + 1);printf("父进程向管道里写入了 %d 个字符: %s\n", count, msg);int status;wait(&status);}return 0; }
#include <cstdlib> #include <cstring> #include <stdio.h> #include <stdlib.h> #include <unistd.h>int main(int argc, char* argv[]) {// 检查参数数量是否正确if (argc != 2) {fprintf(stderr, "用法: %s <fd>\n", argv[0]);return EXIT_FAILURE;}// 从argv[1]获取文件描述符int fd;if (sscanf(argv[1], "%d", &fd) != 1) {fprintf(stderr, "错误: 无法将参数转换为整数\n");return EXIT_FAILURE;}char buff[BUFSIZ];int count = read(fd, buff, BUFSIZ);if (count == -1) {perror("read failed");}else {printf("test2从管道里读取了 %d 个字符: %s\n", count, buff);}close(fd); // 关闭读取端return 0; }
4. 管道重定向标准输入 / 输出
- 原理:通过
dup
或dup2
将管道文件描述符复制到stdin(0)
或stdout(1)
,使程序直接通过标准流读写管道。 - 代码示例(子进程从管道读标准输入):
#include <cstdlib> #include <cstring> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h>int main() {// 创建一个管道,用于父子进程间通信// fd[0] 是读端,fd[1] 是写端int fd[2];int ret = pipe(fd);if (ret == -1) {perror("pipe failed.");exit(-1);}// 创建子进程int pid = fork();if (pid < 0) {perror("fork failed.");exit(-2);}else if (pid == 0) {// 子进程代码// 子进程负责从管道读取数据并执行test4程序// 关闭标准输入,准备将管道读端重定向到标准输入close(0); // 将管道的读端复制到文件描述符0(标准输入)dup(fd[0]); // 现在标准输入已经被替换为管道的读端// 关闭不需要的文件描述符,避免资源泄漏close(fd[0]); // 已经复制到标准输入,原fd不再需要close(fd[1]); // 子进程不使用写端,关闭它printf("正在调用 test4进程...\n");// 执行外部程序test4,该程序将从标准输入读取数据// 由于标准输入已被重定向,test4将从管道读取数据execl("test4", "test4", NULL);perror("execl failed"); // 如果execl失败,打印错误信息exit(EXIT_FAILURE);}else {// 父进程代码// 父进程负责向管道写入数据// 关闭标准输出,准备将管道写端重定向到标准输出close(1);// 将管道的写端复制到文件描述符1(标准输出)dup(fd[1]);// 现在标准输出已经被替换为管道的写端// 关闭不需要的文件描述符close(fd[0]); // 父进程不使用读端,关闭它close(fd[1]); // 已经复制到标准输出,原fd不再需要sleep(3);// 向标准输出写入数据,实际上数据会被写入管道char msg[] = "hello world";printf("%s\n", msg);// 刷新标准输出缓冲区,确保数据立即写入管道fflush(stdout);// 等待子进程结束,获取其退出状态int status;wait(&status);}return 0; }
#include <cstdlib> #include <cstring> #include <stdio.h> #include <stdlib.h> #include <unistd.h>int main(int argc, char* argv[]) {char buff[BUFSIZ];int count = read(0, buff, BUFSIZ);if (count == -1) {perror("read failed");} else {printf("test4从标准输出里读取了 %d 个字符: %s\n", count, buff);}return 0; }
四、管道的应用场景
- Shell 命令管道:如
ps -ef | grep test
,前一命令输出通过管道作为后一命令输入。 - 父子进程间数据传输:适用于有血缘关系的进程间简单数据交换。
- 程序间解耦:通过管道将数据生产方与消费方分离,如日志系统。
五、注意事项
- 文件描述符关闭:未使用的读 / 写端需及时关闭,避免资源泄漏或阻塞。
- 缓冲区大小:单次写入不超过
PIPE_BUF
(4096 字节)时保证原子性,否则可能被分割。 - 信号处理:读端关闭时写管道会触发
SIGPIPE
,需通过signal
或sigaction
处理以避免进程崩溃。