【Linux】进程间通信(一)匿名管道原理剖析与进程池手动实现全流程

文章目录


一、进程间通信介绍

首先我们知道进程是具有独立性的,所以这就注定了一个进程想把数据交给另一个进程几乎不可能,因为单单两个进程之间交换数据就相当于其中一个进程对另一个进程的数据做修改。所以如果一定要让进程之间互相通信,就一定需要一个第三者,这个第三者就是操作系统。
这里我们又要引入一则名言:

进程之间通信的前提是让不同的进程看到同一份资源,这份资源一定是某种形式的内存空间并且一定由操作系统提供。

二、进程间通信发展

  • 管道------基于文件的通信方式,本质是一种复用,因为先有文件,再有的进程间通信。
  • System V进程间通信------操作系统内单独实现的通信模块。它是一套标准,在类unix的系统中都适用。受限于时代,该标准只支持本地通信,也就是只能在一台机器内部通信。
  • POSIX进程间通信------支持网络间进程通信。

三、进程间通信分类

管道

• 匿名管道pipe

• 命名管道
System V IPC

• System V 消息队列

• System V 共享内存

• System V 信号量
POSIX IPC

• 消息队列

• 共享内存

• 信号量

• 互斥量

• 条件变量

• 读写锁

四、匿名管道

(下面介绍的管道皆为匿名管道)
管道是Unix中最古⽼的进程间通信的形式,我们把从⼀个进程连接到另⼀个进程的⼀个数据流称为⼀个"管道。

管道的概念

管道我们已经见过了,看上图,学了进程之后我们明白了指令本身就是进程,所以管道承担了一个进程把数据交给另一个进程的载体,所以管道本质就是一种进程间通信。

(who和wc都是bash的子进程,具有血缘关系,所以这里的管道是匿名管道)

管道的底层原理

下面是管道底层实现的示例图:

一般父子进程和文件行为和父子进程管道通信时的文件行为的本质区别就是struct file是否共享。
下面是详细原理图:

看上面原理图,这里有几个问题:

第一为什么父进程要同时读写打开一个文件呢,可以只用读或者写打开文件吗?肯定不行,因为子进程会继承父进程的文件打开方式,而要通信必须一个进程对文件读,一个进程对文件写。

第二为什么第三步要关闭父子进程要分别关闭自己的一侧读写段?首先是管道本质是单向通信的(正因为它有单向通信的特性所以它才叫管道),所以父子进程都要一侧读写段的多余的。但是关闭读写段并不是强制要求的,关闭只是为了避免误操作。

第三父子进程如果要相互通信该怎么做呢?开两个管道就行。

所以,看待管道,就如同看待⽂件⼀样!管道的使⽤和⽂件⼀致,迎合了"Linux⼀切皆⽂件思想"。

管道的定义

管道是一种基于文件系统的内存级单向通信文件,主要是用来进程间通信(IPC:Inter-Process Communication)的。

管道的demo代码

管道就只有一个系统调用,它是用来打开管道的,用法如下:

pipefd[0]对应读文件fd,pipefd[1]对应写文件fd。

(形象记忆:0是嘴巴,读,1是笔,写。)

下面我们来操作一下:

看上面操作,确实打开了文件,但是并没有看到文件路径,所以我们打开的文件本质是内存级文件,不用向磁盘刷新。没有名字,所以这是匿名管道。
下面我们来尝试父子进程之间进行通信,代码示例是子进程读,父进程写。
我们先做准备工作,为父子进程建立通信信道:

cpp 复制代码
// 子进程写, 父进程读
int main()
{
    // 1、父进程创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    (void)n; // 防止编译器告警,因为我们定义的变量后续没有使用

    // 2、父进程fork子进程
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if (id == 0)
    {
        // 子进程
        // 3、父子关闭不需要的fd
        close(pipefd[0]);
    }
    else
    {
        // 父进程
        // 3、父子关闭不需要的fd
        close(pipefd[1]);


        pid_t rid = waitpid(id, nullptr, 0);
        (void)rid;
    }

    return 0;
}

下面我们正式编写进程通信代码,小编先讲一下write和read,关于write的返回值我们会在把网络部分介绍完后再讲解,这里小编先介绍一下read的参数和返回值:

补充:read返回值为0表示读到了文件结尾。

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>

// 子进程写, 父进程读
int main()
{
    // 1、父进程创建管道
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    (void)n; // 防止编译器告警,因为我们定义了变量后续没有使用

    // 2、父进程fork子进程
    pid_t id = fork();
    if (id < 0)
    {
        perror("fork");
        exit(1);
    }
    else if (id == 0)
    {
        // 子进程
        // 3、父子关闭不需要的fd
        close(pipefd[0]);

        std::string str = "hello father, 我是子进程";
        std::string self = std::to_string(getpid());
        std::string message = str + "," + self + ",";
        int cnt = 0;
        while (1)
        {
            message += std::to_string(cnt++);
            write(pipefd[1], message.c_str(), message.size());
            sleep(1);
        }
    }
    else
    {
        // 父进程
        // 3、父子关闭不需要的fd
        close(pipefd[1]);
        while (1)
        {
            char inbuffer[1024] = {0};
            // read第三个参数是期望读取数据大小,也就是buffer最大值,需要-1去掉\0
            ssize_t n = read(pipefd[0], inbuffer, sizeof(inbuffer) - 1);
            if (n > 0)
            {
                inbuffer[n] = 0;
                std::cout << "client->father# " << inbuffer << std::endl;
            }
        }

        pid_t rid = waitpid(id, nullptr, 0);
        (void)rid;
    }

    return 0;
}

