Nginx进程管理

来源:互联网 发布:在淘宝上买女士秋衣 编辑:程序博客网 时间:2024/06/09 21:53
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框架的意义:
信号对应进程中的全局标志位变量意义QUITngx_quit优雅地关闭整个服务TERM或INTngx_terminate强制关闭整个服务USR1ngx_reopen重新打开服务中的所有文件WINCHngx_noaccept所有子进程不再接受处理新的连接,实际相当于对所有子进程发送QUIT信号USR2ngx_change_binary平滑升级到新版本的Nginx程序HUPng_reconfigure重读配置文件CHLDngx_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;        }


参考:http://www.alidata.org/archives/1174