在分析nginx源码时,对于nginx启动过程总是感觉模棱两可,有些混乱。因此以博客方式对启动流程做个总结。nginx启动流程代码比较多,不可能陷入到源码细节中。这里不会对源码细节进行过渡分析,而是对于整体脉络的梳理。整体框架清晰后,源码的细节则留给读者去分析了。
对于nginx的启动流程,准备用6篇文章来分析。
<1>、nginx启动流程之master进程初始化
<2>、nginx启动流程之work进程初始化
<3>、nginx配置解析之缓冲区管理
<4>、nginx配置解析流程
<5>、nginx配置解析之配置合并
<6>、nginx监听socket流程分析
一、基础结构初始化
1、获取系统错误码以及对应的描述信息
ngx_strerror_init函数会开辟135个空间,存放系统的错误码以及对应的字符串形式的描述信息。在nginx进程运行过程中,如果出错就可以调用gx_log_errno函数获取出错的错误码以及对应的字符串内容,方便排错。
2、解析nginx启动过程中的命令下参数。
例如以下几种方式 ./nginx -s reload; ./nginx -v ./nginx -c /usr/local/nginx/conf/nginx.conf运行nginx时,函数ngx_get_options将负责解析nginx的命令下参数,并给几个全局变量打上标记。
3、系统调用获取当前时间,并缓存起来
获取系统时间的目的: 在打印日志时,要显示出打印日期,这日期是从日志时间缓存中读取的; http响应头部的date日期时间,是从http时间缓存读取的。
//缓存事件初始化
void ngx_time_init(void)
{
//初始化几种当前时间的字符串长度
ngx_cached_err_log_time.len = sizeof("1970/09/28 12:00:00") - 1;
ngx_cached_http_time.len = sizeof("Mon, 28 Sep 1970 06:00:00 GMT") - 1;
ngx_cached_http_log_time.len = sizeof("28/Sep/1970:12:00:00 +0600") - 1;
ngx_cached_http_log_iso8601.len = sizeof("1970-09-28T12:00:00+06:00") - 1;
ngx_cached_time = &cached_time[0];
//更新缓存时间
ngx_time_update();
}
ngx_cached_err_log_time等几种变量为当前时间的不同表现形式。
ngx_time_init函数进而调用ngx_time_update函数,获取64个缓存时间。为什么需要缓存64个时间呢? 为了避免分裂读,即某worker进程读时间缓存过程中接受中断请求,在这期间,时间缓存被其他worker进程更新,导致前后读取时间不一致。因此nginx引入时间缓存数组(共64个成员),每次都更新数组中的下一个元素;读取时间缓存时,也是读取最新的时间,从而导致时间读写一致性。
void ngx_time_update(void)
{
//获取当前系统时间
ngx_gettimeofday(&tv);
ngx_current_msec = (ngx_msec_t) sec * 1000 + msec;
tp = &cached_time[slot];
//比较当前slot与刚刚计算出来的时间,若相同则保存毫秒时间,然后返回。
if (tp->sec == sec)
{
tp->msec = msec;
ngx_unlock(&ngx_time_lock);
return;
}
//slot数共64个,循环使用,也就是可以保存64个缓存时间
if (slot == NGX_TIME_SLOTS - 1)
{
slot = 0;
}
else
{
//使用下一个slot存当前时间
slot++;
}
//将当前时间放到新的slot中
tp = &cached_time[slot];
tp->sec = sec;
tp->msec = msec;
ngx_gmtime(sec, &gmt);
//以下几个操作po,p1, p2, p3,都是把当前缓存时间赋值给对应不同形式的变量保存
p0 = &cached_http_time[slot][0];
(void) ngx_sprintf(p0, "%s, %02d %s %4d %02d:%02d:%02d GMT",
week[gmt.ngx_tm_wday], gmt.ngx_tm_mday,
months[gmt.ngx_tm_mon - 1], gmt.ngx_tm_year,
gmt.ngx_tm_hour, gmt.ngx_tm_min, gmt.ngx_tm_sec);
ngx_localtime(sec, &tm);
cached_gmtoff = ngx_timezone(tm.ngx_tm_isdst);
tp->gmtoff = cached_gmtoff;
p1 = &cached_err_log_time[slot][0];
(void) ngx_sprintf(p1, "%4d/%02d/%02d %02d:%02d:%02d",
tm.ngx_tm_year, tm.ngx_tm_mon,
tm.ngx_tm_mday, tm.ngx_tm_hour,
tm.ngx_tm_min, tm.ngx_tm_sec);
p2 = &cached_http_log_time[slot][0];
(void) ngx_sprintf(p2, "%02d/%s/%d:%02d:%02d:%02d %c%02d%02d",
tm.ngx_tm_mday, months[tm.ngx_tm_mon - 1],
tm.ngx_tm_year, tm.ngx_tm_hour,
tm.ngx_tm_min, tm.ngx_tm_sec,
tp->gmtoff < 0 ? '-' : '+',
ngx_abs(tp->gmtoff / 60), ngx_abs(tp->gmtoff % 60));
p3 = &cached_http_log_iso8601[slot][0];
(void) ngx_sprintf(p3, "%4d-%02d-%02dT%02d:%02d:%02d%c%02d:%02d",
tm.ngx_tm_year, tm.ngx_tm_mon,
tm.ngx_tm_mday, tm.ngx_tm_hour,
tm.ngx_tm_min, tm.ngx_tm_sec,
tp->gmtoff < 0 ? '-' : '+',
ngx_abs(tp->gmtoff / 60), ngx_abs(tp->gmtoff % 60));
//将当前的slot时间赋给ngx_cached_time
ngx_cached_time = tp;
//将上面集中不同形式的缓存时间,保存到响应变量中。
ngx_cached_http_time.data = p0;
ngx_cached_err_log_time.data = p1;
ngx_cached_http_log_time.data = p2;
ngx_cached_http_log_iso8601.data = p3;
}
还有一个问题? 那什么时候会更新缓存时间呢? 更新时机:1、进程启动时会更新一次,也就是调用ngx_time_init函数; 2;在函数ngx_epoll_process_events调用epoll_wait返回时有可能有会更新。
static ngx_int_t ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
......................
//等待事件返回
events = epoll_wait(ep, event_list, (int) nevents, timer);
err = (events == -1) ? ngx_errno : 0;
//更新缓存时间
if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm)
{
ngx_time_update();
}
.......................
}
4、函数ngx_process_options保存nginx配置的绝对路径,nginx安装目录等到ngx_cycle_t对应变量中
//处理可选项,存储nginx安装后的根目录、配置文件的目录、配置文件的绝对路径
static ngx_int_t ngx_process_options(ngx_cycle_t *cycle)
{
//保存/usr/local/nginx/根目录
if (ngx_prefix)
{
len = ngx_strlen(ngx_prefix);
p = ngx_prefix;
cycle->conf_prefix.len = len;
cycle->conf_prefix.data = p; //nginx.conf的目录/usr/local/nginx/conf/
cycle->prefix.len = len;
cycle->prefix.data = p; //为nginx安装后的根目录,默认为/usr/local/nginx/
}
//保存/usr/local/nginx/conf/nginx.conf路径
if (ngx_conf_file)
{
cycle->conf_file.len = ngx_strlen(ngx_conf_file);
cycle->conf_file.data = ngx_conf_file;//nginx.conf的绝对路径,例如/usr/local/nginx/conf/nginx.conf
}
//确包nginx.conf为一个绝对路径,如果不是绝对路径,则加上前缀变成一个绝对路径
if (ngx_conf_full_name(cycle, &cycle->conf_file, 0) != NGX_OK)
{
return NGX_ERROR;
}
//保存配置参数
if (ngx_conf_params)
{
cycle->conf_param.len = ngx_strlen(ngx_conf_params);
cycle->conf_param.data = ngx_conf_params; //使用-g选项指定新的参数,替换nginx.conf中的参数
}
return NGX_OK;
}
5、nginx平滑升级时,不需要关闭旧进程监听的所有套接字,新进程直接继承旧进程已经打开的socket
问题1:为什么要继承旧进程已经打开的socket呢?
如果新的进程启动后,不继承老进程已经打开的所有监听socket, 则新进程将执行创建socket, 绑定socket, 监听socket等一系列操作。
在监听完成前,无法处理来自客户端的连接请求。而继承后,新进程直接可以使用老进程的所有监听套接字,无需再执行一系列重复操作。
函数ngx_add_inherited_sockets就了为了完成从老nginx进程继承所有已经监听的socket过程。
问题2:旧的master进程什么时候保存所有监听的socket到环境变量中
当旧的master进程收到NGX_CHANGEBIN_SIGNAL信号,也就是USR2信号时,在信号处理函数ngx_signal_handler中,会将ngx_change_binary设置为1,表示需要进行平滑升级。
void ngx_signal_handler(int signo)
{
......
ngx_change_binary = 1;
......
}
而在master进程处理函数ngx_master_process_cycle中,会进行平滑升级操作
//平滑升级nginx
if (ngx_change_binary)
{
ngx_change_binary = 0;
ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv);
}
//平滑升级处理过程,会创建一个子进程,使用这个子进程替换为master进程,从而实现平滑升级
//升级前master进程会将自己监听的所有描述符放入到环境变量中,新的master进程会从环境变量读取这些socket。
//返回值;新master进程的pid文件
ngx_pid_t ngx_exec_new_binary(ngx_cycle_t *cycle, char *const *argv)
{
var = ngx_alloc(sizeof(NGINX_VAR)
+ cycle->listening.nelts * (NGX_INT32_LEN + 1) + 2,
cycle->log);
//将所有监听的描述符加入到环境变量中,格式为NGINX="fd1:fd2:fd3"
p = ngx_cpymem(var, NGINX_VAR "=", sizeof(NGINX_VAR));
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++)
{
p = ngx_sprintf(p, "%ud;", ls[i].fd);
}
env[n++] = var;
//重新创建子进程,创建完成后,会使用子进程替换master进程,从而实现平滑升级
pid = ngx_execute(cycle, &ctx);
//返回新的master进程id
return pid;
}
问题3:新进程如何继承旧进程所有打开的监听socket?
新旧进程使用环境变量方式进行通信,旧进程会将自己所有已经处理监听状态的socket写入到环境变量中。新进程就可以从环境变量中读取到旧进程已经处于监听状态的所有套接字。环境变量为:NGINX, 格式为:NGINX=fd1:fd2:fd3,其它fd1,fd2,fd3为已经打开的socket。例如"NGINX=101:102:103"
//将监听的ip与端口保存到监听数组中
static ngx_int_t ngx_add_inherited_sockets(ngx_cycle_t *cycle)
{
//读取环境变量值,格式为"NGINX=100:101:102:103",
//其中100,101,102, 103,104表示上一个nginx进程监听的套接字fd
inherited = (u_char *) getenv(NGINX_VAR);
//初始化新master进程的监听数组
ngx_array_init(&cycle->listening, cycle->pool, 10,sizeof(ngx_listening_t))
//从环境变量中读取旧master进程监听的所有fd,并将其加入到新master进程的监听数组中
for (p = inherited, v = p; *p; p++)
{
if (*p == ':' || *p == ';')
{
s = ngx_atoi(v, p - v);
v = p + 1;
ls = ngx_array_push(&cycle->listening);
ls->fd = (ngx_socket_t) s;
}
}
//新的master进程继承旧master进程监听的所有fd
ngx_inherited = 1;
//设置监听套接字选项
return ngx_set_inherited_sockets(cycle);
}
问题4:ngx_add_inherited_sockets函数只是将将老进程所有监听socket保存到ngx_cycle_t中的listening数组中。那什么时候使用这个listening数组呢?
在ngx_init_cycle函数中,会使用这个listening数组,直接使用老进程已经打开的监听socket。
//将监听数组中的所有open设置为1,表示监听开始
if (old_cycle->listening.nelts)
{
ls = old_cycle->listening.elts;
for (i = 0; i < old_cycle->listening.nelts; i++)
{
ls[i].remain = 0;//将老进程所有监听socket标记为要关闭状态(不一定会关闭,只是一个初值,下面进行是否关闭判断)
}
//对应新进程的每一个socket,都在老进程中查找。如果找到,说明新旧进程都需要监听同一个socket,因为旧进程已经打开并处于监听状态,因此新进程直接使用老进程的fd,不需要在打开socket操作
nls = cycle->listening.elts;
for (n = 0; n < cycle->listening.nelts; n++)
{
for (i = 0; i < old_cycle->listening.nelts; i++)
{
if (ls[i].ignore)
{
continue;
}
//查找到新老进程都监听同一个socket
if (ngx_cmp_sockaddr(nls[n].sockaddr, ls[i].sockaddr) == NGX_OK)
{
nls[n].fd = ls[i].fd; //直接使用老进程的fd
nls[n].previous = &ls[i]; //构造链表,新进程指向老进程
ls[i].remain = 1; //值为1表示使用原有的ngx_cycle_t来初始化新的ngx_cycle_t结构,不关闭原先打开的监听端口,这对运行中升级程序很有用。
}
}
if (nls[n].fd == -1)
{
nls[n].open = 1;
}
}
}
问题5: 新的nginx进程继承了旧进程所有已经处于监听状态的socket。 但如果新nginx进程不需要监听某个socket时,怎么处理?
新nginx直接通过ngx_listening_s中的remain标记从而获取是否需要关闭旧nginx监听的socket。remain值为1时表示需要使用旧进程监听的socket,不应该关闭; 否则应该关闭。
在ngx_init_cycle函数中可以看到这个关闭逻辑。
ls = old_cycle->listening.elts;
for (i = 0; i < old_cycle->listening.nelts; i++)
{
//仍然需要使用旧进程的socket,则直接跳过
if (ls[i].remain || ls[i].fd == -1)
{
continue;
}
//不需要使用旧进程的socket,则关闭
ngx_close_socket(ls[i].fd);
}
6、nginx模块数组中,每一个模块在数组都有一个下标。下面这个代码段就是给所有nginx模块一个下标。其中ngx_max_module只的是当前nginx一共有多少个模块。
//初始化各个模块在数组中的索引
ngx_max_module = 0;
for (i = 0; ngx_modules[i]; i++)
{
ngx_modules[i]->index = ngx_max_module++;
}
二、ngx_init_cycle初始化流程。
master进程,每一个work进程都有各自唯一的一个ngx_cycle_t结构。该结构记录了nginx所有的客户端连接、读写事件、各个nginx模块的上下文句柄、配置文件路径以及所有监听socket。函数ngx_init_cycle就是用来初始化ngx_cycle_t中的各个成员。同时在该函数内也会对nginx.conf配置文件进行解析。接下来分析下这个函数的处理流程。
1、创建一个ngx_cycle_t对象
//创建新cycle
cycle = ngx_pcalloc(pool, sizeof(ngx_cycle_t));
if (cycle == NULL)
{
ngx_destroy_pool(pool);
return NULL;
}
2、保存配置路径的路径、参数路径、nginx的前缀等参数到相应变量中。
3、为nginx目录数组开辟空间。这个数组记录了nginx需要创建的所有目录。
为什么需要这些目录?nginx进程日志文件、缓存文件等都需要保持到相应的目录。而这个目录数组记录了要创建的所有目录。
目录数组中的目录从哪里来?在接下来进行nginx.conf配置文件时,会将需要创建的所有目录信息保持到这个数组中。这部分内容可以参考nginx配置解析
//为新cycle创建目录数组
cycle->pathes.elts = ngx_pcalloc(pool, n * sizeof(ngx_path_t *));
if (cycle->pathes.elts == NULL)
{
ngx_destroy_pool(pool);
return NULL;
}
4、nginx使用链表存放所有已经打开的文件,例如日志文件。同样,在解析nginx.conf配置文件时,会将所有要打开的文件保存到链表中。
//创建已经打开文件链表
if (ngx_list_init(&cycle->open_files, pool, n, sizeof(ngx_open_file_t))
!= NGX_OK)
{
ngx_destroy_pool(pool);
return NULL;
}
5、创建共享内存空间。在work进程间通信时,会使用到共享内存。例如引用计数accep-count记录了nginx进程已经连接了多少客户端。这个变量就放到了共享内存,每一个work进程接收客户端连接时,都会将这个变量值加1.
//创建共享内存链表
if (ngx_list_init(&cycle->shared_memory, pool, n, sizeof(ngx_shm_zone_t))
!= NGX_OK)
{
ngx_destroy_pool(pool);
return NULL;
}
6、创建监听数组,存放nginx将监听的所有socket,也就是"ip:port"。这个数组中的内容从何而来?一方面像前面讲的,从旧进程继承而来。另一方面,在解析nginx.conf配置文件时,会解析listen, server配置项,获取到nginx服务器监听的所有端口,以及虚拟主机名,从而保存到监听数组中。
//为新cycle创建监听数组
cycle->listening.elts = ngx_pcalloc(pool, n * sizeof(ngx_listening_t));
if (cycle->listening.elts == NULL)
{
ngx_destroy_pool(pool);
return NULL;
}
7、创建所有核心模块的上下文句柄。这个句柄存放了这个核心模块关心的nginx.conf配置内容。例如ngx_core_module核心模块的上下文结构为ngx_core_conf_t。那非核心模块,例如事件模块、http模块、upstream负载均衡模块,这些非核心模块什么时候创建上下文句柄呢? 在解析nginx.conf配置时,将会创建这些上下文句柄。这些先不讲,在解析nginx.conf配置时在详细描述。
//将每一个核心模块上下文create_conf的返回值存入到conf_ctx中
for (i = 0; ngx_modules[i]; i++)
{
if (ngx_modules[i]->type != NGX_CORE_MODULE)
{
continue;
}
module = ngx_modules[i]->ctx;
if (module->create_conf)
{
//目前只有ngx_core_module模块的上下文实现了ngx_core_module_create_conf方法
rv = module->create_conf(cycle);
if (rv == NULL)
{
ngx_destroy_pool(pool);
return NULL;
}
cycle->conf_ctx[ngx_modules[i]->index] = rv; //将上下文句柄保存到对应模块
}
}
8、调用ngx_conf_parse函数开始解析nginx.conf配置文件。将解析后的配置文件保存到各个模块的上下文句柄中。参考解析配置文件流程。
9. 在解析完配置文件后,如果核心模块中所关心的配置值在配置文件中没有配置,则nginx将会为这个配置项赋值一个默认值。
//调用所有核心模块的init_conf方法,目的是给没有解析到的配置项赋默认值
for (i = 0; ngx_modules[i]; i++)
{
if (ngx_modules[i]->type != NGX_CORE_MODULE) //只处理核心模块
{
continue;
}
module = ngx_modules[i]->ctx;
//目前只有ngx_core_module模块的上下文实现了ngx_core_module_init_conf方法
if (module->init_conf)
{
if (module->init_conf(cycle, cycle->conf_ctx[ngx_modules[i]->index])
== NGX_CONF_ERROR)
{
environ = senv;
ngx_destroy_cycle_pools(&conf);
return NULL;
}
}
10、调用ngx_create_pidfile函数创建一个master进程id文件,存放master进程的进程id。这个文件有什么作用? 平滑升级时,新启动的nginx进程可以读取这个配置文件获取到旧master进程的id, 从而向旧master进程发送消息。
11、在解析完nginx.conf配置后,会将所有需要创建的目录保存到cycle->pathes数组中,接下来将创建数组中的所有目录。
//创建所有目录
if (ngx_create_pathes(cycle, ccf->user) != NGX_OK)
{
goto failed;
}
12、同样,在解析完nginx.conf配置文件后,会将所有需要打开的文件存放到cycle->open_files打开文件链表中。接下来将遍历这个链表,打开链表中的所有文件。
13、在解析完nginx.conf配置文件后,会把所有ip:port组成的套接字加入到cycle的监听数组中。接下来就需要对监听数组中所有的ip:port执行创建套接字,绑定套接字,监听套接字操作。ngx_open_listening_sockets函数将执行创建套接字,绑定套接字,监听套接字三个过程。
//创建监听数组中的所有socket
ngx_int_t ngx_open_listening_sockets(ngx_cycle_t *cycle)
{
//对监听数组的所有ip:port对创建socket
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++)
{
if (ls[i].ignore)
{
continue;
}
if (ls[i].fd != -1) //套接字已经创建,则忽略
{
continue;
}
//创建套接字
s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0);
//绑定套接字
bind(s, ls[i].sockaddr, ls[i].socklen)
//监听套接字
(listen(s, ls[i].backlog);
ls[i].listen = 1;
ls[i].fd = s;
}
return NGX_OK;
}
在master进程执行了创建套接字,绑定套接字,监听套接字三个操作后,就可以处理来自客户端的连接了。但有一个问题,master进程相当于一个管理进程,用来管理所有work进程。实际上处理客户端连接请求是work进程的功能。现在master进程也创建了监听socket,岂不是也可以处理来自客户端连接???答案是,但只有nginx进程处于单进程模式才会监听客户端的连接,也就是nginx只有一个进程,既充当master模式,也充当work模式。ngx_single_process_cycle就是这样的单进程处理逻辑。ngx_single_process_cycle
---->ngx_process_events_and_timers
----->ngx_trylock_accept_mutex
ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
//获取到锁
if (ngx_shmtx_trylock(&ngx_accept_mutex))
{
//将所有监听fd加入监听事件中(加入epoll)
if (ngx_enable_accept_events(cycle) == NGX_ERROR)
{
ngx_shmtx_unlock(&ngx_accept_mutex);
return NGX_ERROR;
}
return NGX_OK;
}
//未获取到锁,应该把所有监听连接都关闭
if (ngx_disable_accept_events(cycle) == NGX_ERROR)
{
return NGX_ERROR;
}
return NGX_OK;
}
而在master-work进程模型中,master进程是不会处理来自客户端连接的,因为这些socket没有加入到epoll中。
14、接下来将会调用所有模块的init_module方法,用来初始化模块。不过大部分模块都没有实现这个方法。ngx_event_core_module的init module方法为ngx_event_module_init
//调用所有模块的init_module方法
for (i = 0; ngx_modules[i]; i++)
{
if (ngx_modules[i]->init_module)
{
if (ngx_modules[i]->init_module(cycle) != NGX_OK)
{
/* fatal */
exit(1);
}
}
}
到目前为止,ngx_init_cycle函数的整体上已经分析完成了。下面以一张思维导图来总结ngx_init_cycle函数的初始化流程。
三、master进程信号处理
接下里nginx将会注册各个信号处理函数。master进程接收来自外部信号,例如./nginx -s stop, ./nginx -t 等。master进程收到信号后,信号处理函数被调用,执行相应操作。nginx使用表驱动算法,使用数组存放所有需要处理的信号。
ngx_signal_t signals[] =
{
{ ngx_signal_value(NGX_RECONFIGURE_SIGNAL),
"SIG" ngx_value(NGX_RECONFIGURE_SIGNAL),
"reload",
ngx_signal_handler },
{ ngx_signal_value(NGX_REOPEN_SIGNAL),
"SIG" ngx_value(NGX_REOPEN_SIGNAL),
"reopen",
ngx_signal_handler },
{ 0, NULL, "", NULL }
};
//注册各种信号处理函数
ngx_int_t ngx_init_signals(ngx_log_t *log)
{
ngx_signal_t *sig;
struct sigaction sa;
for (sig = signals; sig->signo != 0; sig++)
{
ngx_memzero(&sa, sizeof(struct sigaction));
sa.sa_handler = sig->handler;
sigemptyset(&sa.sa_mask);
if (sigaction(sig->signo, &sa, NULL) == -1)
{
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
"sigaction(%s) failed", sig->signame);
return NGX_ERROR;
}
}
return NGX_OK;
}
在有信号发生时,将会调用ngx_signal_handler函数,对信号进行处理。而ngx_signal_handler处理函数收到各种信号后,只会给相应变量打上标记,之后master事件循环检测到这个标记后,将进行实际的信号处理。这也体现了信号处理函数只处理简单事件逻辑思想。
void ngx_signal_handler()
{
switch (signo)
{
case ngx_signal_value(NGX_SHUTDOWN_SIGNAL)://如果接受到quit信号,则准备退出进程
ngx_quit = 1;
action = ", shutting down";
break;
case ngx_signal_value(NGX_TERMINATE_SIGNAL)://sigint信号,TERM信号
case SIGINT:
ngx_terminate = 1;
action = ", exiting";
break;
}
}
而master进程的事件循环ngx_master_process_cycle,将循环检测各个变量是否被打上标记。如果打上标记,说明信号发生了,执行相应的相应处理逻辑。
void ngx_master_process_cycle(ngx_cycle_t *cycle)
{
for ( ;; )
{
//大部分时间都挂起进程,等待信号
sigsuspend(&set);
//检测到子进程退出信号
if (ngx_reap)
{
//所有子进程都已经退出,则live为0,否则为1
live = ngx_reap_children(cycle);
}
//master进程收到quit信号,向所有子进程发送信号
if (ngx_quit)
{
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
continue;
}
//收到重新打开所有文件信号
if (ngx_reopen)
{
ngx_reopen_files(cycle, ccf->user);
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_REOPEN_SIGNAL));
}
//平滑升级nginx
if (ngx_change_binary)
{
ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv);
}
}
}
四、master进程事件循环
1、接下来nginx将创建master进程的pid文件,有什么意义呢?如果要平滑升级,新的nginx进程可以读取就master进程的pid文件,进而获取旧master进程的进程id, 从而向旧master进程发送信号。
//创建进程id文件
if (ngx_create_pidfile(&ccf->pid, cycle->log) != NGX_OK)
{
return 1;
}
2、接来下会判断是否以单进程方式运行nginx,还是以master--work进程方式运行nginx。 由于单进程模式比较简单,既充当master进程也充当work进程角色,一般用于调试环境,单进程方式的函数入口为ngx_single_process_cycle。这里以重点分析master--work进程的工作方式,函数入口为ngx_master_process_cycle
3、在master-work工作模式下,会根据nginx.conf的配置,从而知道需要创建多少个work进程。ngx_start_worker_processes函数就是用来创建work进程。函数内部会把创建好的work进程保存到work进程数组中。这样master进程知道所有的的work进程,方便对work进程统一管理。work进程创建完成后,master进程将通过管道或者信号方式通知其它所有的work进程,让其它work进程也知道多了一个新的work进程,方便用于work进程直接的通信。
static void ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)
{
ngx_int_t i;
ngx_channel_t ch;
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "start worker processes");
ch.command = NGX_CMD_OPEN_CHANNEL;
for (i = 0; i < n; i++)
{
//指定work进程绑定哪个cpu
cpu_affinity = ngx_get_cpu_affinity(i);
//创建work进程
ngx_spawn_process(cycle, ngx_worker_process_cycle, NULL,
"worker process", type);
//work进程数组
ch.pid = ngx_processes[ngx_process_slot].pid;
ch.slot = ngx_process_slot;
ch.fd = ngx_processes[ngx_process_slot].channel[0];
//通知所有work进程开启读管道[1]
ngx_pass_open_channel(cycle, &ch);
}
}
而函数ngx_spawn_process会在work进程表中查找可用空间,之后创建一个work进程,并把work进程的pid存放到work进程表中。函数内部同时会创建一个读写管道,master进程使用channel[0]进行写操作,而work进程使用channel[1]进行读操作,实现master-work进程通信。
ngx_pid_t ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
char *name, ngx_int_t respawn)
{
//在进程表中查找可用空间
for (s = 0; s < ngx_last_process; s++)
{
if (ngx_processes[s].pid == -1)
{
break;
}
}
if (respawn != NGX_PROCESS_DETACHED)
{
//创建读写管道,基于tcp方式。master进程使用channel[0]进行写操作
//work进程使用channel[1]进行读操作,实现master-work进程通信
if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1)
{
return NGX_INVALID_PID;
}
//设置写管道为非阻塞模式
if (ngx_nonblocking(ngx_processes[s].channel[0]) == -1)
{
return NGX_INVALID_PID;
}
//设置读管道为非阻塞模式
if (ngx_nonblocking(ngx_processes[s].channel[1]) == -1)
{
return NGX_INVALID_PID;
}
}
ngx_process_slot = s;
pid = fork(); //创建work进程
switch (pid)
{
case -1: //错误
return NGX_INVALID_PID;
case 0: //子进程处理逻辑
ngx_pid = ngx_getpid();
proc(cycle, data);
break;
default: //父进程处理逻辑
break;
}
//将新创建的work进程保存到进程表中
ngx_processes[s].pid = pid;
ngx_processes[s].exited = 0;
ngx_processes[s].proc = proc;
ngx_processes[s].data = data;
ngx_processes[s].name = name;
ngx_processes[s].exiting = 0;
//更新进程表的下标
if (s == ngx_last_process)
{
ngx_last_process++;
}
return pid;
}
master进程创建完work进程后,会向channel[0]写入数据,通知work进程打开channel[1]的读管道操作。这样master打开了channel[0]写管道,work进程打开了channel[1]读管道。之后master-work进程就可以使用管道进行通信了。
//通知所有work进程开启读管道channel[1]
static void ngx_pass_open_channel(ngx_cycle_t *cycle, ngx_channel_t *ch)
{
for (i = 0; i < ngx_last_process; i++)
{
if (i == ngx_process_slot
|| ngx_processes[i].pid == -1
|| ngx_processes[i].channel[0] == -1)
{
continue;
}
//master进程向写管道[0]写入数据,通知work进程打开读管道
ngx_write_channel(ngx_processes[i].channel[0],
ch, sizeof(ngx_channel_t), cycle->log);
}
}
最后master进程会进入一个for循环,挂起在信号中。mater进程等待来自外部信号(例如./nginx -s reload),或者来自work进程的信号(work进程异常退出,master进程将重新创建一个work进程),收到信号后,master进程将被唤醒,进而执行相应处理逻辑。
void ngx_master_process_cycle(ngx_cycle_t *cycle)
{
{
//超时等待子进程退出,如果超时子进程还没有退出,则发送sigkill信号给子进程强制退出。
//超时时如果子进程已退出,我们父进程就直接退出
if (delay)
{
//启动定时器,延迟关闭
itv.it_value.tv_sec = delay / 1000;
itv.it_value.tv_usec = (delay % 1000 ) * 1000;
if (setitimer(ITIMER_REAL, &itv, NULL) == -1)
{
}
}
//大部分时间都挂起进程,等待信号
sigsuspend(&set);
ngx_time_update();
//检测到子进程退出信号
if (ngx_reap)
{
ngx_reap = 0;
//所有子进程都已经退出,则live为0,否则为1
live = ngx_reap_children(cycle);
}
//如果所有work进程都已经退出了,则结束master进程
if (!live && (ngx_terminate || ngx_quit))
{
ngx_master_process_exit(cycle);
}
//master进程收到终止信号,向所有子进程发送退出信号NGX_TERMINATE_SIGNAL。
//目的是为了使得work进程还能对已经连接的客户端进行处理
//延迟一段时间在向所有work进程发送SIGKILL强制关闭信号,而不管work进程是否还在处理客户端的数据
if (ngx_terminate)
{
sigio = ccf->worker_processes + 2 /* cache processes */;
if (delay > 1000)
{
ngx_signal_worker_processes(cycle, SIGKILL);
}
else
{
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_TERMINATE_SIGNAL));
}
continue;
}
//master进程收到quit信号,向所有子进程发送信号
if (ngx_quit)
{
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
//关闭所有的监听端口
ls = cycle->listening.elts;
for (n = 0; n < cycle->listening.nelts; n++)
{
ngx_close_socket(ls[n].fd)
}
cycle->listening.nelts = 0;
continue;
}
//master进程收到重新读取nginx.conf配置信号。nginx不会让原先的work子进程重新读取配置。
//而是重新初始化ngx_cycle_t结构,用它来读取新配置。最后在创建新的work进程,同时销毁旧的work进程
if (ngx_reconfigure)
{
ngx_reconfigure = 0;
//初始化一个新的cycle,读取新的nginx.conf配置
cycle = ngx_init_cycle(cycle);
ngx_cycle = cycle;
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,
ngx_core_module);
//创建新的work进程
ngx_start_worker_processes(cycle, ccf->worker_processes,
NGX_PROCESS_JUST_RESPAWN);
live = 1;
//销毁旧的work进程
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
}
//收到重新打开所有文件信号,将把文件缓存中的数据保存到文件中,然后重新打开文件
if (ngx_reopen)
{
ngx_reopen = 0;
ngx_reopen_files(cycle, ccf->user);
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_REOPEN_SIGNAL));
}
//平滑升级nginx,会重新创建一个master进程,替换旧的master进程
if (ngx_change_binary)
{
ngx_change_binary = 0;
ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv);
}
//接收到停止连接信号,向work进程发送信号,优雅关闭服务。同时停止接收新的连接
if (ngx_noaccept)
{
ngx_noaccept = 0;
ngx_noaccepting = 1;
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
}
}
}
函数ngx_signal_worker_processes负责master进程与work进程的通信。 函数内部有两种方式向work进程发消息;如果是结束work进程或者打开文件操作,则使用管道向work进程发送消息;如果是其它消息,则直接使用kill向work进程发送信号。而master-work进程通信的详细过程,则留到work进程初始化章节进行分析。
到此为止,master进程的初始化流程已经分析完成了。下面以一张思维导图来总结master进程的初始化流程。