初识Linux · 匿名管道

目录

前言:

匿名管道

理解为什么?

理解是什么?

理解怎么做?


前言:

引入管道之前,我们引入几个问题,进程通信的相关问题。

第一个是进程之间为什么要通信,对于进程间通信来说,进程是具有独立性的,而进程 = 内核数据结构 + 代码数据,进程通信就是因为需要协同,协同的本质是通过数据的的流动来协同的。所以第二个问题,进程如何通信?

进程间通信是通过数据进行通信的,那么也就是说A进程给某些数据,B进程需要接受到这个数据,可是以什么作为数据流通的平台呢?此时管道就出场了,管道可以说是作为信息的载体保证两个进程之间可以通信。对于进程间的通信常见的方式有消息队列,共享内存,信号量,后面介绍。

使用管道通信是直接复用的内核代码,这样不仅可以简单一点,还可以降低成本。

可是说了这么多,管道究竟是什么呢?

两个进程之间想要通信一定要看到同一份资源,或者是同一份内存空间,所以管道实际上就是OS开辟的堆区和栈区之间的那一块共享区的资源。

管道分为匿名管道和有名管道,我们从匿名管道开始介绍,到下篇文章介绍的进程池的小项目,到最后的命名管道,这是管道的介绍顺序,那么直接进入主题吧!


匿名管道

理解为什么?

我们通过这个图简单理解一下为什么?为什么要存在管道?

假设现在有两个进程,A进程将文件输入到了内核级文件缓冲区,然后数据通过OS到了磁盘,B现在通过read方法,读取到了A进程write的数据,这个过程看起来好像没有什么槽点?

实际上,为什么我们不能直接让A进程输入的数据直接给B呢?

那么这个过程是不需要重新设计一个通信端口的,太麻烦了,我们需要一个fork函数 close函数什么的我们就可以实现这样一个功能:

cpp 复制代码
int main()
{
    pid_t id = fork();

    if(id == 0)
    {
        //子进程准备work...

    }
    //父进程准备work...
    
    

    return 0;
}

实现这个功能之前,我们需要了解到管道通信的文件描述符是如何的?

先看一段代码:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        std::cout << "I am a child process!" << std::endl;
    }
    if(id > 0)
    {
        std::cout << "I am a father process!" << std::endl;
    }

    return 0;
}

我们思考一个现象,为什么父子进程默认的都是打印在了1上?

进程打开的时候我们知道是默认打开了三个流,但是我们是否思考过为什么默认打开了吗?前文提及到了历史原因是存在的。所以当我们启动了Linux机器的时候,bash进程已经启动了,此时bash进程的三个流已经打开了,我们后面启动的所有进程都是bash进程的子进程,子进程的三个流也默认打开了,那么如果我们子进程close到0 1 2呢?

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        close(0);
        close(1);
        close(2);
        std::cout << "I am a child process!" << std::endl;

    }
    if(id > 0)
    {
        std::cout << "I am a father process!" << std::endl;
    }

    return 0;
}

现象就是父进程能正常打印,所以关闭了fd实际上不会影响自己的父进程,所以我们利用这个点

可以实现管道单向通信的功能,可是为什么实现的是单向的呢?因为如果是双向的,也就是父进程子进程的数据全部都在管道,读取的时候不经过一些操作肯定是要出错的,所以我们先简单就看看单向的。

为什么这里我们能得出的结论是子进程能继承父进程的文件描述符表,为了实现单向的管道通信我们需要关闭文件描述符。


理解是什么?

我们实现管道的时候,需要用到的函数是pipe:

对于该函数来说,我们使用的时候不使用那个结构体,使用的int pipe(int pipefd[2])即可,结构体暂时先不管,而对于pipefd[2]这是个输出型参数,管道开辟成功之后,fd[1]是管道的写入文件描述符,fd[0]是文件描述符的读端。

而为什么管道叫做匿名管道是因为我们得到该文件描述符甚至不需要文件名,不需要文件路径,所以叫做匿名管道。

这是创建管道最开始的模样,最后需要我们手动的关闭几个文件描述符,至于为什么单向,为什么要关,是否可以不关等问题这里不做讨论,因为上文已经介绍了。

我们今天的重点是放在怎么做上。


理解怎么做?

由前文的是什么为什么,我们知道了基本操作是需要我们创建管道,使用pipe函数,开辟好管道之后,我们需要手动将两个文件描述符关闭,因为子进程会继承父进程的文件描述符表,所以对于父进程来说我们同样需要关闭对应的文件描述符表。

对于0 1是读还是写来说,我们结合形状吧,0是张开了嘴巴,所以是读取,1就是另一个了。

怎么做我们从三个部分开始,第一个是创建管道,第二个是子进程写入数据,第三个是父进程读取数据。

如果成功创建了管道,返回的就是0,如果不等于0我们就可以cerr了。

cpp 复制代码
    int pipefd[2];
    int n = pipe(pipefd);
    if(n)
    {
        std::cerr << "errno:" << errno << ":"\
        << "errstring is :" << strerror(errno) << std::endl; 
    }
    std::cout << "pipefd[0]:" << pipefd[0] << " pipefd[1]:" << pipefd[1] << std::endl;
    sleep(1);

创建管道部分,如果返回值不是0的话也就是创建失败了,所以我们打印出来具体的错误信息,使用到的是前面学习到的errno和strerror,一个是错误码,一个是错误码对应的字符串,然后打印出来0 1对应的文件描述符,就算是管道创建成功了。

现在就是子进程的写入数据部分,我们写对应的代码之前,简单思考一下大体的写入思路是什么样的?

