匿名管道、命名管道--Linux

🚩管道的理解

我们在生活中对管道并不陌生,水管、煤气管道……所有的这些管道都是用来运输某种东西,水管将水从一个地方送到另一个地方,煤气管道将煤气从一个地方送到另一个地方,完成物资的运输。在不同的进程之间,信息就是所谓的物资,进程间的信息通讯可以通过管道来实现(并不一定得是管道,还有其他方式)。举个例子,父子进程由于写时拷贝的存在,是没法直接进行信息交流的,只能借助一些手段来辅助完成。

首先,管道分为匿名管道与命名管道,管道的种类不同,作用的场景也会有所不同。但由于我是刚刚接触管道,具体应用场景使用的还不算多,因此这里就简单介绍一下匿名管道与命名管道的基本功能与使用,后面学习到更多的知识会持续更新滴✌。

🚩匿名管道

匿名管道,字面意思就是没有名字的管道,主要是在有血缘关系的进程之间(父子进程)起作用。匿名管道的是如何做到两个独立的进程之间的信息交流的呢?

进程之间进行信息交流的前提就是看到同一份资源,具体的处理方式就是通过一个文件来存放需要交流的信息。而这个文件是系统提供的函数接口创建的,将这个匿名文件的读写端都打开。之后子进程创建就会继承父进程的文件描述符表,这样父子进程都可以对这个匿名文件进行操作了。接下来我们看一下这个匿名管道创建的函数。

🍁pipe函数创建匿名管道

image-20221109235259080

头文件:unistd.h

参数:pipefd数组

返回值:如果管道创建成功,返回0,否则的话返回-1,并设置errno。

如果我们想要实现父子进程之间的通信,首先在父进程中创建管道,之后再创建子进程。这样的话就在父进程开始的时候关闭读端,在子进程开始的时候关闭写端,就会形成单向信息流的管道。

🔺这里的所谓的读端与写端,其实并不是真的把一个文件分成读端和写端两部分,而是打开文件的方式分别为读和写的方式,为了更形象的描述管道,我们就根据打开的方式对应现实中的管道两个端口。

image-20221110151840134

信息传递完之后,再关闭父进程的写端与子进程的读端,管道文件会从缓存中被清除,完成信息传递的使命。

⌨实测环节:

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <vector>
#include <unordered_map>
#include <ctime>
#define processNum 3
using namespace std;
const char *msg = "It's impossible to not fall in love with you!";
int main()
{
    //循环processNum次数
    for (int i = 0; i < processNum; ++i)
    {
        int pipefd[2] = {0};
        //打开管道,并将读和写对应的文件描述符分别写入pipefd[0]和pipefd[1]。
        if (pipe(pipefd) == -1)
        {
            cerr<< strerror(errno) << endl;
        }
        //创建子进程
        pid_t id = fork();
        if (id == 0)
        {
            //子进程关闭写端,进行读取操作
            close(pipefd[1]);
            //操作
            char str[1024];
            ssize_t size = read(pipefd[0], str, sizeof(str) - 1);
            while(size)
            {
                str[size] = '\0';
                cout << str << endl;
                size=read(pipefd[0], str, sizeof(str) - 1);
            }
            //关闭读端,退出子进程
            close(pipefd[0]);
            cout<<getpid()<<" 进程已退出"<<endl;
            exit(0);
        }
        else if (id > 0)
        {
            //父进程关闭读端,进行写操作
            close(pipefd[0]);
            
            //操作
            write(pipefd[1], msg, strlen(msg));
            
            //关闭写,回收子进程资源
            close(pipefd[1]);
            if (waitpid(id, nullptr, 0))
            {
                cout << "子进程[" << id << "]"
                     << "已被回收" << endl;
            }
        }
    }
    return 0;
}

💻:

image-20221110001628470

🔺值得一提的是,假如父进程一直不写入数据,子进程就会一直阻塞等待数据的写入,一旦管道文件的写端全部被关闭,读端又把管道文件内部的数据全部读完,read函数的返回值就会是0,并不再读取数据了。我们平常在使用read函数时,并没有出现出现阻塞等待读取数据的情况。原因在于平常我们read的文件是普通文件,而这次read的是管道文件。由于文件的属性不同,读取的方式就会有所区别,这就是在设计的时候根据需求所定好的规则。(文件种类问题)

上面说的问题用图片不好证明,有兴趣的小伙伴可以自己尝试一下,将父进程中的写入操作屏蔽掉,改成一个死循环(作用就是一直不关闭父进程的写端),子进程就一直不会退出,卡在read函数那里。

🔺还有一种情况,假如子进程的读端关闭了,父进程还一直在写入数据,就会将管道文件写满,导致写入阻塞。

🍁多个匿名管道的控制

上面的代码是一个父进程通过一个匿名管道与一个子进程进行通信。假如此时想要与多个子进程通信呢?那么就需要多个匿名管道来实现了。

假设还是父进程控制写端,子进程控制读端。

