1. 概述
管道(Pipe)是一种在 Unix/Linux 等操作系统中**
用于进程间通信的机制
**。它可以用于在两个相关的进程之间传递数据,实现简单的数据流通信。管道分为匿名管道和命名管道(FIFO)两种。
管道的本质其实就是
内核中的一块内存(或者叫内核缓冲区)
,这块缓冲区中的数据存储在一个环形队列中,因为管道在内核里边,因此我们不能直接对其进行任何操作。
管道特点:
-
管道对应的内核缓冲区大小是固定的,默认为4k(也就是队列最大能存储4k数据)
-
管道分为两部分:读端和写端(队列的两端),数据从写端进入管道,从读端流出管道。
-
管道中的数据只能读一次,做一次读操作之后数据也就没有了(读数据相当于出队列)。
-
管道是单工的:数据只能单向流动, 数据从写端流向读端。
-
对管道的操作(读、写)默认是阻塞的
-
读管道:管道中没有数据,读操作被阻塞,当管道中有数据之后阻塞才能解除
-
写管道:管道被写满了,写数据的操作被阻塞,当管道变为不满的状态,写阻塞解除
读和写都是通过Linux文件IO函数实现的:
// 读管道
ssize_t read(int fd, void *buf, size_t count);
// 写管道的函数
ssize_t write(int fd, const void *buf, size_t count);
原理:
在上图中假设
父进程通过一系列操作可以通过文件描述符表中的文件描述符fd3写管道,通过fd4读管道
,然后再**通过 fork() 创建出子进程,那么在父进程中被分配的文件描述符 fd3, fd4也就被拷贝到子进程中,子进程通过 fd3可以将数据写入到内核的管道中,通过fd4将数据从管道中读出来。**也就能够进行数据的通信
2. 匿名管道
匿名管道是一种
只能在相关的进程之间使用的管道
。通常,匿名管道通过
pipe
系统调用创建,其特点包括:
-
单向通信:
匿名管道是单向的,可以是单向读或单向写。对于双向通信,需要创建两个管道。
-
进程衍生关系:
通常在创建子进程时,父进程会创建管道,并将其传递给子进程。
-
阻塞:
如果管道中没有数据可读,读取进程会被阻塞,直到有数据可用。同样,如果管道已满,写入进程会被阻塞,直到有空间可用。
-
生命周期:
匿名管道在进程间通信结束后自动被关闭,且无法用于无关的进程之间通信。
创建函数
#include <unistd.h>
// 创建一个匿名的管道, 得到两个可用的文件描述符
int pipe(int pipefd[2]);
以下是一个简单的 C 语言示例,演示了父子进程通过匿名管道进行通信:
#include <stdio.h>
#include <unistd.h>
int main(){
int pipefd[2];
char buffer[20];
// 创建管道
if(pipe(pipefd) == -1){
perror("pipe");
return -1;
}
pid_t pid = fork();
if(pid == -1){
perror("fork");
return 1;
}
else if(pid == 0) { // 子进程
// 关闭写入端
close(pipefd[1]);
read(pipefd[0], buffer, sizeof(buffer));
close(pipefd[0]);
printf("Child Process: Received message - %s\n", buffer);
} else { // 父进程
close(pipefd[0]); // 关闭读取段
write(pipefd[1], "Hello, child", 13);
close(pipefd[1]);
printf("Parent Process: Sent message\n");
}
return 0;
}
3. 有名管道
命名管道是一种在无关的进程之间进行通信的管道,它通过文件系统中的特殊文件来实现。特点包括:
-
命名:
命名管道有一个关联的文件路径,可以通过文件路径在无关的进程之间进行通信。
-
阻塞:
与匿名管道类似,命名管道也会在读取端没有数据可读或写入端已满时阻塞。
-
持久性:
命名管道的生命周期独立于创建进程,需要显式删除。
以下是一个简单的 shell 命令示例,演示了命名管道的使用:
# 创建命名管道
mkfifo myfifo
# 在一个终端中写入数据
echo "Hello, FIFO!" > myfifo
# 在另一个终端中读取数据
cat myfifo
# 删除命名管道
rm myfifo
总的来说,管道是一种简单而有效的进程间通信方式,特别适用于相关进程之间的通信。
4. 管道的读写行为
关于管道不管是有名的还是匿名的,在进行读写的时候,它们表现出的行为是一致的,下面是对其读写行为的总结:
读管道,需要根据写端的状态进行分析:
-
写端没有关闭 (操作管道写端的文件描述符没有被关闭)
-
如果管道中没有数据 ==> 读阻塞, 如果管道中被写入了数据, 阻塞解除
-
如果管道中有数据 ==> 不阻塞,管道中的数据被读完了, 再继续读管道还会阻塞
-
写端已经关闭了 (没有可用的文件描述符可以写管道了)
-
管道中没有数据 ==> 读端解除阻塞, read函数返回0
-
管道中有数据 ==> read先将数据读出, 数据读完之后返回0, 不会阻塞了
写管道,需要根据读端的状态进行分析:
-
读端没有关闭
-
如果管道有存储的空间, 一直写数据
-
如果管道写满了, 写操作就阻塞, 当读端将管道数据读走了, 解除阻塞继续写
-
读端关闭了,管道破裂(异常), 进程直接退出