目录
前言:
本篇文章将会为大家带来进程间通信的基础概念,随后为大家大致了解一下我们古老的进程间通信的方法------管道。
希望通过本篇文章,能够帮助大家掌握进程间通信的基本概念,以及掌握匿名命名管道原理操作!
一、进程间通信概念
我们曾经说过,进程具有独立性,操作系统中的每个进程都拥有自己独立的运行环境和资源,彼此之间相互隔离,互不干扰的特性。这是现代操作系统设计中的一个基本原则。
但是,现代计算机不可能独立完成一项任务,进程与进程之间,也必须要出现交流。
那么如何进行这样的通信呢?我们比如面临着一个前提:得让不同的进程,看到同一份资源。
操作系统提供了打破这种独立性的机制:进程间通信(IPC)机制。
为什么要提供这种机制呢?因为我们面临以下问题:
:数据传输:一个进程将它的数据发送给另一个进程
:资源共享:多个进程之间需要共享一些资源
:进程控制:有些进程希望完全控制另外一个进程的执行(比如说debug进程),此时控制进程希望拦截另外一个进程的所有陷入和异常,并能够及时知道它的状态改变。
所以我们需要设计一套通信的接口,调用系统调用,设定接口标准
于是进程间通信通过时代的发展,主要形成以下三种形式:
1、管道
2、System V进程间通信
3、POSIX进程间通信(这个属于操作系统网络通信,我们后面只会讲到system V)
其中前面两个可以归纳到本地通信的范畴,就是在同一台主机下,同一个操作系统中的不同进程之间实现通信。
我们可以把这三个具体划分为以下内容:
管道 :
匿名管道pipe
命名管道
System V IPC:
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC:
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
二、什么是管道呢?
管道属于linux系统比较古老的通信方式,最早可以溯源到unix操作系统上。我们把从一个进程连接到另外一个进程上的一个数据流,称为一个管道。、
在linux系统上我们通常通过 | 这个符号来形成两个进程之间的管道:

我们可以看到,我们刚才执行的两个sleep命令变成了两个进程,并且,这两个进程的关系是兄弟进程。(这里的&是让命令后台执行)。他们的本质其实就是进程间通信。
我们以前讲过:我要访问一个文件,需要用到我的文件操作符,找到该下标的指针,找到我要访问的文件的struct file。而一个文件要从磁盘加载到内存,是需要先进行路径解析,确定自己所在分区,找到inode,就找到了自己的属性与内容,从而也能形成struct file被管理起来。而在我们的struct file上,我们可以找到这个结构体对应的inode,以及方法指针集合,还有就是这个文件所独有的内核级文件缓冲区。
那么我想问一下。如果我们的这个进程已经打开了文件访问了,如果此时再创建一个子进程,会出现什么情况呢?
我们之前讲进程创建的时候说过,创建子进程,会把父进程的task_struct复制一份,也就是说,二者的文件描述符表也会被复制。那么父进程打开文件的struct file与文件的inode结构体与这个文件的内核级缓冲区呢?
也会被复制吗?
:只有struct file会被拷贝一份,但是新的struct file所指向的inode与文件缓冲区是一样的指针,也就是说,更靠近文件层面的inode与缓冲区是共享的,不会被复制,而靠近进程方面的struct file等结构体会被子进程拷贝一份。
为什么inode不会被子进程拷贝呢?
:inode 是文件系统层面的数据结构,存储文件的元信息(权限、大小、磁盘块位置等),而不是进程级别的数据。所有进程访问同一个文件时,共享同一个 inode,只是通过各自的 文件描述符来引用它。
为什么缓冲区不会被子进程拷贝呢?
:因为这个内核级缓冲区,是由操作系统提供并管理的,该文件的内核缓冲区由所有打开此文件的进程共用,不属于单个进程。文件缓冲区虽然被进程打开,但是他的整个资源管理属于操作系统,这也就是为什么我们把进程关掉了,文件需要的时候也会自动释放的根本原因。
那么同学们有没有想过一个问题,父子进程,算不算看到了同一份数据呢?
答案肯定是算的。所以父子进程就可以实现我们进程间的通信。
在进程间通信中,当我们将数据放在内核管理的缓冲区而不需要持久化到磁盘时,这种机制本质上创建了一个纯内存的通信通道。操作系统通过专门的数据结构(如环形缓冲区)和特定的系统调用接口(pipe()等)对这种通信方式进行封装和优化,最终形成了一种高效的进程间通信机制------管道。
三、匿名管道
我们有一个专门的系统调用接口来创建管道:

