進程池
進程池是我們創建一組進程,進程數目可以自己的規定,目的是爲了讓我們可以併發的去處理任務,此文處理的是網絡請求的進程池。
採用的是半同步/半反應堆模式的進程池。
主進程epoll監聽listenfd,如有客戶連接根據特定算法將客戶連接套接字利用管道發送給進程池中的某個進程,讓其進行監聽。該算法主要是爲了平衡進程池中每個進程epoll監聽的套接字數量。防止某個進程監聽過多性能降低。
然後每個進程中都有epoll來監聽客戶請求,當被加入進程池中的套接字有客戶請求時,就進行業務處理。
下面我們貼代碼一點一點進行分析
//描述一個子進程的類,m_pid是目標子進程的PID,m_pipefd是父進程和子進程通信用的管道
class process
{
public:
process() : m_pid(-1) {}
public:
pid_t m_pid;
int m_pipefd[2]; //就是爲了讓父進程有新的連接可以通知子進程
};
下面就是進程池中的主要框架,要仔細斟酌,認真理解
//進程池類,將它定義爲模板類是爲了代碼複用。其模板參數是處理邏輯任務的類
template<typename T> //模板就不做過多解釋
class processpool
{
private:
//將構造函數定義爲私有的,因此我們只能通過後面的create靜態函數來創建processpoll的實例
processpool(int listenfd, int process_number = 8);
public:
/*單體模式,以保證程序最多創建一個processpool實例,這是程序正確處理信號的必要條件,該類只能通過create函數來創建*/
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(); //創建信號處理管道的函數,爲了將信號加入到epoll中進行處理
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_epollfd來標識
int m_epollfd;
//監聽socket,就是主進程需要監聽的套接字
int m_listenfd;
//子進程通過m_stop來決定是否停止運行
int m_stop;
//保存所有子進程的描述信息
process* m_sub_process;
//進程池靜態實例
static processpool<T>* m_instance;
};
//用於處理信號的管道,以實現統一事件源。後面稱之爲信號管道
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;
}
//將文件描述符加入epoll事件集中
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', sizeof(sa));
sa.sa_handler = handler;
if(restart)
sa.sa_flags |= SA_RESTART;
sigfillset(&sa.sa_mask);
assert(sigaction(sig, &sa, NULL) != -1);
}
//可以讓信號在epoll中進行處理,不瞭解的看以看看我之前寫統一事件源那篇博客
template<typename T>
void processpool<T>::setup_sig_pipe()
{
//創建epoll事件監聽表和信號管道
m_epollfd = epoll_create(5);
assert(m_epollfd != -1);
int ret = socketpair(AF_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);
}
首先就是創建進程池函數
/*進程池構造函數。參數listenfd是監聽socket,它必須在創建進程池之前被創建,
否則子進程無法直接引用它。參數process_number指定進程池中子進程的數量*/
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) );
//創建子進程的類,爲了每個子進程都可以訪問這個數組,可以通過訪問它得到與父進程通信的管道,所以將其在fork()之前定義
m_sub_process = new process[process_number];
assert(m_sub_process);
//創建process_number個子進程,並建立它們和父進程之間的管道
for(int i = 0; i < process_number; ++i)
{
//創建用來實現父子進程通信的管道,socketpair創建的管道是全雙工通信的管道
int ret = socketpair(AF_UNIX, SOCK_STREAM, 0, m_sub_process[i].m_pipefd);
assert(ret == 0);
//調用fork函數創建子進程
m_sub_process[i].m_pid = fork();
assert(m_sub_process[i].m_pid >= 0);
//父進程運行
if(m_sub_process[i].m_pid > 0)
{
//關閉掉管道的一端,一般父進程習慣於關閉[1]這一端
close(m_sub_process[i].m_pipefd[1]);
//調用continue是讓父進程接着循環創建子進程,保證進程池中的所有進程的父進程都是同一個
continue;
}
else
{
//關閉子進程中管道的一端和父進程關閉的相反就行
close(m_sub_process[i].m_pipefd[0]);
//然後將i賦予子進程類中的m_idx。
m_idx = i;
//一定要break退出循環,不然子進程也會循環創建進程...
break;
}
}
//進程池在此就創建完成,業務邏輯也是很重要的,如何讓進程之間進行合理的處理也是非常重要的。
}
下面就是父子進程中邏輯處理的函數
//父進程中m_idx值爲-1,子進程中m_idx值大於等於0, 我們據此判斷接下來要運行的父進程代碼還是子進程代碼
template<typename T>
void processpool<T>::run()
{
if(m_idx != -1)
{
run_child();
return;
}
run_parent();
}
template<typename T>
void processpool<T>::run_parent()
{
setup_sig_pipe(); //創建信號管道
//父進程監聽m_listenfd
addfd(m_epollfd, m_listenfd);
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;
if(sockfd == m_listenfd)
{
//如果有新的連接到來,就採用Round Robin方式將其分配給一個子進程處理
int i = sub_process_counter;
do
{
//如果不是正在運行的子進程就退出
if(m_sub_process[i].m_pid != -1)
break;
i = (i + 1)% m_process_number;
} 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[i].m_pipefd[0], (char*)&new_conn, sizeof(new_conn), 0);
printf("send request to child %d\n", i);
}
//下面處理父進程接收到的信號
else if( (sockfd == sig_pipefd[0]) && (events[i].events & EPOLLIN) )
{
int sig;
char signals[1024];
ret = recv(sockfd, 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;
/*WNOHANG參數: 如果pid=-1,等待任意子進程,等待的子進程沒有結束,則waitpid()函數立即返回0,而不是阻塞在這個函數上等待;如果結束了,則返回該子進程的進程號。*/
while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
{
for(int i = 0; i < m_process_number; ++i)
{
//如果進程池中第i個子進程退出了,則主進程關閉相應的通信管道,並設置相應的m_pid爲-1,以標記該子進程退出
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;
}
}
}
//如果所有子進程都已經退出了,則父進程也退出
m_stop = true;
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 child 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);
}
/*
for(int i = 0; i < m_sub_process; ++i)
{
int pid = m_sub_process[i].m_pid;
int pipefd;
if(pid != -1)
{
pipefd = m_sub_process[i].m_pipefd[0];
send(pipefd, )
}
}
*/
break;
}
default:
break;
}
}
}
}
else
{
continue;
}
}
}
close(m_listenfd); //由創建者關閉這個文件描述符
close(m_epollfd);
}
template<typename T>
void processpool<T>::run_child()
{
//創建處理信號的管道
setup_sig_pipe();
//每個子進程都通過其在進程池中的序號值m_idx找到與父進程通信的管道
int pipefd = m_sub_process[m_idx].m_pipefd[1];
//子進程需要監聽管道文件描述符pipefd,因爲父進程將通過它來通知子進程accept新連接
addfd(m_epollfd, pipefd);
epoll_event events[MAX_EVENT_NUMBER];
T* users = new T[USER_PER_PROCESS];
assert(users);
int number = 0;
int ret = -1;
while(!m_stop)
{
number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
if((number < 0) && (errno != EINTR) )
{
std::cout << "epoll failure\n";
break;
}
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(pipefd, (char*)&client, sizeof(client), 0);
if( ( (ret < 0) &&(errno != EAGAIN) ) || ret == 0)
continue;
//子進程有新的連接
else
{
struct sockaddr_in client_address;
socklen_t client_len = sizeof(client_address);
int connfd = accept(m_listenfd, (struct sockaddr*)&client_address, &client_len);
if(connfd < 0)
{
std::cout << "errno is " << errno <<"\n";
continue;
}
addfd(m_epollfd, connfd);
/*模板類T必須實現init方法,以初始化一個客戶連接。
我們直接使用connfd來索引邏輯處理對象(T類型的對象),以提高程序效率*/
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(sockfd, 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)
continue;
break;
}
case SIGTERM:
case SIGINT:
{
m_stop = true;
break;
}
default:
break;
}
}
}
}
//如果是其它可讀數據,那麼必然是客戶連接請求到來。調用邏輯處理對象的process方法處理
else if(events[i].events & EPOLLIN)
{
users[sockfd].process();
}
else
{
continue;
}
}
}
delete [] users;
users = NULL;
close(pipefd);
/*close(m_listenfd)我們將這段話註釋掉,以提醒大家:應該由m_listenfd的創建者來關閉該文件描述符(見後文),
既所謂的“對象(比如一個文件描述符,又或者一段內存)由哪個函數創建,就應該由哪個函數銷燬”*/
close(m_epollfd);
}
總結
- 上述代碼中有一個需要注意的點
while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
這個代碼爲什麼要這麼寫呢?
因爲我們處理信號用的是sigaction函數,如果有子進程結束,則在父進程中會產生SIGCHLD信號,該信號會阻塞等待sigaction中的handler信號處理函數進行處理,如果該信號未及時處理又有子進程結束產生SIGCHLD信號,則sigaction中還是隻有一個SIGCHLD信號需要進行處理,所以可能會遺漏一些子進程的消亡,並且不能及時回收子進程的資源,生成殭屍進程。這段代碼就很好的處理了這種情況,當有SIGCHLD信號產生,我們就將所有子進程輪詢一遍,確定消亡的子進程。
上述就是我們的進程池。
進程池實現的幾個關鍵點
- 創建進程的時候,父進程執行continue,子進程一定要break退出。
- 將父子進程都需要的資源要在fork函數之前創建
- 父子進程處理業務邏輯的時候,要處理好如何讓父子進程進行通信,如果還需要訪問共享資源,一定要注意同步問題,防止產生惡性競爭。
- 父進程要管理好進程池中進程的退出和異常。