【Linux系统】匿名管道以及进程池的简单实现

前言:

上文我们讲到了动静态库的简单制作【Linux系统】动静态库的制作-CSDN博客

本文我们来讲一讲进程通信中的匿名管道!

++点个关注!++


理解

1.进程间通信的目的

数据传输:一个进程需要将它的数据发送给另一个进程

资源共享:多个进程之间共享同样的资源

通知事件的发生:一个进程需要向另一个进程或一组进程发送进程,通知它们发生了上什么事件(如:子进程退出时,要通知父进程进行等待)

进程控制:有些进程希望完全控制另一个进程,控制的进程希望拦截到另一个进程的所有陷入和异常,并能及时的知道它的状态改变

2.什么是进程间通信

进程通信(Inter-Process Communication,简称 IPC)是指操作系统中不同进程之间交换数据、传递信息或协调行为的机制

在操作系统中,进程是独立运行的程序实例,拥有各自独立的内存空间和资源(如地址空间、文件描述符等),默认情况下相互隔离。但实际应用中,多个进程 often 需要协作完成任务(例如一个进程生成数据,另一个进程处理数据),这就需要通过特定的方式打破隔离,实现信息交换 ------ 这就是进程通信的核心目的

3.如何通信

进程通信的本质是:先让不同的进程看到同一份资源,再然后才能实现通信!

而我们知道进程的有独立性的!想让进程之间看到同一份资源,这份资源就只能由OS提供,而如何提供呢?通过系统调用 ->设计统一的通信接口!


匿名管道

1.接口使用方法

cpp 复制代码
#include <unistd.h>
int pipe(int fd[2]);


输出型参数:int fd[2]
这是一个长度为 2 的 int 数组,用于存储管道的两个文件描述符
fd[0]:管道的读端(read end),专门用于从管道读取数据
fd[1]:管道的写端(write end),专门用于向管道写入数据
调用 pipe() 后,内核会自动填充这两个文件描述符,进程通过操作它们实现对管道的读写


返回值:成功返回0,失败返回-1

2.匿名管道原理

匿名管道是基于已经有的技术,再次开发的!

匿名管道本质也是文件!也有缓冲区(管道缓冲区)!

但是匿名管道的管道缓冲区不需要刷新到磁盘,也与磁盘没有关系!起作用主要用于暂时保存通信数据!

匿名管道是内存级的文件!在原有的文件控制上被再次设计,配上单独的接口。并且匿名管道没有文件路径,没有文件名!

如下图:

如何保证两个进程打开是同一个管道:

子进程会继承父进程的file_struct!所有指向的管道一定是同一个!

就算是子进程与子进程之间通信,它们的file_struct都是继承父进程的,都会指向同一个管道的。

3.匿名管道的特性与通信

5种特性:

|--------------------------------------------------------|
| 匿名管道,只能用于本机通信,只能用于有血缘关系的进程进行通信(常用于父子通信)! |
| 匿名管道文件,自带同步机制:包含4种通信情况! |
| 匿名管道的面向字节流的 |
| 匿名管道是单向通信的!(属于半双工。半双工:任何时候一个发,一个收。全双工:任何时候,可以同时收发) |
| 匿名管道的生命周期是随进程的! |

4种通信情况:

|--------------------------------------------------|
| 写慢,读快:读端阻塞,等待写端 |
| 写快,读慢:管道缓冲区写满了,就要阻塞等待读端 |
| 写关闭,读继续:一直读取,知道读到完,返回0,表示读取到文件末尾 |
| 写继续,读关闭:无意义操作!OS会自动杀掉写端进程(通过信号:13 SIGPIPE杀掉) |

4.测试匿名管道接口

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<cstdlib>
#include<sys/types.h>
#include<cstring>
#include<sys/wait.h>
using namespace std;

void ChildWrite(int fd)
{
    while(1)
    {
        char buffer[] = {"hello pipe!"};
        write(fd, buffer, strlen(buffer));
        sleep(1);
    }
}


