开源软件|菜鸟学习Nginx之启动流程(1)

对于C语言编写的程序来说,main函数就是入口函数,把main函数研究清楚对于理解软件架构、功能会有事半功倍的效果。好在Nginx的main函数并不是很复杂,这里会把启动流程分成两篇来介绍,希望能够描述清楚。
我把启动流程划分成两部分:cycle核心结构体初始化、master/worker进程启动。本篇介绍cycle核心结构体初始化。
一、初始化流程 在Nginx中有一个结构体伴随Nginx进程整个生命周期,那就是ngx_cycle。在Nginx中有且只有一个对象ngx_cycle_t。先来看一下Nginx启动流程图,有一些无关紧要的功能并没有在图中体现:
开源软件|菜鸟学习Nginx之启动流程(1)
文章图片


特别说明:

  1. 继承socket,此部分功能主要用于平滑升级功能并且继承的是监听socket,为了保证服务不中断。
  2. 初始化cycle,核心结构体初始化是Nginx源码中最长的函数,里面涉及的内容非常多,本篇就是详细分析该方法。
  3. 启动后台进程,一般情况下我们是通过终端,直接运行Nginx进程启动服务。这种场景属于前台进程,为了脱离终端进程,必须要以后台方式运行Nginx方式。
  4. 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模式。

    推荐阅读