运行结果:

我们来分析上面代码,当我们传输字符串数据时,因为read和write只负责字节流的传输,都不会在字符串末尾自动加\0,在write的时候不用也不需要加\0,只不过在read之后需要我们手动在读取的字符串末尾加\0,因为cout在处理C风格字符串时会以\0为输出结束标志。
小编再说明一点,可能有的读者会疑惑,不是本来父子进程都能看到同一份文件吗,那父进程对文件内容做修改子进程不就能看到了吗?

其实这是假通信,因为只有fork后初期父子进程共享数据,其次无法传输变化的数据,因为父子进程其中一方对数据做修改了,就会发生写时拷贝,无法影响另一个进程看到的数据。

管道的特性与情况

五大特性:

1、只能让具有血缘关系的进程进行IPC,因为需要利用具有血缘关系进程的继承特性使进程间看到同一份文件,并且常用父子进程。

2、单向导通。

3、管道也属于文件,也有引用计数的特性,所以管道的生命周期随进程。

4、官道本质是文件,所以它是面向字节流的,通俗来讲就是写次数和读次数没有强相关,至于这个概念我们留到网络部分再细讲。

5、管道自带同步机制,比如上面代码,写端一秒写一次,但读端是阻塞式一直在读没有停歇的,但是读段会同步写端的频率,读端也1秒读一次。

四种极端情况:

情况一、写端不关,写端不写,读端一直读,那么管道里没有数据,并且fd[1]引用计数不为0,读端进程会被阻塞,等待数据写入或写端关闭才会被唤醒。

情况二、读端不关、读端不读,写端一直写,直到管道写满为止,ubuntu下管道的容量的64kb。

情况三、写端关闭,写端不写,读端一直读,fd[1]引用计数为0,OS判断不会再有数据写入管道,所以读端read不会阻塞,若读端一直调用read,read会一直返回0。

情况四、读端关闭、读端不读,写端一直写,这时操作系统会杀掉写进程,因为写操作非常消耗CPU资源,而OS不会做无意义且浪费资源的事。
我相信大家此时应该有个问题,为什么写端关闭,写端不写,读端一直读时OS不会杀掉读进程,而读端关闭、读端不读,写端一直写,OS会杀掉写进程呢?

这是因为读端 "读不到数据但写端已关闭" 是「预期的通信终止」,进程可正常处理;写端 "写不进数据且读端已关闭" 是「无意义的错误循环」,会浪费系统资源,OS 需通过信号终止进程以止损,可以类比我们打电话:

对方挂了(写端关闭),你对着听筒 "一直听"(读端一直读),只会听到 "忙音 / 静音"(返回 0),但你没有做错任何事,电话公司(OS)不会强行挂掉你的电话(杀进程)。

你对着听筒 "一直说"(写端一直写),但对方早就挂了(读端关闭),电话公司(OS)发现你在 "对着空线路无意义地说话",还占用通信资源,就会强行挂断你的电话(杀进程)。

抛出原子概念

单次向管道写入或读取时,若写入/读取的字节数若小于PIPE_BUF(4kb),那么这个写入/读取操作就是原子的。原子的意思就是该操作不可被分割,就像自然界的原子一样(实际可分,只是比喻)。原子性的特点就是该操作要么不做,要做就做完,没有第三状态。

五、匿名管道实践------手搓进程池

我们先分析一下设计思路,这段代码核心是一个父进程,多个子进程,由父进程来控制子进程做什么,什么时候做。因为是匿名管道实践,所以肯定会用到匿名管道的特性:

1、父进程可以通过是否向管道中写入数据来控制子进程的启停。(阻塞<->运行)

2、父进程传输四字节的整数给子进程,那么可以通过整数数值的不同,表示不同的任务。
下面是示意图:

初始化进程池

首先父进程先打开管道,然后依次fork五个子进程,但是这时会出现一个问题,父进程无法将管道和子进程相匹配,所以这时为什么需要先描述管道,然后把所有管道组织起来。思路是创建一个channel类,用于描述管道,channel内部的成员变量有三个,分别是:

  • 管道入口,父进程打开管道的pipefd[0]文件描述符(读端)
  • 管道本身,管道名
  • 管道出口,子进程的pid(写端)

然后在主函数中创建一个vector数组,把所有管道组织起来,父进程在循环中每fork一个子进程,都要为其创建一个对应的channel,然后将channel插入到vector数组中,这样父进程对子进程的管理就转化成了父进程对vector数组的管理。

