前言
我们在学习进程管理,进程替换时,都强调了
进程的独立性
,那进程间通信是什么?这好像和进程的独立性相矛盾
吧?
那么今天,我们就来学习进程间通信
,和第一种通信方式 –管道
文章目录
- 前言
- 一. 进程间通信
- 二. 管道
- 三. 匿名管道的使用
- 1. pipe的使用
- 2. 准备通信
- 3. 匿名管道的特点和场景
- 结束语
一. 进程间通信
进程间通信,并没有破坏进程的独立性这一特点,这点我们在
管道
讲解
而进程通信的目的有如下几个:
数据传输
:一个进程需要将它的数据发送给另一个进程
资源共享
:多个进程之间共享同样的资源
通知事件
:一个进程需要向另一个或一组进程发送消息,通知
它(它们)发生了某种事件
(如进程终止时要通知父进程)进程控制
:有些进程希望完全控制另一个进程的执行
(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常
,并能够及时找到它的状态改变。
进程间通信的
三种常用方法
管道
匿名管道pipe
命名管道
System V进程间通信
System V消息队列
System V共享内存
System V信号量
POSIX进程间通信
消息队列
共享内存
信息号
互斥量
条件变量
读写锁
进程依旧具有独立性
,一个进程不可能可以直接从另一个进程的堆,栈区获取数据,那么进程间通信是怎么实现的呢?
要让两个不同的进程,进行通信,前提条件是:先让不同进程,看到同一份“资源”
而管道就是,看到同一份“资源”的实现方式之一
二. 管道
管道是Unix中最古老的进程间通信形式
我们把一个进程连接到另一个进程的一个数据流称为一个“管道”
我们在Linux指令中使用的|
就是管道
who | wc -l
,who
可以查看有几个用户登录服务器,wc
可以统计有几行文本行
这里就是who创建进程,先显示有几个用户,然后将数据传输给wc,wc处理完再输出结果
我们再具象的理解管道
当我们创建一个进程,OS会创建
task_struct
维护,管理进程。而该结构体里有一个strust file_struct*
,指向一个结构体,该结构体是管理文件的,里面存储了打开文件的文件描述符
,默认0,1,2分别是标准输出,标准输入,标准错误
而Linux下一切皆文件,管道也是文件
,但是是OS为了实现进程间通信,而临时创建的一个内存文件
。默认是空闲文件描述符的后两个
。如下图
而创建子进程,需要重新创建task_struct ,但是struct files_struct的内容是拷贝父进程的,但也仅是拷贝,
拷贝一份文件描述符和文件的映射关系,不会重新创建新文件
fork创建子进程后,只会赋值进程相关的数据结构对象,不会复制父进程曾经打开的文件对象!就像浅拷贝一样
这种管道,只支持单向通信
,叫做匿名管道
。因为文件只有一个缓冲区
,所以读写同时只能进行一项
所以我们需要手动确定数据流向,关闭不需要的文件描述符fd
三. 匿名管道的使用
接下来,我们就来简单模拟一下进程间通信。
用匿名管道实现进程通信,需要父进程创建匿名管道
,创建的方法是使用pipe函数
该函数的参数较为特殊:是输出型参数
。类似waitpid的status。
我们传过去一个2大小的整型数组,pipe函数内部会将创建的管道的读和写两个文件描述符写入这个数组,返回给我们
返回值:成功调用,返回0;错误,返回-1,并设置错误码
。
1. pipe的使用
我们首先使用一下pipe函数,看一下其效果
#include<iostream>
#include<cerrno>
#include<unistd.h>
#include<string.h>
using std::cout;
using std::endl;
int main()
{
//创建管道所需传参的数组
int pipefd[2]={0};
//1.创建管道
int n=pipe(pipefd);
if(n<0)
{
//如果返回值小于0,即-1,还会设置错误码
//我们再把错误码对应的错误信息,打印一下
cout<<"pipe error,"<<errno<<":"<<strerror(errno)<<endl;
return 1;
}
cout<<"pipefd[0]:"<<pipefd[0]<<endl;
cout<<"pipefd[1]:"<<pipefd[1]<<endl;
return 0;
}
正如前面所说,管道的两个文件描述符,默认使用当前空闲的前两个文件描述符
。
管道创建的两个文件描述符,默认第一个是读,第二个是写
记忆法
pipe[0]的是读端,0 -> 嘴巴 -> 读
pipe[1]的是写端,1 -> 笔 -> 写
2. 准备通信
我们知道了管道的创建方法,接下来就可以准备实现父子进程通信了。
我们上面说到,进程间通信的前提条件就是:让不同的进程,看到同一份资源。
管道就可以是这份资源,我们模拟父子进程通信,让子进程往管道里写数据,然后父进程接收数据
代码如下:
#include<iostream>
#include<cerrno>
#include<cassert>
#include<unistd.h>
#include<string.h>
#include<string>
#include<sys/types.h>
using std::cout;
using std::endl;
int main()
{
//创建管道所需传参的数组
int pipefd[2]={0};
//1.创建管道
int n=pipe(pipefd);
if(n<0)
{
//如果返回值小于0,即-1,还会设置错误码
//我们再把错误码对应的错误信息,打印一下
cout<<"pipe error,"<<errno<<":"<<strerror(errno)<<endl;
return 1;
}
cout<<"pipefd[0]:"<<pipefd[0]<<endl;//读端
cout<<"pipefd[1]:"<<pipefd[1]<<endl;//写端
//2.创建子进程
pid_t id = fork();
//获取错误。意料之外,使用if;意料之中,用assert
//此处应该使用if,但为了简单一些,使用assert
assert(id!=-1);
if(id==0)
{
//子进程
//3.关闭不需要的fd
close(pipefd[0]);//关闭子进程的读端
//4.开始通信
const std::string namestr="hello ,我是子进程";
int cnt=1;//计数器
char buffer[1024];//write的字符数组
while(true)
{
//将内容写入buffer字符串
snprintf(buffer,sizeof(buffer)-1,"%s,计数器:%d,我的PID:%d\n",namestr.c_str(),cnt++,getpid());
//将内容写入管道
write(pipefd[1],buffer,strlen(buffer));
sleep(1);
}
//关闭子进程的写端,再exit退出
close(pipefd[1]);
exit(0);
}
//父进程
//3.关闭不需要的fd
//让父进程进行读取
close(pipefd[1]);//关闭父进程的写端
//4.开始通信
char buffer[1024];
while(true)
{
//读取的大小,至少要留一个位置写入\0
int n = read(pipefd[0],buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n]='\0';
cout<<"我是父进程, child give me a message: "<<buffer<<endl;
}
}
//关闭父进程的读端,结束进程
close(pipefd[0]);
return 0;
}
这样我们就实现了父子进程的通信。
3. 匿名管道的特点和场景
- 当我们将子进程写入数据的sleep(1)注释掉。程序运行的结果就变得不一样了。
我们看到,子进程写了很多次,父进程才进行了一次读取。
我们再改变一下,在子进程写入后的sleep改成sleep(5)
在父进程读取数据时,我们10个字节,10个字节的读取
程序运行结果就又变了
这两个实验验证出了这样一个结论:
在匿名管道的通信中,写入的次数,和读取的次数,不是严格匹配的
,读写次数的多少没有强相关 — 因为缓冲区的读写都是以字节
为单位 —字节流
- 接下来,我们再做个实验:
我们让父进程正常的读取数据,但子进程每次写入间隔10秒。
观察运行,我们发现,我们让子进程写入变慢,但是父进程的读取也变慢了。这是怎么回事呢?
首先,管道文件的数据类似队列,写入一次是入队列,读取是出队列,
只要读取,数据就没了。
所以,子进程写入变慢,父进程没有东西可读,就进入了阻塞状态。
- 我们再让父进程的读取变慢,子进程正常写入
我们发现,管道进行了
65536(从0开始)次
写入之后就没有写入了。等父进程时间一到,读取了很多的X
而每次写入,我们都只写入1个字节。所以管道最多可以写入65536字节
,也就是2的16次方
,64kb
,16个数据块。
- 如果我们将子进程的写端关闭,父进程的读端继续,会发生什么呢?
我们让子进程写入一次数据就关闭写端,父进程仍然一直读取数据,但要对read的返回值多作一个判断
运行结果如下
当我们
关闭子进程的写端,父进程再读取就会读到文件尾,就会返回0
,父进程就终止了。
- 如果我们关闭父进程的读端,结果又会是这样呢?
直接说结论:
当一个管道只有写端,没有读端,代表着无论怎么写,都不会有人获取,这是没有意义
的事,而操作系统不会维护无意义的,低效率的,或者浪费资源的事情
。OS会杀死一直在写入的这个进程!通过13号信号 SIGPIPE,杀死进程
接下来,我们总结一下匿名管道的特点和场景
特点
单向通信
,半双工
的一种情况,双方同时只能一方写入
因为匿名管道只有一个缓冲区
,同时只能有一方进行读写。
全双工
,双方可以同时写入- 匿名管道的本质是
文件
,因为fd的生命周期随进程
,所以管道的生命周期也是随进程的。
- 匿名管道通信,通常用来进行
具有“血缘关系”的进程之间的进程通信
,因为匿名管道是内存级文件
,所以只有创建的子进程可以获得父进程创建的匿名管道,所以常用于父子间通信 -- pipe 打开匿名管道。
4.在匿名管道的通信中,写入的次数,和读取的次数,不是严格匹配的
,读写次数的多少没有强相关 — 因为缓冲区的读写都是以字节
为单位 —字节流
- 具有一定的
协同能力
,让read和write能够按照一定的步骤进行通信 —自带同步机制
场景
- 如果我们read,读端,读取完毕了所有的管道数据,如果对方不发,读端就会
堵塞
- 如果我们write,写端,将
管道写满了
,那就暂时
不能继续写了,需要读端读取数据
,被读取的数据从管道中去除,就可以继续写入- 如果我们
关闭了写端
,读取完毕管道数据
,再读,就会读到文件尾
,read就会返回0
。- 写端一直写,
读端关闭
,操作系统会发送13号 SIGPIPE信号
杀死写端的进程。
当单次写入的数据量不大于PIPE_BUF时,LInux将保证写入的原子性
反之大于PIPE_BUF时,Linux不再保证写入的原子性。
目前的理解是,保证写入时不会被读取
结束语
本篇博客的内容到此就结束了。
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。