其中 pipefd是一个⽂件描述符数组,其中fd[0]表⽰读端, fd[1]表示写端,这个数组参数是一个输出型参数,我们需要自己创建一个数组然后把这个传进去。
没错,管道其实是一种单向通信的东西。

一般来说,我们都使用带有血缘关系的进程实现管道通信(尤其是父子,因为天生具备看到同一份文件的条件)

管道只能进行单向通信,并且我们必须先打开,随后创建子进程,利用的就是这个子进程继承父进程资源的特性。
为什么我们要父子进程一个关系写,另外一个就要关闭读呢?这样是防止fd泄露以及误操作。
而这种管道就不需要名字,所以不需要在操作系统下带路径(文件路径解析),我们把这种管道,称为匿名管道。
四、匿名管道的使用
我们有以下程序及其相应的Makefile:
cpp
#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
std::cerr<<"pipe error"<<std::endl;
return 1;
}
int id = fork();
//创建进程后,父子进程需要各自关闭一个读写端
//我们这里让子进程写,父进程读
if(id==0)
{
//子进程
::close(fd[0]);
int cnt=0;
while(1)
{
std::string message="hello world,";
message+=std::to_string(getpid());
message+=",";
message+=std::to_string(cnt++);
write(fd[1],message.c_str(),message.size());
sleep(1);
}
exit(0);
}
else if(id>0)
{
//父进程
::close(fd[1]);
char buffer[1024];
while(1)
{
ssize_t s=read(fd[0],buffer,1024);
if(s>0)
{
buffer[s]=0;//将读取到的内容结尾添加'\0',因为字符串以\0结尾时C语言的规定,系统调用write时可没这个规定
//write写入的时候是不需要size+1的,我们就没有写入\0,这里需要手动补上
std::cout<<"parent read:"<<buffer<<std::endl;
}
}
pid_t pid=waitpid(id,nullptr,0);
}
return 0;
}

运行之后我们可以看见,这样就实现了一个简单的,父子进程的管道通信。
我们间隔一秒才写入一次,那么这个时候父进程一直在while循环读,没有内容写入时,父进程就处于堵塞状态。
现象1:管道为空&&管道正常,read会阻塞(read本身是系统调用)
因为我们是共享的一份资源,看到的是同一个缓冲区,那么既然是同一份,会不会出现,你读一半时,我就开始写,或者说我写一半时,你就开始读的情况呢?
这个情况就是数据不一致情况。
面对这个问题,管道内部进行了处理,所以才会出现刚刚父进程阻塞的情况(即系统调用read自己会处理)。
那么我们现在改一下代码,每次只输入一个字符,但是不限制写入时间,而读数据确实要等待十秒钟才开始读,我们看看会出现什么情况:
cpp
int main()
{
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
std::cerr<<"pipe error"<<std::endl;
return 1;
}
int id = fork();
//创建进程后,父子进程需要各自关闭一个读写端
//我们这里让子进程写,父进程读
if(id==0)
{
//子进程
::close(fd[0]);
int total = 0;
int cnt=0;
while(1)
{
std::string message="h\n";
total += ::write(fd[1], message.c_str(), message.size());
cnt++;
std::cout << "total: " << total << std::endl;
}
exit(0);
}
else if(id>0)
{
//父进程
::close(fd[1]);
char buffer[1024];
sleep(10);
while(1)
{
ssize_t s=read(fd[0],buffer,1024);
if(s>0)
{
buffer[s]=0;
std::cout<<"parent read:"<<buffer<<std::endl;
}
}
pid_t pid=waitpid(id,nullptr,0);
}
return 0;
}