我们可以把上述所有操作封装成一个方法:InitProcessPool,因为上述操作本质就是父进程创建子进程并初始化channel,想当于是对进程池的初始化工作。
代码如下:

cpp 复制代码
//ProcessPool.hpp
#ifndef __PROCESS_POOL_HPP_
#define __PROCESS_POOL_HPP_

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <string>
#include <stdlib.h>
#include <vector>
#include <iostream>

#define gdefault_process_num 5


#endif
cpp 复制代码
//Main.cc
#include "ProcessPool.hpp"

// 管道
class channel
{
public:
//默认构造
channel()
{}
//带参构造
channel(int fd, const std::string name, pid_t id) :_wfd(fd), _name(name), _sub_target(id)
{}
~channel()
{}

private:
    int _wfd;           //该管道对应父进程的写文件
    std::string _name;  //管道名字
    pid_t _sub_target;  //管道目标子进程
};

bool InitProcessPool(std::vector<channel>& channels)
{
    for(int i = 0; i < gdefault_process_num; i++)
    {
        // 为每个子进程创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        if(n < 0)
        {
            return false;
        }

        pid_t id = fork();
        if(id < 0)
        {
            return false;
        }
        else if(id == 0)
        {
            //子进程
            close(pipefd[1]);
            //子进程做父进程指派的事情

            exit(0);
        }

        //父进程
        close(pipefd[0]);
        std::string name = "channels" + std::to_string(i);
        //emplace_back支持直接传参数就地构造对象插入
        channels.emplace_back(pipefd[1], name, id);
    }
    return true;
}

//父写子读
int main()
{
    std::vector<channel> channels;
    InitProcessPool(channels);
    
    return 0;
}

子进程逻辑(回调)

下面我们实现子进程的执行流逻辑,在理想情况下父进程没给子进程发信息之前子进程应该处于阻塞状态,通过read管道文件被阻塞住就是很好的实现。但这里子进程业务逻辑不想直接在InitProcessPool框架中直接写死,而是通过调用方如这里的main函数传递不同的可调用对象,让子进程回调执行不同的业务逻辑,这样就可以实现让"框架"(InitProcessPool) 和 "业务逻辑"(调用方lambda)解耦,当业务逻辑发生变化时就不用频繁得改变框架代码了。 所以需要在InitProcessPool在引入一个类模板包装器的具体示例化类型,给他取别名为callback_t:

cpp 复制代码
//ProcessPool.hpp
using callback_t = std::function<void (int fd)>;

我们需要把callback_t作为InitProcessPool的第二个参数,这样子进程就可以通过调用callback_t对象实现回调功能了。
子进程回调的执行流如下:

代码实现如下:

cpp 复制代码
//ProcessPool.hpp
#ifndef __PROCESS_POOL_HPP_
#define __PROCESS_POOL_HPP_

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <string>
#include <stdlib.h>
#include <vector>
#include <functional>
#include <iostream>

#define gdefault_process_num 5

//下面等价于 typedef std::function<void (int fd)> func_t;
using callback_t = std::function<void (int fd)>;

#endif
cpp 复制代码
#include "ProcessPool.hpp"

// 管道
class channel
{
public:
//默认构造
channel()
{}
//带参构造
channel(int fd, const std::string name, pid_t id) :_wfd(fd), _name(name), _sub_target(id)
{}
~channel()
{}

private:
    int _wfd;           //该管道对应父进程的写文件
    std::string _name;  //管道名字
    pid_t _sub_target;  //管道目标子进程
};

bool InitProcessPool(std::vector<channel>& channels, callback_t cb)
{
    for(int i = 0; i < gdefault_process_num; i++)
    {
        // 为每个子进程创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        if(n < 0)
        {
            return false;
        }

        pid_t id = fork();
        if(id < 0)
        {
            return false;
        }
        else if(id == 0)
        {
            //子进程
            close(pipefd[1]);
            //子进程做父进程指派的事情
            cb(pipefd[0]); //回调执行main函数中的逻辑

            exit(0);
        }

        //父进程
        close(pipefd[0]);
        std::string name = "channels" + std::to_string(i);
        //emplace_back支持直接传参数就地构造对象插入
        channels.emplace_back(pipefd[1], name, id);
    }
    return true;
}

//父写子读
int main()
{
    std::vector<channel> channels;
    //子进程通过回调执行这里lambda的逻辑,
    //让"框架"(InitProcessPool) 和 "业务逻辑"(调用方lambda)解耦
    InitProcessPool(channels, [](int fd){
        while(true)
        {
            int code = 0; //读取父进程的整数信息
            std::cout << "子进程被阻塞:" << getpid() << std::endl;
            ssize_t n = read(fd, &code, sizeof(code)); //fd参数是子进程管道的读端
            if(n > 0)
            {
                std::cout << "子进程被唤醒:" << getpid() << std::endl;
            }
        }
    });

    //父进程等待,防止提前退出导致管道关闭
    sleep(20);
    
    return 0;
}

选择子进程

