Apache源代码全景分析(第1卷):体系结构与核心模块
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.4 Apache运行流程

在本节中我们将概要描述Apache的整个运行流程。Apache的运行流程可以细分为三个大的阶段。

■ Apache启动过程,如图2-7中的A部分所示。

图2-7 Apache执行流程

■ 接受客户端连接,并处理该连接,如图2-7中的B部分所示。

■ 从连接中读取请求数据,处理客户端的一次请求,如图2-7中的C部分所示。

整个运行流程可以使用图2-7进行描述。

2.4.1 Apache启动过程

Apache 的启动包括两个阶段:高权限启动阶段和低权限运行阶段,通常称之为两阶段启动方式。Apache 总是以 root 用户身份启动运行,因此启动 Apache 时首先必须获得 root权限。在启动的过程中,初始化的工作包括以下几个方面。

1. 初始化系统所需要的资源

由于Apache中绝大多数的内存都是基于内存池分配的,因此,在Apache启动的时候必须首先初始化内存池资源。另外,初始化的一个主要任务就是对配置文件进行读取和解析。在启动的过程中,配置文件会被读取两次。第一次读取称之为预读取,主要是从配置文件中获得第二次配置文件中需要的一些配置信息;基于第一次读取的结果,第二次才是真正的配置文件读取。

根据配置文件的解析结构,启动过程还会对虚拟主机进行相关的初始化准备工作。如果要连接数据库,我们还需要初始化数据库连接。

在启动的最后阶段,Apache将通过调用ap_mpm_run函数将控制权交给MPM模块。只有MPM模块执行失败或执行完毕的时候,执行权才会从MPM返回到主程序中。MPM是Apache服务器和操作系统之间的接口,它的目的只有一个,就是充分利用操作系统的特性,对服务器的并发效率进行最大的优化。

在早期的Apache 1.3版本中,仅有prefork模型被支持,这种模型以进程作为基本的执行单位。显然,在其余的操作系统中,比如 Windows 中,进程是个重量级的家伙,如果以进程为执行单元,那么服务器的负载会变得极其有限。事实上,由于各种操作系统本身特性的限制,不存在一种能“走遍天下”的适用于所有操作系统的并发模型。Apache 将这些并发模型作为Apache的模块,这样,在特定的操作系统上,为了达到更高的并发效率,只要使用与之对应的MPM模块即可。

一旦权限从Apache主程序交给MPM,MPM将生成一定数目的进程或线程,侦听指定的端口,并等待接受客户端的连接。一旦接收到客户端请求,Apache 将进入连接和请求处理阶段。Apache 在接受和处理来自客户端的连接时,会以普通用户的权限去处理,而不会采用系统级别的权限。这种基本的安全策略可以防止 Apache(或模块或脚本程序)遭到损害和变得脆弱。一旦黑客攻克了Apache服务器,那么此时它能够获得的权限也仅仅是Apache正在运行的普通用户权限,而不是超级用户权限。这样可以确保服务器的安全。

2.4.2 HTTP连接处理

在接收到客户端的HTTP连接之后,客户端的IP地址已经可以获取。通过该IP地址,便可以确定该 IP 地址所对应的虚拟主机族。因此,连接处理的第一件事情就是更新虚拟主机的相关信息。

对于连接处理,最主要的任务就是调用预先定义好的连接处理挂钩process_connection。任何模块如果要处理连接,都可以实现该挂钩。一旦某个模块实现了该挂钩,那么在连接处理的过程中它们将会被调用。

在连接处理的过程中,通过调用 ap_read_request 函数进入请求读取过程,然后调用ap_process_request对该请求进行处理。

2.4.3 请求报文读取

一旦接收到客户端的连接,请求数据将被读取出来,然后HTTP_PROTOCOL模块将开始对该报文进行解析,请求的解析包括三部分:

■ HTTP请求头,比如“GET index.html HTTP/1.1”。

