Apache中的挂钩剖析

来源:互联网 发布:mysql 1103 编辑:程序博客网 时间:2024/04/28 03:25
5.5 挂钩(HOOK)
5.5.1 为什么引入挂钩
在Apache1.3版本中,对HTTP请求的处理包括若干个固定阶段,比如地址转换阶段、身份确认阶段、身份认证阶段、权限确认阶段、MIME类型识别阶段等等,这也意味着Apache1.3中的挂钩数目是有限的,固定的。这个反映在模块结构中就是针对每个HOOK都对应一个函数指针。比如如果需要检查用户的身份是否合法则只需要调用ap_check_user_i;如果需要核查用户的权限是否满足则只需要调用auth_checker;而如果需要记录日志,则只需要调用logger函数即可。这种对请求的处理策略非常清晰明了。不过这种方法也有明显的局限性,首先就是所有的模块都维护所有的挂钩,但对于某个模块而言只有很少的几个挂钩会被使用,比如ap_check_user_id挂钩可能只有安全模块才用到,模块中不使用的挂钩都被置为NULL;最重要的问题是很难增加新的挂钩。如果需要增加一个新的挂钩,就必须修改module结构,这涉及到所有的模块的修改,工作量很大,而且需要重新进行编译,因此这种策略使得模块的扩展性能极差。
为了使得Apache 2.0版本为了能够更具模块化,更具扩展性,apache采取了更加灵活的处理策略,即“模块自行管理”策略,即挂钩(Hooks)的概念,Hooks的使用使得函数从静态的变为动态的,模块编写者可以通过Hooks自行增加处理句柄,而不需要所有的模块都千篇一律。因此每次增加新函数,唯一必须修改的就是增加函数的模块而已。
为了对挂钩有大体的了解,我们首先来看一下Apache2.0的HTTP请求处理流程。
从大的方面来看,Apache对HTTP的请求可以分为连接、处理和断开连接三个阶段。从小的方面而言,每个阶段又可以分为更多的子阶段。比如对HTTP的请求,我们可以进一步划分为客户身份验证、客户权限认证、请求校验、URL重定向等阶段,每一个阶段调用相应的函数进行处理。在Apache中,这些子阶段可以用术语“挂钩(HOOK)”来描述。Apache中对请求的处理过程实质上就是依次调用一系列挂钩的过程,不过由于HTTP请求类型的不同,它们所对应的挂钩数目和类型也不尽相同。对于典型的HTTP请求,有一个默认的挂钩调用顺序,你可以按照这个默认的顺序进行调用,也可以不遵守这个顺序,你可以根据自己的情况调整调用顺序。整个HTTP的请求可以用下图来描述:

 
在上面的图示中,存在六种不同的挂钩,对于请求a,其指需要调用Hook2.、1;而对于请求b,则需要调用1、6、5;请求c则需要调用1、4、3、6四个挂钩。
在Apache中,挂钩总是和挂钩函数联系在一起的。挂钩是用来表示在处理HTTP请求中一组类似的操作,与之对应,挂钩函数就是操作函数。不过即使挂钩相同,对应的挂钩函数也未必相同。举个简单的例子,配置文件模块和虚拟主机模块都需要身份验证挂钩来确认访问者能否访问相应的资源,但两个模块的验证方法则差别很大。因此一个挂钩同时是和一组挂钩函数联系在一起的。因此请求处理过程中,调用挂钩的时候实际上就是调用挂钩函数组。调用过程可以用下图示意。Apache对指定挂钩的挂钩函数调用有两种,一种就是将挂钩函数组中的每个函数都调用一次,比如图中的挂钩4;另一种就是从前往后调用,一旦找到合适的就停止继续调用,图中1、2、3就是这种情况。
通过挂钩机制,你可以自行修改服务器的行为,比如修改挂钩函数或者增加挂钩函数,甚至增加挂钩。不过增加挂钩只是2.0才提供的功能。

5.5.2 声明挂钩
Apache中关于挂钩的实现大部分是通过宏来实现的,而且这些宏大多数非常复杂。挂钩的实现主要定义在文件apr_hook.h和apr_hook.c中,另外在config.c中也有部分定义。
Apache中对挂钩的使用总是从定义一个挂钩开始的,在Apache中声明一个挂钩,总是通过宏
#define AP_DECLARE_HOOK(ret,name,args) \
    APR_DECLARE_EXTERNAL_HOOK(ap,AP,ret,name,args)