下面就该让父进程选择一个子进程,给子进程派发任务了,我们选择子进程的时候需要遵循负载均衡原则,让每个子进程都承担差不多的任务量。实现负载均衡有三种思路,其一是轮询选择,其二是随机选择,第三是在channel中加一个存储负载的成员变量,每次选取负载最低的channel。

下面我们用最简单的轮询方式来实现负载均衡,代码如下:

cpp 复制代码
//父写子读
int main()
{
    std::vector<channel> channels;
    // 1、创建子进程,让子进程进入自己的工作流中,父进程得到channels
    //子进程通过回调执行这里lambda的逻辑,
    //让"框架"(InitProcessPool) 和 "业务逻辑"(调用方lambda)解耦
    InitProcessPool(channels, [](int fd){
        while(true)
        {
            int code = 0; //读取父进程的整数信息
            // std::cout << "子进程被阻塞:" << getpid() << std::endl;
            ssize_t n = read(fd, &code, sizeof(code)); //fd参数是子进程管道的读端
            if(n > 0)
            {
                std::cout << "子进程被唤醒:" << getpid() << std::endl;
            }
        }
    });

    for(auto e : channels)
    {
        e.DebugPrint();
    }

    sleep(3);
    std::cout << "父进程开始控制" << std::endl;

    //2、父进程唤醒一个指定的子进程,让该子进程完成指定任务
    //2.1、轮询选择一个子进程(信道)-- 负载均衡
    int index = 0;
    while (true)
    {
        int who = index;
        index++;
        index %= _channels.size();
        int x = 1;
        std::cout << "选择信道:" << _channels[who].Name() << ", subtarget: " << _channels[who].SubTarget() << std::endl;
        write(_channels[who].Wfd(), &x, sizeof(x));
        sleep(1);
    }
    return 0;
}

封装进程池

下面我们要开始调整代码了,前面的代码只是为了方便大家理解进程池的实现逻辑,接下来就要对已有代码进行封装,还会增添一些必要的代码。
封装进程池ProcessPool思路及进程内部接口实现:

1、内部包含两个成员变量,进程池中有多少信道:_channels,进程池中有多少子进程: _subprocessnum。

2、构造函数,析构函数。注意构造函数只用传子进程数目,不用传channels,进程池内部的初始化接口会自动根据子进程数目创建子进程并初始化channels的。

3、初始化进程池InitProcessPool,进程池的初始化接口不能直接复用原来的InitProcessPool,需要做一些修改。首先把第一个参数channels去掉,因为channels已经是内部成员变量了,不用从外部传递进来。

4、父进程轮询控制子进程PollingCtrlSubProcess,可以直接复用index那部分的代码逻辑,我们还可以对该接口写一个函数重载,当不传任何参数时一直死循环选取并控制子进程,当传参时则根据传的参数来决定控制子进程次数。

5、回收进程池资源WaitSubProcessed,当我们完成进程池任务后,需要把让所有子进程退出并回收所有子进程。所以第一步要让所有子进程退出,思路是在子进程回调逻辑中动手脚,原本子进程的回调逻辑是一直死循环的,所以当子进程调用read函数返回0时,说明父进程已经将管道写端关闭了,这时子进程就需要退出回调的死循环逻辑了,子进程退出循环后回调完成,转头执行InitProcessPool中的子进程exit退出逻辑。第二步父进程回收子进程的僵尸状态。这里的关闭管道写端和等待回收子进程可以在channel中封装为函数接口,这样调用更方便。

cpp 复制代码
//Processpool.hpp
#ifndef __PROCESS_POOL_HPP_
#define __PROCESS_POOL_HPP_

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <string>
#include <stdlib.h>
#include <vector>
#include <functional>
#include <iostream>

#define gdefault_process_num 5

// 下面等价于 typedef std::function<void (int fd)> func_t;
using callback_t = std::function<void(int fd)>;

// 管道
class channel
{
public:
    // 默认构造
    channel()
    {
    }
    // 带参构造
    channel(int fd, const std::string name, pid_t id) : _wfd(fd), _name(name), _sub_target(id)
    {
    }
    // 调试接口
    void DebugPrint()
    {
        printf("channel name:%s, wfd:%d, _sub_target:%d\n", _name.c_str(), _wfd, _sub_target);
    }
    // 析构
    ~channel()
    {
    }

    int Wfd() { return _wfd; }

    std::string Name() { return _name; }

    pid_t SubTarget() { return _sub_target; }

    void Close() { close(_wfd); } //关闭管道的写端

    void Wait()
    {
        pid_t rid = waitpid(_sub_target, nullptr, 0);
        if(rid < 0)
        {
            perror("waitpid");
        }
    }

private:
    int _wfd;          // 该管道对应父进程的写文件
    std::string _name; // 管道名字
    pid_t _sub_target; // 管道目标子进程
};

class ProcessPool
{
public:
    ProcessPool(int subprocessnum = gdefault_process_num) :_subprocessnum(subprocessnum)
    {
    }
    ~ProcessPool() 
    {}

