【Linux】基于匿名管道通信实现的简易进程池

 本文仅作为对于匿名管道通信的练习和进程池的初识,对于进程池的深度学习还请阅读《Linux高性能服务器编程》。

目录

一、进程池的概念

二、使用匿名管道实现进程池的注意事项:

1.1、主函数逻辑

1.2、父进程创建进程池并对子进程进行分流 

1.3、在每个子进程中将管道的读端重定向到标准输入流(0)的原因:

2、子进程在何时退出? 

三、基于匿名管道通信实现的简易进程池完整代码


一、进程池的概念

  1. 定义:进程池是一种技术应用,它由资源进程和管理进程组成。资源进程是预先创建好的空闲进程,用于处理分配给它们的任务。管理进程则负责创建资源进程、分配任务给空闲的资源进程,并在任务完成后回收这些资源进程。

  2. 作用:进程池的主要作用是优化资源管理和提高系统效率。通过预先创建一定数量的进程并放入池中,当有任务需要执行时,可以直接从池中取出空闲进程来处理,而无需每次都创建新的进程。这种方式避免了频繁创建和销毁进程所带来的开销,节省了系统资源,并降低了操作系统的调度难度。

  3. 交互手段:管理进程与资源进程之间的交互是通过各种通信机制实现的,如IPC(进程间通信)、信号、信号量、消息队列和管道等。这些机制确保了管理进程可以有效地将任务分配给资源进程,并在任务完成后及时回收资源。

  4. 并发效果:由于进程池中的进程数量是固定的,因此同一时间最多只有固定数量的进程在运行。这种方式虽然限制了并发度,但也在一定程度上实现了并发效果,因为多个任务可以在不同的资源进程中并行处理。

二、使用匿名管道实现进程池的注意事项:

1、本文进程池的实现思路:

1.1、主函数逻辑
int main()
{
    // 创建任务
    CreateTask();

    std::vector<channel> channels;
    // 1、创建子进程池,子进程个数由用户进行输入
    if (CreatePipeAndProcess(channels, ExecuteTask))
    {
        std::cout << "进程池创建成功。" << std::endl;
    }
    else
    {
        std::cout << "进程池创建失败。" << std::endl;
    }
    // 发送执行任务,轮流向指定进程的管道中写入
    SendTask(channels, 30);
    sleep(5);
    // 关闭写端、回收子进程
    CloseWfdAndWaitChildProcess(channels);
    return 0;
} 
1.2、父进程创建进程池并对子进程进行分流 

在父进程中使用fork()函数循环创建一定数量的子进程,当子进程创建完毕时,立即去等待处理父进程派发任务——即当管道写端未关闭且未读端未读取到数据时,子进程的运行会阻塞在read()处,直到读取到数据或写端关闭为止。因此,子进程不再参与后续子进程的创建(不再执行后续的循环代码,当子进程执行完自己的任务后直接使用exit函数退出进程),即进程池均由同一个父进程进行统一管理。

bool CreatePipeAndProcess(std::vector<channel> &channels, task_t handle_task)
{
    int pool_size = 0;
    std::cout << "请输入需要创建的进程池的大小:" << std::endl;
    std::cin >> pool_size;
    if (pool_size <= 0 || pool_size > POOL_SIZE)
    {
        std::cout << "进程池大小无效,创建失败!" << std::endl;
        return false;
    }
    for (int i = 0; i < pool_size; i++)
    {
        int pipefd[2] = {0};
        if (pipe(pipefd) < 0)
        {
            std::cerr << "管道创建失败!错误码:" << errno << std::endl;
            return false;
        }
        pid_t id = fork();
        if (id < 0)
        {
            std::cerr << "子进程创建失败!错误码:" << errno << std::endl;
            return false;
        }
        else if (id == 0)
        {
            // 1、关闭子进程的写端
            close(pipefd[1]);
            // 2、将子进程的读端重定向到子进程的标准输入流中
            dup2(pipefd[0], 0);
            // 由于子进程会继承父进程的文件描述符表,所以需要关闭所有其他管道的写端描述符
            if (!channels.empty())
                for (auto &ch : channels)
                {
                    ch.Close();
                }
            // 3、执行各自的任务
            handle_task();
            close(pipefd[0]); // 关闭原始读端描述符
            exit(0);          // 子进程正常退出
        }
        else
        {
            // 父进程负责建立进程池
            // 1、关闭读端
            close(pipefd[0]);
            // 2、存入管道的写端、进程标识符,构造channel
            char buffer[BUFFER_SIZE];
            memset(buffer, 0, sizeof(buffer));
            snprintf(buffer, sizeof(buffer), "Process - %d, id : %d", i, id);
            std::string description(buffer);
            channels.emplace_back(pipefd[1], description, id);
            // 父进程关闭管道写端后由子进程处理
        }
    }
    return true;
}

