Linux——进程通信

目录

一.进程通信

1.为什么会有进程间的通信呢?

2.所以进程通信的特点为:

3.进程通信的三种主要方式:

 3.1先来说一说System V进程间通信方式:

3.2接下来是POSIX进程间通信方式:

3.3管道

四.匿名管道

        4.1管道的来源

        4.2管道的实用性。

        4.3匿名管道实现图解:

         如上图所示:

4.4匿名管道的使用:

4.5案例:

情况1:子进程写入速度过慢,父进程读取速度快时:

情况2:子进程写入速度过快,而父进程读取速度过慢时:

运行结果:

情况3:当子进程写了一次就把写端关闭了,父进程仍在在循环中读,会发生什么?

运行结果: 

优化代码:

优化运行结果:

情况4:当父进程关闭了读端,子进程仍在循环中写数据,又会发生什么呢?

运行结果:

五.总结:


一.进程通信

        在生活中我们见到过很多有关进程通信的例子,例如:QQ,微信,钉钉等聊天工具,多人联网游戏......,聊天工具的进程通信就是双方拿着各自的手机打开QQ、微信开始互相聊天,共享、传输数据;打游戏也是在一个游戏公司搭建好的平台中,通过网络将各自电脑端的进程连接在一块就可以和兄弟一起玩。

1.为什么会有进程间的通信呢?


        原因在于:我们需要多个进程相互协同进行某件事的合作!

例如:在Linux种使用指令:cat file | grep "hello"

        这条语句代表着:cat(将文件读到内存后打印该文件内容到屏幕);

        而grep是查询"hello"关键字;

        而 “ | ”符号表示管道,作用是将左右两个指令相互连接,进程1在cat指令后将文件内容打印到管道中,之后由进程2接手该文件内容,在此基础上过滤文件内容中有关"hello"关键字,最终将文件内容携带hello的语句统一打印到屏幕中。

        以上这两个进程,一个进程cat文件,一个进程grep搜索关键字,它们就是在相互协同合作进行通信!

2.所以进程通信的特点为:

1.数据传输:一个进程可以将它的数据发送给另一个进程;

2.资源共享:多个进程之间可以共享同样的资源;

3.通知事件:一个进程可以向另一个进程发送消息,通知他们发生了某种事件(如进程终止时需要通知父进程)。

4.进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能拦截获取另一个进程的所有异常,并能够及时知道它的状态改变。

        

3.进程通信的三种主要方式:

        我们知道进程是具有独立性的,想让多个进程相互通信就需要付出巨大的成本。在上个世纪80年代,科学家为了研制进程间的相互通信,发明了三种进程通信的方式:

1.管道

2.System V进程间通信

3.POSIX 进程间通信 

 3.1先来说一说System V进程间通信方式:

        System V通信方式是由Unix开发的,也是相当陈旧的一套标准。它的通信特点是聚焦在本地(本主机)中实现多个进程间的相互通信,在几十年前这种方式应用很广泛,但是现在它几乎已经被时代给淘汰了,因为有了更好的一种通信方式POSIX通信方式!System V通信方式中划分为了三种形式的通信:1.共享内存、2.消息队列、3.信号量。其中最主要的是共享内存,该种方式的实现我之后会讲到。

3.2接下来是POSIX进程间通信方式:

        POSIX通信方式是由IEEE 和ISO/IEC 开发的一簇标准。是基于现有的UNIX 实践和经验,描述了操作系统的调用服务接口,用于保证编制的应用程序可以在源代码一级上在多种操作系统上移植运行。所以它的通信特点是可跨越主机式的多个进程之间的通信,这也是System V方式逐渐被淘汰的原因。System V的实用性没用POSIX强。

3.3管道

        管道想必大家生活中就有,天然气,饮用水就是通过管道进行输送的;城市下雨了,降雨量过大会导致出现洪水等灾害,而下水管道的出现便是为此做准备的。管道的作用就是传输资源的。管道在计算机中也是一种通信方式,分为匿名管道和命名管道,下面我将用代码为大家实现匿名管道。

