Linux:进程池制作(匿名管道版本 & 命名管道版本)
前言
创建进程是有时间成本的。当计算机要执行任务时才创建进程,势必会影响执行任务的性能。所以我们可以通过提前创建一批进程,当有任务需要被执行时直接喂给这些进程即可。我们把这些提前创建好的进程称为进程池!!
下面我们会通过一个主进程(父进程)通过匿名管道和一批工作进程(子进程)进行通信。父进程通过不断派发任务给子进程,子进程通过读取管道文件中的任务码来执行对应的任务,从而模拟进程池的整个行为!!
一、匿名管道制作进程池
一、进程池框架
父进程创建一批子进程,并建立单向通信通道。我们这里规定父进程向匿名管道中一次只能写4字节数据,子进程一次只能从管道文件中读取4字节数据。由于匿名管道的性质,父进程只需向目标子进程所对应的管道中发送任务码即可。如果管道文件中存在数据,子进程会不断读取任务码,执行对应的任务;否则子进程进入阻塞状态,等待主进程向管道中写入数据!
但主进程需要知道向那个管道中发送任务码,向那个匿名管道中进行写入,以及子进程信息。即我们需要对管道进行管理!先描述,在组织!这里我们通过一个类对管道进行描述,其中保存着:主进程控制写端文件描述符、工作进程id和名字!由于我们需要执行快速随机访问,所以选择vector
进行组织管理!所以这个进程池的制作大致框架如下:
【描述结构体】:
#define NUM 5//任务进程个数
int number = 1;//管道编号
class channel
{
public:
channel(int fd, pid_t id)
:ctrlfd(fd)
,workerid(id)
{
name = "channle-" + std::to_string(number++);
}
public:
int ctrlfd;
pid_t workerid;
std::string name;
};
【框架】:
int main()
{
std::vector<channel> channels;//用于管理管道
//1、创建管道,创建进程,工作进程工作方式
CreaterChannel(&channels);
//2、主进程向工作进程发送任务
// 这里特殊设计,我们通过g_always_loop来判断主进程是一直发送任务,还是发送指定次后就结束退出
const bool g_always_loop = true;
SendTask(channels, !g_always_loop, 10);
// 回收资源: 关闭写端,子进程自动退出
ReleaseChannels(channels);
return 0;
}
二、创建管道、创建进程、工作进程执行任务
2.1 创建管道、创建进程
我们可以通过父进程循环NUM
次,每次先创建管道,然后创建子进程。此时关闭父进程和子进程中不需要的读写段,建立单向通信通道。此时就可以建立如下关系:
但上述简单关闭管道文件的读写段会存在问题的!我们需要特殊处理:
上述这些多余的指向管道读端是fork创建子进程时,子进程继承父进程的信息之一(红线)!所以我们每次创建出的子进程还需将所有继承父进程多余的读端全部关闭,否则无法回收子进程导致内存泄漏!!
其中最简单的解决办法就是:我们将父进程的写端文件描述符全部记录下来,每次创建出子进程时,子进程所继承的多余读写信息已经全部保存。我们只需依次将其关闭即可!!
void CreaterChannel(std::vector<channel> *channels)
{
std::vector<int> old;//记录子进程继承的多余读端
for(int i = 0; i < NUM; i++)//创建NUM个工作进程
{
// 1. 创建管道
int pipefd[2];
int n = pipe(pipefd);
assert(n == 0);
(void)n;//防止编译器报警
//2. 创建子进程
pid_t id = fork();
if(id < 0)
{
perror("fork");
return;
}
// 3. 形成单向通信
if(id == 0)//子进程,工作进程
{
if(!old.empty())//将子进程继承父进程的多余读端全部关闭
{
for(auto rfd : old)
{
close(rfd);
}
}
close(pipefd[1]);
dup2(pipefd[0], 0);//将读端重定向到标准输入
work();//子进程执行任务
exit(0);
}
//父进程,主进程
close(pipefd[0]);
channels->push_back(channel(pipefd[1], id));//主进程对应写端和工作进程信息
old.push_back(pipefd[1]);
}
}
2.2 工作进程执行任务
当管道中有数据时,子进程只需读取相关任务码,然后不断执行即可!但如果此时父进程退出时,由于匿名管道的特性,read的返回值会被设为0,此时子进程在进行读取就没有任何意义了,子进程退出!!(检查任务码和执行任务码实现后续会统一分析)
void work()
{
while(1)
{
int code = 0;
ssize_t n = read(0, &code, sizeof(code));
if(n == 0)
{
//主进程写端退出,子进程也退出
break;
}
else if(n > 0)
{
if(!init.CheckSafe(code))//检查任务码是否合法
continue;
init.RunTask(code);//执行任务码
}
else
{
//nothing
}
}
}
三、主进程向子进程发送任务
3.1 任务封装
下面我们仅仅通过一些打印数据来子进程待执行的所有模拟任务
【待执行任务】:
void Download()
{
std::cout << "我是一个下载任务" << std::endl;
}
void Printflog()
{
std::cout << "我是一个打印日志任务" << std::endl;
}
void PushVideoStream()
{
std::cout << "我是一个推送视频流任务" << std::endl;
}
【任务封装】:
我们通过包装器function
将上述指针函数进行统一。同时我们向管道中读取和写入的是任务码,所以下面我们给出了相应的任务码,并将上述如何通过vector
容器进行管理,下标对应任务码信息,并封装成了类!
除此之外,还提供选择任务接口(随机选择)、检查任务码是否合理、任务码转对应任务名、运行特定任务码对应任务等接口!
具体如下:
using task_t = std::function<void()>;
class Init
{
public:
//任务码
static const int g_Download_code = 0;
static const int g_Printflog_code = 1;
static const int g_PushVideoStream_code = 2;
std::vector<task_t> tasks;
public:
Init()
{
tasks.push_back(Download);
tasks.push_back(Printflog);
tasks.push_back(PushVideoStream);