■ HTTP请求域,比如“Accept:*/*”等。

■ HTTP请求体。对于一些特殊的请求(比如Post方法),它一般会在报文体中保存数据。

请求报文读取以后,所有的请求相关信息都保存到请求数据结构request_rec中。该结构是一个复杂的数据结构,其中包含了HTTP请求的各个方面。该结构在整个请求处理过程中一直存在,直到请求处理结束。

对于读取的报文还有一个重要的任务就是对读取的报文进行输入过滤器处理。对于过滤器来说,读取的报文就类似于自来水管中的水,每流过一个过滤器都会受到一些影响,比如下面的情况就可能发生。

■ 对一个搜索引擎而言,它需要过滤器查询关键字,比如一些敏感词汇,此时对于在表单中输入的字符串,最终将保存到请求体中,关键字过滤器就会对其进行检查,发现含有敏感词汇就会进行特殊的处理。

■ Window 和 Unix 中对于文件的分隔符处理是不一致的,Window 是“/”,而 Unix则是“\”,因此,如果是在Window中提交URL给Apache,那么Apache需要将“/”转换为“\”。

所有的输入处理过滤器组成输入过滤器链表,在请求报文从网络中读取之后,它就直接进入了过滤器链表中,然后每个过滤器对其进行处理,并将其传递给下一个过滤器,直到最后一个过滤器,如图2-8所示。

图2-8 Apache输入过滤器演示

在所有的过滤器处理完毕后,我们得到的就是一个最终的处理后的请求报文。此时请求处理模块HTTP_REQUEST将对该请求作更进一步的处理。

2.4.4 请求处理

对于HTTP报文,Apache调用ap_process_request函数对请求进行实质的处理。Apache中的请求处理包括三个大的阶段:请求解析阶段、安全处理阶段、请求准备阶段。每一个大的阶段又可以细分为多个子阶段,如下所示。

请求解析阶段

1. URL字符转义

通常情况下,浏览器会自动转换请求地址栏中的一些特殊字符,如空格转换为%和十六进制的组合就是“%20”(比如空格是“%20”),因此对于服务器而言,它就要将“%xx”格式的字符串重新还原为原来的字符串。整个URL的转义由函数ap_unescape_url ()完成。

该阶段并不是一个必需的阶段,如果请求是一个代理请求或请求结构的 parsed_uri.path没有赋值,那么该阶段将不会被处理。

2. 从URL中剔除/../和/./字符

URL中所有的/../和/./字符串都在这一阶段调用ap_getparents()函数并被剔除,该阶段能够确保将URL提交给后续阶段处理的时候是一个绝对路径的URL。这个阶段是不能省略的,必须被执行。

3. 首次读取URL相关配置信息

一旦第二步处理完毕后,只包含绝对路径的 URL 就生成完毕,此时 Apache 将调用ap_location_walk从配置系统中查找与该URL关联的配置信息。在请求处理的后续阶段中,比如用户授权验证,权限控制等都需要依赖于读取的配置信息。如果当前请求是一个内部重定向请求或子请求,那么该请求的配置信息可能有很大一部分或全部继承自父请求。

URL关联的配置信息需要读取两次,此处是第一次读取。在对URL进行转换映射后,它可能被转换为一个完全不同的 URL,此时还需要再次读取一次配置信息。完整的配置信息是两次读取信息的最终叠加。

4. URL名称转换(translate_name)

该阶段主要用于对URL进行转换。比如Alias指令用于将某个URL映射到另外一个特定的URL中;而mod_writer模块则用于对URL的完全重写。另外,比如代理请求可能需要在请求的URL之前添加上proxy:://。如果某个模块需要对URL进行修改,那么都可以在这一阶段实现。

5. map_to_storage

如果在translate_name阶段,请求的URL最终转换本次的磁盘路径,map_to_storage将用来确定特定的资源是否在磁盘上存在。使用这个挂钩的原因在于如果所提供的数据不是动态生成的,而是位于磁盘上的文件,那么服务器可能要执行更加严格的安全检查。

6. 二次URL相关配置文件读取

在进行了名称转换和映射后,原先的 http://www.xxx.com 可能被映射为新的http://www.yyy.com,此时我们还需要针对新的www.yyy.com再次读取与它新的URL关联的配置信息。

7. header_parser

该阶段用来检查HTTP请求头,目前该阶段已经很少使用,Apache 2.x中只有标准模块mod_setenvif使用header_parser根据请求信息头来设置内部的环境变量。对于其余的模块,如果要使用类似的功能,再使用post_read_request进行替换。

安全处理阶段

1. access_checker

该阶段主要是对客户的访问做一些基础性的限制工作,比如 mod_access 模块在该阶段就可以根据IP限制用户的访问,如果用户的IP在黑名单中,那么该用户将无法进行访问。如果你愿意,还可以通过访问时间等进行限制。

2. check_user_id

该阶段主要是检查用户的身份权限,具体就是检查用户是否存在,或者检查用户的密码是否正确。大多数情况下,如果模块使用了check_user_id,就会弹出一个对话框允许用户输入用户名和密码。

3. auth_checker

尽管用户可能通过了身份认证,但是,如果要访问一些极其私密的文档,那么也应该对其进行限制。该阶段主要检查用户是否具有访问该资源的权限。只有用户具有给定的权限,才能访问该资源。

请求准备阶段

1. type_checker

在权限检查完毕之后,说明可以响应该客户的请求了,这时,Apache 开始处理客户请求的资源,并返回给客户。客户端所请求的信息可能多种多样,这就要求Apache对各种信息都能处理,比如对文本文件、html文件和gif图片等,而且每种处理都不一样,所以Apache会在这一步中确认客户请求的资源类型。通常这一步是由mod_mime模块来完成的。

2. fixups

这是请求处理的最后一个阶段。该阶段之后就是生成返回给客户端的响应内容。如果你希望在生成内容之前再做点什么事情,放到该阶段处理就行了。该阶段为fixups,顾名思义,即补丁修复。它与 access_checker、check_user_id 等阶段明显不一样,它没有特别明显的存放要求。只要你认为适合,都可以放到该阶段。

2.4.5 内容生成

请求处理最重要的内容就是响应内容生成。对静态的 HTML 文件而言,就是简单地读取HTML文件的内容,然后直接将其返回给客户端即可。但是对于一些脚本文件,比如CGI或PHP,则须要调用专门的程序进行处理。甚至一些所请求的内容在磁盘中根本不存在。不同类型的内容对应的不同处理程序称之为处理器,或者是内容生成器。通常必须预先在配置文件中设定好。

当所有的请求在请求处理阶段处理完毕之后,它就调用适当的处理器生成响应内容。如果没有找到对应的处理器,那么默认处理器将被调用。

与请求数据一旦读取就被压入输入过滤器链表一样,生成的这些内容一旦生成,就将被压入到输出过滤器链表进行输出处理。输出过滤器链表中的每一个输出过滤器都会对生成的数据进行处理,直到最后一个输出过滤器——网络输出过滤器,它将数据直接写入网络,然后返回给客户端。

2.4.6 关闭与重启

当你希望Apache停止运行或重新启动的时候,你必须向httpd进程发送关闭或重启信号。有两种发送信号的方法。第一种方法是直接使用UNIX的kill命令向运行中的进程发送信号。你也许会注意到系统里运行着很多httpd进程。但你不应该直接对它们中的任何一个发送信号,而只要对已经在PidFile中记载下了自身PID的父进程发送信号。也就是说,你不必对父进程以外的任何进程发送信号。你可以向父进程发送三种信号:TERM、HUP、USR1,比如:

            kill -TERM `cat /usr/local/apache2/logs/httpd.pid`

第二种方法是apachectl控制脚本的 -k 命令行选项:stop、restart、graceful、graceful-stop,用来执行关闭、启动和重启。比如:

■ 立即关闭,发送信号TERM。

            apachectl -k stop

发送TERM或stop信号到父进程可以使它立刻杀死所有子进程。这将花费一些时间来杀死所有子进程。然后父进程自己也退出。所有进行中的请求将被强行中止,而且不再接受其他请求。

■ 立即重启,发送信号HUP。

            apachectl -k restart

向父进程发送HUP或restart信号会使它像收到TERM信号一样杀掉所有的子进程,不同之处在于父进程本身并不退出。它重新读入配置文件、重新打开日志文件。然后产生一系列新的子进程来继续服务。

■ 优雅重启,发送信号USR1。

            apachectl -k graceful

USR1或graceful信号使得父进程建议子进程在完成它们现在的请求后退出(如果它们没有进行服务,将会立刻退出)。父进程重新读入配置文件并重新打开日志文件。当一个子进程死掉时,父进程立刻用新的配置文件产生一个新的子进程并立刻开始伺服新的请求。

■ 优雅停止,发送信号WINCH。

            apachectl -k graceful-stop

WINCH或graceful-stop信号使得父进程建议子进程在完成它们现在的请求后退出(如果它们没有进行服务,将会立刻退出)。然后父进程删除 PidFile 并停止在所有端口上的监听。父进程仍然继续运行并监视正在处理请求的子进程,一旦所有子进程完成任务并退出或超过由 GracefulShutdownTimeout 指令规定的时间,父进程将会退出。在超时的情况下,所有子进程都将接收到TERM信号并被强制退出。

在“优雅”状态下,TERM信号将会立即中止父进程和所有子进程。由于PidFile已经被删除,你将无法使用apachectl或httpd发送该信号。

当你向httpd发送信号时,你可以这样来读取它的进行过程:

            tail -f /usr/local/apache2/logs/error_log

2.5 主程序main

2.5.1 主程序概要

在本章最后我们将引导大家分析Apache程序的入口函数main函数。从main函数中我们可以看到Apache大致的启动过程。对于main中被调用的函数,如果将它们详细展开,可能就是一章的内容。

Apache的主函数main()位于${Apache}/server目录下,它的主要功能则着重于预处理,主要包括以下几个方面。

1. 读取Apache的配置文件

用过Apache的人都应该知道Apache的配置文件,说它是控制整个Apache性能的核心一点也不为过。通过配置文件,几乎可以对Apache服务器各方面的性能进行调整,而无须修改源代码及重新编译。

由于Apache很多方面的性能都受到配置文件的影响,因此,在正式启动之前必须先读取并处理配置文件,然后才能根据配置文件中的具体配置设置Apache。

2. 检查启动Apache的指令行参数

通常情况下,Apache是通过类似“http –D -d…”格式的指令进行启动的。主程序的一项重要任务就是解析这些指令。

3. 虚拟主机的设置

main函数的内部函数调用可以用图2-9描述。

图2-9 main函数内部调用示意

2.5.2 主程序细节

Apache主程序的实现代码如下所示:

        int main(int argc, const char * const argv[])
        {
            char c;
            int configtestonly= 0;
            const char *confname= SERVER_CONFIG_FILE;
            const char *def_server_root= HTTPD_ROOT;
            const char *temp_error_log =NULL;
            const char *error;
            process_rec*process;
            server_rec *server_conf;
            apr_pool_t *pglobal;
            apr_pool_t *pconf;
            apr_pool_t *plog; /* Pool of log streams, reset _after_ each read of conf */
            apr_pool_t *ptemp; /* Pool for temporary config stuff, reset often */
            apr_pool_t *pcommands; /*Pool for -D, -C and -c switches */
            apr_getopt_t *opt;
            apr_status_t rv;
            module **mod;
            const char *optarg;
            APR_OPTIONAL_FN_TYPE(ap_signal_server) *signal_server;
            AP_MONCONTROL(0); /* turn off profiling of startup */