void ParentRead(int fd)
{
    char buffer[1024];
    while(1)
    {
        int n=read(fd,buffer,sizeof(buffer)-1);
        if (n > 0)
        {
            buffer[n] = 0;
            printf("子进程说:%s\n", buffer);
        }
        else if(n==0)
        {
            cout<<"子进程已经退出!父进程也执行退出!\n";
            break;
        }
        else
        {
            cout<<"读取失败!\n";
            break;
        }
    }
}



// 子写 -> 父读
int main()
{
    //1.创建管道
    int fds[2] = {0};
    ssize_t n = pipe(fds);
    if(n<0)
    {
        cout<<"pipi error!\n";
        exit(1);
    }
    cout<<"fds[0]:"<<fds[0]<<endl;  //fds[0]:表示读取端
    cout<<"fds[0]:"<<fds[1]<<endl;  //fds[1]:表示写入端

    //2.创建子进程
    int x = fork();
    if(x<0)
    {
        cout<<"fork error!\n";
        exit(1);
    }
    else if(x==0)
    {
        //子进程
        //3.关闭不需要端口,关闭读端
        close(fds[0]);
        ChildWrite(fds[1]);

        //子进程退出
        close(fds[1]);
    }
    else
    {
        //父进程
        //3.关闭不需要端口,关闭写端
        close(fds[1]);
        ParentRead(fds[0]);

        //父进程退出
        close(fds[0]);

        
        //4.等待
        int states=0;
        int q = wait(&states);
    }
}

基于匿名管道实现进程池

把进程当成 "可复用的资源",提前建好一批,随时待命:

  1. 预先创建 "资源进程" :程序启动时,创建固定数量的进程(比如 4 个,或和 CPU 核心数一致),这些进程平时处于 空闲等待状态(不干活,但占着资源待命)。
  2. 管理进程调度任务 :有一个 "管理进程"(比如父进程)负责把任务分配给空闲的资源进程;资源进程处理完任务后,不会销毁,而是回到池里继续等下一个任务

类比:餐厅预先雇好 4 个厨师(资源进程),来了订单(任务),经理(管理进程)安排厨师做菜,做完继续待命,不用每次重新雇厨师。

1. 大体思路

1.进程池对象的创建:要有对应的类,采用先描述在组织的思路!先描述管道,在采用对应的结构体组织管道,最后再封装为进程池

2.进程池的启动:由父进程创建多个子进程,并由管道链接!父进程执行记录子进程的工作,子进程执行指定的任务。此时子进程阻塞在读取端,等待管道有数据写入!

3.任务的执行:选择子进程、选择任务返回任务码、向指定的子进程对应的管道写入任务码!子进程通过管道读取任务码,根据任务码去执行指定的任务!

4.进程池的中止:上面我们讲到,写端关闭、读端会自动关闭!所有只要将管道的写段全部关闭!那么读端就会全部关闭,子进程识别到读端关闭,就会退出!退出之后,再由父进程继续等待即可完成中止!

2.完整代码

cpp 复制代码
//ProgressPool.hpp

#pragma once
#include<iostream>
#include<vector>
#include<unistd.h>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/wait.h>
#include"Task.hpp"
using namespace std;
// 可以之间写方法的实现



//先描述
class Channel
{
public:
    Channel(int fd=0,int id=0)
    :_fd(fd)
    ,_id(id)
    {
        _name="Channel"+to_string(fd)+to_string(id);
    }

    string& name()
    {
        return _name;
    }

    //发送任务码
    void Send(int code)
    {
        write(_fd,&code,sizeof(code));
    }


    void Close()
    {
        close(_fd);
    }


    void Wait()
    {
        int states=0;
        waitpid(_id,&states,0);
    }

private:
    int _fd;
    pid_t _id;
    string _name;
};

// 在组织
class ManagerChannel
{
public:
    //记录管道fd、子进程的pid
    void Inset(int fd,pid_t id)
    {
        _c.emplace_back(fd,id);
    }

