Linux:基于匿名管道创建出简易进程池(进程间通信五)

我们已经认识到了匿名管道,这篇博客我们不仅要将匿名管道的一些其他知识点讲解,还要写出属于自己的进程池,话不多说,开始我们今天的学习啦~~

1.管道内存上限

我们之前一直说管道并不是无限内存的,而是可以被写满的,那么我们怎么才知道管道究竟有多大内存呢,下面我们来看看

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

void ChildWrite(int wfd)
{
    char c = 0;
    int cnt = 0;
    while (true)
    {
        write(wfd, &c, 1);
        std::cout << "cnt : " << cnt++ << std::endl;
    }
}

void FutherRead(int rfd)
{
    char buffer[1024];
    int cnt = 0;
    while (true)
    {
        //sleep(5);
        buffer[0] = '\0';
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = '\0';
            std::cout << buffer << std::endl;
        }
        /*
        else if (n == 0)
        {
            std::cout << "没有内容 : " << n << std::endl;
            std::cout << "子进程退出,我也退出" << n << std::endl;
            break;
        }
        else
        {
            break;
        }*/
       break;
    }
}

int main()
{
    // 创建管道
    int pfd[2] = {0};
    int n = pipe(pfd);
    if (n == -1)
    {
        std::cerr << "pipe error!" << std::endl;
        return 1;
    }
    std::cout << "pfd[0] : " << pfd[0] << std::endl;
    std::cout << "pfd[1] : " << pfd[1] << std::endl;
    // 一般来说,pfd[0] -> 读端   pfd[1] -> 写端
    // 创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        // 关闭子进程读端
        close(pfd[0]);

        // 子进程向管道写入内容
        ChildWrite(pfd[1]);

        // 关闭子进程写端
        close(pfd[1]);
    }
    // 关闭父进程写端
    close(pfd[1]);

    // 父进程读取管道内容
    //FutherRead(pfd[0]);

    //等待子进程
    waitpid(id, nullptr, 0);

    // 关闭父进程读端
    close(pfd[0]);
    return 0;
}

这个代码我们只写不读,且一个一个字符写入,同时让cnt++,发现到65535的时候(即写入了65536个字节)时,管道被写满,而65535 / 1024 = 64,所以在我的系统下,管道内存为64KB

2.进程池

1.什么是进程池

如图,我们创建一个父进程,父进程创建了很多通道,之后fork子进程,关闭父进程读端与子进程写端,那么此时我们只需要写入code(任务码),就可以控制不同的子进程来完成我们想完成的功能,这个就叫做进程池,就比如我们写的string,当size满了的时候,我们不是让capacity一个一个加上去的,而是直接乘以1.5or2倍,这样可以提高效率,随时可以使用--所以进程池的优点:提前预分配资源,避免频繁创建 / 销毁的开销,实现资源复用、提升效率

2.操作

操作 目的 & 原理
父进程创建多个管道 为每个子进程分配专属单向通信通道(一对一管道),用于父进程向子进程发送「任务码」(比如 1 = 执行计算、2 = 执行文件读写、3 = 执行网络请求);
fork 多个子进程 预创建一批子进程(进程池),避免每次有任务才 fork(fork 进程的开销极大:需要复制进程地址空间、页表、文件描述符等);
父关读端、子关写端 确保每个管道是「父写、子读」的单向通信,避免同一管道双向读写导致数据混乱,同时符合管道 "半双工" 的特性;
父写任务码→子执行 子进程启动后,阻塞在管道的read()调用上(等待任务);父进程根据需求,向对应管道写入任务码,子进程拿到后执行指定功能,执行完继续阻塞等待下一个任务 ------ 这就是进程池 "随时可用、复用进程" 的核心。

3.实现

Makefile:
bash 复制代码
processpool : Main.cc
	g++ $^ -o $@ -std=c++11
.PHONEY : clean
clean :
	rm -f processpool

