上篇中我们提到ngx_init_cycle是nginx的启动过程中最关键的一个步骤,这里面会创建出nginx整个生命周期中最重要的结构体ngx_cycle_t,它用到了一个4级指针****conf_ctx存储着所有模块的配置信息。而每个模块的配置信息来源于配置文件nginx.conf,那么nginx是如何解析这份配置文件的呢?它是怎么将每个配置项准确无误的添加到conf_ctx对应的位置上呢?我们最常用的http、server、location配置信息又是如何存储的呢?当一个指令出现在多处时nginx该采取哪一项呢?本章我们将逐一揭晓。
在谈配置解析之前,我们还需要认识一下nginx提供的两个高级数据结构。首先是ngx_conf_t,每个配置指令都对应了一个ngx_conf_t的结构体,ngx_conf_t的原型摘要如下:
struct ngx_conf_s {
char *name; // 当前解析的指令名
ngx_array_t *args; // 当前指令的所有参数
ngx_cycle_t *cycle; //该指令对应的ngx_cycle_t
ngx_pool_t *pool;
void *ctx; // 该指令的上下文
ngx_conf_handler_pt handler; // 指令自定义的处理函数
… …
};
ngx_conf_t记录了每个指令的属性,例如当解析到一个配置指令时,会将指令名保存在name成员中,将参数保存到args数组中,指令的上下文信息ctx也会动态更新,同时也会去执行该指令的自定义处理函数handler(如果有实现)。可以说nginx的整个解析配置文件的过程都是围绕着ngx_conf_t来进行的。
第二个比较重要的结构体是ngx_command_t,多数情况下配置指令是没有实现自定义的handler的,这时候nginx会让各模块去处理解析到的指令。那么ngx_command_t就维护了一个模块所能处理的指令信息,它的原型摘要如下:
struct ngx_command_s {
ngx_str_t name; //配置指令名称
ngx_uint_t type; //type决定了配置指令可以出现的位置以及携带的参数类型以及个数
char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); //该配置项的处理函数
ngx_uint_t conf; //conf用于指示配置项所处内存的相对偏移位置
ngx_uint_t offset; //offset表示当前配置项在整个存储配置项的结构体中的偏移位置(以字节(Byte) 为单
位)
… …
};
其中,最重要的成员set是一个函数指针,它指向的就是每个模块用于处理该配置项的回调方法。
工欲善其事,必先利其器。nginx的配置文件复杂多样,指令灵活,同一个配置项可以出现在多处,而且支持嵌套以及include的文件引用,针对如此灵活的配置该如何设计一个万能的解析函数呢。nginx创造了一把解析利器:ngx_conf_parse,解析任何配置,只需要这一个函数便能解决,我们先大致浏览一遍它的实现流程:
1、解析分类
首先,nginx将当前需要解析的配置分为三种类型:文件解析、块解析、参数解析。文件解析是指接下来要解析的内容是一份文件,比如第一次进入ngx_conf_parse的时候,此时的类型就是文件解析,对应的filename为nginx.conf,又比如遇到“include vhosts/a.conf”指令时,则会再次调用ngx_conf_parse,此时对应的filename就是a.conf了。而块解析则是针对由{}引用的一块配置内容,常见的http{},server{},location{},upstream{},event{}都属于块解析。最后就是单一指令的解析,nginx将此类归为参数解析。为什么要进行解析分类呢,因为在进行解析之前,会有一些必要的预备工作,而不同的类型对应的预备工作是不同的。例如在进行文件解析nginx都需要先打开文件获取句柄,并且创建buf来暂存解析到的字符, 进行块解析前需要创建并初始化存储当前块配置的结构体。
2、解析token
上图中有一个非常重要的函数ngx_conf_read_token, 它是整个解析过程的基石,nginx通过一个死循环无限调用该函数去解析配置文件,每次调用完成后会返回一个token,并将其存储在当前配置项结构体ngx_conf_t对应的args数组里。所谓一个token是指一组有意义的配置项,它包含了配置指令以及配置参数,例如“error_log /home/nginx/logs/error.log” 就是一组token,它的指令是”error_log”,参数则是路径” /home/nginx/logs/error.log”;每次解析完成之后,nginx会根据当前的返回结果进行处理,如果是解析异常,比如语法错误,则会跳出整个解析循环并退出程序。
3、处理配置
nginx主框架只负责解析出一个个token,但是它并不会处理这些配置,而将这份工作移交给了各个模块,那么接下来nginx就需要在众多模块中找到对该配置感兴趣的模块。整个过程实现很简单,就是去遍历ngx_modules[]数组中的所有模块,通过字符串对比找到一个有能力处理该配置指令的模块,然后调用此模块的ngx_command_t对应的set方法。例如当解析到了”worker_processes 4”这组token,则nginx会找到对该配置感兴趣的是ngx_core_module,然后调用其set指向的回调方法ngx_set_worker_processes,在这个函数里,会将ngx_core_conf_t的woker_processes成员赋值为4 (ngx_core_conf_t就是ngx_core_module用于存储配置项的结构体)。可以想见每个模块的优先级是不一样的,如果一个指令可以同时由多个模块处理,那么优先级高的模块会优先处理,后续的模块很有可能没有机会执行。而模块的优先级决定因素则是它在ngx_modules[]中出现的位置。
综上,nginx在ngx_conf_parse通过循环调用ngx_conf_read_token从配置文件中解析出token,然后去调用某个模块对应的handler处理函数。那么对这个流程有了简单理解后,我们一起来看看最重要http配置解析是如果进行的。
http段的配置可以说是整个配置文件中最重要的部分了,当nginx主框架解析到了一个“http” 的token后,会找到对该配置感兴趣的是核心模块ngx_http_module,并调用对应的set函数ngx_http_block。也就是从这里开始,http框架正式登上了历史舞台,接下来它会接管nginx主框架的工作,并负责整个http块的解析。
1、准备工作ngx_http_conf_ctx_t
typedef struct {
void **main_conf; //存储所有http模块http段配置项信息
void **srv_conf; //存储所有http模块server段配置项信息
void **loc_conf; //存储所有http模块location段配置项信息
} ngx_http_conf_ctx_t;
2、户口登记
3、创建各模块存储配置项结构体
for (m = 0; cf->cycle->modules[m]; m++) {
if (cf->cycle->modules[m]->type != NGX_HTTP_MODULE) {
continue;
}
module = cf->cycle->modules[m]->ctx;
mi = cf->cycle->modules[m]->ctx_index;
if (module->create_main_conf) {
ctx->main_conf[mi] = module->create_main_conf(cf);
if (ctx->main_conf[mi] == NULL) {
return NGX_CONF_ERROR;
}
}
if (module->create_srv_conf) {
ctx->srv_conf[mi] = module->create_srv_conf(cf);
if (ctx->srv_conf[mi] == NULL) {
return NGX_CONF_ERROR;
}
}
if (module->create_loc_conf) {
ctx->loc_conf[mi] = module->create_loc_conf(cf);
if (ctx->loc_conf[mi] == NULL) {
return NGX_CONF_ERROR;
}
}
}
执行完以上流程后,ngx_http_conf_ctx_t对应的内存示意图如下,从图中可以看到每个模块对配置信息的敏感度是有差异的,有的模块在处理配置时并不需要存储配置信息的结构体,例如模块ngx_http_static_module,它在main_conf、srv_conf和loc_conf数组中对应的位置都是空指针。
4、开始解析
万事俱备,只欠东风。既然前期的准备工作都完成了,那么接下来就要正式开始解析了,此时我们的解析利器ngx_conf_parse又再次亮相,和第一次在ngx_init_cycle中调用该方法的区别是这里的解析类型变成了块解析,即type=parse_block。接下来的过程和上文提到的解析流程就完全一致了,也会通过ngx_conf_read_token从配置文件中读取http段下面的一组组token,并遍历所有的http模块,寻找到对此配置项感兴趣的模块去处理。大家可能会自然想到,当解析到“server”这个token时,意味着接下来要解析的是server段的配置了,那对应的处理方法里是不是也会再次调用ngx_conf_parse,遇到location段时还会调用一次?没错就是这样,包括location中又嵌套一层location时,也会进一步递归地调用ngx_conf_parse去解析。每次遇到一段块配置的开始,nginx都会拿出这把利刀,如庖丁解牛般将复杂的配置一层层剖开。下图展示了一个最简单的nginx.conf的解析过程:
图中,ngx_init_cycle和ngx_http_block已经是我们熟悉的面孔了,而ngx_http_core_server和ngx_http_core_location则分别对应这“server”和“location”指令对应的set处理函数,可见从解析http段到location段,至少要嵌套调用ngx_conf_parse四次,从下图的调用栈就能看到这点。
对于用户来说,有时候希望在http{}段写下的某个配置项对所有的server块生效,有时候又希望某个server有自己的特有配置。这些需求是合理的,因为这既能减少用户的工作量,也能赋予他们足够的选择自由。因此nginx的配置指令被设计的十分灵活,大部分配置项能出现在多个地方,例如“error_log”和“root”能出现在main,server,location各个块内。那么对于不同位置的同一配置项,nginx在解析时该以哪一个为准呢,它是怎么解决指令的冲突问题呢?
暂且抛开这个问题,我们先看看上文提到的ngx_http_core_server和ngx_http_core_location两个函数,他们分别是在nginx解析到“server”和“location”时要调用的处理函数。其中ngx_http_core_server的原型代码摘要如下:
static char *
ngx_http_core_server(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
{
ngx_http_conf_ctx_t *ctx, *http_ctx;
// 1 创建ngx_http_conf_ctx_t结构体
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));
if (ctx == NULL) {
return NGX_CONF_ERROR;
}
// 2 将ctx的mian_conf指向http_ctx的mainx_conf, http_ctx为http段对应的ngx_http_conf_ctx_t
http_ctx = cf->ctx;
ctx->main_conf = http_ctx->main_conf;
ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
if (ctx->srv_conf == NULL) {
return NGX_CONF_ERROR;
}
ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
if (ctx->loc_conf == NULL) {
return NGX_CONF_ERROR;
}
// 3 调用所有http模块的create_srv_conf和create_loc_conf
for (i = 0; cf->cycle->modules[i]; i++) {
if (cf->cycle->modules[i]->type != NGX_HTTP_MODULE) {
continue;
}
module = cf->cycle->modules[i]->ctx;
if (module->create_srv_conf) {
mconf = module->create_srv_conf(cf);
if (mconf == NULL) {
return NGX_CONF_ERROR;
}
ctx->srv_conf[cf->cycle->modules[i]->ctx_index] = mconf;
}
if (module->create_loc_conf) {
mconf = module->create_loc_conf(cf);
if (mconf == NULL) {
return NGX_CONF_ERROR;
}
ctx->loc_conf[cf->cycle->modules[i]->ctx_index] = mconf;
}
}
// 4 开始解析server段内的配置
rv = ngx_conf_parse(cf, NULL);
… …
}
看注释3中的代码段,是不是有种似曾相识的感觉?上节的http段配置解析中,我们展示的ngx_http_block中创建各模块存储配置项结构体的那段代码,http框架会遍历所有的http模块,并调用所有http模块的create_main_conf, create_srv_conf,create_loc_conf来得到它们存储配置项的结构体, 而这里在解析server块配置之前又再次创建了一个ngx_http_conf_ctx_t容器,并调用所有http模块的create_srv_conf和create_loc_conf方法。注意在注释2对应的代码中能看到nginx将这个server段的ngx_http_conf_ctx_t的main_conf指向了它所属的http段ctx的main_conf。说到这里,想必你也能猜到ngx_http_core_location会干哪些事情了,让我们看看它的核心代码:
static char *
ngx_http_core_location(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
{
ngx_http_conf_ctx_t *ctx, *pctx;
// 1 创建ngx_http_conf_ctx_t结构体
ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));
if (ctx == NULL) {
return NGX_CONF_ERROR;
}
// 2 将ctx的main_conf和srv_conf指向server段ctx的main_conf和srv_conf
pctx = cf->ctx;
ctx->main_conf = pctx->main_conf;
ctx->srv_conf = pctx->srv_conf;
ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
if (ctx->loc_conf == NULL) {
return NGX_CONF_ERROR;
}
// 3 调用所有http模块的create_loc_conf方法
for (i = 0; cf->cycle->modules[i]; i++) {
if (cf->cycle->modules[i]->type != NGX_HTTP_MODULE) {
continue;
}
module = cf->cycle->modules[i]->ctx;
if (module->create_loc_conf) {
ctx->loc_conf[cf->cycle->modules[i]->ctx_index] =
module->create_loc_conf(cf);
if (ctx->loc_conf[cf->cycle->modules[i]->ctx_index] == NULL) {
return NGX_CONF_ERROR;
}
}
}
// 4 解析loaction块内的配置项
rv = ngx_conf_parse(cf, NULL);
}
没错,在解析location段配置之前,又再次创建了一个ngx_http_conf_ctx_t容器,它的main_conf和srv_conf指向了上一级的结构体对应ctx的main_conf和srv_conf,而对于本段location内的配置,nginx会再次遍历所有的http模块并调用create_loc_conf获取各个存储配置的结构体。可以想见,对于一个最简单的nginx配置(只包含一个http,server,location),其内存布局如下:
你可能会惊叹按照这种逻辑,岂不是每配置一个server或loaction都至少要遍历所有的http模块,而且内存中保留了大量冗余的存储配置结构体,这岂不是既浪费空间又降低效率吗?其实不然,这是一种非常人性化的设计,充分考虑到了nginx.conf中高、中、低级别的配置兼容问题,能让每个级别的配置项拥有不同的作用域,同时也给出了不同级别配置项冲突时的解决方案。不论同一个指令出现在哪些地方,一定有一个对应的ngx_http_conf_ctx_t容器里保存了它,而真正在处理请求时,以哪个配置项为准,完全由各模块决定,这充分体现了http框架对各模块的包容性。你甚至可以自定义一个模块,将http、server和location段中的指令参数之和作为最终生效的值。既然完全由各http模块决定,那么问题来了:模块怎么告诉http框架我将以哪个配置为准呢?还记得上篇提到的http模块的公共接口ctx吗,里面有两个我们至今未用到的成员merge_srv_conf和merge_loc_conf,这里就该他们登场了。从字面上就能理解这是用于合并配置项的函数,各http模块可以实现自己合并配置逻辑,http框架会在解析完配置后依次调用各模块的这两个方法来解决配置冲突问题。由于篇幅有限,合并过程这里就不一一展开。