    //选择管道
    Channel& Select()
    {
        Channel& c=_c[_num];
        _num++;
        _num%=_c.size();
        return c;
    }

    //关闭写端
    void CloseRead()
    {
        for(auto& e:_c)
        {
            e.Close();
            cout<<"关闭:"<<e.name()<<endl;
        }
    }

    //回收子进程
    void WaitChild()
    {
        for(auto& e:_c)
        {
            e.Wait();
            cout<<"回收:"<<e.name()<<endl;
        }
    }

private:
    int _num=0;
    vector<Channel> _c;
};


class ProgressPoll
{
public:
    ProgressPoll(int num)
        : _num(num)
    {
    }

    //读取任务码,并执行
    void Work(int fd)
    {
        while (1)
        {
            int code;
            int n = read(fd, &code, sizeof(code));
            if (n > 0)
            {
                if(n!=sizeof(code))
                    continue;

                cout<<"子进程["<<getpid()<<"]收到一个任务码!\n";
                
                //根据任务码,执行任务
                _tm.execute(code);
            }
            else if (n == 0)
            {
                cout<<"父进程没有指定任务!子进程退出!\n";
                break;
            }
            else
            {
                cout<<"读取任务码错误!\n";
                break;
            }
        }
    }

//启动进程池
void Start()
{
    for(int i=0;i<_num;i++)
    { 
        // 创建管道
        int fd[2];
        int n = pipe(fd);
        if (n == -1)
        {
            cout << "pipe error!\n";
        }
        else
        {
            // 创建子进程
            pid_t id = fork();
            if (id == 0)
            {
                // 子进程
                // 关闭不需要的端口
                close(fd[1]);

                // 执行任务
                Work(fd[0]);

                // 退出
                close(fd[0]);
                exit(0);     //!!!!!严重注意
            }
            else
            {
                // 父进程
                // 关闭不需要的端口
                close(fd[0]);

                //记录子进程
                _m.Inset(fd[1],id);
            }
        }
    }
}


//派送任务
void run()
{
    //选择任务
    int taskcode=_tm.code();

    //选着管道(子进程):采取轮询选择
    auto& c=_m.Select();
    cout<<"选择子进程["<<c.name()<<"]执行任务\n";

    //发送任务码
    c.Send(taskcode);
    cout<<"发送任务码:"<<taskcode<<endl;
}

//停止进程池
void Stop()
{
    //关闭写端,读端自动关闭!
    //关闭父进程写段
    _m.CloseRead();

    //等待子进程
    _m.WaitChild();
}

private:
    int _num; //需要几个子进程
    ManagerChannel _m;
    TaskManager _tm;
};




//TaskManager.hpp

#pragma once
#include <iostream>
#include <ctime>

class TaskManager
{
public:

    //选择任务码
    int code()
    {
        srand(time(nullptr));
        return rand() % 5;
    }

    //根据子进程传递的任务码,执行对应任务
    void execute(int code)
    {
        std::cout<<"执行任务码为:"<<code<<"的任务!\n";
    }

private:
};




//test.cpp


#include "ProgressPool.hpp"

int main()
{
    // 创建进程池对象
    ProgressPoll pp(5);

    // 启动进程池
    pp.Start();

    // 派送任务
    for(int i=0;i<5;i++)
    {
        pp.run();
        sleep(2);
    }
    //pp.run();
    // 停止进程池
    pp.Stop();
}

3.修改小bug

上面的程序中存在一个小bug!

是在关闭写端的过程中,我们看上面代码关闭写段是只调用了一个close函数关闭了一个端口!但事实上我们的代码中管道不仅仅一个端口!

因为子进程的创建是会继承父进程的数据的!父进程中指向的管道,会被子进程继承,那么子进程也会指向对应的管道了!

这就会导致一个管道可能有多个进程指向!存在多个写端!

而存在多个写段会影响什么呢?因为我们上面的代码确实存在这个问题,但是可以正常运行啊!