注意:子进程会继承父进程的文件描述符表。当创建第一个子进程时,正常关闭该进程的写端描述符即可。但从第二个子进程创建开始,每个进程都需要关闭从父进程继承而来的所有管道的写端文件描述符。

原因:1、关闭不需要使用的文件描述符可以减少资源浪费

2、主要原因:进程池中子进程的退出依赖于管道写端的关闭,只要管道中尚有写端存在,子进程就会一直阻塞在read()处,无法正常退出。因为每创建一个新的子进程,该子进程都会继承父进程的文件描述符表,这就导致了一个管道的写端被多个子进程指向。所以从第二个子进程创建开始,每个进程都需要关闭从父进程继承而来的所有管道的写端文件描述符!!!

else if (id == 0)
        {
            // 1、关闭子进程的写端
            close(pipefd[1]);
            // 2、将子进程的读端重定向到子进程的标准输入流中
            dup2(pipefd[0], 0);
            // 由于子进程会继承父进程的文件描述符表,所以需要关闭所有其他管道的写端描述符
            if (!channels.empty())
                for (auto &ch : channels)
                {
                    ch.Close();
                }
            // 3、执行各自的任务
            handle_task();
            close(pipefd[0]); // 关闭原始读端描述符
            exit(0);          // 子进程正常退出
        }
1.3、在每个子进程中将管道的读端重定向到标准输入流(0)的原因:
// 2、将子进程的读端重定向到子进程的标准输入流中
            dup2(pipefd[0], 0);
void ExecuteTask()
{
    int read_num = 0;
    int task_index = 0;
    while (true)
    {
        read_num = read(0, &task_index, sizeof(int));
        //..............
    }
}

 因为每个进程都是独立的,他们拥有各自的文件描述符表,在子进程内修改文件描述符表并不会影响到其他进程的文件描述符表的状态。使用dup2()函数将管道的读端重定向至标准输入流:是为了每个子进程都独立拥有一个管道,管道的读端文件描述符各不相同。重定向到标准输入流可以使每个子进程在读取数据时都可以从0号文件(即标准输入流)读取,因此可以减少传参操作,简化了数据的传输方式,同时使得读取操作在行为上也得到了统一。

2、子进程在何时退出? 

 当需要让进程池停止运行时,主进程不再为进程池派发任务,即不再对管道进行写入操作。此时关闭所有管道的写端,则管道的读端使用read()函数的返回值为0,意味着写端已经关闭。此时我们对read()的返回值进行判断,返回值为0时终止子进程运行,等待父进程回收退出的子进程即可。当写端没有关闭但未写入数据时,读端会阻塞在read()函数处,所以不用担心管道中无数据时的返回值问题。

三、基于匿名管道通信实现的简易进程池完整代码

// 基于匿名管道的进程池实现,采用轮询方案使用进程池中的进程,模拟对多项任务的处理

#include <iostream>
#include <vector>
#include <functional>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <ctime>
#include <string>

#define POOL_SIZE 10
#define TASK_SIZE 3
#define BUFFER_SIZE 512

using task_t = std::function<void()>;
// 创建任务栏
std::vector<task_t> tasks(TASK_SIZE);

void Print()
{
    std::cout << "I am print task" << std::endl;
}
void DownLoad()
{
    std::cout << "I am a download task" << std::endl;
}
void Flush()
{
    std::cout << "I am a flush task" << std::endl;
}

void CreateTask()
{
    srand(time(nullptr)); // 种下随机数种子
    tasks[0] = Print;
    tasks[1] = DownLoad;
    tasks[2] = Flush;
}
int GetNextChannelIndex(int channel_size)
{
    static int next = 0;
    int ret = (next++) % channel_size;
    return ret;
}
void ExecuteTask()
{
    int read_num = 0;
    int task_index = 0;
    while (true)
    {
        read_num = read(0, &task_index, sizeof(int));
        if (read_num == -1)
        {
            std::cerr << "管道读取失败!错误码:" << errno << std::endl;
            exit(-1);
        }
        else if (read_num == 0) // 读到0代表写端关闭,直接停止
        {
            std::cout << "子进程 " << getpid() << " 任务读取完成或管道关闭,退出。" << std::endl;
            break;
        }
        else
        {
            if (task_index >= 0 && task_index < TASK_SIZE)
            {
                std::cout << "子进程 " << getpid() << " 执行任务 " << task_index << std::endl;
                tasks[task_index]();
            }
            else
            {
                std::cerr << "无效的任务索引:" << task_index << std::endl;
            }
        }
    }
}