来实现的。在该宏中,ret是定义的挂钩的返回类型;而name则是定义的挂钩的名称;args则是挂钩函数需要的额外参数,通常是以bracket形式出现的。例如下面的语句就声明了一个返回类型为整数,函数名称为do_something,函数参数为(request_rec *r,int n)的挂钩:
AP_DECLARE_HOOK(int , do_something , (request_rec *r , int n))
不过AP_DECLARE_HOOK内部则是调用的APR_DECLARE_EXTERNAL_HOOK宏。该宏定义如下:
#define APR_DECLARE_EXTERNAL_HOOK(ns,link,ret,name,args) \
typedef ret ns##_HOOK_##name##_t args; \
link##_DECLARE(void) ns##_hook_##name(ns##_HOOK_##name##_t *pf, \
                                      const char * const *aszPre, \
                                      const char * const *aszSucc, int nOrder); \
link##_DECLARE(ret) ns##_run_##name args; \
APR_IMPLEMENT_HOOK_GET_PROTO(ns,link,name); \
typedef struct ns##_LINK_##name##_t \
{ \
    ns##_HOOK_##name##_t *pFunc; \
    const char *szName; \
    const char * const *aszPredecessors; \
    const char * const *aszSuccessors; \
    int nOrder; \
} ns##_LINK_##name##_t;
从上面的定义可以看出,该宏定义冗长,而且事实上,Apache中的关于挂钩定义的每个宏都是很长的。分析颇为费力。在该宏中出现最多的符号当之不让的就是“##”。##宏主要用来实现连接前后的字符串。
APR_DECLARE_EXTERNAL_HOOK宏展来后实现了五个子功能的定义,这几个功能我们分别介绍,其所用的示例则是配置模块的post_config挂钩,在Http_config.中Apache定义了post_config挂钩如下:
AP_DECLARE_HOOK(int,post_config,(apr_pool_t *pconf,apr_pool_t *plog, apr_pool_t *ptemp,server_rec *s)),该宏进一步替换展开后为
APR_DECLARE_EXTERNAL_HOOK(ap,AP,int,post_config, (apr_pool_t *pconf,apr_pool_t *plog, apr_pool_t *ptemp,server_rec *s)),在该宏中:
(1)、typedef ret ns##_HOOK_##name##_t args;
     该宏主要用来定义对应的post_config的挂钩的执行函数原型,比如post_config挂钩执行函数原型定义则是:
     typedef int ap_HOOK_post_config_t (apr_pool_t *pconf,apr_pool_t *plog,apr_pool_t *ptemp,server_rec *s);
(2)、link##_DECLARE(void) ns##_hook_##name(ns##_HOOK_##name##_t *pf, \
                                      const char * const *aszPre, \
                                      const char * const *aszSucc, int nOrder); \
