本文是小编巩固自身而作,如有错误,欢迎指出!
目录
[(1)准备阶段:创建子进程与通信管道(Init 函数)](#(1)准备阶段:创建子进程与通信管道(Init 函数))
一、进程池简介
进程池是什么?简单来是就是一个容纳多个进程的池子,但是它存在的作用又是为了什么呢?
进程池 = 提前创建好一批子进程,放在池子里备用,需要任务直接分配给它们,不临时频繁创建 / 销毁进程。
类比:
普通方式:来一个任务 → 临时招一个临时工(fork)→ 干完立马辞退 → 再来任务再招,频繁招人辞退,开销大、慢。
进程池:提前固定招 5 个正式员工 ,一直在工位待命,来任务直接分给员工做,不用反复招人、辞退,省资源、速度快。
为什么要有进程池?(为什么不每次任务都 fork)
Linux 下 fork () 创建进程开销很大:
- 要拷贝页表、虚拟内存、文件描述符、进程上下文
- 频繁 fork /exit 会 CPU 开销高、卡顿、效率低
- 还容易产生僵尸进程
二、父进程对子进程的管理
父进程对子进程的管理,我们可以归结为一句话:
先描述,再组织。
先描述
先用变量、结构体、类 ,把要用到的资源、属性、特征先定义、先描述出来。
先把 "东西长什么样、有哪些成员" 定义好,先造模板。
再组织
再写逻辑、写流程、写循环,把这些描述好的资源调度起来、管理起来、用起来。
再用模板去创建实例、调度工作、完成业务逻辑。
比如做学校管理系统:
-
先描述
- 先定义学生类:姓名、学号、年龄
- 先定义班级类:班级编号、班主任、学生列表
-
再组织
- 创建班级、招收学生
- 分班、排课、考勤、管理作息
先把实体结构定义好 ,再写业务流程调度。
cpp
#include <iostream>
#include <string>
#include <vector>
//先描述
class channel
{
public:
channel(int cmdfd, pid_t slaverid, const std::string& processname)
:_cmdfd(cmdfd), _slaverid(slaverid), _processname(processname)
{}
public:
int _cmdfd; //发送任务的文件描述符fd
pid_t _slaverid; //子进程的pid
std::string _processname; //子进程的名字
};
int main()
{
//再组织
std::vector<channel> channels;
return 0;
}
三、建立信道,初始化工作
当我们已经有父进程管理子进程的方法和手段之后,接下来就是初始化工作,建立父进程与子进程的信道,for循环fork创建多个子进程。
功能作用
批量创建若干子进程 ,同时给每个子进程单独创建一条匿名管道作为通信信道;
核心流程
- 循环创建指定个数子进程
- 每次循环新建一根匿名管道
- fork 创建子进程
- 子进程:关闭管道写端,保留读端,进入任务循环干活
- 父进程:关闭管道读端,保留写端
- 父进程把 写端 fd + 子进程 pid 封装 Channel,保存起来
cpp
// 建立通信信道、初始化子进程工作
void CreateProcessChannel(cb_t cb)
{
// 循环创建固定数量的子进程
for (int i = 0; i < gprocessnum; i++)
{
// 1. 创建匿名管道,建立父子通信信道
int pipefd[2] = {0};
pipe(pipefd);
// 2. fork 创建子进程
pid_t id = fork();
// 子进程分支
if (id == 0)
{
// 子进程关闭不用的管道写端,只留读端
close(pipefd[1]);
// 初始化子进程工作:绑定任务处理函数,阻塞等待父进程发任务
cb(pipefd[0]);
// 子进程工作结束退出
exit(0);
}
// 父进程分支
else
{
// 父进程关闭不用的管道读端,只留写端
close(pipefd[0]);
// 把管道写端、子进程pid封装成信道对象,统一管理
channels.emplace_back(pipefd[1], id);
}
}
}
四、Task与Slaver
在前文,我们已经了解了创建多个子进程以其对应的管道,现在我们看看不同的子进程是怎么被划分执行不同的任务的
下面是给每个slaver分配task的详细过程:
(1)准备阶段:创建子进程与通信管道(Init 函数)
cpp
void Init(callback func)
{
// 1. 创建管道
int pipefd[2];
pipe(pipefd);
// 2. 创建子进程
pid_t id = fork();
if (id == 0)
{
// 子进程:关闭写端,只保留读端
close(pipefd[1]);
// 子进程开始等待任务
func(pipefd[0]);
exit(0);
}
else
{
// 父进程:关闭读端,只保留写端
close(pipefd[0]);
// 将管道和子进程PID打包成Channel
Channel ch(pipefd[1], id, "slaver");
_channels.push_back(ch);
}
}
- 父进程调用
Init,为每个子进程创建匿名管道。- 使用
fork创建 Slaver 子进程。- 子进程关闭管道写端,进入阻塞等待,随时准备接收任务。
- 父进程关闭管道读端,将管道写端
_wfd和子进程 PID_sub_pid封装进Channel。- 所有信道存入
_channels容器,完成任务派送通道的建立。
(2)选择子进程:轮询调度算法(ChooseSlaver)
cpp
int ChooseSlaver()
{
static int idx = 0;
idx %= _channels.size();
int ret = idx;
idx++;
return ret;
}
- 父进程要派发任务前,必须先选定一个子进程。
- 使用轮询算法 ,从
_channels里按顺序挑选子进程下标。idx从 0 开始,依次返回 0 → 1 → 2 → 3 → 4 → 0...- 保证任务均匀分配给所有 Slaver 子进程。
- 返回值
ret就是目标子进程的 index
(3)发送任务:通过管道写给
cpp
void SendTask(int task, int idx)
{
// 向指定Channel的管道写端发送任务
write(_channels[idx]._wfd, &task, sizeof(task));
}
- 父进程拿到子进程下标
idx。- 通过
_channels[idx]找到对应子进程的通信信道。- 调用系统
write,把任务编号task通过管道写端_wfd发送出去。- 内核将任务数据传递给对应子进程,完成任务派送。
(4)子进程接收并执行任务(DoTask)
cpp
void DoTask(int readfd)
{
while (true)
{
int task;
// 阻塞等待父进程发送任务
int n = read(readfd, &task, sizeof(task));
// 根据任务编号执行任务
switch (task)
{
case 1: ... break;
case 2: ... break;
...
}
}
}
- 子进程一直阻塞在
read,等待父进程下发Task。- 父进程发送任务后,子进程读取到任务编号
task。- 使用
switch匹配任务,执行对应的业务逻辑。- 完整流程总结执行完成后继续循环,等待下一个任务
(5)完整流程总结
Init:父进程创建多个 Slaver 子进程,为每个子进程建立管道通信,封装成 Channel 统一管理。ChooseSlaver:父进程通过轮询算法选择子进程下标,确定任务接收者。SendTask:父进程通过下标找到对应管道,将 Task 任务派发给子进程。DoTask:子进程从管道读取任务,根据任务编号执行对应工作。
五、完整代码呈现
ProcessPool.cc
cpp
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <string>
#include <vector>
#include <functional>
#include <unistd.h>
#include <sys/wait.h>
///////////////////////////////子进程要完成的任务/////////////////////////
void SyncDisk()
{
std::cout << getpid() << ": 刷新数据到磁盘任务" << std::endl;
sleep(1);
}
void Download()
{
std::cout << getpid() << ": 下载数据到系统中" << std::endl;
sleep(1);
}
void PrintLog()
{
std::cout << getpid() << ": 打印日志到本地" << std::endl;
sleep(1);
}
void UpdateStatus()
{
std::cout << getpid() << ": 更新一次用户的状态" << std::endl;
sleep(1);
}
typedef void (*task_t)(); // 函数指针
task_t tasks[4] = {SyncDisk, Download, PrintLog, UpdateStatus}; // 任务表
///////////////////////////////进程池相关////////////////////////////////
enum
{
OK = 0,
PIPE_ERROR,
FORK_ERROR
};
// 子进程的入口函数 ------ 【修复:补全了read调用】
void DoTask(int fd)
{
while (true)
{
int task_code = 0;
// 修复:缺失读取管道的代码
ssize_t n = read(fd, &task_code, sizeof(task_code));
if (n == sizeof(task_code))
{
if (task_code >= 0 && task_code < 4)
{
tasks[task_code](); // 执行任务表中的任务
}
}
else if (n == 0)
{
// 父进程关闭写端,子进程读到0,退出
std::cout << getpid() << ": task quit ..." << std::endl;
break;
}
else
{
perror("read");
break;
}
}
close(fd); // 子进程退出前关闭读端
}
const int gprocessnum = 5;
using cb_t = std::function<void(int)>;
class ProcessPool
{
private:
// 父进程管理"通道"
class Channel
{
public:
Channel(int wfd, pid_t pid) : _wfd(wfd), _sub_pid(pid)
{
_sub_name = "sub-channel-" + std::to_string(_sub_pid);
}
~Channel()
{
}
void Write(int index)
{
ssize_t n = write(_wfd, &index, sizeof(index));
(void)n;
}
std::string Name()
{
return _sub_name;
}
void ClosePipe()
{
std::cout << "关闭wfd: " << _wfd << std::endl;
close(_wfd);
}
void Wait()
{
pid_t rid = waitpid(_sub_pid, nullptr, 0);
(void)rid;
std::cout << "回收子进程: " << _sub_pid << std::endl;
}
void PrintInfo()
{
printf("wfd: %d, who: %d, channel name: %s\n", _wfd, _sub_pid, _sub_name.c_str());
}
// 调试查看所有Slaver进程信息
void Debug(const std::vector<Channel>& channels)
{
for(const auto& c : channels)
{
std::cout << "_cmdfd: " << c._wfd
<< " _slaverid: " << c._sub_pid
<< " _processname: " << c._sub_name
<< std::endl;
}
}
private:
int _wfd; // 父进程写端fd
pid_t _sub_pid; // 子进程PID
std::string _sub_name; // 通道名字
};
public:
ProcessPool()
{
srand((unsigned int)time(nullptr) ^ getpid());
}
~ProcessPool() {}
void Init(cb_t cb)
{
CreateProcessChannel(cb);
}
void Run()
{
// 【修复:打开任务调度逻辑】
while (true)
{
std::cout << "------------------------------------------------" << std::endl;
// 1. 随机选择一个任务
int itask = SelectTask();
std::cout << "选中任务: " << itask << std::endl;
// 2. 轮询选择一个子进程
int index = SelectChannel();
std::cout << "选中进程: " << channels[index].Name() << std::endl;
// 3. 发送任务
printf("发送任务 %d to %s\n", itask, channels[index].Name().c_str());
SendTask2Salver(itask, index);
sleep(1); // 每秒发一个任务
}
}
void Quit()
{
// 正确写法:先全部关闭写端,再统一等待
for (auto &channel : channels)
{
channel.ClosePipe();
}
for (auto &channel : channels)
{
channel.Wait();
}
}
void Debug()
{
for (auto &c : channels)
{
c.PrintInfo();
}
}
private:
void SendTask2Salver(int itask, int index)
{
if (itask >= 4 || itask < 0)
return;
if (index < 0 || index >= channels.size())
return;
channels[index].Write(itask);
}
int SelectChannel()
{
static int index = 0;
int selected = index;
index++;
index %= channels.size();
return selected;
}
int SelectTask()
{
int itask = rand() % 4;
return itask;
}
void CreateProcessChannel(cb_t cb)
{
for (int i = 0; i < gprocessnum; i++)
{
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
{
std::cerr << "pipe create error" << std::endl;
exit(PIPE_ERROR);
}
pid_t id = fork();
if (id < 0)
{
std::cerr << "fork error" << std::endl;
exit(FORK_ERROR);
}
else if (id == 0)
{
// 子进程:关闭所有父进程的写端
if(!channels.empty())
{
for(auto &channel : channels)
channel.ClosePipe();
}
close(pipefd[1]); // 子进程只保留读端
cb(pipefd[0]); // 执行任务循环
exit(OK);
}
else
{
// 父进程只保留写端
close(pipefd[0]);
channels.emplace_back(pipefd[1], id);
std::cout << "创建子进程: " << id << " 成功..." << std::endl;
}
}
}
private:
std::vector<Channel> channels;
};
int main()
{
// // 1. 初始化进程池
// ProcessPool pp;
// pp.Init(DoTask);
// pp.Debug();
// // 2. 父进程发送任务
// pp.Run();
// // 3. 释放资源
// pp.Quit();
// return 0;
}
makeflie
bash
ProcessPool:ProcessPool.cc
g++ $^ -o $@ -g -std=c++11
.PHONY:clean
clean:
rm -f ProcessPool
本次分享就到这里结束了,后续会继续更新,感谢阅读!