class channel
{
private:
    int _wfd;          // 管道的读端文件描述符
    std::string _name; // 管道的名称
    pid_t _pid;        // 子进程的标识符
public:
    channel(int wfd, const std::string &name, pid_t pid)
        : _wfd(wfd), _name(name), _pid(pid)
    {
    }
    int GetWfd()
    {
        return _wfd;
    }
    void Wait()
    {
        if (waitpid(_pid, nullptr, 0) > 0)
        {
            std::cout << "wait " << _pid << " success" << std::endl;
        }
        else
        {
            std::cout << "wait " << _pid << " false" << std::endl;
        }
    }
    void Close()
    {
        close(_wfd);
    }
    std::string& GetName(){
        return _name;
    }
};

bool CreatePipeAndProcess(std::vector<channel> &channels, task_t handle_task)
{
    int pool_size = 0;
    std::cout << "请输入需要创建的进程池的大小:" << std::endl;
    std::cin >> pool_size;
    if (pool_size <= 0 || pool_size > POOL_SIZE)
    {
        std::cout << "进程池大小无效,创建失败!" << std::endl;
        return false;
    }
    for (int i = 0; i < pool_size; i++)
    {
        int pipefd[2] = {0};
        if (pipe(pipefd) < 0)
        {
            std::cerr << "管道创建失败!错误码:" << errno << std::endl;
            return false;
        }
        pid_t id = fork();
        if (id < 0)
        {
            std::cerr << "子进程创建失败!错误码:" << errno << std::endl;
            return false;
        }
        else if (id == 0)
        {
            // 1、关闭子进程的写端
            close(pipefd[1]);
            // 2、将子进程的读端重定向到子进程的标准输入流中
            dup2(pipefd[0], 0);
            // 由于子进程会继承父进程的文件描述符表,所以需要关闭所有其他管道的写端描述符
            if (!channels.empty())
                for (auto &ch : channels)
                {
                    ch.Close();
                }
            // 3、执行各自的任务
            handle_task();
            close(pipefd[0]); // 关闭原始读端描述符
            exit(0);          // 子进程正常退出
        }
        else
        {
            // 父进程负责建立进程池
            // 1、关闭读端
            close(pipefd[0]);
            // 2、存入管道的写端、进程标识符,构造channel
            char buffer[BUFFER_SIZE];
            memset(buffer, 0, sizeof(buffer));
            snprintf(buffer, sizeof(buffer), "Process - %d, id : %d", i, id);
            std::string description(buffer);
            channels.emplace_back(pipefd[1], description, id);
            // 父进程关闭管道写端后由子进程处理
        }
    }
    return true;
}
void SendTaskOnce(std::vector<channel> &channels)
{
    // 1、选择一个进程
    int channel_index = GetNextChannelIndex(channels.size());
    // 2、选择一个任务
    int task_index = rand() % TASK_SIZE;
    std::cout << "发送任务 " << task_index << " 到进程 " << channels[channel_index].GetName() << std::endl;
    // 3、向指定子进程管道中写入任务码
    if (write(channels[channel_index].GetWfd(), &task_index, sizeof(int)) == -1)
    {
        std::cout << "管道写入失败!" << std::endl;
        exit(-1);
    }
}
void SendTask(std::vector<channel> &channels, int counts)
{
    while (counts--)
    {
        SendTaskOnce(channels);
    }
}
void CloseWfdAndWaitChildProcess(std::vector<channel> &channels)
{
    for (auto &ch : channels)
    {
        ch.Close();
        ch.Wait();
    }
}
int main()
{
    // 创建任务
    CreateTask();

    std::vector<channel> channels;
    // 1、创建子进程池,子进程个数由用户进行输入
    if (CreatePipeAndProcess(channels, ExecuteTask))
    {
        std::cout << "进程池创建成功。" << std::endl;
    }
    else
    {
        std::cout << "进程池创建失败。" << std::endl;
    }
    // 发送执行任务,轮流向指定进程的管道中写入
    SendTask(channels, 30);
    sleep(5);
    // 关闭写端、回收子进程
    CloseWfdAndWaitChildProcess(channels);
    return 0;
} 

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

这题怎么做?!?

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值