AP_MONCONTROL 主要用于打开和关闭代码剖析。代码剖析用于检查代码的运行情况,从而可以找到程序执行的瓶颈,通常用于调试阶段。

            apr_app_initialize(&argc, &argv, NULL);

我们会在第3卷的第1章中介绍,任何一个应用程序如果要使用APR库进行二次开发,那么它首先必须完成的任务是对APR库进行必要的初始化。Apache从某个角度而言,它仅仅也是APR库的普通的使用者,因此它也不能例外。apr_app_initialize负责对APR库进行必要的初始化工作,代码如下:

process = create_process(argc, argv);
pglobal =process->pool;
pconf = process->pconf;
ap_server_argv0 = process->short_name;

由于Apache通常是通过指令行进行启动的,因此指令行的相关信息(指令数目argc及指令行字符串argv)都非常重要。对Apache而言,指令信息不仅主程序要使用,而且在一些子进程程序中也要使用,因此这就存在指令行信息传递的问题。一种最简单的方法就是使用全局变量。这样,如果任何一个地方需要 argc 或 argv,那么它只须引用对应的全局变量即可。不过引用全局变量不是一个好的办法,如果用得少还可以忍受,如果使用的地方很多,则在每一个使用的文件头部都须使用“extern xxxx”。

Apache把所有的指令行相关的信息都包装在process_rec结构中,代码如下:

        struct process_rec
        {
            apr_pool_t *pool;
            apr_pool_t *pconf;
            int argc;
            const char * const *argv;
            const char *short_name;
        };

