nginx配置解析流程

来源:互联网 发布:马自达 mx5 rf 知乎 编辑:程序博客网 时间:2024/05/20 11:24

        上一篇文章分析了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初始值为1if (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块的合并流程。



1 0