只看了 master 模式的代码,没有考虑单进程的情况,master 主循环位于ngx_master_process_cycle
函数,在main
函数的最后被调用
首先是阻塞信号,这些信号的信号处理函数已经在main
函数中设置了
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) { ... }
sigemptyset(&set);
这里阻塞信号是为了之后调用sigsuspend
函数等待信号,而参数就是最后被置为空的set
然后是设置进程标题,方法在之前说过,修改argv[0]
的值,具体体现就是ngx_setproctitle
这个函数
size = sizeof(master_process);
for (i = 0; i < ngx_argc; i++) {
size += ngx_strlen(ngx_argv[i]) + 1;
}
title = ngx_pnalloc(cycle->pool, size);
if (title == NULL) {
/* fatal */
exit(2);
}
p = ngx_cpymem(title, master_process, sizeof(master_process) - 1);
for (i = 0; i < ngx_argc; i++) {
*p++ = ' ';
p = ngx_cpystrn(p, (u_char *) ngx_argv[i], size);
}
ngx_setproctitle(title);
master_process
是一个char
数组,值为master process
,这段代码整体逻辑比较简单
然后是根据配置文件中work_processes
的值,启动指定数量的进程
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);
这里我略过了关于cache_manager
的代码,以后有机会再看吧,主要看下ngx_start_worker_processes
函数
for (i = 0; i < n; i++) {
ngx_spawn_process(cycle, ngx_worker_process_cycle,
(void *) (intptr_t) i, "worker process", type);
...
}
首先看下生成子进程的部分,循环调用ngx_spawn_process
函数,其中第二个参数ngx_worker_process_cycle
即 worker 主循环,先来看下ngx_spawn_process
函数的原型
ngx_pid_t ngx_spawn_process(ngx_cycle_t *cycle,
ngx_spawn_proc_pt proc, void *data, char *name, ngx_int_t respawn);
proc
表示生成的子进程需要调用的函数,data
作为传入proc
的参数,而respawn
比较特殊,如果这个参数是非负数,则代表ngx_processes
数组的下标,这个数组是个存放了ngx_process_t
的数组,代表所有子进程
typedef struct {
ngx_pid_t pid; // 进程 id
int status; // 进程退出状态
ngx_socket_t channel[2]; // unix domain socket 对 用于进程间通信
ngx_spawn_proc_pt proc; // 进程执行函数
void *data; // 自定义数据
char *name; // 进程名称
unsigned respawn:1; // 进程需要重新启动 例如子进程意外退出时
unsigned just_spawn:1; // 进程刚刚启动 用于 reload 时区分新旧进程
unsigned detached:1; // 进程为分离状态 例如平滑升级启动新的二进制文件时
unsigned exiting:1; // 进程正在退出
unsigned exited:1; // 进程已退出
} ngx_process_t;
如果respawn
为负数则可以取下面几个值,这些值用于设置进程的一些属性,也就是respawn
位,just_spawn
位和detached
位
#define NGX_PROCESS_NORESPAWN -1
#define NGX_PROCESS_JUST_SPAWN -2
#define NGX_PROCESS_RESPAWN -3
#define NGX_PROCESS_JUST_RESPAWN -4
#define NGX_PROCESS_DETACHED -5
接下来看ngx_spawn_process
函数,首先是开头的部分
if (respawn >= 0) {
s = respawn;
} else {
for (s = 0; s < ngx_last_process; s++) {
if (ngx_processes[s].pid == -1) {
break;
}
}
if (s == NGX_MAX_PROCESSES) {
return NGX_INVALID_PID;
}
}
决定了s
的值,这个值表示ngx_processes
数组的下标,如果respawn
为非负数,由于含义相同所以直接赋值即可,否则寻找ngx_processes
数组中第一个可用的下标,代码中ngx_last_process
代表目前数组的最大下标之后的位置,默认为 0
if (respawn != NGX_PROCESS_DETACHED) {
if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1) { ... }
if (ngx_nonblocking(ngx_processes[s].channel[0]) == -1) { ... }
if (ngx_nonblocking(ngx_processes[s].channel[1]) == -1) { ... }
on = 1;
if (ioctl(ngx_processes[s].channel[0], FIOASYNC, &on) == -1) { ... }
if (fcntl(ngx_processes[s].channel[0], F_SETOWN, ngx_pid) == -1) { ... }
if (fcntl(ngx_processes[s].channel[0], F_SETFD, FD_CLOEXEC) == -1) { ... }
if (fcntl(ngx_processes[s].channel[1], F_SETFD, FD_CLOEXEC) == -1) { ... }
ngx_channel = ngx_processes[s].channel[1];
} else {
ngx_processes[s].channel[0] = -1;
ngx_processes[s].channel[1] = -1;
}
ngx_process_slot = s;
如果进程不被指定为分离状态则创建 socket 对用于进程间通信,然后设置为非阻塞的方式,其中第二个 socket 给即将生成的子进程使用,第一个 socket 给 master 和其他子进程使用,这里对于第一个 socket 还设置了信号驱动 IO,最后设置当前操作的进程下标ngx_process_slot
为s
接下来调用fork
生成子进程
pid = fork();
switch (pid) {
case -1:
ngx_close_channel(ngx_processes[s].channel, cycle->log);
return NGX_INVALID_PID;
case 0:
ngx_pid = ngx_getpid();
proc(cycle, data);
break;
default:
break;
}
可以看到,子进程首先设置了ngx_pid
,然后调用传入的proc
函数,例如 worker 主循环,父进程则接下去执行
ngx_processes[s].pid = pid;
ngx_processes[s].exited = 0;
if (respawn >= 0) {
return pid;
}
这里设置了进程的pid
和退出状态,如果respawn
参数为非负数则到这里就返回了,那么什么时候会以非负数调用呢?有一种情况是 worker 进程意外退出,需要 master 进程重新启动时,会以此 worker 进程的下标作为respawn
参数,这时新的子进程除了pid
与退出状态,其余各项属性与之前相同,所以可以不必设置
ngx_processes[s].proc = proc;
ngx_processes[s].data = data;
ngx_processes[s].name = name;
ngx_processes[s].exiting = 0;
switch (respawn) {
case NGX_PROCESS_NORESPAWN:
ngx_processes[s].respawn = 0;
ngx_processes[s].just_spawn = 0;
ngx_processes[s].detached = 0;
break;
case NGX_PROCESS_JUST_SPAWN:
ngx_processes[s].respawn = 0;
ngx_processes[s].just_spawn = 1;
ngx_processes[s].detached = 0;
break;
case NGX_PROCESS_RESPAWN:
ngx_processes[s].respawn = 1;
ngx_processes[s].just_spawn = 0;
ngx_processes[s].detached = 0;
break;
case NGX_PROCESS_JUST_RESPAWN:
ngx_processes[s].respawn = 1;
ngx_processes[s].just_spawn = 1;
ngx_processes[s].detached = 0;
break;
case NGX_PROCESS_DETACHED:
ngx_processes[s].respawn = 0;
ngx_processes[s].just_spawn = 0;
ngx_processes[s].detached = 1;
break;
}
其中设置了子进程的其余属性,并根据respawn
参数设置了三个标志位,函数最后根据情况判断是否需要递增ngx_last_process
,然后返回
if (s == ngx_last_process) {
ngx_last_process++;
}
return pid;
回到之前的ngx_start_worker_processes
函数,看下剩余的部分
ngx_channel_t ch;
ngx_memzero(&ch, sizeof(ngx_channel_t));
ch.command = NGX_CMD_OPEN_CHANNEL;
for (i = 0; i < n; i++) {
... // ngx_spawn_process
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);
}
这段代码首先根据新的子进程的各项信息设置ch
,然后通过调用ngx_pass_open_channel
传递这些信息给其他子进程,函数有点长且个人认为不是很重要就不贴出来了,主要就是利用sendmsg
系统调用,通过辅助数据传递文件描述符
那么,worker 进程的启动到这里就结束了,在回到 master 主循环之前,先来看下main
函数中设置的信号处理函数
ngx_signal_t signals[] = {
{ ngx_signal_value(NGX_RECONFIGURE_SIGNAL),
"SIG" ngx_value(NGX_RECONFIGURE_SIGNAL), // SIGHUP
"reload", ngx_signal_handler },
{ ngx_signal_value(NGX_REOPEN_SIGNAL),
"SIG" ngx_value(NGX_REOPEN_SIGNAL), // SIGUSR1
"reopen", ngx_signal_handler },
{ ngx_signal_value(NGX_NOACCEPT_SIGNAL),
"SIG" ngx_value(NGX_NOACCEPT_SIGNAL), // SIGWINCH
"", ngx_signal_handler },
{ ngx_signal_value(NGX_TERMINATE_SIGNAL),
"SIG" ngx_value(NGX_TERMINATE_SIGNAL), // SIGTERM
"stop", ngx_signal_handler },
{ ngx_signal_value(NGX_SHUTDOWN_SIGNAL),
"SIG" ngx_value(NGX_SHUTDOWN_SIGNAL), // SIGQUIT
"quit", ngx_signal_handler },
{ ngx_signal_value(NGX_CHANGEBIN_SIGNAL),
"SIG" ngx_value(NGX_CHANGEBIN_SIGNAL), // SIGUSR2
"", ngx_signal_handler },
{ SIGALRM, "SIGALRM", "", ngx_signal_handler },
{ SIGINT, "SIGINT", "", ngx_signal_handler },
{ SIGIO, "SIGIO", "", ngx_signal_handler },
{ SIGCHLD, "SIGCHLD", "", ngx_signal_handler },
{ SIGSYS, "SIGSYS, SIG_IGN", "", SIG_IGN },
{ SIGPIPE, "SIGPIPE, SIG_IGN", "", SIG_IGN },
{ 0, NULL, "", NULL }
};
可以看到,这些信号的处理函数除了忽略SIG_IGN
外,都是ngx_signal_handler
,接下来看下信号处理函数中与 master 相关的部分
switch (signo) {
case ngx_signal_value(NGX_SHUTDOWN_SIGNAL):
ngx_quit = 1;
break;
case ngx_signal_value(NGX_TERMINATE_SIGNAL):
case SIGINT:
ngx_terminate = 1;
break;
case ngx_signal_value(NGX_NOACCEPT_SIGNAL):
if (ngx_daemonized) {
ngx_noaccept = 1;
}
break;
case ngx_signal_value(NGX_RECONFIGURE_SIGNAL):
ngx_reconfigure = 1;
break;
case ngx_signal_value(NGX_REOPEN_SIGNAL):
ngx_reopen = 1;
break;
case ngx_signal_value(NGX_CHANGEBIN_SIGNAL):
if (getppid() > 1 || ngx_new_binary > 0) {
break;
}
ngx_change_binary = 1;
break;
case SIGALRM:
ngx_sigalrm = 1;
break;
case SIGIO:
ngx_sigio = 1;
break;
case SIGCHLD:
ngx_reap = 1;
break;
}
if (signo == SIGCHLD) {
ngx_process_get_status();
}
可以看到,收到不同信号的时候,会设置相应的标志位,这里比较特殊的是SIGUSR2
,也就是需要平滑升级时发送的信号,这里有两种情况会忽略掉这个信号
- 向成功启动新进程的旧进程再次发送这个信号,这时
ngx_new_binary
的值为新进程的pid
- 向新进程发送这个信号,而此时旧进程还没退出,这时
getppid()
会得到旧进程(也就是父进程)的pid
,这种情况应该只有不以守护进程形式启动时才会发生,还有就是在我的 ubuntu 环境下,如果是在图形界面下杀掉旧进程,新进程会被startup
进程收养,此时startup
的pid
也是大于 1 的
紧接着是ngx_process_get_status
函数,调用的条件是收到SIGCHLD
信号,在 worker 进程停止或者中止时会向 master 进程发送这个信号,简单看下重要的部分
for ( ;; ) {
pid = waitpid(-1, &status, WNOHANG);
...
for (i = 0; i < ngx_last_process; i++) {
if (ngx_processes[i].pid == pid) {
ngx_processes[i].status = status;
ngx_processes[i].exited = 1;
break;
}
}
...
if (WEXITSTATUS(status) == 2 && ngx_processes[i].respawn) {
ngx_processes[i].respawn = 0;
}
ngx_unlock_mutexes(pid);
}
循环waitpid
得到子进程的pid
和状态,如果有则在ngx_processes
数组中找到相应的项,修改其status
和exited
,如果退出状态码为 2 则代表致命错误,这时将respawn
设为 0,表示不重新启动 worker 进程,最后是调用ngx_unlock_mutexes
,函数里调用ngx_shmtx_force_unlock
强制释放了该进程锁持有的锁,这个强制释放锁的函数之前也提到过
那么回到 master 的主循环,接下来设置了几个变量的值
ngx_new_binary = 0; // 平滑升级时新进程的 pid
delay = 0; // 接收到中止信号时 延迟的时间
sigio = 0; // 接收到中止信号时 可以接收的最大信号数量
live = 1; // 标志位 表示至少有一个子进程存活
之后的部分都位于for
循环中,这是一个无限的循环
if (delay) {
if (ngx_sigalrm) {
sigio = 0;
delay *= 2;
ngx_sigalrm = 0;
}
itv.it_interval.tv_sec = 0;
itv.it_interval.tv_usec = 0;
itv.it_value.tv_sec = delay / 1000;
itv.it_value.tv_usec = (delay % 1000 ) * 1000;
if (setitimer(ITIMER_REAL, &itv, NULL) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, "setitimer() failed");
}
}
循环开头这段代码一开始看得我一脸懵逼,需要看后面才能理解,主要用于强制退出时,根据delay
设置定时器,每次到期则将delay
翻倍,直到delay
大于 1000 则发送SIGKILL
信号,也就是必杀的信号
接下来会调用sigsuspend
等待信号到达,然后更新时间
sigsuspend(&set);
ngx_time_update();
从信号处理函数中知道,子进程中止或停止时,会设置ngx_reap
位,首先处理的是这种情况
if (ngx_reap) {
ngx_reap = 0;
live = ngx_reap_children(cycle);
}
ngx_reap_children
函数遍历ngx_processes
数组,如果遇到设置了exited
的进程,则关闭其channel
文件描述符对并通知其他子进程,接着根据respawn
位判断是否需要重新启动子进程,此外还有一种特殊情况,就是平滑升级时新进程意外退出了,这时旧进程若没有退出也会收到信号,如果此时旧进程为noaccepting
状态则设置ngx_restart
重新启动 worker 进程,最后函数返回是否有进程存活
接着根据live
判断是否需要退出 master 进程
if (!live && (ngx_terminate || ngx_quit)) {
ngx_master_process_exit(cycle);
}
可以看到,如果live
为 0,且设置了退出或中止的标记,代表进程需要退出且子进程已经全部成功退出,此时退出 master 进程即可
接着是ngx_terminate
的情况,这个代表强制中止但其实还是有一段延迟,也就是与循环内开头那段代码相关
if (ngx_terminate) {
if (delay == 0) {
delay = 50;
}
if (sigio) {
sigio--;
continue;
}
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;
}
首次收到这个信号,会设置delay
和sigio
的值,sigio
的值是 worker 进程数量,然后向 worker 进程发送中止信号,这里sigio
可以理解为定时器过期前可以接收的最大信号数量,避免了每次接收到信号都调用ngx_signal_worker_processes
,这里应该含有一个假设,定时器过期前,每个 worker 进程都成功退出并发送 SIGCHLD,由于标准信号是不会排队的,所以接收到的 SIGCHLD 一定小于等于sigio
,但是也可能收到其他信号
每次定时器到期或sigio
递减为 0 时,都会再次调用ngx_signal_worker_processes
,直至delay
大于 1000 则发送SIGKILL
信号
接下来是ngx_signal_worker_processes
,看下主体部分
for (i = 0; i < ngx_last_process; i++) {
if (ngx_processes[i].detached || ngx_processes[i].pid == -1) {
continue;
}
if (ngx_processes[i].just_spawn) {
ngx_processes[i].just_spawn = 0;
continue;
}
if (ngx_processes[i].exiting && signo == ngx_signal_value(NGX_SHUTDOWN_SIGNAL)) {
continue;
}
if (ch.command) {
if (ngx_write_channel(ngx_processes[i].channel[0],
&ch, sizeof(ngx_channel_t), cycle->log) == NGX_OK) {
if (signo != ngx_signal_value(NGX_REOPEN_SIGNAL)) {
ngx_processes[i].exiting = 1;
}
continue;
}
}
if (kill(ngx_processes[i].pid, signo) == -1) {
err = ngx_errno;
if (err == NGX_ESRCH) {
ngx_processes[i].exited = 1;
ngx_processes[i].exiting = 0;
ngx_reap = 1;
}
continue;
}
if (signo != ngx_signal_value(NGX_REOPEN_SIGNAL)) {
ngx_processes[i].exiting = 1;
}
}
依旧是遍历ngx_processes
数组,有以下几种情况
- 子进程设置了
detached
,无视即可,这种情况对应于平滑升级 - 子进程设置了
just_spawn
,则简单地将其设置为 0,这种情况对应于 reload - 子进程设置了
exiting
且需要发送的信号为退出信号,这时也是无视即可,如果是中止信号,依旧需要发送
若不是上面几种情况,则首先尝试使用channel
通知子进程,若失败则通过kill
系统调用向子进程发送信号,每次成功通知子进程且目的为退出或中止时,都将exiting
设置为 1,还有一个特例就是kill
返回ESRCH
,表示pid
对应的子进程不存在,这时需要设置ngx_reap
以检查子进程
接着是ngx_quit
,这种情况下进程会优雅地退出,也就是会等到 worker 进程的当前任务完成后才退出
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
}
}
cycle->listening.nelts = 0;
continue;
}
接着是ngx_reconfigure
也就是 reload 重新读取配置文件的情况
if (ngx_reconfigure) {
ngx_reconfigure = 0;
if (ngx_new_binary) {
ngx_start_worker_processes(cycle, ccf->worker_processes, NGX_PROCESS_RESPAWN);
ngx_start_cache_manager_processes(cycle, 0);
ngx_noaccepting = 0;
continue;
}
cycle = ngx_init_cycle(cycle);
if (cycle == NULL) {
cycle = (ngx_cycle_t *) ngx_cycle;
continue;
}
ngx_cycle = cycle;
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
ngx_start_worker_processes(cycle, ccf->worker_processes, NGX_PROCESS_JUST_RESPAWN);
ngx_start_cache_manager_processes(cycle, 1);
/* allow new processes to start */
ngx_msleep(100);
live = 1;
ngx_signal_worker_processes(cycle, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
}
可以看到,如果平滑升级后 reload 旧的 master 进程,旧的 master 进程会重新启动 worker 进程,如果不是这种情况,则调用ngx_init_cycle
,然后再重新启动 worker 进程,这时的第三个参数与之前不同,是NGX_PROCESS_JUST_RESPAWN
,也就是新进程会设置just_respawn
位,那么这个位有什么用呢?看到最后一行,调用ngx_signal_worker_processes
向所有进程发出了退出的信号,刚刚说过这个函数,在遍历ngx_processes
数组遇到设置just_respawn
的进程时,简单地将其置 0,而不会真正向其发送信号,所以接收到退出信号的只有旧的 worker 进程
然后是最后四种情况,ngx_restart
、ngx_reopen
和ngx_noaccept
的情况比较直观,就不多说了,主要是看下ngx_change_binary
的情况
if (ngx_restart) {
ngx_restart = 0;
ngx_start_worker_processes(cycle, ccf->worker_processes, NGX_PROCESS_RESPAWN);
ngx_start_cache_manager_processes(cycle, 0);
live = 1;
}
if (ngx_reopen) {
ngx_reopen = 0;
ngx_reopen_files(cycle, ccf->user);
ngx_signal_worker_processes(cycle, ngx_signal_value(NGX_REOPEN_SIGNAL));
}
if (ngx_change_binary) {
ngx_change_binary = 0;
ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv);
}
if (ngx_noaccept) {
ngx_noaccept = 0;
ngx_noaccepting = 1;
ngx_signal_worker_processes(cycle, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
}
函数ngx_exec_new_binary
首先将listening
数组中 socket 对应的文件描述符的值,以某种格式放入环境变量NGINX_VAR
中,然后调用ngx_execute
启动新进程,ngx_execute
函数比较简单
ngx_pid_t
ngx_execute(ngx_cycle_t *cycle, ngx_exec_ctx_t *ctx) {
return ngx_spawn_process(cycle, ngx_execute_proc, ctx, ctx->name,
NGX_PROCESS_DETACHED);
}
这里以NGX_PROCESS_DETACHED
的形式启动新进程,然后子进程会调用ngx_execute_proc
,这个函数简单地包装了execve
,执行的路径为我们之前在main
函数中保存过的argv[0]