argc、argv含义不需多说,short_name则是应用程序的缩略名称。pool和pconf则分别是全局内存池和配置相关内存池。一旦定义了该结构,就可以不使用全局变量。任何函数或过程如果需要对应的指令行信息,只须对process_rec进行初始化,然后直接传递给函数即可。

创建一个process_rec结构可通过create_process函数完成。

          apr_pool_create(&pcommands, pglobal);
        apr_pool_tag(pcommands, "pcommands");
        ap_server_pre_read_config  =apr_array_make(pcommands,1,sizeof(char*));
        ap_server_post_read_config=apr_array_make(pcommands,1,sizeof(char*));
        ap_server_config_defines   =apr_array_make(pcommands,1,sizeof(char*));

Apache 中的指令分为两大类:指令行中的指令及配置文件中的指令。而对于指令行中的指令又包括两种:读取配置文件之前必须处理的指令和读取配置文件后必须处理的指令。前者是防止指令行中的指令被覆盖,而后者则相反。一旦命令行被解析完毕后,这两种特殊的指令将被保存起来,以便在合适的时候执行。如果不保存,这些指令将会丢失。保存可使用如下数组数据结构:

          error=ap_setup_prelinked_modules(process);
        if (error) {
          ap_log_error(APLOG_MARK, APLOG_STARTUP|APLOG_EMERG, 0,NULL,"%s:%s",
                      ap_server_argv0, error);
          destroy_and_exit_process(process, 1);
        }