首先是创建子进程,创建之后,关闭不需要的fd,然后子进程开始work,对应的工作做完之后,关闭掉对应的文件描述符,然后子进程退出,父进程回收即可,这个过程文件描述符肯定都是要关闭的,因为管道这个内存是一个引用计数的空间,所以如果不关闭,导致的结果就是内存泄漏,毕竟是空间都没有释放。

整体代码为:

cpp 复制代码
    //2.创建子进程
    pid_t id = fork();
    if(id == 0)
    {
        //子进程开始准备工作
        std::cout << "子进程准备开始写入数据了..." << std::endl;
        sleep(1);
        close(pipefd[0]);
        
        SubProcessWrite(pipefd[1]);
        close(pipefd[1]);
        exit(0);
    } 

然后就是子进程的subProcessWrite函数了:

cpp 复制代码
std::string getOtherMessage()
{
    //消息次数
    static int cnt = 0;
    std::string message = std::to_string(cnt);
    cnt++;
    //子进程的pid
    pid_t self_id  = getpid();
    std::string stringpid = std::to_string(self_id);

    std::string info = "messageid: ";
    message += message;
    message += " My pid is :";
    message += stringpid;

    return message;
}
void SubProcessWrite(int wfd)
{
    int pipesize = 0;
    std::string message = "Father,I am your son process! ";
    char charactor = 'A';
    while(true)
    {
        std::cout << "+++++++++++++++++++++++++++++++++++++++++++++++++" << std::endl;
        //得到数据
        std::string info = message + getOtherMessage();
        //开始写入数据
        write(wfd,info.c_str(),info.size());
        std::cerr << info << std::endl;
    

    }
    std::cout  << "child quit......" << std::endl;
}

写入数据的同时通过cerr打印到显示器上,并且写入的时候我们通过函数GetOtherMessage获取到子进程的Pid和写入了多少次的字符串。

这是子进程的写入函数部分。

子进程写入完毕之后是父进程开始读取数据:

cpp 复制代码
void ProcessFatherRead(int rfd)
{
    char inbuffer[SIZE];
    while(true)
    {
        //休眠一会儿开始读取
        sleep(2);
        std::cout << "---------------------------------------------------" << std::endl;
        sleep(500);
        ssize_t n = read(rfd,inbuffer,sizeof(inbuffer) - 1);
        if(n > 0)
        {
            inbuffer[n] = 0;// == '\0'
            std::cout << inbuffer << std::endl;
        }
        else if(n == 0) //如果n == 0代表读到了文件结尾
        {
            std::cout << "client quit, father get return val: " << n << " father quit tool" << std::endl;
            break; 
        }
        else if(n > 0)
        {
            std::cout << "Read error!" << std::endl;
            break;
        }
    }
}

父进程使用函数read,这里不妨温习一下read函数:

返回值是ssize_t ,读取count个字符,读取到buf数组里面。

如果返回值是0,代表读取到了文件的末尾,如果返回的是-1代表read出错了,> 0的代表的是success。

然后是主函数的父进程开始读取数据部分函数,大体思路仍然先关闭掉不需要的文件描述符,读取完之后,需要等待子进程退出,为了收集子进程的退出信息,并且我们可以打印出来:

cpp 复制代码
    //3.父进程开始读取 
    std::cout << "父进程关闭不需要的fd, 准备接收消息了..." << std::endl;
    sleep(1);
    close(pipefd[1]);
    ProcessFatherRead(pipefd[0]);
    std::cout << "5s,father close fd" << std::endl;
    sleep(5);
    close(pipefd[0]);
    //4.父进程开始等待子进程
    int status = 0;
    pid_t rid = waitpid(id,&status,0);
    if(rid > 0)
    {
        std::cout << "wait child process done, exit sig: " << (status&0x7f) << std::endl;
        std::cout << "wait child process done, exit code(ign): " << ((status>>8)&0xff) << std::endl;
    }

目前看来是正常写入,但是父进程是否读取到了我们并不知道,所以我们打算让子进程write到一定程度的时候break:

cpp 复制代码
    while(true)
    {
        std::cout << "+++++++++++++++++++++++++++++++++++++++++++++++++" << std::endl;
        //得到数据
        std::string info = message + getOtherMessage();
        //开始写入数据
        write(wfd,info.c_str(),info.size());
        std::cerr << info << std::endl;
        
        sleep(1);

        write(wfd,&charactor,1);
        std::cout << "pipesize: " << ++pipesize << " write charactor is: " << charactor++ << std::endl;
        if(charactor == 'H') break;

    }

此时,子进程退出之后,子进程的状态成功变成了僵尸状态,我们将父进程的sleep时间缩短,准备让父进程进行回收子进程。

匿名管道粗略的到这里吧,,后面等着二刷。


感谢阅读!

相关推荐
梅见十柒9 分钟前
wsl2中kali linux下的docker使用教程(教程总结)
linux·经验分享·docker·云原生
Koi慢热12 分钟前
路由基础(全)
linux·网络·网络协议·安全
传而习乎22 分钟前
Linux:CentOS 7 解压 7zip 压缩的文件
linux·运维·centos
soulteary24 分钟前
突破内存限制:Mac Mini M2 服务器化实践指南
运维·服务器·redis·macos·arm·pika
我们的五年32 分钟前
【Linux课程学习】:进程程序替换,execl,execv,execlp,execvp,execve,execle,execvpe函数
linux·c++·学习
爱吃青椒不爱吃西红柿‍️1 小时前
华为ASP与CSP是什么?
服务器·前端·数据库
IT果果日记1 小时前
ubuntu 安装 conda
linux·ubuntu·conda
Python私教1 小时前
ubuntu搭建k8s环境详细教程
linux·ubuntu·kubernetes
羑悻的小杀马特1 小时前
环境变量简介
linux
小陈phd2 小时前
Vscode LinuxC++环境配置
linux·c++·vscode