Nginx 源码中的四级指针
涛叔都说C语言的指针难,多级指针更是难上加难受。在 Nginx 源码有一个四级指针,就是少有的多级指针实例。今天就来分享这个四级指针功能和设计。
conf_ctx
的定义如下:
struct ngx_cycle_s {
void ****conf_ctx;
// ...
}
Nginx 从1.9开始支持三层代理,这个模块叫 stream。它的虽然在功能上比 http 模块简单很多,但是用来分析这个四级指针还是绰绰有余的。
stream 模块默认不开启,我们需要编译时显式开启:
./auto/configure --with-stream --without-http --prefix=/tmp/ngx
make 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 1024
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello
Connection closed by foreign host.
到现在,我们对 stream 模块就有了一个感性的认识。接下来我们开始揭开这个四级指针的神秘面纱。
Nginx的main()
函数有200多行,但跟模块加载相关的只有下面几行:
int main(int argc, char *const *argv) {
// ...
*cycle, init_cycle;
ngx_cycle_t // ...
if (ngx_preinit_modules() != NGX_OK) { return 1; }
= ngx_init_cycle(&init_cycle);
cycle // ...
}
先看这个ngx_preinit_modules()
函数:
(void) {
ngx_int_t ngx_preinit_modulesfor (ngx_uint_t i = 0; ngx_modules[i]; i++) {
[i]->index = i; // 设置模块编号
ngx_modules[i]->name = ngx_module_names[i]; // 设置模块名称
ngx_modules}
= i; // 模块总数
ngx_modules_n // 模块最大数量,含动态加载模块
= ngx_modules_n + NGX_MAX_DYNAMIC_MODULES;
ngx_max_module
return NGX_OK;
}
代码一看就明白,但这里的ngx_modules
是从哪来的呢?
还记得最开始的auto/configure
脚本吗?这个configure
脚本有很多 --(with|without)-xxx
参数。我们可以通过它们控制要编译哪些模块以及模块的加载顺序。
confgigure
脚本执行完成后会生成objs/ngx_modules.c
文件。ngx_modules
就是在这个文件定义的。
ngx_modules
是一个ngx_module_t
数组。在本例中内容如下:
*ngx_modules[] = {
ngx_module_t &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 */
, /* module directives */
ngx_core_commands, /* module type */
NGX_CORE_MODULE, /* init master */
NULL, /* init module */
NULL, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NULL
NGX_MODULE_V1_PADDING};
NGX_MODULE_V1
和NGX_MODULE_V1_PADDING
都是 Nginx 为代码清晰起见定义的快速填充宏,多是空值填充,大可不必纠结。这里最核心的是ctx
,commands
和type
三个字段。
这个type
就是模块的类型。每个种类型的模块可以有不同的ctx
和对应不同的commands
。 Nginx 的核心模块的类型为NGX_CORE_MODULE
,对应的ctx
都是ngx_core_module_t
类型的:
typedef struct {
;
ngx_str_t namevoid *(*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_init_cycle(ngx_cycle_t *old_cycle) {
ngx_cycle_t // ...
->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *));
cycle// ...
if (ngx_cycle_modules(cycle) != NGX_OK) { /* ... */ }
// ...
for (i = 0; cycle->modules[i]; i++) {
if (cycle->modules[i]->type != NGX_CORE_MODULE) { continue; }
= cycle->modules[i]->ctx;
module if (module->create_conf) {
= module->create_conf(cycle);
rv ->conf_ctx[cycle->modules[i]->index] = rv;
cycle}
}
// ...
.ctx = cycle->conf_ctx;
conf.cycle = cycle;
conf.module_type = NGX_CORE_MODULE;
conf.cmd_type = NGX_MAIN_CONF;
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_DIRECT_CONF|NGX_CONF_FLAG, // type
NGX_MAIN_CONF, // set function
ngx_conf_set_flag_slot0, // conf
(ngx_core_conf_t, daemon), // offset
offsetof// post
NULL },
// ...
}
static ngx_command_t ngx_events_commands[] = {
{ ngx_string("events"),
|NGX_CONF_BLOCK|NGX_CONF_NOARGS,
NGX_MAIN_CONF,
ngx_events_block0,
0,
NULL},
ngx_null_command};
大家注意daemon
的类型包含NGX_MAIN_CONF
和NGX_DIRECT_CONF
。所谓NGX_MAIN_CONF
就是 Nginx 的顶级配置。
在本文的例子中, daemon
, events
, stream
都是顶级配置。
虽然同为NGX_MAIN_CONF
,它们又有区别。比如,daemon
包含NGX_DIRECT_CONF
,而events
和stream
则包含NGX_CONF_BLOCK
。对应的配置文件上则表现为daemon
可对应on/off
配置值,而 events
和 stream
对应一个{}
。
只有定义了 NGX_DIRECT_CONF
指令的核心模块才需要设置 ctx->create_conf
函数!
我们知道,设置 ctx->create_conf
是为了分配内存保存配置。而 events
, stream
这些模块没有分配内存,那它们的配置存到哪呢?其实还是保存在 cycle->conf_ctx
。只不过它们是在配置文件解析的过程中动态分配的。对于 ngx_core_module 这样的模块,它们指定了 ctx->create_conf
,所以对应的内存在解析配文件之前就分配好了。那这样做有什么好处呢?好处只有一个,节省内存!像 stream
, events
这些模块,如果不配置,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 ( ;; ) {
// 解析单个指令及其参数
= ngx_conf_read_token(cf);
rc // ...
// 保存配置内容
= ngx_conf_handler(cf, rc);
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++) {
= cf->cycle->modules[i]->commands;
cmd // ...
for ( /* void */ ; cmd->name.len; cmd++) {
// ...
if (cmd->type & NGX_DIRECT_CONF) {
= ((void **) cf->ctx)[cf->cycle->modules[i]->index];
conf } else if (cmd->type & NGX_MAIN_CONF) {
= &(((void **) cf->ctx)[cf->cycle->modules[i]->index]);
conf } else if (cf->ctx) {
= *(void **) ((char *) cf->ctx + cmd->conf);
confp if (confp) {
= confp[cf->cycle->modules[i]->ctx_index];
conf }
}
= cmd->set(cf, cmd, conf);
rv // ...
}
}
// ...
}
这里有三个分支。
对于前面说的 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"; }
= ngx_pcalloc(cf->pool, sizeof(ngx_stream_conf_ctx_t));
ctx // ...
*(ngx_stream_conf_ctx_t **) conf = ctx;
= ngx_count_modules(cf->cycle, NGX_STREAM_MODULE);
ngx_stream_max_module // ...
->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);
ctx// ...
for (m = 0; cf->cycle->modules[m]; m++) {
if (cf->cycle->modules[m]->type != NGX_STREAM_MODULE) { continue; }
= cf->cycle->modules[m]->ctx;
module = cf->cycle->modules[m]->ctx_index;
mi
if (module->create_main_conf) {
->main_conf[mi] = module->create_main_conf(cf);
ctxif (ctx->main_conf[mi] == NULL) { return NGX_CONF_ERROR; }
}
if (module->create_srv_conf) {
->srv_conf[mi] = module->create_srv_conf(cf);
ctxif (ctx->srv_conf[mi] == NULL) { return NGX_CONF_ERROR; }
}
}
// 备份 cf 状态
= *cf;
pcf ->ctx = ctx;
cf// ...
->module_type = NGX_STREAM_MODULE;
cf->cmd_type = NGX_STREAM_MAIN_CONF;
cf= ngx_conf_parse(cf, NULL);
rv // ...
// 恢复 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_t
。 ngx_stream_module_t
比 ngx_core_module_t
要复杂:
typedef struct {
(*preconfiguration)(ngx_conf_t *cf);
ngx_int_t (*postconfiguration)(ngx_conf_t *cf);
ngx_int_t
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) {
= *(void **) ((char *) cf->ctx + cmd->conf);
confp if (confp) {
= confp[cf->cycle->modules[i]->ctx_index];
conf }
}
= cmd->set(cf, cmd, conf); rv
这个时候 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_CONF_TAKE1,
NGX_STREAM_SRV_CONF,
ngx_stream_return,
NGX_STREAM_SRV_CONF_OFFSET0,
},
NULL
ngx_null_command};
这个 cmd->conf
就是 ngx_stream_conf_ctx_t
中 srv_conf
的遍移量。所以此时 cf->ctx + cmd->conf
就是 ngx_stream_conf_ctx_t->srv_conf
。这正是在 ngx_stream_block()
中 ngx_stream_return_module
新分配的内存。