    bool InitProcessPool(callback_t cb)
    {
        for (int i = 0; i < _subprocessnum; i++)
        {
            // 为每个子进程创建管道
            int pipefd[2] = {0};
            int n = pipe(pipefd);
            if (n < 0)
            {
                return false;
            }

            pid_t id = fork();
            if (id < 0)
            {
                return false;
            }
            else if (id == 0)
            {
                // 子进程
                close(pipefd[1]);
                // 子进程做父进程指派的事情
                cb(pipefd[0]); // 回调执行main函数中的逻辑

                exit(0);
            }

            // 父进程
            close(pipefd[0]);
            std::string name = "channel-" + std::to_string(i);
            // emplace_back支持直接传参数就地构造对象插入
            _channels.emplace_back(pipefd[1], name, id);
        }
        return true;
    }

      void PollingCtrlSubProcess()
    {
        int index = 0;
        while (true)
        {
            int who = index;
            index++;
            index %= _channels.size();
            int x = 1;
            std::cout << "选择信道:" << _channels[who].Name() << ", subtarget: " << _channels[who].SubTarget() << std::endl;
            write(_channels[who].Wfd(), &x, sizeof(x));
            sleep(1);
        }
    }
    // 函数重载
    void PollingCtrlSubProcess(int count)
    {
        if(count < 0) return;
        int index = 0;
        while (count)
        {
            int who = index;
            index++;
            index %= _channels.size();
            int x = 1;
            std::cout << "选择信道:" << _channels[index].Name() << ", subtarget: " << _channels[index].SubTarget() << std::endl;
            write(_channels[index].Wfd(), &x, sizeof(x));
            sleep(1);
            count--;
        }
    }
    
    void WaitSubProcessed()
    {
        // 1、让所有子进程退出:依次关闭管道读端
        for(auto& e : _channels)
        {
            e.Close();
        }
        // 2、回收所有僵尸状态的子进程
        for(auto& e : _channels)
        {
            e.Wait();
            std::cout << "回收子进程:" << e.SubTarget() << std::endl;
        }
    }

    private:
        std::vector<channel> _channels; // 进程池中有多少信道
        int _subprocessnum;             // 进程池中有多少子进程
    };

#endif
cpp 复制代码
//Main.cc
#include "ProcessPool.hpp"

int main()
{
    // 1、创建进程池
    ProcessPool pp(5);

    // 2、初始化进程池
    pp.InitProcessPool([](int fd)
    {
        while(true)
        {
            int code = 0; //读取父进程的整数信息
            // std::cout << "子进程被阻塞:" << getpid() << std::endl;
            ssize_t n = read(fd, &code, sizeof(code)); //fd参数是子进程管道的读端
            if(n > 0)
            {
                std::cout << "子进程被唤醒:" << getpid() << std::endl;
            }

            // 父进程写端关闭
            if(n == 0)
            {
                std::cout << "子进程应该退出了:" << getpid() << std::endl;
                break;
            }
        }
    });

    // 3、控制进程池(选择子进程,派发任务)
    pp.PollingCtrlSubProcess(10);

    // 4、回收进程池资源
    pp.WaitSubProcessed();

    std::cout << "父进程控制子进程完成,父进程结束" << std::endl;

    return 0;
}

子进程需要完成的任务

下面我们要模拟一些子进程需要完成的任务,并把所有任务存出数组tasks中,父进程通过传递tasks的下标来让子进程完成指定任务。

下面我们再创建一个Task.hpp,模拟子进程的任务,tasks数组中的元素都是可调用对象。这里小编写了一个很特别的初始化tasks的方法,是在头文件中创建了一个Init类,我们把Init类的构造函数逻辑写成初始化tasks,所以我们可以在头文件中定义一个Init类对象,但其他文件包含这个头文件时就可以直接使用已经初始化好的tasks了。

cpp 复制代码
//Task.hpp
#pragma once

#include <iostream>
#include <functional>
#include <vector>

using task_t = std::function<void()>;

void Download()
{
    std::cout << "我是一个Download任务" << std::endl;
}

void Mysql()
{
    std::cout << "我是一个Mysql任务" << std::endl;
}

void sync()
{
    std::cout << "我是一个数据刷新同步任务" << std::endl;
}

void Log()
{
    std::cout << "我是一个日志保存任务" << std::endl;
}

std::vector<task_t> tasks;

// 创建Init类,定义出Init自动初始化tasks
class Init
{
public:
    // 构造函数
    Init()
    {
        tasks.push_back(Download);
        tasks.push_back(Mysql);
        tasks.push_back(sync);
        tasks.push_back(Log);
    }
};

Init ginit;

下面来调整PollingCtrlSubProcess逻辑,父进程要对子进程随机写入一个任务吗,我们这里有4个任务那么就随机写0-3其中一个数字。要生成随机数就需要包< ctime >头文件,并在ProcessPool的构造函数中srand种一颗种子。

因为我们的PollingCtrlSubProcess有函数重载,调整函数逻辑时要对两个函数都作修改,所以我们把核心逻辑CtrlSubProcessHelper提出来,让两个函数调用这个核心逻辑。

