对于C语言编写的程序来说,main函数就是入口函数,把main函数研究清楚对于理解软件架构、功能会有事半功倍的效果。好在Nginx的main函数并不是很复杂,这里会把启动流程分成两篇来介绍,希望能够描述清楚。
我把启动流程划分成两部分:cycle核心结构体初始化、master/worker进程启动。本篇介绍cycle核心结构体初始化。
一、初始化流程 在Nginx中有一个结构体伴随Nginx进程整个生命周期,那就是ngx_cycle。在Nginx中有且只有一个对象ngx_cycle_t。先来看一下Nginx启动流程图,有一些无关紧要的功能并没有在图中体现:
文章图片
特别说明:
- 继承socket,此部分功能主要用于平滑升级功能并且继承的是监听socket,为了保证服务不中断。
- 初始化cycle,核心结构体初始化是Nginx源码中最长的函数,里面涉及的内容非常多,本篇就是详细分析该方法。
- 启动后台进程,一般情况下我们是通过终端,直接运行Nginx进程启动服务。这种场景属于前台进程,为了脱离终端进程,必须要以后台方式运行Nginx方式。
- Nginx生存模式,一般由两种,单机模式和master/woker模式。在真正服务部署时,均采用master/worker模式。
typedef struct ngx_cycle_sngx_cycle_t;
struct ngx_cycle_s
{
/**
* 指向一个指针数组,该数组元素又指向了一个指针数组,因此时4级指针.
* 第一层数组下标,是ngx_module_t.index
* 第二层数组下标,是ngx_module_t.ctx_index
* 返回配置结构体,每个模块都一个配置模块结构体(自定义).
* 由create_conf回调创建出来的配置结构体
* 注: conf_ctx是一个数组,大小与ngx_modules一样 初始化在函数ngx_init_cycle
*/
void ****conf_ctx;
ngx_pool_t *pool;
/* 进程级内存池 */ngx_log_t *log;
ngx_log_t new_log;
ngx_uint_t log_use_stderr;
/* unsignedlog_use_stderr:1;
*/ngx_connection_t **files;
ngx_connection_t *free_connections;
/* 空闲连接 */
ngx_uint_t free_connection_n;
/* 空闲连接数 */ngx_module_t **modules;
/* 实际指向ngx_modules.c中ngx_modules */
ngx_uint_t modules_n;
ngx_uint_t modules_used;
/* unsignedmodules_used:1;
*/ngx_queue_t reusable_connections_queue;
/* 双向链表 空闲connections 可重复使用 */
ngx_uint_t reusable_connections_n;
ngx_array_t listening;
/* 动态数组 监听socket */
ngx_array_t paths;
ngx_array_t config_dump;
ngx_rbtree_t config_dump_rbtree;
ngx_rbtree_node_t config_dump_sentinel;
ngx_list_t open_files;
ngx_list_t shared_memory;
ngx_uint_t connection_n;
/* 当前活跃连接数 */
ngx_uint_t files_n;
/* 三者对应关系是 按照数组下标对应 */
ngx_connection_t *connections;
/* 连接池 数组 默认1024 */
ngx_event_t *read_events;
/* 数组 每一个连接至少对应一个读事件 默认1024 */
ngx_event_t *write_events;
/* 数组 每一个连接至少对应一个写事件 默认1024 */ngx_cycle_t *old_cycle;
ngx_str_t conf_file;
/* 默认/usr/local/nginx/conf/nginx.conf */
ngx_str_t conf_param;
ngx_str_t conf_prefix;
/* 默认/usr/local/nginx/conf/ */
ngx_str_t prefix;
/* /usr/local/nginx/ */
ngx_str_t lock_file;
ngx_str_t hostname;
};
我相信所有人第一次看到该结构体,第一反应绝对是:我靠,4级指针,什么鬼!不错,我就是这个反应,当初看到了这个成员突然有种不想继续想法。这里我想说的是:当遇到奇葩的数据结构或者定义的时候,我们需要冷静下来,慢慢分析,一定能分析出个所以然。如果还不行就是百度/谷歌,我们相信自己绝对不是第一个吃螃蟹的人。
这个4级指针conf_ctx,在注释中已经介绍的详细了,如果还是比较模糊,在下面还会具体介绍。
三、详细说明 函数ngx_init_cycle主要功能就是初始化ngx_cycle_t数据结构中各个成员,该函数大概有900行代码,应该是Nginx中最长的代码。具体如下:
/**
* 创建ngx_cycle_t核心结构
* @param old_cycle 旧的核心结构
* @return 返回新的ngx_cycle_t结构
*/
ngx_cycle_t *
ngx_init_cycle(ngx_cycle_t *old_cycle)
{
void *rv;
char **senv;
ngx_uint_t i, n;
ngx_log_t *log;
ngx_time_t *tp;
ngx_conf_t conf;
ngx_pool_t *pool;
ngx_cycle_t *cycle, **old;
ngx_shm_zone_t *shm_zone, *oshm_zone;
ngx_list_part_t *part, *opart;
ngx_open_file_t *file;
ngx_listening_t *ls, *nls;
ngx_core_conf_t *ccf, *old_ccf;
ngx_core_module_t *module;
char hostname[NGX_MAXHOSTNAMELEN];
ngx_timezone_update();
/* force localtime update with a new timezone */tp = ngx_timeofday();
tp->sec = 0;
ngx_time_update();
log = old_cycle->log;
/* 创建内存池 */
pool = ngx_create_pool(NGX_CYCLE_POOL_SIZE, log);
if (pool == NULL)
{
return NULL;
}
pool->log = log;
/* 在内存池中为ngx_cycle_t分配内存 */
cycle = ngx_pcalloc(pool, sizeof(ngx_cycle_t));
if (cycle == NULL)
{
ngx_destroy_pool(pool);
return NULL;
}cycle->pool = pool;
cycle->log = log;
cycle->old_cycle = old_cycle;
/* 配置文件相关 */
cycle->conf_prefix.len = old_cycle->conf_prefix.len;
cycle->conf_prefix.data = https://www.it610.com/article/ngx_pstrdup(pool, &old_cycle->conf_prefix);
if (cycle->conf_prefix.data =https://www.it610.com/article/= NULL)
{
ngx_destroy_pool(pool);
return NULL;
}cycle->prefix.len = old_cycle->prefix.len;
cycle->prefix.data = https://www.it610.com/article/ngx_pstrdup(pool, &old_cycle->prefix);
if (cycle->prefix.data =https://www.it610.com/article/= NULL)
{
ngx_destroy_pool(pool);
return NULL;
}cycle->conf_file.len = old_cycle->conf_file.len;
cycle->conf_file.data = https://www.it610.com/article/ngx_pnalloc(pool, old_cycle->conf_file.len + 1);
if (cycle->conf_file.data =https://www.it610.com/article/= NULL)
{
ngx_destroy_pool(pool);
return NULL;
}
ngx_cpystrn(cycle->conf_file.data, old_cycle->conf_file.data,
old_cycle->conf_file.len + 1);
cycle->conf_param.len = old_cycle->conf_param.len;
cycle->conf_param.data = https://www.it610.com/article/ngx_pstrdup(pool, &old_cycle->conf_param);
if (cycle->conf_param.data =https://www.it610.com/article/= NULL)
{
ngx_destroy_pool(pool);
return NULL;
}n = old_cycle->paths.nelts ? old_cycle->paths.nelts : 10;
if (ngx_array_init(&cycle->paths, pool, n, sizeof(ngx_path_t *)) != NGX_OK)
{
ngx_destroy_pool(pool);
return NULL;
}ngx_memzero(cycle->paths.elts, n * sizeof(ngx_path_t *));
if (ngx_array_init(&cycle->config_dump, pool, 1, sizeof(ngx_conf_dump_t)) != NGX_OK)
{
ngx_destroy_pool(pool);
return NULL;
}ngx_rbtree_init(&cycle->config_dump_rbtree, &cycle->config_dump_sentinel,
ngx_str_rbtree_insert_value);
if (old_cycle->open_files.part.nelts)
{
n = old_cycle->open_files.part.nelts;
for (part = old_cycle->open_files.part.next;
part;
part = part->next)
{
n += part->nelts;
}
}
else
{
n = 20;
}if (ngx_list_init(&cycle->open_files, pool, n, sizeof(ngx_open_file_t)) != NGX_OK)
{
ngx_destroy_pool(pool);
return NULL;
}if (old_cycle->shared_memory.part.nelts)
{
n = old_cycle->shared_memory.part.nelts;
for (part = old_cycle->shared_memory.part.next;
part;
part = part->next)
{
n += part->nelts;
}
}
else
{
n = 1;
}if (ngx_list_init(&cycle->shared_memory, pool, n, sizeof(ngx_shm_zone_t)) != NGX_OK)
{
ngx_destroy_pool(pool);
return NULL;
}
这段代码逻辑比较简单,只是单纯的调用内部api对各个成员 进行初始化,例如内存池,共享内存,配置文件等。这里需要特别说明ngx_cycle_t中内存池pool。在开篇的时候就已经说了ngx_cycle_t生命周期是和进程一样的,那么ngx_cycle_t中的pool也是一样,在这里我称呼它为进程级内存池。后续所有内存的申请以及子内存池(连接级内存池、请求级内存池)均来自此池。
/* 分配listening数组动态数组 如果第一次启动old_cycle->listening为0 */
n = old_cycle->listening.nelts ? old_cycle->listening.nelts : 10;
if (ngx_array_init(&cycle->listening, pool, n, sizeof(ngx_listening_t)) != NGX_OK)
{
ngx_destroy_pool(pool);
return NULL;
}ngx_memzero(cycle->listening.elts, n * sizeof(ngx_listening_t));
初始化listening结构,该结构主要用于保存监听socket相关信息,例如:监听80端口的socket。为什么listening是动态数组呢?对于Tcp监听来说,我们可以指定多个端口同时提供服务,例如:http默认端口80,https默认端口是443。这个时候就需要有多个listening保存。
ngx_queue_init(&cycle->reusable_connections_queue);
/* 创建大小为ngx_max_module,数组元素类型为void* 其实创建的指针数组 */
cycle->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *));
if (cycle->conf_ctx == NULL)
{
ngx_destroy_pool(pool);
return NULL;
}
注意conf_ctx初始化工作,通过内存池申请内存,其大小为ngx_max_module * sizeof(void *)。实质内容是,创建了一个指针数组,等价于void* conf_ctx[ngx_max_module],数组每一个项保存的是一个指针。个人认为这样解释应该比较清晰。数组与ngx_modules定义顺序是保持一致的。例如:conf_ctx[0]是模块ngx_core_module上下文,conf_ctx[4]是模块ngx_events_module上下文,下面代码可验证
/* 从全局变量ngx_modules拷贝到cycle->modules*/
if (ngx_cycle_modules(cycle) != NGX_OK)
{
ngx_destroy_pool(pool);
return NULL;
}
/* 初始化核心模块即类型为NGX_CORE_MODULE */
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)
{
/**
* 目前定义create_conf回调方法 只有ngx_core_module和ngx_regex_module
* ngx_event_module没有定义create_conf,只定义了init_conf,可知
* ngx_event_module对应的conf_ctx是NULL, 但是在经过ngx_conf_parse后
* conf_ctx不为NULL.
*/
rv = module->create_conf(cycle);
if (rv == NULL)
{
ngx_destroy_pool(pool);
return NULL;
}
cycle->conf_ctx[cycle->modules[i]->index] = rv;
//给指针数组赋值
}
}
调用核心模块(类型为NGX_CORE_MODULE)中定义的create_conf回调函数。该函数主要用于创建配置文件上下文,用于赋值给conf_ctx。在模块声明时只有ngx_core_module和ngx_regex_module定义了create_conf,看到这里不知道是否和我有一样的疑问:那其他模块是在什么时候生成conf_ctx呢?继续往下看
senv = environ;
//保存环境变量ngx_memzero(&conf, sizeof(ngx_conf_t));
/* STUB: init array ? */
conf.args = ngx_array_create(pool, 10, sizeof(ngx_str_t));
if (conf.args == NULL)
{
ngx_destroy_pool(pool);
return NULL;
}conf.temp_pool = ngx_create_pool(NGX_CYCLE_POOL_SIZE, log);
if (conf.temp_pool == NULL)
{
ngx_destroy_pool(pool);
return NULL;
}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;
#if 0
log->log_level = NGX_LOG_DEBUG_ALL;
#endifif (ngx_conf_param(&conf) != NGX_CONF_OK)
{
environ = senv;
ngx_destroy_cycle_pools(&conf);
return NULL;
}
/**
* 解析配置文件
* 注1: 经过此方法之后 核心模块ngx_event_module对应的conf_ctx有数据了
* 在执行ngx_conf_parse函数时,会解析nginx.conf配置文件,当遇到event标签,会调用
* ngx_events_block回调方法 该方法会设置conf_ctx
* 注2: 经过此方法cycle->listening中会保存真正数据
*/
if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK)
{
environ = senv;
ngx_destroy_cycle_pools(&conf);
return NULL;
}
通过执行ngx_conf_parse函数,解析nginx.conf配置文件,在解析过程中遇到标签就会调用对应的解析函数,在解析函数中会对conf_ctx进行赋值。例如:events标签,会调用ngx_events_block。对于其他modules都这样操作的。
for (i = 0;
cycle->modules[i];
i++)
{
if (cycle->modules[i]->type != NGX_CORE_MODULE)
{
continue;
}module = cycle->modules[i]->ctx;
if (module->init_conf)
{
if (module->init_conf(cycle,
cycle->conf_ctx[cycle->modules[i]->index]) == NGX_CONF_ERROR)
{
environ = senv;
ngx_destroy_cycle_pools(&conf);
return NULL;
}
}
}
调用init_conf回调函数,大部分模块都没有定义该回调方法。
在接下来,为了节约篇幅,这里忽略一些逻辑简单的初始化流程。
/* handle the listening sockets */if (old_cycle->listening.nelts)
{
/* 只有在平滑升级才会进入此分支,第一次启动服务不会进入。 针对平滑升级的会有独立一篇,介时会详细说明 */
}
else
{
/**
* listening赋值是在执行ngx_conf_parse 即解析配置文件时,
* 入口ngx_http_block,最终会ngx_create_listening
*/
ls = cycle->listening.elts;
for (i = 0;
i < cycle->listening.nelts;
i++)
{
ls[i].open = 1;
#if (NGX_HAVE_DEFERRED_ACCEPT && defined SO_ACCEPTFILTER)
if (ls[i].accept_filter)
{
ls[i].add_deferred = 1;
}
#endif
#if (NGX_HAVE_DEFERRED_ACCEPT && defined TCP_DEFER_ACCEPT)
if (ls[i].deferred_accept)
{
ls[i].add_deferred = 1;
}
#endif
}
}
/* open listening socket */
if (ngx_open_listening_sockets(cycle) != NGX_OK)
{
goto failed;
}if (!ngx_test_config)
{//对监听套接字进行配置 主要是socket选项
ngx_configure_listening_sockets(cycle);
}/* commit the new cycle configuration */if (!ngx_use_stderr)
{
(void)ngx_log_redirect_stderr(cycle);
}pool->log = cycle->log;
/* initialize all modules 调用所有模块init_module方法*/
if (ngx_init_modules(cycle) != NGX_OK)
{
/* fatal */
exit(1);
}
这部分功能是创建监听socket并且对socket进行基本设置。 那么cycle->listening.elts是在什么地方设置的呢?其实仔细思考一下,我们监听端口配置是写在nginx.conf配置文件,那么肯定是在解析配置文件时对其赋值,所以应该ngx_conf_parse。至此,针对ngx_cycle_t初始化基本主要内容介绍完毕。
我们回过头在来看一下conf_ctx赋值这部分代码,上面介绍过conf_ctx有两种赋值方式,一个是调用回调函数create_conf,另外一个是解析配置文件标签信息。通过ngx_core_module和ngx_event_module来举例说明这两种方式:
/**
* create_conf回调函数 用于生成conf_ctx上下文
*/
static void *
ngx_core_module_create_conf(ngx_cycle_t *cycle)
{
ngx_core_conf_t*ccf;
ccf = ngx_pcalloc(cycle->pool, sizeof(ngx_core_conf_t));
if (ccf == NULL) {
return NULL;
}/*
* set by ngx_pcalloc()
*
*ccf->pid = NULL;
*ccf->oldpid = NULL;
*ccf->priority = 0;
*ccf->cpu_affinity_auto = 0;
*ccf->cpu_affinity_n = 0;
*ccf->cpu_affinity = NULL;
*/ccf->daemon = NGX_CONF_UNSET;
ccf->master = NGX_CONF_UNSET;
ccf->timer_resolution = NGX_CONF_UNSET_MSEC;
ccf->shutdown_timeout = NGX_CONF_UNSET_MSEC;
ccf->worker_processes = NGX_CONF_UNSET;
ccf->debug_points = NGX_CONF_UNSET;
ccf->rlimit_nofile = NGX_CONF_UNSET;
ccf->rlimit_core = NGX_CONF_UNSET;
ccf->user = (ngx_uid_t) NGX_CONF_UNSET_UINT;
ccf->group = (ngx_gid_t) NGX_CONF_UNSET_UINT;
if (ngx_array_init(&ccf->env, cycle->pool, 1, sizeof(ngx_str_t))
!= NGX_OK)
{
return NULL;
}return ccf;
}
/**
* 解析events标签,调用此函数。用于创建conf_ctx上下文
* @param conf是就是cycle中conf_ctx元素地址
*/
static char *
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模块数量并且设置他们的索引值ctx_index*/
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;
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)
{//创建子模块conf_ctx上下文
(*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;
}
//初始化NGX_EVENT_MODULE模块
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;
}
四、总结 【开源软件|菜鸟学习Nginx之启动流程(1)】至此Nginx启动流程,关于ngx_cycle_t初始化介绍完毕,有些地方可能介绍不到位,请大家多多指点。后面介绍Nginx启动流程中关于master/worker模式。
推荐阅读
- Go|Docker后端部署详解(Go+Nginx)
- 后台|NATAPP内网穿透通过nginx实现一个端口访问多个不同端口服务
- nginx-1.20.2安装使用
- NGINX 创始人 Igor Sysoev 退出 F5(20 年发展“简史”令人肃然起敬)
- Faker.js 作者“删库跑路”引开源圈“震动”(该开源项目现已被社区控制)
- Nginx|Nginx~从入门到入坑。
- nginx|Mac配置PHP环境(brew安装nginx+php)
- 使用ngx_lua构建高并发应用(2)
- svn|前瞻(Spring Boot 2.4.0 第二个里程碑版本发布)
- nginx cache踩坑