ngx-conf-parsing
对 Nginx 配置文件的一些认识:
- 配置指令具有作用域,分为全局作用域和使用
{}
创建其它作用域。 - 同一作用域的不同的配置指令没有先后顺序;同一作用域是否能使用相同的指令,和对相 同指令的处理由各模块自行决定
- 整个 Nginx 的运行时各模块行为都和配置指令密切相关
- 每个配置指令都只能在预先定义好的作用域中使用
- 配置指令解析使用递归的方式
配置解析相关代码量巨大,本文势必很冗长。所以,都要做好心理准备。同时,由于 mail
模块平时使用实在不多,故,本文只对 http
应用场景下的配置解析过程进行分析。
同时,为了行文方便,先约定一些名称:
-
配置文件 – 指 Nginx 的主配置文件
nginx.conf
。在配置文件中使用include
引入其它配置文件的方式这里不做分析。 -
配置指令 – 配置文件中的基本单位,配置指令可接收0个或多个配置指令参数。
-
配置项 – 配置指令对应的内部存储。一个配置指令的值可能会影响到多个配置项的 值。
-
配置项结构体 – 每个模块相关的配置项集中管理用的结构体。
-
作用域模块配置数组 – 每个作用域对应的一个
void *
类型的数组。每个void *
指针指向可用于此作用域的模块对应的配置项结构体。
基本概念
-
Nginx 的模块使用
ngx_module_t
结构表示 -
Nginx 按分类和等级,将模块划分为
NGX_CORE_MODULE
、NGX_EVENT_MODULE
、NGX_HTTP_MODULE
、NGX_MAIL_MODULE
等类型,由ngx_module_t.type
存储。 -
Nginx 的配置指令使用
ngx_command_t
结构进行 “声明”。模块支持的配置指令由ngx_module_t.commands
数组存储。 -
模块上下文 (
module context
) 定义了针对模块配置的一系列操作 (申请配置存储、 继承上级模块的配置等等),不同的操作在配置分析的不同阶段被调用。模块上下文根据模 块类型分为ngx_core_module_t
、ngx_event_module_t
、ngx_http_module_t
、ngx_mail_module_t
等等。模块上下文由ngx_module_t.ctx
存储。 -
配置指令的作用域。任何一个配置指令都有其能够使用的范围,即其作用域。配置指令如 果在其作用域外使用,Nginx 都会打印错误信息并退出。配置指令作用域目前有
、NGX_DIRECT_CONF
、NGX_MAIN_CONFNGX_EVENT_CONF
、NGX_HTTP_MAIN_CONF
、NGX_HTTP_SRV_CONF
、NGX_HTTP_LOC_CONF
、NGX_MAIL_MAIN_CONF
、NGX_MAIL_SRV_CONF
等。- 一个配置指令可以在多个作用域中定义。
- 在父作用域里定义的指令,也会被子作用域继承。但是,如果子作用域也使用了同一 个指令,子作用域的指令值会覆盖高级别作用域的指令。
-
配置指令的参数。配置指令可以接受 0 个或多个参数。它能接受的参数个数也需要在定 义配置指令时明确声明,以便
Nginx
在解析配置过程中,对配置文件正确性进行检查。- 有一些配置指令,比如,
http
,event
,server
,location
,mail
,types
等,用来显式的定义作用域,它们被标识为NGX_CONF_BLOCK
,表示一个{}
块的开始。
- 有一些配置指令,比如,
-
配置指令的作用域和可接受参数等信息,由
ngx_command_t.type
字段存储。 -
配置指令解析函数。基本每个配置指令 (简单的单值指令除外) 都定义了相应的回调函数。
Nginx
在解析配置过程中,如果碰到了该指令,会调用此回调函数对指令进行进一步的解 析。
下图是一个简单 nginx.conf
的作用域示意图:
-
Nginx 配置存储的内部结构,也是按作用域组织的。每个作用域都为其支持指令可以出现 在该作用域的模块预留有位置 (一个
void *
)。 -
默认配置情况下,Nginx 启用的模块对应的类型 (模块按启用顺序从上到下) 如下:
------------------------------------------------------ module type ------------------------------------------------------ ngx_core_module NGX_CORE_MODULE ngx_errlog_module NGX_CORE_MODULE ngx_conf_module NGX_CONF_MODULE ngx_events_module NGX_CORE_MODULE ngx_event_core_module NGX_EVENT_MODULE ngx_epoll_module NGX_EVENT_MODULE ngx_http_module NGX_CORE_MODULE ngx_http_core_module NGX_HTTP_MODULE ngx_http_log_module NGX_HTTP_MODULE ngx_http_upstream_module NGX_HTTP_MODULE ngx_http_autoindex_module NGX_HTTP_MODULE ... NGX_HTTP_MODULE
准备工作
配置文件解析有两种场景:
- Nginx 进程启动时,读取配置文件,并根据配置完成相应初始化。
- Nginx 收到重新加载配置文件的信号后,读取配置文件,并根据当前配置文件完成相成相 应初始化。随后,释放根据老配置文件申请的资源 (被新配置重复利用的除外)。
本文也只针对第一种场景进行分析。
在配置解析开始之前,Nginx 要为配置解析过程分配一个持久内存池和临时内存池。从持久 内存池中申请的内存,用于存储从配置文件中解析完成并正确初始化的配置结构体;而临时 内存池中申请的内存,只在解析过程中作为临时存储,待配置解析完成后,就会被回收。
随后,Nginx 准备第一层级的模块配置索引数组,第一层级只存储 NGX_CORE_MODULE
模 块的配置结构体,这一层级的作用域为NGX_MAIN_CONF
:
------------core/ngx_cycle.c:183------------------------
cycle->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *));
...
if (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->type != NGX_CORE_MODULE) {
continue;
}
module = ngx_modules[i]->ctx;
if (module->create_conf) {
rv = module->create_conf(cycle);
...
cycle->conf_ctx[ngx_modules[i]->index] = rv;
}
}
解析框架
配置解析流程相当简洁标准:准备当前作用域上下文,读取指令并分析指令参数,最后再调 用指令对应的回调函数。
拿第一层级 NGX_MAIN_CONF
作用域来说,其解析过程大致如下:
------------core/ngx_cycle.c:246------------
conf.ctx = cycle->conf_ctx;
conf.cycle = cycle;
conf.pool = pool;
conf.log = log;
conf.module_type = NGX_CORE_MODULE;
conf.cmd_type = NGX_MAIN_CONF;
...
ngx_conf_parse(&conf, &cycle->conf_file);
for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->type != NGX_CORE_MODULE) {
continue;
}
module = ngx_modules[i]->ctx;
if (module->init_conf) {
module->init_conf(cycle, cycle->conf_ctx[ngx_modules[i]->index]);
...
}
}
上面的代码解析 NGX_CORE_MODULE
类型的模块,解析出来的指令位于作用域 NGX_MAIN_CONF
中。同时,解析完成的数据,按模块存放于conf.ctx
中。在配置文件 解析完毕后,Nginx 调 NGX_CORE_MODULE
类模块的 init_conf
函数根据已经读取到的 配置完成进一步初始化操作。
ngx_conf_parse
是配置解析的入口函数,它根据当成上下文读取配置文件数据,进行分 析的同时对配置文件语法进行检查。同时,这个函数会在指令处理函数修改作用域等上下文 信息后,被间接递归调用。
下面就来看一看 ngx_conf_parse
的执行流程。
----------core/ngx_conf_file.c:101----------------
char *
ngx_conf_parse(ngx_conf_t *cf, ngx_str_t *filename)
{
ngx_buf_t buf;
if (filename) {
fd = ngx_open_file(filename->data, NGX_FILE_RDONLY, NGX_FILE_OPEN, 0);
...
cf->conf_file = &conf_file;
...
cf->conf_file->buffer = &buf;
...
buf.start = ngx_alloc(NGX_CONF_BUFFER, cf->log);
...
cf->conf_file->file.fd = fd;
...
type = parse_file;
} else if (cf->conf_file->file.fd != NGX_INVALID_FILE) {
type = parse_block;
} else {
type = parse_param;
...
for ( ;; ) {
rc = ngx_conf_read_token(cf);
...
rc = ngx_conf_handler(cf, rc);
}
...
}
第一次调用 ngx_conf_parse
时,函数会打开配置文件,设置正在执行的解析类型,读取 token
,然后调用ngx_conf_handler
。那么 ngx_conf_handler
又做了些什么呢?
-------------core/ngx_conf_file.c:279-------------
static ngx_int_t
ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last)
{
...
for (i = 0; ngx_modules[i]; i++) {
/* module type checking */
...
cmd = ngx_modules[i]->commands;
...
for ( /* void */ ; cmd->name.len; cmd++) {
/* name comparison */
...
/* namespace checking */
...
/* checking argument numbers */
...
/* set up the directive's configuration context */
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];
}<