很平常的Makefile实现,这个就不详细讲解啦

ProcessPool.hpp:
cpp 复制代码
#include <iostream>
#include <string>
#include <cstdlib>
#include <vector>
#include <unistd.h>
#include <sys/wait.h>
#include "Task.hpp"

class Channel // 通道
{
private:
    int _wfd;
    int _id;
    std::string _name;

public:
    Channel(int wfd, int id)
        : _wfd(wfd), _id(id)
    {
        _name = "channel -- " + std::to_string(_wfd) + " -- " + std::to_string(_id);
    }

    ~Channel()
    {
    }

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

    void Send(int taskcode)
    {
        int n = write(_wfd, &taskcode, sizeof(taskcode));
        (void)n;
    }

    void CloseFatherProcess()
    {
        std::cout << "父进程写端 : " << _wfd << " 关闭" << std::endl;
        close(_wfd);
    }

    void WaitChildProcess()
    {
        pid_t rid = waitpid(_id, nullptr, 0);
        (void)rid;
    }
};

class ChannelManager // 管理通道
{
private:
    std::vector<Channel> _channels;
    int _next;
public:
    ChannelManager()
        : _next(0)
    {
    }
    ~ChannelManager()
    {
    }

    void Insert(int wfd, pid_t id)
    {
        _channels.push_back({wfd, id});
    }

    void Print()
    {
        for (auto &e : _channels)
        {
            std::cout << e.Name() << std::endl;
        }
    }

    Channel& Select()
    {
        auto& e = _channels[_next];       
        _next++;
        _next %= _channels.size();
        return e;
    }

    void CloseSubFatherProcess()
    {
        for (auto& e : _channels)
        {
            e.CloseFatherProcess();
        }
    }

    void WaitSubChildProcess()
    {
        for (auto& e : _channels)
        {
            e.WaitChildProcess();
        }
    }
};

class ProcessPool
{
public:
    ProcessPool(int num = 5)
        : _process_num(num)
    {
        _tm.Register(PrintLog);
        _tm.Register(DownLoad);
        _tm.Register(UpLoad);
    }

    ~ProcessPool()
    {
    }

    void Work(int rfd)
    {
        /*while (true)
        {
            std::cout << "我是子进程, 我的rfd : " << rfd << std::endl;
            sleep(5);
        }*/
        while (true)
        {
            int taskcode = 0;
            ssize_t n = read(rfd, &taskcode, sizeof(taskcode));
            if (n > 0)
            {
                if (n != sizeof(taskcode))
                    continue;
                std::cout << "子进程[" << getpid() << "]收到一个任务码 : " << taskcode << std::endl;
                _tm.Execute(taskcode);
            }
            else if (n == 0)
            {
                std::cout << "子进程[" << getpid() << "]退出" << std::endl;
                break;
            }
            else
            {
                std::cout << "读取错误" << std::endl;
                break;
            }
        }
    }

    // 创建(初始化)
    bool Start()
    {
        for (int i = 0; i < _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]);
                Work(pipefd[0]);
                exit(0);
            }
            else
            {
                // 父进程
                // 关闭父进程读端
                close(pipefd[0]);
                _cm.Insert(pipefd[1], id);
            }
        }
        return true;
    }

    void DeBug()
    {
        _cm.Print();
    }

    void Run()
    {
        //随机选择一个任务
        int taskcode = _tm.Code();
        //轮询选择一个子进程来执行任务
        auto& e = _cm.Select();
        std::cout << "选择了一个子进程 : " << e.Name() << std::endl;
        //发射任务
        std::cout << "发射了一个任务码 : " << taskcode << std::endl;
        e.Send(taskcode);
    }

    void Stop()
    {
        //关闭所有父进程写端
        _cm.CloseSubFatherProcess();
        //等待所有子进程
        _cm.WaitSubChildProcess();
    }
