在Linux系统中,管道(Pipes)是一种强大的进程间通信(IPC)机制。通过管道,多个命令可以串联起来执行,前一个命令的输出作为下一个命令的输入,这种操作常见于Linux命令行中的流水线式操作。然而,管道不仅限于命令行使用,还可在编程中用于进程间通信。管道可以分为两类:匿名管道 和 有名管道(FIFO,First In First Out)。
1. 管道的基本概念
Linux中的管道是操作系统用于在进程之间传递数据的机制。一个进程可以通过管道将其标准输出(stdout)传递给另一个进程作为标准输入(stdin)。这种方法常用于命令行操作,例如:
ls | grep "txt"
这个命令将 ls
命令的输出传递给 grep
,从而筛选出包含"txt"的文件。
2. 匿名管道(Anonymous Pipe)
2.1 什么是匿名管道?
匿名管道是最常见的管道类型,通常用于父子进程之间的数据传输。它在创建时只存在于内存中,无法通过文件系统访问。由于其匿名性,匿名管道只能用于具有共同祖先的进程之间,通常由一个进程创建后与其子进程通信。
2.2 使用匿名管道
在编程中,匿名管道通常通过系统调用 pipe()
创建。该函数创建一对文件描述符,一个用于读取数据,另一个用于写入数据。
int pipefd[2]; pipe(pipefd);
pipefd[0]
是管道的读端,pipefd[1]
是写端。父进程可以通过写端向管道发送数据,而子进程则可以通过读端读取这些数据。
示例代码:
#include <stdio.h>
#include <unistd.h>int main() {int pipefd[2];pid_t pid;char buffer[100];// 创建匿名管道if (pipe(pipefd) == -1) {perror("pipe");return 1;}pid = fork();if (pid == -1) {perror("fork");return 1;}if (pid == 0) { // 子进程close(pipefd[1]); // 关闭写端read(pipefd[0], buffer, sizeof(buffer)); // 从管道读取数据printf("Child process read: %s\n", buffer);close(pipefd[0]);} else { // 父进程close(pipefd[0]); // 关闭读端write(pipefd[1], "Hello from parent", 17); // 向管道写入数据close(pipefd[1]);}return 0;
}
2.3 特性和限制
- 单向通信:匿名管道只允许单向数据流动,一个进程写入,另一个进程读取。
- 父子进程间通信:匿名管道主要用于具有共同祖先的进程,例如父子进程。
- 进程生命周期:匿名管道的生命周期与创建它的进程相关,进程终止时,管道也随之销毁。
3. 有名管道(Named Pipe, FIFO)
3.1 什么是有名管道?
有名管道(FIFO, First In First Out)是一种存在于文件系统中的管道,它可以在不相关的进程之间实现通信。有名管道通过文件系统的路径名来标识,因而不同的进程只要知道该路径,就可以通过读写该管道文件进行通信。
3.2 创建和使用有名管道
有名管道可以使用 mkfifo
命令或系统调用 mkfifo()
创建。与匿名管道不同,有名管道可以在系统的任何地方被访问,且可以用于完全不相关的进程之间的通信。
创建有名管道
在命令行中,可以通过 mkfifo
命令创建一个有名管道:
mkfifo mypipe
在C语言中,mkfifo()
可以用于创建一个有名管道:
#include <sys/types.h>
#include <sys/stat.h>
int status = mkfifo("/tmp/myfifo", 0666); // 创建有名管道
3.3 使用有名管道
有名管道可以通过普通的文件I/O操作来使用。一个进程可以打开管道文件进行写操作,而另一个进程可以打开它进行读操作。
命令行中的使用示例:
# 在一个终端中,写入有名管道
echo "Hello, world" > mypipe
# 在另一个终端中,读取管道
c
at < mypipe
C语言中的使用示例:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main() {int fd;char buffer[100];// 打开有名管道fd = open("/tmp/myfifo", O_RDONLY);read(fd, buffer, sizeof(buffer));printf("Read from FIFO: %s\n", buffer);close(fd);return 0;
}
3.4 特性和限制
- 双向通信:有名管道可以在不相关的进程间实现双向通信,前提是双方都打开了管道的读写端。
- 持久化:有名管道在文件系统中持久存在,直到被手动删除。
- 同步问题:与匿名管道一样,有名管道的数据传输是同步的,读取操作会阻塞,直到写入数据完成。
4. 匿名管道与有名管道的区别
特性 | 匿名管道 | 有名管道 |
---|---|---|
通信范围 | 父子进程或兄弟进程间 | 不相关的进程之间 |
管道标识 | 不可通过文件系统访问 | 通过路径标识,可持久化 |
生命周期 | 进程结束后销毁 | 文件系统中持久存在 |
数据传输方向 | 单向 | 可双向(需读写都打开) |
5. 管道的进阶使用
5.1 多进程通信的管道
在复杂的系统中,多个进程之间可以通过多重管道来实现复杂的数据通信。例如,可以创建多个匿名管道来在父进程和多个子进程之间传递数据。
5.2 管道的阻塞与非阻塞模式
默认情况下,管道是阻塞的:写入端没有数据时,读取端会阻塞等待数据,反之亦然。可以通过 fcntl()
函数设置管道为非阻塞模式,从而避免进程阻塞。
5.3 与其他IPC机制结合使用
管道可以与其他进程间通信机制(如共享内存、消息队列等)结合使用,构建更复杂的进程通信模型。例如,使用共享内存处理大量数据,而使用管道来同步各个进程的工作。
6. 小结
Linux管道(包括匿名管道和有名管道)是实现进程间通信的核心工具。匿名管道适用于父子进程之间的快速通信,而有名管道可以跨越不相关的进程,实现更加灵活的通信需求。理解并熟练运用这两种管道,可以帮助开发人员在系统编程中高效实现进程间的数据交互。
7.思考
1. 如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),
而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,
再次 read 会返回0,就像读到文件末尾一样。
2. 如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),
而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,
那么管道中剩余的数据都被读取后,再次 read 会阻塞,
直到管道中有数据可读了才读取数据并返回。
考虑到如下代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
int main(void)
{int n;char buff[128];pid_t pid;int fd[2];if(pipe(fd)<0) {perror("pipe");exit(0);}if((pid=fork())<0){perror("fork");exit(0);}if(pid>0){/* parent */ printf("+++++++++++++\n");close(fd[0]); write(fd[1],"hello world",11);//sleep(5);//write(fd[1],"I am a Student",14);printf("+++++++++++++\n");}else{printf("--------------\n");//close(fd[1]);memset(buff,0,128);n = read(fd[0],buff,20);printf("buff=%s\n",buff);memset(buff,0,128);printf("read twice\n");n = read(fd[0],buff,20);printf("buff=%s\n",buff);printf("--------------\n");}return 0;
}
父进程关闭了读端口,通过写端口向pipe中写入了hello world。然后父进程结束。关闭相关文件(读写)描述符
子进程在关闭写端口的时候,父进程结束时候,写文件描述符引用计数为0。所以子进程再次读取后返回0。子进程结束退出。
子进程在不关闭写端口的时候,父进程结束时候,写文件描述符引用计数为1(自己的没关闭)。所以子进程再次读取时候陷入阻塞状态。
因为父进程是在SHELL下执行的。所以当父进程结束时候,Shell进程认为命令执行结束了,于是打印Shell提示符,而子进程等待读取输入。
父进程已经结束,不会给他输入数据,而子进程本身只是为了读取而不是向管道写数据。所以子进程一直在后台运行,通过ps命令可以查看到子进程信息。
所以,子进程只用到读端,因而把写端关闭。防止造成子进程做无用功。。。