首先,这里有一个需要注意的点:由于是一个父进程与多个子进程通信,因此在循环创建子进程时会将父进程的文件描述符多次拷贝到子进程中去。而父进程每控制一个管道,就要在自己的文件描述符表中添加一个写端,就会被下次循环而创建的子进程继承,但实际上子进程用不到这个被继承下来的文件描述符,因此我们在子进程开始的时候除了要把新开启的匿名管道的写端给关闭,还要将之前继承的无用写端给关闭。

🙋‍♂️:既然是无用的,不管它就是了,为什么还要多此一举去关闭它呢?

👨‍🏫:因为这是read函数读取管道文件数据的特殊性导致的哇。记不记得我们想要持续多次的从文件中获得数据,就要保证文件至少有一个写端打开才行,使得就算匿名管道没有数据,子进程也会阻塞等待数据的到来。但是我们要想通过关闭管道的写端,来实现读端最后拿到的数据字节数为0的条件判断,从而退出子进程循环的读取数据,进而退出子进程的逻辑,就得把所有的读端都给关闭才行,子进程继承下来来的读端当然得关闭啦。

🙋‍♂️:最后几句话好绕啊……

👨‍🏫:是的,因此我们再结合一下代码,画个图会比较好理解。下面的代码的核心思想就是父进程开始的时候将三个函数作为任务放进vector中去,然后打开匿名管道,接着循环创建子进程,父进程通过写入数字来控制哪个子进程执行哪个函数。细节都已经加上注释了,看的时候可以仔细想想。

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <vector>
#include <unordered_map>
#include <ctime>
#define processNum 3
using namespace std;
typedef void (*functor)();
vector<functor> functors;//任务集

//三个任务
void f1()
{
    cout << "进程[" << getpid() << "]正在执行,时间戳[" << (unsigned)time(nullptr) << "]\n\n";
}
void f2()
{
    cout << "父进程[" << getppid() << "]给"<<"["<<getpid()<< "]" << "的任务还在执行……\n\n";
}
void f3()
{
    cout << "["<<getpid()<<"]:It's impossible to not fall in love with you\n\n";
}

//任务集加载
void LoadFunction()
{
    functors.push_back(f1);
    functors.push_back(f2);
    functors.push_back(f3);
}
typedef pair<int32_t, int32_t> elem;//用来存放子进程的pid和对应管道的管道写端
int main()
{
    srand((unsigned)time(nullptr));
    LoadFunction();
    vector<elem> assginTable;
    vector<int> last;
    for (int i = 0; i < processNum; ++i)
    {
        int pipefd[2] = {0};

        //子进程创建前先打开管道
        if (pipe(pipefd) == -1)
        {
            cout << "管道建立失败" << endl;
            return 1;
        }

        //创建子进程
        pid_t id = fork();
        if (id == 0)//子进程只会执行if内部的代码,因为只要结束if语句,if内的exit就会使得子进程退出
        {
            for (auto &oldFd : last) close(oldFd); //子进程关闭从父进程继承下来的无用写端
            cout << "子进程的pid: " << getpid() << "对应的读端fd是: " << pipefd[0] << " 对应的写端是: " << pipefd[1] << endl;
            close(pipefd[1]); //子进程关闭写端
            
            //循环读取数据
            while (true)
            {
                //从管道中读取父进程写入的任务编号存在operatorCode中
                int32_t operatorCode = 0;
                ssize_t size = read(pipefd[0], &operatorCode, sizeof(int32_t)); //读取数据
                
                if (size == 0) //读取数据字节数为0,说明所有的读端关闭并且管道内没有数据,因此退出循环读取数据
                {
                    cout << "子进程: [" << getpid() << "]进程管道写端关闭,进程退出" << endl;
                    break;
                }

                //读到任务编号就调用任务集中的函数,执行任务
                if (operatorCode < functors.size())
                    functors[operatorCode](); //执行任务
            }
            close(pipefd[0]);
            exit(0); //子进程退出,保证子进程只会执行if语句内部的代码
        }
        //父进程
        close(pipefd[0]);          //父进程关闭读端
        last.push_back(pipefd[1]); //下次创建新的子进程,关闭从父进程继承的写端时会用到
        elem e(id, pipefd[1]);     //将本次开启的   子进程id    和    管道写入端    给存起来
        assginTable.push_back(e);  //父进程给各个子进程派送任务编号会用到
    }
    sleep(2);
    //进行五次随机任务的派发
    int cnt = 5;
    while (cnt--)
    {
        int32_t pick = rand() % assginTable.size(); //随机选取管道写入操作数
        int32_t task = rand() % functors.size();    //随机选取方法
        write(assginTable[pick].second, &task, sizeof(int32_t));
    }
    sleep(1);

    for (int i = 0; i < assginTable.size(); ++i)
    {
        close(assginTable[i].second);
        cout << "父进程: 已关闭子进程[" << assginTable[i].first << "]"
             << "对应管道,"
             << "对应的控制fd是: " << assginTable[i].second << endl;

        if (waitpid(assginTable[i].first, nullptr, 0) > 0)
        {
            cout << "父进程:子进程[" << assginTable[i].first << "]"
                 << "已被回收" << endl << endl;
        }
    }
    return 0;
}