private:
    ChannelManager _cm;
    TaskManager _tm;
    int _process_num;
};
1. Channel 核心
  • 成员:_wfd(父进程管道写端)、_child_id(对应子进程 PID)------ 这是父进程管理子进程的两个核心标识;
  • 核心方法:Send(发任务码)、CloseWriteFd(关写端)、WaitChild(等子进程)------ 封装单个子进程的通信和生命周期管理。
2. ChannelManager 核心
  • 容器:vector<Channel> _channels ------ 批量管理所有子进程的 Channel,替代逐个维护的繁琐;
  • 核心逻辑:SelectChannel 轮询算法 ------ 保证任务均匀分发给每个子进程,体现 "池化复用" 思想。
3. ProcessPool 核心
  • 启动(Start):循环创建管道 + fork 子进程,严格遵循 "父关读端、子关写端" 的管道单向通信规则;
  • 运行(RunTask):整合 "任务生成 + 通道选择 + 任务发送",体现进程池 "主进程分发、子进程执行" 的核心模式;
  • 停止(Stop):先关所有写端(触发子进程 EOF),再 waitpid 等待 ------ 完整的资源回收逻辑,避免僵尸进程。
Main.cc:
cpp 复制代码
#include "ProcessPool.hpp"
int main()
{
    //初始
    ProcessPool p(5);

    //开始
    p.Start();

    //调试
    //p.DeBug();

    //运行
    int cnt = 10;
    while (cnt--)
    {       
        //选择一个通道
        p.Run();
        sleep(1);
        std::cout << "---------------------------" << std::endl;
    }

    //结束
    p.Stop();
    return 0;
}
Task.hpp:
cpp 复制代码
#include <iostream>
#include <vector>
#include <ctime>

void PrintLog()
{
    std::cout << "我是一个打印日志的任务" << std::endl;
}

void DownLoad()
{
    std::cout << "我是一个下载的任务" << std::endl;
}

void UpLoad()
{
    std::cout << "我是一个上传的任务" << std::endl;
}

typedef void (*task_t)();

class TaskManager
{
public:
    TaskManager()
    {
        srand(time(nullptr));
    }

    void Register(task_t t)
    {
        _tasks.push_back(t);
    }

    int Code()
    {
        return rand() % _tasks.size();
    }

    void Execute(int taskcode)
    {
        if (taskcode >= 0 && taskcode < _tasks.size())
        {
            _tasks[taskcode]();
        }
    }

    ~TaskManager()
    {}
private:
    std::vector<task_t> _tasks;
};

这就是基于匿名管道创建出的内存池啦,但是这个内存池有一个我们自己引发的Bug,你可以找到吗??找不到没关系,我们下篇博客将会为你解答,因为这个Bug非常的深,所以值得一探~~

相关推荐
我送炭你添花11 分钟前
树莓派部署 GenieACS 作为终端TR-069 ACS(自动配置服务器)的详细规划方案
运维·服务器·网络协议
m0_7360348512 分钟前
1.27笔记
linux·服务器·笔记
华农第一蒟蒻18 分钟前
一次服务器CPU飙升的排查与解决
java·运维·服务器·spring boot·arthas
NGINX开源社区19 分钟前
借助 Okta 和 NGINX Ingress Controller 实现 K8s OpenID Connect 身份验证
运维·nginx·kubernetes
旖旎夜光32 分钟前
Linux(12)(下)
linux·网络
郝亚军44 分钟前
如何在windows11和Ubuntu linux之间互传文件
linux·运维·ubuntu
ghostmen1 小时前
Kuboard 离线安装与 K3s 集群绑定完整指南
linux·kuboard·k3s
j_xxx404_1 小时前
Linux:进程状态
linux·运维·服务器
济6171 小时前
linux 系统移植(第二十三期)---- 进一步完善BusyBox构建的根文件系统---- Ubuntu20.04
linux·运维·服务器
程序员 _孜然1 小时前
openkylin、ubuntu等系统实现串口自动登录
linux·运维·ubuntu