目录

1.项目回顾

2.问题

测试代码

发现的问题

原因

3.改进方法:

方法1: 子进程手动关闭继承自父进程的管道写端

方法2: 让子进程一开始就关闭继承得到的写端,手动close


1.项目回顾

参见OS46.【Linux】进程间通信 管道的应用场景和进程池项目文章

2.问题

进程池项目的init_process_pool其实有点问题

测试代码

main函数手动执行shell命令,查看每个子进程的文件描述符,改成:

int main()
{
    std::vector<channel> channels;
    init_process_pool(channels);
    std::cout<<"所有子进程的文件描述符: "<<std::endl;
    for (auto& obj:channels)
    {
        //等价为ls -l /proc/pid/fd
        std::cout<<obj._childpid<<"子进程的文件描述符为: "<<std::endl;
        std::string str="ls -l /proc/"+std::to_string(obj._childpid)+"/fd";
        system(str.c_str());
    }
    clean_process_pool(channels);
    std::cout<<"父进程退出"<<std::endl;
    return 0;
}

运行结果:

发现的问题

如果仔细看的话,后创建的子进程占用的文件描述符比前创建的子进程占用的要多

如果再仔细看的话,会发现管道文件被继承

原因

之前在OS43.【Linux】进程间通信 管道通信理论部分文章讲过:

父进程创建了子进程,子进程需要继承父进程打开的文件

1.父进程执行init_process_pool,进入for (int i=0;i<max_process_num;i++)循环

2.父进程创建管道后,现在一个创建了2个管道,执行fork()创建子进程,子进程1继承父进程打开的文件,含管道的写端!!!

3.父进程进入else if (fork_ret>0)里面,关闭读端

4.下一次循环父进程再次创建管道,现在一共创建了2个管道,继续执行fork()创建子进程,子进程2继承父进程打开的文件,含管道的写端!!!

如此反复,最后创建的子进程(max_process_num==10):

而且如果clean_process_pool改成下面这样,waitpid会阻塞等待:

void clean_process_pool(std::vector<channel>& channels)
{
    for (auto& obj:channels)
    {
        close(obj._pipefd);
        waitpid(obj._childpid,nullptr,0);
        std::cout<<obj._childpid<<"子进程退出"<<std::endl;
    }
}

运行结果(随机选子进程):

原因:

//......
channels.push_back(channel(pipefd[1],fork_ret,std::string("child process ")+std::to_string(i)));
//......
for (auto& obj:channels)
{
    close(obj._pipefd);
    waitpid(obj._childpid,nullptr,0);
    std::cout<<obj._childpid<<"子进程退出"<<std::endl;
}

由于channels中的元素是尾插进来的,那么范围for从channels[0]一直遍历到channels中的最后一个元素

那么父进程先close第一个创建的子进程的管道的读端,再使用waitpid等待第一个子进程退出

但是有一个问题:

这是所有子进程的fd数组:

        共同特点: 各个子进程的fd=0都是管道的读端,fd=1是标准输出,fd=2是标准错误

        不同点: 各个子进程继承了父进程生成的管道的写端

之前在OS45.【Linux】进程间通信 管道通信代码部分(2)文章说过:

结论: 读写端正常,管道为空,读端会阻塞

对于这个代码:

for (auto& obj:channels)
{
    close(obj._pipefd);
    waitpid(obj._childpid,nullptr,0);
    std::cout<<obj._childpid<<"子进程退出"<<std::endl;
}

虽然关闭了第一个进程的管道写端,但是其余子进程仍然继承了这个写端,也即: 写端没有完全关闭,那么read仍然阻塞,第一个子进程无法退出,那么父进程等待第一个子进程的waitpid也会阻塞!

3.改进方法:

方法1: 子进程手动关闭继承自父进程的管道写端

最后一个创建的子进程所属的管道的写端没有被任何子进程继承,因此关闭最后一个子进程的管道的写端,那么read就不会阻塞,子进程就能正常退出execute_cmd函数的无限循环,执行到init_process_pool的exit(0)了

依据上面这个,稍加处理,倒着关闭子进程即可:

void clean_process_pool(std::vector<channel>& channels)
{
    for (int i=channels.size()-1;i>=0;i--)
    {
        close(channels[i]._pipefd);
        waitpid(channels[i]._childpid,nullptr,0);
        std::cout<<channels[i]._childpid<<"子进程退出"<<std::endl;
    }
}

运行结果:

方法2: 让子进程一开始就关闭继承得到的写端,手动close

使用pipe_write_fd存储父进程打开的写端.那么创建的子进程就能通过遍历pipe_write_fd来关闭之前继承的读端

void init_process_pool(std::vector<channel>& channels)
{
    std::vector<int> pipe_write_fd;
    for (int i=0;i<max_process_num;i++)
    {
        int pipefd[2];
        int pipe_ret=pipe(pipefd);
         if (pipe_ret==-1)
             std::cerr<<"pipe创建管道失败! "<<std::endl;
 
        pid_t fork_ret=fork();       
        if (fork_ret==0)
        {
            //子进程只读管道,关闭写端
            close(pipefd[1]);
            for (auto elem:pipe_write_fd)
                close(elem);
            dup2(pipefd[0],0);
            execute_cmd();
            //执行完后关闭读端
            close(pipefd[0]);
            exit(0);
        }
        else if (fork_ret>0)
        {
            //父进程只写管道,关闭读端
            close(pipefd[0]);
            pipe_write_fd.push_back(pipefd[1]);
            channels.push_back(channel(pipefd[1],fork_ret,std::string("child process ")+std::to_string(i)));
        }
        else//fork_ret==-1
        {
            std::cerr<<"fork创建子进程失败! "<<std::endl;
        }
    }
    //send_cmd不能放循环里面,否则只会创建一个子进程
    send_cmd();
}

其实还有一个方法3,这个下篇文章会重点分析!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