cpp 复制代码
//ProcessPool.hpp
#ifndef __PROCESS_POOL_HPP_
#define __PROCESS_POOL_HPP_

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <string>
#include <stdlib.h>
#include <vector>
#include <functional>
#include <iostream>
#include <ctime>
#include "Task.hpp"

#define gdefault_process_num 5

// 下面等价于 typedef std::function<void (int fd)> func_t;
using callback_t = std::function<void(int fd)>;

// 管道
class channel
{
public:
    // 默认构造
    channel()
    {
    }
    // 带参构造
    channel(int fd, const std::string name, pid_t id) : _wfd(fd), _name(name), _sub_target(id)
    {
    }
    // 调试接口
    void DebugPrint()
    {
        printf("channel name:%s, wfd:%d, _sub_target:%d\n", _name.c_str(), _wfd, _sub_target);
    }
    // 析构
    ~channel()
    {
    }

    int Wfd() { return _wfd; }

    std::string Name() { return _name; }

    pid_t SubTarget() { return _sub_target; }

    void Close() { close(_wfd); } // 关闭管道的写端

    void Wait()
    {
        pid_t rid = waitpid(_sub_target, nullptr, 0);
        if (rid < 0)
        {
            perror("waitpid");
        }
    }

private:
    int _wfd;          // 该管道对应父进程的写文件
    std::string _name; // 管道名字
    pid_t _sub_target; // 管道目标子进程
};

class ProcessPool
{
private:
    // CtrlSubProcess核心逻辑,内部自动对index++(内部逻辑,不暴露给外部)
    void CtrlSubProcessHelper(int &index)
    {
        int who = index;
        index++;
        index %= _channels.size();
        int x = rand() % tasks.size();
        std::cout << "选择信道:" << _channels[who].Name() << ", subtarget: " << _channels[who].SubTarget() << std::endl;
        write(_channels[who].Wfd(), &x, sizeof(x));
        sleep(1);
    }

public:
    ProcessPool(int subprocessnum = gdefault_process_num) : _subprocessnum(subprocessnum)
    {
        srand(time(nullptr));
    }
    ~ProcessPool()
    {
    }

    bool InitProcessPool(callback_t cb)
    {
        for (int i = 0; i < _subprocessnum; i++)
        {
            // 为每个子进程创建管道
            int pipefd[2] = {0};
            int n = pipe(pipefd);
            if (n < 0)
            {
                return false;
            }

            pid_t id = fork();
            if (id < 0)
            {
                return false;
            }
            else if (id == 0)
            {
                // 子进程
                close(pipefd[1]);
                // 子进程做父进程指派的事情
                cb(pipefd[0]); // 回调执行main函数中的逻辑

                exit(0);
            }

            // 父进程
            close(pipefd[0]);
            std::string name = "channel-" + std::to_string(i);
            // emplace_back支持直接传参数就地构造对象插入
            _channels.emplace_back(pipefd[1], name, id);
        }
        return true;
    }

    void PollingCtrlSubProcess()
    {
        int index = 0;
        while (true)
        {
            CtrlSubProcessHelper(index);
        }
    }

    // 函数重载
    void PollingCtrlSubProcess(int count)
    {
        if (count < 0)
        {
            return;
        }
        int index = 0;
        while (count)
        {
            CtrlSubProcessHelper(index);
            count--;
        }
    }

    void WaitSubProcessed()
    {
        // 1、让所有子进程退出:依次关闭管道读端
        for (auto &e : _channels)
        {
            e.Close();
        }
        // 2、回收所有僵尸状态的子进程
        for (auto &e : _channels)
        {
            e.Wait();
            std::cout << "回收子进程:" << e.SubTarget() << std::endl;
        }
    }

private:
    std::vector<channel> _channels; // 进程池中有多少信道
    int _subprocessnum;             // 进程池中有多少子进程
};

#endif
cpp 复制代码
//Main.cc
#include "ProcessPool.hpp"

int main()
{
    // 1、创建进程池
    ProcessPool pp(5);

    // 2、初始化进程池
    pp.InitProcessPool([](int fd)
        {
        while(true)
        {
            int code = 0; //读取父进程的整数信息
            // std::cout << "子进程被阻塞:" << getpid() << std::endl;
            ssize_t n = read(fd, &code, sizeof(code)); //fd参数是子进程管道的读端
            if(n == sizeof(code)) //任务码字节数要匹配
            {
                std::cout << "子进程被唤醒:" << getpid() << std::endl;
                if(code >= 0 && code < tasks.size())
                {
                    tasks[code]();
                }
                else{
                    std::cerr << "父进程给我的错误码是不对的:" << code << std::endl;
                }
            }
            // 父进程写端关闭
            else if(n == 0)
            {
                std::cout << "子进程应该退出了:" << getpid() << std::endl;
                break;
            }
            else{
                std::cerr << "read error, fd:" << fd << std::endl;
                break;
            }
        } });

    // 3、控制进程池(选择子进程,派发任务)
    pp.PollingCtrlSubProcess(10);

    // 4、回收进程池资源
    pp.WaitSubProcessed();

    std::cout << "父进程控制子进程完成,父进程结束" << std::endl;

    return 0;
}

