nginx源码分析-配置文件解析

nginx源码分析--配置文件解析

ngx-conf-parsing

对 Nginx 配置文件的一些认识:

  • 配置指令具有作用域,分为全局作用域和使用 {} 创建其它作用域。
  • 同一作用域的不同的配置指令没有先后顺序;同一作用域是否能使用相同的指令,和对相 同指令的处理由各模块自行决定
  • 整个 Nginx 的运行时各模块行为都和配置指令密切相关
  • 每个配置指令都只能在预先定义好的作用域中使用
  • 配置指令解析使用递归的方式

配置解析相关代码量巨大,本文势必很冗长。所以,都要做好心理准备。同时,由于 mail 模块平时使用实在不多,故,本文只对 http 应用场景下的配置解析过程进行分析。

同时,为了行文方便,先约定一些名称:

  • 配置文件 – 指 Nginx 的主配置文件 nginx.conf。在配置文件中使用 include 引入其它配置文件的方式这里不做分析。

  • 配置指令 – 配置文件中的基本单位,配置指令可接收0个或多个配置指令参数

  • 配置项 – 配置指令对应的内部存储。一个配置指令的值可能会影响到多个配置项的 值。

  • 配置项结构体 – 每个模块相关的配置项集中管理用的结构体。

  • 作用域模块配置数组 – 每个作用域对应的一个 void * 类型的数组。每个 void * 指针指向可用于此作用域的模块对应的配置项结构体

基本概念

  • Nginx 的模块使用 ngx_module_t 结构表示

  • Nginx 按分类和等级,将模块划分为 NGX_CORE_MODULENGX_EVENT_MODULENGX_HTTP_MODULENGX_MAIL_MODULE 等类型,由 ngx_module_t.type 存储。

  • Nginx 的配置指令使用 ngx_command_t 结构进行 “声明”。模块支持的配置指令由 ngx_module_t.commands 数组存储。

  • 模块上下文 (module context) 定义了针对模块配置的一系列操作 (申请配置存储、 继承上级模块的配置等等),不同的操作在配置分析的不同阶段被调用。模块上下文根据模 块类型分为ngx_core_module_tngx_event_module_tngx_http_module_tngx_mail_module_t 等等。模块上下文由 ngx_module_t.ctx 存储。

  • 配置指令的作用域。任何一个配置指令都有其能够使用的范围,即其作用域。配置指令如 果在其作用域外使用,Nginx 都会打印错误信息并退出。配置指令作用域目前有NGX_DIRECT_CONF、NGX_MAIN_CONFNGX_EVENT_CONFNGX_HTTP_MAIN_CONFNGX_HTTP_SRV_CONFNGX_HTTP_LOC_CONFNGX_MAIL_MAIN_CONFNGX_MAIL_SRV_CONF 等。

    • 一个配置指令可以在多个作用域中定义。
    • 在父作用域里定义的指令,也会被子作用域继承。但是,如果子作用域也使用了同一 个指令,子作用域的指令值会覆盖高级别作用域的指令。
  • 配置指令的参数。配置指令可以接受 0 个或多个参数。它能接受的参数个数也需要在定 义配置指令时明确声明,以便 Nginx 在解析配置过程中,对配置文件正确性进行检查。

    • 有一些配置指令,比如,http, event, server, location, mail, types 等,用来显式的定义作用域,它们被标识为 NGX_CONF_BLOCK,表示一个 {} 块的开始。
  • 配置指令的作用域和可接受参数等信息,由 ngx_command_t.type 字段存储。

  • 配置指令解析函数。基本每个配置指令 (简单的单值指令除外) 都定义了相应的回调函数。 Nginx 在解析配置过程中,如果碰到了该指令,会调用此回调函数对指令进行进一步的解 析。

下图是一个简单 nginx.conf 的作用域示意图:

nginx源码分析-配置文件解析

  • 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];

                } else if (cf->ctx) {
                    confp = *(void **) ((char *) cf->ctx + cmd->conf);

                    if (confp) {
                        conf = confp[ngx_modules[i]->ctx_index];
                    }
                }

                rc = cmd->set(cf, cmd, conf);
                ...
            }
        }
    }

