什么是进程池?
线程池
就是服务器预先创建好的一组子进程
,并且子进程的数量是由人为规定的(和CPU
数量差不多最好,数量太多或太少甚至对性能提升没有实质性的帮助)
为什么要使用进程池?
- 为了实现高效的并发模式,我们使用
多进程
- 实现多进程的方法:
动态创建进程
/进程池
动态创建进程的缺点:
1.动态创建耗时
2.切换耗时(由于动态创建的进程一般只服务一个客户对象,所以产生大量进程/线程
并且进程/线程
间切换十分频繁)
3.动态创建的子进程复制父进程的资源导致消耗大量系统资源
如何实现进程池?
半同步半异步进程池模型:
进程 | 工作 | 通信方式 |
---|---|---|
主进程 | 管理监听Socket | 管道 |
子进程 | 管理连接Socket | 管道 |
问题1:客户任务是否具有上下文关系
如果客户任务具有上下文关系,我们最好使用一个子进程对应处理一个客户连接。
处理方法:为客户连接文件描述符注册epoll
的EPOLLONESHOT事件
问题2:进程池里子进程这么多,应该通知哪一个子进程?
使用随机算法
或者RoundRobin(轮询调度)算法
来选择子进程
我们这边使用轮询调度算法来实现:
i=(i+1)%mod
//这里的mod数值为进程池内子进程数量
//使用轮询调度算法代码简洁易读
//由于无需关心任务的状态,所以该调度为无状态调度
问题3:选好子进程后,主进程如何将客户"交付"给子进程?
这里的交付
其实并不用真的将客户连接文件描述符
通过管道
发送给子进程
,主进程通过管道
通知子进程
自己从Socket监听文件描述符
中获取客户连接描述符
即可
下面代码来自《Linux高性能服务器编程》实现的一个进程池,并且我添加了大量注释方便阅读
代码:
#ifndef PROCESSPOOL_H
#define PROCESSPOOL_H
#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
//进程类
class process {
public:
process() : m_pid(-1) {}
public:
pid_t m_pid; //子进程的PID
int m_pipefd[2]; //父进程与子进程通信的管道
};
//进程池类
template <typename T> //定义成模板是为了代码复用,模板参数为处理任务逻辑的类
class processpool {
private:
processpool(int listenfd, int process_number = 8);
public:
//静态函数与静态方法实现类之间共享数据
//静态成员与静态方法实现单例模式,即保证程序最多只能创建一个线程池类
static processpool<T> *create(int listenfd, int process_number = 8) {
if (!m_instance) {//m_instance为进程池类静态实例,如果当前程序未创建进程池,则创建一个
m_instance = new processpool<T>(listenfd, process_number);
}
return m_instance;
}
~processpool() { delete[] m_sub_process; }
void run();//对外开启进程的接口,该方法内部选择调用函数run_parent()/run_child();
private:
void setup_sig_pipe();//设置系统信号管道
void run_parent();//开启父进程
void run_child();//开启子进程
private:
//进程池信息
static const int MAX_PROCESS_NUMBER = 16;//最大进程数量
static const int USER_PER_PROCESS = 65536;//子进程最多能处理的客户数量
static const int MAX_EVENT_NUMBER = 10000;//epoll最多能处理的事件数量
//进程池中进程总数
int m_process_number;
//子进程在池子中的序号,从0开始
int m_idx;
//每一个进程都有一个epoll事件表
int m_epollfd;
//监听socket
int m_listenfd;
//决定是否停止运行
int m_stop;
//存储所有子进程实例
process *m_sub_process;
//静态进程池实例
static processpool<T> *m_instance;
};
template <typename T>
processpool<T> *processpool<T>::m_instance = NULL;
static int sig_pipefd[2];//系统信号管道
static int setnonblocking(int fd) {//设置文件描述符为非阻塞
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
static void addfd(int epollfd, int fd) {//为epoll内核事件表添加文件描述符fd
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;//注册的文件描述符的事件类型为可读和ET模式
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
static void removefd(int epollfd, int fd) {//从epoll内核事件表中删除文件描述符fd
epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
close(fd);
}
static void sig_handler(int sig) {//系统信号回调函数
int save_errno = errno;
int msg = sig;
send(sig_pipefd[1], (char *)&msg, 1, 0);
errno = save_errno;
}
static void addsig(int sig, void(handler)(int), bool restart = true) {
struct sigaction sa;//sigaction结构体
memset(&sa, '\0', sizeof(sa));
sa.sa_handler = handler;//将sa结构体与回调函数绑定
if (restart) {
sa.sa_flags |= SA_RESTART;
}
sigfillset(&sa.sa_mask);//设置信号掩码
assert(sigaction(sig, &sa, NULL) != -1);//将sig信号与sa结构体绑定并且生效
}
//线程池构造函数
template <typename T>
processpool<T>::processpool(int listenfd, int process_number)
: m_listenfd(listenfd), m_process_number(process_number), m_idx(-1),
m_stop(false) {
assert((process_number > 0) && (process_number <= MAX_PROCESS_NUMBER));
//创建进程信息数组
m_sub_process = new process[process_number];
assert(m_sub_process);
for (int i = 0; i < process_number; ++i) {
//创建主进程和子进程之间的管道
int ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_sub_process[i].m_pipefd);
assert(ret == 0);
m_sub_process[i].m_pid = fork();//创建子进程
assert(m_sub_process[i].m_pid >= 0);
if (m_sub_process[i].m_pid > 0) {//主进程调用段
close(m_sub_process[i].m_pipefd[1]);//主进程使用管道0号端口
continue;
} else { //子进程调用段
close(m_sub_process[i].m_pipefd[0]);//子进程使用管道1号端口
m_idx = i;//设置子进程在进程池的序号
break;
}
}
}
//统一事件源
template <typename T> void processpool<T>::setup_sig_pipe() {
//为进程注册epoll内核事件表
m_epollfd = epoll_create(5);
assert(m_epollfd != -1);
//创建系统信号管道
int ret = socketpair(PF_UNIX, SOCK_STREAM, 0, sig_pipefd);
assert(ret != -1);
setnonblocking(sig_pipefd[1]);//设置管道为非阻塞
addfd(m_epollfd, sig_pipefd[0]);//注册系统信号事件
addsig(SIGCHLD, sig_handler);//添加信号:将信号与回调函数绑定
addsig(SIGTERM, sig_handler);
addsig(SIGINT, sig_handler);
addsig(SIGPIPE, SIG_IGN);
}
template <typename T> void processpool<T>::run() {//启动进程
if (m_idx != -1) {
run_child();
return;
}
run_parent();//只有父进程的m_idx是-1
}
//运行子进程
template <typename T> void processpool<T>::run_child() {
setup_sig_pipe();//统一事件源
int pipefd = m_sub_process[m_idx].m_pipefd[1];//获取与父进程通信的管道文件描述符
addfd(m_epollfd, pipefd);//将与父进程通信的管道加入epoll内核事件表
epoll_event events[MAX_EVENT_NUMBER];//创建触发事件数组
T *users = new T[USER_PER_PROCESS];//创建任务逻辑数组
assert(users);
int number = 0;//触发事件数目
int ret = -1;//处理事件后的结果变量
//子进程的事件获取loop,只要m_stop成员没有被置为true,则一直循环获取
while (!m_stop) {
number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);//获取触发事件
if ((number < 0) && (errno != EINTR)) {
printf("epoll failure\n");
break;
}
//for循环依次处理每一个事件
for (int i = 0; i < number; i++) {
int sockfd = events[i].data.fd;//获取事件对应的文件描述符
if ((sockfd == pipefd) && (events[i].events & EPOLLIN)) {//如果事件为父进程通信管道,则代表有任务事件到来
int client = 0;
ret = recv(sockfd, (char *)&client, sizeof(client), 0);
if (((ret < 0) && (errno != EAGAIN)) || ret == 0) {
continue;
} else {
struct sockaddr_in client_address;//创建socket号
socklen_t client_addrlength = sizeof(client_address);//socket号长度
int connfd = accept(m_listenfd, (struct sockaddr *)&client_address,
&client_addrlength);//从监听socket文件描述符中获取客户文件描述符
if (connfd < 0) {
printf("errno is: %d\n", errno);
continue;
}
addfd(m_epollfd, connfd);//将客户文件描述符加入epoll内核事件表中
users[connfd].init(m_epollfd, connfd, client_address);//初始化客户文件描述符对应的任务逻辑
}
} else if ((sockfd == sig_pipefd[0]) && (events[i].events & EPOLLIN)) {//触发事件为系统信号
int sig;
char signals[1024];
ret = recv(sig_pipefd[0], signals, sizeof(signals), 0);//从管道sig_pipfd[0]获取信号到signals字符数组中
if (ret <= 0) {
continue;
} else {
for (int i = 0; i < ret; ++i) {
switch (signals[i]) {
case SIGCHLD: {//子进程的子进程发生状态变化信号
pid_t pid;
int stat;
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {
continue;
}
break;
}
case SIGTERM:
case SIGINT: {//中断进程信号
m_stop = true;
break;
}
default: {
break;
}
}
}
}
} else if (events[i].events & EPOLLIN) {//客户文件描述符的可读事件
users[sockfd].process();//启动客户对应的任务逻辑处理函数
} else {
continue;
}
}
}
//子进程跳出事件获取loop意味着子进程要返回进程池待命了
delete[] users;//清空用户数组
users = NULL;
close(pipefd);//关闭与父进程通信管道
// close( m_listenfd ); m_listenfd应该由父进程关闭,因为socket监听文件描述符是在父进程创建的
close(m_epollfd);//关闭epoll内核事件表
}
//启动父进程
template <typename T> void processpool<T>::run_parent() {
setup_sig_pipe();//设置统一事件源
addfd(m_epollfd, m_listenfd);//为epoll内核事件表添加监听文件描述符
epoll_event events[MAX_EVENT_NUMBER];
int sub_process_counter = 0;//子进程计数器
int new_conn = 1;//该变量表示由新的客户连接到来,父进程将该标识量发送给子进程
int number = 0;//触发事件数目
int ret = -1;//事件结果变量
while (!m_stop) {
number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
if ((number < 0) && (errno != EINTR)) {
printf("epoll failure\n");
break;
}
for (int i = 0; i < number; i++) {
int sockfd = events[i].data.fd;
//触发事件为监听文件描述符意味着由新的客户连接到来,
//那么使用RoundRobin(轮询调度)算法来将该新客户分配给子进程
if (sockfd == m_listenfd) {
int i = sub_process_counter;
do {
if (m_sub_process[i].m_pid != -1) {
break;
}
i = (i + 1) % m_process_number;
//调度执行i = (i + 1) mod n,并选出第i个子进程。算法的优点是其简洁性,它无需记录当前所有连接的状态,所以它是一种无状态调度。
} while (i != sub_process_counter);
if (m_sub_process[i].m_pid == -1) {//无可用的子进程
m_stop = true;
break;
}
sub_process_counter = (i + 1) % m_process_number;//设置更新子进程计数器
// send( m_sub_process[sub_process_counter++].m_pipefd[0], ( char*
// )&new_conn, sizeof( new_conn ), 0 );
send(m_sub_process[i].m_pipefd[0], (char *)&new_conn, sizeof(new_conn),
0);//将新客户连接标识量通过进程池的i号子进程的通信管道发送给子进程
printf("send request to child %d\n", i);
// sub_process_counter %= m_process_number;
}
//触发事件为系统信号事件
else if ((sockfd == sig_pipefd[0]) && (events[i].events & EPOLLIN)) {
int sig;
char signals[1024];
ret = recv(sig_pipefd[0], signals, sizeof(signals), 0);
if (ret <= 0) {
continue;
} else {
for (int i = 0; i < ret; ++i) {
switch (signals[i]) {
case SIGCHLD: {//父进程的子进程发生状态变化信号
pid_t pid;
int stat;
while ((pid = waitpid(-1, &stat, WNOHANG)) > 0) {//获取状态变化的子进程pid
for (int i = 0; i < m_process_number; ++i) {//找到对应的子进程
if (m_sub_process[i].m_pid == pid) {
printf("child %d join\n", i);
close(m_sub_process[i].m_pipefd[0]);//关掉该进程的通信管道
m_sub_process[i].m_pid = -1;//设置该子进程的pid为父进程对应的-1
}
}
}
m_stop = true;
//若还有子进程在进程池中,父进程就还得进行事件获取loop
for (int i = 0; i < m_process_number; ++i) {
if (m_sub_process[i].m_pid != -1) {
m_stop = false;
}
}
break;
}
case SIGTERM:
case SIGINT: {//进程中断信号
printf("kill all the clild now\n");//结束父进程前必须先将所有进程池中的所有子进程关闭,不然会出现僵尸进程
for (int i = 0; i < m_process_number; ++i) {
int pid = m_sub_process[i].m_pid;
if (pid != -1) {
kill(pid, SIGTERM);
}
}
break;
}
default: {
break;
}
}
}
}
} else {
continue;
}
}
}
// close( m_listenfd );
close(m_epollfd);
}
#endif