Apache源代码分析——模块的加载

时间:2022-01-17 05:33:28

转载请著名来源:http://blog.csdn.net/tingya

本文分析了Apache中关于模块的加载过程

阅读本文之前,请先阅读Apache源代码分析——关于模块结构的几个重要概念一文

///////////////////////////////////////////////////////////////////////////////////////
在了解了上面的四个重要的概念后,我们现在来看看Apache中是如何进行模块加载的。
Apache中对模块的装载处理的相关函数主要集中于mod_so.c中,其本身也是一个子模块,该子模块用来动态状态其余的模块。在mod_so.c文件的开始位置有很长的一段文件描述,将该描述翻译成中文如下:
该模块(指Mod_so)主要用来在运行的时候动态装入Apache模块,这意味着对服务器可以进行功能扩展而不需要重新对源代码进行编译,甚至根本不需要停止服务器。我们所需要做的仅仅是给服务器发送信号HUP或者AP_SIG_GRACEFUL通知服务器重新载入模块。
当然,在动态加载之前,你首先必须将你的模块编译成为动态链接库,然后更新你所指定的配置文件,通常情况下是httpd.conf。这样Apache核心就可以在启动的时候调用你的模块了。
将模块编译成为共享受库的最简单的方法就是在配置中使用ShareModule命令,而不是使用AddModule命令,而且你必须将文件的扩展名称从‘.o’改变为‘.so’。比如如果我们想将status模块变为共享库,在配置文件中我们只需要将
AddModule        modules/standard/mod_status.o
更改为
SharedModule     modules/standard/mod_status.so
一旦更改完毕,运行配置文件同时进行编译。现在Apache的httpd的二进制文件中并没有包含mod_status模块,。。。。
为了使用共享模块,将.so文件拷贝到适当的目录中。你可能需要在服务器根目录下创建一个名称为“modules”的目录,比如“/usr/local/httpd/modules”。
下面的事情就是编辑你的conf/httpd.conf文件,同时增加一行“LoadModule”命令。比如:
LoadModule      status_module    modules/mod_status.so
该命令的第一个参数是模块的名称,名称可以在module_source的最后找到。第二个选项是模块所处的路径,这个路径是相对于服务器路径而言。
如果服务器还在运行的时候,你就编辑LoadModule命令,那么你可以通过发送信号HUP或者AP_SIG_GRACEFUL给服务器,一旦接受到该信号,Apache将重新装载模块,而不需要重新启动服务器。

下面我们将来看看Apache是如何动态装载模块的。在分析每个模块之前,我们首先需要分析的就是模块的数据结构以及该模块能够处理的命令表。对于mod_so模块也不例外。mod_so模块的结构和其中的命令表格如下所示:
static const command_rec so_cmds[] = {
    AP_INIT_TAKE2("LoadModule", load_module, NULL, RSRC_CONF | EXEC_ON_READ,
      "a module name and the name of a shared object file to load it from"),
    AP_INIT_ITERATE("LoadFile", load_file, NULL, RSRC_CONF  | EXEC_ON_READ,
      "shared object file or library to load into the server at runtime"),
    { NULL }
};
module AP_MODULE_DECLARE_DATA so_module = {
   STANDARD20_MODULE_STUFF,
   NULL,     /* create per-dir config */
   NULL,     /* merge per-dir config */
   so_sconf_create, /* server config */
   NULL,     /* merge server config */
   so_cmds,    /* command apr_table_t */
   NULL    /* register hooks */
};
模块中能够处理的所有命令都保存在so_cmds中,从命令表中可以看出,mod_so模块可以处理的命令只有“LoadModule”和“LoadFile”,相应的处理函数分别为load_module和loadfile。下面我们首先来看load_module函数的实现,该函数也是模块装载处理的入口。
static const char *load_module(cmd_parms *cmd, void *dummy,
                               const char *modname, const char *filename)
该函数用来将共享对象载入到服务器的地址空间中。其中,cmd和dummy是所有的命令处理程序都必须具有的,其用于Apache核心给模块传递相应的信息。modname是需要状态的模块的路径名称。
函数首先要做的事情就是在现有的所有的模块中查找是否已经存在需要载入的模块,如果该模块已经载入,则什么都不处理,否则则对其进行装载。遍历的模块包括动态装载的和静态编译两种。
对于动态载入模块,函数首先得到模块so_module中的模块配置信息,这个通过宏ap_get_module_config来实现。
在server_rec结构中,成员module_config是一个一维向量结构,专门用来存储各个模块针对本服务器的配置信息,向量结构中的每一个元素对应存储一个模块针对本服务器的所有配置信息,结构如下所示: 
从上面的结构图中可以看出,如果要获取第i个即索引为i的模块针对本服务器的配置信息的话,可以通过下面的表示式来获取:module_config[i],事实上模块的索引由模块的module_index决定,因此式子展开为:module_config[(m)->module_index]。这实际上真是宏ap_get_module_config展开后的结果,在Apache中,该宏定义为
#define ap_get_module_config(v,m)
    (((void **)(v))[(m)->module_index])