我们一次只打印一个字符,居然发现,写入停留在了65536这个数字上。
如果我们把这个数字除以1024,你会发现,这个刚好等于64kb。
也就是说,我们的管道也会有写入上限的。
现象2:管道为满&&管道正常,write会阻塞(write本身是系统调用)
最后我们可以发现,哪怕我们是一个一个写入的,我要读数据,却不一定要一个一个的读。
管道根本不关心写了什么,也不管你写了多少次,只关心要多少个数据。
这个特性:叫做面向字节流
我们再改变一下代码:
使得子进程写入一次数据后,就break退出循环,关闭写端,我们在父进程新增s==0的检测:
cpp
int main()
{
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
std::cerr<<"pipe error"<<std::endl;
return 1;
}
int id = fork();
//创建进程后,父子进程需要各自关闭一个读写端
//我们这里让子进程写,父进程读
if(id==0)
{
//子进程
::close(fd[0]);
int total = 0;
int cnt=0;
while(1)
{
std::string message="h\n";
total += ::write(fd[1], message.c_str(), message.size());
cnt++;
std::cout << "total: " << total << std::endl;
break;//结束循环
}
exit(0);
}
else if(id>0)
{
//父进程
::close(fd[1]);
char buffer[1024];
//sleep(10);
while(1)
{
ssize_t s=read(fd[0],buffer,1024);
if(s>0)
{
buffer[s]=0;
std::cout<<"parent read:"<<buffer<<std::endl;
}
else if(s==0)
{
std::cout<<"child quit"<<std::endl;
}
sleep(1);
}
pid_t pid=waitpid(id,nullptr,0);
}
return 0;
}
我们会发现,当写端关闭时,读段会进入返回值为0的判断语句中:
现象3:写端关闭&&读段正常,读端读到0就表示读到了结尾
那如果反着过来呢?
我们关闭读段只剩下写端:
同学们,操作系统不会做浪费时间浪费空间的事情,而这个情况就是浪费时间与空间的事情,所以操作系统会直接杀掉写端进程。
我们讲进程结束的时候提到,进程结束分为,代码执行完毕,任务完成与不完成,另外一种就是信号异常杀死。
而这个杀进程,就属于异常结束。
我们可以通过waitpid的输出参数status找到子进程退出的信号来验证:
cpp
int main()
{
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
std::cerr<<"pipe error"<<std::endl;
return 1;
}
int id = fork();
//创建进程后,父子进程需要各自关闭一个读写端
//我们这里让子进程写,父进程读
if(id==0)
{
//子进程
::close(fd[0]);
int total = 0;
int cnt=0;
while(1)
{
std::string message="h\n";
total += ::write(fd[1], message.c_str(), message.size());
cnt++;
std::cout << "total: " << total << std::endl;
//继续无限循环break;//结束循环
}
exit(0);
}
else if(id>0)
{
//父进程
::close(fd[1]);
// char buffer[1024];
// //sleep(10);
// while(1)
// {
// ssize_t s=read(fd[0],buffer,1024);
// if(s>0)
// {
// buffer[s]=0;
// std::cout<<"parent read:"<<buffer<<std::endl;
// }
// else if(s==0)
// {
// std::cout<<"child quit"<<std::endl;
// }
// sleep(1);
// }
::close(fd[0]);//关闭读端
int status=0;
pid_t pid=waitpid(id,&status,0);
std::cout<<"child quit signal:"<<((status>>8)&0xFF) << " child quit code:"<<(status&0x7F)<<std::endl;
}
return 0;
}

我们可以看见,进程结束的非常快,所以没有进行子进程的无限循环,且子进程的退出信号为13!!
所以:
现象4:写端正常&&读段关闭,操作系统会杀死写端进程
根据以上实现,我们可以总结出匿名管道的五个特性:
1、面向字节流
2、用来进行具有血缘关系的进程,如父子
3、文件的声明周期随进程结束而结束,管道也是(所有相关进程结束时,内核自动回收管道缓冲区,未读取的数据永久丢失)
4、单向数据通信
5、管道自带同步互斥等保护机制 !(现象3、4)
以上就是我们匿名管道的四大现象五大特性!!!!
总结:
我们今天的文章主要就进程间通信进行了一个开篇,并介绍了一下匿名管道的特性与现象,希望对大家有所帮助。下篇文章我们将会为大家介绍的主要使用匿名管道的例子:进程池 ,有兴趣的可以关注一下。
有疑问的大家可以在评论区或者私信我,谢谢大家!!!