ap_setup_prelinked_modules用于将所有预链接的模块加入到加载模块链表中。模块只有加入到加载模块链表中才能称之为活动模块,然后才能被Apache核心调用,否则该模块仍然处于非活动状态。关于该函数的更加详细的分析我们会在后面的章节中进行。

        ap_run_rewrite_args(process);

rewrite_args挂钩主要是为MPM模块设置的,它允许MPM对命令行中的传入参数进行重写。对于一些MPM(如Event MPM、worker MPM),它们须将命令行中传入的参数转换为 MPM 内部的参数。通过调用 ap_run_rewrite_args,使得每一个模块都有这种机会进行命令行重写。

当httpd启动的时候,它可能会带有一系列的命令行参数。完整的httpd命令如下:

          httpd[-d serverroot][-f config][-C directive][-c directive][-D parameter]
        [ -e level ] [ -E file ] [ -k start|restart|graceful|stop|graceful-stop ] [ -R
        directory ] [ -h ] [ -l ] [ -L ] [ -S ] [ -t ] [ -v ] [ -V ] [ -X ] [ -M ]

在Windows系统中,还可以使用下列参数:

          httpd [ -k install|config|uninstall ] [ -n name ] [ -w ]

各个参数的处理如下:

          apr_getopt_init(&opt, pcommands,process->argc,process->argv);
        while ((rv=apr_getopt(opt,AP_SERVER_BASEARGS, &c, &optarg))
              == APR_SUCCESS) {
          char **new;
          switch (c) {
          case 'c':
              new = (char **)apr_array_push(ap_server_post_read_config);
              *new = apr_pstrdup(pcommands, optarg);
              break;
          case 'C':
              new = (char **)apr_array_push(ap_server_pre_read_config);
              *new = apr_pstrdup(pcommands, optarg);
              break;

-C选项指定了在读取配置文件之前必须先处理directive的配置指令。而-c 指令则指定了在读取配置文件之后,才能再处理 directive 的配置指令。-c 中的指令保存到 ap_server_post_read_config数组中,而-C中的指令则保存到ap_server_pre_read_config中。

          case 'd':
              def_server_root =optarg;
              break;

-d serverroot 选项将 ServerRoot 指令设置初始值为 serverroot。它可以被配置文件中的ServerRoot指令所覆盖。其默认值为/usr/local/apache2。

          case 'D':
              new = (char **)apr_array_push(ap_server_config_defines);
              *new = apr_pstrdup(pcommands, optarg);
              if (strcmp(optarg, "DUMP_VHOSTS")== 0)
                  configtestonly = 1;
              if (strcmp(optarg, "DUMP_MODULES")== 0)
                  configtestonly = 1;
              break;

-D parameter选项用于设置参数parameter,它配合配置文件中的<IfDefine>段,用于在服务器启动和重新启动时,有条件地跳过或处理某些命令。

          case 'e':
              if (strcasecmp(optarg, "emerg") == 0) {
                  ap_default_loglevel = APLOG_EMERG;
              }
              else if(strcasecmp(optarg, "alert")==0){
                  ap_default_loglevel = APLOG_ALERT;
              }
              else if (strcasecmp(optarg, "crit")== 0) {
                  ap_default_loglevel = APLOG_CRIT;
              }
              else if(strncasecmp(optarg,"err", 3)== 0){
                  ap_default_loglevel = APLOG_ERR;
              }
              else if (strncasecmp(optarg, "warn", 4)== 0) {
                  ap_default_loglevel = APLOG_WARNING;
              }
              else if(strcasecmp(optarg, "notice")== 0){
                  ap_default_loglevel = APLOG_NOTICE;
              }
              else if (strcasecmp(optarg, "info")==0) {
                  ap_default_loglevel = APLOG_INFO;
              }
              else if(strcasecmp(optarg, "debug")== 0){
                  ap_default_loglevel = APLOG_DEBUG;
              }
              else{
                  usage(process);
              }
              break;

-e level 选项在服务器启动时,设置日志的LogLevel为level。它用于在启动时,临时提高出错信息的详细程度,以帮助排错。

                case 'E':
                    temp_error_log= apr_pstrdup(process->pool, optarg);
                    break;

-E file选项用于将服务器启动过程中的出错信息发送到文件file。

                case 'X':
                    new = (char **)apr_array_push(ap_server_config_defines);
                    *new = "DEBUG";
                    break;

-X选项指定当前Apache以调试模式运行,在此模式下,Apache仅启动一个工作进程,并且服务器不与控制台脱离。

          case 'f':
              confname =optarg;
              break;

-f config选项在启动中使用config作为配置文件。如果config不以“/”开头,则它是相对于ServerRoot的路径。其默认值为conf/httpd.conf。

          case 'v':
              printf("Server version:%s\n", ap_get_server_version());
              printf("Server built:   %s\n", ap_get_server_built());
              destroy_and_exit_process(process, 0);

-v选项只是简单地显示httpd的版本,然后退出。

          case 'V':
              show_compile_settings();
              destroy_and_exit_process(process, 0);

-V选项用于显示httpd和APR/APR-Util的版本和编译参数,然后退出。

          case 'l':
              ap_show_modules();
              destroy_and_exit_process(process, 0);

-l选项用于输出一个静态编译在服务器中的模块列表。它不会列出使用LoadModule指令动态加载的模块。

          case 'L':
              ap_show_directives();
              destroy_and_exit_process(process, 0);

-L选项输出一个指令的列表,并包含了各指令的有效参数和使用区域。

          case 't':
              configtestonly= 1;
              break;

-t选项意味着仅对配置文件执行语法检查。程序在语法解析检查结束后立即退出,或者返回“0”(OK),或者返回非0的值(Error)。如果还指定了“-D DUMP_VHOSTS”,则会显示虚拟主机配置的详细信息。

          case 'S':
              configtestonly= 1;
              new = (char **)apr_array_push(ap_server_config_defines);
              *new = "DUMP_VHOSTS";
              break;

-S显示从配置文件中读取并解析的设置结果(目前仅显示虚拟主机的设置)。

          case 'M':
              configtestonly= 1;
              new = (char **)apr_array_push(ap_server_config_defines);
              *new = "DUMP_MODULES";
              break;

-M输出一个已经启用的模块列表,包括静态编译在服务器中的模块和作为DSO动态加载的模块。

          case 'h':
          case '?':
              usage(process);

-h和-?输出一个可用的命令行选项的简要说明,以便提示对命令行不熟悉的用户启动Apache。

          }
      }