该宏则用来定义指定挂钩的函数原型,其中ns##_HOOK_##name##_t *pf则是前面定义了对应于该挂钩的执行函数指针,这个函数最终会加入到在请求处理期间的指定时刻进行调用的列表中,而aszPre和aszSucc在形式和功能上都很相近,都是以NULL为结束符的字符串数组,比如{“mod_mime.c”,NULL}。aszPre数组会为这个挂钩规定必须在这个函数之前调用的函数模块,而aszSucc则是规定必须在这个函数之后进行调用的函数模块。上面的post_config挂钩其定义展开后如下:
     AP_DECLARE(void) ap_hook_post_config(ap_HOOK_post_config* pf;
const char * const *aszPre,
const char * const *aszSucc,
int nOrder);
函数的第四个参数nOrder则是该挂钩的综合排序参数,整数类型。该整数代表了这个函数与所有的其余同一阶段的其他函数相比较时候的综合位置。如果这个数值越低,则这个挂钩函数将在列表中排列越靠前,因此也越早被调用。如果两个模块为它们的模块设定相同的nOrder,而且模块之间没有依赖关系,则它们的调用顺序则不确定。针对这个参数,Apache提供了5个通用的宏:APR_HOOK_REALLY_FIRST、APR_HOOK_FIRST、APR_HOOK_MIDDLE、APR_HOOK_LAST、APR_HOOK_REALLY_LAST,其定义如下:
#define APR_HOOK_REALLY_FIRST    (-10)
#define APR_HOOK_FIRST            0
#define APR_HOOK_MIDDLE           10
#define APR_HOOK_LAST             20
#define APR_HOOK_REALLY_LAST      30
从上面可以看出,nOrder的值得范围介于-10到30之间。如果函数是在列表中是必须第一个得到的,则必须将其设置为APR_HOOK_FIRST或者APR_HOOK_REALLY_FIRST;如果是必须最后一个调用的,则将其设置为APR_HOOK_LAST或者APR_HOOK_REALLY_LAST。如果其调用次序无关紧要,则可以设置为APR_HOOK_MIDDLE。
所有挂钩最终通过排序函数进行,这个我们在后面的部分介绍。
(3)、link##_DECLARE(ret) ns##_run_##name args; \
挂钩的定义最终是为了被调用,因此Apache中定义了对挂钩的调用函数。通常挂钩调用函数形式如下:ap_run_hookname();上面的宏真是用来定义挂钩调用函数。
比如对于post_config挂钩而言,其对外的调用函数则是ap_run_post_config()。
(4)、APR_IMPLEMENT_HOOK_GET_PROTO(ns,link,name); \
该宏展开后如下所示
#define APR_IMPLEMENT_HOOK_GET_PROTO(ns,link,name) \
link##_DECLARE(apr_array_header_t *) ns##_hook_get_##name(void)
该宏了用于返回挂钩访问函数原型,在模块外部可以调用改函数获得注册为该挂钩的所有函数。参数ns和link分别为挂钩函数名字空间前缀和联接申明前缀,一般情况为ap和AP,name为挂钩名。访问函数的原型一般为AP_DECLARE(apr_array_header_t *) ap_hook_get_name(void)。如果对于post_config挂钩而言,该宏对应的则是AP_DECLARE(apr_array_header_t *) ap_hook_get_post_config(void)。通过该函数,Apache可以获取挂钩定义的原型。
(5)、typedef struct ns##_LINK_##name##_t \
{ \
    ns##_HOOK_##name##_t *pFunc; \
    const char *szName; \
    const char * const *aszPredecessors; \
    const char * const *aszSuccessors; \
    int nOrder; \
} ns##_LINK_##name##_t;
该宏定义了一个结构类型,用来保存挂钩的相关定义信息,比如挂钩的调用函数指针、挂钩名称等等,这些信息与在挂钩定义宏中的信息完全相同。比如对于post_config来说,其展开后的结果如下所示,结构中每个字段的含义前面已经说明,此处不再赘述。
typedef struct ap_LINK_post_config_t
{
    ap_HOOK_post_config_t *pFunc;
    const char *szName;
    const char * const *aszPredecessors;
    const char * const *aszSuccessors;
    int nOrder;
} ap_LINK_post_config_t;
为此,我们可以看到,尽管对外我们声明的仅仅是AP_DECLARE_HOOK宏,而内部却一口气实现了五个功能,每个功能彼此相关。
5.5.3 挂钩链声明(APR_HOOK_LINK)
在Apache中,系统定义了一定数量的挂钩,这些挂钩总的来说可以分为两大类:启动挂钩和请求挂钩。启动挂钩是随着服务器启动进行调用的挂钩;而请求挂钩则是服务器处理请求时候进行调用的挂钩。这两种挂钩的实质唯一区别就是它们在服务器中的平均调用频率以及它们在被调用时候服务器充当的角色。所有启动挂钩作为启动服务器的用户进行调用,通常是指UNIX超级用户,而所有请求挂钩都是作为配置文件中指定的用户进行调用。
Apache中预定义了大量的挂钩,启动挂钩有pre_config、post_config、open_logs以及child_init;请求挂钩包括pre_connection、process_connection、create_request等等。Apache中声明的挂钩通常都是全局的,存在且仅存在一个。不过对应于挂钩的调用函数则通常不止一个。比如对于挂钩post_config,在核心模块core.c中调用的挂钩函数是core_post_config,在status模块mod_status.c中调用的挂钩函数是status_init,而在include模块mod_include.c中对应的挂钩函数则是include_post_config。对于同一个挂钩,不同模块对应于其的处理函数各不相同,为了能够保存各个模块中对同一挂钩的使用信息,Apache使用apr_array_header_t数组进行保存。
为此该宏的定义很简单,实际上无非就是声明了一个apr_array_header_t类型的数组,用来保存对应于某个挂钩的所有挂钩函数。比如APR_HOOK_LINK(post_config)展开后定义如下:
apr_array_header_t *link_post_config;
各个模块中对挂钩post_config进行处理的信息都保存在数组link_post_config中,其中link_post_config数组中每个元素的类型都是ap_LINK_post_config_t结构;当然不同的数组,其类型也不相同,不过形式上都是ap_LINK_XXXX格式的。关于挂钩的大部分信息都由这个结构提供。
5.5.4 挂钩结构(APR_HOOK_STRUCT)
正如前面所说,对于每一个挂钩,Apache都会定义一个apr_array_header_t数组来保存它的相关信息。通常,该数组定义在实现挂钩的文件中,比如在config.c文件Apache实现了header_parser、pre_config、post_config、open_logs、child_init以及handler、quick_handler、optional_fn_retrieve八个挂钩,那么该文件中就应该定义八个数组来保存它们的信息。一旦定义挂钩数组,那么该数组将在整个Apache中保持唯一。当某个模块想要使用该挂钩的时候,其就向模块内对于该挂钩的处理结构压入数组。为了便于各模块对数组的访问,原则上必须将数组声明为全局变量,这是最简单的实现方式。
不过Apache2.0中并不支持直接访问挂钩数组,因此你想直接将数据压入处理结构那是“妄想”。为此Apache2.0中引入了APR_HOOK_STRUCT宏。该宏定义如下:
#define APR_HOOK_STRUCT(members) \
static struct { members } _hooks;
该宏展开后实际上定义了一个限于模块内使用的结构_hook,该模块内实现的所有挂钩的对应数组都保存为_hook的成员。比如config.c中的APR_HOOK_STRUCT定义如下:
APR_HOOK_STRUCT(
           APR_HOOK_LINK(header_parser)
           APR_HOOK_LINK(pre_config)
           APR_HOOK_LINK(post_config)
           APR_HOOK_LINK(open_logs)
           APR_HOOK_LINK(child_init)
           APR_HOOK_LINK(handler)
           APR_HOOK_LINK(quick_handler)
           APR_HOOK_LINK(optional_fn_retrieve)
)
其展开后变为如下:
static struct{
           apr_array_header_t *link_header_parser;
           apr_array_header_t *link_pre_config;
           apr_array_header_t *link_post_config;
           apr_array_header_t *link_open_logs;
           apr_array_header_t *link_child_init;
           apr_array_header_t *link_handler;
           apr_array_header_t *link_quick_handler;
           apr_array_header_t *link_optional_fn_retrieve;
}_hook;
因此任何对挂钩数组的访问都必须通过_hook来实现。比如我们在某个模块中使用挂钩post_config,那么在数组中增加数据可以用下面的代码:
    ap_LINK_post_config_t *pHook;
    if (!_hooks.link_post_config) {
        _hooks.link_post_config = apr_array_make(apr_hook_global_pool, 1,
                                                 sizeof(ap_LINK_post_config_t));
        apr_hook_sort_register("post_config", &_hooks.link_post_config);
    }
    pHook = apr_array_push(_hooks.link_post_config);
    pHook->pFunc = …;
    pHook->aszPredecessors = …;
    pHook->aszSuccessors = …;
    pHook->nOrder = …;
    pHook->szName = …;
