# Nginx源码-启动流程概述 ## 写在前面 我从去年开始接触到nginx,工作之余也看过不少关于nginx的资料,但也只是停留在使用阶段,后来由于工作特性和它打交道次数的越来越多,遇到不少问题也是通过翻阅nginx文档资料来解决的,整个过程中逐渐感受到了nginx的美妙之处。于是开始尝试阅读nginx源码,并将一些心得体会记录下来,分享给所有的nginx使用者和爱好者,希望能和大家一起成长,一起进步。 Nginx有着很多优秀的设计思想,底层实现了诸如内存池,连接池,自旋锁,红黑树,共享内存等常见的数据结构。然而,最吸引我的却是它的进程模型、模块化设计以及事件驱动,这些是Nginx具有高可靠性、高扩展性、以及轻松应对C10K,C100K场景的核心要素。 ## 进程模型 Nginx使用了管理进程(master)和工作进程(worker)的设计,master进程负责管理各个worker,通过信号或管道的方式来控制worker的动作。当某个worker异常退出时,master进程在大多数情况下会立即启动新的worker进程替代它。worker进程是真正处理用户请求的,各worker进程是平等的,它们通过共享内存,原子操作等一些进程间通信机制来实现负载均衡。多进程模型的设计模式充分利用了SMP多核架构的并发处理能力,保障了服务的健壮性。 模块化设计:Nginx主框架中只提供了少量的核心代码,大量强大的功能都是在各模块中实现的,各模块继承了同一套标准的接口规范和数据结构,在开发新功能时,只需要按照这套标准来实现逻辑即可。同时,Nginx将模块进行了分类和分层,官方将模块分为核心模块,配置模块,事件模块,HTTP模块,mail模块。每类模块都遵循各自的通用接口规范。在Nginx的启动时各模块会分别创建存储配置的结构体以及进行初始化操作,HTTP模块还会将各自逻辑以hook形式注册到Nginx的处理请求的11各阶段中。这种设计思想给Nginx带来了良好的扩展性、伸缩性和可靠性。 ## 事件驱动 作为一款web服务器,Nginx处理的事件主要来自于网络和磁盘,包括TCP连接的建立与断开,接收和发送网络数据包,磁盘文件的I/O操作等等,每种类型都对应了一个读事件和写事件,Nginx通过event模块实现了读写事件的收集、管理和分发,同时采用了红黑树这种高效的数据结构管理各事件定时器。Nginx的事件处理框架完美的支持了各类操作系统提供的事件驱动模型,包括epoll,poll,select,kqueue,eventport等。它定义了一系列运行在不同操作系统,不同内核版本的事件驱动模块。特别是在Liunx2.6之后,Nginx采用了最强大的事件管理机制epoll,这是它能够支持百万级并发连接的重要基石。 我一直试图将Nginx和生活中的事物类比,在找到案例的过程中发现,我们的组织结构就是一个"活生生"的Nginx。站在模块的角度上看,网校有研发、测试、运营、产品、客服等多个部门,每个部门分工明确,都专注于完成自己的使命,大家共同的服务于每一个客户,就好像Nginx的每类模块有自己独有的功能,共同愿景就是服务于每一条请求。站在进程的角度上看,网校有总负责人,他下面又有各部门的leader,每个leader又管理着多个worker,领导们就好像Nginx进程模型中的master,他们管理和监控所有的worker进程,同时做出决策并将信号下达给各worker,当某个worker进程因为异常状况"离职"时,leader就会立马招一个新的worker替换他。站在事件的角度看,当接受用户的需求,指挥中心或决策团队将这些需求转化为指令下发给各部门,各部门则会驱动各组的worker干活,网校的每个worker都异常勤奋,坚守在自己的工作岗位上全程“无阻塞”的高效工作。可见网校具有优秀的"Nginx基因",有能力支撑起强大的用户群体和海量的用户流量。 这里,我会以我的理解向大家讲述Nginx的基本设计思想与实现原理,本文章都是基于nginx 1.15.8.1,也是网校现在生产环境正在使用的版本。 ## Nginx启动流程--创建ngx_cycle_t 当你敲下命令/home/nginx/sbin/nginx -c /home/nginx/conf/nginx.conf启动nginx,你可知道Nginx都干了哪些事情吗?本章就让我们一起探索整个nginx帝国建立的过程吧! ## 1解析命令行ngx_get_options 首先Nginx总得知道你想干嘛,所以需要先解析输入的命令行参数。nginx会提前定义几个全局标志位,然后调用ngx_get_options(argc, argv)来解析命令,解析的过程中会修改不同的标志位,nginx会在之后的启动过程中根据这些标志位来执行不同的动作。例如当你输入的是“-v”,nginx就知道了原来你只是想获取当前版本号,那么它会将ngx_show_version这个标志位设置为1,接下里就会调用ngx_show_version_info()函数向屏幕上打印版本信息。下面是常见的命令参数,对应的全局变量标志位和相关动作。 ![1579420883297_FFF1F8E3-4AD3-4034-A9FB-3EE3B035FD0A](https://img.kancloud.cn/02/95/0295658fafa65a685c52f41a935091fb_830x334.gif) ## 2初始化ngx_*_init 这里,nginx开始针对各种环境变量进行初始化操作,其中较为重要的有以下三个: ### 2.1时间初始化ngx_time_init nginx通过ngx_time_init() 函数初始化了几个全局的时间变量,其中包括了log的时间格式,http协议的时间格式,系统时间等等,然后使用ngx_time_update()函数更新时间值,这里面会使用ngx_trylock()获取时间更新的互斥锁,避免进程或线程间并发更新系统时间,最后通过ngx_gettimeofday()系统调用获取当前时间并更新到nginx独有的ngx_time_t结构体里。nginx在处理请求的流程中涉及到了大量获取时间的操作,出于性能的考虑,nginx在启动的时候就一次性设置好了各时间对象,同时通过ngx_cached_*等全局变量缓存在内存中,在必要的时候再进行更新,就是为了避免过多的系统调用带来的额外损耗。 ### 2.2正则初始化ngx_regex_init 无论是location的配置,还是rewrite的规则,nginx都支持基于pcre库的正则表达式,同时nginx在ngx_regex.c里实现了自己的内存管理回调函数ngx_regex_malloc,ngx_regex_free以及全局的ngx_pcre_pool内存池,这部分数据结构的初始化都会在nginx启动阶段执行。 ### 2.3日志初始化ngx_log_init ngx_log_t是nginx存储日志相关信息的结构体,其中有两个重要的成员log_level和*file,前者是用来表示日志的级别,nginx的日志一共有STDERR,EMERG,ALERT,CRIT,ERR,WARN,NOTICE,INFO,DEBUG九个级别。而后者是一个ngx_open_file_t的结构体指针,存储着文件fd以及文件名等关键信息。大家知道日志级别和日志路径都可以通过“error_log”在配置文件中指定,但是到这里nginx还没有开始解析配置文件,那么它该如何设置log的级别和文件路径呢?nginx会暂时通过预定的全局变量将log设置成NOTICE级别,同时将文件指定为安装路径下的“/logs/error.log”。如果在后面通过解析nginx.conf配置文件发现用户的有自定义的配置,则会更新ngx_log_t对应的成员变量。 当然在nginx的启动流程中,还有其他的初始化操作,比如与https相关的ngx_ssl_init,与获取操作系统内核参数配置相关的ngx_os_init(),与循环冗余校验相关的ngx_crc32_table_init(),与slab内存管理相关的ngx_slab_sizes_init(),这里就不一一展开,有兴趣的可以参考nginx源码深入研究。 ## 3 socket继承 这是一个必不可少的阶段,因为NGINX需要判断当前是否处于升级状态,也就是我们常说的平滑升级。这时候新版本的master进程需要对旧版本的Nginx服务监听的句柄做继承处理。继承操作是通过调用ngx_add_inherited_sockets函数,该函数的实现也很简单: 首先是通过getenv()系统调用获取环境变量“NGINX”,那这个环境变量是在哪里设置的呢,它又保存着什么信息呢?其实旧版本master进程是通过execve的方式来产生新的master进程,因此新的master进程并不会像直接fork子进程那样继承父进程的fd,于是旧的master进程在execve之前将自己监听的套接字描述符保存在NGINX环境变量中,形式为“NGINX=fd1;fd2;fd3...”,这样新版本的master就可以从中获知旧的master的fd了。新master进程拿到“NGINX”环境变量之后,会通过双指针的遍历操作解析出fd1,fd2 ... fdn,同时依次保存在ngx_listening_t类型的数组中,而ngx_listening_t是nginx特有的保存监听socket信息的结构体。当然,通过这种方式共享fd的前提是旧master进程提前打开了fd并持有文件锁。 熟悉Unix编程的读者都知道要描述一个套接字的基础信息包括了套接字句柄fd和监听的sockaddr地址。同样,这些都是ngx_listening_t里的重要成员,另外为了给用户更多配置上的自由,nginx还往其中添加了更多成员,包括指定监听时backlog队列,内核中对应该套接字的缓冲区大小rcvbuf与sndbuf,TCP建立成功之后的handler函数,当前套接字对应的连接信息ngx_connection_t等等,其中大部分是在ngx_set_inherited_sockets函数中设置的。 ## 4 ngx_init_cycle 这是nginx启动过程中最重要的一环,也是最复杂的一部分,这里面包含了创建目录,打开文件,初始化共享内存,解析配置文件,初始化各模块等操作。在了解这部分内容之前,读者需要了解一个名为ngx_cycle_t的结构体。 ngx_cycle_t是nginx最为核心的一个数据结构体,无论是master进程、worker进程还是cache manager进程都拥有唯一一个ngx_cycle_t结构体,正如它的名字一样,nginx的整个生命周期都是围绕着这个结构体来运行的。可想而知nginx在启动时,必定会创建这样一个结构体,下面让我们看看ngx_cycle_t的重要成员变量: ``` struct ngx_cycle_s { voidconf_ctx; //保存所有模块存储配置项结构体 ngx_pool_t*pool;//内存池 ngx_log_t*log; //日志对象 ngx_connection_t*free_connections; //可用连接池 ngx_uint_tfree_connection_n;//可以用连接池中的连接总数 ngx_module_t**modules;//保存nginx编译的所有模块 ngx_array_tlistening; //保存所有需要监听的端口 ngx_array_tpaths;//保存nginx要操作的目录 ngx_list_topen_files;//保存nginx要打开的文件 ngx_list_tshared_memory;//保存所有共享内存 ngx_uint_tconnection_n; //当前进程中所有连接的总数 ngx_connection_t*connections;//当前进程中所有的连接 ngx_event_t*read_events;//当前进程中所有读事件 ngx_event_t*write_events; //当前进程中所有写事件 …… }; ``` 当含有动态资源时,动静分离是一个常用的方案,对于静态资源我们可以利用Nginx充在ngx_cycle_t里,我们都能找到上文提到的ngx_log_t以及ngx_listening_t的影子。这里先对它有一个初步的印象,把目光专注在与启动流程相关最为密切的成员的两个成员上:conf_ctx和modules。 conf_ctx:可以看到它的类型是void,这是因为它是一个数组,其中的每个成员又是一个指针,每个指针又指向另一个存储着指针的数组。为什么设计的如此复杂,就是因为它保存着所有模块存储配置项的结构体信息,好似一个户口册,每家每户的信息都登记在这里,并且分配好了位置序号index。而每个模块会在自家的户口簿里(实际上是各自实现的存储配置的结构体)记录下自家所在的index序号。这样一来,nginx想知道任意一个模块的详细配置信息,只要拿着该模块的index序号,然后去户口册里查找就行,无需遍历整个户口册。 modules:顾名思义,它表示的就是目前编译进nginx的所有模块,它是一个指向元素类型为ngx_module_t的数组指针。ngx_module_t也是nginx中极其重要的结构体,它容纳了一个模块的所有信息,包括模块类型,模块指令,模块的通用接口,以及该模块在nginx整个声明周期里注册的各种回调函数,当然也有我们刚刚提到的“户口地址”index。这里可以简单看下它的原型摘要: struct ngx_module_s { /*该模块在同类型模块中的位置序号*/ ngx_uint_tctx_index; /*该模块在所有模块中的位置序号*/ ngx_uint_tindex; /*模块名称*/ char*name; /*模块签名*/ const char*signature; /*该模块的公共接口,它是ngx_module_t和所有模块的关系纽带*/ void*ctx; /*存储着它支持的指令集以及每条指令对应的handler处理方法*/ ngx_command_t*commands; /*模块类型*/ ngx_uint_ttype; /*模块在nginx整个声明周期里注册的7个回调函数*/ ngx_int_t(*init_master)(ngx_log_t *log); ngx_int_t(*init_module)(ngx_cycle_t *cycle); ngx_int_t(*init_process)(ngx_cycle_t *cycle); ngx_int_t(*init_thread)(ngx_cycle_t *cycle); void(*exit_thread)(ngx_cycle_t *cycle); void(*exit_process)(ngx_cycle_t *cycle); void(*exit_master)(ngx_cycle_t *cycle); …… }; 我对其中的ctx成员简单说明一下,读者可能不太能理解注释里所谓的“该模块的公共接口”,为什么说它是ngx_module_t和各类模块的关系纽带?前面我们提到,nginx对所有模块都进行了分类和分层,每类模块都有自己的特性,都实现了自己的特有的方法,那怎样能将各类模块都能和ngx_module_t这唯一的结构体关联起来呢,这时候通过一个void指针类型的ctx变量来进行接口抽象,同类型的模块只需要遵循这一套规范即可。这里拿核心模块和http模块举例说明: 对于核心模块,ctx指向的是名为ngx_core_module_t的结构体,这个结构体很简单,除了一个name成员就只有create_conf和init_conf两个方法,那么所有的核心模块都会去实现这两个方法,如果有一天nginx又创造了新的核心模块,那他也一定是按照ngx_core_module_t这个公共接口来实现。 typedef struct { ngx_str_tname; void*(*create_conf)(ngx_cycle_t *cycle); char*(*init_conf)(ngx_cycle_t *cycle, void *conf); } ngx_core_module_t;} 对于http模块,ctx就指向的是名为ngx_http_module_t的结构体,这个结构体里定义了8个通用的方法,分别是http模块在解析配置文件前后,以及创建、合并http段,server段和loc段配置时所调用的方法,如下所示: 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); void*(*create_loc_conf)(ngx_conf_t *cf); char*(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf); } ngx_http_module_t; 那么nginx在启动的时候,就可以根据当前的执行上下文来依次调用所有http模块里ctx所指定的某个方法。更重要的是对于一个开发者来说,只需要按照ngx_http_module_t里的接口规范实现自己想要的逻辑,这样不仅降低了开发成本,也增加了nginx模块的可扩展性和可维护性。 ngx_cycle_t和ngx_module_t的相关内容不是本章的重点,就先暂时讲解到这里,后续章节里会对它们的每个成员以及设计思想进行详细剖析。下面我们继续回到nginx的启动流程里的ngx_init_cycle()这里。 ### 4.1新旧cycle交接 前面提到了,在进入ngx_init_cycle之前nginx会先解析命令行参数,进行部分初始化操作,继承监听句柄等等,那么nginx会先创建一个临时的ngx_cycle_t用来保存这部分已经确定的信息。然后在进入ngx_init_cycle之后,nginx会创建新的ngx_cycle_t,此时的cycle才是真正的伴随着nginx进程一生的结构体。那么自然会有一个新旧cycle交接的过程,需要将old_cycle中已确认的成员信息赋值到新cycle里,对应的部分代码如下: ngx_cycle_t * ngx_init_cycle(ngx_cycle_t *old_cycle) { …… 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; } //依次将old_cycle中的log,conf_prefix,prefix,conf_file,conf_params信息赋值到新cycle中 cycle->pool = pool; cycle->log = log; cycle->old_cycle = old_cycle; cycle->conf_prefix.len = old_cycle->conf_prefix.len; cycle->conf_prefix.data = ngx_pstrdup(pool,&old_cycle->conf_prefix); if (cycle->conf_prefix.data == NULL) { ngx_destroy_pool(pool); return NULL; } cycle->prefix.len = old_cycle->prefix.len; cycle->prefix.data = ngx_pstrdup(pool, &old_cycle->prefix); if (cycle->prefix.data == NULL) { ngx_destroy_pool(pool); return NULL; } cycle->conf_file.len = old_cycle->conf_file.len; cycle->conf_file.data = ngx_pnalloc(pool, old_cycle->conf_file.len + 1); if (cycle->conf_file.data == 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 = ngx_pstrdup(pool, &old_cycle->conf_param) if (cycle->conf_param.data == NULL) { ngx_destroy_pool(pool); return NULL; 可能大家会有疑惑,为什么不在进入ngx_init_cycle之前直接将那部分信息放在新的cycle中,然后直接将新cycle作为参数传到ngx_init_cycle()函数里,这样不就省去了重复赋值的操作吗?我的猜想是nginx想把关于ngx_cycle_t的所有初始化操作都集中在ngx_init_cycle()里,作为整个启动流程中最重要的一步。但是在此之前又需要做一些前置操作,比如解析并保存配置参数,这时候不得不需要一个cycle来保存这些信息,于是就创建了一个临时的ngx_cycle_t,然后在ngx_init_cycle()里进行新旧cycle的交接工作。 ### 1.4.2容器初始化 细心的读者可能有注意到上面的ngx_cyele_t原型中有四个成员变量:listening, pathes, open_files, shared_memory,他们对应的数据类型是ngx_array_t和ngx_list_t,除此之外nginx大家族里还有很多类似的成员,比如ngx_queue_t, ngx_rbtree_t, ngx_pool_t等等,这些具有鲜明特征的结构体是nginx的一大亮点,nginx在c基础上封装了一层特有的ngx风格的数据结构,包括字符串,数组,链表,红黑树等常见容器,nginx对内存和效率的严苛都体现在了这些基础结构体的实现细节上。对于刚才提到的那四个变量信息都是存储在这样的容器中,那么在nginx的启动过程中,必定需要创建申请各种容器的内存并进行初始化,设置默认大小等。nginx的主框架会在解析完配置文件时,根据配置信息向各容器中添砖加瓦,例如每解析到一个listen指令,就会往listening对应数组容器中添加对应的sockaddr信息;每解析到一个error_log或access_log指令,就会往open_files对应的链表容器里添加打开的日志文件信息。关于容器初始化对应核心代码如下: //对于nginx所要操作的文件目录,初始化对应的动态数组容器,默认大小为10 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; } //初始化保存dump文件的动态数组容器 if (ngx_array_init(&cycle->config_dump, pool, 1, sizeof(ngx_conf_dump_t)) != NGX_OK) { ngx_destroy_pool(pool); return NULL; } //初始化保存sentinel的rbtree容器 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; } //对于nginx已经打开的文件,存储在open_files里,采用的是单链表容器 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; } // listening数组,在进入ngx_init_cycle之前可能已经通过继承赋值了,默认值为10,这里采用了动态数组容器 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)); ### 1.4.3核心模块creat_conf 这段源码逻辑十分简单,我们先简单浏览一遍: 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); if (rv == NULL) { ngx_destroy_pool(pool); return NULL; } cycle->conf_ctx[cycle->modules[i]->index] = rv; } } 从上面的代码段可以看到,这里nginx只做了两件事:一是遍历所有的模块,找到类型为NGX_CORE_MODULE的模块,即核心模块。目前官方定义的核心模块一共有6个:ngx_core_module, ngx_http_module, ngx_errlog_module, ngx_events_module, ngx_openssl_module,ngx_mail_module。二是调用核心模块的create_conf方法,并将返回的值保持在conf_ctx中该模块index对应的位置上。 前文我们有提到,对于核心模块的公共接口里都有一个create_conf方法,顾名思义就是创建存储配置项的结构体。从这段代码中可以看到,nginx主框架只关心核心模块的配置,那其他非核心模块怎么办呢?这时候就交给各核心模块去管理了,比如所有的http模块都由核心模块ngx_http_module管理,所有的event模块都由核心模块ngx_events_module管理。前文提到的网校的例子里,最高领导只用关心各部门leader,各部门leader负责管理该部门的员工们,也是一样的道理。 事实上,nginx会将所有编译的模块都保存在一个ngx_modules[]数组中,其中可能包含了上百个模块,但是核心模块只有6个,而实现了create_conf方法的又只有三个: ![1579423439993_E62538E3-3259-4e3e-875E-9D2B92071C62](https://img.kancloud.cn/6f/57/6f57d4eb8231b63e95f47fb4f06f5669_829x331.gif) 以我本地环境为例,此时的conf_ctx的内存分配如下图示,这点也是很容易理解的。 ![1579423507689_3E69AC80-7BB6-48f8-A4D0-4173955D9B25](https://img.kancloud.cn/8c/59/8c59b145aeb688a47562b0ba1e85b20c_830x249.gif) ### 1.4.4解析配置文件 走到这里,用于存储所有配置项的conf_ctx结构体已经创建完毕,万事俱备,只欠解析。这时候nginx就开始读取配置并解析文件nginx.conf: if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) { environ = senv; ngx_destroy_cycle_pools(&conf); return NULL; } 每次解析到一条指令,就会遍历所有的模块,找到对该指令感兴趣的模块,然后调用此模块对应于该指令的handler回调方法,同时会将解析到的配置值存储到conf_ctx中对应的结构体里。例如,当读取到“worker_connection 4;”这个条指令时,nignx首先会找到对它感兴趣的是核心模块ngx_core_module,而该模块对这个指令设置的handler方法是ngx_set_worker_processes函数,这里nginx会将ngx_core_conf_t结构体里的worker_processes成员设置为4,此时上图中的conf_ctx[0]里发生了变化。整个解析过程好比失物招领现场,每个配置项都有一个模块去认领,并且会在conf_ctx里记录下。 关于解析配置文件的代码这里仅仅只有5行,但事实上整个过程极其复杂,nginx如何识别一个配置项?我们最关心的http段的配置项如何解析和存储?遇到event,upstream这种块配置的时候如何处理?存在配置冲突时nginx该如何选择?很多问题值得我们细细探究,由于篇幅有限,本文不会深入展开,后续章节我会详细介绍nginx如何解析一份完整的配置文件。 ### 1.4.5核心模块init_conf 前文在介绍ngx_module_t结构体的时候提到,核心模块的ctx的成员中包含create_conf和init_conf两个方法,其中create_conf已经在前面全部执行了,那么这里nginx会遍历所有的模块,找出核心模块后调用其init_conf方法。因为在解析完配置文件之后,nginx发现有些配置项并没有写在配置文件中,于是会在这一步进行一些默认值的设置,例如在ngx_core_module的init_conf函数中,会设置daemon,shutdown_timeout, worker_processes等等默认配置。 ### 1.4.6创建目录与打开文件 在容器初始化中我们有提到nginx会根据配置信息往各容器中添砖加瓦,其中也包括了在各核心模块在执行create_conf和init_conf方法时对这些容器变量的修改。比如缓存模块会在ngx_cycle_t结构体的pathes动态数组和open_files添加需要打开的文件或者目录,那么在这里,nginx就会去检测各文件是否存在以及是否有读写权限,如果不存在,则会创建并打开对应的目录或文件,其中包括了以下类型的文件。 l临时文件 /home/nginx/client_body_temp :保存客户端POST请求的body /home/nginx/( proxy_temp |fastcgi_temp| uwsgi_temp| scgi_temp):保存后端Server返回的Response l访问日志文件 /home/nginx/logs/access.log l错误日志文件 /home/nginx/logs/error.log 同时,nginx也会对ngx_cycle_t的其他容器中已经添加过的成员进行处理,比如这里也会针对shared_memory链表开始初始化用于进程间通信的共享内存。 ### 1.4.7 socket创建与监听 之前在解析配置文件时,所有的模块已经解析出自己需要监听的端口,例如http模块已经在解析http{…}配置项时得到它想要监听的端口,并且添加了ngx_cycle_t的listening数组中。那么在这里,nginx就会按照listening数组里的每一个ngx_listening_t元素设置socket句柄并监听端口,这一步主要是通过ngx_open_listening_sockets函数实现的,该函数的核心摘要代码如下: ngx_int_t ngx_open_listening_sockets(ngx_cycle_t *cycle) { … for (tries = 5; tries; tries--) { /* for each listening socket */ ls = cycle->listening.elts; for (i = 0; i listening.nelts; i++) { //系统调用socket对象 s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); … //绑定sockaddr地址 if (bind(s, ls[i].sockaddr, ls[i].socklen) == -1) {…} // listen函数指定了backlog队列长度 if (listen(s, ls[i].backlog) == -1){…} } return NGX_OK; }} 这里我摘出了最核心的几行代码,熟悉网络编程的读者能一目了然的看到,这是一个标准的server端的实现流程了,包括了socket创建套接字描述符,bind绑定sockaddr的ip和port,listen指定同时连接的客户端的最大长度等等。同时这里可以看到为了提高容错性,nginx对于每个socket对象的创建和监听,都会有重试5次的重试机会,从代码TODO注释里也能看到,未来nginx打算将这个纳入配置选项,让用户有更大的自由来决定失败重试次数。 ### 1.4.8收尾工作 最后,nginx就会处理一些收尾性的工作了,比如调用所有模块的init_module方法来完成模块的初始化操作,释放不需要的内存,关闭旧master进程监听的socket和文件、销毁临时内存池等等。到这里整个ngx_init_cycle工作已经接近尾声了,nginx也在这里孕育出了一个全新的充满活力的ngx_cycle_t结构体,它将陪伴着nginx进程走过整个生命周期。 ## 5启动进程 到这里ngx_init_cycle函数已经完成了他的使命了,接下来回到nginx启动的主流程中,之后nginx将会根据配置的运行模式决定如何工作,这里篇幅有限,后面章节会详细叙述nginx如何创建各种进程,如何进入工作循环模式,如何监听网络事件等等。 ### 小结 本小节主要介绍了nginx的设计理念和思想,并拿网校进行类比,希望能给读者一个更贴近生活的感受。同时讲述了关于nginx启动流程中关键的几步,也简单介绍几个nginx的核心数据结构和设计思想,关于nginx解析配置文件和启动进程过程会放在后续章节讲解。 (文/张报编/刘宣麟)