Loading

Linux高性能服务器编程—进程池和线程池

1. 池的概念

池的描述和定义:Pool(池)的概念被广泛的应用在服务器端软件的开发上。使用池结构可以明显的提高你的应用程序的速度,改善效率和降低系统资源的开销。所以在应用服务器端的开发中池的设计和实现是开发工作中的重要一环。

那么到底什么是池呢?我们可以简单的想象一下应用运行时的环境,当大量的客户并发的访问应用服务器时我们如何提供服务呢?我们可以为每一个客户提供一个新的服务对象进行服务。这种方法看起来简单,在实际应用中如果采用这种实现会有很多问题,显而易见的是不断的创建和销毁新服务对象必将给造成系统资源的巨大开销,导致系统的性能下降。针对这个问题我们采用池的方式。池可以想象成就是一个容器保存着各种我们需要的对象。我们对这些对象进行复用,从而提高系统性能。从结构上看,它应该具有容器对象和具体的元素对象。从使用方法上看,我们可以直接取得池中的元素来用,也可以把我们要做的任务交给它处理。

2. 动态创建进程(或子线程)来实现服务器的缺点

(1)动态创建进程(或线程)是比较耗费时间的,这将导致较慢的客户响应。
(2)动态创建的子进程(或子线程)通常只用来为一个客户服务(除非我们做特殊的处理),这将导致系统上产生大量的细微进程(或线程),进程(或线程)间的切换将消耗大量CPU时间。
(3)动态创建的子进程是当前进程的完整映像。当前进程必须谨慎地管理其分配的文件描述符和堆内存等系统资源,否则子进程可能复制这些资源,从而使系统的可用资源急剧下降,进而影响服务器的性能。

3. 进程池和线程池概述

进程池和线程池相似,所以这里我们只以进程池为例进行介绍。

进程池由服务器预先创建的一组子进程,这些子进程的数目在3~10个之间。线程池中的线程数目应该和CPU数量差不多。

进程池中的所有子进程都运行相同的代码,有着相同的属性,比如优先级、PGID等,因为进程池在服务器启动之初就创建好了,所有每个子进程都相对干净,没有打开不必要的文件描述符(从父进程继承而来),也不会错误地使用大块的堆内存(从父进程复制得到)。

当新的任务来临,主进程选择哪个子进程来为新任务服务,则有两种方式:
1、主进程使用某种算法来主动选择子进程,最简单的算法是随机算法和Round Robin(轮流选取)算法。更加优秀的算法可以使任务在各个工作进程中更加均匀地分配,从而减轻服务器的整体压力。
2、主线程和子线程通过一个共享的工作队列来同步,子进程都睡眠在该工作队列上。当有新任务来到时,主进程将任务添加到工作队列中。这将唤醒正在等待任务的子进程,不过只有一个子进程将获得信任的“接管权”,它可以从工作队列中取出任务并执行之,而其他子进程将继续睡眠在工作进程中。
当选择好子进程之后,主进程还需要使用某种通知机制来告诉目标子进程有新任务需要处理,并传递必要的数据。最简单的方法是,在父进程和子进程之间建立一条管道,通过管道来实现进程间通信。父线程和子线程之间的通信相对简单,只需要创建全局变量,因为它们共享全局变量。

综上所述,我们一般将进程池的一般模型描绘为下图所示。

4. 处理多客户

在使用进程池处理多客户任务时,首先要考虑的一个问题是:监听socket和连接socket是否都由主进程来统一管理。
半同步/半反应堆模式是由主进程统一管理这两种socket的;而半同步/半异步模式,以及领导者/追随者模式,则是由主进程管理所有监听socket,而各个子进程分别管理属于自己的连接socket的。
对于前一种情况,主进程接受新的连接以得到连接socket,然后它需要将该socket传递给子进程(对于线程池而言,父线程将socket传递给子线程是很简单的,因为它们可以很容易地共享该socket。但对于进程池而言,我们必须使用UNIX域socket在进程间传递特殊的辅助数据,以实现文件描述符的传递)。后一种情况的灵活性更大一些,因为子进程可以自己调用accept来接受新的连接,这样父进程就无须向子进程传递socket,而只需要简单地通知一声:“我检测到新的连接,你来接受它。”
常连接是指一个客户的多次请求可以复用一个TCP连接。那么,在设计进程池时还需要考虑:一个客户连接上的所有任务是否始终由一个子进程来处理。如果说客户任务是无状态的,那么我们可以考虑使用不同的子进程来为该客户的不同请求服务,如下图所示。

5. 半同步半异步进程池实现

下面我们要实现一个基于半同步/半异步并发模式的进程池。
首先我们先介绍一下半同步/半异步模式,其模型如下图所示。

如上图所示,主线程只管监听socket,连接socket由工作线程来管理。当有新的连接到来时,主线程就接收之并将新返回的连接socket派发给某个工作线程,此后新socket上的任何I/O操作都由被选中的工作线程来处理,直到客户关闭连接。主线程向工作线程派发socket的最简单的方式,是往它和工作线程之间的管道里写数据。工作线程检测到管道上有数据可读时,就分析是否是一个新的客户连接请求到来。如果是,则把该新socket上的读写事件注册到自己的epoll内核事件表中。

代码清单

#ifndef PROCESSPOOL_H
#define PROCESSPOOL_H

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <sys/wait.h>
#include <sys/stat.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 <signal.h>

using namespace std;