主程序中对于Window所需要的-K、-n及-w选项并没有处理,这些选项由于只有MPM才会使用到,因此它们在MPM中被处理,处理由rewrite_args挂钩触发。各个MPM如何处理Window下的这三个选项,在具体的MPM中再详细描述。

      /* bad cmdline option?  then we die */
      if (rv != APR_EOF || opt->ind< opt->argc) {
          usage(process);
      }
      apr_pool_create(&plog, pglobal);
      apr_pool_tag(plog,"plog");
      apr_pool_create(&ptemp, pconf);
      apr_pool_tag(ptemp,"ptemp");

Apache 中使用的所有内存资源都是基于内存池的概念而分配的,所有的内存池之间形成内存池树的概念。层次越深的内存池它的生存周期就越短,反之,距离根节点越近,它的生存周期就越长。所有结点的根节点是全局内存池 pglobal,在启动的时候被分配。除此之外,在启动的时候还需要一个临时内存池—— ptemp。

      ap_server_root = def_server_root;
      if (temp_error_log) {
          ap_replace_stderr_log(process->pool, temp_error_log);
      }

一般情况下,如果没有指定日志输出文件,就使用标准的输出设备 stderr。如果在启动Apache的时候通过-E选项指定了日志文件,那么,此时必须使用ap_replace_stderr_log进行输出日志文件替换。

      if (ap_run_pre_config(pconf, plog, ptemp) !=OK) {
          destroy_and_exit_process(process, 1);
      }
      rv = ap_process_config_tree(server_conf, ap_conftree,
                                process->pconf, ptemp);
      if (rv == OK){
          ap_fixup_virtual_hosts(pconf, server_conf);
          ap_fini_vhost_config(pconf, server_conf);
          apr_hook_sort_all();
          if (configtestonly) {
              ap_run_test_config(pconf, server_conf);
              ap_log_error(APLOG_MARK, APLOG_STARTUP, 0, NULL, "Syntax OK");
              destroy_and_exit_process(process, 0);
          }
      }

