池的概念:
由于服务器的硬件资源“充裕”,那么提高服务器性能的一个很直接的方法就是以空间换时间,即“浪费”服务器的硬件资源,以换取其运行效率。这就是池的概念。
池是一组资源的集合,这组资源在服务器启动之初就完全被创建并初始化,这称为静态资源分配。当服务器进入正是运行阶段,即开始处理客户请求的时候,如果它需要相关的资源,就可以直接从池中获取,无需动态分配。很显然,直接从池中取得所需资源比动态分配资源的速度要快得多,因为分配系统资源的系统调用都是很耗时的。
当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用来释放资源。从最终效果来看,池相当于服务器管理系统资源的应用设施,它避免了服务器对内核的频繁访问。
池可以分为多种,常见的有内存池、进程池、线程池和连接池。
进程池和线程池概述:
进程池是由服务器预先创建的一组子进程,这些子进程的数目在 3~10 个之间(当然这只是典型情况)。线程池中的线程数量应该和 CPU 数量差不多。
进程池中的所有子进程都运行着相同的代码,并具有相同的属性,比如优先级、 PGID 等。
当有新的任务来到时,主进程将通过某种方式选择进程池中的某一个子进程来为之服务。相比于动态创建子进程,选择一个已经存在的子进程的代价显得小得多。
至于主进程选择哪个子进程来为新任务服务,则有两种方法:
(1)主进程使用某种算法来主动选择子进程。
最简单、最常用的算法是随机算法和 Round Robin (轮流算法)。
(2)主进程和所有子进程通过一个共享的工作队列来同步,子进程都睡眠在该工作队列上。当有新的任务到来时,主进程将任务添加到工作队列中。这将唤醒正在等待任务的子进程,不过只有一个子进程将获得新任务的“接管权”,它可以从工作队列中取出任务并执行之,而其他子进程将继续睡眠在工作队列上。
当选择好子进程后,主进程还需要使用某种通知机制来告诉目标子进程有新任务需要处理,并传递必要的数据。最简单的方式是,在父进程和子进程之间预先建立好一条管道,然后通过管道来实现所有的进程间通信。在父线程和子线程之间传递数据就要简单得多,因为我们可以把这些数据定义为全局,那么它们本身就是被所有线程共享的。
综上所述,进程池的一般模型如下所示:
处理多客户
在使用进程池处理多客户任务时,首先考虑的一个问题是:监听socket和连接socket是否都由主进程来统一管理。并发模型,其中半同步/半反应堆模式是由主进程统一管理这两种socket的。而高效的半同步/半异步和领导者/追随者模式,则是由主进程管理所有监听socket,而各个子进程分别管理属于自己的连接socket的。对于前一种情况,主进程接受新的连接以得到连接socket,然后它需要将该socket传递给子进程(对于线程池而言,父线程将socket传递给子线程是很简单的。因为他们可以很容易地共享该socket。但对于进程池而言,必须通过管道传输)。后一种情况的灵活性更大一些,因为子进程可以自己调用accept来接受新的连接,这样该父进程就无须向子进程传递socket。而只需要简单地通知一声:“我检测到新的连接,你来接受它。
常连接,即一个客户的多次请求可以复用一个TCP连接。那么,在设计进程池时还需要考虑:一个客户连接上的所有任务是否始终由一个子进程来处理。如果说客户任务是无状态的,那么我们可以考虑使用不同的进程为该客户不同请求服务。
但如果客户任务是存在上下文关系的,则最好一直用同一个进程来为之服务,否则实现起来比较麻烦,因为我们不得不在各个子进程传递上下文数据,我们采用epoll的EPOLLONESHOT事件,这一事件能够确保一个客户连接在整个生命周期中仅被一个线程处理。
半同步/半异步进程池实现
综合前面的讨论,我们可以实现这个进程池,为了避免在父、子进程之间传递文件描述符,我们将接受新连接的操作放到子进程中,很显然,对于这种模式而言,一个客户连接上的所有任务始终是由一个子进程来处理的。
1、包含的头文件:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>
2、process结构体实现:
//描述一个子进程的类,
//m_pid是目标子进程的PID,m_pipefd是父进程和子进程通信用的管道
class process
{
public:
process() : m_pid( -1 ){}
public:
pid_t m_pid;
int m_pipefd[2];
};
3、进程池类的实现
//将它定义为模板类是为了代码复用
//其模板参数是处理逻辑任务的类
template< typename T >
class processpool
{
private:
//将构造函数定义为私有,因此我们只能通过后面的create静态函数来创建
//processpool实例
processpool( int listenfd, int process_number = 8 );
public:
//单例模式,以保证程序最多创建一个processpool实例,这是程序正确处理信号的必要条件
static processpool< T > *create( int listenfd, int process_number = 8 )
{
if( !m_instance )
{
m_instance = new processpool< T >( listenfd, process_number );
}
return m_instance;
}
~processpool()
{
delete [] m_sub_process;
}
//启动进程池
void run();
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;
//epoll最多能处理的事件数
static const int MAX_EVENT_NUMBER = 10000;
//进程池中的进程总数
int m_process_number;
//子进程在池中的序号,从0开始
int m_idx;
//每个进程都有一个epoll内核事件表,用m_epoolfd标识
int m_epollfd;
//监听socket
int m_listenfd;
//子进程通过m_stop来决定是否停止运行
int m_stop;
//保存所有子进程的描述信息
process *m_sub_process;
//进程池静态实例
static processpool< T > *m_instance;
};
4、细节实现如下:
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_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
setnonblocking( fd );
}
//从epollfd标识的epoll内核事件表中删除fd上的所有注册事件
static void removefd( int epollfd, int 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;
memset( &sa, '\0',