Nginx进程管理
1. Nginx进程管理之master进程
监控进程充当整个进程组与用户的交互接口,同时对进程进行监护。它不需要处理网络事件,不负责业务的执行,只会通过管理worker进程来实现重启服务、平滑升级、更换日志文件、配置文件实时生效等功能。
master进程全貌图(来自阿里集团数据平台博客):
master进程中for(::)无限循环内有一个关键的sigsuspend()函数调用,该函数调用是的master进程的大部分时间都处于挂起状态,直到master进程收到信号为止。
master进程通过检查一下7个标志位来决定ngx_master_process_cycle方法的运行:
sig_atomic_t ngx_reap;
sig_atomic_t ngx_terminate;
sig_atomic_t ngx_quit;
sig_atomic_t ngx_reconfigure;
sig_atomic_t ngx_reopen;
sig_atomic_t ngx_change_binary;
sig_atomic_t ngx_noaccept;
进程中接收到的信号对Nginx框架的意义:
信号 | 对应进程中的全局标志位变量 | 意义 |
QUIT | ngx_quit | 优雅地关闭整个服务 |
TERM或INT | ngx_terminate | 强制关闭整个服务 |
USR1 | ngx_reopen | 重新打开服务中的所有文件 |
WINCH | ngx_noaccept | 所有子进程不再接受处理新的连接,实际相当于对所有子进程发送QUIT信号 |
USR2 | ngx_change_binary | 平滑升级到新版本的Nginx程序 |
HUP | ng_reconfigure | 重读配置文件 |
CHLD | ngx_reap | 有子进程以外结束,需要监控所有子进程 |
还有一个标志位会用到:ngx_restart,它仅仅是在master工作流程中作为标志位使用,与信号无关。
我们知道在main函数中完成了Nginx启动初始化过程,启动初始化过程中的一个重要环节就是解析配置文件,回调各个配置指令的回调函数,因此完成了各个模块的配置及相互关联。在所有的这些重要及不重要的初始化完成后,main函数就开始为我们打开进程的“大门”——调用ngx_master_process_cycle(cycle); 接下来的文字里面,我们就重点来看看这个函数里做了一些什么事情。
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigaddset(&set, SIGALRM);
sigaddset(&set, SIGIO);
sigaddset(&set, SIGINT);
sigaddset(&set, ngx_signal_value(NGX_RECONFIGURE_SIGNAL));
sigaddset(&set, ngx_signal_value(NGX_REOPEN_SIGNAL));
sigaddset(&set, ngx_signal_value(NGX_NOACCEPT_SIGNAL));
sigaddset(&set, ngx_signal_value(NGX_TERMINATE_SIGNAL));
sigaddset(&set, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
sigaddset(&set, ngx_signal_value(NGX_CHANGEBIN_SIGNAL));
if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"sigprocmask() failed");
}
上面为了屏蔽一系列的信号。
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
ngx_start_worker_processes(cycle, ccf->worker_processes,
NGX_PROCESS_RESPAWN);
ngx_start_cache_manager_processes(cycle, 0);
这里好像要开始创建子进程了哦,没错,master进程就是通过依次调用这两个函数来创建子进程。第一个调用的函数创建的子进程我们称为worker进程,第二调用的函数创建的是有关cache的子进程。接收请求,完成响应的就是worker进程。光光是调用这个函数好像没什么看头,我们深入“虎穴”窥探一下究竟。
for (i = 0; i < n; i++) {
cpu_affinity = ngx_get_cpu_affinity(i);
ngx_spawn_process(cycle, ngx_worker_process_cycle, NULL,
"worker process", type);
ch.pid = ngx_processes[ngx_process_slot].pid;
ch.slot = ngx_process_slot;
ch.fd = ngx_processes[ngx_process_slot].channel[0];
ngx_pass_open_channel(cycle, &ch);
}
其实吧,ngx_start_worker_processes函数挺短小精干的,再截取主体就剩下这么一个for循环了。此处就是循环创建起n个worker进程,fork新进程的具体工作在ngx_spawn_process函数中完成。这里涉及到了一个全局数组ngx_processes(定义在src/os/unix/ngx_process.c文件中),这个数组的长度为NGX_MAX_PROCESSES(默认1024),存储的元素类型是ngx_process_t(定义在src/os/unix/ngx_process.h文件中)。全局数组ngx_processes就是用来存储每个子进程的相关信息,如:pid,channel,进程做具体事情的接口指针等等,这些信息就是用结构体ngx_process_t来描述的。在ngx_spawn_process创建好一个worker进程返回后,master进程就将worker进程的pid、worker进程在ngx_processes数组中的位置及channel[0]传递给前面已经创建好的worker进程,然后继续循环开始创建下一个worker进程。刚提到一个channel[0],这里简单说明一下:channel就是一个能够存储2个整型元素的数组而已,这个channel数组就是用于socketpair函数创建一个进程间通道之用的。master和worker进程以及worker进程之间都可以通过这样的一个通道进行通信,这个通道就是在ngx_spawn_process函数中fork之前调用socketpair创建的。有兴趣的自己读读ngx_spawn_process吧。
至于ngx_start_cache_manager_processes函数,和start_worker的工作相差无几,这里暂时就不纠结了。至此,master进程就完成了worker进程的创建工作了,此时此刻系统中就有一个master进程+N个worker进程在工作了哦,接下来master进程将“陷入”死循环中守护着worker进程,担当起伟大的幕后工作。在master cycle中调用了sigsuspend(),因而将master进程挂起,等待信号的产生。master cycle所做的事情虽然不算复杂,但却比较多;主要过程就是:【收到信号】,【调用信号处理函数(在初始化过程中注册了)】,【设置对应的全局变量】,【sigsuspend函数返回,判断各个全局变量的值并采取相应的动作】。在这里,我们不对每个信号的处理情况进行分析,随便看看两个信号就好了。
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++) {
if (ngx_close_socket(ls[n].fd) == -1) {
ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno,
ngx_close_socket_n " %V failed",
&ls[n].addr_text);
}
}
cycle->listening.nelts = 0;
continue;
}
这段位于master cycle中的代码是对SIGQUIT信号进行的处理动作。ngx_quit就那个全局变量之一,当master进程收到这个信号的时候,就调用ngx_signal_handler(定义在src/os/unix/ngx_process.c文件中)设置ngx_quit为1。因此master从sigsuspend返回后,检测到ngx_quit为1,就调用ngx_signal_worker_processes函数向每个worker进程递送SIGQUIT信号,通知worker进程们开始退出工作。然后就关闭所有的监听套接字。最后居然来了一个continue就又回到了cycle中,不是退出吗?为什么是continue而不是exit呢。前面已经提过了,master进程是幕后者,需要守护着worker进程们,既然是守护哪能worker进程没撤退,自己就先撤退了呢。由于,worker进程是master的子进程,所以worker退出后,将发送SIGCHLD信号给master进程,好让master进程为其善后(否则将出现“僵尸”进程)。在master进程收到SIGCHLD信号,就会设置全局变量ngx_reap为1了。
if (ngx_reap) {
ngx_reap = 0;
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "reap children");
live = ngx_reap_children(cycle);
}
此时,ngx_reap为1了,master进程调用ngx_reap_children处理所有的worker子进程。这个ngx_reap_children函数不光担任起为worker进程善后的工作(子进程的收尸处理是在信号处理函数直接完成的),还担任了重启worker进程的任务。当然,这个重启worker进程是在一些异常情况下导致worker进程退出后的重启,并不是在“君要臣死、臣不得不死”的时候的顽强抵抗。Nginx具有高度的模块化优势,每个人都可以开发自己需要的模块程序,难免会出现一些bug引起worker进程的崩溃,因此master进程就肩负起了容错任务,这样才能够保证24小时的提供服务。
2. Nginx进程管理之worker进程
首先找到worker进程的入口地方——ngx_worker_process_cycle。这个函数不光是worker进程的入口函数,同时也是worker进程循环工作的主体函数,看函数名含有一个cycle嘛。进入这个cycle函数,第一件事就是调用ngx_worker_process_init(cycle, 1);对worker进程进行初始化操作。先看看这个worker进程的初始化过程。
ngx_process = NGX_PROCESS_WORKER;
if (ngx_set_environment(cycle, NULL) == NULL) {
/* fatal */
exit(2);
}
进入初始化就将全局变量ngx_process设置为worker进程的标志,由于这个变量是从master进程复制过来的,所以没设置前就是master进程的标志。然后设置相应的环境变量。接下去就是设置了一些列的资源限制,id等玩意,这里就忽略代码了。
for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->init_process) {
if (ngx_modules[i]->init_process(cycle) == NGX_ERROR) {
/* fatal */
exit(2);
}
}
}
此处循环调用每个模块的init_process,完成每个模块自定义的进程初始化操作,一般在模块定义的时候设置这个回调指针的值,即注册一个函数给它。做模块开发的时候,貌似使用得挺少的,遇到的时候好好关注下。
/*
此处循环用于关闭其他worker进程的无用channel资源
*/
for (n = 0; n < ngx_last_process; n++) {
/*
ngx_processes数组中n位置的进程不存在。
*/
if (ngx_processes[n].pid == -1) {
continue;
}
/*
全局变量ngx_process_slot的值是创建worker进程的时候,从
master进程复制过来的,所以此处ngx_process_slot就指本worker
进程在ngx_process_slot数组中的索引位置。此处不处理本worker
进程,所以跳过。
*/
if (n == ngx_process_slot) {
continue;
}
/*
channel不存在,继续跳过。
*/
if (ngx_processes[n].channel[1] == -1) {
continue;
}
/*
ngx_processes数组中存储的是每个worker进程的资源,是master进程负责创建的。
因此创建一个worker进程的时候,就一同将这些资源复制过来了,所以此处就关闭
无用的channel——其他worker进程的读端文件描述符,保留写端文件描述符做
worker间的通信之用。
*/
if (close(ngx_processes[n].channel[1]) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"close() channel failed");
}
}
/*
关闭本worker进程channel的写端文件描述符,因为每个worker进程只从自己的channel
上读,而不会写。写自己channel的是master和其他worker进程。这也是上面为什么要
关闭其他worker进程channel的读端。
*/
if (close(ngx_processes[ngx_process_slot].channel[0]) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"close() channel failed");
}
if (ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT,
ngx_channel_handler)
== NGX_ERROR)
{
/* fatal */
exit(2);
}
ngx_channel就是worker进程channel的读端,这里调用ngx_add_channel_event将channel放入Nginx关心的集合中,同时关注起这个channel上的读事件,也即这个channel上有数据到来后,就立马采取读channel操作。此处的添加一个channel的读事件是worker进程初始化的关键之处。到此,初始化过程就结束了,回到worker循环主体看看吧。
for ( ;; ) {
/*
ngx_exiting是在worker进程收到SIGQUIT信号后设置的,稍后就能看到庐山真面目了。
*/
if (ngx_exiting) {
c = cycle->connections;
/*
worker进程退出前,先得处理完每个connection上已经发生的事件。
*/
for (i = 0; i < cycle->connection_n; i++) {
/* THREAD: lock */
if (c[i].fd != -1 && c[i].idle) {
c[i].close = 1;
c[i].read->handler(c[i].read);
}
}
/*
处理完所有事件后,调用ngx_worker_process_exit函数,worker进程退出。
*/
if (ngx_event_timer_rbtree.root == ngx_event_timer_rbtree.sentinel)
{
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
ngx_worker_process_exit(cycle);
}
}
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "worker cycle");
/*
这里是worker进程处理事件的核心开始。也即是,worker进程从里开始做一些特定的事情了,
我们完全可以修改此处的代码,让Nginx为我们做一些其他的事情,呵呵。
*/
ngx_process_events_and_timers(cycle);
/*
worker进程收到了SIGINT信号,退出。
*/
if (ngx_terminate) {
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
ngx_worker_process_exit(cycle);
}
/*
worker进程收到了SIGQUIT信号,如果此时worker进程不是出于exiting状态,
就将设置ngx_exiting为1,让其进入exiting状态;同时关闭监听套接口。
*/
if (ngx_quit) {
ngx_quit = 0;
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,
"gracefully shutting down");
ngx_setproctitle("worker process is shutting down");
if (!ngx_exiting) {
ngx_close_listening_sockets(cycle);
ngx_exiting = 1;
}
}
/*
worker进程收到了SIGUSR1信号
*/
if (ngx_reopen) {
ngx_reopen = 0;
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");
ngx_reopen_files(cycle, -1);
}
}
3. master进程与worker进程间通信
这部分实现的源码主要分布于src/os/unix/channel.h和channel.c两个文件中。实现极其简单,没有什么复杂的逻辑。下面,我绘制了一个简单的master进程和worker进程间的关系,图中的箭头符号指出数据是由master进程传给worker进程,而没有从worker到master;这是因为channel不是一个普通的数据传输管道,在Nginx中它仅仅是用着master发送指令给worker的一个管道,master借此channel来告诉worker进程该做什么了,worker却不需要告诉master该做什么,所以是一个单向的通道。
master进程每次发送给worker进程的指令用如下一个结构来完成封装:
typedef struct {
ngx_uint_t command;
ngx_pid_t pid;
ngx_int_t slot;
ngx_fd_t fd;
} ngx_channel_t;
这个结构中的4个字段分别是发送的指令、worker进程的pid、worker进程的slot(在ngx_proecsses中的索引)及一个文件描述符。master进程可能会将一个打开的文件描述符发送给worker进程进行读写操作,那么此时就需要填写fd这个字段了。worker进程在收到一个这样的结构数据后,通过判断command的值来采取相应的动作;command就是master给worker下达的命令。
master进程用于处理SIGCHLD信号的函数ngx_reap_children中就有向worker进程发送关闭channel的指令,我们看看这个例子是怎么做的。
ch.command = NGX_CMD_CLOSE_CHANNEL;
ch.fd = -1;
ch.pid = ngx_processes[i].pid;
ch.slot = i;
ngx_write_channel(ngx_processes[n].channel[0],
&ch, sizeof(ngx_channel_t), cycle->log);
这几行代码是我从ngx_reap_children函数中拼凑起来的,所以看上去好像有点奇怪,不那么顺畅;但却清晰的给我们展现了master进程怎么给一个worker进程发送指令,此处发送的指令时NGX_CMD_CLOSE_CHANNEL。发送指令的函数ngx_write_channel是利用sendmsg来完成,《Unix网络编程》可以详细了解sendmsg。
worker进程在调用ngx_worker_process_init进行初始化的时候,使用了如下两行代码将channel放到epoll等事件处理模块中。
if (ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT,
ngx_channel_handler)
== NGX_ERROR)
{
/* fatal */
exit(2);
}
当master进程发来指令后,就调用ngx_channel_handler函数进行事件的响应。下面浓缩的代码给出了ngx_channel_handler所做的事情。
/*
读出master进程发送给过来的指令数据, ngx_read_channel
是利用recvmsg实现,详细介绍见《unix网络编程》
*/
n = ngx_read_channel(c->fd, &ch, sizeof(ngx_channel_t), ev->log);
/*
判断command的值,从而采取具体的动作,代码意图都写得很明显,
就不在这里多说了。
*/
switch (ch.command) {
case NGX_CMD_QUIT:
ngx_quit = 1;
break;
case NGX_CMD_TERMINATE:
ngx_terminate = 1;
break;
case NGX_CMD_REOPEN:
ngx_reopen = 1;
break;
case NGX_CMD_OPEN_CHANNEL:
ngx_processes[ch.slot].pid = ch.pid;
ngx_processes[ch.slot].channel[0] = ch.fd;
break;
case NGX_CMD_CLOSE_CHANNEL:
if (close(ngx_processes[ch.slot].channel[0]) == -1) {
ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_errno,
"close() channel failed");
}
ngx_processes[ch.slot].channel[0] = -1;
break;
}