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");
}
如果服务器被要求强制重启,那么所有的子进程包括那些仍然在处理请求的都将被统统终止。
6.3.3.4空闲子进程维护
6.3.3.4.1概述
主服务进程一方面除了必须维护平稳启动之外,另外一个最重要的职责就是对空闲子进程的数目进行管理,整个空闲管理功能在perform_idle_server_maintenance()中描述。
空闲进程的整个内部是示意图可以用下面的图进行描述。
6.3.3.4.2代码分析
static void perform_idle_server_maintenance(apr_pool_t *p)
{
int i;
int to_kill;
int idle_count;
worker_score *ws;
int free_length;
int free_slots[MAX_SPAWN_RATE];
int last_non_dead;
int total_non_dead;
/* initialize the free_list */
free_length = 0;
to_kill = -1;
idle_count = 0;
last_non_dead = -1;
total_non_dead = 0;
在分析具体的空闲子进程维护过程之前,函数中的几个重要的变量需要解释一下:
ap_daemons_limit,该值描述了当前Apache中允许存在的子进程的最多数目,或者是空闲进程、非空闲进程数目以及公告板中空闲的插槽数目的总和、或者是公告板中插槽的总和。
ap_daemons_max_free,该值描述了当前系统中允许存在的空闲子进程的最大数目。如果空闲子进程数目过多,那么在每一次循环中都会有一个空闲进程被杀死。
ap_daemons_min_free,该值描述了系统中至少必须存在的空闲子进程的数目。如果当前的空闲子进程数目过低,那么主服务进程将创建新的子进程。如果当前公告板中没有可用插槽(因为ap_daemons_limit已经达到),此时将产生一个警告。
如果在短时间内系统中创建了太多的子进程,一些操作系统可能会性能降低。因此主服务进程并不是立即调用make_child()创建所需的所有子进程。相反,它采用递增创建的策略:在第一次循环中创建一个子进程;在第二次循环中创建二个子进程,第三次创建四个,第N次创建2N个。下一个循环中需要创建的子进程数目用变量idle_spawn_rate进行记录,每次循环中都会对该变量进行递增直到其达到极限值。
举例说明:如果给定的ap_daemons_min_free的值为5,但是系统中的空闲进程数目仅为1。因此主服务进程此时创建一个新的进程,同时进行等待。当然两个进程是不够的,因此主进程在第二次循环中创建2个进程同时继续等待。如果此时一个新的客户端连接发生,那么一个空闲进程将转换忙碌进程。因此主服务进程统计的空闲进程数目为3,同时创建四个空闲进程。当超时的时候,主服务进程统计出7个空闲子进程同时重新设置idle_spawn_rate为1。
ap_max_daemons_limit, Necessary to deal with MaxClients changes across AP_SIG_GRACEFUL restarts. 尽管Apache内部允许生成的最大进程数为ap_daemons_limit,但是实际上每次产生的进程数目不一定会有这么多。每一个进程都对应记分板中的一个插槽。为了了解各个进程的状态,MPM必须逐一循环遍历记分板中的每一个插槽,这样共计ap_daemons_limit次。显然一些无效的插槽也进行了遍历,这部分本来可以避免的。为此,MPM中使用ap_max_daemons_limit记录记分板中曾经使用的最大的插槽号,一旦记录下来,遍历不再是从0到ap_daemons_limit,而是从0到ap_max_daemons_limit,可以省去ap_daemons_limit - ap_max_daemons_limit-1次的循环。这是一种优化策略。
last_non_dead与ap_max_daemons_limit的含义非常的相近。
idle_count:当前服务器中的空闲进程的数目。
totaol_non_dead:当前服务器中的活动进程的数目,包括空闲进程和非空闲进程。
last_non_dead:
idle_spawn_rate
:这是另外一个重要的变量。当MPM模块发现空闲子进程数目current_num少于ap_min_daemons_limit的时候,它将会产生足够的进程ap_min_daemons_limit-current_num个。一般的操作系统允许一次性的产生所需要的进程。不过一些操作系统则不然,如果一次性在很短的时间内产生大量的系统进程,操作系统性能会明显的降低,从而导致服务器响应变慢。这种情况要尽量避免。因此Apache并没有采用这种“激进”的创建措施,而是采取了折衷的温和的逐步递增创建策略:在第一次循环中创建一个,第二次循环中创建二个,第三次循环中创建四个,第N次循环中创建2N个,直到1+2+4+…+2N值达到所需要的进程数。idle_spawn_rate指示下一个循环中必须创建的进程数目,记住不是本次循环中。
两个变量就是free_length和free_slots,这两个变量我们在稍后描述。
只有对这些变量的含义有了清晰的认识之后我们才能进行分析。
for (i = 0; i < ap_daemons_limit; ++i) {
int status;
if (i >= ap_max_daemons_limit && free_length == idle_spawn_rate)
break;u
ws = &ap_scoreboard_image->servers[i][0];
status = ws->status;
if (status == SERVER_DEAD) {
if (free_length < idle_spawn_rate) {
free_slots[free_length] = i;
++free_length;
}
}
else {
if (status <= SERVER_READY) {
++ idle_count;
to_kill = i;
}
++total_non_dead;
last_non_dead = i;
}
}
Apache所作的第一件事情就是要统计当前运行的各类进程的信息,包括空闲进程,终止进程以及忙碌进程的数目。为此它必须能够逐一访问记分板中的每一个插槽并读取相应的进程信息,其中我们最关心的就是进程的状态信息:
如果插槽的状态SERVER_DEAD,则意味着对应的进程已经终止,该插槽可以被再次利用;所有可以被再次利用的插槽统一的保存在free_slot数组中。数组中仅仅保存插槽的索引号。其能保存的最多数目为32个,但是通常情况下不是所有的元素都会被使用,因此配合free_slots数组,free_length用于记录当前的最高可用的元素的索引。因此free_slots的操作更像是一个堆栈,每次的元素总是压入最顶部。
比如上图的free_slots就反映了当前记分板中插槽索引为2,3,7,13,14的进程已经终止。
如果插槽的状态为SERVER_STARTING或者是SERVER_READY,则意味着当前的进程处于空闲状态;在遍历过程中,一旦发现空闲进程,idle_count的值将递增1,因此当上面的整个循环结束的时候,idle_count则就是实际的空闲进程数。
另外在Linux中,进程编号如果较低的话,在调度的时候其被命中的概率也就越高,因此如果需要终止某些进程的话,Apache会倾向于终止那些编号较高的进程,为此模块中使用to_kill变量跟踪当前的最高进程编号,这样下次终止进程直接终止to_kill指定的即可。
如果进程状态既不是SERVER_DEAD,也不是SERVER_READY,则意味着该线程正在处理客户端的请求。对于这些进程直接累计total_non_dead变量并设置last_non_dead。
不过如前所叙,经过优化后,MPM不需要遍历所有的记分板插槽了,只需要遍历ap_max_daemons_limit即可。
free_length == idle_spawn_rate意味着
ap_max_daemons_limit = last_non_dead + 1;
if (idle_count > ap_daemons_max_free) {
ap_mpm_pod_signal(pod);
idle_spawn_rate = 1;
}
一旦获取了系统中空闲进程的数目,模块则开始对进程进行调整:
ap_daemons_max_free是Apache中允许的空闲进程的最大值,如果当前的空闲进程数目idle_count超过该值,那么多余的空闲进程必须退出。一般情况下,父进程可以强制子进程立即退出,但是如果某些进程正在处理客户端的请求,那么该连接将会被粗鲁的终止,造成数据丢失,为此Apache使用“终止管道(Pipe of Death)”通知空闲子进程退出,这样需要退出的子进程可以执行平稳的退出,以防止连接数据丢失。ap_mpm_pod_signal函数用于在终止管道中写入终止数据。不过需要注意的是,每次循环只能退出一个子进程,因此如果需要终止的线程有N个,那么至少需要循环N次才能全部退出。这种策略称之为“缓慢退出”,有利于防止进程“创建/终止”摆动。
else if (idle_count < ap_daemons_min_free) {
if (free_length == 0) {
static int reported = 0;
if (!reported) {
ap_log_error(APLOG_MARK, APLOG_ERR, 0, ap_server_conf,
"server reached MaxClients setting, consider"
" raising the MaxClients setting"); u
reported = 1;
}
idle_spawn_rate = 1;
}
else {
if (idle_spawn_rate >= 8) {
ap_log_error(APLOG_MARK, APLOG_INFO, 0, ap_server_conf,
"server seems busy, (you may need "
"to increase StartServers, or Min/MaxSpareServers), "
"spawning %d children, there are %d idle, and "
"%d total children", idle_spawn_rate,
idle_count, total_non_dead); v
}
如果当前空闲进程的数目低于允许的最少进程数目,那么此时MPM必须增加空闲进程数目。在前面的部分,我们曾经说过,idle_spawn_rate表示本次循环中需要产生的子进程的数目。如果idle_spawn_rate为8,则意味着本次循环需要产生8个子进程。至此,系统必须连续产生1+2+4+8=15个进程,显然如果出现这种情况则意味着系统实在太忙了。这可能是因为初始启动的服务太低或者允许的空闲进程指标Min/MaxSpareServers太小,因此此时必须增加这些参数的值,并在日志中写入警告。如v所示。
如果free_length=0,意味着当前free_slots中没有可用的插槽,这表明当前系统中的所有的进程都处于活动状态:或者忙碌或者空闲等待请求。这种情况的出现可能是因为MaxClient参数设置过低,因此有必要增加MaxClient的值。
for (i = 0; i < free_length; ++i) {
#ifdef TPF
if (make_child(ap_server_conf, free_slots[i]) == -1) {
if(free_length == 1) {
shutdown_pending = 1;
ap_log_error(APLOG_MARK, APLOG_EMERG, 0, ap_server_conf,
"No active child processes: shutting down");
}
}
#else
make_child(ap_server_conf, free_slots[i]);
#endif /* TPF */
}
尽管原则上第N次可以产生2
N
的进程,但是实际上真正能够产生的进程数目还得由记分板中的空闲插槽数目free_length决定。生成具体的子进程使用make_child例程,在后面的部分会详细的对其进行分析。
if (hold_off_on_exponential_spawning) {
--hold_off_on_exponential_spawning;
}
else if (idle_spawn_rate < MAX_SPAWN_RATE) {
idle_spawn_rate *= 2;
}
在进程产生后紧接着的任务就是设置下一循环中需要产生的进程数目:idle_spawn_rate*2;当然这个值不能超出允许产生的最大值MAX_SPAWN_RATE。
}
}
else {
idle_spawn_rate = 1;
}
6.3.3.4.3子进程创建
主进程的一个很重要的任务就是在空闲进程不够的情况下创建足够的子进程。子进程的创建在函数make_child中进行。
static int make_child(server_rec *s, int slot)
对于任何一个创建的子进程,其都必须在计分板中占据一个插槽来保存自己的信息,slot就是创建的进程在计分板中的插槽索引,不过有一点需要确保的slot的值不能超过系统中允许的进程极限数,即slot必须满足slot <= ap_max_daemons_limit -1
整个子进程的创建过程可以分割为下面二个步骤:
(1)、更新计分板。一旦进程创建,它的状态将被设置为SERVER_STARTING,表明该进程开始运行。
(2)、调用fork()真正生成子进程。如果生成子进程失败,还得将原先的计分板SERVER_STARTING状态更新为SERVER_DEAD。一旦更新完毕,该插槽将再次变得可用。当fork函数调用失败的时候,为了防止系统不停的尝试去重新fork从而将CPU资源耗尽,因此一旦如果fork失败,那么Apache将等待10毫秒后再去尝试新的fork。
对于子进程而言,一旦其创建完毕,除了正常了父子进程之间的通信,子进程不应该再受到父进程的其余的无端打断,因此子进程必须重新SIGHUP和SIGTERM信号。任何时候子进程接受到SIGHUP和SIGTERM信号之后,除了退出之外,再退出之前还需要完成几个清除工作,包括:
■ 关闭父子进程之间通信的“终止管道”
■
尽管父进程会发送AP_SIG_GRACEFUL给子进程,但子进程并不对其进行处理:apr_signal(AP_SIG_GRACEFUL, SIG_IGN);一旦子进程处理完所有的准备工作,其将开始进入子进程的内部处理过程child_main()。
另一方面,在生成子进程之后,主进程则必须将子进程号填入记分板的相关插槽中:
ap_scoreboard_image->parent[slot].pid = pid;
如果服务器是单进程运行模式,那么处理的工程要简单的多。由于不涉及到创建子进程,因此实际上主进程本身直接进入内部循环操作。另外与多进程相比,它仅仅处理三种信号:SIGINT、SIGTERM、SIGQUIT。
另外一种子进程生成方式就是批量子进程生成,即函数static void startup_children(int number_to_start)。number_to_start是批量产生的进程的数目。
static void startup_children(int number_to_start)
{
int i;
for (i = 0; number_to_start && i < ap_daemons_limit; ++i) {
if (ap_scoreboard_image->servers[i][0].status != SERVER_DEAD) {
continue;
}
if (make_child(ap_server_conf, i) < 0) {
break;
}
--number_to_start;
}
}
在计分板中,如果某个进程的状态为SRVER_DEAD,则意味着当前的记分板插槽可用。因此对于需要创建的number_to_start个进程,需要通过逐一遍历计分板从而才可以给创建的进程分配插槽。一旦分配成功,那么函数将调用make_child创建进程。
6.3.3.5工作子进程管理
子进程通常被视为工作者,其组成了HTTP服务器的核心。它们负责处理对客户端的请求的处理。尽管多任务体系结构并不负责对请求的处理,不过他仍然负责创建子进程、对其进行初始化并且将客户端请求转交给它们进行处理。子进程的所有的行为都被封状在函数child_main()中。
6.3.3.5.1子进程的创建
在深入到子进程工作的内部细节之前,我们有必要了解一下主服务进程是如何创建子进程的。事实上,从主服务进程的最后的代码中也可以看出,主服务进程是通过调用make_child函数来创建一个子进程的,该函数定义如下:
static int make_child(server_rec *s, int slot)
该函数具有两个参数,slot是当前进程在记分板中的索引。
u的代码主要用于处理单进程。如前所述,单进程主要用于调试。对于单进程服务器而言,唯一的进程就是主服务进程,因此不需要创建任何额外的子进程,需要处理的就是转换主服务进程的角色为子服务进程,这种转变包括两部分:
1)、处理信号。
6.3.3.5.2初始化、配置以及服务重启
(
void
) ap_update_child_status_from_indexes(slot,
0
, SERVER_STARTING,
(request_rec
*
) NULL);
if
((pid
=
fork())
==
-
1
)
...
{
ap_log_error(APLOG_MARK, APLOG_ERR, errno, s, "fork: Unable to fork new process");
(void) ap_update_child_status_from_indexes(slot, 0, SERVER_DEAD,
(request_rec *) NULL);
sleep(10);
return-1;
}
if
(
!
pid)
...
{
RAISE_SIGSTOP(MAKE_CHILD);
AP_MONCONTROL(1);
apr_signal(SIGHUP, just_die);
apr_signal(SIGTERM, just_die);
apr_signal(AP_SIG_GRACEFUL, stop_listening);
child_main(slot);
}
ap_scoreboard_image
->
parent[slot].pid
=
pid;
return
0
;
}
主服务进程使用fork()创建子进程。每个子进程都具有独立的内存区域并且不允许读取其余子进程的内存。因此由主服务器一次处理配置文件要比由各个子进程各自处理明智的多。配置文件的相关信息可以保存在共享内存区域中,该内存区域可以被每一个子进程读取。由于不是每一个操作系统平台都支持共享内存的概念,因此主服务进程在创建子进程之前处理配置文件。子服务进程通常是父进程的克隆,因此它们与主服务进程具有相同的配置信息而且从来不会改变。
任何时候,如果管理员想更改服务器配置。他都必须让主服务进程重新读取配置信息。当前存在的子进程所拥有的则是旧的配置信息,因此它们都必须被替换为新产生的子进程。为了避免打断正在处理的HTTP请求,Apache提供了一种平稳启动的方法,该模式下允许子进程使用旧的配置信息进行处理直到其退出。
子进程的初始化可以在相应的MPM中找到,对于预创建Preforking MPM而言就是child_main()。该函数包括下面的几个步骤:
调用ap_init_child_modules()重新初始化模块:每一个模块都由主进程预先进行初始化。如果模块中分配系统资源或者决定于进程号,那么模块重新进行初始化就是必须的。
建立超时处理句柄:为了避免子进程的无线阻塞,Apache对客户端请求使用超时处理。其中使用警告,警告的概念与信号的概念非常类似,通常将报警时钟设置为给定的时间,而当报警响起的时候系统将离开请求的处理。
循环内还有两种初始化:
清除超时设置:重置报警定时器
清除透明内存池:在请求响应循环中每个内存的分配都涉及到透明内存池。在循环的开始,内存池必须进行清理。
将公告板中的status设置为ready。
ptrans的创建,以及访问公告板,同时由于多个进程之间可能存在竞争,因此另外一个准备工作就是创建进程间的接受互斥锁。由于通常情况下,父进程都是使用fork生成子进程,此时子进程基本是父进程的克隆。一般情况下,Apache的启动都是使用超级用户进行的,因此子进程实际上也就具有与父进程等同的操作权限,父进程能够访问的资源子进程都能够访问。
static
void
child_main(
int
child_num_arg)
...
{
apr_pool_t *ptrans;
apr_allocator_t *allocator;
conn_rec *current_conn;
apr_status_t status = APR_EINIT;
int i;
ap_listen_rec *lr;
int curr_pollfd, last_pollfd =0;
apr_pollfd_t *pollset;
int offset;
void*csd;
ap_sb_handle_t *sbh;
apr_status_t rv;
apr_bucket_alloc_t *bucket_alloc;
mpm_state = AP_MPMQ_STARTING;
my_child_num = child_num_arg;
ap_my_pid = getpid();
csd = NULL;
requests_this_child =0;
ap_fatal_signal_child_setup(ap_server_conf);
apr_allocator_create(&allocator);
apr_allocator_max_free_set(allocator, ap_max_mem_free);
apr_pool_create_ex(&pchild, pconf, NULL, allocator);
apr_allocator_owner_set(allocator, pchild);
apr_pool_create(&ptrans, pchild);
apr_pool_tag(ptrans, "transaction");
ap_reopen_scoreboard(pchild, NULL, 0);
rv = apr_proc_mutex_child_init(&accept_mutex, ap_lock_fname, pchild);
if (rv != APR_SUCCESS) ...{
ap_log_error(APLOG_MARK, APLOG_EMERG, rv, ap_server_conf,
"Couldn't initialize cross-process lock in child");
clean_child_exit(APEXIT_CHILDFATAL);
}
每个子进程在真正处理请求之前,都必须进行相关的资源准备工作,包括信号设置,私有内存池
Apache通常会将子进程的用户设置为一个普通的用户,比如nobody或者WWWRun之类从而来降低子进程的执行权限,原则上,子进程用户的权力应该尽可能的小。Unixd_setup_child将用户的ID从正在运行父进程的用户改变为在配置文件中规定的用户,如果不能改变用户的ID,子进程就立即退出。另外相关的初始化工作必须在unixd_setup_child()调用之前进行,因为一旦子进程权限降低,一些只能超级用户进行的初始化可能无法正常进行。
if
(unixd_setup_child())
...
{
clean_child_exit(APEXIT_CHILDFATAL);
}
但是子进程具有与父进程相同的权限具有一定的潜在的危险。由于网络连接通常由子进程直接处理,因此如果黑客通过某种权限控制了子进程,那么他就能够任意的控制系统的。因此通常在进行了资源准备工作之后,
Ap_run_child_init调用child_init挂钩进行子进程本身的初始化。
ap_run_child_init(pchild, ap_server_conf);
ap_create_sb_handle(
&
sbh, pchild, my_child_num,
0
);
(
void
) ap_update_child_status(sbh, SERVER_READY, (request_rec
*
) NULL);
当所有的准备工作结束以后,子进程可以与客户进行会话。
80,但是在Apache中,则允许服务器在多个端口上同时进行侦听,这些侦听端口用结构ap_listen_rec进行描述:
listensocks
=
apr_pcalloc(pchild,
sizeof
(
*
listensocks)
*
(num_listensocks));
for
(lr
=
ap_listeners, i
=
0
; i
<
num_listensocks; lr
=
lr
->
next, i
++
)
...
{
listensocks[i].accept_func = lr->accept_func;
listensocks[i].sd = lr->sd;
}
pollset
=
apr_palloc(pchild,
sizeof
(
*
pollset)
*
num_listensocks);
pollset[
0
].p
=
pchild;
for
(i
=
0
; i
<
num_listensocks; i
++
)
...
{
pollset[i].desc.s = listensocks[i].sd;
pollset[i].desc_type = APR_POLL_SOCKET;
pollset[i].reqevents = APR_POLLIN;
}
mpm_state
=
AP_MPMQ_RUNNING;
bucket_alloc
=
apr_bucket_alloc_create(pchild);
一般的情况下,服务器只会侦听固定的端口,比如
描述了绑定到该端口的套接字,而bind_addr则描述了套接字必需关联的地址。Accept_func是一个回调函数,当从该侦听端口上接受到客户端连接的时候,该函数将被执行从而来处理连接。Active用以描述当前端口是否处于活动状态。
struct
ap_listen_rec
...
{
ap_listen_rec *next;
apr_socket_t *sd;
apr_sockaddr_t *bind_addr;
accept_function accept_func;
int active;
}
;
sd
对于服务器端的多个侦听端口,Apache使用链表进行保存,因此next用以指向下一个侦听套接字结构。整个链表的用ap_listen_rec全局变量记录,因此沿着ap_listen_rec可以遍历所有的侦听套接字。与此同时,侦听端口的数目也保存在全局变量num_listensocks中。
上面的代码所作的事情无非就是生成指定的需要逐一遍历的文件结果集合。
任何时候,子进程如果要正常退出,其都必须由主进程通过“终止管道”通知,另一方面,子进程也将不停的检查终止管道。一旦发现需要退出,子进程将die_now设置为1,这时候实际上就自动退出循环。相反,如果子进程不需要退出,那么它所作的事情只有一个,就是使用poll对所有的端口进行轮询,直到某个端口准备完毕,则调用相关的连结处理函数进行处理。
while
(
!
die_now)
...
{
current_conn = NULL;
apr_pool_clear(ptrans);
if ((ap_max_requests_per_child >0
&& requests_this_child++>= ap_max_requests_per_child)) ...{
clean_child_exit(0);
}
(void) ap_update_child_status(sbh, SERVER_READY, (request_rec *) NULL);
SAFE_ACCEPT(accept_mutex_on());
虽然多个子进程同属于一个父进程,但是多个子进程之间则是相互并行的,当多个子进程同时扫描侦听端口的时候,很可能发生多个子进程同时竞争一个侦听端口的情况。因此所有的子进程有必要互斥的等待TCP请求。
接受互斥锁能够确保只有一个子进程独占的等待TCP请求(使用系统调用accept())——这些都是侦听者所做的事情。接受互斥锁是控制访问TCP/IP服务的一种手段。它的使用能够确保在任何时候只有一个进程在等待TCP/IP的连接请求。
不同的操作系统有不同的接受互斥锁(Accept Mutex)的实现。有一些操作系统对于每一个子进程需要一个特殊的初始化阶段。它的工作方式如下:
调用过程accept_mutex_on():申请互斥锁或者等待直到该互斥锁可用
调用过程accept_mutex_off():释放互斥锁
prefork MPM中通过SAFE_ACCEPT(accept_mutex_on())实现子进程对互斥锁的锁定;而SAFE_ACCEPT(accept_mutex_off())则是完成互斥锁的释放。
if (num_listensocks == 1) {
offset = 0;
}
对于整个侦听套接字数组而言,任何时候只有一个侦听端口能被处理。Offset实际上描述了当前正在被处理的侦听端口在数组中的索引。如果当前服务器的侦听端口只有一个,那么几乎没有任何事情要做,也就没有所谓的轮询。
如果服务器配置使用多个侦听端口,那么Apache就必须使用poll()来确定客户正在连接哪个端口,然后我们就可以知道哪个端口正在受到访问,这样才能在这个端口上调用接受函数。如果轮询返回的值是EBADF,EINTR或者EINVAL之类的错误,那么轮询并不应该被终止,但是如果返回的不是这些错误,那么子进程应该调用clean_child_exit退出。
else
...
{
for (;;) ...{
apr_status_t ret;
apr_int32_t n;
ret = apr_poll(pollset, num_listensocks, &n, -1);
if (ret != APR_SUCCESS) ...{
if (APR_STATUS_IS_EINTR(ret)) ...{
continue;
}
ap_log_error(APLOG_MARK, APLOG_ERR, ret, ap_server_conf,
"apr_poll: (listen)");
clean_child_exit(1);
}
尽管服务器可能会同时侦听多个端口,但有的时候各个端口的重要性并不是一样的。比如某个服务器开通了80和8080两个端口,但是可能频繁使用的是80,而8080则只是偶尔使用,如果不加任何控制的话,8080端口可能会被忽略。为此,大多数MPM都会记住最后提供服务的那个端口,并且从这个端口开始搜索新的连接,通过这种方法,就可以确保服务器不会忽略任何端口。Last_pollfd用于标记最后提供服务的端口。Curr_pollfd是当前需要处理的端口的索引。
curr_pollfd
=
last_pollfd;
do
...
{
curr_pollfd++;
if (curr_pollfd >= num_listensocks) ...{
curr_pollfd =0;
}
if (pollset[curr_pollfd].rtnevents & APR_POLLIN) ...{
last_pollfd = curr_pollfd;
offset = curr_pollfd;
goto got_fd;
}
}
while
(curr_pollfd
!=
last_pollfd);
continue
;
}
}
got_fd:
status = listensocks[offset].accept_func(&csd,
&listensocks[offset], ptrans);
SAFE_ACCEPT(accept_mutex_off()); /* unlock after "accept" */
if (status == APR_EGENERAL) {
clean_child_exit(1);
}
else if (status != APR_SUCCESS) {
continue;
}
上面的代码用于接受客户端的连接。事实上,从上面的代码可以看到,接受客户端连接并没有直接使用apr_accept函数,而是将其作为上面描述的ap_listen_rec结构组成部分的函数指针。接受函数会进行有效的错误检查,并返回有效的套接字。使用这种函数指针策略的还会得到额外的好处。模块可以在侦听套接字列表中增加他们自己的通信原语。如果这些套接字不是正常的套接字,比如是UNIX IPC套接字,那么他们可能就需要不同的函数进行处理,此时,accept_func指着就可以指向这些函数。增加这么代码,不仅可以允许在内核缓存和Web服务器之间进行通信,而且可以用于其余的方面,例如,许多UNIX MPM都同通过POD向子进程发出关闭信号,如前所述,如果将POD强行编入代码中,它就不容易维护,而使用accept_func函数,POD就可以实现特殊的处理函数,实现无缝处理。
一旦接受了连接请求,一个子进程将释放互斥锁同时处理请求——此时它变成一个工作者,而下一个进程将继续申请互斥锁从而进行等待。这通常称之为Leader-follower模式:侦听者是leader,而空闲的工作者则是follower。由于Apache实现互斥锁使用的是操作系统相关技术,因此有些操作系统上,当一个子进程接受到连接释放互斥锁的时候,当前的所有的阻塞的子进程都将被唤醒。如果是这样,那么一些过分的调度将是不必要的,因此只有一个子进程会得到互斥锁,而其余的都将继续被阻塞睡眠。这个问题是Leader MPM需要解决的,在该MPM中,所有的follower进行一定的组织,从而当互斥锁释放的时候,只有它们中间的一个会被唤醒。
一旦子进程接受到一个客户端连接,那么多任务模块的职责也就结束了。子进程将继续调用请求处理例程进行处理。不管对于任何MPM,它们都是一样的。
current_conn = ap_run_create_connection(ptrans, ap_server_conf, csd, my_child_num, sbh, bucket_alloc);
if (current_conn) {
ap_process_connection(current_conn, csd);
ap_lingering_close(current_conn);
}
客户端连接一旦接受成功,此时就在客户端和服务器之间存在一条TCP连接。ap_process_connection会处理这个连接上的所有的请求,然后退出。完成这些工作的第一步就是建立连接结构,在该结构中会存储客户端的套接字,以及所有的连接相关信息,比如,服务器的IP地址和唯一标示符ID。一旦连接确定好,它就可以通过ap_process_connection接受服务了。ap_process_connection的内部隐藏了相当多的细节,这些细节,我们在后面的部分会详细介绍。
在每个请求的最后阶段,我们都要调用ap_lingering_close。这个函数据说是Apache中最糟糕的函数之一,这不仅是因为它很难理解,而且也涉及到了许多的OS的问题。必须要解决的问题是,在客户承认已经接受到所有的响应数据之前,都不可以关闭连接的服务器端。如果这样做了,那么客户就会丢失你最后发送的数据包。为了防止这种情况的发生,就必须要保持连接处于打开状态,直到出现超时或者客户端关闭连接。大多数OS都可以设置套接字选项来实现延迟关闭。遗憾的是,对于Web服务器而言,套接字选先不会总是进行了设置,与此相反,服务器需要实现延迟关闭,并且确保在每个连接结束时被调用。为了完成这项工作,核心服务器需要为请求的终止注册清除程序,以确保可以调用lingering_close。然而,类似Window的一些OS可以让你在某些条件下重用套接字。如果你打算重用套接字,那么就不需要将其关闭,因而也就不需要调用lingering_close函数。如果你正在编写用鱼支持重用套接字的MPM,ap_process_connection之后的程序就可以删除。
if (ap_mpm_pod_check(pod) == APR_SUCCESS) { /* selected as idle? */
die_now = 1;
}
else if (ap_my_generation !=
ap_scoreboard_image->global->running_generation) { /* restart? */
die_now = 1;
}
}
clean_child_exit(0);
每次连接处理完毕,子进程都必须检查终止管道。如果父进程通知它退出,那么此时die_now=1,下次子进程直接跳出循环执行clean_child_exit退出。
另一个可能导致子进程立即退出的原因就是该子进程属于上一家族的残留子进程。这通常是因为非强制启动引起的。因此每个子进程在退出之前要将自己的家族号ap_my_generation与父进程的家族号即记分板中的running_generation进行对比。如果不符合,表示子进程不属于本家族,将立即退出。除此两种情况之外,子进程将继续循环。