/* 描述一个子进程的类,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静态函数来创建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_epollfd标识 */
    int m_epollfd;
    /* 监听socket */
    int m_listenfd;
    /* 子进程通过m_stop来决定是否停止运行*/
    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];

/* 将文件描述符设置成非阻塞,调用非阻塞I/O跟阻塞I/O的差别为调用之后立即返回,
 * 返回后,CPU的时间片可以用来处理其他事务,此时性能是提升的。
 * 但是非阻塞I/O的问题是:由于完整的I/O没有完成,立即返回的并不是业务层期望的数据,
 * 而仅仅是当前调用的状态。为了获取完整的数据,应用程序需要重复调用I/O操作来确认是否完成。
 * 这种重复调用判断操作是否完成的技术叫做轮询。 */
static int setnonblocking(int fd)
{
    /* fcntl函数可以改变文件描述符的性质 */
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

/* 将文件描述符上的读事件注册到epollfd标识的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 removedfd(int epollfd, int fd)
{
    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, 0);
    close(fd);
}

/*信号处理函数*/
static void sig_handler(int sig)
{
    /* 保存原来的errno,在函数最后恢复,以保证函数的可重入性,errno是记录系统的最后一次错误代码 */
    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)
{
    /* sigaction函数的功能是检查或修改与指定信号相关联的处理动作 */
    struct sigaction sa;
    memset(&sa, '\0', sizeof(sa));
    /* sa_handler此参数和signal()的参数handler相同,代表新的信号处理函数 */
    sa.sa_handler = handler;
    if (restart)
    {
        /* SA_RESTART重新调用被该系统终止的系统调用 */
        sa.sa_flags |= SA_RESTART;
    }
    sigfillset(&sa.sa_mask);
    assert(sigaction(sig, &sa, NULL) != -1);
}

/* 进程池的构造函数 */
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));

    /* 创建process_number个子进程,并建立它们与父进程之间的管道 */
    m_sub_process = new process[process_number];
    assert(m_sub_process);
    for (int i = 0; i < process_number; ++i)
    {
        /* socketpair函数能够创建一对套接字进行进程间通信,每一个套接字既可以读也可以写,
         m_sub_process[i].m_pipefd是指每一个子进程与父进程通信的两根管道 */
        int ret = socketpair(PF_UNIX, SOCK_STREAM, 0, m_sub_process[i].m_pipefd);
        assert(0 == ret);
        /* 通过fork函数创建线程池中的子进程 */
        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]);
            continue;
        }
        else
        {
            /* 子进程只管从管道里面读,所以关闭子进程的写文件描述符 */
            close(m_sub_process[i].m_pipefd[0]);
            m_idx = i;
            break;
        }
    }
}

/*统一事件源*/
template<typename T>
void processpool<T>::setup_sig_pipe()
{
    /*创建epoll事件监听表和信号管道*/
    m_epollfd = epoll_create(5);
    assert(m_epollfd != -1);

    /* 使用socketpair函数创建管道,注册sig_pipefd[0]上的可读事件 */
    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);   /* 终止进程,kill命令默认发射的信号就是SIGTERM */
    addsig(SIGINT, sig_handler);    /* 键盘输入以中断进程 */
    addsig(SIGPIPE, SIG_IGN);       /* 往读端被关闭的管道或者socket连接中写数据 */
}

/*父进程在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_child()
{
    /* 设置统一事件源,创建两个进程间通信的管道,利用epoll监听管道的可读事件从而处理信号 */
    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);
    /* number是就绪的文件描述符的个数 */
    int number = 0;
    int ret = -1;

    while (!m_stop)
    {
        number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
        if ((number < 0) && (errno != EINTR))
        {
            cout << "epoll failure\n" << endl;
            break;
        }

        for (int i = 0; i < number; ++i)
        {
            int sockfd = events[i].data.fd;
            if ((sockfd == pipefd) && (events[i].events & EPOLLIN))
            {
                int client = 0;
                /*从父、子进程之间的管道读取数据,并将结果保存在变量client中。
                 * 如果读取成功,则表示有新客户连接到来*/
                ret = recv(sockfd, (char*)&client, sizeof(client), 0);
                if (((ret < 0) && (errno != EAGAIN)) || ret == 0)
                {
                    continue;
                }
                else
                {
                    struct sockaddr_in client_address;
                    socklen_t client_addr_length = sizeof(client_addr_length);
                    int connfd = accept(m_listenfd, (struct sockaddr*)&client_address,
                                        &client_addr_length);
                    if (connfd < 0)
                    {
                        cout << "errno is:" << errno << endl;
                        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(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)
                                {
                                    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);
}

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))
        {
            cout << "epoll failure" << endl;
            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);
                cout << "send request to child " << i << endl;
            }
            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)
                                {
                                    for (int i = 0; i < m_process_number; ++i)
                                    {
                                        /*如果进程池中第i个子进程退出了,则主进程关闭相应的通信管道,
                                          并设置相应的m_pid为-1,以标记该子进程已经退出*/
                                        if (m_sub_process[i].m_pid == pid)
                                        {
                                            cout << "child " << i << "join" << endl;
                                            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:
                            {
                                /*如果父进程接受到终止信号,那么就杀死所有子进程,并等待它们全部结束。
                                  当然,通知子进程结束更好的方法是向父、子进程之间的通信管道发送特殊数据*/
                                cout << "kill all the child now" << endl;
                                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 // PROCESSPOOL_H
posted @ 2021-07-08 18:08  小森林呐  阅读(231)  评论(0编辑  收藏  举报