不过有几点必须注意的是:
(1)、_hook在某个文件中如果定义,则只能定义一次。该文件中使用的定义的挂钩数组统统添加到该结构中,正所谓“大肚能容,容天下难容之事”。
(2)、_hook的定义为static,这意味这该结构实际上是模块内部的私有结构,外部模块无法直接访问_hook结构,而且_hook结构不仅仅在一个文件中出现,只要实现了挂钩,按道理就应该有一个_hook结构。尽管如此,由于static属性,它们相互之间“绝缘”,不会相互干扰。
(3)、当某个模块想使用某个挂钩,比如post_config的时候,其一不能直接访问post_config挂钩数组,二不能访问被屏蔽的模块内_hook,那么它应该怎么办呢?它只能使用挂钩注册函数。正如前面所述,挂钩注册函数通常形如ap_hook_name。比如模块需要使用post_config挂钩,其必须调用ap_hook_post_config进行注册。现在如果我们将在编写模块的时候遇到类似下面的代码,你就不会纳闷了:
static void register_hooks(apr_pool_t *p)
{
    ap_hook_handler(status_handler, NULL, NULL, APR_HOOK_MIDDLE);
    ap_hook_post_config(status_init, NULL, NULL, APR_HOOK_MIDDLE);
}
register_hooks是模块mod_status.c中的挂钩注册函数,在该模块中,Apache注册了两个挂钩:handler和status_init,分别对应的挂钩函数为status_handler和status_init。
不过即使你查遍Apache的是所有的文件,你也不可能找到ap_hook_handler和ap_hook_post_config的函数声明和实现。不仅如此,所有的ap_hook_HOOKNAME形式的函数你都不可能找到你平常希望的函数声明和实现。那么Apache到底是在哪儿实现了这些挂钩函数呢??一切答案都在宏APR_IMPLEMENT_EXTERNAL_HOOK_BASE里面。
关于作者
张中庆,目前主要的研究方向是嵌入式浏览器,移动中间件以及大规模服务器设计。目前正在进行Apache的源代码分析,计划出版《Apache源代码全景分析》上下册。Apache系列文章为本书的草案部分,对Apache感兴趣的朋友可以通过flydish1234 at sina.com.cn与之联系!

原创粉丝点击