1. 引言
在计算机系统中,信号处理是一项重要的任务,它允许操作系统和应用程序之间进行通信和协调。在网络服务器软件中,如Nginx,信号处理机制起着关键作用,它能够捕获和处理各种类型的信号,从而实现服务器的灵活控制和运行时的动态行为。
nginx是一款高性能、轻量级的Web服务器和反向代理服务器,被广泛应用于构建可靠、高效的Web应用程序和服务。为了满足各种需求和应对不同的运行时情况,nginx提供了丰富的信号处理机制,使得管理员和开发人员能够通过发送信号来实现对服务器的管理和控制。
信号是一种在操作系统中用于通知进程发生某种事件或请求某种操作的机制。它可以用于向进程发送中断信号、终止信号、重启信号等,以及自定义的应用程序信号。nginx利用信号处理机制,可以捕获和处理各种信号,例如重新加载配置文件、优雅地停止或重启服务器等。
深入理解nginx中的信号处理机制需要了解信号的基本概念和操作系统对信号的支持。当nginx接收到一个信号时,它会根据信号的类型和当前的运行状态执行相应的操作。例如,当接收到重新加载配置文件的信号时,nginx会重新读取配置文件并应用新的配置,而不需要重启整个服务器。
2. signal信号处理函数的注册
在nginx的main函数中有一个函数调用,如下:
if (ngx_init_signals(cycle->log) != NGX_OK) {
return 1;
}
这个调用的作用就是向操作系统注册当前进程的signal处理函数。
下面是ngx_init_signals函数的实现源码:
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));
if (sig->handler) {
sa.sa_sigaction = sig->handler;
sa.sa_flags = SA_SIGINFO;
} else {
sa.sa_handler = SIG_IGN;
}
sigemptyset(&sa.sa_mask);
if (sigaction(sig->signo, &sa, NULL) == -1) {
#if (NGX_VALGRIND)
ngx_log_error(NGX_LOG_ALERT, log, ngx_errno,
"sigaction(%s) failed, ignored", sig->signame);
#else
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
"sigaction(%s) failed", sig->signame);
return NGX_ERROR;
#endif
}
}
return NGX_OK;
}
在ngx_init_signals函数中,对定义的signals数组进行遍历,并将对应的signal处理函数注册到操作系统中。
在注册一个signal信号的时候,需要分几步:
-
- 初始化一个sigaction结构体。
-
- 设置sigaction结构体中sa_sigaction或者sa_handler(二选一)至信号处理函数。对于前者,需要设置sa_flags = SA_SIGINFO。
-
- 如果不希望在处理当前signal的时候block其他信号,那么用sigemptyset清空sa_mask。
-
- 最后,通过sigaction向操作系统注册消息处理函数。
通过上面的循环遍历,nginx注册了SIGHUP(reload)、SIGUSR1(reopen)、SIGWINCH(noaccept)、SIGTERM(stop)、SIGQUIT(quit)、SIGUSR2(change bin)、SIGARLRM(timer)、SIGINT(stop)、SIGIO()、SIGCHLD(child reap)、SIGSYS(ignore)、SIGPIPE(ignore)共12个信号。
signals的定义如下:
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 },
{ ngx_signal_value(NGX_NOACCEPT_SIGNAL),
"SIG" ngx_value(NGX_NOACCEPT_SIGNAL),
"",
ngx_signal_handler },
{ ngx_signal_value(NGX_TERMINATE_SIGNAL),
"SIG" ngx_value(NGX_TERMINATE_SIGNAL),
"stop",
ngx_signal_handler },
{ ngx_signal_value(NGX_SHUTDOWN_SIGNAL),
"SIG" ngx_value(NGX_SHUTDOWN_SIGNAL),
"quit",
ngx_signal_handler },
{ ngx_signal_value(NGX_CHANGEBIN_SIGNAL),
"SIG" ngx_value(NGX_CHANGEBIN_SIGNAL),
"",
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", "", NULL },
{ SIGPIPE, "SIGPIPE, SIG_IGN", "", NULL },
{ 0, NULL, "", NULL }
};
3. 设置信号阻塞
为了nginx在处理信号的过程中确保能够正确地处理并且避免被中断,需要对信号设置block阻塞标记,从而能够在处理指定信号的时候,避免新的信号进来打扰处理过程。
在ngx_master_process_cycle函数(当配置开启了master_process模式时会作用master进程的主循环)的开头部分,进行了相关设置,源码如下:
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");
}
sigemptyset(&set);
这里对前面设置的10个信号(除了SIG_IGN)进行了设置。
4. signal信号的处理
在nginx中,signal信号是由ngx_signal_handler函数负责接收处理的。不过,ngx_signal_handler函数对信号的处理其实就是对应接收到的信号设置相应的标记,然后立即返回。譬如收到SIGTERM信号,则设置ngx_terminate = 1,收到SIGHUP信号,则设置ngx_reconfigure等等。其自己本身不进行实际的信号处理。
signal信号的处理逻辑是在主循环中进行的。如果是master/worker多进程运行模式下,在ngx_master_process_cycle函数中处理,如果是单进程运行模式下,则是在ngx_single_process_cycle函数中进行处理。在主循环函数中,它会检查ngx_signal_handler中设置的标记位,然后根据各个标记位进行对应的处理。
譬如在ngx_master_process_cycle函数中对配置重加载信号的处理逻辑如下:
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;
}
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reconfiguring");
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));
}
&esmp;首先它判断是否ngx_reconfigure被设置为1了,如果没有设置,那么不执行配置重加载的操作。
接着,如果是正在更新二进制文件操作,即ngx_new_binary=1,那么需要在这里启动新的worker进程和cache manager进程。
再下来是调用ngx_init_cycle重新加载配置文件。
加载新的worker进程,最后通知老的worker进程进行优雅退出。
5. 跨进程发送signal
在nginx运行的过程中,如果我们需要让当前的nginx能够重新加载配置文件,我们可以在命令行输入以下命令:
nginx -s reload
又或者,如果我们希望停止nginx运行,我们可以在命令行输入一下命令:
nginx -s stop
因为我们在命令行输入以上命令的时候,其实shell又重新启动了一个新的nginx进程,那新的nginx进程是如何通知正在提供服务的nginx进程执行相应的动作的呢?
这里就涉及到跨进程信号发送的操作了。
&esmp; 新启动的进程根据命令行参数,会读取正在提供服务的nginx进程的pid文件,得到它的master进程的pid,然后调用系统函数kill来向master进程,这样子master进程就会收到对应的信号,然后master主循环函数就会进行信号的处理。
在main函数中,我们可以看到下面的代码:
if (ngx_signal) {
return ngx_signal_process(cycle, ngx_signal);
}
意思就是向nginx进程发送指定的信号。再看ngx_signal_process函数的实现:
ngx_int_t
ngx_signal_process(ngx_cycle_t *cycle, char *sig)
{
ssize_t n;
ngx_pid_t pid;
ngx_file_t file;
ngx_core_conf_t *ccf;
u_char buf[NGX_INT64_LEN + 2];
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "signal process started");
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
ngx_memzero(&file, sizeof(ngx_file_t));
file.name = ccf->pid;
file.log = cycle->log;
file.fd = ngx_open_file(file.name.data, NGX_FILE_RDONLY,
NGX_FILE_OPEN, NGX_FILE_DEFAULT_ACCESS);
if (file.fd == NGX_INVALID_FILE) {
ngx_log_error(NGX_LOG_ERR, cycle->log, ngx_errno,
ngx_open_file_n " \"%s\" failed", file.name.data);
return 1;
}
n = ngx_read_file(&file, buf, NGX_INT64_LEN + 2, 0);
if (ngx_close_file(file.fd) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
ngx_close_file_n " \"%s\" failed", file.name.data);
}
if (n == NGX_ERROR) {
return 1;
}
while (n-- && (buf[n] == CR || buf[n] == LF)) { /* void */ }
pid = ngx_atoi(buf, ++n);
if (pid == (ngx_pid_t) NGX_ERROR) {
ngx_log_error(NGX_LOG_ERR, cycle->log, 0,
"invalid PID number \"%*s\" in \"%s\"",
n, buf, file.name.data);
return 1;
}
return ngx_os_signal_process(cycle, sig, pid);
}
非常好理解,就是读取pid文件,然后调用ngx_os_signal_process函数对pid发送signal。由于linux/unix和windows的signal机制是不一样的,所以ngx_os_signal_process函数针对两类操作系统nginx进行了单独实现,这里不再赘述。
6. 总结
以上通过对nginx的源码分析,从signal信号的注册和阻塞状态设置,到signal信号的处理,最后到跨进程singla信号的发送进行了详细的介绍,我们可以从中一窥nginx如何利用操作系统的signal机制来实现对进程的各种控制功能,有不当之处敬请指正。