一 ngx配置解析框架
1.1 配置解析流程
配置解析源码在 ngx_conf_file.c文件中实现, 函数 ngx_conf_parse.
如上图所示, 配置解析采用递归的方式进行解析配置. 每次配置文件读取会读取一组数据, 其中第一个为配置项名称, 后面根据配置类型有不同数量的值. 示例 error_log /var/log/nginx/error.log; 其中键为 error_log 值为 /var/log/nginx/error.log. 除了普通的键值对类型, 还有递归类型 如:
当遇到递归类型配置时, 处理函数将会1.先保存配置解析的上下文. 2.递归调用配置解析函数. 3.恢复上下文.
1.2 配置文件读取
源码在 ngx_conf_file.c 文件的 ngx_conf_read_token 函数. 该函数主要实现 : a. 逐字符读取. b. 状态机处理字符. c. 有四种返回值, 错误 模块开始 模块结束 文件结束. d. 读取的键值对存放在 cf->args 数组中.
1.3 寻找配置处理函数
ngx_conf_t *cf; 数据结构理解 :
struct ngx_conf_s {
char *name; //配置文件名
ngx_array_t *args; //存储ngx_conf_read_token中读取的键值对
ngx_cycle_t *cycle; //全局的数据结构体
ngx_pool_t *pool; //全局内存池
ngx_pool_t *temp_pool; //临时内存池
ngx_conf_file_t *conf_file; //对应的配置文件结构体
ngx_log_t *log; //日志
void *ctx; //描述当前配置解析的上下文(event模块用于指向对应的配置项)
ngx_uint_t module_type; //当前的模块类型 core event http mail
ngx_uint_t cmd_type; //当前的指令类型
ngx_conf_handler_pt handler; //自定义的处理函数
void *handler_conf; //自定义处理函数的数据
};
源码在ngx_conf_file.c文件的ngx_conf_handler函数中. 处理顺序 : 遍历所有模块的所有commands中依次1.匹配配置项名称. 2.匹配模块类型. 3.匹配指令类型. 4. 调用对应的处理函数.
在解析配置文件的过程中, 使用的始终是同一个 ngx_conf_t.
1.4 普通配置项的处理
通常是给指定配置项赋值, ngx提供了多种类型的赋值函数. 下面以 ngx_conf_set_num_slot为例.
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); //位移到配置项所在的内存
if (*np != NGX_CONF_UNSET) {
return "is duplicate";
}
value = cf->args->elts;
*np = ngx_atoi(value[1].data, value[1].len); // 将配置转成对应的类型 并赋值到对应的配置项
if (*np == NGX_ERROR) {
return "invalid number";
}
if (cmd->post) {
post = cmd->post;
return post->post_handler(cf, post, np); // 调用对应的处理函数
}
return NGX_CONF_OK;
}
其中cmf->offset通过 offsetof 宏获取字段在结构体中的偏移量.
其余的处理函数和上面的区别主要在于类型, 上面是针对 ngx_int_t类型, 还有 size_t, off_t, ngx_msec_t 等类型.
1.5 递归配置项的处理
递归配置项的处理流程: 1.保存当前上下文 2.设置上下文 3.调用配置解析函数. 4.恢复上下文. 其中上下文主要是 ngx_conf_t 中的 ctx, module_type, cmd_type. 参考 events 配置项的解析 :
ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
char *rv;
void ***ctx;
ngx_uint_t i;
ngx_conf_t pcf;
ngx_event_module_t *m;
if (*(void **) conf) {
return "is duplicate";
}
/* count the number of the event modules and set up their indices */
//生成 event 配置项指针
ngx_event_max_module = ngx_count_modules(cf->cycle, NGX_EVENT_MODULE);
ctx = ngx_pcalloc(cf->pool, sizeof(void *));
if (ctx == NULL) {
return NGX_CONF_ERROR;
}
*ctx = ngx_pcalloc(cf->pool, ngx_event_max_module * sizeof(void *));
if (*ctx == NULL) {
return NGX_CONF_ERROR;
}
*(void **) conf = ctx;
// 依次调用 event 模块的配置生成函数
for (i = 0; cf->cycle->modules[i]; i++) {
if (cf->cycle->modules[i]->type != NGX_EVENT_MODULE) {
continue;
}
m = cf->cycle->modules[i]->ctx;
if (m->create_conf) {
(*ctx)[cf->cycle->modules[i]->ctx_index] =
m->create_conf(cf->cycle);
if ((*ctx)[cf->cycle->modules[i]->ctx_index] == NULL) {
return NGX_CONF_ERROR;
}
}
}
// 保存上下文
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;
if (rv != NGX_CONF_OK) {
return rv;
}
// 依次调用 event 模块的配置初始化函数
for (i = 0; cf->cycle->modules[i]; i++) {
if (cf->cycle->modules[i]->type != NGX_EVENT_MODULE) {
continue;
}
m = cf->cycle->modules[i]->ctx;
if (m->init_conf) {
rv = m->init_conf(cf->cycle,
(*ctx)[cf->cycle->modules[i]->ctx_index]);
if (rv != NGX_CONF_OK) {
return rv;
}
}
}
return NGX_CONF_OK;
}
二 配置存储
图片来源https://ialloc.org/blog/ngx-notes-conf-parsing/
在 ngx_cycle.c文件 ngx_init_cycle() 函数中, 创建配置存储数组.
cycle->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *));
if (cycle->conf_ctx == NULL) {
ngx_destroy_pool(pool);
return NULL;
}
配置获取, 以event 模块为例.
#define ngx_event_get_conf(conf_ctx, module) \
(*(ngx_get_conf(conf_ctx, ngx_events_module))) [module.ctx_index]
#define ngx_get_conf(conf_ctx, module) conf_ctx[module.index]
四大模块 Core Event Http Mail. cyclt->conf_ctx数组中, event的数据实际存储在 ngx_events_module 对应下标的指针指向的数组中, 该指针数组的大小为Event模块的数量.
三 吐槽
ngx源码通过 void * 和函数指针把C语言的多态发挥的淋漓尽致. 同时, nginx 把业务抽象化, 模块化做的令人叹为观止. 但是ngx也有让我异常想吐槽的. 举例
1.枚举类型的边界. ngx_conf_t 结构体中有 module_type 和 cmd_type 两个字段. 其值本应是枚举类型, 通过位运算判断属于哪些类型. 但采用的是#define的方式, 导致很难确定具体有哪些 module_type 和 cmd_type. 类型的边界很难确定.
2.数据的边界. 以ngx非常重要的一个数据 ngx_cycle_s为例. ngx_cycle_s存储了一个进程所有的相关数据, 并且独一份. 这个结构体在各个业务模块中被使用, 导致你很难确定一个变量在哪些地方被使用. 对比 golang的做法, 以日志模块为例. 在 ngx 中 ngx_log_t 是全局独一份的日志数据, 其值被各个模块引用. 如果是 golang, 它的做法是把日志相关的数据封装在这个模块内, 提供日志记录接口. 如果实在有必要获取日志结构体, 也可以提供接口获取日志结构体.
吐槽到此为止, ngx确实有太多可以学习的地方, 但是依旧要以批判的眼光看待, 取其精华, 弃其糟粕.
当然 或许这就是大神和凡人的区别 但是自己作为一个凡人 一定不要没有大神的能力就玩些骚的