写点什么

深入理解 Nginx 的四级指针

用户头像
赖猫
关注
发布于: 2021 年 01 月 07 日

Nginx 源码有一个四级指针——ngx_cycle_t.conf_ctx。一级指针都不容易理解,更何况四级。今天就来说说这个四级指针。


Nginx 从 1.9 开始支持三层代理,这个模块叫 stream。它的虽然在功能上比 http 模块简单很多,但是用来分析这个四级指针还是绰绰有余的。


stream 模块默认不开启,我们需要编译时显式开启:


./auto/configure --with-stream --without-http --prefix=/tmp/ngxmake install
复制代码


然后修改/tmp/ngx/conf/nginx.conf内容如下:


daemon off;events {}stream {    server {        listen 1024;        return "hello\n";    }}
复制代码


运行 Nginx 命令后,使用 telnet 连接 127.0.0.1:1024。你会看到 Nginx 输出"hello\n"并关闭连接。


➜ ngx telnet 127.0.0.1 1024Trying 127.0.0.1...Connected to localhost.Escape character is '^]'.helloConnection closed by foreign host.
复制代码


到现在,我们对 stream 模块就有了一个感性的认识。接下来我们开始揭开这个四级指针的神秘面纱。


Nginx 的main()函数有 200 多行,但跟模块加载相关的只有下面几行:


int main(int argc, char *const *argv) {    // ...    ngx_cycle_t      *cycle, init_cycle;    // ...    if (ngx_preinit_modules() != NGX_OK) { return 1; }
cycle = ngx_init_cycle(&init_cycle); // ...}
复制代码


先看这个ngx_preinit_modules()函数:


ngx_int_t ngx_preinit_modules(void) {    for (ngx_uint_t i = 0; ngx_modules[i]; i++) {        ngx_modules[i]->index = i;                  // 设置模块编号        ngx_modules[i]->name = ngx_module_names[i]; // 设置模块名称    }
ngx_modules_n = i; // 模块总数 // 模块最大数量,含动态加载模块 ngx_max_module = ngx_modules_n + NGX_MAX_DYNAMIC_MODULES;
return NGX_OK;}
复制代码


代码一看就明白,但这里的ngx_modules是从哪来的呢?


还记得最开始的auto/configure脚本吗?这个configure脚本有很多--(with|without)-xxx参数。我们可以通过它们控制要编译哪些模块以及模块的加载顺序。


confgigure脚本执行完成后会生成objs/ngx_modules.c文件。ngx_modules就是在这个文件定义的。


ngx_modules是一个ngx_module_t数组。在本例中内容如下:


ngx_module_t *ngx_modules[] = {      &ngx_core_module,      &ngx_errlog_module,      &ngx_conf_module,      &ngx_events_module,      &ngx_event_core_module,      &ngx_kqueue_module,      &ngx_stream_module,      &ngx_stream_core_module,      &ngx_stream_write_filter_module,      &ngx_stream_return_module,      // ...      NULL};
复制代码


我们以ngx_core_module为例看看如何声明一个模块:


ngx_module_t  ngx_core_module = {      NGX_MODULE_V1,      &ngx_core_module_ctx,   /* module context */      ngx_core_commands,      /* module directives */      NGX_CORE_MODULE,        /* module type */      NULL,                   /* init master */      NULL,                   /* init module */      NULL,                   /* init process */      NULL,                   /* init thread */      NULL,                   /* exit thread */      NULL,                   /* exit process */      NULL,                   /* exit master */      NGX_MODULE_V1_PADDING};
复制代码


NGX_MODULE_V1NGX_MODULE_V1_PADDING都是 Nginx 为代码清晰起见定义的快速填充宏,多是空值填充,大可不必纠结。这里最核心的是ctxcommandstype三个字段。


这个type就是模块的类型。每个种类型的模块可以有不同的ctx和对应不同的commands。Nginx 的核心模块的类型为NGX_CORE_MODULE,对应的ctx都是ngx_core_module_t类型的:


typedef struct {      ngx_str_t    name;      void      *(*create_conf)(ngx_cycle_t *cycle);      char      *(*init_conf)(ngx_cycle_t *cycle, void *conf);} ngx_core_module_t;
复制代码


ctx有两个函数指针,要想理解它们的用途,我们就得看一下这个ngx_init_cycle()函数。ngx_init_cycle()函数有近一千行,但跟模块相关的大约有以下 30 多行:


ngx_cycle_t * ngx_init_cycle(ngx_cycle_t *old_cycle) {    // ...    cycle->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *));    // ...    if (ngx_cycle_modules(cycle) != NGX_OK) { /* ... */ }    // ...    for (i = 0; cycle->modules[i]; i++) {       if (cycle->modules[i]->type != NGX_CORE_MODULE) { continue; }       module = cycle->modules[i]->ctx;       if (module->create_conf) {           rv = module->create_conf(cycle);           cycle->conf_ctx[cycle->modules[i]->index] = rv;       }    }    // ...    conf.ctx = cycle->conf_ctx;    conf.cycle = cycle;    conf.module_type = NGX_CORE_MODULE;    conf.cmd_type = NGX_MAIN_CONF;    // ...    if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) { /* ... */ }     // ...    for (i = 0; cycle->modules[i]; i++) {        // ...        if (module->init_conf) {            if (module->init_conf(cycle, cycle->conf_ctx[cycle->modules[i]->index]) == NGX_CONF_ERROR) { /*..*/ }        }    }    // ...}
复制代码


这个四级指针指向了一个void *数组,其长度为ngx_max_module,也就说为每一个模块预留了一个位置。Nginx 的核心模块可以通过设置ctx->create_conf函数指针来动态分配内存存储配置。等配置解析完成后,Nginx 还会调用ctx->init_conf,为核心模块提供一个初始化的机会。


那问题来了,哪些核心模块设置了ctx->create_conf指针呢?一共有六个:


  • ngx_core_module

  • ngx_errlog_module

  • ngx_google_perftools_module

  • ngx_openssl_module

  • ngx_regex_module

  • ngx_thread_pool_module


咋看没有什么规律,实则不然。在揭晓迷底之前我们有必要看一下ctx->commands结构:


static ngx_command_t  ngx_core_commands[] = {    { ngx_string("daemon"),                         // name      NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_FLAG,  // type      ngx_conf_set_flag_slot,                       // set function      0,                                            // conf      offsetof(ngx_core_conf_t, daemon),            // offset      NULL                                          // post    },    // ...}
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};
复制代码


大家注意daemon的类型包含NGX_MAIN_CONFNGX_DIRECT_CONF。所谓NGX_MAIN_CONF就是 Nginx 的顶级配置。


在本文的例子中, daemon, events, stream 都是顶级配置。


虽然同为NGX_MAIN_CONF,它们又有区别。比如,daemon包含NGX_DIRECT_CONF,而eventsstream则包含NGX_CONF_BLOCK。对应的配置文件上则表现为daemon可对应on/off配置值,而 events 和 stream 对应一个{}


只有定义了 NGX_DIRECT_CONF 指令的核心模块才需要设置 ctx->create_conf 函数!


我们知道,设置 ctx->create_conf 是为了分配内存保存配置。而 eventsstream 这些模块没有分配内存,那它们的配置存到哪呢?其实还是保存在 cycle->conf_ctx。只不过它们是在配置文件解析的过程中动态分配的。对于 ngx_core_module 这样的模块,它们指定了 ctx->create_conf,所以对应的内存在解析配文件之前就分配好了。那为这样做有什么好处呢?好处只有一个,省内存!像 streamevents 这些模块,只要不配置,Nginx 就不会为它们分配额外的内存。其实也省不了多少内存,但设计很清真


对应设置了 ctx->create_conf 的模块,cycle->conf_ctx[x] 指向的其实是一块连续的内存区域。比如,ngx_core_module 指向的内存结构为 ngx_core_conf_t


等等,cycle->conf_ctx 是四级指针,cycle->conf_ctx[x] 就是三级指针,不是应该指向一个二级指针吗?Nginx 没有拘泥于此,而是采用了强转类型赋值。这里的四级指针只是逻辑上的四级指针而非真正的四级指针。


为了深入理解这个四级指针,我们有必要看一下 ngx_conf_parse() 函数,其核心逻辑如下:


char * ngx_conf_parse(ngx_conf_t *cf, ngx_str_t *filename) {      // ...      for ( ;; ) {          // 解析单个指令及其参数          rc = ngx_conf_read_token(cf);          // ...      // 保存配置内容          rc = ngx_conf_handler(cf, rc);      // ...      }      // ...}
复制代码


ngx_conf_read_token() 的任务是扫描配置文件内容,识别配指令和参数。比如:


  • daemon on; 解析后对应的 cf->args 保存 ["daemon", "on"]

  • events { 解析后对应的 cf->args 保存 ["events"]


ngx_conf_read_token() 基于状态机解析配置文件,代码非常经典。而后面执行的 ngx_conf_handler() 函数是我们理解四级指针的另一个关键:


static ngx_int_t ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last) {    // ...    for (i = 0; cf->cycle->modules[i]; i++) {        cmd = cf->cycle->modules[i]->commands;        // ...        for ( /* void */ ; cmd->name.len; cmd++) {            // ...            if (cmd->type & NGX_DIRECT_CONF) {                conf = ((void **) cf->ctx)[cf->cycle->modules[i]->index];            } else if (cmd->type & NGX_MAIN_CONF) {                conf = &(((void **) cf->ctx)[cf->cycle->modules[i]->index]);            } else if (cf->ctx) {                confp = *(void **) ((char *) cf->ctx + cmd->conf);                if (confp) {                    conf = confp[cf->cycle->modules[i]->ctx_index];                }            }            rv = cmd->set(cf, cmd, conf);            // ...        }    }    // ...}
复制代码


这里有三个分支。


对于前面说的 NGX_DIRECT_CONF 配置,因为已经分配好了内存,所以直接调用 cmd->set() 函数就行了。


对于像 events 和 stream 这样的指令,Nginx 将 cf->ctx[x] 也就是 cycle->conf_ctx[x]地址传给了 cmd->set() 函数。此处传地址,就是为了让这些自行分配内存,并将地址保存到cycle->conf_ctx[x]


第三个分支则是第二个分支的延伸情形。要想弄清这两种情况,我们须要考查 stream 对应的 cmd->set() 函数,其主要流程如下:


static char * ngx_stream_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) {    // ...    if (*(ngx_stream_conf_ctx_t **) conf) { return "is duplicate"; }    ctx = ngx_pcalloc(cf->pool, sizeof(ngx_stream_conf_ctx_t));    // ...    *(ngx_stream_conf_ctx_t **) conf = ctx;    ngx_stream_max_module = ngx_count_modules(cf->cycle, NGX_STREAM_MODULE);    // ...    ctx->main_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_stream_max_module);    // ...    ctx->srv_conf = ngx_pcalloc(cf->pool, sizeof(void *) * ngx_stream_max_module);    // ...    for (m = 0; cf->cycle->modules[m]; m++) {        if (cf->cycle->modules[m]->type != NGX_STREAM_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; } } }
// 备份 cf 状态 pcf = *cf; cf->ctx = ctx; // ... cf->module_type = NGX_STREAM_MODULE; cf->cmd_type = NGX_STREAM_MAIN_CONF; rv = ngx_conf_parse(cf, NULL); // ... // 恢复 cf 状态 *cf = pcf; // ...}
复制代码


Nginx 解析配置文件如果扫描到 stream { 则会执行 ngx_stream_block(),这个时候正是ngx_conf_handler()函数的分支二。首先要检查 conf 有没有被赋值。如果有,则说明配置文件有多个 stream {,需要报错。然后分配一段内存,其类型是 ngx_stream_conf_ctx_t,并将其地址保存到 cycle->conf_ctx[x]。我们看看 ngx_stream_conf_ctx_t 的结构:


typedef struct {    void  **main_conf;    void  **srv_conf;} ngx_stream_conf_ctx_t;
复制代码


stream 模块工作在传输层,只有 stream 和 server 两级配置,所以 ngx_stream_conf_ctx_t也只有 main_conf 和 srv_conf 两个成员。


接着 Nginx 会调用 ngx_count_modules() 统计 stream 子模块的数量,并依次编号,编号保存在该模块的 ctx_index 字段。然后就是为所有子模块分配内存地址指针。这一步跟 cycle->conf_ctx 很类似。


stream 所有子模块都是 NGX_STREAM_MODULE 类型,对应的 ctx 则为 ngx_stream_module_tngx_stream_module_t 比 ngx_core_module_t 要复杂:


typedef struct {    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);
void *(*create_main_conf)(ngx_conf_t *cf); char *(*init_main_conf)(ngx_conf_t *cf, void *conf);
void *(*create_srv_conf)(ngx_conf_t *cf); char *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);} ngx_stream_module_t;
复制代码


这里的 create_main_conf() 和 create_srv_conf() 跟之前的 create_conf() 很类似。


接着,Nginx 会把配置解析切换成 stream 模式,然后继续解析配置,这个时候,就会进入前面说的第三个分支。


if (cmd->type & NGX_DIRECT_CONF) {    // ...} else if (cmd->type & NGX_MAIN_CONF) {    // ...} else if (cf->ctx) {    confp = *(void **) ((char *) cf->ctx + cmd->conf);    if (confp) {        conf = confp[cf->cycle->modules[i]->ctx_index];    }}rv = cmd->set(cf, cmd, conf);
复制代码


这个时候 cf->ctx 自然是刚才的 ngx_stream_conf_ctx_t 了,那这里的 cmd->conf 是什么呢?让我们看一下本文例子中 return 的定义:


#define NGX_STREAM_SRV_CONF_OFFSET offsetof(ngx_stream_conf_ctx_t, srv_conf)
static ngx_command_t ngx_stream_return_commands[] = { { ngx_string("return"), NGX_STREAM_SRV_CONF|NGX_CONF_TAKE1, ngx_stream_return, NGX_STREAM_SRV_CONF_OFFSET, 0, NULL },
ngx_null_command};
复制代码


这个 cmd->conf 就是 ngx_stream_conf_ctx_t 中 srv_conf 的遍移量。所以此时 cf->ctx + cmd->conf 就是 ngx_st】ream_conf_ctx_t->srv_conf。这正是在 ngx_stream_block() 中 ngx_stream_return_module 新分配的内存。最终的内存结构如下:


【文章福利】小编推荐自己的Linux、C/C++技术交流群:【960994558】整理了一些个人觉得比较好的学习书籍、大厂面试题、有趣的项目和热门技术教学视频资料共享在里面(包括 C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK 等等.),有需要的可以自行添加哦!~


用户头像

赖猫

关注

还未添加个人签名 2020.11.28 加入

纸上得来终觉浅,绝知此事要躬行

评论

发布
暂无评论
深入理解Nginx的四级指针