看似没问题!实则暗藏玄机:

首先,如果存在多个写端,在进程池终止的时候,我们仅仅关闭的父进程的写段端,没有关闭其他子进程的写段 ,这会导致什么?这会导致管道的读端无法自动关闭 !从而导致子进程无法识别到中止意图,无法退出!

然后,那为什么我们上面的代码可以正常运行呢? 那是因为我们上面的代码是在任务指向完之后,再去进行的中止!而执行完任务后子进程是自动退出的! 子进程退出后,**子进程中的写段、读端页会被自动关闭!**所以我们之前的代码才可以正常运行!

但是!如果在子进程任务还没有执行完时,子进程还没有退出时,我们之间调用Stop函数就会卡死! 因为父进程写段关闭,但还有其他子进程指向,并且子进程并没有写入数据的功能!子进程就会直接卡死在read函数中!

修改bug:

既然知道bug以及bug的原因,那我们如何修改bug呢?

很简单!只需要在创建子进程的时候将继承下来的写端全部关闭即可!(子进程只有读端)

cpp 复制代码
// 在组织
class ManagerChannel
{
public:

    //关闭继承下来的全部写端
    void CloseAll()
    {
        for(auto& e:_c)
        {
            e.Close();
        }
    }

private:
    int _num=0;
    vector<Channel> _c;
};



//启动进程池
void Start()
{
    for(int i=0;i<_num;i++)
    { 
        // 创建管道
        int fd[2];
        int n = pipe(fd);
        if (n == -1)
        {
            cout << "pipe error!\n";
        }
        else
        {
            // 创建子进程
            pid_t id = fork();
            if (id == 0)
            {
                // 子进程
                //关闭继承父进程的全部写端
                _m.CloseAll();

                // 关闭不需要的端口
                close(fd[1]);

                // 执行任务
                Work(fd[0]);

                // 退出
                close(fd[0]);
                exit(0);     //!!!!!严重注意
            }
            else
            {
                // 父进程
                // 关闭不需要的端口
                close(fd[0]);

                //记录子进程
                _m.Inset(fd[1],id);
            }
        }
    }
}

4.最终完整代码

cpp 复制代码
//ProgressPool.hpp


#pragma once
#include<iostream>
#include<vector>
#include<unistd.h>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/wait.h>
#include"Task.hpp"
using namespace std;
// 可以之间写方法的实现



//先描述
class Channel
{
public:
    Channel(int fd=0,int id=0)
    :_fd(fd)
    ,_id(id)
    {
        _name="Channel"+to_string(fd)+to_string(id);
    }

    string& name()
    {
        return _name;
    }

    //发送任务码
    void Send(int code)
    {
        write(_fd,&code,sizeof(code));
    }


    void Close()
    {
        close(_fd);
    }


    void Wait()
    {
        int states=0;
        waitpid(_id,&states,0);
    }

private:
    int _fd;
    pid_t _id;
    string _name;
};

// 在组织
class ManagerChannel
{
public:
    //记录管道fd、子进程的pid
    void Inset(int fd,pid_t id)
    {
        _c.emplace_back(fd,id);
    }

    //选择管道
    Channel& Select()
    {
        Channel& c=_c[_num];
        _num++;
        _num%=_c.size();
        return c;
    }

    //关闭写端
    void CloseRead()
    {
        for(auto& e:_c)
        {
            e.Close();
            cout<<"关闭:"<<e.name()<<endl;
        }
    }

    //回收子进程
    void WaitChild()
    {
        for(auto& e:_c)
        {
            e.Wait();
            cout<<"回收:"<<e.name()<<endl;
        }
    }


    //关闭继承下来的全部写端
    void CloseAll()
    {
        for(auto& e:_c)
        {
            e.Close();
        }
    }

private:
    int _num=0;
    vector<Channel> _c;
};


class ProgressPoll
{
public:
    ProgressPoll(int num)
        : _num(num)
    {
    }

