nginx配置信息的解析流程
请关注最新修正合订:http://lenky.info/ebook/
这一系列的文章还是在09年写的,存在电脑里很久了,现在贴出来。顺序也不记得了,看到那个就发那个吧,最近都会发上来。欢迎转载,但请保留链接:http://lenky.info/,谢谢。
nginx的配置文件格式是nginx作者自己定义的,并没有采用像语法分析生成器LEMON那种经典的LALR(1)来描述配置信息,这样做的好处就是自由,而坏处就是对于nginx的每一项配置信息都必须自己去解析,因此我们很容易看到nginx模块里大量篇幅的配置信息解析代码,比如模块ngx_http_core_module。
当然,nginx配置文件的格式也不是随意的,它有自己的一套规范:
nginx配置文件是由多个配置项组成的。每一个配置项都有一个项目名和对应的项目值,项目名又被称为指令(Directive),而项目值可能简单的字符串(以分号结尾),也可能是由简单字符串和多个配置项组合而成配置块的复杂结构(以大括号}结尾),因此我们可以将配置项归纳为两种:简单配置项和复杂配置项。
从上面这条规范可以看到这里包含有递归的思想,因此在后面的配置解析代码里可以看到某些函数被递归调用,其原因也就在这里。
对于复杂配置项来说,其值是由多个简单/复杂配置项组成,因此nginx不做过细的处理,一般就是申请内容空间、切换解析状态,然后递归调用解析函数;真正将用户配置信息转换为nginx内变量的值,还是那些简单配置项所对应的处理函数。
不管是简单配置项还是复杂配置项,它们的项目名和项目值都是由标记(token:这里指一个配置文件字符串内容中被空格、引号、括号,比如’{‘、换行符等分割开来的字符子串。)组成的,配置项目名就是一个token,而配置项目值可以是一个、两个、多个token组成。
比如简单配置项:
daemon off;
其项目名daemon为一个token,项目值off也是一个token。而简单配置项:
error_page 404 /404.html;
其项目值就包含有两个token,分别为404和/404.html。
对于复杂配置项:
location /www {
index index.html index.htm index.php;
}
其项目名location为一个token,项目值是一个token(/www)和多条简单配置项组成的复合结构。
前面将token解释为一个配置文件字符串内容中被空格、引号、括号,比如’{‘等分割开来的字符子串,那么很明显,上面例子中的taken是被空格分割出来,事实上下面这样的配置也是正确的:
“daemon” “off”;
‘daemon’ ‘off’;
daemon ‘off’;
“daemon” off;
当然,一般情况下没必要这样费事去加些引号,除非我们需要在token内包含空格而又不想使用转义字符(\)的话就可以利用引号,比如:
log_format main ‘$remote_addr – $remote_user [$time_local] $status ‘
‘”$request” $body_bytes_sent “$http_referer” ‘
‘”$http_user_agent” “$http_x_forwarded_for”‘;
但是像下面这种格式就会有问题,这对于我们来说很容易理解,不多详叙:
“daemon “off”;
对于如此多的配置项,nginx怎样去解析它们呢?在什么时候去解析呢?事实上,对于nginx所有可能出现的配置项(通过项目名即指令Directive去判断),nginx都会提供有对应的代码去解析它,如果配置文件内出现了nginx无法解析的配置项,那么nginx将报错并直接退出程序。
举例来说,对于配置项daemon,在模块ngx_core_module的命令解析数组内的第一项就是保存的对该配置项进行解析所需要的信息,比如daemon配置项的类型,执行实际解析操作的回调函数,解析出来的配置项值所存放的地址等:
{ ngx_string(“daemon”),
NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_FLAG,
ngx_conf_set_flag_slot,
0,
offsetof(ngx_core_conf_t, daemon),
NULL },
而如果我在配置文件内加入如下配置内容:
lenky on;
启动nginx,直接返回错误,这是因为对于lenky指令,nginx没有对应的代码去解析它:
[emerg]: unknown directive “lenky” in /usr/local/nginx/conf/nginx.conf:2
上面给出的解析daemon配置项的数据类型为ngx_command_s结构体类型,该结构体类型对所有的nginx配置项进行了统一的描述:
typedef struct ngx_command_s ngx_command_t;
struct ngx_command_s {
ngx_str_t name;
ngx_uint_t type;
char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
ngx_uint_t conf;
ngx_uint_t offset;
void *post;
};
一个ngx_command_s结构体类型的元素用于解析并获取一项nginx配置,其中字段name指定获取的配置项目名称,字段set指向一个回调函数,该函数执行解析并获取配置项值的操作;而type指定该配置项的相关信息,比如:
1,该配置的类型:NGX_CONF_FLAG表示该配置项目有一个布尔类型的值,例如”daemon”就是一个布尔类型配置项,值为”on”或”off”;NGX_CONF_BLOCK表示该配置项目有一个块类型的值,比如配置项”http”、”events”等。
2,该配置接收的参数个数:NGX_CONF_NOARGS、NGX_CONF_TAKE1、NGX_CONF_TAKE2、……、NGX_CONF_TAKE7,分别表示该配置项没有参数、一个、两个、七个参数。
3,该配置的位置域:NGX_MAIN_CONF、NGX_HTTP_MAIN_CONF、NGX_EVENT_CONF、NGX_HTTP_SRV_CONF、NGX_HTTP_LOC_CONF、NGX_HTTP_UPS_CONF等等。
字段conf被NGX_HTTP_MODULE类型模块所用,该字段指定当前配置项所在的大致位置,取值为NGX_HTTP_MAIN_CONF_OFFSET、NGX_HTTP_SRV_CONF_OFFSET、NGX_HTTP_LOC_CONF_OFFSET三者之一;其它模块不用该字段,直接指定为0。
字段offset指定该配置项值的精确存放位置,一般指定为某一个结构体变量的字段偏移。也有那种块配置项,例如”server”,它不用保存配置项值,或者说无法保存,或者说其值被分得更细小而被保存起来,此时字段offset也指定为0即可。
字段post在大多数情况下为NULL,但在某些特殊配置项中也会指定值,而且多为回调函数指针,例如auth_basic、connection_pool_size、request_pool_size、optimize_host_names、client_body_in_file_only等配置项。
对于配置文件的格式以及配置项在nginx中的封装基本就描述到这,下面开始对整个nginx配置信息的解析流程进行描述。
假设我们以命令:
nginx -c /usr/local/nginx/conf/nginx.conf
启动nginx,而配置文件nginx.conf也比较简单,如下所示:
worker_processes 2;
error_log logs/error.log debug;
events {
use epoll;
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
server {
listen 8888;
server_name localhost;
location / {
root html;
index index.html index.htm;
}
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
下面就来描述nginx是如何将这些配置信息转化为nginx内各对应变量的值以控制nginx工作的。
首先,抹掉一些细节,我们跟着nginx的启动流程进入到与配置信息相关的函数调用处:
main–>ngx_init_cycle–>ngx_conf_parse:
if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) {
environ = senv;
ngx_destroy_cycle_pools(&conf);
return NULL;
}
此处调用ngx_conf_parse传入了两个参数,第一个参数为ngx_conf_s变量,而第二个参数就是保存的配置文件路径字符串[i]“/usr/local/nginx/conf/nginx.conf”。ngx_conf_parse函数是执行配置解析的关键函数,其原型如下:
char * ngx_conf_parse(ngx_conf_t *cf, ngx_str_t *filename);
它是一个间接的递归函数,也就是说虽然我们在该函数体内看不到直接的对其本身的调用,但是它执行的一些函数(比如ngx_conf_handler)内又会调用ngx_conf_parse函数,因此形成递归,这一般在处理一些特殊配置指令或复杂配置项,比如指令include、events、http、server、location等的处理时。
ngx_conf_parse函数体代码量不算太多,但是它也将配置内容的解析过程分得很清楚,总体来看分成三个步骤:1,区分当前解析状态;2,读取配置标记token;3,当读取了合适数量的标记token之后对其进行实际的处理,转换为nginx内变量的值。
当执行到ngx_conf_parse函数内时,配置的解析可能处于三种状态:
第一种,刚开始解析一个配置文件,即此时的参数filename指向一个配置文件路径字符串,需要函数ngx_conf_parse打开该文件并获取相关的文件信息以便下面代码读取文件内容并进行解析,除了在上面介绍的nginx启动时开始主配置文件解析时属于这种情况,还有当遇到include指令时也将以这种状态调用ngx_conf_parse函数,因为include指令表示一个新的配置文件要开始解析。状态标记为type = parse_file;。
第二种,开始解析一个配置块,即此时配置文件已经打开并且也已经对文件部分进行了解析,当遇到复杂配置项比如events、http等时,这些复杂配置项的处理函数又会递归的调用ngx_conf_parse函数,此时解析的内容还是来自当前的配置文件,因此无需再次打开它,状态标记为type = parse_block;。
第三种,开始解析配置项,这在对用户通过命令行-g参数输入的配置信息进行解析时处于这种状态,如:
nginx -g ‘daemon on;’
nginx在调用ngx_conf_parse函数对配置信息’daemon on;’进行解析时就是这种状态,状态标记为type = parse_param;。
前面说过,nginx配置是由标记组成的,在区分好了解析状态之后,接下来就要读取配置内容,而函数ngx_conf_read_token就是做这个事情的:
rc = ngx_conf_read_token(cf);
函数ngx_conf_read_token对配置文件内容逐个字符扫描并解析为单个的token,当然,该函数并不会频繁的去读取配置文件,它每次从文件内读取足够多的内容以填满一个大小为NGX_CONF_BUFFER的缓存区(除了最后一次,即配置文件剩余内容本来就不够了),这个缓存区在函数ngx_conf_parse内申请并保存引用到变量cf->conf_file->buffer内,函数ngx_conf_read_token反复使用该缓存区,该缓存区可能有如下一些状态:
初始状态,即函数ngx_conf_parse内申请后的初始状态。
这是在处理过程中的状态,有一部分配置内容已经被解析为一个个token并保存起来,而有一部分内容正要被组合成token,还有一部分内容等待处理。
这是在字符都处理完了,需要继续从文件内读取新的内容到缓存区。前面图示说过,已解析字符已经没用了,因此我们可以将已扫描但还未组成token的字符移动到缓存区的前面,然后从配置文件内读取内容填满缓存区剩余的空间,情况如下:
如果配置文件内容不够,即最后一次,那么情况就是下面这样:
函数ngx_conf_read_token在读取了合适数量的标记token之后就开始下一步骤即对这些标记进行实际的处理。那多少才算是读取了合适数量的标记呢?区别对待,对于简单配置项则是读取其全部的标记,也就是遇到结束标记分号;为止,此时一条简单配置项的所有标记都被读取并存放在cf->args数组内,因此可以调用其对应的回调函数进行实际的处理;对于复杂配置项则是读完其配置块前的所有标记,即遇到大括号{为止,此时复杂配置项处理函数所需要的标记都已读取到,而对于配置块{}内的标记将在接下来的函数ngx_conf_parse递归调用中继续处理,这可能是一个反复的过程。
当然,函数ngx_conf_read_token也可能在其它情况下返回,比如配置文件格式出错、文件处理完(遇到文件结束)、块配置处理完(遇到大括号}),这几种返回情况的处理都很简单,不多详叙。
对于简单/复杂配置项的处理,一般情况下,这是通过函数ngx_conf_handler来进行的,而也有特殊的情况,也就是配置项提供了自定义的处理函数,比如types指令。函数ngx_conf_handler也做了三件事情,首先,它需要找到当前解析出来的配置项所对应的ngx_command_s结构体,前面说过该ngx_command_s包含有配置项的相关信息以及对应的回调实际处理函数。如果没找到配置项所对应的ngx_command_s结构体,那么谁来处理这个配置项呢?自然是不行的,因此nginx就直接进行报错并退出程序。其次,找到当前解析出来的配置项所对应的ngx_command_s结构体之后还需进行一些有效性验证,因为ngx_command_s结构体内包含有配置项的相关信息,因此有效性验证是可以进行的,比如配置项的类型、位置、带参数的个数等等。只有经过了严格有效性验证的配置项才调用其对应的回调函数:
rv = cmd->set(cf, cmd, conf);
进行处理,这也就是第三件事情。在处理函数内,根据实际的需要又可能再次调用函数ngx_conf_parse,如此反复直至所有配置信息都被处理完。
下面来看一个set回调函数的例子,以对配置指令daemon的解析函数为例,根据前面给出的指令daemon对应的ngx_command_s结构体可以看到,其set回调函数指向的是函数ngx_conf_set_flag_slot,该函数的原型如下:
char * ngx_conf_set_flag_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
这是一个公共的解析函数,即它并不是单独为解析daemon配置指令而存在,而是对于所有NGX_CONF_FLAG类型的配置项都是用的该函数来进行解析。
源文件:ngx_conf_file.c |
char * |
ngx_conf_set_flag_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
char *p = conf;
ngx_str_t *value;
ngx_flag_t *fp;
ngx_conf_post_t *post;
/* 解析出来的对应值存放的内存位置 */
fp = (ngx_flag_t *) (p + cmd->offset);
/* 该内存位置已有值,故知配置指令重复 */
if (*fp != NGX_CONF_UNSET) {
return “is duplicate”;
}
/* cf->args存放的是与当前处理配置项相关的各个token,比如解析daemon配置指令时, cf->args内的数据详细如下,以便于理解(通过gdb调试获得的结果):
(gdb) p *cf->args
$1 = {elts = 0x9a0c798, nelts = 2, size = 8, nalloc = 10, pool = 0x9a0bf00}
(gdb) p *(ngx_str_t*)(cf->args->elts)
$2 = {len = 6, data = 0x9a0c7e8 “daemon”}
(gdb) p *(((ngx_str_t*)(cf->args->elts)+1))
$3 = {len = 3, data = 0x9a0c7f0 “off”}
*/
value = cf->args->elts;
/* 解析,布尔值的配置很好解析,”on”转为nginx内的1,”off”转为0。*/
if (ngx_strcasecmp(value[1].data, (u_char *) “on”) == 0) {
*fp = 1;
} else if (ngx_strcasecmp(value[1].data, (u_char *) “off”) == 0) {
*fp = 0;
} else { /* 出错提示 */
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
“invalid value \”%s\” in \”%s\” directive, ”
“it must be \”on\” or \”off\”",
value[1].data, cmd->name.data);
return NGX_CONF_ERROR;
}
/* 其它处理函数,对于daemon配置指令来说为NULL,但是对于其它指令,比如optimize_server_names则还需调用自定义的处理。*/
if (cmd->post) {
post = cmd->post;
return post->post_handler(cf, post, fp);
}
return NGX_CONF_OK;
}
对于nginx配置文件的解析流程基本就是如此,上面的介绍忽略了很多细节,前面也说过,事实上对于配置信息解析的代码(即各种各样的回调函数cmd->set的具体实现)占去了nginx大幅的源代码,而我们这里并没有做过多的分析,仅例举了daemon配置指令的解析过程,因为对于不同的配置项,解析代码完全是根据自身应用而不同的,当然,除了一些可公共出来的代码以外。最后,看一个nginx配置文件解析的流程图,如下:
//这里面有三个函数参数,cf里面有我们需要的命令的值,cmd则有自定义配置项的偏移值,conf则是自定义配置项的起始地址,将cf中的值,赋给conf中的某个变量即可
char *ngx_conf_set_str_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
char *p = conf;
ngx_str_t *field, *value;
ngx_conf_post_t *post;
// offset主要是存放转换后的值在配置结构体的偏移
field = (ngx_str_t *) (p + cmd->offset);
if (field->data) {
return "is duplicate";
}
value = cf->args->elts;//我们知道参数已经放在cf-args-elts当中了,只是我们需要将该数据放在准确的位置,而value,则是位置选项
*field = value[1];
if (cmd->post) {
post = cmd->post;
return post->post_handler(cf, post, field);
}
return NGX_CONF_OK;
}