Linux进程间通信(补充)
匿名管道的使用
- 1.实现父进程控制多个子进程(进程池)
- 2.解析代码
- 2.1.while命令
- 2.2.dup2函数
- 2.3.回收子进程遇到的问题
- 2.4重定向
🌟🌟hello,各位读者大大们你们好呀🌟🌟
🚀🚀系列专栏:【Linux的学习】
📝📝本篇内容:实现父进程控制多个子进程(进程池),解析代码;while命令;dup2函数;回收子进程遇到的问题;重定向
⬆⬆⬆⬆上一篇:进程间通信
💖💖作者简介:轩情吖,请多多指教(> •̀֊•́ ) ̖́-
1.实现父进程控制多个子进程(进程池)
// main.cc
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#include <vector>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"
using namespace std;
const static int pnum = 5; // 子进程的个数
class CommunicationInfo
{
public:
CommunicationInfo(int writefd, int pid)
: _writefd(writefd),
_pid(pid)
{
// 格式为process-0[writefd:pid]
_name += "process-" + to_string(_cnt) + "[" + to_string(_writefd) + ":" + to_string(_pid) + "]";
_cnt++;
}
string processname() const
{
return _name;
}
public:
int _writefd; // 父进程的写描述符
int _pid; // 子进程的pid
string _name; // 子进程的名字
private:
static int _cnt; // 表示第几个进程
};
int CommunicationInfo::_cnt = 0;
void WaitCommand()
{
while (true)
{
Task task;
// 子进程在这读取任务
// 1.读取任务
int command = 0;
int n = read(0, &command, sizeof(command)); // 之前重定向的原因就是为了在这能够更好的统一读取
if (n == sizeof(int))
{
// 读取成功
// 2.执行任务
task.Execute(command);
cerr<<getpid()<<":任务执行完成"<<endl;
}
else if (n == 0)
{
cerr << getpid() << ":父进程让我退出,我就退出了" << endl;
// 写端关闭了,子进程那么也可以关闭读端了
sleep(1);//更好的查看子进程退出时的情况
break;
}
else
{
cerr<<getpid()<<":读取错误"<<endl;
// 读取错误
break;
}
}
}
void CreateProcess(vector<CommunicationInfo> *communicationinfo)
{
vector<int> fds;//当多次循环时,父进程的write描述符会越来越多,这导致创建的子进程也会复制到自己的描述符表中,最后的结果就是子进程无法读到0
//循环创建多个子进程
for (int i = 0; i < pnum; i++)
{
// 1.先创建匿名管道
int fd[2] = {0};
int n = pipe(fd);
assert(n == 0);
// 防止编译时报警告
(void)n;
// 2.创建多个子进程
int pid = fork();
if (pid == 0)
{
//先释放父进程的写描述符
for(auto e:fds)
{
close(e);
}
// 子进程
// 2.1将子进程不需要的描述符关闭
close(fd[1]);
// 2.2将子进程通信的管道重定向到0号标准输入中,这是为了更方便的统一使用0号描述符进行读
dup2(fd[0], 0);
// 2.3获取命令,执行任务
WaitCommand(); // 对于前面没有进行重定向,也可以直接通过传递参数的方式进行通信
// 2.4父进程关闭写端,子进程则关闭读端,然后退出
close(fd[0]);
exit(0);
}
// 3.父进程
// 3.1关闭子进程读的描述符
close(fd[0]);
// 3.2父进程将自己写端的描述符和子进程的pid保存起来,方便后面通信,用先描述再组织的方式
communicationinfo->push_back(CommunicationInfo(fd[1], pid));
fds.push_back(fd[1]);
//--debug--查看我们的文件描述符的值->通过重定向,正常打印用cerr即可
cout<<i+1<<":parent_fd->"<<fd[1]<<",child_fd->"<<fd[0]<<endl;
}
}
int ShowTask()
{
fprintf(stderr,"******************************************\n");
fprintf(stderr,"******* 0.MySQL 1.Reuqest *******\n");
fprintf(stderr,"******* 2.Link 3.exit *******\n");
cerr<<"请选择:";
int command = 0;
cin >> command;
return command;
}
void CtrlProcess(const vector<CommunicationInfo> &communicationinfo)
{
int cnt = 0;
while (true)
{
//1.选择任务
int command = ShowTask();
//用户想要退出
if(command==3)
{
break;
}
if (command < 0 || command > 2)
{
continue;
}
//2.选择子进程
int index=cnt++;
cnt%=communicationinfo.size();
cerr<<"选择了子进程:"<<communicationinfo[index]._name<<"|"<<"处理任务:"<<(command==0?"MySQL":command==1?"Request":"Link")<<endl;
//2.对指定子进程发送任务
write(communicationinfo[index]._writefd, &command, sizeof(command));
sleep(1);
}
}
void WaitAllProcess(const vector<CommunicationInfo>& communicationinfo)//处理所有的子进程的退出
{
//通过关闭写端的描述符,读端就会接收到0返回值,即可使子进程退出
for(int i=0;i<communicationinfo.size();i++)
{
close(communicationinfo[i]._writefd);
waitpid(communicationinfo[i]._pid,nullptr,0);
cerr<<communicationinfo[i]._name<<"已退出"<<endl;
}
}
// 父进程负责写,子进程负责读
int main()
{
vector<CommunicationInfo> communicationinfo; // 存储通信有关的父进程写端描述符和子进程pid
// 1.创建子进程(多进程)
CreateProcess(&communicationinfo);
// 2.显示让用户选择执行哪个任务,按照进程的顺序执行
CtrlProcess(communicationinfo);
//3.回收子进程,执行退出
WaitAllProcess(communicationinfo);
return 0;
}
// Task.hpp
#pragma once
#include <iostream>
#include <vector>
using namespace std;
using func_t = void (*)(void);
// typedef void(*fun_t)(void);
// 假定为任务
void DealMySQL()
{
cerr << "正在处理MySQL..." << endl;
}
void DealRequest()
{
cerr << "正在处理网络请求..." << endl;
}
void DealLink()
{
cerr << "正在处理链接..." << endl;
}
#define MYSQL 0
#define Request 1
#define Link 2
class Task
{
public:
Task()
{
// 构造时,将任务都存入
_task.push_back(DealMySQL);
_task.push_back(DealRequest);
_task.push_back(DealLink);
}
void Execute(int command)
{
// 执行任务
if (command >= 0 && command < _task.size())
{
_task[command](); // 调用函数
}
}
vector<func_t> _task;
};
CtrlProcess:main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf CtrlProcess
2.解析代码
前面已经将所有的代码以及使用的情况都展示了出来,接下来就是要讲一下在写这个代码中,要注意的点
2.1.while命令
在上面使用的展示中,可以看到使用了while命令后,所想要查看的东西会不停地打印出来,它的作用和语言中的while循环是一样的,不停地执行某些操作。
while true;
do
# 执行命令
done;
它的基本格式如上所示,do到done;之间的是所想要执行的命令,要注意,每个命令后面要带上分号
它还有一种写法,就是把true替换成了:,:是一个空命令,这种写法常用于死循环
while :;
do
# 执行命令
done;
2.2.dup2函数
在我们的代码片段中,在处理子进程的读取描述符时,使用了dup2函数
这里使用这个函数的意义虽然在代码注释中写明了,但还是需要提一下
首先dup2函数的作用是简单来说就是将oldfd拷贝到newfd的位置,相等于就是覆盖。
我们来看下面的这幅图
这是子进程拷贝父进程的task_struct内容后的指向,但是为了方便通信,我们把fd[0]重定向到0号描述符中,更加方便后面的统一在匿名管道内读
2.3.回收子进程遇到的问题
可以看到我们的代码中有一个容器是专门用来存储父进程的写描述符,这是因为当多次创建子进程后,我们的父进程中的写描述符越来越多,虽然我们的子进程会在创建好后,会把父进程的写描述符关掉,但是其余和其他子进程通信的写描述符呢?创建第二个子进程时就会把向第一个子进程进行写的描述符给拷贝下来,这就导致了在想要回收释放子进程时,waitpid等不到子进程退出,因为我们的子进程的没有读到0,还有其他进程有让对应的写端还打开着。我们来看图理解
首先看这个打印的debug信息,可以看到我们的子进程的读描述符都是3,这是因为每一次创建好子进程后,父进程都会把不需要的读描述符关闭掉,这样就会导致下一次创建子进程时,pipe函数分配的读描述符还是3,因此拷贝给子进程的读描述符也还是3。这是其中要关注的一点,还有一点就是我们的parent写描述符可以发现一直在增加,不难理解我们的之前所提到的问题(为什么需要一个专门的容器来存储父进程的写描述符)看下面的图演示的更详细
其中除了要注意的是所有的子进程的读描述符是3,还要注意的一个点是绿色的部分。每创建一个子进程,就会把上一个子进程的写描述符通过父进程拷贝下来,这就会导致如下图所示的复杂形式
可以发现,虽然前面的子进程都会产生这个问题,但是我们的创建的最后一个子进程就没问题,它的写端只有父进程有,当最后一个子进程释放后,倒数第二个进程的写描述符也只有父进程有了,一直循环往复到第一个进程。因此我们的代码还有另一种写法,可以倒过来进行等待释放进程。
代码如下:
void WaitAllProcess(const vector<CommunicationInfo>& communicationinfo)//处理所有的子进程的退出
{
//使用这种写法就可以不使用容器保存父进程的写描述符
for(int i=communicationinfo.size()-1;i>=0;i--)
{
close(communicationinfo[i]._writefd);
waitpid(communicationinfo[i]._pid,nullptr,0);
}
}
这里再介绍一种写法,比较粗暴,就是先把父进程所有的写端关掉,再进行等待子进程的退出
void WaitAllProcess(const vector<CommunicationInfo>& communicationinfo)//处理所有的子进程的退出
{
for(int i=0;i<communicationinfo.size();i++)
{
close(communicationinfo[i]._writefd);
}
for(int i=0;i<communicationinfo.size();i++)
{
waitpid(communicationinfo[i]._pid,nullptr,0);
cerr<<communicationinfo[i]._name<<"已退出"<<endl;
}
}
注意这种写法是两个循环,不能放在一起,通过这种写法也可以不用保存父进程的写描述符
它的原理是这样的:通过将父进程的所有的写端全部关闭,这样我们创建的最后一个进程读到了0,那么就会退出;当最后一个子进程退出后,它的文件描述符表中的前一个子进程的写端就会关闭,而父进程也早已把写端关闭了,此时倒数第二个进程也就关闭了,就这样循环往复至所有的子进程退出;最后统一释放子进程。它的思想类似于倒着释放子进程的写法,只不过那个写法是一个一个的关闭父进程的写端,一个一个的释放子进程。
可以发现我们的代码是按照我们的预期来的,我们的子进程退出后,waitpid立马把所有的僵尸进程给回收了。不过这边有个需要注意的点就是使用的命令,不能是ps axj|grep ./CtrlProcess而是ps axj|grep CtrlProcess,要是写错了,就无法正确的查看有没有变成僵尸进程,作者也在这迷糊了一下。
这边也展示一下如果没有使用容器来保存父进程的写描述符,并且使用了一开始代码的运行情况
void WaitAllProcess(const vector<CommunicationInfo>& communicationinfo)//处理所有的子进程的退出
{
//通过关闭写端的描述符,读端就会接收到0返回值,即可使子进程退出--需要创建子进程时使用容器来保存父进程的写描述符的写法
for(int i=0;i<communicationinfo.size();i++)
{
close(communicationinfo[i]._writefd);
waitpid(communicationinfo[i]._pid,nullptr,0);
cerr<<communicationinfo[i]._name<<"已退出"<<endl;
}
}
可以发现我们的代码执行不下去了,直接卡死了,具体的原因也在前面很详细的讲过了,就是因为子进程拷贝了父进程的写描述符使得无法正常读到0,从而导致waitpid无法回收到对应的子进程
2.4重定向
在这份代码中,对于打印的内容使用cerr,而测试的信息使用的时cout,这是为了测试时方便查看父子进程的打开文件描述符,将内容重定向到其他文件中
如下
☞在gitee查看具体的代码
🌸🌸匿名管道的使用的知识大概就讲到这里啦,博主后续会继续更新更多Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