OS47.【Linux】进程间通信 进程池项目的改进版(1)
本文分析了Linux进程池项目中子进程继承父进程管道写端导致的问题及改进方法。测试发现后创建的子进程会继承更多文件描述符,包括之前创建的管道写端,导致clean_process_pool时waitpid阻塞。提出了两种解决方案:1)倒序关闭子进程管道写端,使read不再阻塞;2)在子进程中主动关闭继承的所有写端。这两种方法都有效解决了进程池中子进程无法正常退出的问题,为进程间通信的管道管理提供了优
目录
方法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,这个下篇文章会重点分析!
更多推荐


所有评论(0)