💻:

image-20221111155311011

灰色代表当前打开的匿名管道的文件描述符关闭,绿色代表开启,虚线代表关闭对应的文件描述符。

🍃第一次创建子进程时的管道处理:

因为第一次没有从父进程那里继承之前打开的写端,故不需要额外关闭其他写端。

image-20221111165911053

🍃第二次创建子进程的管道处理:

由于父进程此时已经有了一个写端描述符,因此子进程继承之后得额外多关闭一个写端,这里也就是要额外关闭子进程的4。

image-20221111170450893

🍃第三次创建子进程的管道处理:

父进程又多了一个文件描述符,因此子进程继承之后得额外多关闭两个写端,这里也就是要额外关闭子进程的4、5。

image-20221111170630017

而且我们发现,由于每次父进程都关闭3,会导致再次开启管道时会使得3被重复开启,也就导致子进程每次都会以3为读端。

经过上面的处理,最后我们想要退出程序并关闭管道,回收子进程,只需要依次关闭父进程中记录好的文件写端,就会使所有的子进程读取数据为0退出进程,父进程的waitpid等待成功,子进程从僵尸状态转变为死亡状态,回收完成。

上面我们实现的其实是一个简单的进程池,假如将父进程派发任务的算法从随机指派改为某种高效利用多个进程的指派,那么就可以利用多进程完成某些特定的工作。虽然还没有学习到相关内容,但是已经可以展望一下之后关于管道的深度应用了。

🚩命名管道(FIFO)

上面我们提到匿名管道的是在有血缘关系的进程之间使用的,那么如果想要在没有血缘关系的进程之间使用,就没办法了。这个时候就只能使用所谓的命名管道了。

命名管道的实现方式和匿名管道的实现方式很相似,都是使得两个进程看到同一份资源,但是这次我们自己指定一份资源给两个进程通信使用,也就是指定一个文件作为管道文件。由于open函数创建的文件只能是普通文件,因此只能求助于系统提供的另一个函数mkfifo。

🍁mkfifo函数创建命名管道

image-20221111180108791

头文件:sys/types.h、sys/stat.h

参数:pathname–要创建的管道文件名、mode–文件权限设置(默认情况下,创建的FIFO的模式为0666(‘a+rw’)减去umask中设置的位)

返回值:如果文件创建成功,返回0,否则的话返回-1,并设置errno。

下面我们可以用命名管道实现一个有意思的操作:模拟 客户端与服务端的数据实时传输,两个完全没有关系的文件进行交流。

简单来说就是先启动服务端可执行文件,然后创建命名管道,之后以读的方式打开管道文件,进行数据的循环阻塞式读取。而客户端则是在此之后运行,不用创建管道文件了,直接打开已经被创建的管道文件,进行数据写入。

服务端只要拿到了数据可以进行想要的操作,这里我们就直接输出拿到的数据。

⌨:

//common.h
#pragma once
#include<iostream>
#include<unistd.h>
#include <sys/stat.h>
#include<sys/types.h>
#include<cstring>
#include<cerrno>
#include<fcntl.h>
using namespace std;
#define IPC_PATH "fifo"

//Serve.cpp
#include "common.h"
int main()
{
    cout << "This is server" << endl;
    umask(0);
    int fifo = mkfifo(IPC_PATH, 0600);
    if (fifo < 0)
    {
        cerr << strerror(errno) << endl;
        return 1;
    }
    int pipeFd = open(IPC_PATH, O_RDONLY);
    if (pipeFd < 0)
    {
        cerr << "open fifo error" << endl;
        return 2;
    }
    char str[1024];
    while (true)
    {
        ssize_t size = read(pipeFd, str, sizeof(str) - 1);
        if (size == 0)
        {
            break;
        }
        str[size] = '\0';
        cout << "server# " << (char*)str;
    }
    close(pipeFd);
    unlink(IPC_PATH);//服务端删除管道文件
    cout << "server# server exit" << endl;
    return 0;
}

//Client.cpp
#include "common.h"
int main()
{
    cout << "This is Client" << endl;
    int pipeFd = open(IPC_PATH, O_WRONLY);
    if (pipeFd < 0)
    {
        cerr << strerror(errno) << endl;
        return 1;
    }
    char line[1024];
    while (true)
    {
        cout << "client# ";
        fflush(stdout);
        if (fgets(line, sizeof(line), stdin) != nullptr)
        {
            line[strlen(line) - 1] = '\0';
            write(pipeFd, line, strlen(line));
        }
        else
        {
            break;
        }
    }
    close(pipeFd);
    cout << "client exit" << endl;
    return 0;

💻:

image-20221111195515227

🚩总结

管道就是文件。这句话才是最关键的,所有关于管道的操作都是围绕文件进行的。管道作为进程间通信方式的一种,算是比较基础的了。这算是我第一次接触进程间的通信方法吧,感觉之前学习的各种知识已经串起来了,继续努力😎!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值