Apache概述
Apache是目前世界上使用最为广泛的一种Web Server,它以跨平台、高效和稳定而闻名。按照去年官方统计的数据,Apache服务器的装机量占该市场60%以上的份额。尤其是在X(Unix/Linux)平台上,Apache是最常见的选择。其它的Web Server产品,比如IIS,只能运行在Windows平台上,是基于微软.Net架构技术的不二选择。
Apache支持许多特性,大部分通过模块扩展实现。常见的模块包括mod_auth(权限验证)、mod_ssl(SSL和TLS支持) mod_rewrite(URL重写)等。一些通用的语言也支持以Apache模块的方式与Apache集成。 如Perl,Python,Tcl,和PHP等。
Apache并不是没有缺点,它最为诟病的一点就是变得越来越重,被普遍认为是重量级的WebServer。所以,近年来又涌现出了很多轻量级的替代产品,比如lighttpd,nginx等等,这些WebServer的优点是运行效率很高,但缺点也很明显,成熟度往往要低于Apache,通常只能用于某些特定场合。
Apache组件逻辑图
Apache是基于模块化设计的,总体上看起来代码的可读性高于php的代码,它的核心代码并不多,大多数的功能都被分散到各个模块中,各个模块在系统启动的时候按需载入。你如果想要阅读Apache的源代码,建议你直接从main.c文件读起,系统最主要的处理逻辑都包含在里面。
MPM(Multi -Processing Modules,多重处理模块)是Apache的核心组件之一,Apache通过MPM来使用操作系统的资源,对进程和线程池进行管理。Apache为了能够获得最好的运行性能,针对不同的平台(Unix/Linux、Window)做了优化,为不同的平台提供了不同的MPM,用户可以根据实际情况进行选择,其中最常使用的MPM有prefork和worker两种。至于您的服务器正以哪种方式运行,取决于安装Apache过程中指定的MPM编译参数,在X系统上默认的编译参数为prefork。由于大多数的Unix都不支持真正的线程,所以采用了预派生子进程(prefork)方式,象Windows或者Solaris这些支持线程的平台,基于多进程多线程混合的worker模式是一种不错的选择。对此感兴趣的同学可以阅读有关资料,此处不再多讲。Apache中还有一个重要的组件就是APR(Apache portable Runtime Library),即Apache可移植运行库,它是一个对操作系统调用的抽象库,用来实现Apache内部组件对操作系统的使用,提高系统的可移植性。Apache对于php的解析,就是通过众多Module中的php Module来完成的。
PHP与Apache
当PHP需要在Apache服务器下运行时,一般来说,它可以mod_php5模块的形式集成, 此时mod_php5模块的作用是接收Apache传递过来的PHP文件请求,并处理这些请求, 然后将处理后的结果返回给Apache。如果我们在Apache启动前在其配置文件中配置好了PHP模块(mod_php5), PHP模块通过注册apache2的ap_hook_post_config挂钩,在Apache启动的时候启动此模块以接受PHP文件的请求。
除了这种启动时的加载方式,Apache的模块可以在运行的时候动态装载, 这意味着对服务器可以进行功能扩展而不需要重新对源代码进行编译,甚至根本不需要停止服务器。 我们所需要做的仅仅是给服务器发送信号HUP或者AP_SIG_GRACEFUL通知服务器重新载入模块。 但是在动态加载之前,我们需要将模块编译成为动态链接库。此时的动态加载就是加载动态链接库。 Apache中对动态链接库的处理是通过模块mod_so来完成的,因此mod_so模块不能被动态加载, 它只能被静态编译进Apache的核心。这意味着它是随着Apache一起启动的。
Apache是如何加载模块的呢?我们以前面提到的mod_php5模块为例。 首先我们需要在Apache的配置文件httpd.conf中添加一行:
1 |
LoadModule php5_module modules/mod_php5.so |
这里我们使用了LoadModule命令,该命令的第一个参数是模块的名称,名称可以在模块实现的源码中找到。 第二个选项是该模块所处的路径。如果需要在服务器运行时加载模块, 可以通过发送信号HUP或者AP_SIG_GRACEFUL给服务器,一旦接受到该信号,Apache将重新装载模块, 而不需要重新启动服务器。
在配置文件中添加了所上所示的指令后,Apache在加载模块时会根据模块名查找模块并加载, 对于每一个模块,Apache必须保证其文件名是以“mod_”开始的,如PHP的mod_php5.c。 如果命名格式不对,Apache将认为此模块不合法。Apache的每一个模块都是以module结构体的形式存在, module结构的name属性在最后是通过宏STANDARD20_MODULE_STUFF以__FILE__体现。 关于这点可以在后面介绍mod_php5模块时有看到。这也就决定了我们的文件名和模块名是相同的。 通过之前指令中指定的路径找到相关的动态链接库文件后,Apache通过内部的函数获取动态链接库中的内容, 并将模块的内容加载到内存中的指定变量中。
在真正激活模块之前,Apache会检查所加载的模块是否为真正的Apache模块, 这个检测是通过检查module结构体中的magic字段实现的。 而magic字段是通过宏STANDARD20_MODULE_STUFF体现,在这个宏中magic的值为MODULE_MAGIC_COOKIE, MODULE_MAGIC_COOKIE定义如下:
1 |
#define MODULE_MAGIC_COOKIE 0x41503232UL /* "AP22" */ |
最后Apache会调用相关函数(ap_add_loaded_module)将模块激活, 此处的激活就是将模块放入相应的链表中(ap_top_modules链表: ap_top_modules链表用来保存Apache中所有的被激活的模块,包括默认的激活模块和激活的第三方模块。)
Apache对PHP的支持是通过Apache的模块mod_php5来支持的。如果希望Apache支持PHP的话,在./configure步骤需要指定--with-apxs2=/usr/local/apache2/bin/apxs 表示告诉编译器通过Apache的mod_php5/apxs来提供对PHP5的解析。
在最后一步make install的时候我们会看到将动态链接库libphp5.so(Apache模块)拷贝到apache2的安装目录的modules目录下,并且还需要在httpd.conf配置文件中添加LoadModule语句来动态将libphp5.so 模块加载进来,从而实现Apache对php的支持。
由于该模式实在太经典了,因此这里关于安装部分不准备详述了,相对来说比较简单。我们知道nginx一般包括两个用途HTTP Server和Reverse Proxy Server(反向代理服务器)。在前端可以部署nginx作为reverse proxy server,后端布置多个Apache来实现机群系统server cluster架构的。
因此,实际生产中,我们仍旧能够保留Apache+mod_php5的经典App Server,而仅仅使用nginx来当做前端的reverse proxy server来实现代理和负载均衡。 因此,建议nginx(1个或者多个)+多个apache的架构继续使用下去。
Apache2的mod_php5模块包括sapi/apache2handler和sapi/apache2filter两个目录 在apache2_handle/mod_php5.c文件中,模块定义的相关代码如下:
01 |
AP_MODULE_DECLARE_DATA module php5_module = { |
02 |
STANDARD20_MODULE_STUFF, |
它所对应的是Apache的module结构,module的结构定义如下:
01 |
typedef struct module_struct module; |
02 |
struct module_struct { |
07 |
void *dynamic_load_handle; |
08 |
struct module_struct *next; |
10 |
void (*rewrite_args) (process_rec *process); |
11 |
void *(*create_dir_config) (apr_pool_t *p, char *dir); |
12 |
void *(*merge_dir_config) (apr_pool_t *p, void *base_conf, void *new_conf); |
13 |
void *(*create_server_config) (apr_pool_t *p, server_rec *s); |
14 |
void *(*merge_server_config) (apr_pool_t *p, void *base_conf, void *new_conf); |
15 |
const command_rec *cmds; |
16 |
void (*register_hooks) (apr_pool_t *p); |
上面的模块结构与我们在mod_php5.c中所看到的结构有一点不同,这是由于STANDARD20_MODULE_STUFF的原因, 这个宏它包含了前面8个字段的定义。STANDARD20_MODULE_STUFF宏的定义如下:
2 |
#define STANDARD20_MODULE_STUFF MODULE_MAGIC_NUMBER_MAJOR, \ |
3 |
MODULE_MAGIC_NUMBER_MINOR, \ |
在php5_module定义的结构中,php_dir_cmds是模块定义的所有的指令集合,其定义的内容如下:
01 |
const command_rec php_dir_cmds[] = |
03 |
AP_INIT_TAKE2( "php_value" , php_apache_value_handler, NULL, |
04 |
OR_OPTIONS, "PHP Value Modifier" ), |
05 |
AP_INIT_TAKE2( "php_flag" , php_apache_flag_handler, NULL, |
06 |
OR_OPTIONS, "PHP Flag Modifier" ), |
07 |
AP_INIT_TAKE2( "php_admin_value" , php_apache_admin_value_handler, |
08 |
NULL, ACCESS_CONF|RSRC_CONF, "PHP Value Modifier (Admin)" ), |
09 |
AP_INIT_TAKE2( "php_admin_flag" , php_apache_admin_flag_handler, |
10 |
NULL, ACCESS_CONF|RSRC_CONF, "PHP Flag Modifier (Admin)" ), |
11 |
AP_INIT_TAKE1( "PHPINIDir" , php_apache_phpini_set, NULL, |
12 |
RSRC_CONF, "Directory containing the php.ini file" ), |
这是mod_php5模块定义的指令表。它实际上是一个command_rec结构的数组。 当Apache遇到指令的时候将逐一遍历各个模块中的指令表,查找是否有哪个模块能够处理该指令, 如果找到,则调用相应的处理函数,如果所有指令表中的模块都不能处理该指令,那么将报错。 如上可见,mod_php5模块仅提供php_value等5个指令。
php_ap2_register_hook函数的定义如下:
1 |
void php_ap2_register_hook(apr_pool_t *p) |
3 |
ap_hook_pre_config(php_pre_config, NULL, NULL, APR_HOOK_MIDDLE); |
4 |
ap_hook_post_config(php_apache_server_startup, NULL, NULL, APR_HOOK_MIDDLE); |
5 |
ap_hook_handler(php_handler, NULL, NULL, APR_HOOK_MIDDLE); |
6 |
ap_hook_child_init(php_apache_child_init, NULL, NULL, APR_HOOK_MIDDLE); |
以上代码声明了pre_config,post_config,handler和child_init 4个挂钩以及对应的处理函数。 其中pre_config,post_config,child_init是启动挂钩,它们在服务器启动时调用。 handler挂钩是请求挂钩,它在服务器处理请求时调用。其中在post_config挂钩中启动php。 它通过php_apache_server_startup函数实现。php_apache_server_startup函数通过调用sapi_startup启动sapi, 并通过调用php_apache2_startup来注册sapi module struct(此结构在本节开头中有说明), 最后调用php_module_startup来初始化PHP, 其中又会初始化ZEND引擎,以及填充zend_module_struct中 的treat_data成员(通过php_startup_sapi_content_types)等。
到这里,我们知道了Apache加载mod_php5模块的整个过程,可是这个过程与我们的SAPI有什么关系呢? mod_php5也定义了属于Apache的sapi_module_struct结构:
01 |
static sapi_module_struct apache2_sapi_module = { |
06 |
php_module_shutdown_wrapper, |
11 |
php_apache_sapi_ub_write, |
12 |
php_apache_sapi_flush, |
13 |
php_apache_sapi_get_stat, |
14 |
php_apache_sapi_getenv, |
18 |
php_apache_sapi_header_handler, |
19 |
php_apache_sapi_send_headers, |
22 |
php_apache_sapi_read_post, |
23 |
php_apache_sapi_read_cookies, |
25 |
php_apache_sapi_register_variables, |
26 |
php_apache_sapi_log_message, |
27 |
php_apache_sapi_get_request_time, |
30 |
STANDARD_SAPI_MODULE_PROPERTIES |
这些方法都专属于Apache服务器。以读取cookie为例,当我们在Apache服务器环境下,在PHP中调用读取Cookie时, 最终获取的数据的位置是在激活SAPI时。它所调用的方法是read_cookies。
1 |
SG(request_info).cookie_data = sapi_module.read_cookies(TSRMLS_C); |
对于每一个服务器在加载时,我们都指定了sapi_module,而Apache的sapi_module是apache2_sapi_module。 其中对应read_cookies方法的是php_apache_sapi_read_cookies函数。 这也是定义SAPI结构的理由:统一接口,面向接口的编程,具有更好的扩展性和适应性。