四.匿名管道

   

        4.1管道的来源

        在前段时间,我讲过进程的底层,进程是磁盘的文件被加载到内存的称呼。进程也有其相应的结构体task_struct,该结构体内存放着进程的所有属性,而属性之一:struct file*指针,该指针指向文件描述符表,它的底层也是一个结构体struct file(该知识点参考我之前写过的博客:文件基础IO的理解1里面有说到):

 此时创建子进程的话,子进程会继承父进程的大多属性,父子进程会连接同一个文件,但是由于写实拷贝的缘故,父子进程各自的行为并不会互相影响对方。

         有了子进程之后,便有了迈出通信的第一步,此时进程数量大于1。之后便是需要建立通信了。通过之前对文件基础IO的学习,我们可以通过将内容写入文件的形式实现通信,即一个进程往文件写内容,一个进程读取内容。那么便可使用write、read系统调用函数实现。这就相当于是一种进程间的通信了。

        但是这种思路的缺点在于从进程A想要从内存中写入数据经过内存传输到外设(磁盘),速度很慢,效率很低!读也是一样的,进程B想要读取文件需要大老远跑到磁盘种读取。(注:这是对于上个世纪80年代来讲,不要带入到现在。)若是写入数据量很少的话还可以,但是若是写入几百兆,几千兆的内容,以计算机的传输速度怕是要写到明年了!所以这种方式压根不能用。

       

        4.2管道的实用性。

        于是科学家们创建出了一种名为管道的文件,使用管道文件的好处在于:管道文件是存储在内存中的,是内存级文件。两个进程想要读/写不需要大老远跑到磁盘中。而内存的传输速度相较于外设来说可是快了很多很多了。于是管道的通信方式诞生了。

        那么如何让两个进程都看到管道文件呢?那么管道文件的前提一定得是公共资源!既不能属于进程A,也不能属于进程B。使用fork()方式创建的子进程,能够继承父进程的属性,所以这份公共资源一定要在fork函数前建立出来,这样它们就能各自拥有这个管道的使用权了。

        所以以上这两段话透露出通信的两个本质:

1.多个进程之间想要通信,就一定需要传输数据,传输数据就需要缓冲区,而这个缓冲区只能由OS操作系统直接或者间接去提供一段内存空间。

2.让多个进程同时看到一份公共的资源,这样才能互相访问通信。

注:父子进程实现的进程间通信的管道方式称为——匿名管道

        4.3匿名管道实现图解:

         如上图所示:

        1、匿名管道的数据流向是固定的,即管道左右有两个端口,一端用于读,另一端用于写。相当于让管道是半双工,单向的数据传输方式,即一方在写的时候,另一方不能读;另一方读的时候,这一方不能写。

        2、那为什么要采用单向的传输方式呢?

        因为单向的管道更好进行分辩,若是父子进程都可以进行任意的对管道进行读写,那么要考虑的情况、接口就很多,变得更复杂了。

        但我们可以设置两个管道,一个管道只允许父进程写子进程读,另一个管道只允许父进程读子进程写,这样便可以实现全双工通信。

        

4.4匿名管道的使用:

系统调用函数:pipe();

        pipe函数用于创建一个管道文件,以实现进程间的通信,pipe函数参数fds,是一个大小为2的整型数组指针,该函数若创建成功管道则返回0,并将一对打开的读写描述符值填入fds参数指向的数组中;创建失败则返回-1并设置errno。

        通过pipe函数创建的这两个文件描述符fds[0]和fds[1]分别构成管道的两端,往fds[1]写入的数据可以从fds[0]中读出,并且fds[1]一端只能进行写操作。我们可以将fds[0]这一端比作一个嘴巴,嘴巴用于说——读取;fds[1]这一端比作一支钢笔,钢笔用于写。

 

运行结果: 

以下为使用匿名管道实现两个父子进程间通信的案例代码,重点!!!!

4.5案例:

#include<iostream>
#include<cassert>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<wait.h>
#include<cstdio>
#include<cstdlib>
#include<cstring>
using namespace std;