代码中的bug

到目前为止其实我们的代码还有一个bug,WaitSubProcessed中我们只能等所有子进程都退出后再一起回收子进程,不能一边退出一般回收。原因是匿名管道的写端并不只被父进程持有,还可能被其他子进程持有,因为创建的每一个子进程都会继承父进程历史对其他管道的持有,所以只让父进程关闭wfd并不会让管道的写端关闭,因为还有可能有其他子进程持有该管道的写端,导致引用计数未减到0。这就会导致子进程一直read返回值一直不为0,无法及时退出。
问题示意图如下,绿色引用就是多余的,需要去掉。

要解决这个问题我们首先要明白两点,第一每次子进程创建时都会继承父进程的历史channels,channels中就有父进程历史持有的所有管道的写端,子进程就需要遍历channels把所有继承到的管道写端都关闭,并且子进程对channels的修改不会影响父进程的channels,因为会发生写时拷贝保证进程的独立性。第二是fork之后子进程只能看到所有历史的wfd(channels中),并且不会受后续父进程emplace_back的影响。

cpp 复制代码
// ProcessPool.hpp
// class ProcessPool
    bool InitProcessPool(callback_t cb)
    {
        for (int i = 0; i < _subprocessnum; i++)
        {
            // 为每个子进程创建管道
            int pipefd[2] = {0};
            int n = pipe(pipefd);
            if (n < 0)
            {
                return false;
            }

            pid_t id = fork();
            if (id < 0)
            {
                return false;
            }
            else if (id == 0)
            {
                // 子进程
                // 将子进程继承的父进程管道写端全部关闭
                for(auto& e : _channels)
                {
                    e.Close();
                }

                close(pipefd[1]);
                // 子进程做父进程指派的事情
                cb(pipefd[0]); // 回调执行main函数中的逻辑

                exit(0);
            }

            // 父进程
            close(pipefd[0]);
            std::string name = "channel-" + std::to_string(i);
            // emplace_back支持直接传参数就地构造对象插入
            _channels.emplace_back(pipefd[1], name, id);
        }
        return true;
    }
    
    void WaitSubProcessed()
    {
        // // 1、让所有子进程退出:依次关闭管道读端
        // for (auto &e : _channels)
        // {
        //     e.Close();
        // }
        // // 2、回收所有僵尸状态的子进程
        // for (auto &e : _channels)
        // {
        //     e.Wait();
        //     std::cout << "回收子进程:" << e.SubTarget() << std::endl;
        // }

        // 现在可以边退出边回收子进程了
        for(auto &e : _channels)
        {
            e.Close();
            e.Wait();
        }
    }

源码

ProcessPool.hpp

cpp 复制代码
#ifndef __PROCESS_POOL_HPP_
#define __PROCESS_POOL_HPP_

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <string>
#include <stdlib.h>
#include <vector>
#include <functional>
#include <iostream>
#include <ctime>
#include "Task.hpp"

#define gdefault_process_num 5

// 下面等价于 typedef std::function<void (int fd)> func_t;
using callback_t = std::function<void(int fd)>;

// 管道
class channel
{
public:
    // 默认构造
    channel()
    {
    }
    // 带参构造
    channel(int fd, const std::string name, pid_t id) : _wfd(fd), _name(name), _sub_target(id)
    {
    }
    // 调试接口
    void DebugPrint()
    {
        printf("channel name:%s, wfd:%d, _sub_target:%d\n", _name.c_str(), _wfd, _sub_target);
    }
    // 析构
    ~channel()
    {
    }

    int Wfd() { return _wfd; }

    std::string Name() { return _name; }

    pid_t SubTarget() { return _sub_target; }

    void Close() { close(_wfd); } // 关闭管道的写端

    void Wait()
    {
        pid_t rid = waitpid(_sub_target, nullptr, 0);
        if (rid < 0)
        {
            perror("waitpid");
        }
    }

private:
    int _wfd;          // 该管道对应父进程的写文件
    std::string _name; // 管道名字
    pid_t _sub_target; // 管道目标子进程
};

class ProcessPool
{
private:
    // CtrlSubProcess核心逻辑,内部自动对index++(内部逻辑,不暴露给外部)
    void CtrlSubProcessHelper(int &index)
    {
        int who = index;
        index++;
        index %= _channels.size();
        int x = rand() % tasks.size();
        std::cout << "选择信道:" << _channels[who].Name() << ", subtarget: " << _channels[who].SubTarget() << std::endl;
        write(_channels[who].Wfd(), &x, sizeof(x));
        sleep(1);
    }

public:
    ProcessPool(int subprocessnum = gdefault_process_num) : _subprocessnum(subprocessnum)
    {
        srand(time(nullptr));
    }
    ~ProcessPool()
    {
    }