根据上面的描述,我们不难理解该宏的含义。不过通过该宏获取的信息各个模块是千差万别,因此返回的类型只能是void**,因此如果需要使用最好进行转换。就so_module模块而言,其每个元素都是so_server_conf类型的。该类型非常简单,其内部就是一个简单的数组:
typedef struct so_server_conf {
    apr_array_header_t *loaded_modules;
} so_server_conf;
模块对应的配置信息实际上都保存在数组loaded_modules中。数组中的每个元素是moduleinfo结构,因此函数需要做的实质上就是遍历查找loaded_modules数组,判断其中是否存在给定的模块,比较只需要通过模块名称进行。下面的代码完成的就是查找动态加载模块:
sconf = (so_server_conf *)ap_get_module_config(cmd->server->module_config,
                                       &so_module);
modie = (moduleinfo *)sconf->loaded_modules->elts;
for (i = 0; i < sconf->loaded_modules->nelts; i++) {
modi = &modie[i];
if (modi->name != NULL && strcmp(modi->name, modname) == 0) {
ap_log_perror(APLOG_MARK, APLOG_WARNING, 0,
                          cmd->pool, "module %s is already loaded, skipping",
                          modname);
return NULL;
}
    }
对动态加载模块检查完毕后,Apache将检查静态链接模块数组ap_preloaded_modules。在ap_preloaded_modules数组中查找指定模块相对简单。对于每一个模块,Apache必须保证其文件名是以“mod_”开始的,比如mod_so.c、mod_alias.c等等,如果命名格式不是这样,函数将认为模块不是合法的模块。函数的名称最终通过宏STANDARD20_MODULE_STUFF以__FILE__反映到module结构的name属性中,因此函数只需要判断name是否合法就可以了。
如果需要载入的模块没有被载入,则函数首先在动态载入模块数组sconf->loaded_modules中压入一个新的module元素,同时将该结构的名称置为modname,既而函数调用apr_dso_load将文件载入到Apache的地址空间中,同时调用apr_dso_sym获取动态链接库中的module结构,返回的结构保存在modsym中。一旦得到modsym,我们就相当于得到了该模块的module结构了,用modp表示,同时该模块内的动态加载句柄dynamic_load_handle设置为dlopen取得的句柄。
在真正的使用即激活加载模块之前,Apache必须确保加载的模块确实是Apache模块,为此Apache在模块结构中设定了magic字段,通过检查magic字段,apache确定加载的模块是否是apache模块。对于Apache2.0而言,该值为“AP2.0”。另外apache还可以通过结构中的version来判断模块的兼容性。如果加载的是合法的2.0模块,函数将立即调用ap_add_loaded_module将模块激活,所谓的激活无非就是将模块放入ap_top_modules链表中。
此外apache含需要在配置内存池pconf中注册cleanup处理函数。这样当我们重新启动或者关闭服务器的时候,cleanup函数将自动调用将共享模块卸载;最后我们需要做的就是为模块运行配置过程。

///////////////////////////////////////////////////////////////////////////////////////
APR_DECLARE(apr_status_t) apr_dso_load(apr_dso_handle_t **res_handle,
                                       const char *path, apr_pool_t *ctx);
