匿名管道
实现目标
实现一个父进程创建出多个子进程,并且父进程与每个子进程都有独立的管道
父进程可以通过管道写入指定的任务,并随机指派某个子进程通过管道读取任务并执行
思路
- 首先需要创建出多个子进程(静态实现指定数量),因为每个子进程与父进程之间的管道是独立的,到后面需要进行读写时对应的文件描述符都是不一样的,所以可以选用数组将每个子进程对应的管道读写fd记录下来。因为涉及到了多个属性,因此可以将这些属性归并成一个类。数组的类型就采用这个类。其中可以包括管道的名字,子进程id,父进程写端的fd。
- 指派的任务可以有很多个,所以可以利用函数指针的类型数组将所有的任务记录下来,访问数组就可以执行对应的函数
- 这里需要注意一个问题:父进程创建子进程是一个一个的创建的。在父进程创建的第2个子进程开始,由于已经创建过管道所以父进程已经打开过写端,而子进程被创建出来会继承父进程的文件描述符表,因此这个已经被打开的写端也会基础下去。如此反复则从第2个开始被创建出来的子进程都会拥有上一个子进程的写端,这样会导致即使关闭了父进程对应的写端时,子进程的读端仍然会处于阻塞状态等待读取。为了避免这种情况可以在每一次创建子进程后都将子进程继承下来的上一个子进程的写端记录下来,然后在进程创建后如果当前有记录就将记录下来的写端全部关闭。因为进程具有独立性所以每一次创建出来子进程都要进行一次这个操作才能保证每个子进程都没有写端。
- 因为子进程的id和任务都有被保存在数组中,只需要下标就可以找到。因此可以使用随机数来随机指派子进程和任务。将随机的任务下标写到随机指派的子进程对应的管道中,再由该子进程去读取任务下标并执行。因为所有的子进程都是在阻塞中等待管道有数据,因此不管指派的是哪个子进程都是可以读到数据的
- 当子进程拿到下标后就执行对应的函数。因为写入的是整数,所以如果读取到的不是整数说明读取失败,失败时那么子进程的读端也就可以退出了。
- 可以使用计数来指定父进程写入多少次,如果父进程的写端退出了,那么要把所有子进程对应的写端都要关闭掉。
- 当父进程写端全部退出后子进程的读端也可以退出了,全部子进程的读端都退出后要对子进程依次进行回收。
代码实现
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<cstring>
#include<cassert>
#include<sys/wait.h>
#include<sys/types.h>
#include<vector>
#include<ctime>
using namespace std;
//静态指定池里的子进程数
#define childNum 15
//利用函数指针记录需要传给子进程完成的任务
typedef void(*func)();
void Delete(){
cout << getpid() << ": " << "删除任务" << endl;
sleep(1);
}
void Install(){
cout << getpid() << ": " << "安装任务" << endl;
sleep(1);
}
void Renew(){
cout << getpid() << ": " << "更新任务" << endl;
sleep(1);
}
void Pack(){
cout << getpid() << ": " << "打包任务" << endl;
sleep(1);
}
void Send(){
cout << getpid() << ": " << "发送任务" << endl;
sleep(1);
}
//保存任务的函数
void LoadFunc(vector<func>& Funcgroup){
Funcgroup.push_back(Delete);
Funcgroup.push_back(Install);
Funcgroup.push_back(Renew);
Funcgroup.push_back(Pack);
Funcgroup.push_back(Send);
}
//创建保存子进程id、写端的fd、每个池的名字的类
class Ed{
public:
Ed(const string& name, const pid_t& id, const int& writefd)
:_name(name)
,_id(id)
,_writefd(writefd)
{}
const string getname() const{
return _name;
}
const pid_t getid() const{
return _id;
}
const int getwritefd() const{
return _writefd;
}
private:
string _name;
pid_t _id;
int _writefd;
};
//读取管道里的任务码
int ReadTask(int readFd){
int code;
ssize_t s = read(readFd, &code, sizeof(code));
if(s == 4) return code;
return -1;
}
//实现保存所有创建出来的子进程信息
//并实现子进程接收任务且执行任务
void CreateChild(vector<Ed>& childgroup, vector<func>& Taskgroup){
//因为子进程会继承父进程的文件描述符表,因此从第二个子进程创建开始
//每个子进程都会打开了一个对应着上个子进程的写端
//因为父进程里对应的写端是打开的 所以再次创建子进程就会被继承下去
//因此要记录下来每一次创建子进程时打开的写端文件描述符,在下一次创建子进程后
//将子进程继承下来的文件描述符表关掉,这样所有创建出来的子进程才不会有联系
//利用数组记录子进程继承下来的写端
vector<int> DeleteFd;
//创建子进程并创建管道
for(int i = 0; i < childNum; ++i){
//创建管道
int fds[2];
int n = pipe(fds);
assert(n == 0);
pid_t id = fork();
//子进程
if(id == 0){
//在子进程被创建出来后,先将继承下来的上个子进程的写端关闭
for(int i = 0; i < DeleteFd.size(); ++i)
close(DeleteFd[i]);
//关闭子进程的写
close(fds[1]);
while(1){
//读取
int TaskNum = ReadTask(fds[0]);
//执行任务
if(TaskNum >= 0 && TaskNum < Taskgroup.size())
Taskgroup[TaskNum]();
else if(TaskNum == -1)
break;
else
cout << "没有此任务" << endl;
}
exit(0);
}
//关闭父进程的读
close(fds[0]);
//记录子进程
childgroup.push_back({"proce " + to_string(i), id, fds[1]});
//记录子进程被创建后继承下来的上一个进程的写端
DeleteFd.push_back(fds[1]);
}
}
//发送任务给子进程
void SendChild(const Ed& s, int taskNum){
cout << "send " << taskNum << " " << "to " << s.getname() << endl;
//写入任务到子进程所对应的管道文件
int n = write(s.getwritefd(), &taskNum, sizeof(taskNum));
assert(n == sizeof(int));
}
//父进程随机选择子进程和任务并发送
//参数cnt是用来计数的,如果传入时为负数则一直循环,否则计数
void SelSend(const vector<func>& Funcgroup, const vector<Ed>& childgroup, int cnt){
while(1){
if(cnt >= 0)
if(cnt-- == 0)
break;
//随机选择一个进程
int procI = rand() % childNum;
//随机选择一个任务
int taskI = rand() % Funcgroup.size();
//发送任务
SendChild(childgroup[procI], taskI);
sleep(2);
}
//如果写端退出,则关闭所有子进程对应的写端
for(int i = 0; i < childNum; ++i)
close(childgroup[i].getwritefd());
}
//回收子进程
void waitProcess(vector<Ed>& childgroup){
for(int i = 0; i < childgroup.size(); ++i){
waitpid(childgroup[i].getid(), nullptr, 0);
cout << "等待成功: " << childgroup[i].getid() << endl;
}
}
int main(){
//随机数种子
srand(time(nullptr));
//记录所有的子进程的任务
vector<func> Funcgroup;
LoadFunc(Funcgroup);
//记录创建出来的子进程的pid
vector<Ed> childgroup;
CreateChild(childgroup, Funcgroup);
//父进程任意选择控制子进程
SelSend(Funcgroup, childgroup, 3);
//回收子进程
waitProcess(childgroup);
return 0;
}
动图结果演示
以下动图结果演示为计数器是3的情况,因此只会有3次读写
命名管道
实现目的
实现一个写端进程负责发送数据,运行多个读端进程,并且写端进程与每个读端进程都有独立的命名管道
写端进程可以随机向某个管道中写入数据后,管道对应的读端将数据读取输出
思路
- 编写一个头文件,定义所有需要创建的命名管道的文件名,并将所有文件名集合到一个数组中
- 定义创建命名管道文件的函数
- 定义删除明明管道文件的函数
- 编写写端程序,一次性在当前目录下将所有的命名管道文件创建,并记录所有命名管道的fd到一个数组中,将所有管道文件打开
- 编写向管道文件中写入的方法,首先先将写入的字符串保存,随机生成一个下标,知道到该下标对应的fd,并往该fd对应的管道文件中写入
- 编写全部读端程序,每个读端程序对应一个管道文件,并将管道文件打开
- 编写读端读取数据的方法
- 在每一个读端程序中加入删除管道文件的步骤,当写端退出时,读端也会随着退出,退出关闭fd后将管道文件删除
- 需要注意,以下代码示例为写端创建管道文件,因此要先运行写端程序。
NAME_PIPE.hpp
#include<iostream>
#include<unistd.h>
#include<string>
#include<cstring>
#include<sys/types.h>
#include<sys/stat.h>
#include<cassert>
#include<errno.h>
#include<vector>
#include<fcntl.h>
#include<ctime>
//宏定义管道名字
#define NAME_PIPE_1 "./NAME_PIPE_1"
#define NAME_PIPE_2 "./NAME_PIPE_2"
#define NAME_PIPE_3 "./NAME_PIPE_3"
#define NAME_PIPE_4 "./NAME_PIPE_4"
//将管道名字保存
std::vector<std::string> NameGroup({NAME_PIPE_1, NAME_PIPE_2, NAME_PIPE_3, NAME_PIPE_4});
//创建管道函数
bool CreatPipe(const std::string& path){
umask(0);
int n = mkfifo(path.c_str(), 0666);
if(n == 0)
return true;
else{
std::cout << "管道创建失败:" << strerror(errno) << std::endl;
return false;
}
}
//删除管道函数
void RemovePipe(const std::string& path){
assert(unlink(path.c_str()) == 0);
}
WProc.cc
#include"NAME_PIPE.hpp"
int main(){
srand(time(nullptr));
std::vector<int> Fdgroup;
//创建所有的管道文件
for(int i = 0; i < NameGroup.size(); ++i)
assert(CreatPipe(NameGroup[i]));
//打开所有的管道文件,并将所有的管道文件fd保存
for(int i = 0; i < NameGroup.size(); ++i){
int fd = open(NameGroup[i].c_str(), O_WRONLY);
if(fd < 0)
return 1;
Fdgroup.push_back(fd);
}
std::string str;
while(1){
//随机生成一个下标,往该下标的管道文件中写入
int i = rand() % Fdgroup.size();
std::cout << "开始写入:";
getline(std::cin, str);
int s = write(Fdgroup[i], str.c_str(), strlen(str.c_str()));
}
//关闭所有管道文件
for(int i = 0; i < Fdgroup.size(); ++i)
close(Fdgroup[i]);
return 0;
}
RProc1.cc
#include"NAME_PIPE.hpp"
int main(){
//打开管道文件只读
int fd = open(NAME_PIPE_1, O_RDONLY);
assert(fd > 0);
//阻塞着随时等待管道文件中有数据,一有数据立刻读取
while(1){
char buff[1024];
int s = read(fd, buff, sizeof(buff) - 1);
if(s > 0)
buff[s] = 0;
else
break;
std::cout << "RProc1 read : " << buff << std::endl;
}
close(fd);
//删除对应的管道文件
RemovePipe(NAME_PIPE_1);
return 0;
}
RProc2.cc
#include"NAME_PIPE.hpp"
int main(){
int fd = open(NAME_PIPE_2, O_RDONLY);
assert(fd > 0);
while(1){
char buff[1024];
int s = read(fd, buff, sizeof(buff) - 1);
if(s > 0)
buff[s] = 0;
else
break;
std::cout << "RProc2 read : " << buff << std::endl;
}
close(fd);
RemovePipe(NAME_PIPE_2);
return 0;
}
RProc3.cc
#include"NAME_PIPE.hpp"
int main(){
int fd = open(NAME_PIPE_3, O_RDONLY);
assert(fd > 0);
while(1){
char buff[1024];
int s = read(fd, buff, sizeof(buff) - 1);
if(s > 0)
buff[s] = 0;
else
break;
std::cout << "RProc3 read : " << buff << std::endl;
}
close(fd);
RemovePipe(NAME_PIPE_3);
return 0;
}
RProc4.cc
#include"NAME_PIPE.hpp"
int main(){
int fd = open(NAME_PIPE_4, O_RDONLY);
assert(fd > 0);
while(1){
char buff[1024];
int s = read(fd, buff, sizeof(buff) - 1);
if(s > 0)
buff[s] = 0;
else
break;
std::cout << "RProc4 read : " << buff << std::endl;
}
close(fd);
RemovePipe(NAME_PIPE_4);
return 0;
}