代码运行及测试环境:linux centos7.6
在阅读这篇文章时,需要掌握OS对文件管理的基础知识(文件打开表、文件描述符、索引结点…)
前言
我们都知道进程是具有独立性的,意味着进程之间无法相互通信。但在一些情况下,不得不让进程之间进行通信。通信的作用主要有:
- 数据的传输。
- 数据的共享。
- 控制其他进程。
数据的传输、共享是非常容易理解的。为什么需要一个进程去控制另一个进程呢?做一个虚拟的假设,进程A负责检测地铁进站,进程B负责打开地铁门。进程B一直处于休眠状态,当进程A检测出地铁到站停靠后,进程A可以唤醒进程B,打开地铁门。可见,让一个进程去控制另外一个进程,在日常生活中是非常频繁的, 也是十分重要的。
如果要让两个相互独立的进程进行通信,那它们之间需要一个媒介。OS提供了这个媒介,并且对它们之间的通信进行管理。根据OS提供媒介的不同,产生了许多进程间通信的方式,例如:共享内存、消息队列、管道等等。我在这篇文章里只谈论管道通信——匿名管道。
匿名管道的底层原理
首先强调匿名管道只能用于 具有“血缘关系”的进程之间的通信。
当用户请求建立匿名管道时,操作系统会为进程创建一个管道文件,这个文件它并不需要文件名,因为OS建立管道文件后,就会直接把它的文件属性拷贝到打开文件表的一个条目内,用户可以获取到对应的文件描述符,进而读写管道。
它会让俩个相邻的打开文件表表项同时指向这个管道文件的inode, 是为了读写操作分离,一个文件描述符对应的是写入操作,另一个文件描述符对应的是读取操作。
虽然匿名管道的本质是一个文件,但和普通文件有许多差别。从另外一个角度来看,匿名管道更偏向于解释成一块缓冲区。用户并不是直接将数据写入磁盘上的数据块,也不会直接从磁盘上读取数据出来, 而是以内核缓冲区为媒介。
由OS再去调用数据从内核缓冲区写入磁盘和读到内核缓冲区的接口。
那么匿名管道是如何实现 具有血缘关系的进程 之间的通信呢?以父子进程间的管道通信为例。
首先父进程创建一个管道, 再创建子进程,子进程的PCB是以父进程为模板的,父子进程此刻的打开文件表是一样的, 所以父进程文件打开表有两个表项指向管道,子进程打开文件表也有两个表项指向该管道,并且文件描述符此刻是一样的。这样,相互独立的两个父子进程看到了同一份资源——“管道”, 管道自然而然充当了它们通信的媒介。
管道通信是单向通信的,只允许一端进行写入,另一端进行读取。如果要实现管道通信,需要根据情况,关闭对应的读写端。
匿名管道创建过程
我们将上述的过程简化一下
🍍第一步:父进程创建匿名管道
🥑第二步:父进程创建出子进程
🍉第三步:根据需要,关闭对应的读端和写端
(这里以父进程写入,子进程读取为例)
匿名管道的几种情况
创建匿名管道的系统调用pipe
#include <unistd.h>
int pipe(int pipefd[2]);
//pipefd是一个输出型参数:
//pipefd[0]对应读端的文件描述符 pipefd[1]对应写端
//如果创建管道成功返回0,否则返回-1
简单写一个父进程读取管道、子进程写数据进管道的例子。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
//1.创建管道
int pipefd[2];
if(pipe(pipefd) != 0)
{
perror("pipe");
exit(-1);
}
//2.父进程创建出子进程
if(fork() == 0)
{
//child
//3.1 让子进程写入数据进入管道 ,关闭读端
close(pipefd[0]);
const char *str = "i am ydy.";
while(1)
{
write(pipefd[1], str, strlen(str));
sleep(1);
}
}
else
{
//father
//3.2 父进程从匿名管道种读取数据, 关闭写端
close(pipefd[1]);
while(1)
{
char buffer[64] = {0};
ssize_t s = read(pipefd[0], buffer, sizeof(buffer));
if(s < 0){
//读取失败
break;
}
else if(s == 0){
//写端关闭,且数据读取完毕
printf("child quit...\n");
break;
}
else{
printf("child say# %s\n", buffer);
}
}
}
waitpid(-1, NULL, WNOHANG);
return 0;
}
匿名管道的几种情况:
🌻读端读得慢,写端写得快
🌼读端读得快,写端写得慢
💐写端在写入数据,读端突然关闭
🌷读端在读取数据,写端突然关闭
这几种情况的特点,可以自行检测得出,我在这里直接写结论,当然这些结论的正确性我在此之前都用代码检验过了,毕竟“实践是检验真理的唯一标准”。
🌻读端读得慢,写端写得快
管道内有数据,读端就可以读取。由于写端写得比较快,所以按照趋势下去,管道一定会满。管道满了以后,写端进程会被阻塞,等待读端读取数据。你可能认为,管道满了后,读端读取一份数据,写端就会写入数据,事实并不是这样。实际上,写进程一旦被阻塞,只有管道能够提供一定的空闲空间大小(这个大小具体我并不知道是多少, 在我的机子上跑,测出来的是至少是1kb)后,写进程才会被唤醒。
🌼读端读得快,写端写得慢
由于写的速度赶不上读取的速度, 所以管道极可能某个时刻没有数据。读进程会被阻塞,等待写进程写入数据,直到管道有数据。
💐写端在写入数据,读端突然关闭
管道的作用就是为了让两个进程通信,读端关闭了,写进程又不需要拿取数据,所以读进程不在,管道就没有意义了。所以读端关闭,写进程也会退出。
🌷读端在读取数据,写端突然关闭
写端关闭了,管道内可能还会有数据。读进程是需要拿数据的,管道里的东西还有用,所以读端把数据全部读完后,读进程再退出。
读进程读取得慢,写进程写入得快,会出现这种现象的原因本质上是因为管道是有大小的,在Linux往管道写入64KB数据的时候,写进程就不再写入了,而超过4KB的时候,OS就不再保证管道的写入具有原子性了。对于一端关闭的现象,间接阐述了管道机制必须要有确定对方存在的协调能力。
匿名管道特点
- 仅限具有血缘关系的进程使用
根据匿名管道的创建过程可知,匿名管道的本质是一个文件,没有文件名,由用户请求,OS帮助建立后,直接把文件属性拷贝到进程的打开文件表内。血缘关系的进程PCB都是以“父进程”模板创建的,所以子进程以及有血缘关系的进程,打开文件表内都有关于匿名管道的属性。由于没有文件名,其他进程无法自行打开文件,因此非血缘关系的进程间无法使用匿名管道。 - 匿名管道的生命周期是随进程的
因为下一次,进程无法打开上一个管道文件(没有文件名)。 - 管道是单向通信的。
- 管道是面向字节流的
- 管道机制必须能够拥有互斥、同步和确定对方存在的协调能力