    //读取任务码,并执行
    void Work(int fd)
    {
        while (1)
        {
            int code;
            int n = read(fd, &code, sizeof(code));
            if (n > 0)
            {
                if(n!=sizeof(code))
                    continue;

                cout<<"子进程["<<getpid()<<"]收到一个任务码!\n";
                
                //根据任务码,执行任务
                _tm.execute(code);
            }
            else if (n == 0)
            {
                cout<<"父进程没有指定任务!子进程退出!\n";
                break;
            }
            else
            {
                cout<<"读取任务码错误!\n";
                break;
            }
        }
    }

//启动进程池
void Start()
{
    for(int i=0;i<_num;i++)
    { 
        // 创建管道
        int fd[2];
        int n = pipe(fd);
        if (n == -1)
        {
            cout << "pipe error!\n";
        }
        else
        {
            // 创建子进程
            pid_t id = fork();
            if (id == 0)
            {
                // 子进程
                //关闭继承父进程的全部写端
                _m.CloseAll();
                
                // 关闭不需要的端口
                close(fd[1]);

                // 执行任务
                Work(fd[0]);

                // 退出
                close(fd[0]);
                exit(0);     //!!!!!严重注意
            }
            else
            {
                // 父进程
                // 关闭不需要的端口
                close(fd[0]);

                //记录子进程
                _m.Inset(fd[1],id);
            }
        }
    }
}


//派送任务
void run()
{
    //选择任务
    int taskcode=_tm.code();

    //选着管道(子进程):采取轮询选择
    auto& c=_m.Select();
    cout<<"选择子进程["<<c.name()<<"]执行任务\n";

    //发送任务码
    c.Send(taskcode);
    cout<<"发送任务码:"<<taskcode<<endl;
}

//停止进程池
void Stop()
{
    //关闭写端,读端自动关闭!
    //关闭父进程写段
    _m.CloseRead();

    //等待子进程
    _m.WaitChild();
}

private:
    int _num; //需要几个子进程
    ManagerChannel _m;
    TaskManager _tm;
};





//TaskManager.hpp


#pragma once
#include <iostream>
#include <ctime>

class TaskManager
{
public:

    //选择任务码
    int code()
    {
        srand(time(nullptr));
        return rand() % 5;
    }

    //根据子进程传递的任务码,执行对应任务
    void execute(int code)
    {
        std::cout<<"执行任务码为:"<<code<<"的任务!\n";
    }

private:
};



//test.cpp


#include "ProgressPool.hpp"

int main()
{
    // 创建进程池对象
    ProgressPoll pp(5);

    // 启动进程池
    pp.Start();

    // 派送任务
    for(int i=0;i<5;i++)
    {
        pp.run();
        sleep(2);
    }
    //pp.run();
    // 停止进程池
    pp.Stop();
}
相关推荐
苦学编程的谢6 分钟前
Linux
linux·运维·服务器
G_H_S_3_13 分钟前
【网络运维】Linux 文本处理利器:sed 命令
linux·运维·网络·操作文本
Linux运维技术栈24 分钟前
多系统 Node.js 环境自动化部署脚本:从 Ubuntu 到 CentOS,再到版本自由定制
linux·ubuntu·centos·node.js·自动化
long_run25 分钟前
C++之auto 关键字
c++
拾心2141 分钟前
【运维进阶】Linux 正则表达式
linux·运维·正则表达式
疯狂的代M夫1 小时前
C++对象的内存布局
开发语言·c++
xcs194051 小时前
AI 自动化编程 trae 体验 页面添加富编辑器
运维·自动化·编辑器
Gss7772 小时前
源代码编译安装lamp
linux·运维·服务器
444A4E2 小时前
深入理解Linux进程管理:从创建到替换的完整指南
linux·c语言·操作系统
重启的码农2 小时前
llama.cpp 分布式推理介绍(4) RPC 服务器 (rpc_server)
c++·人工智能·神经网络