科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道应用软件Apache中预创建Preforking MPM 机制剖析(1)

Apache中预创建Preforking MPM 机制剖析(1)

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

本文详细的剖析了apache中preforking MPM的实现机制

作者:tingya 来源:CSDN 2008年5月30日

关键字: 机制 创建 python 软件

  • 评论
  • 分享微博
  • 分享邮件

6.3.1 Leader/Follow模式
在了解Preforking MPM之前有必要首先了解Leader/Follow模型。Preforking模型本质上也属于Leader/Follow模型。通常情况下,L/F可以用下图进行描述:
通常情况下,对于服务器中的进程采用的都是即时创建的策略,即一旦有一个新的客户端请求立即创建一个新的进程或者线程,而当进程或者线程执行完毕后,进程和线程也随之退出。显然这种策略对于小规模的服务器还能接受,但是如果对于大规模的服务器而言,创建进程或者线程的时间将增加,最终会导致响应时间变长,单位时间内请求处理效率降低。L/F模式则不同,它首先一次性创建多个进程或者线程,包括到系统中,这些进程或者线程担任三种不同的角色:侦听者、工作者以及空闲者,其含义分别如下:
1)、侦听者的角色。该线程负责侦听客户端的请求。在L/F模式中它属于Leader的角色。通常情况下只允许一个进程或者线程担当侦听者的角色。
2)、工作者的角色。当侦听者侦听到客户端的请求之后,它将立即转换为工作者角色并开始处理客户端的请求。工作者角色的线程可以有多个。
3)、空闲者的角色。当工作者执行任务完毕后它并不会立即退出,而是转变它的角色为空闲者,并呆在空闲队列中。空闲者出现的原因是因为客户端请求不够多。空闲者们等待变为侦听者。而当侦听者变为工作者之后,空闲者中的每一个都相互竞争,最终将会有一个线程变为侦听者,其余的继续保持空闲者的状态。
4)、几个极端的情况也是可能出现的:所有线程都变为工作者,忙于处理客户端的请求,没有线程担任侦听者的角色,因此此时客户端的请求都被拒绝;没有工作者,如果没有任何请求到达,那么所有的线程都处于空闲状态。
线程的三个角色的相互转换关系可以用上图的红线进行描述。
6.3.2 Preforking MPM概述
UNIX平台上可以使用的第一个MPM就是预先派生(Preforking)MPM,也是默认的MPM。该模型在功能上等同于Apache1.3上的模型,该MPM的示意图如下所示:
该MPM中,存在一个主进程和多个子进程。每个子进程都会为所进行的请求侦听一个套接字。当接受到请求之后,子进程就会接受它并且提供响应。父进程会监控所有的子进程以确保总是可以使用最少数量的进程来处理请求,并且确保等候请求到达的闲置进程不能过少。如果没有足够的空闲进程来处理潜在的请求高峰,那么父进程就会启动新的子进程。如果存在过多的进程,那么父进程会每次终止一个空闲进程,直到服务器回到最大空闲子进程数量之下。通过保持一定数量的空闲子进程来接受所引入的请求,服务器就可以避免在接受到请求时再去启动新进程的开销。
父进程和子进程之间通过记分板进行通信。对于每一个产生的子进程,它的状态信息都写入到记分板中,父进程通过读取记分板可以了解子进程的状态。当需要关闭子进程的时候它将通过终止管道发送终止信息给子进程,另外的一种通知方法就是通过信号。
预先派生模型有一些优点,例如健壮性以及可靠性。Apache允许使用动态模块将第三方的代码加入到服务器。这意味着如果管理员在服务器中增加了第三方软件,而且模块导致了子进程出现段故障,那么服务器就会丢失一个连接,而且仅仅丢失一个连接。服务器的其余部分还将继续运行,并且可以为请求提供服务。唯一可以注意到问题出现的用户就是不幸地进行了导致问题地请求的用户。另一方面必须要意识到,可能并不是遭遇了故障的请求而导致了问题,可能会是因为有一系列请求而导致了问题的出现。
这个模型的另外一个优点就是可以很容易地编写采用这种方式运行的MPM。如果每个进程每次只需要处理一次请求,那么就没有过多的边界条件需要考虑。例如,如果管理员想平稳重新启动服务器,那么她就要等待所有的子进程完成当前请求,并且适时强制其终止来完成任务,然后父进程就可以启动新的进程来代替旧的进程了。
不过预派生模型也有自己的缺点,比如扩充性。因为预先派生模型是依赖于进程,所以在某些平台上并不能很好的执行,比如在Window平台上则由于进程的代价太高,耗时太长的原因则弃用该方案。当然Window并不是唯一的遭遇该问题的OS。比如在高负载的情况下AIX也会遇到这个问题。
预先派生模型的另外一个问题就是安全性。许多ISP都会在相同的计算机上使用Apache来为多个公司的Web站点提供需要的Web服务。为了完成这个任务,每个公司都要被赋予一个虚拟主机,但是因为所有的站点都需要访问Web服务器,所以必须通过运行子进程的用户ID来读取所有的页面。大多数Apache用户运行Apache的方式是作为超级用户运行父进程,然后作为专用于Web服务器的用户运行子进程。这样就可以让服务器打开特权端口80,而且还可以确保侦听网络的进程不作为根用户运行,因此就减少了发现安全漏洞所带来的风险。因为所有的虚拟主机都会使用相同的用户ID运行,所以它们的CGI教本也会使用这个ID运行。这意味着任何为这个站点存储信息的数据库都必须要通过这个用户可读。这样就会让任何站点访问其他站点的私有信息。当然,Apache已经针对这个问题提供了解决方案,它可以让站点规定它们的CGI脚本作为哪个用户运行但是这只是解决了实际的CGI教本的问题,并没有解决PHP,Apache模块或者通过mod_perl运行的Perl脚本的问题。
这种设计的最后问题就是它会消弱某些优化。因为每个请求都会在它自己的进程中运行,所以进程之间很难共享任何信息。增加Web服务器性能的常见方式就是缓存最近所发送的所有的页面。但是,如果每个进程都要保存它自己的缓存,那么缓存的作用就会降低。只有多次获取缓存中所缓存的页面缓存才会有作用。大多数情况下,这种方式会随着时间的推移逐渐显示其作用,缓存总是会缓存最常受到请求的页面。这意味着,到缓存起作用的时候,进程就要受到没有缓存的新进程的替换。Apache中的子进程会在由配置文件中的MaxRequestsPerChild指令所控制的指定时间间隔终止。Apache在指定的请求数量之后强制终止子进程的原因是为了防止内存泄漏。因为Apache要在相当长的时间内运行,所以很小的内存泄漏也会导致服务器出现问题。而且因为子进程是唯一分配内存的进程——所以强制其退出,并且偶尔对其重新启动——就有可能避免由于内存泄漏而导致的问题。
在下面的部分,我们将描述MPM的实现细节。首先我们分析主进程管理细节,然后分析子进程的工作细节,最后我们分析主进程是如何与子进程进行通信并对其进行管理的。
6.3.3 Preforking MPM实现
6.3.3.1 内部结构
在对Preforking MPM进行深入的分析之前,我们先从整体上了解Preforking MPM的内部结构,从而能够从整体上有一个认识,这样在后面的具体分析中不至于迷失方向。下图给出的则是Preforking MPM的中的内部大概的实现机制:

一个完整的MPM包括下面几个数据流程:
1)、第一次初始化
第一次初始化的时候是分配资源(主要是内存池),读取以及检查配置文件,然后服务器进程将其变为一个后台进程。
2)、重启循环
重启循环是指不关闭Apache而进行的启动。重启循环中主要重新读取配置文件防止配置文件发生变化,创建子进程池并进入服务器主循环。
3)、服务器主进程循环
服务器主循环主要是控制进程池中空闲子进程的数目,具体到细节中则是循环监控记分板,并根据记分板中子进程的状态作出反应。
4)、客户端请求/响应循环
这个循环只适合于子进程。在该循环中,子进程等待自己变为侦听者,然后等待客户端的连接,一旦获取到连接则变为工作者开始处理请求,同时进入Keep-alive循环中。
5)、Keep-alive循环
Keep-alive循环主要是处理客户端的请求,该循环仅仅适合子进程。
6)、在退出之前进行清理工作
6.3.3.2 MPM中的定义
预创建MPM所对应的文件为prefork.c。通常情况下,MPM的名称总是和MPM的文件具有相同的名称,这样做可以让配置更合理一些。对于每一个MPM,其遇到的第一件事情就是定义一些全局变量,它们各自的含义描述在右边的注释中:
int ap_threads_per_child=0;                                       /* 每个进程对应的线程数目 */
static apr_proc_mutex_t *accept_mutex;                          /*连接接受互斥锁,用以确保在任何时候只有一个连
接被接受*/
static int ap_daemons_to_start=0;                             /*初始启动的进程数目*/
static int ap_daemons_min_free=0;                                   /*可以接受的空闲进程的最小数目*/
static int ap_daemons_max_free=0;                                  /*允许空闲的进程的最大数目*/
static int ap_daemons_limit=0;                                          /*允许同时运行的进程的最大值*/
static int server_limit = DEFAULT_SERVER_LIMIT;        /**/
static int first_server_limit;
static int changed_limit_at_restart;
static int mpm_state = AP_MPMQ_STARTING;                     /*描述当前*/
static ap_pod_t *pod;
 
int ap_max_daemons_limit = -1;
 
server_rec *ap_server_conf;
static int one_process = 0;
 
static apr_pool_t *pconf;                               /* Pool for config stuff */
static apr_pool_t *pchild;                             /* Pool for httpd child stuff */
 