该函数用来载入DSO动态共享库。res_handle_Location用来存储新的DSO的处理句柄;path则是DSO库的路径位置;
apr_dso_load的实现分为七种平台:AIX、beos、netware、OS2、OS390、Unix以及Win32。我们首先来看看Linux平台下的实现。为此我们首先了解一下Linux下对动态链接库是如何的操作。
dso.c中的大部分函数都是对dl_xx实现了简单的封装而已。
现在我们再来看apr_dso_load的实现。函数首先调用dlopen对模块进行加载,返回加载后的句柄os_handle,同时将句柄保存在apr_dso_handle_t类型变量res_handle的handle中。res_handle所需要的所有资源来自内存池ctx。最后调用apr_pool_cleanup_register函数注册内存池ctx的清除函数dso_cleanup。
apr_dso_unload函数则更简单,其只是调用了apr_pool_cleanup_run函数清除分配的内存池,同时调用dso_cleanup函数进行模块卸载。
///////////////////////////////////////////////////////////////////////////////////////
AP_DECLARE(void) ap_add_loaded_module(module *mod, apr_pool_t *p)
该函数功能非常的简单,其实现了模块的动态加载和激活。所谓加载就是将模块假如ap_loaded_modules数组中;所谓的激活就是调用ap_add_module将模块放入ap_top_modules链表中。
///////////////////////////////////////////////////////////////////////////////////////
AP_DECLARE(void) ap_add_module(module *m, apr_pool_t *p)
该函数用于在服务器中激活指定的模块m。通常其对应于httpd.conf文件中的AddModule命令。
在将模块加入服务器之前,Apache必须判断模块的版本是否与当前Apache服务器的版本相同。Apache2.0的所有模块的版本号统一为MODULE_MAGIC_NUMBER_MAJOR;min_version则为MODULE_MAGIC_NUMBER_MINOR。不过apache仅仅核对主版本号码,次版本号不同无所谓。
在Apache中,主版本号的变更意味着模块的兼容性出现了问题,包括模块数据结构的变化等等,次版本号则意味着模块的小范围的调整,兼容性没有出现问题。
所有的模块最终都被加入到全局模块列表ap_top_module中,所有新增加的模块都插入ap_top_module链表的起始位置。一旦插入完成,apache将对插入的模块进行索引标记。标记的方法很简单,无非就是将当前的总模块数作为标记索引;同时累加总模块数和动态载入的模块数,有一点需要注意的事,累加后的总模块数不能超过动态载入模块的最大数DYNAMIC_MODULE_LIMIT。
另外还有一个小小的实现细节必须考虑到。正如我们前面曾经说过,module结构中的name值最终实际上填充的是__FILE__,不过不同的C编译器对__FILE__支持不一样。有的C编译器其值仅仅是文件名称,而有的C编译器其值则是完整的文件路径名称。而我们需要的仅仅是个文件名而已,因此对于第二种情况,我们需要将名称提取出来,在DOS和Unix下实现代码如下:
if (ap_strrchr_c(m->name, '/'))
m->name = 1 + ap_strrchr_c(m->name, '/');
if (ap_strrchr_c(m->name, ''))
m->name = 1 + ap_strrchr_c(m->name, '');
至此函数需要做的最后一件事情就是调用ap_register_hook注册该模块的所有钩子。ap_register_hook的内部无非还是调用的模块本身的钩子注册函数句柄register_hooks。
///////////////////////////////////////////////////////////////////////////////////////
AP_DECLARE(void) ap_remove_module(module *m)
该函数的功能与ap_add_module对应,其用于将指定的模块修改为非激活状态。在函数内部,apache遍历整个ap_top_modules链表查找模块m。查找分为三种情况:
(1)要查找的模块位于ap_top_modules的头部,此时仅仅需要修改ap_top_modules指向下一个结点。
(2)要查找的模块不在链表头部,函数将顺着next往后遍历,一旦找到模块m,则将指针定位于其前面的一个结点,修改其next指向模块m后面的结点,即modp->next = modp->next->next。
(3)如果查找到链表的末尾都没有找到,则意味着该模块不存在。
一旦将module被移除,就修改total_modules和dynamic_modules的值。
///////////////////////////////////////////////////////////////////////////////////////
AP_DECLARE(void) ap_setup_prelinked_modules(process_rec *process)
该函数将Apache中的缺省模块加载并激活。该函数也是Apache中所有的模块处理的入口函数,最早出现在main.c中。
函数首先对ap_preloaded_modules进行索引标记工作,然后将其中的元素逐一拷贝到ap_loaded_modules数组中。加载后需要激活的模块都保存在ap_prelinked_modules中,为此函数将对该数组中的每一个元素调用ap_add_modules进行激活。
Apache2.0中ap_preloaded_modules和ap_prelinked_modules是完全相同的,该函数刻意分为两个数组进行处理实际上延续的还是老版本的。
一旦激活所有的模块,函数调用apr_hook_sort_all对所有模块内的钩子进行排序,以便于处理。
///////////////////////////////////////////////////////////////////////////////////////
AP_DECLARE(const char *) ap_find_module_name(module *m)
函数获取模块m的名称
///////////////////////////////////////////////////////////////////////////////////////
AP_DECLARE(module *) ap_find_linked_module(const char *name)
函数在所有的激活模块中查找名称为name的模块,如果查到,返回该模块;否则返回NULL。
///////////////////////////////////////////////////////////////////////////////////////
AP_DECLARE(int) ap_add_named_module(const char *name, apr_pool_t *p)
该函数将已经装入即存在于ap_loaded_modules数组中的具有指定名称name的模块激活。成功时返回1;否则返回0。

关于作者
张中庆,目前主要的研究方向是嵌入式浏览器,移动中间件以及大规模服务器设计。目前正在进行Apache的源代码分析,计划出版《Apache源代码全景分析》上下册。Apache系列文章为本书的草案部分,对Apache感兴趣的朋友可以通过flydish1234 at sina.com.cn与之联系!

如果你觉得本文不错,请点击文后的“推荐本文”链接!!