对上述代码的几点补充:

  • 完全读取到一条配置指令后,Nginx 会使用该指令名在所有指令定义中进行查找。但是, 在进行查找之前,Nginx 会先验证模块的类型和当前解析函数上下文中的类型是否一致。 随后,在某个模块中找到匹配的指令定义后,还会验证指令可以出现的作用域是否包含当前 解析函数上下文中记录的作用域。最后,检查指令的参数个数是否和指令定义中标明的一 致。

  • 校验工作完成后,Nginx 将指令名和所有模块预定义支持的指令进行对比,找到完全匹配 的配置指令定义。根据配置指令的不同类型,配置项的存储位置也不同。

    • NGX_DIRECT_CONF 类型的配置指令,其配置项存储空间是全局作用域对应的存储空 间。这个类型的指令主要出现在 ngx_core_module 模块里。

      conf = ((void **) cf->ctx)[ngx_modules[i]->index];

    • NGX_MAIN_CONF 表示配置指令的作用域为全局作用域。纵观 Nginx 整个代码, 除了 ngx_core_module 的配置指令 (同时标识为NGX_DIRECT_CONF) 位于这个 作用域中外,另外几个定义新的子级作用域的指令 – eventshttpmailimap。 非 NGX_DIRECT_CONFNGX_MAIN_CONF 指令在全局作用域中并未被分 配空间,所以在指令处理函数中分配的空间需要挂接到全局作用域中。

      conf = &(((void **) cf->ctx)[ngx_modules[i]->index];

    • 其它类型配置指令项的存储位置和指令出现的作用域 (并且非全局作用域) 有关:

      confp = *(void **) ((char *) cf->ctx + cmd->conf);

      if (confp) { conf = confp[ngx_modules[i]->ctx_index]; }

  • 配置项将要存储的位置确定后,调用指令回调函数,完成配置项初始化和其它工作。

    rc = cmd->set(cf, cmd, conf);

配置文件解析框架大抵如此,再进行更详细的代码分析之前,先来看一张配置文件解析后的 配置结构图:

nginx源码分析-配置文件解析

指令读取

了解了大致框架,现在再从细节着手。接下来分析一下配置指令读取和分解函数,也就是 ngx_conf_read_token 函数的实现。

ngx_conf_read_token 将从磁盘配置文件读取部分数据到内存 buffer,然后再按照 Nginx 支持的格式从buffer 中读出一条合法的指令。如果,buffer 中数据不足以构 成一条完整的指令,则需要再从配置文件中读取更多数据。

配置文件读取

buffer 中数据不足时,也就是 b->pos >= b->last 时,从配置文件中读取更多数据。

    --------------core/ngx_conf_file.c:463---------------
    if (b->pos >= b->last) {
        ...
        len = b->pos - start;

        if (len) {
            ngx_memcpy(b->start, start, len);
        }

        size = (ssize_t) (file_size - cf->conf_file->file.offset);

        if (size > b->end - (b->start + len)) {
            size = b->end - (b->start + len);
        }

        n = ngx_read_file(&cf->conf_file->file, b->start + len, size,
                          cf->conf_file->file.offset);
        b->pos = b->start + len;
        b->last = b->pos + n;
        start = b->start;
    }

对上述代码的补充说明:

  • ngx_buf_t 结构简单描这一下其字段 ———–core/ngx_buf.h:19—————– struct ngx_buf_s { u_charpos; / b 中还未使用的数据起始位置 / u_char *last; / b 中存储的有效数据结束位置 / … u_char *start; / 连续内存块 b 的起始位置/ u_char *end; / 连续内存块 b 的结束位置 */ … }; typedef struct ngx_buf_s ngx_buf_t;

  • buffer 中有效数据已经用尽时 (b->pos >= b->last),一条指令的某个组成部分 (从start 开始到 b->pos 结束 len 字节) 可能还未被完整读取,这部分数据需要 保留。

    ngx_memcpy(b->start, start, len);
  • buffer 现在就腾出了 b->end - (b->start + len) 的可用空间。

  • start 始终指向一条指令某个组成部分的起始位置。

语法状态机

ngx_conf_read_token 剩余部分实现了逐字符分析指令行语法的状态机。这部分代码位于 core/ngx_buf.h:593-734。语义的正确性验证在配置解析的基它部分完成。

  • 指令行允行出现的格式 (使用正则定义,对语法定义不熟,凑乎看啊 /* TODO */):

    line := token token? { line+ }
            | token space token? space?;
    token := ["'][ \t\r\na-z0-9\\$]+["']
            | [a-z0-9$]+
            | ( token )
    space := [ \t\r\n]+
  • last_space 用于表示上一个字符是否为 space。初始值为 1

  • need_space 用于表示状态机要求下一个字符必须为 space (或者 ‘;’, ‘{‘, ‘)’)。 初始值0

  • start 指向正在分析中的 token 起始位置。初始值为 b->pos

  • d_quoted 用于表示当前字符处于 "" 中。初始值为 0

  • s_quoted 用于表示当前字符处于 '' 中。初始值为 0

  • sharp_comment 用于表示当前字符是注释的一部分。初始值为 0

  • variable 用于表示当前字符是变量名的一部分。初始值为 0

  • quote 用于表示当前字符前一个字符是否为 \。初始值为 0
  • cf->args 存储指令名和所有参数。它是 ngx_str_t 类型的数组。

这部分的代码实在太长 (200多行) 这里只将框架缩略如下

    for ( ;; ) {
        ch = *b->pos++; /* 当前字符存储于 ch 中 */

        if (ch == LF) {
            if (sharp_comment) {  /* # 是行注释 */
                sharp_comment = 0;
            }
        }

        if (sharp_comment) {    /* 忽略 # 后的所有字符,直到进行新行为止 */
            continue;
        }

        if (quoted) {           /* 不再单独处理 \ 后的字符,后面读取 token
            quoted = 0;            时,会做专门处理 */
            continue;
        }

        if (need_space) {       /* 此字符必须为 space 分隔符 */
            /*
              忽略掉 space 字符;
              如果字符是 ';','{',此次指令行读取完成;
              如果字符是 ')',开始识别新的 token;
              如果读到其它字符则解析失败
             */
        }

        if (last_space) { /* 找到新 token 的开始位置 - 第一个非 space 字符 */
            /*
              忽略掉 space 字符;
              如果字符是 ';''{''}',此次指令行读取完成;
              如果字符是 '#',该字符后面都是注释;
              如果字符是 '\',下一个字符将被忽略;
              如果字符是 '"', ''', 下一个字符是被引用的内容;
             */
        } else { /* 开始读取新 token */
            /*
              如果字符是 '$',后面的非 space 字符组成变量 token;
              碰到了结束引号 '"', ''' 时,token 读取完成;
              碰到了 space 字符,token 读取完成;
             */
            if (found) {
                /*
                  将找到的 token 追加到 cf->args 数组中,并且每个 token 字符
                  串以 '\0' 结束
                 */
            }
        }
    }

作用域解析

events作用域

events 作用域由 ngx_events_moduleevents 指令处理创建。其定义为:

    -------------event/ngx_event.c:83------------
    { ngx_string("events),
      NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
      ngx_events_block,
      0,
      0,
      NULL }

开始配置文件解析之前,ngx_events_module 在全局作用域中配置结构体指针值为 NULL, 所以,ngx_events_block 函数的第一件事情就是,创建配置结构体,并挂载到全局作用 域上。

    ------------event/ngx_event.c:902------------
    ctx = ngx_pcalloc(cf->pool, sizeof(void *));
    ...
    *ctx = ngx_pcalloc(cf->pool, ngx_event_max_module * sizeof(void *));
    ...
    *(void **) conf = ctx;

对上述代码的补充说明:

  • 为什么要多次一举的多一次中转,使用 ctx 指向 void * 类型指针,而此指针再指 向作用域模块配置数组呢? 接着往下看!

然后,调用类型为 NGX_EVENT_MODULE 的模块上下文中的 create_conf 函数,为各个 模块在此作用域分配配置解析体。拿ngx_epoll_module 举例,其 create_conf 函数 从内存池中申请用于存储 ngx_epoll_conf_t 的内存块,并挂接到 events 作用域中的 相应位置。

随后,如上文已经描述过的,切换配置解析上下文,然后调用 ngx_conf_parse 函数解析 此作用域对应的配置文件 block 中出现的指令行:

    -----------event/ngx_event.c:929-------------
    pcf = *cf;
    cf->ctx = ctx;
    cf->module_type = NGX_EVENT_MODULE;
    cf->cmd_type = NGX_EVENT_CONF;

    rv = ngx_conf_parse(cf, NULL);

    *cf = pcf;

epoll 模块举例,来看下在上面的上下文环境中,碰到它支持的指令 epoll_events 时,整个过程是如何进行的。

    ----------event/modules/ngx_epoll_module.c:129------------
    { ngx_string("epoll_events"),
      NGX_EVENT_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_num_slot,
      0,                                         /* conf */
      offsetof(ngx_epoll_conf_t, events),        /* offset */
      NULL },

ngx_conf_read_token 成功返回后,ngx_conf_handler 根据该指令类型和上下文定 位到指令对应的配置项所在的位置:

    ----------core/ngx_conf_file.c:386------------------------
    confp = *(void **) ((char *) cf->ctx + cmd->conf)

    if (confp) {
        conf = confp[ngx_modules[i]->ctx_index];
    }

对上述代码的补充说明:

  • cf->ctx 指向在 ngx_events_block 中分配的 void * 指针,void * 指针进而 又指向events 作用域模块配置数组。所以,confp 最后指向了 events 作用域模块 配置数组,conf 就指向了ngx_epoll_module 对应的配置项结构体,即 ngx_epoll_conf_t

epoll_events 指令的处理函数是通用的处理函数 ngx_conf_set_num_slot

    ------------core/ngx_conf_file.c:1182-------------------
    char *
    ngx_conf_set_num_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
    {
        char *p = conf;
        ...
        np = (ngx_int_t *) (p + cmd->offset);
        ...

        *np = ngx_atoi(value[1].data, value[1].len);
    }

对上述代码的补充说明:

  • cmd->offset 值是 offsetof(ngx_epoll_conf_t, events),即配置指令epoll_events 对就的配置项 ngx_uint_t events 在配置结构体 ngx_epoll_conf_t 中的偏移。Nginx 预定义处理函数基本都使用这样的方式,以实现通用性。

最终,epoll_events 指令的参数,经过上面的一番周折,被存储到了 events 作用域 里 ngx_epoll_module 的配置结构体 ngx_epoll_conf_tevents 成员中。

events 作用域下的其它指令的解析过程和 epoll_events 都大同小异,就略过不表了。 下面搞一下http 作用域。

http作用域

http 作用域由 ngx_http_modulehttp 指令处理函数创建。其定义为:

    -------------event/ngx_event.c:83------------
    { ngx_string("http),
      NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
      ngx_http_block,
      0,
      0,
      NULL }

events 作用域类似,开始配置文件解析之前,ngx_http_module 在全局作用域中配 置结构体指针值为 NULL,所以,ngx_http_block 函数的第一件事情也是,创建配置结构 体,并挂载到全局作用域上。

    ------------http/ngx_http.c:128--------------
    ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));
    ...
    *(ngx_http_conf_ctx_t **) conf = ctx;

但是,http 作用域和 events 作用域的地方是,ctx 并不是指向一个void *,而 是一个 ngx_http_conf_ctx_t 结构体。

    ------------http/ngx_http_config.h:16--------
    typedef struct {
        void        **main_conf;
        void        **srv_conf;
        void        **loc_conf;
    } ngx_http_conf_ctx_t;

造成这点区别的原因是:

  • http 作用域是可以嵌套的。一个 http 作用域中以嵌套多个server 作用域,一个server 作用域又可以嵌套多个 location 作用域。

  • 同时,NGX_HTTP_MODULE 类型的模块的指令可以定义在 http 作用域中 (NGX_HTTP_MAIN_CONF),也可以定义在server 作用域 (NGX_HTTP_SRV_CONF), 也可以定义在 location 作用域 (NGX_HTTP_LOC_CONF)。事实上,大部分的 HTTP 模块 支持的指定都同时定义在这三个作用域中。

  • NGX_HTTP_MODULE 类型的模块将自己支持的指令对应的配置项分为三类:第一类的指令 只能在 NGX_HTTP_MAIN_CONF 作用域中使用;第二类的指令可以在NGX_HTTP_MAIN_CONFNGX_HTTP_SRV_CONF 作用域中使用;第三类可以在 NGX_HTTP_MAIN_CONF NGX_HTTP_SRV_CONFNGX_HTTP_LOC_CONF 作用域中使用。这三类指令对应的配置项 分为三个结构体存储ngx_http_xxx_main_conf_tngx_http_xxx_srv_conf_tngx_http_xxx_loc_conf_t

  • main_conf, srv_confloc_conf 就是给三类配置结构体提供的作用域模块配置 数组。在http 作用域,三个数组都是需要的;在 server 作用域和 父作用域 http 使用同一个main_conf;在 location 作用域,和其父 server 作用域使用同一个main_conf srv_conf。 `

ngx_http_block 中分配完 http 作用域的三类作用域模块配置数组后,会调用 各个 NGX_HTTP_MODULE 类型的模块上下文定义的 create_main_conf, create_srv_confcreate_loc_conf 回调函数分别在三类作用域模块配置数组创建 模块的三类配置项结构体:

    -------------http/ngx_http.c:150---------------
    ctx->main_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
    ...
    ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
    ...
    ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
    ...
    for (m = 0; ngx_modules[m]; m++) {
        if (ngx_modules[m]->type != NGX_HTTP_MODULE) {
            continue;
        }

        module = ngx_modules[m]->ctx;
        mi = ngx_modules[m]->ctx_index;

        if (module->create_main_conf) {
            ctx->main_conf[mi] = module->create_main_conf(cf);
            ...
        }

        if (module->create_srv_conf) {
            ctx->srv_conf[mi] = module->create_srv_conf(cf);
            ...
        }

        if (module->create_loc_conf) {
            ctx->loc_conf[mi] = module->create_loc_conf(cf);
            ...
        }
     }

接下来,和 events 作用域一样,切换配置解析上下文,调用 ngx_conf_parse 函数:

    -------------http/ngx_http.c:214------------
    pcf = *cf;
    cf->ctx = ctx;
    ...
    cf->module_type = NGX_HTTP_MODULE;
    cf->cmd_type = NGX_HTTP_MAIN_CONF;
    rv = ngx_conf_parse(cf, NULL);
    ...
    *cf = pcf;

http 作用域中的普通指令解析和上面介绍的 events 作用域中 ngx_epoll_moduleepoll_events 解析过程大同小异。

接下来主要分析一下 http 作用域中 server 作用域的创建及 server 中子作用域location 的创建以及多个 location 作用域是如何挂接到其父作用域 server 上的 和多个server 作用域是如何挂接到它的父作用域 http 上的。

server作用域

配置解析函数现在处于 http 作用域的上下文中: * cf->ctx 指向 ngx_http_conf_ctx_t *cf->module_type 值为 NGX_HTTP_MODULE * cf->cmd_type 值为NGX_HTTP_MAIN_CONF

解析函数碰到 server {} 后,会调用 server 指令的处理函数 ngx_http_core_server

    -------------http/ngx_http_core_module.c:2300------------
    static char *
    ngx_http_core_server(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
    {
        ctx = ngx_pcalloc(cf->pool, sizeof(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_moudle);
        ...
        ctx->loc_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_http_max_module);
        ...
        cscf = ctx->srv_conf[ngx_http_core_module.ctx_index];
        cscf->ctx = ctx;

        cmcf = ctx->main_conf[ngx_http_core_module.ctx_index];

        cscfp = ngx_array_push(&cmcf->servers);
        ...
        *cscfp = cscf;

        pcf = *cf;
        cf->ctx = ctx;
        cf->cmd_type = NGX_HTTP_SRV_CONF;

        rv = ngx_conf_parse(cf, NULL);

        *cf = pcf;
    }

对上述代码的补充说明:

  • server 作用域和其父作用域共用一个 main_conf

  • server 作用域也要使用 ngx_http_conf_ctx_t 代表此作用域的三类作用域模块配置 数组

  • server 作用域的 ngx_http_conf_ctx_t 存储到 ngx_http_core_module 配置项 结构体ngx_http_core_srv_conf_tctx

  • ngx_http_core_module 负责在 http 及其各个子作用域中起关联作用。刚刚解析的 server 作用域,会被存储 (通过 ngx_http_core_srv_conf_t 间接存储) 到 ngx_http_core_main_conf_tservers 数组中。

配置函数再次调用之前,切换上下文:

    pcf = *cf;
    cf->ctx = ctx;
    cf->cmd_type = NGX_HTTP_SRV_CONF;

    rv = ngx_conf_parse(cf, NULL);

    *cf= pcf;

下面,拿 listen 指令作用例子,分析一下当前作用域的指令解析流程。

listen 指令的定义为:

    --------------http/ngx_http_core_module.c:278---------
    { ngx_string("listen"),
      NGX_HTTP_SRV_CONF|NGX_CONF_1MORE,
      ngx_http_core_listen,
      NGX_HTTP_SRV_CONF_OFFSET,
      0,
      NULL },

ngx_conf_read_token 从配置文件中读取完此指令后,由 ngx_conf_handler 校验指令 的合法性,然后找到指令项所在结构体的存储位置,即

    confp = *(void **) ((char *) cf->ctx + cmd->conf);

    if (confp) {
        conf = confp[ngx_modules[i]->ctx_index];
    }

对上述代码的补充说明:

  • cf->ctx 现在指向进入 server 作用域后,申请的 ngx_http_conf_ctx_tcmd->conf 值是 NGX_HTTP_SRV_CONF_OFFSET,即 offsetof(ngx_http_conf_ctx_t, srv_conf)。 那么confp 最终指向了新申请的 ngx_http_conf_ctx_tsrv_conf 指向的作用域 模块配置数组

  • conf 指向了 server 作用域的 ngx_http_core_modulengx_http_core_srv_conf_t

随后,调用 listen 指令处理函数 ngx_http_core_listen

    -------------http/ngx_http_core_module.c:3256--------
    static char *
    ngx_http_core_listen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
    {
        ngx_http_core_srv_conf_t *cscf = conf;
        ...
        cscf->listen = 1;
        ...
        ngx_http_add_listen(cf, cscf, &lsopt);
        ...
    }

    ------------http/ngx_http.c:1105-----------------
    ngx_int_t
    ngx_http_add_listen(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
        ngx_http_listen_opt_t *lsopt)
    {
        ...
        ngx_http_conf_port_t        *port;
        ngx_http_core_main_conf_t   *cmcf;
        ...
        cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
        ...
        port = ngx_array_push(cmcf->ports);
        ...
        return ngx_http_add_address(cf, cscf, port, lsopt);
    }

    -----------http/ngx_http.c:1281-----------------
    static ngx_int_t
    ngx_http_add_address(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
        ngx_http_conf_port_t *port, ngx_http_listen_opt_t *lsopt)
    {
        ...
        addr = ngx_array_push(&port->addrs);
        ...
        return ngx_http_add_server(cf, cscf, addr);
    }

    -----------http/ngx_http.c:1319--------------------
    static ngx_int_t
    ngx_http_add_server(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf,
        ngx_http_conf_addr_t *addr)
    {
        ...
        server = ngx_array_push(&addr->servers);
        ...
        *server = cscf;
        ...
    }

listen 指令的配置就这么就被解析并存储了。最后附一张 Nginx 监听地址和端口的数据 结构组织图。

nginx源码分析-配置文件解析

location作用域

配置解析函数现在处于 server 作用域的上下文中: * cf->ctx 指向 server 使用域创建的ngx_http_conf_ctx_t * cf->module_type 值为 NGX_HTTP_MODULE *cf->cmd_type 值为 NGX_HTTP_SRV_CONF

解析函数碰到 location {} 后,会调用 location 指令的处理函数 ngx_http_core_location

    -------------http/ngx_http_core_module.c:271-----------
    { ngx_string("location"),
      NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_BLOCK|NGX_CONF_TAKE12,
      ngx_http_core_location,
      NGX_HTTP_SRV_CONF_OFFSET,
      0,
      NULL },

再回到 ngx_conf_handler 里,从当前上下文中计算 location 指令处理函数的参数:

    confp = *(void **) ((char *) cf->ctx + cmd->conf);

    if (confp) {
        conf = confp[ngx_modules[i]->ctx_index];
    }

对上述代码的补充说明:

  • confp 指向了父作用域 server 中分配的 ngx_http_conf_ctx_tsrv_conf 变量。

  • conf 指向了 ngx_http_core_modulengx_http_core_srv_conf_t 配置结构体。

  • conf 参数在 ngx_http_core_location 并未被使用。

接下来,ngx_http_core_location 函数被调用了

    -----------http/ngx_http_core_module.c:2391-----------
    static char *
    ngx_http_core_location(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
    {
        ...
        ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));
        ...
        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);
        ...
        clcf = ctx->loc_conf[ngx_http_core_module.ctx_index];
        clcf->loc_conf = ctx->loc_conf;
        ...
        pclcf = pctx->loc_conf[ngx_http_core_module.ctx_index];
        ...

        ngx_http_add_location(cf, &pclcf->locations, clcf);

        save = *cf;
        cf->ctx = ctx;
        cf->cmd_type = NGX_HTT_LOC_CONF;

        rc = ngx_conf_parse(cf, NULL);

        *cf = save
        ...
    }

上面这段代码建立的关系可以参考本文提供的配置结构全图。

其它普通指令的解析,和上面的流程大同小异,就不再赘述了。

到这里为止,整个 http 作用域的配置文件解析过程就算是完成了。在配置文件解析完毕 后,ngx_http_block 还会做一系列其它工作,比如对所有模块的三类配置结构体进行初 始化、最内层作用域继承外层作用域的同一个指令值、重新整理location、初始化 phase,初始化 Nginx 允许的 HTTP 请求包头等等工作。

作用域继承

前面已经介绍过了,http 作用域是可以嵌套的。同时,NGX_HTTP_MODULE 类型模块的 大部分指令也是可以同时用于http 和其子作用域 (server, location)中的。所以, 内层子作用域,需要从父作用域继承配置项。

这个行为最后达到的结果就是,出现在 NGX_HTTP_MAIN_CONF 中的指令配置项,在 NGX_HTTP_SRV_CONFNGX_HTTP_LOC_CONF 中依然可以发挥作用 (子作用域未显式配 置该指令时)。同时,子作用域里的配置项,可以覆盖父作用域里的配置项。

这个目标的达成,是由 ngx_http_block 函数完成的。在继续进行之前,先拉回思绪。注 意此时的配置解析上下文:

* `cf->ctx` 指向 `NGX_HTTP_MAIN_CONF` 作用域的 `ngx_http_conf_ctx_t`
* `cf->module_type` 值为 `NGX_HTTP_MODULE`
* `cf->cmd_type` 值为 `NGX_HTTP_MAIN_CONF`

于是,在解析完整个 http 作用域的指令后,

    ------------http/ngx_http.c:246-------------
    cmcf = ctx->main_conf[ngx_http_core_module.ctx_index];
    cscfp = cmcf->servers.elts;

    for (m = 0; ngx_modules[m]; m++) {
        if (ngx_modules[m]->type != NGX_HTTP_MODULE) {
            continue;
        }

        module = ngx_modules[m]->ctx;
        mi = ngx_modules[m]->ctx_index;

        if (module->init_main_conf) {
            rv = module->init_main_conf(cf, ctx->main_conf[mi]);
            ...
        }

        for (s = 0; s < cmcf->servers.nelts; s++) {
            if (module->merge_srv_conf) {
                rv = module->merge_srv_conf(cf, ctx->srv_conf[mi], 
                                            cscfp[s->ctx->srv_conf[mi]);
                ...
            }

            if (module->merge_loc_conf) {
                rv = module->merge_loc_conf(cf, ctx->loc_conf[mi],
                                            cscfp[s]->ctx->loc_conf[mi]);
                ...
                clcf = cscfp[s]->ctx->loc_conf[ngx_http_core_module.ctx_index];

                rv = ngx_http_merge_locations(cf, clcf->locations, 
                                              cscfp[s]->ctx->loc_conf,
                                              module, mi);
            }
        }
    }

对上述代码的补充说明:

  • 首先,参照上面的结构图

  • 代码只对 main_conf 作用域模块配置数组中每个模块的配置结构体进行了初始化。这 是因为,这些结构体在运行时,都会被用到。而srv_confloc_conf 里面的配置结 构体在运行时并不会被直接用到,如果其中有些模块的配置项在解析过程中被赋值的话,下 面的合并操作,会把这些已经赋值的配置项都拷贝至子作用域的对应位置。

  • merge 操作的流程是:如果子作用域某配置项在解析过程中未被赋值,则将父作用域的 相同的配置项值拷贝至此配置项里;如果子作用域配置项在解析过程中被赋值了,则保留原 样;如果子作用域配置项和父作用域配置项都没有被初始化,则填入代码中预设的默认值。

下面选取 ngx_http_core_module 的相关函数代码对上述结论进行验证:

    ----------------http/ngx_http_core_module.c:2751-------------
    static char *
    ngx_http_core_init_main_conf(ngx_conf_t *cf, void *conf)
    {
        ngx_http_core_main_conf_t *cmcf = conf;

        if (cmcf->server_names_hash_max_size == NGX_CONF_UNSET_UINT) {
            cmcf->server_names_hash_max_size = 512;
        }
        ...
    }

    ---------------http/ngx_http_core_module.c:2821-------------
    static char *
    ngx_http_core_merge_srv_conf(ngx_conf_t 8cf, void *parent, void *child)
    {
        ngx_http_core_srv_conf_t *prev = parent;
        ngx_http_core_srv_conf_t *conf = child;
        ...
        ngx_conf_merge_size_value(conf->connection_pool_size,
                                  prev->connection_pool_siez, 256);
        ...
    }

    #define ngx_conf_merge_size_value(conf, prev, default)          \
        if (conf == NGX_CONF_UNSET_SIZE) {                          \
            conf = (prev == NGX_CONF_UNSET_SIZE) ? default : prev;  \
        }

至此为止,作用域的继承是如何实现的也介绍完了。

最后的整理

上面已经提到了,在 http 作用域解析完成后,ngx_http_block 还会继续完成一系统 初始化工作,比如将配置中出现的location 组织成能快速访问的结构、初始化请求包头 结构体、初始化 phase 访问结构等等。

这些基本上都可以独立开辟一篇文章进行分析,这里就先略过了。

请求来了

当一个 HTTP 请求到达 Nginx 时,什么阶段该使用哪个作用域的配置,配置又是怎么得 到的呢?

request 初始化函数 ngx_http_init_request 会被一个新创建的 request 配置 上下文做一次初始化 (此时,还无法判断此请求到底属于哪个 server 作用域,先选 用默认 server 作用域):

    ---------------http/ngx_http_request.c:384------------
    cscf = addr_conf->default_server;

    r->main_conf = cscf->ctx->main_conf;
    r->src_conf = cscf->ctx->srv_conf;
    r->loc_conf = cscf->ctx->loc_conf;
    ...

在读完 HTTP 请求包头,能够判断请求应该由哪个 server 作用域处理时,这时需要调 整一下 request 的配置上下文:

    -------------http/ngx_http_request.c:1688-------------
    static ngx_int_t
    ngx_http_find_virtual_server(ngx_http_request_t *r, u_char *host, size_t len)
    {
        ...
        r->srv_conf = cscf->ctx->srv_conf;
        r->loc_conf = cscf->ctx->loc_conf;
        ...
    }

此时,r->loc_conf 指向 server 作用域的 loc_conf,在确定了request 的 所属的 location 后,会接着调整 r->loc_conf 的指向。

同时,内部 request,rewrite 的 request,被重定向的 request 都要通过改变r->loc_conf 的指向来实现同一个 request 在不同 location 作用域跳转的功能。

另外,配置结构的获取,主要通过下面的宏实现:

    #define ngx_http_get_module_main_conf(r, module)                             \
        (r)->main_conf[module.ctx_index]
    #define ngx_http_get_module_srv_conf(r, module)  (r)->srv_conf[module.ctx_index]
    #define ngx_http_get_module_loc_conf(r, module)  (r)->loc_conf[module.ctx_index]


    #define ngx_http_conf_get_module_main_conf(cf, module)                        \
        ((ngx_http_conf_ctx_t *) cf->ctx)->main_conf[module.ctx_index]
    #define ngx_http_conf_get_module_srv_conf(cf, module)                         \
        ((ngx_http_conf_ctx_t *) cf->ctx)->srv_conf[module.ctx_index]
    #define ngx_http_conf_get_module_loc_conf(cf, module)                         \
        ((ngx_http_conf_ctx_t *) cf->ctx)->loc_conf[module.ctx_index]

    #define ngx_get_conf(conf_ctx, module)  conf_ctx[module.index]