static pid_t ap_my_pid;                          /* it seems silly to call getpid all the time */
static pid_t parent_pid;
#ifndef MULTITHREAD
static int my_child_num;
#endif
ap_generation_t volatile ap_my_generation=0;
static int die_now = 0;
另外一个所有的MPM都用到的函数就是ap_mpm_query(),外界调用该函数通常是想了解当前MPM的一些私有属性。比如在mod_snake(在Apache进程中嵌入python解释器的模块)中就会使用这个函数来查询给定的MPM是否进行了线程化。如果进行了线程化,那么该模块就必须同步某些python函数,否则就不需要进行同步了。
Apache中关于MPM的状态分类可以归结为两大类:运行状态和内部状态。
#define AP_MPMQ_STARTING              0
#define AP_MPMQ_RUNNING               1
#define AP_MPMQ_STOPPING              2
上面三种属于运行状态,分别表示Apache处理启动、运行和停止状态。
#define AP_MPMQ_MAX_DAEMON_USED       1 /* 所有的进程都已经满了         */
#define AP_MPMQ_IS_THREADED          2 /* MPM 能够支持线程化           */
#define AP_MPMQ_IS_FORKED             3 /* MPM 能够调用fork产生子进程 */
#define AP_MPMQ_HARD_LIMIT_DAEMONS    4 /* The compiled max # daemons   */
#define AP_MPMQ_HARD_LIMIT_THREADS    5 /* The compiled max # threads   */
#define AP_MPMQ_MAX_THREADS           6 /* # of threads/child by config */
#define AP_MPMQ_MIN_SPARE_DAEMONS     7 /* Min # of spare daemons       */
#define AP_MPMQ_MIN_SPARE_THREADS     8 /* Min # of spare threads       */
#define AP_MPMQ_MAX_SPARE_DAEMONS     9 /* Max # of spare daemons       */
#define AP_MPMQ_MAX_SPARE_THREADS    10 /* Max # of spare threads       */
#define AP_MPMQ_MAX_REQUESTS_DAEMON 11 /* Max # of requests per daemon */
#define AP_MPMQ_MAX_DAEMONS          12 /* Max # of daemons by config   */
#define AP_MPMQ_MPM_STATE            13 /* starting, running, stopping */
上面的都属于Apache的内部状态。预创建MPM中的函数定义如下:
AP_DECLARE(apr_status_t) ap_mpm_query(int query_code, int *result)
{
    switch(query_code){
        case AP_MPMQ_MAX_DAEMON_USED:
            *result = ap_daemons_limit;
            return APR_SUCCESS;
        case AP_MPMQ_IS_THREADED:
            *result = AP_MPMQ_NOT_SUPPORTED;
            return APR_SUCCESS;
        case AP_MPMQ_IS_FORKED:
            *result = AP_MPMQ_DYNAMIC;
            return APR_SUCCESS;
        case AP_MPMQ_HARD_LIMIT_DAEMONS:
            *result = server_limit;
            return APR_SUCCESS;
        case AP_MPMQ_HARD_LIMIT_THREADS:
            *result = HARD_THREAD_LIMIT;
            return APR_SUCCESS;
        case AP_MPMQ_MAX_THREADS:
            *result = 0;
            return APR_SUCCESS;
        case AP_MPMQ_MIN_SPARE_DAEMONS:
            *result = ap_daemons_min_free;
            return APR_SUCCESS;
        case AP_MPMQ_MIN_SPARE_THREADS:
            *result = 0;
            return APR_SUCCESS;
        case AP_MPMQ_MAX_SPARE_DAEMONS:
            *result = ap_daemons_max_free;
            return APR_SUCCESS;
        case AP_MPMQ_MAX_SPARE_THREADS:
            *result = 0;
            return APR_SUCCESS;
        case AP_MPMQ_MAX_REQUESTS_DAEMON:
            *result = ap_max_requests_per_child;
            return APR_SUCCESS;
        case AP_MPMQ_MAX_DAEMONS:
            *result = server_limit;
            return APR_SUCCESS;
        case AP_MPMQ_MPM_STATE:
            *result = mpm_state;
            return APR_SUCCESS;
    }
    return APR_ENOTIMPL;
}
这个是ap_mpm_query函数是所有的MPM都要求的函数,它可以让模块发现MPM的运行特性。尽管这个函数在所有的MPM中看起来很相似,但是细节还是十分重要,因为每个MPM都必须实现自己的这个函数。使用这个函数最常见的原因就是要通过web或者管理应用程序报告信息。可以采用很多方式来使用这个函数中的信息,所以应该保证它正确无误。比如mod_snake(在Apache中嵌入python解释器的模块)就会使用这个查询模块来确定MPM是否进行了线程化,如果进行了线程化,那么该模块可能就必须同步某些python函数;否则就不需要进行同步。
6.3.3.3主服务进程管理
6.3.3.3.1主服务进程概述
所有的MPM都是从ap_mpm_run()函数开始执行,对此预创建MPM也不例外。ap_mpm_run()函数通常由Apache核心在main()中进行调用,一旦调用,运行服务器的职责就从Apache核心移交给了MPM。这个函数是所有的MPM都必须实现的。通常情况下,ap_mpm_run的实现会比较复杂。对于Preforking MPM,它的执行流程可以用下图描述:
图 主进程工作流程
从上图中可以看出,主服务进程的功能主要包括下面几部分:
1)、接受进程外部信号进行重启,关闭以及平稳启动等等操作。外部进程通过发送信号给主服务进程以实现控制主服务进程的目的。
2)、在启动的时候创建子进程或者在平稳启动的时候用新进程替代原有进程。
3)、监控子进程的运行状态,并根据运行负载自动调节空闲子进程的数目:在存在过多空闲子进程的时候终止部分空闲进程;在空闲子进程较少的时候创建更多的空闲进程以便将空闲进程的数目维持在一定的数目之内。
上图的上半部分主要处理子进程的终止以及平稳启动的一些内容,而标有”维护空闲子进程数目”的下半部分则主要处理平稳启动的一些内容。在该循环中,主进程统计空闲子进程的数目并从记分板中获得详细的空闲进程列表,然后将统计得到的空闲进程数目idle_count与系统设置的极限ap_daemons_max_free和ap_daemons_min_free)进行对比。如果有过多的空闲子进程,那么它将每次循环中终止一个子进程;反之它将一次性创建足够的空闲进程。
下面的部分我们概要的描述一下主服务进程是如何处理平稳启动,子进程终止以及如何维持空闲子进程数目的。
平稳启动以及处理子进程终止
u设置remaining_children_to_start(以后简称rem.child2start)。变量rem.child2start只有在平稳启动的时候才会用到。它记录的是服务器启动后需要启动的子进程的数目。需要注意的是主服务进程并没有使用startup_children过程创建子进程。对于每一个被终止的子进程,主服务进程通过调用wait()可以得到终止通知。如果初始的子进程的数目在配置文件中被更改了,那么仅仅用新进程替代终止的子进程肯定会出错,因此主服务进程用rem.child2start控制这个数目。
vpid = wait 或者超时。对于使用fork创建的子进程,主服务进程调用wait等待它们的终止,这种做法确保没有僵尸进程的产生。如果在指定的时间内进程仍然没有终止,那么主服务进程算超时处理,即使没有等到终止通知,主服务进程将继续执行它的循环。
w等待成功的情况。如果在指定的时间内子进程终止,此时主服务器进程将完成下面的各项工作:
-                 process_child_status:获取子进程终止的原因。子进程的终止可能有很多情况,比如正常终止,异常终止等等。正常终止对于主服务进程到无所谓,异常终止主进程则必须知道具体原因。
-                 find_child_by_pid:当进程终止后,必须在记分板中更新它的状态信息,因此首先必须在记分板中查找该进程对应的插槽信息。如果查找,则将进程的状态信息设置为SERVER_DEAD。如果remaining_children_to_start不为零的话,创建一个新的子进程来代替终止的子进程。
-                 如果没有在记分板中没有找到终止进程的对应插槽,那么检查该进程是否是“其余子进程”。一些情况下,主服务进程会创建一些非子服务进程的进程,它们称之为“其余进程”,并用一个单独的列表进行登记。比如,一般情况下,Apache会将日志写入到文件中,但是有的时候Apache则希望将数据写入到一个给定的应用程序中。因此主服务器进程必须为该应用程序创建该进程,并且将该进程的标准输入STDIN关联道主服务进程的日志流中。这种进程并不是用来执行处理HTTP请求的子服务进程,因此称之为“其余进程”。任何时候只要服务器重启,日志应用进程都会接受到SIGHU和SIGUSR1信号,然后终止退出,对应的模块必须重新创建这种进程。如果进程既不是“其余进程”,在记分板中也找不到对应的插槽,并且设置了平稳启动模式,那么肯定发生了下面的情况:管理员减少了允许的子进程数目同时进行了平稳启动或者。。。
x等待超时:如果所有的终止了的进程被新创建的新进程代替之后rem.child2start变量的值仍然不为零,那么这意味着必须创建更多的子进程。创建由startup_children()函数完成。
yz
空闲子进程维护
下面的一节我们将详细的分析主进程以及子进程相关的源代码。
6.3.3.3.2主服务进程概述
int ap_mpm_run(apr_pool_t *_pconf, apr_pool_t *plog, server_rec *s)
{
    int index;
    int remaining_children_to_start;
    apr_status_t rv;
 
    ap_log_pid(pconf, ap_pid_fname);
对于所有的Apache MPM而言,其应该首先完成的工作就是在文件pidfile中记录进程的ID。因为启动和终止Apache的默认脚本通常会读取pidfile文件,从中查找所有记录的进程然后逐个终止它。因此如果不进行记录的话,启动的这些进程可能无法通过脚本进行终止,这项操作进行的越快越好。
    first_server_limit = server_limit;
    if (changed_limit_at_restart) {
        ap_log_error(APLOG_MARK, APLOG_WARNING, 0, s,
                     "WARNING: Attempt to change ServerLimit "
                     "ignored during restart");
        changed_limit_at_restart = 0;
    }
理解上面这段代码的关键在于理解两个变量first_server_limit和changed_limit_at_restart的作用。
server_limit变量用以记录服务器内允许同时存在的子服务进程的数目,用过通过配置文件中的ServerLimit指令可以修改这个值。当每次Apache启动的时候,通过读取配置文件这个指令的参数值最终保存到了server_limit变量中并影响服务器的进程产生。由于当Apache重新启动(restart)的时候也会读取配置文件,因此如果服务器重新启动之前修改了配置文件中的ServerLimit指令参数,那么毫无疑问,这种变化Apache重启的时候肯定会看到的。那么Apache该如何处理这种变化呢?是使用新的server_limit还是使用原有的server_limit?Apache的做法是不允许在重启的时候修改server_limit值,即使你修改了Apache也会忽略。
为了能够检查出这种修改,在Apache第一次启动的时候,ServerLimit的值就被记录在first_server_limit变量中,在整个Apache运行期间即使重新启动,这个值也不会变化。first_server_limit=server_limit就是保存初始的值。
按正常的处理策略,对于每次重启后都应该把server_limit与first_server_limit的值进行比较判断是否发生变化,如果发生变化就给出警告,但是上面的代码中并没有这种比较。那么比较在哪儿发生的呢?钥匙在change_limit_at_restart变量上。当重新启动后读取配置文件的时候,遇到ServerLimit指令会调用函数set_server_limit处理该指令,该函数中会将指令参数后面的值与first_server_limit进行比较:
    int tmp_server_limit;
    tmp_server_limit = atoi(arg);//ServerLimit指令后的参数值
    if (first_server_limit &&tmp_server_limit != server_limit) {
        changed_limit_at_restart = 1;
        return NULL;
    }
    server_limit = tmp_server_limit;
从上面的代码中可以看出,changed_limit_at_restart反映了ServerLimit在重启期间是否发生了更改。对于这种更改,Apache并不理会,只是简单的警告,并将changed_limit_at_restart设置为零,这样下次重启就不要进行判断了。
    /* Initialize cross-process accept lock */
    ap_lock_fname = apr_psprintf(_pconf, "%s.%" APR_PID_T_FMT,
                                 ap_server_root_relative(_pconf, ap_lock_fname),
                                 ap_my_pid);
 
    rv = apr_proc_mutex_create(&accept_mutex, ap_lock_fname,
                               ap_accept_lock_mech, _pconf);
    if (rv != APR_SUCCESS) {
        ap_log_error(APLOG_MARK, APLOG_EMERG, rv, s,
                     "Couldn't create accept lock (%s) (%d)",
                     ap_lock_fname, ap_accept_lock_mech);
        mpm_state = AP_MPMQ_STOPPING;
        return 1;
    }
 
#if APR_USE_SYSVSEM_SERIALIZE
    if (ap_accept_lock_mech == APR_LOCK_DEFAULT ||
        ap_accept_lock_mech == APR_LOCK_SYSVSEM) {
#else
    if (ap_accept_lock_mech == APR_LOCK_SYSVSEM) {
#endif
        rv = unixd_set_proc_mutex_perms(accept_mutex);
        if (rv != APR_SUCCESS) {
            ap_log_error(APLOG_MARK, APLOG_EMERG, rv, s,
                         "Couldn't set permissions on cross-process lock; "
                         "check User and Group directives");
            mpm_state = AP_MPMQ_STOPPING;
            return 1;
        }
    }
在预创建MPM中由于存在多个子进程侦听指定的套接字,因此如果不加以控制可能会出现几个子进程同时对一个连接进行处理的情况,这是不允许的。因此我们必须采取一定的措施确保在任何时候一个客户端连接请求只能由一个子进程进程处理。为此Apache中引入了接受互斥锁(Accept Mutex)的概念。接受互斥锁是控制访问TCP/IP服务的一种手段,它能够确保在任何时候只有一个进程在等待TCP/IP的连接请求,从而对于指定的连接请求也只会有一个进程进行处理。
为此MPM紧接着必须创建接受互斥锁。
    if (!is_graceful) {
        if (ap_run_pre_mpm(s->process->pool, SB_SHARED) != OK) {
            mpm_state = AP_MPMQ_STOPPING;
            return 1;
        }
        ap_scoreboard_image->global->running_generation = ap_my_generation;
    }
多数的MPM紧接着会立即创建记分板,并将它设置为共享,以便所有的子进程都可以使用它。记分板在启动的时候被创建一次,直到服务器终止时才会被释放。上面的代码就是用于创建记分板,但是你可能很奇怪,因为你看不到我们描述的记分板创建函数ap_create_scoreboard()。事实上,创建过程由挂钩pre_mpm完成,通过使用pre_mpm,服务器就可以让其他的模块在分配记分板之前访问服务器或者在建立子进程之前访问记分板。
ap_run_pre_mpm()运行挂钩pre_mpm,该挂钩通常对应类似ap_hook_name之类的函数,对于pre_mpm挂钩,对应的函数则是ap_hook_pre_mpm。在core.c中的ap_hook_pre_mpm(ap_create_scoreboard, NULL, NULL, APR_HOOK_MIDDLE)设定挂钩pre_mpm的对应处理函数则正是记分板创建函数ap_create_scoreboard。
ap_run_pre_mpm挂钩也只有在进行重新启动的时候才会调用,而在进行平稳启动的时候,并不调用这个挂钩,这样做会丢失所有的仍然正在为长期请求提供服务的子进程的信息。挂钩的引入是Apache2.0版本的一个新的实现机制,也是理解Apache核心的一个重要机制之一,关于挂钩的具体的实现细节我们在后面的部分会详细分析。
对于每次冷启动,Apache启动之后,内部的记分板的家族号都是从0开始,而每一次平稳启动后家族号则是在原先的家族号加一。
    set_signals();
当分配了记分板之后,MPM就应该设置信号处理器,一方面允许外部进程通过信号通知其停止或者重新启动,另一方面服务器应该忽略尽可能多的信号,以确保它不会被偶然的信号所中断。正常情况下,父进程需要处理三种信号:正常的重新启动、非正常的重新启动以及关闭的信号。
SIGTERM:该信号用于关闭主服务进程。信号处理函数sig_term中设置shutdown_pending=true;
SIGHUP:该信号用于重启服务器,信号处理函数中设置restart_pending=true和graceful_mode=false
SIGUSR1:该信号用于平稳启动服务器,信号处理函restart数中设置restart_pending=true和graceful_mode=true
至于信号SIGXCPU、SIGXFSZ则由默认的信号处理函数SIG_DFL处理,SIGPIPE则会被忽略。
在Apache主程序的循环中,程序不断的检测shutdown_pending,restart_pending和graceful_mode三个变量的值。通常并不是外部程序一发送信号,Apache就立即退出。最差的情况就是信号是在刚检测完就发送了,这样,主程序需要将该次循环执行完毕后才能发现发送的信号。
    if (one_process) {
        AP_MONCONTROL(1);
        make_child(ap_server_conf, 0);
    }
至此大部分准备工作已经完成,剩下的任务就是创建进程。进程的创建包括两种模式:单进程模式和多进程模式。
单进程模式通常用于Apache调试,由于不管多进程还是单进程,对HTTP请求处理以及模块等的使用都是完全相同的,区别仅仅在于效率。而多线程的调试要比单进程复杂的多。
如果是单进程调试模式,那么上面的两句程序将被程序。我们首先解释一下AP_MONCONTROL宏的含义。对于调试,一方面可能比较关心执行的正确与否,内存是否溢出等等,另外一方面就是能够找出整个服务器的运行瓶颈,只有找到了运行的瓶颈才能进行改善,从而提高效率。例如,假设应用程序花了 50% 的时间在字符串处理函数上,如果可以对这些函数进行优化,提高 10% 的效率,那么应用程序的总体执行时间就会改进 5%。因此,如果希望能够有效地对程序进行优化,那么精确地了解时间在应用程序中是如何花费的,以及真实的输入数据,这一点非常重要。这种行为就称为代码剖析(code profiling)。
An executable program compiled using the -pg option to cc(1) automatically cally includes calls to collect statistics for the gprof(1) call-graph execution profiler. In typical operation, profiling begins at program startup and ends when the program calls exit. When the program exits, the profiling data are written to the file gmon.out, then gprof(1) can be used to examine the results.
一个可执行的应用程序可以在使用gcc编译的时候利用-pg选项自动的调用相关函数收集一些执行统计信息以便gprof execution profiler使用。
moncontrol() selectively controls profiling within a program. When the program starts, profiling begins. To stop the collection of histogram ticks and call counts use moncontrol(0); to resume the collection of his-histogram togram ticks and call counts use moncontrol(1). This feature allows the      cost of particular operations to be measured. Note that an output file will be produced on program exit regardless of the state of moncontrol().
Programs that are not loaded with -pg may selectively collect profiling statistics by calling monstartup() with the range of addresses to be pro-profiled. filed. lowpc and highpc specify the address range that is to be sampled; the lowest address sampled is that of lowpc and the highest is just below highpc. Only functions in that range that have been compiled with the -pg option to cc(1) will appear in the call graph part of the output;
however, all functions in that address range will have their execution time measured.  Profiling begins on return from monstartup().
单进程的另外一个任务就是调用make_child。对于单进程,make_child非常的简单:
static int make_child(server_rec *s, int slot)
{
    int pid;
    ……
    if (one_process) {
        apr_signal(SIGHUP, sig_term);
        apr_signal(SIGINT, sig_term);
        apr_signal(SIGQUIT, SIG_DFL);
        apr_signal(SIGTERM, sig_term);
        child_main(slot);
        return 0;
    }
    /*多进程处理代码*/
}
从代码中可以看出,单进程直接调用了child_main,该函数用于直接处理与客户端的请求。在整个系统中只有一个主服务进程存在。
    else {
    if (ap_daemons_max_free < ap_daemons_min_free + 1) /* Don't thrash... */
        ap_daemons_max_free = ap_daemons_min_free + 1;
 
    remaining_children_to_start = ap_daemons_to_start;
    if (remaining_children_to_start > ap_daemons_limit) {
        remaining_children_to_start = ap_daemons_limit;
    }
    if (!is_graceful) {
        startup_children(remaining_children_to_start);
        remaining_children_to_start = 0;
    }
    else {
        hold_off_on_exponential_spawning = 10;
    }
对于多进程模式而言,处理要复杂的多。与多进程类似,上面的代码负责创建子进程。在创建之前对其中使用到的几个核心变量进行必要的调整,这几个变量的含义分别如下:
ap_daemons_max_free:服务器中允许存在的空闲进程的最大数目,一旦超过这个数目,一部分空闲进程就会被迫终止,直到最后的空闲进程数目降低到该值。
ap_daemons_min_free:服务器中允许存在的空闲进程的最小数目,一旦低于这个数目,服务器将创建新的进程直到最后空闲进程数目抵达这个数目。任何时候空闲进程的数目都维持在ap_daemons_max_free和ap_daemons_min_free之间。
ap_daemons_limit:服务器中允许存在的进程的最大数目。包括空闲进程、忙碌进程以及当前的记分板中的空余插槽。
ap_daemons_to_start:服务器起始创建的进程数目。这个值不能超出ap_daemons_limit。
remaining_child_to_start:需要启动的子进程的数目。对于初始启动,remaining_child_to_start的值就是ap_daemons_to_start的值。因此服务器是刚启动,那么函数直接调用start_children创建remaining_child_to_start个子进程,同时将remaing_child_to_start设置为零。
对于平稳启动,remaining_child_to_start的含义则要发生一些变化。
 
如果我们所进行的是平稳启动,那么在我们进入下面的主循环之前应该可以观察到相当多的子进程立即陆续退出,其中的原因则是因为我们向它们发出了AP_SIG_GRACEFUL信号。这一切发生的非常的快。对于每一个退出的子进程,我们都将启动一个新的进程来替换它直到进程数目达到ap_daemons_min_free。因此
    restart_pending = shutdown_pending = 0;
    mpm_state = AP_MPMQ_RUNNING;
 
    while (!restart_pending && !shutdown_pending) {
        int child_slot;
        apr_exit_why_e exitwhy;
        int status, processed_status;
        apr_proc_t pid;
至此,主服务进程则可以进入循环,它所作的事情只有两件事情,一个是负责对服务器重新启动或者关闭,另一个就是负责监控子进程的数目,或者关闭多余的空闲子进程或者在空闲子进程不够的时候启动新的子进程。restart_pending用于指示服务器是否需要进行重新启动,为1的话表明需要重启;shutdown_pending则指示是否需要关闭服务器,为1则表明需要关闭。除此之外,graceful用于指示是否进行平稳启动。当外界需要对主进程进行控制的时候只需要设置相应的变量的值即可,而主进程中则根据这些变量进行相应的处理。
        ap_wait_or_timeout(&exitwhy, &status, &pid, pconf);
如果restart_pending=0并且shutdown_pending=0的话意味着外部进程不需要服务器终止或者重新启动,此时主服务进程将进入无限循环,监视子进程。对于平稳启动而言,正常情况下,在每一轮循环中,主服务进程都会调用ap_wait_or_timeout()等待子进程终止。通常情况下,子进程的退出有三种可能,分别枚举类型apr_exit_why_e进行描述
1.正常退出,此时APR_PROC_EXIT=1,这种情况通常是进程所有任务完成后退出
2.信号退出,此时APR_PROC_SIGNAL=2,这种情况通常是进程在执行过程中接受到信号半途退出
3.非正常退出,此时APR_PROC_SIGNAL_CORE=4,通常是进程意外中断,同时生成core dump文件。
        if (pid.pid != -1) {
            processed_status = ap_process_child_status(&pid, exitwhy, status);
            if (processed_status == APEXIT_CHILDFATAL) {
                mpm_state = AP_MPMQ_STOPPING; uvwxy
                return 1;
            }
主进程通过ap_wait_or_timeout监视等待每一个子进程退出,同时在exitwhy中保存它们退出的原因。尽管如此,主进程并不会无限制的等待下去。主进程会给出一个等待的超时时间,一旦超时,主进程将会不再理会那些尚未结束的进程,继续执行主循环的剩余部分。如果子进程在规定的时间内完成,那么即等待成功,此时该被终止的子进程的pid.pid将不为-1,否则pid.pid将为-1。
一旦等待到子进程退出,那么进程退出的原因保存在processed_status中。如果processed_satus为APEXIT_CHILDFATAL,则表明发生了致命性的错误,这时候将导致整个服务器的崩溃,此时主进程直接退出,不对记分板做任何的处理,如u所示;
            child_slot = find_child_by_pid(&pid);
            if (child_slot >= 0) {
                (void) ap_update_child_status_from_indexes(child_slot, 0, SERVER_DEAD,
                                                           (request_rec *) NULL);u
 
                if (processed_status == APEXIT_CHILDSICK) {
                    idle_spawn_rate = 1; v
                }
                else if (remaining_children_to_start
                    && child_slot < ap_daemons_limit) {
                    make_child(ap_server_conf, child_slot);
                    --remaining_children_to_start;
                }
#if APR_HAS_OTHER_CHILD
            }
如果子进程发生的错误并不是致命性的,那么一切都得按部就班的处理——更新记分板中对应的插槽中的信息。首要的前提就是在记分板中找到该进程对应的插槽,这由函数find_child_by_pid()完成。
如果能够成功找到终止进程对应的插槽,那么直接在记分板中将该终止进程的状态更新为SERVER_DEAD,这样,该插槽将会再次可用,如u所示。
如果进程退出是因为资源受限,比如磁盘空间不够,内存空间不够等等,此时Apache必须降低生成子进程的速度至最低。如v所示。
如果Apache进行的是平稳重启,那么在进入主循环之前,通过发送终止信号,很多的子进程都将被终止,这些被终止的进程在系统重启后必须被新的进程替换,直到总的进程数目达到daemons_min_free。remaining_children_to_start记录了当前需要重启的子进程的数目。
            else if (apr_proc_other_child_alert(&pid, APR_OC_REASON_DEATH, status)
== APR_SUCCESS) {
#endif
            }
            else if (is_graceful) {
                ap_log_error(APLOG_MARK, APLOG_WARNING,
                            0, ap_server_conf,
                            "long lost child came home! (pid %ld)", (long)pid.pid);
            }
            continue;
        }
如果进程在公告板中没有找到相应记录,此时检查子进程是否是“其余子进程”(reap_other_child)。一些情况下,主服务进程会创建一些进程,这些进程并不是用来接受并处理客户端连接的,而是用作其余用途,通常称之为“其余进程”,并用一个单独的列表进行登记。比如,一般情况下,Apache会将日志写入到文件中,但是有的时候Apache则希望将数据写入到一个给定的应用程序中。因此主服务器进程必须为该应用程序创建该进程,并且将该进程的标准输入STDIN关联道主服务进程的日志流中。这种进程并不是用来执行处理HTTP请求的子服务进程,因此称之为“其余进程”。任何时候只要服务器重启,日志应用进程都会接受到SIGHU和SIGUSR1信号,然后终止退出,对应的模块必须重新创建这种进程。对于其余进程,主服务进程不做任何事情,因为这不是主进程所管辖的范围。
如果既不是“其余子进程”,又没有在公告板中找到相应记录,同时管理员还设置了热启动选项,那么发生这种情况只有一个可能性:管理员减少了允许的子进程的数目同时强制执行了热启动。而一个正在忙碌的子进程拥有的入口记录号比允许的值大。此时它终止的时候自然就不可能在公告板中找到相应的记录入口。
        else if (remaining_children_to_start) {
            startup_children(remaining_children_to_start);
            remaining_children_to_start = 0;
            continue;
        }
如果当所有的终止的子进程都被替换之后,remaining_children_to_start还不为零,此时意味着主服务进程必须创建更多的子进程,这个可以通过函数startup_children()实现。
        perform_idle_server_maintenance(pconf);
#ifdef TPF
        shutdown_pending = os_check_server(tpf_server_name);
        ap_check_signals();
        sleep(1);
#endif /*TPF */
    }
    } /* one_process */
一旦启动完毕,那么主进程将使用perform_idle_server_maintenance进入空闲子进程维护阶段,同时主进程还得监视相关的信号,比如关闭信号,重启信号等等。空闲进程的维护在4.2.1.2中详细描述。
当主进程退出循环while (!restart_pending && !shutdown_pending) 的时候只有两种情况发生,或者被通知关闭,或者被通知重启。一旦如此,Apache将着手进行相关的清除工作。
    mpm_state = AP_MPMQ_STOPPING;
 
    if (shutdown_pending) {
        if (unixd_killpg(getpgrp(), SIGTERM) < 0) {
            ap_log_error(APLOG_MARK, APLOG_WARNING, errno, ap_server_conf,
"killpg SIGTERM");
        }
       ap_reclaim_child_processes(1);          /* Start with SIGTERM */
 
        /* cleanup pid file on normal shutdown */
        {
            const char *pidfile = NULL;
            pidfile = ap_server_root_relative (pconf, ap_pid_fname);
            if ( pidfile != NULL && unlink(pidfile) == 0)
                ap_log_error(APLOG_MARK, APLOG_INFO,
                                0, ap_server_conf,
                                "removed PID file %s (pid=%ld)",
                                pidfile, (long)getpid());
        }
 
        ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf,
                    "caught SIGTERM, shutting down");
        return 1;
    }
如果Apache需要进行关闭,那么Apache的清除工作工作包括下面的几个方面:
■ 如果服务器被要求关闭,那么主服务进程将向整个进程组中的所有子进程发送终止信号,通知其调用child_exit正常退出。
■ 调用ap_reclain_child_process回收相关的子进程。
■ 清除父子进程之间通信的“终止管道”。
    apr_signal(SIGHUP, SIG_IGN);
    if (one_process) {
        /* not worth thinking about */
        return 1;
    }
 
    ++ap_my_generation;
    ap_scoreboard_image->global->running_generation = ap_my_generation;
   
    if (is_graceful) {
        ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf,
                    "Graceful restart requested, doing restart");
 
        /* kill off the idle ones */
        ap_mpm_pod_killpg(pod, ap_max_daemons_limit);
 
        /* This is mostly for debugging... so that we know what is still
         * gracefully dealing with existing request. This will break
         * in a very nasty way if we ever have the scoreboard totally
         * file-based (no shared memory)
         */
        for (index = 0; index < ap_daemons_limit; ++index) {
            if (ap_scoreboard_image->servers[index][0].status != SERVER_DEAD) {
                ap_scoreboard_image->servers[index][0].status = SERVER_GRACEFUL;
            }
        }
    }
如果Apache被要求的是重新启动,那么对于平稳启动和非平稳启动处理则不太相同。对于平稳启动而言,主进程需要终止的仅仅是那些目前空闲的子进程,而忙碌的进程则不进行任何处理。空闲进程终止通过ap_mpm_pod_killpg实现;同时由于记分板并不销毁,因此对于那些终止的进程,必须更新其在记分板中的状态信息为SERVER_DEAD;而对于那些仍然活动的子进程,则将其状态更新为SERVER_GRACEFUL。
    else {
       /* Kill 'em off */
        if (unixd_killpg(getpgrp(), SIGHUP) < 0) {
            ap_log_error(APLOG_MARK, APLOG_WARNING, errno, ap_server_conf, "killpg SIGHUP");
        }
        ap_reclaim_child_processes(0);          /* Not when just starting up */
        ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf,
                    "SIGHUP received. Attempting to restart");
    }
如果服务器被要求强制重启,那么所有的子进程包括那些仍然在处理请求的都将被统统终止。

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章