【Linux】linux进程控制(进程池的详解与实现)

本文是小编巩固自身而作,如有错误,欢迎指出!

目录

一、进程池简介

二、父进程对子进程的管理

三、建立信道,初始化工作

功能作用

核心流程

四、Task与Slaver

[(1)准备阶段:创建子进程与通信管道(Init 函数)](#(1)准备阶段:创建子进程与通信管道(Init 函数))

(2)选择子进程:轮询调度算法(ChooseSlaver)

(3)发送任务:通过管道写给

(4)子进程接收并执行任务(DoTask)

(5)完整流程总结

五、完整代码呈现

ProcessPool.cc

makeflie


一、进程池简介

进程池是什么?简单来是就是一个容纳多个进程的池子,但是它存在的作用又是为了什么呢?

进程池 = 提前创建好一批子进程,放在池子里备用,需要任务直接分配给它们,不临时频繁创建 / 销毁进程。

类比:

普通方式:来一个任务 → 临时招一个临时工(fork)→ 干完立马辞退 → 再来任务再招,频繁招人辞退,开销大、慢

进程池:提前固定招 5 个正式员工 ,一直在工位待命,来任务直接分给员工做,不用反复招人、辞退,省资源、速度快

为什么要有进程池?(为什么不每次任务都 fork)

Linux 下 fork () 创建进程开销很大

  • 要拷贝页表、虚拟内存、文件描述符、进程上下文
  • 频繁 fork /exit 会 CPU 开销高、卡顿、效率低
  • 还容易产生僵尸进程

二、父进程对子进程的管理

父进程对子进程的管理,我们可以归结为一句话:

先描述,再组织。

先描述

先用变量、结构体、类 ,把要用到的资源、属性、特征先定义、先描述出来。

先把 "东西长什么样、有哪些成员" 定义好,先造模板

再组织

再写逻辑、写流程、写循环,把这些描述好的资源调度起来、管理起来、用起来

再用模板去创建实例、调度工作、完成业务逻辑。

比如做学校管理系统:

  1. 先描述

    • 先定义学生类:姓名、学号、年龄
    • 先定义班级类:班级编号、班主任、学生列表
  2. 再组织

    • 创建班级、招收学生
    • 分班、排课、考勤、管理作息

先把实体结构定义好 ,再写业务流程调度

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>

//先描述
class channel
{
public:
    channel(int cmdfd, pid_t slaverid, const std::string& processname)
    :_cmdfd(cmdfd), _slaverid(slaverid), _processname(processname)
    {}
public:
    int _cmdfd;                 //发送任务的文件描述符fd
    pid_t _slaverid;            //子进程的pid
    std::string _processname;   //子进程的名字
};

int main()
{
    //再组织
    std::vector<channel> channels;

    return 0;
}

三、建立信道,初始化工作

当我们已经有父进程管理子进程的方法和手段之后,接下来就是初始化工作,建立父进程与子进程的信道,for循环fork创建多个子进程。

功能作用

批量创建若干子进程 ,同时给每个子进程单独创建一条匿名管道作为通信信道

核心流程

  1. 循环创建指定个数子进程
  2. 每次循环新建一根匿名管道
  3. fork 创建子进程
  4. 子进程:关闭管道写端,保留读端,进入任务循环干活
  5. 父进程:关闭管道读端,保留写端
  6. 父进程把 写端 fd + 子进程 pid 封装 Channel,保存起来
cpp 复制代码
// 建立通信信道、初始化子进程工作
void CreateProcessChannel(cb_t cb)
{
    // 循环创建固定数量的子进程
    for (int i = 0; i < gprocessnum; i++)
    {
        // 1. 创建匿名管道,建立父子通信信道
        int pipefd[2] = {0};
        pipe(pipefd);  

        // 2. fork 创建子进程
        pid_t id = fork();

        // 子进程分支
        if (id == 0)
        {
            // 子进程关闭不用的管道写端,只留读端
            close(pipefd[1]);
            // 初始化子进程工作:绑定任务处理函数,阻塞等待父进程发任务
            cb(pipefd[0]);
            // 子进程工作结束退出
            exit(0);
        }
        // 父进程分支
        else
        {
            // 父进程关闭不用的管道读端,只留写端
            close(pipefd[0]);
            // 把管道写端、子进程pid封装成信道对象,统一管理
            channels.emplace_back(pipefd[1], id);
        }
    }
}

四、Task与Slaver

在前文,我们已经了解了创建多个子进程以其对应的管道,现在我们看看不同的子进程是怎么被划分执行不同的任务的

下面是给每个slaver分配task的详细过程:

(1)准备阶段:创建子进程与通信管道(Init 函数)

cpp 复制代码
void Init(callback func)
{
    // 1. 创建管道
    int pipefd[2];
    pipe(pipefd);

    // 2. 创建子进程
    pid_t id = fork();

    if (id == 0)
    {
        // 子进程:关闭写端,只保留读端
        close(pipefd[1]);
        // 子进程开始等待任务
        func(pipefd[0]);
        exit(0);
    }
    else
    {
        // 父进程:关闭读端,只保留写端
        close(pipefd[0]);
        // 将管道和子进程PID打包成Channel
        Channel ch(pipefd[1], id, "slaver");
        _channels.push_back(ch);
    }
}
  • 父进程调用 Init,为每个子进程创建匿名管道
  • 使用 fork 创建 Slaver 子进程
  • 子进程关闭管道写端,进入阻塞等待,随时准备接收任务。
  • 父进程关闭管道读端,将管道写端 _wfd子进程 PID _sub_pid 封装进 Channel
  • 所有信道存入 _channels 容器,完成任务派送通道的建立

(2)选择子进程:轮询调度算法(ChooseSlaver

cpp 复制代码
int ChooseSlaver()
{
    static int idx = 0;
    idx %= _channels.size();
    int ret = idx;
    idx++;
    return ret;
}
  • 父进程要派发任务前,必须先选定一个子进程
  • 使用轮询算法 ,从 _channels 里按顺序挑选子进程下标。
  • idx 从 0 开始,依次返回 0 → 1 → 2 → 3 → 4 → 0...
  • 保证任务均匀分配给所有 Slaver 子进程。
  • 返回值 ret 就是目标子进程的 index

(3)发送任务:通过管道写给

cpp 复制代码
void SendTask(int task, int idx)
{
    // 向指定Channel的管道写端发送任务
    write(_channels[idx]._wfd, &task, sizeof(task));
}
  • 父进程拿到子进程下标 idx
  • 通过 _channels[idx] 找到对应子进程的通信信道
  • 调用系统 write,把任务编号 task 通过管道写端 _wfd 发送出去。
  • 内核将任务数据传递给对应子进程,完成任务派送。

(4)子进程接收并执行任务(DoTask

cpp 复制代码
void DoTask(int readfd)
{
    while (true)
    {
        int task;
        // 阻塞等待父进程发送任务
        int n = read(readfd, &task, sizeof(task));

        // 根据任务编号执行任务
        switch (task)
        {
            case 1: ... break;
            case 2: ... break;
            ...
        }
    }
}
  • 子进程一直阻塞在 read,等待父进程下发 Task
  • 父进程发送任务后,子进程读取到任务编号 task
  • 使用 switch 匹配任务,执行对应的业务逻辑。
  • 完整流程总结执行完成后继续循环,等待下一个任务

(5)完整流程总结

  • Init:父进程创建多个 Slaver 子进程,为每个子进程建立管道通信,封装成 Channel 统一管理。
  • ChooseSlaver:父进程通过轮询算法选择子进程下标,确定任务接收者。
  • SendTask:父进程通过下标找到对应管道,将 Task 任务派发给子进程。
  • DoTask:子进程从管道读取任务,根据任务编号执行对应工作。

五、完整代码呈现

ProcessPool.cc

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/wait.h>

///////////////////////////////子进程要完成的任务/////////////////////////
void SyncDisk()
{
    std::cout << getpid() << ": 刷新数据到磁盘任务" << std::endl;
    sleep(1);
}

void Download()
{
    std::cout << getpid() << ": 下载数据到系统中" << std::endl;
    sleep(1);
}

void PrintLog()
{
    std::cout << getpid() << ": 打印日志到本地" << std::endl;
    sleep(1);
}

void UpdateStatus()
{
    std::cout << getpid() << ": 更新一次用户的状态" << std::endl;
    sleep(1);
}

typedef void (*task_t)(); // 函数指针

task_t tasks[4] = {SyncDisk, Download, PrintLog, UpdateStatus}; // 任务表

///////////////////////////////进程池相关////////////////////////////////
enum
{
    OK = 0,
    PIPE_ERROR,
    FORK_ERROR
};

// 子进程的入口函数 ------ 【修复:补全了read调用】
void DoTask(int fd)
{
    while (true)
    {
        int task_code = 0;
        // 修复:缺失读取管道的代码
        ssize_t n = read(fd, &task_code, sizeof(task_code));

        if (n == sizeof(task_code))
        {
            if (task_code >= 0 && task_code < 4)
            {
                tasks[task_code](); // 执行任务表中的任务
            }
        }
        else if (n == 0)
        {
            // 父进程关闭写端,子进程读到0,退出
            std::cout << getpid() << ": task quit ..." << std::endl;
            break;
        }
        else
        {
            perror("read");
            break;
        }
    }
    close(fd); // 子进程退出前关闭读端
}

const int gprocessnum = 5;
using cb_t = std::function<void(int)>;

class ProcessPool
{
private:
    // 父进程管理"通道"
    class Channel
    {
    public:
        Channel(int wfd, pid_t pid) : _wfd(wfd), _sub_pid(pid)
        {
            _sub_name = "sub-channel-" + std::to_string(_sub_pid);
        }
        ~Channel()
        {
        }
        void Write(int index)
        {
            ssize_t n = write(_wfd, &index, sizeof(index));
            (void)n;
        }
        std::string Name()
        {
            return _sub_name;
        }
        void ClosePipe()
        {
            std::cout << "关闭wfd: " << _wfd << std::endl;
            close(_wfd);
        }
        void Wait()
        {
            pid_t rid = waitpid(_sub_pid, nullptr, 0);
            (void)rid;
            std::cout << "回收子进程: " << _sub_pid << std::endl;
        }
        void PrintInfo()
        {
            printf("wfd: %d, who: %d, channel name: %s\n", _wfd, _sub_pid, _sub_name.c_str());
        }
            // 调试查看所有Slaver进程信息
void Debug(const std::vector<Channel>& channels)
{
    for(const auto& c : channels)
    {
        std::cout << "_cmdfd: " << c._wfd
                  << " _slaverid: " << c._sub_pid
                  << " _processname: " << c._sub_name
                  << std::endl;
    }
}

    private:
        int _wfd;              // 父进程写端fd
        pid_t _sub_pid;        // 子进程PID
        std::string _sub_name; // 通道名字
    };

public:
    ProcessPool()
    {
        srand((unsigned int)time(nullptr) ^ getpid());
    }
    ~ProcessPool() {}
    void Init(cb_t cb)
    {
        CreateProcessChannel(cb);
    }
    void Run()
    {
        // 【修复:打开任务调度逻辑】
        while (true)
        {
            std::cout << "------------------------------------------------" << std::endl;
            // 1. 随机选择一个任务
            int itask = SelectTask();
            std::cout << "选中任务: " << itask << std::endl;

            // 2. 轮询选择一个子进程
            int index = SelectChannel();
            std::cout << "选中进程: " << channels[index].Name() << std::endl;

            // 3. 发送任务
            printf("发送任务 %d to %s\n", itask, channels[index].Name().c_str());
            SendTask2Salver(itask, index);

            sleep(1); // 每秒发一个任务
        }
    }
    void Quit()
    {
        // 正确写法:先全部关闭写端,再统一等待
        for (auto &channel : channels)
        {
            channel.ClosePipe();
        }
        for (auto &channel : channels)
        {
            channel.Wait();
        }
    }
    void Debug()
    {
        for (auto &c : channels)
        {
            c.PrintInfo();
        }
    }

private:
    void SendTask2Salver(int itask, int index)
    {
        if (itask >= 4 || itask < 0)
            return;
        if (index < 0 || index >= channels.size())
            return;

        channels[index].Write(itask);
    }

    int SelectChannel()
    {
        static int index = 0;
        int selected = index;
        index++;
        index %= channels.size();

        return selected;
    }
    int SelectTask()
    {
        int itask = rand() % 4;
        return itask;
    }
    void CreateProcessChannel(cb_t cb)
    {
        for (int i = 0; i < gprocessnum; i++)
        {
            int pipefd[2] = {0};
            int n = pipe(pipefd);
            if (n < 0)
            {
                std::cerr << "pipe create error" << std::endl;
                exit(PIPE_ERROR);
            }
            pid_t id = fork();
            if (id < 0)
            {
                std::cerr << "fork error" << std::endl;
                exit(FORK_ERROR);
            }
            else if (id == 0)
            {
                // 子进程:关闭所有父进程的写端
                if(!channels.empty())
                {
                    for(auto &channel : channels)
                        channel.ClosePipe();
                }

                close(pipefd[1]); // 子进程只保留读端
                cb(pipefd[0]);    // 执行任务循环
                exit(OK);
            }
            else
            {
                // 父进程只保留写端
                close(pipefd[0]);
                channels.emplace_back(pipefd[1], id);
                std::cout << "创建子进程: " << id << " 成功..." << std::endl;
            }
        }
    }


private:
    std::vector<Channel> channels;
};

int main()
{
    // // 1. 初始化进程池
    // ProcessPool pp;
    // pp.Init(DoTask);
    // pp.Debug();

    // // 2. 父进程发送任务
    // pp.Run();

    // // 3. 释放资源
    // pp.Quit();

    // return 0;
  

    
}

makeflie

bash 复制代码
ProcessPool:ProcessPool.cc
	g++ $^ -o $@ -g -std=c++11

.PHONY:clean
clean:
	rm -f ProcessPool

本次分享就到这里结束了,后续会继续更新,感谢阅读!

相关推荐
zhangfeng11332 小时前
部署到服务器上 宝塔系统 使用宝塔在线编辑器 FTP 批量上传 Git 部署 打包上传 codebudyy 编程程序开发
服务器·git·编辑器
WJ.Polar2 小时前
Scapy基本应用
linux·运维·网络·python
lljss20202 小时前
1. NameServer 域名服务器---NS
linux·服务器·前端
萧行之3 小时前
Ubuntu+Windows双系统:解决GRUB不显示Windows启动项、一闪而过问题
linux·windows·ubuntu
数智顾问4 小时前
(123页PPT)华为流程管理体系精髓提炼(附下载方式)
运维·华为
Yupureki4 小时前
《Linux网络编程》5.HTTPS协议
linux·网络·https
网络工程小王5 小时前
【LCEL 链式调用详解】调用篇-2
java·服务器·前端·数据库·人工智能
搬砖的小码农_Sky5 小时前
Linux操作系统:Ubuntu和Debian的区别
linux·ubuntu·debian
江湖有缘5 小时前
基于Ubuntu系统Docker部署Note Mark:从安装到配置全流程
linux·ubuntu·docker