在nginx启动过程中,模块的初始化是整个启动过程中的重要部分,而且了解了模块初始化的过程对应后面具体分析各个模块会有事半功倍的效果。在我看来,分析源码来了解模块的初始化是最直接不过的了,所以下面主要通过结合源码来分析模块的初始化过程。
稍微了解nginx的人都知道nginx是高度模块化的,各个功能都封装在模块中,而各个模块的初始化则是根据配置文件来进行的,下面我们会看到nginx边解析配置文件中的指令,边初始化指令所属的模块,指令其实就是指示怎样初始化模块的。
模块初始化框架
模块的初始化主要在函数ngx_init_cycle(src/ngx_cycle.c)中下列代码完成:
ngx_cycle_t *
ngx_init_cycle(ngx_cycle_t *old_cycle)
{
...
//配置上下文
cycle->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *));
...
//处理core模块,cycle->conf_ctx用于存放所有CORE模块的配置
for (i = ; ngx_modules[i]; i++) {
if (ngx_modules[i]->type != NGX_CORE_MODULE) { //跳过不是nginx的内核模块
continue;
}
module = ngx_modules[i]->ctx;
//只有ngx_core_module有create_conf回调函数,这个会调用函数会创建ngx_core_conf_t结构,
//用于存储整个配置文件main scope范围内的信息,比如worker_processes,worker_cpu_affinity等
if (module->create_conf) {
rv = module->create_conf(cycle);
...
cycle->conf_ctx[ngx_modules[i]->index] = rv;
}
}
//conf表示当前解析到的配置命令上下文,包括命令,命令参数等
conf.args = ngx_array_create(pool, , sizeof(ngx_str_t));
...
conf.temp_pool = ngx_create_pool(NGX_CYCLE_POOL_SIZE, log);
...
conf.ctx = cycle->conf_ctx;
conf.cycle = cycle;
conf.pool = pool;
conf.log = log;
conf.module_type = NGX_CORE_MODULE; //conf.module_type指示将要解析这个类型模块的指令
conf.cmd_type = NGX_MAIN_CONF; //conf.cmd_type指示将要解析的指令的类型
//真正开始解析配置文件中的每个命令
if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) {
...
}
...
//初始化所有core module模块的config结构。调用ngx_core_module_t的init_conf,
//在所有core module中,只有ngx_core_module有init_conf回调,
//用于对ngx_core_conf_t中没有配置的字段设置默认值
for (i = ; ngx_modules[i]; i++) {
if (ngx_modules[i]->type != NGX_CORE_MODULE) {
continue;
}
module = ngx_modules[i]->ctx;
if (module->init_conf) {
if (module->init_conf(cycle, cycle->conf_ctx[ngx_modules[i]->index])
== NGX_CONF_ERROR)
{
environ = senv;
ngx_destroy_cycle_pools(&conf);
return NULL;
}
}
}
...
}
cycle->conf_ctx是一个指针数组,数组中的每个元素对应某个模块的配置信息。ngx_conf_parse用户真正解析配置文件中的命令,conf存放解析配置文件的上下文信息,如module_type表示将要解析模块的类型,cmd_type表示将要解析的指令的类型,ctx指向解析出来信息的存放地址,args存放解析到的指令和参数。具体每个模块配信息的存放如下图所示,NGX_MAIN_CONF表示的是全局作用域对应的配置信息,NGX_EVENT_CONF表示的是EVENT模块对应的配置信息,NGX_HTTP_MAIN_CONF,NGX_HTTP_SRV_CONF,NGX_HTTP_LOC_CONF表示的是HTTP模块对应的main,server,local域的配置信息。
下面我们来具体看下ngx_conf_parse函数是怎样解析每个指令的。
char *
ngx_conf_parse(ngx_conf_t *cf, ngx_str_t *filename)
{
...
if (filename) {
//打开配置文件
fd = ngx_open_file(filename->data, NGX_FILE_RDONLY, NGX_FILE_OPEN, );
...
prev = cf->conf_file;
cf->conf_file = &conf_file;
//获取配置文件信息
if (ngx_fd_info(fd, &cf->conf_file->file.info) == NGX_FILE_ERROR) {
...
}
//配置缓冲区用于存放配置文件信息
cf->conf_file->buffer = &buf;
//NGX_CONF_BUFFER = 4096,直接malloc
buf.start = ngx_alloc(NGX_CONF_BUFFER, cf->log);
...
//[pos,last)表示缓存中真正存储了数据的区间,[start,end)表示缓存区的物理区域
buf.pos = buf.start;
buf.last = buf.start;
buf.end = buf.last + NGX_CONF_BUFFER;
buf.temporary = ;
cf->conf_file->file.fd = fd;
cf->conf_file->file.name.len = filename->len;
cf->conf_file->file.name.data = filename->data;
cf->conf_file->file.offset = ;
cf->conf_file->file.log = cf->log;
cf->conf_file->line = ;
type = parse_file; }
...
for ( ;; ) {
rc = ngx_conf_read_token(cf); //从配置文件中读取下一个命令
...
rc = ngx_conf_handler(cf, rc); //查找命令所在的模块,执行命令对应的函数
...
}
...
}
ngx_conf_parse是配置解析的入口函数,它根据当成上下文读取配置文件数据,进行分析的同时对配置文件语法进行检查。同时,如果遇到的指令包含块,这个函数会在指令处理函数修改作用域等上下文信息后,被间接递归调用来处理块中的配置信息。第一次调用ngx_conf_parse时,函数会打开配置文件,设置正在执行的解析类型,读取命令,然后调用ngx_conf_handler。我们先来看下ngx_conf_read_token怎么读取命令的:
//解析一个命令和其所带的参数
static ngx_int_t
ngx_conf_read_token(ngx_conf_t *cf)
{
...
found = ; //标记是否找到一个命令
need_space = ; //下一个字符希望是空格
last_space = ; //上一个字符是空格
sharp_comment = ; //注释
variable = ; //存在变量
quoted = ; //\符号
s_quoted = ; //单引号
d_quoted = ; //双引号
...
for ( ;; ) {
ch = *b->pos++; //当前字符存储于ch中
if (ch == LF) { //换行
if (sharp_comment) { //注释结束,只有行注释
sharp_comment = ;
}
}
if (sharp_comment) { //跳过注释,直到换行
continue;
}
if (quoted) { //如果上一个字符是\,则跳过当前字符,后面读取token会做处理
quoted = ;
continue;
}
if (need_space) { //次字符必须是space分割符
/*
忽略掉space字符;space字符包括' ','\t',CR,LF
如果字符是';','{',此次命令读取结束;
如果字符是')',开始读取新的token;
如果读到其它字符则解析失败
*/
}
if (last_space) { //表示一个token开始了,第一个非space字符
/*
忽略掉space字符;
如果字符是';','{','}',此次命令读取结束;
如果字符是'#',字符后面是注释;
如果字符是'\',下一个字符将被忽略
如果字符是'"',''',下一个字符在单引号或双引号中
*/
} else { //开始读取新token
/*
如果字符是'$',后面的非space字符组成变量token;
碰到了结束引号'"','''时,token读取完成;
碰到了space字符,token读取完成
*/
if (found){
/*
将找到的token追加到cf->args数组中,并且每个token字符串以'\0'结束
*/
}
}
}
}
下面我们再来看下命令处理函数ngx_conf_handler:
static ngx_int_t
ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last)
{
...
for (i = ; ngx_modules[i]; i++) {
cmd = ngx_modules[i]->commands;
...
for ( /* void */ ; cmd->name.len; cmd++) {
//命令名称对比
//模块类型对比
//命令类型对比
//命令是否带块
//检测命令参数
conf = NULL;
if (cmd->type & NGX_DIRECT_CONF) {
conf = ((void **) cf->ctx)[ngx_modules[i]->index]; } else if (cmd->type & NGX_MAIN_CONF) {
conf = &(((void **) cf->ctx)[ngx_modules[i]->index]); } else if (cf->ctx) {
confp = *(void **) ((char *) cf->ctx + cmd->conf); if (confp) {
conf = confp[ngx_modules[i]->ctx_index];
}
}
//调用命令中的set函数
rv = cmd->set(cf, cmd, conf);
...
}
}
...
}
完全读取到一条配置指令后,Nginx会使用该指令名在所有指令定义中进行查找。但是,在进行查找之前,Nginx会先验证模块的类型和当前解析函数上下文中的类型是否一致。随后,在某个模块中找到匹配的指令定义后,还会验证指令可以出现的作用域是否包含当前解析函数上下文中记录的作用域。最后,检查指令的参数个数是否和指令定义中标明的一致。
校验工作完成后,Nginx将指令名和所有模块预定义支持的指令进行对比,找到完全匹配的配置指令定义。根据配置指令的不同类型,配置项的存储位置也不同。
NGX_DIRECT_CONF类型的配置指令,其配置项存储空间是全局作用域对应的存储空间。这个类型的指令主要出现在ngx_core_module模块里。
conf = ((void **) cf->ctx)[ngx_modules[i]->index];
NGX_MAIN_CONF表示配置指令的作用域为全局作用域。纵观Nginx整个代码,除了ngx_core_module的配置指令(同时标识为NGX_DIRECT_CONF)位于这个作用域中外,另外几个定义新的子级作用域的指令–events、http、mail、imap,都是非NGX_DIRECT_CONF的NGX_MAIN_CONF指令,它们在全局作用域中并未被分配空间,所以在指令处理函数中分配的空间需要挂接到全局作用域中,故传递给指令处理函数的参数是全局作用域的地址。
conf = &(((void **) cf->ctx)[ngx_modules[i]->index];
其它类型配置指令项的存储位置和指令出现的作用域(并且非全局作用域)有关:
confp = *(void **) ((char *) cf->ctx + cmd->conf);
if (confp) { conf = confp[ngx_modules[i]->ctx_index]; }
配置项将要存储的位置确定后,调用指令处理函数,完成配置项初始化和其它工作。
rc = cmd->set(cf, cmd, conf);
下面通过分析模块HTTP的初始化来进一步加深理解模块的初始化。
HTTP模块的初始化
http模块由很多个模块组成,这里主要讲解两个主要模块的初始化,它们是ngx_http_module和ngx_http_core_module。ngx_http_moduel模块很简单,它的定义如下:
static ngx_command_t ngx_http_commands[] = {
{ ngx_string("http"), //http命令描述结构
NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
ngx_http_block,
,
,
NULL },
ngx_null_command
};
static ngx_core_module_t ngx_http_module_ctx = {
ngx_string("http"),
NULL,
NULL
};
ngx_module_t ngx_http_module = {
NGX_MODULE_V1,
&ngx_http_module_ctx, /* module context */
ngx_http_commands, /* module directives */
NGX_CORE_MODULE, /* module type */
...
};
从模块的定义看,它是一个NGX_CORE_MODULE类型的模块,就包含一条指令http,当在全局作用域中遇到http指令后,会调用ngx_http_block函数,传递给该函数的参数为:cf,当前配置上下文信息;cmd,http命令描述结构;conf,ngx_http_module模块在全局作用域数组中的地址。下面看下ngx_http_block函数:
static char *
ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
...
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t)); //http main配置结构
...
*(ngx_http_conf_ctx_t **) conf = ctx; //将http main配置结构挂到全局作用域上
//计算NGX_HTTP_MODULE类型模块的个数,并计算每个NGX_HTTP_MODULE模块在全部NGX_HTTP_MODULE模块中的下标
ngx_http_max_module = ;
for (m = ; ngx_modules[m]; m++) {
if (ngx_modules[m]->type != NGX_HTTP_MODULE) {
continue;
}
ngx_modules[m]->ctx_index = ngx_http_max_module++;
}
//http的main域配置
ctx->main_conf = ngx_pcalloc(cf->pool,
sizeof(void *) * ngx_http_max_module);
...
//http的server域,用来合并server块中的域
ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
...
//http的local域,用来合并server块中的local块中的域
ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
//创建各个作用域对应的配置结构
for (m = ; ngx_modules[m]; m++) {
if (ngx_modules[m]->type != NGX_HTTP_MODULE) {
continue;
}
module = ngx_modules[m]->ctx;
mi = ngx_modules[m]->ctx_index;
if (module->create_main_conf) {
ctx->main_conf[mi] = module->create_main_conf(cf);
...
}
if (module->create_srv_conf) {
ctx->srv_conf[mi] = module->create_srv_conf(cf);
...
}
if (module->create_loc_conf) {
ctx->loc_conf[mi] = module->create_loc_conf(cf);
...
}
}
pcf = *cf;
cf->ctx = ctx; //更新配置上下文为http的上下文
...
//递归ngx_conf_parse来调用处理http包含的块的配置信息
cf->module_type = NGX_HTTP_MODULE; //模块类型为NGX_HTTP_MODULE
cf->cmd_type = NGX_HTTP_MAIN_CONF; //指令的作用域
rv = ngx_conf_parse(cf, NULL);
...
*cf = pcf; //恢复配置上下文
...
}
到这里,已经创建的配置信息如下图所示:
递归调用ngx_conf_parse后,接下来处理的每个指令都是NGX_HTTP_MAIN_CONF作用域的,每个指令的处理函数会初始化应配置结构信息,而具体的配置信息存放在哪个数组里面由指令定义中的conf字段决定,即一定是main_conf,srv_conf和loc_conf之中的一种。NGX_HTTP_MAIN_CONF作用域中的很多指令都属于ngx_http_core_module模块,下面看下这个模块的定义:
static ngx_command_t ngx_http_core_commands[] = {
...
{ ngx_string("server"), //server指令
NGX_HTTP_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
ngx_http_core_server,
,
,
NULL },
{ ngx_string("location"), //location指令
NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_BLOCK|NGX_CONF_TAKE12,
ngx_http_core_location,
NGX_HTTP_SRV_CONF_OFFSET,
,
NULL },
...
} static ngx_http_module_t ngx_http_core_module_ctx = {
ngx_http_core_preconfiguration, /* preconfiguration */
NULL, /* postconfiguration */
ngx_http_core_create_main_conf, /* create main configuration */
ngx_http_core_init_main_conf, /* init main configuration */
ngx_http_core_create_srv_conf, /* create server configuration */
ngx_http_core_merge_srv_conf, /* merge server configuration */
ngx_http_core_create_loc_conf, /* create location configuration */
ngx_http_core_merge_loc_conf /* merge location configuration */
};
ngx_module_t ngx_http_core_module = {
NGX_MODULE_V1,
&ngx_http_core_module_ctx, /* module context */
ngx_http_core_commands, /* module directives */
NGX_HTTP_MODULE, /* module type */
...
};
这个模块定义了很多指令,其中server和location是比较关键的两条指令,它们分别处理server和location指令后面所带有的块。处理这些块的方法和处理http指令后面的块的方法类似,都是修改配置上下文,调用ngx_conf_parse来处理,当然在处理server块时只会生成srv_conf和loc_conf数组,所有的server块共享一个main_conf数组,在处理location块时只会生成loc_conf数组,同一个server块中的location共享这个server块中的ser_conf数组。
有些指令能同时出现在http块,server块和location块中,并且底层块中指令会覆盖上层块中的定义,如果底层块中指令没定义,上层块的指令定义会传递到底层块中。所以在函数ngx_http_block调用ngx_conf_parse解析处理所有的http模块的配置指令后,调用ngx_http_merge_servers将http块中的srv_conf信息传递到各个server块中,在ngx_http_merge_servers中将server块中的loc_conf信息合并到所有的locations块中。