在main.c中,配置文件在Apache启动或重启的时候总是会被读取两次,一次是在主循环执行之前被读取,正如上面代码中所读取的一样,另外一次是在主循环中被读取。之所以要读取两次,主要基于以下两个目的:

■ 预检查配置文件中可能出现的语法问题,确保在真正处理的时候配置文件是完整无误的。

■ 第一次读取文件会生成在第二次读取文件时所需要的字段。比如,当读取配置文件的时候会需要错误日志指令,但是它在存储段配置文件中,所以须要读取一次这个文件,发现错误日志的正确配置,然后就可以在第二次读取配置文件的时候启动服务器,代码如下:

      signal_server= APR_RETRIEVE_OPTIONAL_FN(ap_signal_server);
      if (signal_server) {
          int exit_status;
          if (signal_server(&exit_status, pconf) != 0) {
              destroy_and_exit_process(process, exit_status);
          }
      }
      /* If our config failed, deal with that here. */
      if (rv != OK){
          destroy_and_exit_process(process, 1);
      }
      apr_pool_clear(plog);
      if ( ap_run_open_logs(pconf, plog,ptemp, server_conf) != OK){
          destroy_and_exit_process(process, 1);
      }
      if ( ap_run_post_config(pconf, plog,ptemp, server_conf) != OK){
          destroy_and_exit_process(process, 1);
      }
      apr_pool_destroy(ptemp);

整个配置文件的完整读取过程包括三个部分。

1. 读取配置文件前的准备工作

当整个配置文件被解析为配置树但是尚未真正被处理的时候,一些模块可能需要对该配置树进行修改。为此,Apache中必须能够提供这种机会。pre_config挂钩正是为此目的而诞生的。任何一个模块如果要修改生成的配置树,它只须实现pre_config挂钩。同样,在Apache启动后,它也须要通过ap_run_pre_config触发所有模块的pre_config挂钩,从而使得它们对配置树的修改生效。

2. 实际的配置文件读取

实际文件的读取通过ap_process_config_tree函数完成,详细具体的配置处理流程,我们会在后面的章节中讨论。

3. 读取配置文件后的处理

