关于【进程池阻塞 + 子进程未回收问题】

续接上文:进程间通信(二):实现一个高可用的进程池-CSDN博客

目录

一、先看现象:两个核心问题

二、核心原因:文件描述符泄漏(管道读端没关干净)

[1. 管道的核心规则回顾](#1. 管道的核心规则回顾)

[2. 后果:管道写端永远关不完](#2. 后果:管道写端永远关不完)

三、原理图解:为什么子进程会继承父进程的文件描述符

四、为什么子进程没被回收?

五、修复方案

[5.1 倒着关](#5.1 倒着关)

[5.2 在子进程中关闭所有继承的管道写端](#5.2 在子进程中关闭所有继承的管道写端)


这个关于进程池的demo ,有一个致命的问题:典型的「文件描述符泄漏导致管道读端未关闭 → 子进程阻塞 → 僵尸进程 / 无法回收」问题

一、先看现象:两个核心问题

  • 进程阻塞 :子进程卡在 read 上,主进程也卡在 waitpid 上,程序无法正常结束
  • 子进程未回收ps ajx | grep ProcessPool 看到 6 个进程(1 个父进程 + 5 个子进程),说明子进程退出后变成了僵尸进程,父进程没回收成功

二、核心原因:文件描述符泄漏(管道读端没关干净)

1. 管道的核心规则回顾

管道的 read 行为由所有写端是否关闭决定:

  • 只要还有任意一个写端文件描述符 没被关闭,read 就会一直阻塞,不会返回 0(EOF)
  • 只有当所有写端都关闭 时,read 才会返回 0,子进程才会退出循环

ProcessPool::Start() 中,创建子进程和管道:

复制代码
for (int i = 0; i < _process_num; i++)
{
    int pipefd[2] = {0};
    pipe(pipefd);
    pid_t subid = fork();
    if (subid == 0)
    {
        close(pipefd[1]); // 子进程关写端 
        Work(pipefd[0]);  // 子进程阻塞在 read(pipefd[0])
        close(pipefd[0]);
        exit(0);
    }
    else
    {
        close(pipefd[0]); // 父进程关读端 
        _cm.Insert(pipefd[1], subid); // 父进程持有写端
    }
}

问题出在:子进程会继承父进程之前创建的所有管道文件描述符!

  • 第 1 次循环:父进程创建管道 1 → fork 子进程 1 → 子进程 1 继承管道 1 的读 / 写端
  • 第 2 次循环:父进程创建管道 2 → fork 子进程 2 → 子进程 2继承管道 1 的写端 + 管道 2 的读 / 写端
  • 第 3 次循环:父进程创建管道 3 → fork 子进程 3 → 子进程 3继承管道 1 的写端 + 管道 2 的写端 + 管道 3 的读 / 写端
  • ... 以此类推,第 5 个子进程会继承前 4 个管道的所有写端

2. 后果:管道写端永远关不完

当你调用 Stop() 关闭父进程持有的所有写端时:

  • 父进程的写端都关了,但子进程手里还握着之前管道的写端
  • 对管道 1 来说:子进程 2~5 都持有它的写端 → 管道 1 的写端总数 > 0
  • 子进程 1 调用 read(pipefd[0]) 时,发现还有写端没关 → 一直阻塞,不会退出
  • 父进程调用 waitpid 时,子进程还活着 → 主进程也阻塞
  • 最终:所有子进程都卡在 read,父进程卡在 waitpid,程序僵死

三、原理图解:为什么子进程会继承父进程的文件描述符

四、为什么子进程没被回收?

因为子进程根本没退出

  • 子进程阻塞在 read,没有执行 exit(0)
  • 父进程调用 waitpid 时,子进程还在运行 → waitpid 会一直阻塞
  • 只有当子进程真正退出后,父进程才能回收它,否则就会一直等下去

如果子进程意外退出,父进程没回收,才会变成僵尸进程 (Z + 状态),这里是子进程活着,父进程阻塞,本质是死锁。

五、修复方案

5.1 倒着关

5.2 在子进程中关闭所有继承的管道写端

复制代码
#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__

#include <iostream>
#include <vector>
#include <unistd.h>
#include <cstdlib>
#include <sys/wait.h>
#include "Task.hpp"

// 先描述
class Channel
{
public:
    Channel(int fd, pid_t id) : _wfd(fd), _subid(id)
    {
        _name = "channel-" + std::to_string(_wfd) + "-" + std::to_string(_subid);
    }
    ~Channel()
    {
    }
    void Send(int code)
    {
        int n = write(_wfd, &code, sizeof(code));
        (void)n; // n被定义,如果未使用的时候,会出现告警
    }
    void Close()
    {
        close(_wfd);
    }
    void Wait()
    {
        pid_t rid = waitpid(_subid, nullptr, 0);
        (void)rid;
    }
    int Fd() { return _wfd; }
    pid_t SubId() { return _subid; }
    std::string Name() { return _name; }

private:
    int _wfd;
    pid_t _subid; // 想知道这个信道是给哪一个子进程的
    std::string _name;
};

// 再组织
class ChannelManager
{
public:
    ChannelManager() : _next(0) {}
    void Insert(int wfd, pid_t subid)
    {
        _channels.emplace_back(wfd, subid);
        // 不用再构建临时变量
        //  Channel c(wfd,subid);
        //  _channels.push_back(std::move(c));
    }
    Channel &Select()
    {
        auto &c = _channels[_next];
        _next++;
        // 下一次访问的时候就去访问下一个了
        _next %= _channels.size();
        return c;
    }

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

    void CloseAll()
    {
        for(auto & channel : _channels)
        {
            channel.Close();
        }
    }
    void StopSubProcess()
    {
        for (auto &channel : _channels)
        {
            channel.Close();
            std::cout << "关闭:" << channel.Name() << std::endl;
        }
    }
    void WaitSubProcess()
    {
        for (auto &channel : _channels)
        {
            channel.Wait();
            std::cout << "回收:" << channel.Name() << std::endl;
        }
    }
    void CloseAndWait()
    {
        
        //解决方案2:
        for(auto &channel : _channels)
        {
            channel.Close();
            std::cout << "关闭:" << channel.Name() << std::endl;
            channel.Wait();
            std::cout << "回收:" << channel.Name() << std::endl;
        }

        // 解决方案1:倒着关
        // for (int i = _channels.size() - 1; i >= 0; i--)
        // {
        //     _channels[i].Close();
        //     std::cout << "关闭:" << _channels[i].Name() << std::endl;
        //     _channels[i].Wait();
        //     std::cout << "回收:" << _channels[i].Name() << std::endl;
        // }

    }

    ~ChannelManager() {}

private:
    std::vector<Channel> _channels;
    int _next;
};
const int gdefaultnum = 5;

class ProcessPool
{
public:
    ProcessPool(int num) : _process_num(num)
    {
        _tm.Register(PrintLog);
        _tm.Register(Downlode);
        _tm.Register(Uplode);
    }

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

    bool Start()
    {

        for (int i = 0; i < _process_num; i++)
        {
            // 1.创建管道
            int pipefd[2] = {0};
            int n = pipe(pipefd);
            if (n < 0)
                return false;

            // 2.创建子进程
            pid_t subid = fork();
            if (subid < 0)
                return false;
            else if (subid == 0)
            {
                // 子进程
                //让子进程关闭自己继承下来的 他的哥哥进程的w端关闭就行了
                //for(std::vector<Channel> _channels) _channels.close();
                _cm.CloseAll();
                // 3.关闭不需要的文件描述符
                close(pipefd[1]);
                Work(pipefd[0]);
                close(pipefd[0]);
                exit(0);
            }
            else
            {
                // 父进程
                // 3.关闭不需要的文件描述符
                close(pipefd[0]); // 写端: pipefd[1];
                _cm.Insert(pipefd[1], subid);
                // wfd,subid,
            }
        }
        return true;
    }

    void Debug()
    {
        _cm.PrintfChannel();
    }

    void Run()
    {
        // 1.选择了一个任务
        int taskcode = _tm.Code();

        // 2.选择一个信道【子进程】,负载均衡的选择一个子进程,完成任务
        auto &c = _cm.Select();
        std::cout << "选择了一个子进程" << c.Name() << std::endl;

        // 3.发送任务
        c.Send(taskcode);
        std::cout << "发送了一个任务码" << c.Name() << std::endl;
    }

    void Stop()
    {
        _cm.CloseAndWait();
        // // 关闭父进程所有的wfd即可
        // _cm.StopSubProcess();
        // // 回收所有子进程
        // _cm.WaitSubProcess();
    }
    ~ProcessPool() {}

private:
    ChannelManager _cm;
    int _process_num; // 创建对应的进程个数
    TaskManager _tm;  //
};

#endif
相关推荐
C澒2 小时前
PC 桌面富应用:速分客户端
前端·c++·electron·web app
深邃-2 小时前
数据结构-双向链表
c语言·开发语言·数据结构·c++·算法·链表·html5
2401_878530212 小时前
分布式任务调度系统
开发语言·c++·算法
wzhidev2 小时前
05、Python流程控制与函数定义:从调试现场到工程实践
linux·网络·python
艾莉丝努力练剑2 小时前
【Linux:文件】文件基础IO进阶
linux·运维·服务器·c语言·网络·c++·centos
艾莉丝努力练剑2 小时前
【MYSQL】MYSQL学习的一大重点:表的约束
linux·运维·服务器·开发语言·数据库·学习·mysql
程序猿编码2 小时前
基于ncurses的TCP连接可视化与重置工具:原理与实现(C/C++代码实现)
linux·c语言·网络·c++·tcp/ip
nunca_te_rindas2 小时前
算法刷体小结汇总(C/C++)20260328
c语言·c++·算法
Sunshine for you2 小时前
高性能压缩库实现
开发语言·c++·算法