int main(){
    int fds[2];            
    int n=pipe(fds);       //创建管道
    assert(n==0);          //断言——确保创建管道成功

    pid_t id=fork();        //创建子进程
    assert(id>=0);          //断言——确保创建子进程成功


    if(id==0){            //子进程

        close(fds[0]);    //关闭读端

        char buffer[1024];    //创建缓冲区

        const char* s="我是子进程,我正在给父进程发消息!";
        int cnt=0;
        while(true){    //采用循环的方式不停的往管道中写内容
            cnt++;
            snprintf(buffer,sizeof(buffer),"%s: 子pid:%d,cnt:[%d] ",s,getpid(),cnt);
            write(fds[1],buffer,strlen(buffer));
            sleep(1);    //每秒写一次
        }

        cout<<"子进程即将退出"<<endl;
        close(fds[1]);
        exit(0);
    }

    //父进程
    close(fds[1]);        //开始先关闭写端
    int status=0;
    int cnt=0;
    char buffer[1024];      

    while(true){        //以循环的方式不停的从管道中读取内容
        sleep(1);
        ssize_t r=read(fds[0],buffer,sizeof(buffer)-1);
        if(r>0){
            buffer[r]=0;
            cout<<"父进程:Get Message# "<<buffer<<"# over ,pid:"<<getpid()<<" "<<++cnt<<endl;        //父进程将管道中读到的子进程发送的信息打印出来
        }
        cout<<"aaaaa"<<endl;
    }

     close(fds[0]);
    int ret=waitpid(id,&status,0);    //等待子进程退出
    assert(ret>0);
    cout <<"pid->"<< ret << " : "<< (status & 0x7F) << endl;    //查看子进程退出信息
    cout<<"父进程退出"<<endl;

    return 0;
}

该案例的默认情况就是:子进程往管道中写内容,父进程往管道中读取内容。

        子进程首先将想要写入的内容通过snprintf函数写入到相当于C缓冲区中, 然后通过write将缓冲区的内容写入到管道中;利用死循环的方式不停的写入;

        父进程是通过将子进程写入管道的内容进行read函数读取,读取到C缓冲区,然后将内容显示到大屏幕中,也是利用死循环的方式不停的读取。

情况1:子进程写入速度过慢,父进程读取速度快时:

        该种情况意味着子进程每3秒写入一次管道数据,而父进程每秒都在进行读取。当父进程从读取端读完子进程写入管道的一行数据后,子进程进入睡眠,而剩下的两秒钟,父进程在干什么?

        其实通过结果数据显示可以知道:父进程在等待着子进程下一次数据的写入,只要子进程不写,那么父进程就只能阻塞的等待着!等到子进程又开始写后,父进程从阻塞等待状态中又回到了运行状态,继续读取。

情况2:子进程写入速度过快,而父进程读取速度过慢时:

 

        此时,父子进程的时间发生了互换,这次轮到父进程每3秒读一次,子进程每秒都在写。当父进程读完子进程写入管道的一行数据后,进入了睡眠。

运行结果:

        从结果中可知:父进程进入休眠的时候,子进程往管道中写了3秒3行的数据,父进程开始读的时候,便会一下就读到子进程发到管道的三行数据。 

         

情况3:当子进程写了一次就把写端关闭了,父进程仍在在循环中读,会发生什么?

 ​​​​​​​

 

        如上图可知:子进程执行了一次循环就退出了,然后关闭了写端;而父进程仍在循环中每秒每秒的读取着管道的数据。         

 

运行结果: 

        那么父进程会一直从管道中读取数据,管道为空后,仍然读取,只不过空也算是成功读取到了,然后继续下一次的读取。 

优化代码:

        将父进程中read函数的返回值再设立一种情况,read函数的返回值为0则表示已经读到了管道数据的结尾,并且已经没有数据可读了。所以再读已经没用了,直接跳出循环即可。

优化运行结果:

 

情况4:当父进程关闭了读端,子进程仍在循环中写数据,又会发生什么呢?

运行结果:

        父进程在第一次循环中读取到了子进程输入管道的数据后就跳出循环,关闭了读端,那么此时子进程再写就是非法写入了,因为写了也没用,已经没人去看了。所以子进程被通知由于异常被迫下线了!

        这就好比两个人在微信中聊天,A将B删掉了,终结掉了与B通信的方式,A再写啥消息B也永远都收不到了。 

 

        而结尾父进程waitpid函数返回值获取到了子进程异常退出的原因:13。13代表的是该进程由于发生异常被13号信号终止,而这个13号信号意为:SIGPIPE

         当往一个写端关闭的管道或socket连接中连续写入数据时,会引发SIGPIPE信号,引发SIGPIPE信号的写操作将设置errno为EPIPE。

 

五.总结:

 匿名管道的特征:

1.管道的生命周期——进程
2.管道可用用来具有血缘关系(如祖孙,兄弟进程》之间的进程通信,但用的最多的是父子进程
3.管道是面向字节流的
4.管道是半双工的进程通信(在某一时刻,一个进程只能向另一个进程发送信息)

5.互斥与同步机制。一一对共享资源进行保护的方案

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橙予清的zzz~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值