上一篇文章分析了nginx配置文件缓冲区的管理,接下来将详细分析nginx是如何解析配置文件的。包含模块上下文结构的创建、core核心模块的解析、event事件模块的解析、http模块的解析。
一、模块上下文结构创建
nginx中的核心模块、事件模块、http模块、赋值均衡模块,反向代理模块等,每一个模块都有一个上下文结构。在解析nginx.conf配置文件时,会将解析出的命令保存到相应模块的上下文结构中。例如:worker_connections 1024;该配置项表示每一个work最大可以处理1024个客户端的连接。nginx在解析到这个配置向时,会将1024这个值保存到事件模块的上下文ngx_event_conf_t中的connections变量中。
nginx有多少个模块,就会创建多少个模块上下文结构。在ngx_init_cycle函数中,会创建所有的模块上下文结构。保存到ngx_cycle_s的conf_ctx变量中,这个变量是一个指针数组。
ngx_cycle_t * ngx_init_cycle(ngx_cycle_t *old_cycle)
{
//创建所有模块的配置上下文空间
cycle->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *));
}
二、core核心模块解析
master进程初始化过程会调用ngx_init_cycle函数初始化ngx_cycle_t结构。这个结构是一个全局结构,每一个进程都有这样一个单独的结构。其中conf_ctx成员是一个指针数组,存放了所有模块的上下文结构。在解析配置文件后,会将解析的结果存储到这个指针数组指向的相应位置。下面代码段将创建一个这样的模块 上下文指针数组。
ngx_cycle_t * ngx_init_cycle(ngx_cycle_t *old_cycle)
{
//将每一个核心模块上下文create_conf的返回值存入到conf_ctx中
for (i = 0; ngx_modules[i]; i++)
{
//必须是核心模块,其它模块暂时不解析,留到ngx_conf_parse函数中解析
if (ngx_modules[i]->type != NGX_CORE_MODULE)
{
continue;
}
//目前只有ngx_core_module模块的上下文实现了ngx_core_module_create_conf方法
module = ngx_modules[i]->ctx;
if (module->create_conf)
{
//创建模块的上下文结构
rv = module->create_conf(cycle);
if (rv == NULL)
{
ngx_destroy_pool(pool);
return NULL;
}
//将上下文保存到上下文指针数组中相应位置
cycle->conf_ctx[ngx_modules[i]->index] = rv;
}
}
}
目前只有ngx_core_module模块实现了create_conf方法。因此创建核心模块的上下文结构后,内存布局如下:
其中cycle->conf_ctx指针数组下标为0的位置就是ngx_core_module核心模块所在的上下文位置,将该指针指向ngx_core_conf_t结构。而ngx_core_module_create_conf方法目前只是给这个结构的所有成员赋一个初值,真正解析则在ngx_conf_parse函数中进行。 在ngx_conf_parse函数解析core模块的配置项时,会将nginx.conf配置文件中所有核心模块所关心的配置项存放到ngx_core_conf_t上下文结构。以worker_process 4;配置项为例说明core模块的解析流程。
ngx_conf_parse
--->ngx_conf_read_token
首先函数ngx_conf_read_token会一个个检测字符,从而得到worker_processes 4;之后会将worker_processes存放到ngx_conf_s的args数组下标为0位置, 4存储到数组下标为1位置。最后根据worker_processes配置项,在所有模块中查找是否有模块实现了worker_processes配置项命令。如果找到,则调用该命令回调。
ngx_conf_parse
--->ngx_conf_handler
//根据配置项查找所有模块,获取到命令,并执行命令
static ngx_int_t ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last)
{
//执行命令
rv = cmd->set(cf, cmd, conf);
}
ngx_core_module核心模块的命令表中,存在worker_processes命令。从而调用命令回调ngx_conf_set_num_slot。
//core模块命令
static ngx_command_t ngx_core_commands[] =
{
{ ngx_string("worker_processes"),
NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
0,
offsetof(ngx_core_conf_t, worker_processes),
NULL },
}
而函数ngx_conf_set_num_slot将把解析到的配置值保存到ngx_core_conf_t成员变量worker_processes。
char * ngx_conf_set_num_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
char *p = conf;
ngx_int_t *np;
ngx_str_t *value;
ngx_conf_post_t *post;
np = (ngx_int_t *) (p + cmd->offset); //找到ngx_core_conf_t成员worker_processes位置
value = cf->args->elts; //解析后的命令,在这个例子中value[0] = worker_processes, value[1] = 4
*np = ngx_atoi(value[1].data, value[1].len); //将值保存到ngx_core_conf_t成员worker_processes
return NGX_CONF_OK;
}
其它核心模块的命令解析流程和这个例子基本上一样。读者可以分析下其它core模块命令的实现流程,后续nginx服务器处理客户端请求时,会使用到各种命令。不对命令解析有个了解,到时还真不知道这个命令是做什么的,以及如何解析的。
三、event事件模块解析
经过core核心模块的分析,我们知道,查找到具体的命令后,最终会调用函数执行具体的命令。
//根据配置项查找所有模块,获取到命令,并执行命令
static ngx_int_t ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last)
{
//执行命令
rv = cmd->set(cf, cmd, conf);
}
而ngx_events_module事件模块的命令数组中有一个events配置项,命令回调方法为
ngx_event_block
static ngx_command_t ngx_events_commands[] =
{
{ ngx_string("events"),
NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
ngx_events_block,
0,
0,
NULL },
ngx_null_command
};
最终解析到"event {"配置项后,会调用ngx_event_block方法。
char * ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
*ctx = ngx_pcalloc(cf->pool, ngx_event_max_module * sizeof(void *));
}
其中ngx_event_max_module表示有多少个事件模块, 该函数会开辟所有事件模块的上下文空间。
char * ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
//调用各个事件模块的create_conf方法,生成每个事件模块的上下文,存放在指针数组中的相应位置
for (i = 0; ngx_modules[i]; i++)
{
//只解析事件模块,其它模块跳过,暂时不解析
if (ngx_modules[i]->type != NGX_EVENT_MODULE)
{
continue;
}
m = ngx_modules[i]->ctx;
//创建各个事件模块的上下文结构
if (m->create_conf)
{
//将事件模块上下文保存到数组中相应位置
(*ctx)[ngx_modules[i]->ctx_index] = m->create_conf(cf->cycle);
}
}
}
各个事件模块实现的create_conf方法只是创建了上下文结构,然后给成员赋初值,此时并还没有解析具体的事件模块的配置。例如ngx_event_core_module模块的create_conf方法为ngx_event_create_conf,函数将创建ngx_event_conf_t结构,并给成员赋初值。
static void * ngx_event_create_conf(ngx_cycle_t *cycle)
{
ngx_event_conf_t *ecf;
//创建ngx_event_core_module模块的上下文件结构
ecf = ngx_palloc(cycle->pool, sizeof(ngx_event_conf_t));
//给各个成员赋初值
ecf->connections = NGX_CONF_UNSET_UINT;
ecf->use = NGX_CONF_UNSET_UINT;
ecf->multi_accept = NGX_CONF_UNSET;
ecf->accept_mutex = NGX_CONF_UNSET;
ecf->accept_mutex_delay = NGX_CONF_UNSET_MSEC;
return ecf;
}
创建完所有事件模块的上下文后,内存布局如下
接下来将开始解析事件模块,函数ngx_events_block会开始调用ngx_conf_parse递归解析。这个时候从解析main块进入解析event块。递归解析完事件块后,会返回继续解析main块。
char * ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
pcf = *cf;
cf->ctx = ctx;
cf->module_type = NGX_EVENT_MODULE; //指定为事件模块,则只会解析事件模块命令
cf->cmd_type = NGX_EVENT_CONF;
//递归调用,解析事件块配置
rv = ngx_conf_parse(cf, NULL);
}
下图是从解析main块进入解析event的流程,在event块解析完成后,会返回到上一层函数调用,继续解析剩余的main块。
在递归调用进入到event块后,开始解析event事件模块。解析具体event块里的所有命令的流程与解析core核心模块的过程是一样的。这里就不在分析了。
在从解析event块返回后,接下来会对未解析到的event事件命令赋一个初值。
char * ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
//调用各个事件模块的init_conf方法,对各个事件模块未赋值的上下文成员进行初始化
for (i = 0; ngx_modules[i]; i++)
{
//只处理事件模块,其它模块忽略
if (ngx_modules[i]->type != NGX_EVENT_MODULE)
{
continue;
}
m = ngx_modules[i]->ctx;
//给各个模块为解析的成员赋一个默认值
if (m->init_conf)
{
rv = m->init_conf(cf->cycle, (*ctx)[ngx_modules[i]->ctx_index]);
}
}
}
四、http模块解析
经过core核心模块的分析,我们知道。查找到具体的命令后,最终会调用函数执行具体的命令。
//根据配置项查找所有模块,获取到命令,并执行命令
static ngx_int_t ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last)
{
//执行命令
rv = cmd->set(cf, cmd, conf);
}
而ngx_http_module事件模块的命令数组中有一个http配置项,命令回调方法为ngx_http_block。最终解析到"http {"配置项后,会调用ngx_http_block方法。
//开始解析http块
static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
//创建一个http块的ngx_http_conf_ctx_t结构
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));
//开辟所有http模块的main_conf
ctx->main_conf = ngx_pcalloc(cf->pool,sizeof(void *) * ngx_http_max_module);
//开辟所有http模块的srv_conf
ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
//开辟所有http模块的loc_conf
ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
}
其中ngx_http_max_module表示有多少个http模块, 对于每个main块,各个http模块对会有一个上下文结构,存放各模块关心的main结构配置。server块,loction块也是如此,各个http模块对会有一个上下文结构,存放各模块关心的server, loction结构配置。
接下来对于main块,创建各个模块关心的上下文结构;对于server块,创建各个模块关心的上下文结构; 对于loction块,创建各个模块关心的上下文结构
//开始解析http块
static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
for (m = 0; ngx_modules[m]; m++)
{
//只处理http模块,其它模块忽略
if (ngx_modules[m]->type != NGX_HTTP_MODULE)
{
continue;
}
module = ngx_modules[m]->ctx;
//该http模块在http类模块中的位置
mi = ngx_modules[m]->ctx_index;
//创建main块结构
if (module->create_main_conf)
{
ctx->main_conf[mi] = module->create_main_conf(cf);
}
//创建server块结构
if (module->create_srv_conf)
{
ctx->srv_conf[mi] = module->create_srv_conf(cf);
}
//创建loc块结构
if (module->create_loc_conf)
{
ctx->loc_conf[mi] = module->create_loc_conf(cf);
}
}
}
同样,create_main_conf, create_srv_conf, create_loc_conf只是创建了模块的上下文结构,然后给成员赋初值。此时还没有解析具体的http模块的配置,稍后会进行解析具体的http配置信息。创建http模块上下文结构后的内存布局如下:
接下来将开始解析http模块,函数ngx_http_block会开始调用ngx_conf_parse递归解析。这个时候从解析main块进入解析http块。递归解析完http块后,会返回继续解析main块。
static char * ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
//开始解析http块
cf->module_type = NGX_HTTP_MODULE; //只解析http块
cf->cmd_type = NGX_HTTP_MAIN_CONF;
//递归解析http块
rv = ngx_conf_parse(cf, NULL);
}
而在解析http块时,如果遇到"server {"则会从http块递归进入到server块。递归解析完server块后,会返回继续解析http块。ngx_http_core_server函数负责解析server块。
static char * ngx_http_core_server(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
{
pcf = *cf;
cf->ctx = ctx;
cf->cmd_type = NGX_HTTP_SRV_CONF; //只解析server块
//开始解析server块
rv = ngx_conf_parse(cf, NULL);
}
而在解析server块时,如果遇到"location {"则会从server块递归进入到location块。递归解析完location块后,会返回继续解析server块。函数ngx_http_core_location中负责解析loaction块
//解析location配置
static char * ngx_http_core_location(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
{
//开始解析local结构
save = *cf;
cf->ctx = ctx;
cf->cmd_type = NGX_HTTP_LOC_CONF;
rv = ngx_conf_parse(cf, NULL);
}
文字描述起来比较吃力,比较难理解,还是上图吧!
五、配置解析源码分析
以上是分析nginx对配置文件的解析流程,现在来分析下解析代码的实现。而解析配置文件的过程还是比较复杂的,细节的东西还是留给读者吧,这里只是梳理下解析配置的框架代码,后续也会将详细注释的源码上传到github中。
解析配置文件的入口为ngx_conf_parse,该函数会间接被递归调用,每次进入函数会触发三种状态中的一种。例如,首次调用函数时,将触发读取配置操作,这个时候状态为parse_file,开始打开文件,准备缓冲区。第二次调用时,则不需要指定filename,参数可以为空,表示已经打开过文件,开始处理复杂块。这个时候状态为parse_block,这种情况是在递归调用过程才会发生。例如解析http块时,会递归调用函数解析server块。parse_param参数只用于解析命令行参数,例如执行./nginx -g "daemon on"时,才会有这种状态。
//解析nginx配置文件,将解析后的配置值保存到各个模块的上下文结构中
char * ngx_conf_parse(ngx_conf_t *cf, ngx_str_t *filename)
{
enum
{
parse_file = 0, //开始解析配置文件
parse_block, //解析复杂块
parse_param //解析命令行参数,例如 nginx -g "daemon on"
} type;
//解析文件状态
if (filename)
{
//打开文件
fd = ngx_open_file(filename->data, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0);
//系统调用获取文件状态,例如获取文件大小
ngx_fd_info(fd, &cf->conf_file->file.info);
cf->conf_file->buffer = &buf;
//分配4K缓冲区,存放从配置文件读取的数据
buf.start = ngx_alloc(NGX_CONF_BUFFER, cf->log);
buf.pos = buf.start;
buf.last = buf.start;
buf.end = buf.last + NGX_CONF_BUFFER;
buf.temporary = 1; //临时缓冲区,内容可以修改
//文件状态结构,每次从位置中读取数据后,会记录已经读取到文件中的哪个位置
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 = 0;
cf->conf_file->file.log = cf->log;
cf->conf_file->line = 1;
//解析文件状态
type = parse_file;
}
else if (cf->conf_file->file.fd != NGX_INVALID_FILE)
{
//解析复杂块状态
type = parse_block;
}
else
{
//解析命令行参数状态
type = parse_param;
}
for ( ;; )
{
//解析配置文件中的每一行记录,会把解析后的结果记录到ngx_conf_s中的args数组中保存。例如解析到worker_process 2;则args下标0存放worker_process,下标1存放2这个值
rc = ngx_conf_read_token(cf);
//解析复杂块完成,例如:解析完了event块,http块等。可以递归返回到上一层
if (rc == NGX_CONF_BLOCK_DONE)
{
goto done;
}
//整个文件解析完成
if (rc == NGX_CONF_FILE_DONE)
{
goto done;
}
//开始解析复杂块,例如在处理http命令时会间接递归调用ngx_conf_parse函数本身
if (rc == NGX_CONF_BLOCK_START)
{
}
//这个回调函数一般用于处理text/html text/css这种样式
//回调函数为ngx_http_core_types。一般不走这个逻辑,而是走ngx_conf_handler逻辑
if (cf->handler)
{
rv = (*cf->handler)(cf, NULL, cf->handler_conf);
continue;
}
//根据解析的配置项,查找相应的命令
rc = ngx_conf_handler(cf, rc);
}
}
ngx_conf_read_token主要做了两件事;1、判断是否需要从配置文件读取数据到缓冲区中,一般情况下在缓冲区数据都解析完成后,会从配置文件读取新数据到缓冲区。这部分内容可以参考"nginx配置解析之缓冲区管理"这篇文件的分析,这里就不在重复了。 2、解析配置文件中的每一行,从而得到配置项 配置值,并把结果保存到数组中。其中配置项与配置值之间使用空格隔开,在解析每一行时就会解析找到配置项与配置值之间的空格,从而获取到配置项。 每行以";"结尾,进而把找到的上一个空格与“;”之间的内容当做配置值。
下面这个代码段就是以空格提取配置项,或者以";"提取配置值。进而将配置项保存到数组下标为0的位置,配置值保存到数组下标为1的位置。
static ngx_int_t ngx_conf_read_token(ngx_conf_t *cf)
{
//查找到配置项与配置值的使用空格分割,或者一行结束时,说明找到配置项,或者配置值
else if (ch == ' ' || ch == '\t' || ch == CR || ch == LF
|| ch == ';' || ch == '{')
{
last_space = 1;
found = 1;
}
//查到到一行的配置信息
if (found)
{
//获取一个空间,存放配置信息
word = ngx_array_push(cf->args);
word->data = ngx_pnalloc(cf->pool, b->pos - start + 1);
//拷贝配置信息
for (dst = word->data, src = start, len = 0;
src < b->pos - 1;
len++)
{
*dst++ = *src++;
}
}
}
而下面的代码段表示在已经找到配置项的条件下,跳过配置项之后的所有空格,开始查找配置值。
static ngx_int_t ngx_conf_read_token(ngx_conf_t *cf)
{
//在已经找到空格情况下,跳过所有空格,从空格之后的内容查找
//例如:worker_process 2; 查找到worker_process后第一个空格后,跳过所有的空格
//last_space初始值为1
if (last_space)
{
if (ch == ' ' || ch == '\t' || ch == CR || ch == LF)
{
continue;
}
//使用局部变量start,记录每一个有效内容开始。最终start与pos之间的内容构成配置项,或者配置值
start = b->pos - 1;
start_line = cf->conf_file->line;
switch (ch)
{
case ';':
case '{': //开始处理复杂块
return NGX_OK;
case '}': //复杂块解析结束
return NGX_CONF_BLOCK_DONE;
case '#': //标记为注释
sharp_comment = 1;
continue;
}
}
}
太细节的东西真不好分析,也没有必要。细节的东西还是读者自己慢慢分析吧! 到此nginx.conf配置文件的解析已经全部分析完成了。下一篇文章将分析http块的合并流程。