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


在预创建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");
    }
如果服务器被要求强制重启,那么所有的子进程包括那些仍然在处理请求的都将被统统终止 本文作者:
« 
» 
快速导航

Copyright © 2016 phpStudy | 豫ICP备2021030365号-3