前言
上篇博客初步学习了匿名管道的周边知识和使用,本篇文章将基于这些知识,实现一下进程间通信
话不多说,马上开始今天的内容
文章目录
- 前言
- 一. 大体框架
- 二. 分配任务
- 三. 创建控制模块
- 四. 开始通信
- 五. 关闭程序
- 六. 完整代码
- 结束语
一. 大体框架
我们创建一个进程,以这个进程为父进程,创建5个子进程和对应的管道,父进程进行写操作,子进程进行读操作,根据父进程写入的数据,进行相应的动作。
部分细节,变量在单独部分没有展示,可以查看完整代码部分
二. 分配任务
这个部分我们可以编写在Task.hpp
#pragma once
#include<iostream>
#include<vector>
#include<unistd.h>
//函数指针
typedef void(*fun_t)();
void PrintLog()
{
std::cout<<"pid:"<<getpid()<<" 打印日志任务,正在被执行"<<std::endl;
}
void InsertMySQL()
{
std::cout<<"pid:"<<getpid()<<" 执行数据库任务,正在被执行"<<std::endl;
}
void NetRequest()
{
std::cout<<"pid:"<<getpid()<<" 执行网络请求任务,正在被执行"<<std::endl;
}
//数字对应的指令
#define COMMAND_LOG 1
#define COMMAND_MYSQL 2
#define COMMAND_REQUEST 3
class Task
{
public:
Task()
{
funcs.push_back(PrintLog);
funcs.push_back(InsertMySQL);
funcs.push_back(NetRequest);
}
void Execute(int command)
{
//commend是执行第几个命令
if(command>=0&&command<funcs.size())
{
funcs[command]();
}
}
~Task()
{
}
public:
std::vector<fun_t>funcs;
};
我们使用
函数指针
的方式。定义一个Task类,内部有一个存储函数指针的vector
,并且我们在其构造函数中,就添加以上三个任务。然后还有一个接口,通过传入一个数字command
,然后执行对应vector里第几个的任务。
三. 创建控制模块
从这部分开始,我们编写在ctrlProcess.cpp中
匿名管道用于具有
“亲戚关系”
的进程,常用的就是父子进程
,而我们需要让子进程有父进程创建的管道
,所以需要先创建管道,然后再创建子进程
,这样因为写时拷贝,子进程就会继承父进程的部分进程信息,当然包括文件描述符。
然后成功创建一个子进程后,父进程要先关闭当前管道的读端,这样不会影响下一次的创建。
同时,因为我们要创建多个子进程,而每次创建的管道,都使用同一个数组,存储其读写文件描述符,所以我们使用一个类
,内部存储子进程的名称,子进程的pid,子进程对应的管道的写端
,因为父进程需要向该写端写入数据。这样也符合,先描述,再组织
的思想。
我们将这部分封装成一个函数,将每一步操作封装起来,这样也可以让代码更有逻辑性,可读性更好。
子进程的waitCommand函数将在下一部分讲解,因为waitCommand是读取管道数据,属于通信部分。
// 用于存储子进程的pid和相对应管道的文件描述符
class EndPoint
{
//计数器
static int number;
public:
EndPoint(pid_t child_id, int write_id)
: _child_id(child_id), _write_id(write_id)
{
//进程名的格式:process-0[pid,fd]
char namebuffer[64];
snprintf(namebuffer,sizeof(namebuffer),"process-%d[%d:%d]",number++,_child_id,_write_id);
processname=namebuffer;
}
std::string name() const
{
return processname;
}
~EndPoint()
{
}
public:
pid_t _child_id; // 子进程的pid
int _write_id; // 相对应管道的文件描述符
std::string processname; //进程的名字
};
int EndPoint::number=0;
// 构建控制结构,父进程写入,子进程读取
void createProcesses(vector<EndPoint> &end_points)
{
// 1.先进行构建控制结构:父进程进行写入,子进程读取
for (int i = 0; i < gnum; i++)
{
// 1.1 创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0);
(void )n; // 防止release版本将为使用的变量删除
// 1.2 创建子进程
pid_t id = fork();
assert(id != -1);
(void )id; // 防止release版本将为使用的变量删除
if (id == 0)
{
// 子进程
//将从父进程那继承来的其他进程的读端关闭
cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端:";
for(const auto&ep:end_points)
{
cout<<ep._write_id<<" ";
close(ep._write_id);
}
cout<<endl;
// 关闭自己的写端
close(pipefd[1]);
// 1.3 通信
// 子进程读取“指令”,都从标准输出中获取
// 将管道的读重定向到标准输出中
dup2(pipefd[0], 0);
// 1.4 子进程开始等待命令。
WaitCommand();
// 关闭读端然后退出子进程
close(pipefd[0]);
exit(0);
}
// 到这的一定是父进程
// 关闭读端
close(pipefd[0]);
// 将新创建的子进程的fd和管道的写的文件描述符存储起来
end_points.push_back(EndPoint(id, pipefd[1]));
}
}
但这里,我们还需要注意一个事项,就是子进程创建成功后,还有一个循环
//将从父进程那继承来的其他进程的读端关闭
cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端"<<endl;
for(const auto&ep:end_points)
{
cout<<ep._write_id<<" ";
close(ep._write_id);
}
cout<<endl;
这一步我们在最后的退出程序再详细讲解。
四. 开始通信
我们现在已经创建好父子进程,并且还存储好了子进程的pid和对应管道的写端的文件描述符。
接下来,我们就可以开始通信了。
父进程往管道写入
//展示面板
int ShowBoard()
{
cout<<endl;
cout<<"#######################################"<<endl;
cout<<"#######################################"<<endl;
cout<<"# 0. 执行日志任务 1. 执行数据库任务 #"<<endl;
cout<<"# 2. 执行请求任务 3. 退出 #"<<endl;
cout<<"#######################################"<<endl;
cout<<"#######################################"<<endl;
cout<<"请选择# ";
int command=0;
std::cin>>command;
return command;
}
//父进程写入
void ctrlProcess(const vector<EndPoint>&end_points)
{
// 父进程开始发布命令
int cnt=0;
while(true)
{
//1. 选择任务
int command=ShowBoard();
//为3就退出
if(command==3)
{
break;
}
if(command<0&&command>2)
{
cout<<"输入有误,请重新输入"<<endl;
continue;
}
//2. 按顺序给子进程派发任务
int indix=cnt++;
cnt%=end_points.size();
cout<<"你选择了进程:"<<end_points[indix].name()<<" | 处理"<<command<<"号任务"<<endl;
//4. 下发任务
write(end_points[indix]._write_id,&command,sizeof(command));
sleep(1);
}
}
子进程读取管道,获取数据,并执行相应任务
// 子进程读数据
void WaitCommand()
{
while(true)
{
int command;
//一次读取4个字节
int n = read(0, &command, sizeof(int));
//成功读取4字节,就执行对应的命令
if (n == sizeof(int))
{
t.Execute(command);
cout<<endl;
}
else if (n == 0)
{
//相对应的写端关闭了
cout<<"父进程让我退出,我就退出了"<<getpid()<<endl;
break;
}
}
}
五. 关闭程序
在关闭程序时,我们要结束子进程,只需要将对应的写端关闭,子进程读取到文件尾,就会自动退出循环,结束进程。
然后父进程还需要回收子进程的僵尸状态。
不过这里我们要讲解第二步. 分配任务时的一个疑问
为什么需要下面这个循环
//将从父进程那继承来的其他进程的读端关闭
cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端"<<endl;
for(const auto&ep:end_points)
{
cout<<ep._write_id<<" ";
close(ep._write_id);
}
cout<<endl;
我们知道,
子进程会继承父进程所有的文件描述符
,那么当我们创建第二个子进程前,父进程是有第一个子进程管道的写端的。所以第二个子进程同样会继承这个文件描述符
,这样就导致,我们创建越多的子进程,前面的子进程的管道的链接数越多
,引用计数不为1
,这样顺序一个一个关闭时
,无法关闭子进程的写端,子进程就不会读到文件尾,而是处于阻塞状态
,不会退出进程,父进程就回收不到子进程了。
所以我们有三种解决这个问题的办法
解决方法一:
我们可以一次性将所有子进程的写端都关闭,再回收子进程
//1.关闭子进程的写端
for(const auto&ep:end_points)
{
close(ep._write_id);
}
sleep(5);
cout<<"父进程让所有的子进程都退出"<<endl;
//2. 父进程回收子进程的僵尸状态
for(const auto&ep:end_points)
{
waitpid(ep._child_id,nullptr,0);
}
cout<<"父进程回收了所有的子进程"<<endl;
sleep(5);
解决方法二:
我们可以倒着关闭子进程的写端,然后再回收子进程
//倒着关闭子进程的写端,再回收子进程
for(int i=end_points.size()-1;i>=0;i--)
{
close(end_points[i]._write_id);
cout<<"父进程让"<<end_points[i]._child_id<<"子进程退出"<<endl;
waitpid(end_points[i]._child_id,nullptr,0);
cout<<"父进程回收了"<<end_points[i]._child_id<<"子进程"<<endl;
cout<<endl;
sleep(1);
}
解决方法三:
在新的子进程创建后,关闭从父进程那继承的其他子进程的管道的文件描述符,就是那个循环,
然后我们就可以顺序的,一个一个关闭写端并回收了
//将从父进程那继承来的其他进程的读端关闭
cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端"<<endl;
for(const auto&ep:end_points)
{
cout<<ep._write_id<<" ";
close(ep._write_id);
}
cout<<endl;
//一个一个退出
for(const auto&ep:end_points)
{
close(ep._write_id);
cout<<"父进程让"<<ep._child_id<<"子进程退出"<<endl;
waitpid(ep._child_id,nullptr,0);
cout<<"父进程回收了"<<ep._child_id<<"子进程"<<endl;
cout<<endl;
sleep(1);
}
六. 完整代码
Task.hpp
#pragma once
#include<iostream>
#include<vector>
#include<unistd.h>
//函数指针
typedef void(*fun_t)();
void PrintLog()
{
std::cout<<"pid:"<<getpid()<<" 打印日志任务,正在被执行"<<std::endl;
}
void InsertMySQL()
{
std::cout<<"pid:"<<getpid()<<" 执行数据库任务,正在被执行"<<std::endl;
}
void NetRequest()
{
std::cout<<"pid:"<<getpid()<<" 执行网络请求任务,正在被执行"<<std::endl;
}
//数字对应的指令
#define COMMAND_LOG 1
#define COMMAND_MYSQL 2
#define COMMAND_REQUEST 3
class Task
{
public:
Task()
{
funcs.push_back(PrintLog);
funcs.push_back(InsertMySQL);
funcs.push_back(NetRequest);
}
void Execute(int command)
{
//commend是执行第几个命令
if(command>=0&&command<funcs.size())
{
funcs[command]();
}
}
~Task()
{
}
public:
std::vector<fun_t>funcs;
};
ctrlProcess.cpp
#include <iostream>
#include <string>
#include <cassert>
#include <unistd.h>
#include <vector>
#include <sys/types.h>
#include <sys/wait.h>
#include "task.hpp"
using namespace std;
const int gnum = 3;
Task t; // 定义为全局的
// 用于存储子进程的pid和相对应管道的文件描述符
class EndPoint
{
//计数器
static int number;
public:
EndPoint(pid_t child_id, int write_id)
: _child_id(child_id), _write_id(write_id)
{
//进程名的格式:process-0[pid,fd]
char namebuffer[64];
snprintf(namebuffer,sizeof(namebuffer),"process-%d[%d:%d]",number++,_child_id,_write_id);
processname=namebuffer;
}
std::string name() const
{
return processname;
}
~EndPoint()
{
}
public:
pid_t _child_id; // 子进程的pid
int _write_id; // 相对应管道的文件描述符
std::string processname; //进程的名字
};
int EndPoint::number=0;
// 子进程读数据
void WaitCommand()
{
while(true)
{
int command;
//一次读取4个字节
int n = read(0, &command, sizeof(int));
//成功读取4字节,就执行对应的命令
if (n == sizeof(int))
{
t.Execute(command);
cout<<endl;
}
else if (n == 0)
{
//相对应的写端关闭了
cout<<"父进程让我退出,我就退出了"<<getpid()<<endl;
break;
}
}
}
// 构建控制结构,父进程写入,子进程读取
void createProcesses(vector<EndPoint> &end_points)
{
// 1.先进行构建控制结构:父进程进行写入,子进程读取
for (int i = 0; i < gnum; i++)
{
// 1.1 创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0);
(void )n; // 防止release版本将为使用的变量删除
// 1.2 创建子进程
pid_t id = fork();
assert(id != -1);
(void )id; // 防止release版本将为使用的变量删除
if (id == 0)
{
// 子进程
//将从父进程那继承来的其他进程的读端关闭
cout<<getpid()<<" 子进程关闭了继承自父进程的其他子进程的写端:";
for(const auto&ep:end_points)
{
cout<<ep._write_id<<" ";
close(ep._write_id);
}
cout<<endl;
// 关闭自己的写端
close(pipefd[1]);
// 1.3 通信
// 子进程读取“指令”,都从标准输出中获取
// 将管道的读重定向到标准输出中
dup2(pipefd[0], 0);
// 1.4 子进程开始等待命令。
WaitCommand();
// 关闭读端然后退出子进程
close(pipefd[0]);
exit(0);
}
// 到这的一定是父进程
// 关闭读端
close(pipefd[0]);
// 将新创建的子进程的fd和管道的写的文件描述符存储起来
end_points.push_back(EndPoint(id, pipefd[1]));
}
}
int ShowBoard()
{
cout<<endl;
cout<<"#######################################"<<endl;
cout<<"#######################################"<<endl;
cout<<"# 0. 执行日志任务 1. 执行数据库任务 #"<<endl;
cout<<"# 2. 执行请求任务 3. 退出 #"<<endl;
cout<<"#######################################"<<endl;
cout<<"#######################################"<<endl;
cout<<"请选择# ";
int command=0;
std::cin>>command;
return command;
}
void ctrlProcess(const vector<EndPoint>&end_points)
{
// 父进程开始发布命令
int cnt=0;
while(true)
{
//1. 选择任务
int command=ShowBoard();
//为3就退出
if(command==3)
{
break;
}
if(command<0&&command>2)
{
cout<<"输入有误,请重新输入"<<endl;
continue;
}
//2. 按顺序给子进程派发任务
int indix=cnt++;
cnt%=end_points.size();
cout<<"你选择了进程:"<<end_points[indix].name()<<" | 处理"<<command<<"号任务"<<endl;
//4. 下发任务
write(end_points[indix]._write_id,&command,sizeof(command));
sleep(1);
}
}
//回收子进程
void waitProcess(vector<EndPoint>&end_points)
{
//如果我们创建管道后,直接再创建子进程,那么子进程将继承父进程的所有文件描述符,
//后创建的子进程会保留指向先创建的管道的读写文件描述符
//所以顺序同时关闭子进程的写端和回收僵尸进程,其实并没有关闭子进程的写端,因为此时其引用计数仍>0
// //这种写法会在waitpid时堵塞,因为子进程的写端还没有关闭
// //所以子进程的读端处于堵塞状态,不会退出
// for(const auto&ep:end_points)
// {
// close(ep._write_id);
// cout<<"父进程关闭了"<<ep._child_id<<"的写端"<<endl;
// waitpid(ep._child_id,nullptr,0);
// cout<<"父进程回收了"<<ep._child_id<<endl;
// }
//解决方法一:倒着关闭写端,回收僵尸进程
//解决方法二:在创建新的子进程后,子进程关闭从父进程那边继承的其他子进程的读写端
//我们只需要让父进程关闭子进程的写端,子进程的读端会读到文件尾,然后自己就退了。
// //1.关闭子进程的写端
// for(const auto&ep:end_points)
// {
// close(ep._write_id);
// }
// sleep(5);
// cout<<"父进程让所有的子进程都退出"<<endl;
// //2. 父进程回收子进程的僵尸状态
// for(const auto&ep:end_points)
// {
// waitpid(ep._child_id,nullptr,0);
// }
// cout<<"父进程回收了所有的子进程"<<endl;
// sleep(5);
//倒着关闭子进程的写端,再回收子进程
for(int i=end_points.size()-1;i>=0;i--)
{
close(end_points[i]._write_id);
cout<<"父进程让"<<end_points[i]._child_id<<"子进程退出"<<endl;
waitpid(end_points[i]._child_id,nullptr,0);
cout<<"父进程回收了"<<end_points[i]._child_id<<"子进程"<<endl;
cout<<endl;
sleep(1);
}
//一个一个退出
for(const auto&ep:end_points)
{
close(ep._write_id);
cout<<"父进程让"<<ep._child_id<<"子进程退出"<<endl;
waitpid(ep._child_id,nullptr,0);
cout<<"父进程回收了"<<ep._child_id<<"子进程"<<endl;
cout<<endl;
sleep(1);
}
}
int main()
{
// 用于存储子进程的pid和相对应管道的文件描述符
vector<EndPoint> end_points;
// 创建控制模块
createProcesses(end_points);
sleep(1);
//开始通信
ctrlProcess(end_points);
//回收子进程
waitProcess(end_points);
cout<<"程序成功退出,欢迎下次使用"<<endl;
return 0;
}
以下为部分运行结果
结束语
本篇文章内容到此结束,感谢你的阅读
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。