与pre_config挂钩对应的是post_config挂钩。该挂钩在每次读取配置文件之后被调用。挂钩是大多数模块执行服务器端初始化的地方。如果某个模块由于某种原因需要启动分离的进程,就应该在这个阶段来完成,代码如下:

      for (;;) {
          apr_hook_deregister_all();
          apr_pool_clear(pconf);
          for (mod= ap_prelinked_modules; *mod != NULL;mod++) {❶
              ap_register_hooks(*mod, pconf);
          }
          ap_conftree= NULL;
          apr_pool_create(&ptemp, pconf);
          apr_pool_tag(ptemp, "ptemp");
          ap_server_root = def_server_root;
          server_conf = ap_read_config(process, ptemp, confname,&ap_conftree);
          if (!server_conf) {
              destroy_and_exit_process(process, 1);
          }
          if (ap_run_pre_config(pconf, plog, ptemp)!= OK) {
              ap_log_error(APLOG_MARK, APLOG_STARTUP |APLOG_ERR,
                          0, NULL, "Pre-configuration failed");
              destroy_and_exit_process(process, 1);
          }
          if (ap_process_config_tree(server_conf, ap_conftree, process->pconf,
                                  ptemp) != OK){
              destroy_and_exit_process(process, 1);❷
          }
          ap_fixup_virtual_hosts(pconf, server_conf);
          ap_fini_vhost_config(pconf, server_conf);
          apr_hook_sort_all();
          apr_pool_clear(plog);
          if (ap_run_open_logs(pconf, plog, ptemp,server_conf) !=OK) {
              ap_log_error(APLOG_MARK, APLOG_STARTUP |APLOG_ERR,
                          0, NULL, "Unable to open logs");
              destroy_and_exit_process(process, 1);
          }
          if (ap_run_post_config(pconf, plog,ptemp, server_conf) !=OK) {
              ap_log_error(APLOG_MARK, APLOG_STARTUP |APLOG_ERR,
                          0, NULL, "Configuration Failed");
              destroy_and_exit_process(process, 1);
          }
          apr_pool_destroy(ptemp);
          apr_pool_lock(pconf, 1);
          ap_run_optional_fn_retrieve();
          if (ap_mpm_run(pconf, plog, server_conf))
              break;
          apr_pool_lock(pconf, 0);
}
apr_pool_lock(pconf, 0);
destroy_and_exit_process(process, 0);
return 0; /* Termination 'ok' */
}

准备就绪后,Apache就进入了主循环 for(;;)。循环中主进程所做的事情包括以下几点。

1. 挂钩注册

Apache 中的整个请求处理会被分割为多个挂钩阶段,每一阶段实现一个特定的任务。如果某个模块希望在请求的某个处理阶段做点什么事情,它只需要实现该阶段的挂钩即可。Apache 核心会依次执行每一个挂钩中所注册的所有挂钩函数。不过,核心必须了解哪些模块注册了哪些函数,为此,主进程通过ap_register_hooks函数让各个模块将自己的挂钩函数注册到核心内部。在注册之前,必须将以前注册的挂钩使用 apr_hook_deregister_all 进行注销。代码如❶所示。

2. 二次配置文件读取

主循环中会基于第一次读取配置文件的结构进行第二次配置文件读取。第二次读取的流程和第一次读取的流程完全相同。在配置文件处理之后,主程序就立即根据配置信息处理虚拟主机相关的内容。当处理配置文件的时候,每遇到<VirtualHost>标签就会生成一个虚拟主机结构,在配置文件处理结束后,ap_fixup_virtual_hosts 将根据配置信息完成对各个虚拟主机结构的初始化。ap_fini_vhost_config则用来构建虚拟主机运行时需要的哈希表。关于这两个函数的细节将在第2卷中介绍。

3. 导出注册所有的可选函数

可选函数是Apache 2.0系列中一个新引入的概念。对于普通的函数,如果它在文件a.c中被定义,那么,如果你想使用该函数,就必须连接a.c文件,否则在编译的时候就会报错。在Apache中须使用某个函数,但是该函数可能被定义在某个模块中的情况很常见。如果模块没有加载,使用函数在编译的时候就会报错。为了解决这个问题,Apache 中引入了可选函数的概念,可选函数它不依赖于任何一个模块,它本质上是一个函数指针。如果某个模块须要实现该函数,只要实现该指针就可以了。所有模块对可选函数的所有实现都保存在一个链表之中。该链表可能为空,即没有任何实现,此时将不做任何事情。

Apache中提供了一个optional_fn_retrieve挂钩,通过该挂钩,每个模块都可以导出声明自己内部定义的可选函数。ap_run_optional_fn_retrieve则会触发该挂钩,从而可以导出所有模块中声明的所有可选函数。

关于可选函数的细节将在第4章详细讨论。

4. ap_mpm_run

主进程最重要的任务就是对ap_mpm_run的调用。一旦调用ap_mpm_run,主进程将进入多进程并发处理模块。在该模块中将会启动多个进程或多个线程,然后侦听客户端的连接请求,一旦接收到请求,就继而进入请求处理模块。

如果ap_mpm_run在执行中发生错误,则返回1,否则返回0。

当ap_mpm_run退出的时候,整个主进程也就相应地执行结束了。