Linux进程间通信——管道(上)

目录

前文

一,进程间通信介绍

二,什么是管道?

三,管道的基本原理

3.1 匿名管道

3.2 管道基本原理

四,样例代码

五,管道的读写规则

六,管道的特点

总结


前文

本文主要是讲解一下进程间通信中管道中的主要内容,如基本原理,样例代码以及特点等

一,进程间通信介绍

首先进程间通信指的是不同进程之间传播或交换信息,众所周知,在Linux中每个进程都是相互独立的,这也就导致进程与进程之间独自沟通困难,因此进程与进程想要沟通交流就需要第三方的帮助,也可以看作是进程间沟通的介质,在进程间通信的发展历程中,介质主要以下面三个为主:管道,System V进程间通信,POSIX进程间通信

而进程间通信的目的主要有以下几个

数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

而本文主要是讲解一下进程间通信中的管道。

二,什么是管道?

  • 管道是Unix中最古老的进程间通信的形式。

  • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

     

    如上,who | wc -l指令就是对管道的一个简单运用,who进程所执行的内容传入管道中,然后wc进程再读取管道中的内容然后完成自身功能,这样两个进程就在不破坏独立性的情况下通过管道看到同一份资源(也就是who指令显示的内容),从而完成进程间通信。

三,匿名管道的基本原理

首先管道主要分为匿名管道和命名管道,而在管道的原理讲解及其样例代码我们都用匿名管道来完成

3.1 匿名管道

首先匿名管道指的是用pipe函数创建的无名管道,其所需头文件及函数原型如下

#include <unistd.h>
int pipe(int fd[2]);

参数

fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

因此从上面文件描述符数组以及linux下一切皆文件的原则我们可以推出管道也是文件

3.2 匿名的管道基本原理

管道的实现主要是依靠fork来共享管道,其主要流程如下(下面都是站在文件描述符的角度来理解管道):

 1.父进程创建管道

 如上所示,父进程先用pipe函数创建一个管道,fd[0](对应文件描述符表中的3)对应读管道,fd[1](对应文件描述符表中的4)对应写管道

 2.父进程fork出子进程

 如上所示,父进程fork出的子进程继承了父进程的文件描述符表(注意这里是浅拷贝,所以子进程并不会新建管道),因此子进程的fd[0],fd[1]所指向的内容和父进程一致。

3.父子进程各自关闭不需要的fd(这里我们以父进程写入,子进程读取为例,在使用中以使用场景为主,不必拘泥与父写子读)

 如上图,父进程写入,则将fd[0]关闭,子进程读取,则将fd[1]关闭,自此父进程便可以通过管道看到同一份资源,而后也就可以在不破坏进程独立性的情况下进行进程通信

因此其实上面的三步可以看作都是为了让父子进程可以看到同一份资源从而进行通信的通信方案

四,样例代码

匿名管道的基本原理已经讲解完毕,接下来我们按照上面的原理完成样例代码,通过样例代码进行相关实验,从而进一步探索管道的性质特点,样例代码如下所示

#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <string>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
using namespace std;

int main()
{
    //1.创建管道
    int mypipe[2]={0};
    int n=pipe(mypipe);
    if(n<0)//创建失败报错返回
    {
        cout<<"pipe error: "<<errno<<":"<<strerror(errno)<<endl;
        return 1;
    }

    cout<<"mypipe[0]: "<<mypipe[0]<<endl;//[0]-0-张嘴读
    cout<<"mypipe[1]: "<<mypipe[1]<<endl;//[1]-1-用笔写

    //2.创建子进程
    pid_t fd=fork();
    assert(fd!=-1);//判断创建是否成功

    //子进程
    if(fd==0)
    {
        //3.父进程写入,子进程读取
        close(mypipe[1]);

        //4.通信
        char buffer[1024];
        while(true)
        {
            int n=read(mypipe[0],buffer,sizeof(buffer)-1);
            
            if(n>0)
            {
                buffer[n]='\0';
                cout<<"我是子进程,父进程给我的讯息是: "<<buffer<<endl;
            }
            
        }

        //退出
        close(mypipe[0]);
        exit(0);

    }

    //父进程
    //3.父进程写入,子进程读取
    close(mypipe[0]);
    
    //4.通信
    const string str=("hello,我是父进程");
    int cur=1;
    char buffer[1024];
    while(true)
    {
        sleep(1);//写入等待一秒

        snprintf(buffer,sizeof(buffer),"%s,计数器:%d,我的pid:%d",str.c_str(),cur++,getpid());
        write(mypipe[1],buffer,strlen(buffer));
        
    }


    close(mypipe[1]);
    return 0;
}

 运行结果如下

 

五,管道的读写规则

1.当没有数据可读时,如果对方不发我们只能等待

 如上图,当将写入设置成五秒一写时,读取方在读取完就会停止执行等数据写入

 2.当管道满的时候,就无法继续写入

 如上图所示,在写入方我们持续写入字母a,并且用cur记录写入的a的个数,然后读取方10s读取一次,我们发现在第一次读取前,cur为65536,也就是说应该写入65536个字母a,但实际上我们发现管道满的时候就不在写入了

3.如果关闭了写端,管道数据读取完后,再次读取read就会返回0,说明读取结束

 如上图所示,我们在停止写入后,管道再次读取,read返回值为0,读取结束

4.写端一直写,读端已经关闭,此时写端的行为是无意义的,os不会允许无意义的行为,低效率,浪费资源的事情,因此会杀死写端,会通过信号来终止。

 如图当读端也就是子进程停止读取时,写端也就是父进程直接被os杀死

5.当要写入的数据量不大于管道容量时,linux将保证写入的原子性:当要写入的数据量大于管道容量时,linux将不再保证写入的原子性

也就是说,当写入的数据量小于管道容量,linux可以保证其数据的正确性,否则数据有可能会失真

六,管道的特点

从上面的内容我们可以的的值管道有如下特点

1.单向通信,也就是其数据流只能单向流动

2.管道的本质就是文件,而且又因为fd(文件操作符)的生命周期随进程结束而结束,因此管道的生命周期也随进程结束而结束

3.具有血缘关系的进程都可以用管道进行进程通信,例如爷孙进程,父子进程,兄弟进程.这是因为,他们都有共同祖先,只要共同祖先调用pipe创建管道,则其后代互相都可以进行管道通信

4.管道通信双方的读写次数没有强相关(字节流),也就是说互相不影响,例如上面的子进程想多久读一次都可以,不受管道另一方的限制

5.具有一定协同能力,让读端和写端都按照一定的规则进行通信——自带同步机制(读写规则)

总结

如上便是本文所有内容如果对你有所收货,希望来个三连关注

猜你喜欢

转载自blog.csdn.net/zcxmjw/article/details/131367115