    bool InitProcessPool(callback_t cb)
    {
        for (int i = 0; i < _subprocessnum; i++)
        {
            // 为每个子进程创建管道
            int pipefd[2] = {0};
            int n = pipe(pipefd);
            if (n < 0)
            {
                return false;
            }

            pid_t id = fork();
            if (id < 0)
            {
                return false;
            }
            else if (id == 0)
            {
                // 子进程
                // 将子进程继承的父进程管道写端全部关闭
                for(auto& e : _channels)
                {
                    e.Close();
                }

                close(pipefd[1]);
                // 子进程做父进程指派的事情
                cb(pipefd[0]); // 回调执行main函数中的逻辑

                exit(0);
            }

            // 父进程
            close(pipefd[0]);
            std::string name = "channel-" + std::to_string(i);
            // emplace_back支持直接传参数就地构造对象插入
            _channels.emplace_back(pipefd[1], name, id);
        }
        return true;
    }

    void PollingCtrlSubProcess()
    {
        int index = 0;
        while (true)
        {
            CtrlSubProcessHelper(index);
        }
    }

    // 函数重载
    void PollingCtrlSubProcess(int count)
    {
        if (count < 0)
        {
            return;
        }
        int index = 0;
        while (count)
        {
            CtrlSubProcessHelper(index);
            count--;
        }
    }

    void WaitSubProcessed()
    {
        // // 1、让所有子进程退出:依次关闭管道读端
        // for (auto &e : _channels)
        // {
        //     e.Close();
        // }
        // // 2、回收所有僵尸状态的子进程
        // for (auto &e : _channels)
        // {
        //     e.Wait();
        //     std::cout << "回收子进程:" << e.SubTarget() << std::endl;
        // }

        // 现在可以边退出边回收子进程了
        for(auto &e : _channels)
        {
            e.Close();
            e.Wait();
        }
    }

private:
    std::vector<channel> _channels; // 进程池中有多少信道
    int _subprocessnum;             // 进程池中有多少子进程
};

#endif

Task.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <functional>
#include <vector>

using task_t = std::function<void()>;

void Download()
{
    std::cout << "我是一个Download任务" << std::endl;
}

void Mysql()
{
    std::cout << "我是一个Mysql任务" << std::endl;
}

void sync()
{
    std::cout << "我是一个数据刷新同步任务" << std::endl;
}

void Log()
{
    std::cout << "我是一个日志保存任务" << std::endl;
}

std::vector<task_t> tasks;

// 创建Init类,定义出Init自动初始化tasks
class Init
{
public:
    // 构造函数
    Init()
    {
        tasks.push_back(Download);
        tasks.push_back(Mysql);
        tasks.push_back(sync);
        tasks.push_back(Log);
    }
};

Init ginit;

Main.cc

cpp 复制代码
#include "ProcessPool.hpp"

int main()
{
    // 1、创建进程池
    ProcessPool pp(5);

    // 2、初始化进程池
    pp.InitProcessPool([](int fd)
        {
        while(true)
        {
            int code = 0; //读取父进程的整数信息
            // std::cout << "子进程被阻塞:" << getpid() << std::endl;
            ssize_t n = read(fd, &code, sizeof(code)); //fd参数是子进程管道的读端
            if(n == sizeof(code)) //任务码字节数要匹配
            {
                std::cout << "子进程被唤醒:" << getpid() << std::endl;
                if(code >= 0 && code < tasks.size())
                {
                    tasks[code]();
                }
                else{
                    std::cerr << "父进程给我的错误码是不对的:" << code << std::endl;
                }
            }
            // 父进程写端关闭
            else if(n == 0)
            {
                std::cout << "子进程应该退出了:" << getpid() << std::endl;
                break;
            }
            else{
                std::cerr << "read error, fd:" << fd << std::endl;
                break;
            }
        } });

    // 3、控制进程池(选择子进程,派发任务)
    pp.PollingCtrlSubProcess(10);

    // 4、回收进程池资源
    pp.WaitSubProcessed();

    std::cout << "父进程控制子进程完成,父进程结束" << std::endl;

    return 0;
}

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

相关推荐
ManageEngineITSM7 小时前
技术的秩序:IT资产与配置管理的现代重构
大数据·运维·数据库·重构·工单系统
Bony-8 小时前
Go语言完全学习指南 - 从基础到精通------语言基础篇
服务器·开发语言·golang
阿巴~阿巴~9 小时前
线程安全单例模式与懒汉线程池的实现与优化
linux·服务器·单例模式·线程池·饿汉模式·懒汉模式·静态方法
大隐隐于野9 小时前
tcp 丢包分析
linux·服务器·网络
梦昼初DawnDream9 小时前
linux安全基线
linux·运维·安全
Broken Arrows9 小时前
在Linux系统中,top命令的显示参数详解
linux·运维·服务器
APIshop9 小时前
PHP:一种强大的服务器端脚本语言
服务器·php
qq_4017004110 小时前
I.MX6U 启动方式详解
linux
code-vibe12 小时前
物理机 kali 改造笔记 (一)
linux·运维·服务器