1. 引言
Nginx 以其高并发、高稳定性和低资源消耗著称,其优雅的进程管理模型和信号处理机制是实现热更新(upgrade)、配置重载(reload)和优雅关闭(shutdown)等高级运维操作的核心。本文将深入 Nginx 源码,详细剖析其信号处理流程,并解释如何通过信号实现 master 进程与 worker 进程的通信,最终结合具体命令演示热更新、回滚和 reload 的实现原理。
2. Nginx 进程模型与信号概览
Nginx 采用 master-worker 多进程模型。master 进程负责读取配置、管理 worker 进程(fork、监控、发送信号);worker 进程则负责处理实际的客户端连接和请求。
2.1 关键进程角色
- Master 进程 :非阻塞、无事件循环,主要职责是管理 worker 进程。它通过
ngx_signal_handler()函数处理操作系统发送给它的信号。 - Worker 进程 :运行事件循环(如 epoll),处理网络事件。它们也会处理信号,但通常只处理
SIGTERM、SIGQUIT、SIGUSR1等少数信号,用于优雅关闭或日志重开。
2.2 核心信号及其用途
| 信号 | 默认行为 | Nginx 中的用途 |
|---|---|---|
SIGTERM / SIGINT |
终止进程 | 快速关闭 Nginx |
SIGQUIT |
终止并产生 core 文件 | 优雅关闭 Nginx(处理完当前请求后退出) |
SIGHUP |
挂起 | 重新加载配置文件(reload) |
SIGUSR1 |
用户自定义信号 1 | 重新打开日志文件 |
SIGUSR2 |
用户自定义信号 2 | 平滑升级 Nginx 可执行文件(热更新) |
SIGWINCH |
窗口大小改变 | 优雅关闭 worker 进程(用于热更新回滚) |
SIGCHLD |
子进程退出 | master 进程监控 worker 进程状态,自动重启异常退出的 worker |
3. 源码分析:Master 进程的信号处理
Nginx 的信号处理核心逻辑位于 src/os/unix/ngx_process.c 和 src/core/ngx_cycle.c 文件中。
3.1 信号注册
在 ngx_init_signals() 函数中,Nginx 为每个需要处理的信号注册了对应的处理函数 ngx_signal_handler()。
c
// src/os/unix/ngx_process.c
static ngx_signal_t signals[] = {
{ ngx_signal_value(NGX_RECONFIGURE_SIGNAL), // SIGHUP
"SIGHUP",
ngx_signal_handler },
{ ngx_signal_value(NGX_REOPEN_SIGNAL), // SIGUSR1
"SIGUSR1",
ngx_signal_handler },
{ ngx_signal_value(NGX_CHANGEBIN_SIGNAL), // SIGUSR2
"SIGUSR2",
ngx_signal_handler },
{ ngx_signal_value(NGX_SHUTDOWN_SIGNAL), // SIGQUIT
"SIGQUIT",
ngx_signal_handler },
{ ngx_signal_value(NGX_TERMINATE_SIGNAL), // SIGTERM
"SIGTERM",
ngx_signal_handler },
{ SIGCHLD, "SIGCHLD", ngx_signal_handler },
{ SIGWINCH, "SIGWINCH", ngx_signal_handler },
{ SIGIO, "SIGIO", ngx_signal_handler },
{ SIGSYS, "SIGSYS", SIG_IGN }, // 忽略 SIGSYS
{ 0, NULL, NULL }
};
3.2 信号处理函数 ngx_signal_handler()
当 master 进程收到信号时,ngx_signal_handler() 被调用。它不会立即执行复杂操作,而是将信号"暂存"到全局变量 ngx_cycle->exiting 或 ngx_reap 等标志位中,然后由 master 进程的主循环在合适的时机处理。
c
// src/os/unix/ngx_process.c
void
ngx_signal_handler(int signo)
{
// ... (省略部分代码)
switch (signo) {
case ngx_signal_value(NGX_RECONFIGURE_SIGNAL): // SIGHUP
ngx_reconfigure = 1; // 设置重载标志
ngx_noaccept = 1; // 停止接受新连接
break;
case ngx_signal_value(NGX_REOPEN_SIGNAL): // SIGUSR1
ngx_reopen = 1; // 设置重新打开日志标志
break;
case ngx_signal_value(NGX_CHANGEBIN_SIGNAL): // SIGUSR2
ngx_change_binary = 1; // 设置更换二进制文件标志
break;
case ngx_signal_value(NGX_SHUTDOWN_SIGNAL): // SIGQUIT
ngx_quit = 1; // 设置优雅退出标志
break;
case ngx_signal_value(NGX_TERMINATE_SIGNAL): // SIGTERM
case SIGINT:
ngx_terminate = 1; // 设置快速终止标志
break;
case SIGCHLD:
ngx_reap = 1; // 设置回收子进程标志
break;
case SIGWINCH:
ngx_noaccept = 1; // 设置停止接受新连接标志
break;
}
// ... (省略部分代码)
}
3.3 Master 进程主循环处理
master 进程的主循环在 ngx_master_process_cycle() 函数中。它会在每次循环中检查上述标志位,并执行相应操作。
c
// src/os/unix/ngx_process_cycle.c
void
ngx_master_process_cycle(ngx_cycle_t *cycle)
{
// ... (初始化)
for ( ;; ) {
// ... (处理定时器、事件等)
if (ngx_reap) { // 处理子进程退出
ngx_reap = 0;
ngx_live_worker_processes = ngx_reap_children(cycle);
}
if (ngx_terminate) { // 快速关闭
// ... 发送 SIGTERM 给所有 worker
ngx_signal_worker_processes(cycle, ngx_signal_value(NGX_TERMINATE_SIGNAL));
// ... 等待 worker 退出
ngx_master_process_exit(cycle);
}
if (ngx_quit) { // 优雅关闭
// ... 发送 SIGQUIT 给所有 worker
ngx_signal_worker_processes(cycle, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
// ... 等待 worker 退出
ngx_master_process_exit(cycle);
}
if (ngx_reconfigure) { // 重新加载配置
ngx_reconfigure = 0;
ngx_noaccept = 0;
// ... 解析新配置
// ... 启动新的 worker 进程
// ... 发送 SIGQUIT 给旧的 worker 进程
}
if (ngx_change_binary) { // 热更新
ngx_change_binary = 0;
ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv); // 执行新二进制
}
if (ngx_noaccept) { // 停止接受新连接
ngx_signal_worker_processes(cycle, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
}
// ... (其他处理)
}
}
4. Master 与 Worker 进程的信号通信
Master 进程通过 ngx_signal_worker_processes() 函数向所有 worker 进程发送信号。这个函数遍历 ngx_processes 数组(存储所有子进程信息),并使用 kill() 系统调用向每个 worker 进程发送指定的信号。
c
// src/os/unix/ngx_process.c
void
ngx_signal_worker_processes(ngx_cycle_t *cycle, int signo)
{
ngx_uint_t i;
ngx_process_t *process;
for (i = 0; i < ngx_last_process; i++) {
process = &ngx_processes[i];
if (process->pid == -1) {
continue;
}
if (process->exiting) { // 如果 worker 正在退出,跳过
continue;
}
if (kill(process->pid, signo) == -1) {
// ... 错误处理
}
}
}
Worker 进程也有自己的信号处理函数 ngx_signal_handler(),但逻辑更简单,主要处理 SIGTERM、SIGQUIT、SIGUSR1 等信号,用于优雅关闭或日志重开。
5. 实战:热更新、回滚与 Reload
5.1 配置重载(Reload)
命令 :nginx -s reload 或 kill -HUP <master_pid>
流程:
- Master 进程收到
SIGHUP信号,设置ngx_reconfigure = 1。 - Master 进程主循环检测到
ngx_reconfigure,开始解析新的配置文件。 - 如果新配置解析成功,master 进程会 fork 出一组新的 worker 进程。
- 新的 worker 进程开始使用新配置处理新连接。
- Master 进程向旧的 worker 进程发送
SIGQUIT信号,让它们优雅退出(处理完当前请求后关闭连接)。 - 旧的 worker 进程退出后,reload 完成。
源码关键点 :ngx_reconfigure 标志位和 ngx_start_worker_processes() 函数。
5.2 热更新(平滑升级)
命令 :kill -USR2 <master_pid>
流程:
- Master 进程收到
SIGUSR2信号,设置ngx_change_binary = 1。 - Master 进程主循环检测到
ngx_change_binary,调用ngx_exec_new_binary()函数。 ngx_exec_new_binary()函数会 fork 一个子进程,并使用execve()系统调用执行新的 Nginx 二进制文件(/usr/local/nginx/sbin/nginx或指定路径)。- 新的 master 进程(由新二进制启动)会继承旧的 master 进程的监听 socket 和进程 ID。
- 新的 master 进程启动一组新的 worker 进程。
- 此时,新旧两套 Nginx 进程同时运行,共享监听端口(通过
SO_REUSEPORT或SO_REUSEADDR实现)。 - 新的 worker 进程开始处理新连接,旧的 worker 进程继续处理旧连接。
- 向旧的 master 进程发送
SIGQUIT信号,让旧的 worker 进程优雅退出。
热更新回滚:
- 如果新版本有问题,可以向旧的 master 进程发送
SIGWINCH信号,让旧的 master 进程向新的 worker 进程发送SIGQUIT,停止新 worker。 - 然后向旧的 master 进程发送
SIGHUP信号,重新启动旧的 worker 进程,完成回滚。
5.2.1 热更新核心源码深度解析
热更新的灵魂在于 ngx_exec_new_binary() 函数,它位于 src/os/unix/ngx_process.c。该函数的核心逻辑如下:
c
// src/os/unix/ngx_process.c
ngx_pid_t
ngx_exec_new_binary(ngx_cycle_t *cycle, char *const *argv)
{
char *env, *var;
ngx_pid_t pid;
ngx_uint_t i;
ngx_exec_ctx_t ctx;
// 1. 准备环境变量:将当前监听 socket 的文件描述符编号
// 编码到环境变量 NGINX_VAR 中,传递给新进程
ctx.env = NULL;
ctx.argv = argv;
ctx.name = "new binary process";
ctx.pid = 0;
// 2. 构建环境变量字符串,格式为:
// "NGINX=pid:<master_pid>;sid:<session_id>;fds:<fd1>,<fd2>,..."
// 其中 fds 是当前 master 持有的所有监听 socket fd
env = ngx_common_add_string(cycle->listening.elts,
cycle->listening.nelts);
if (env == NULL) {
return NGX_INVALID_PID;
}
// 3. fork 子进程
pid = fork();
switch (pid) {
case -1: // fork 失败
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"fork() failed before execve()");
return NGX_INVALID_PID;
case 0: // 子进程(新的 master)
// 4. 在子进程中执行 execve(),用新二进制替换当前进程映像
// 注意:execve 成功后,子进程变为新的 master 进程
// 它继承了父进程的所有文件描述符(包括监听 socket)
// 以及环境变量 NGINX_VAR
ngx_execve(cycle, ctx.argv, ctx.env);
// execve 失败才会走到这里
exit(1);
break;
default: // 父进程(旧的 master)
break;
}
// 5. 父进程返回新子进程的 PID
return pid;
}
关键原理剖析:
-
文件描述符继承 :
fork()后子进程会继承父进程的所有文件描述符,包括监听 socket 的 fd。execve()执行新二进制时,这些 fd 默认保持打开(除非设置了FD_CLOEXEC)。Nginx 在创建监听 socket 时不会 设置FD_CLOEXEC,因此新二进制启动后能直接使用这些 fd 继续监听。 -
环境变量传递 :旧 master 通过
NGINX_VAR环境变量将监听 socket 的 fd 编号列表传递给新 master。新 master 启动时,在ngx_init_cycle()中会读取该环境变量,将对应的 fd 作为已打开的监听 socket 使用,而不再重新bind()和listen()。 -
新旧 master 共存 :
fork()后新旧两个 master 进程同时运行,共享同一组监听 socket。内核的 TCP 协议栈会将新连接分发给其中一个进程(取决于SO_REUSEPORT或accept()的竞争)。新 master 启动后立即fork()自己的 worker 进程,开始处理新连接。 -
旧 worker 优雅退出 :旧 master 在
ngx_change_binary处理完毕后,继续运行其主循环。运维人员随后向旧 master 发送SIGQUIT,触发ngx_quit标志,旧 master 向所有旧 worker 发送SIGQUIT,让它们处理完当前请求后退出。
热更新回滚的源码机制:
回滚依赖 SIGWINCH 信号。当旧 master 收到 SIGWINCH 时,设置 ngx_noaccept = 1,然后调用 ngx_signal_worker_processes() 向所有 worker(包括新 master 的 worker)发送 SIGQUIT。但新 master 的 worker 进程的父进程是新 master,旧 master 的 ngx_processes 数组中只记录了自己 fork() 的子进程,因此 SIGWINCH 实际上只会影响旧 master 自己的 worker。那如何停止新 master 的 worker 呢?
关键在于:新 master 启动时,旧 master 会记录新 master 的 PID 到 ngx_new_binary 变量中。当旧 master 收到 SIGWINCH 后,除了设置 ngx_noaccept,还会向新 master 发送 SIGQUIT,让新 master 优雅关闭其 worker 进程。新 master 收到 SIGQUIT 后,向自己的 worker 发送 SIGQUIT,等待它们退出,然后自己退出。此时旧 master 检测到子进程(新 master)退出(SIGCHLD),调用 ngx_reap_children() 回收,然后重新启动自己的 worker 进程,完成回滚。
c
// ngx_master_process_cycle() 中处理 SIGWINCH 的简化逻辑
if (ngx_noaccept) {
if (ngx_new_binary) { // 如果有新 master 进程
// 向新 master 发送 SIGQUIT,让它优雅关闭
kill(ngx_new_binary, SIGQUIT);
}
// 向自己的 worker 发送 SIGQUIT
ngx_signal_worker_processes(cycle, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
}
5.3 优雅关闭
命令 :nginx -s quit 或 kill -QUIT <master_pid>
流程:
- Master 进程收到
SIGQUIT信号,设置ngx_quit = 1。 - Master 进程主循环检测到
ngx_quit,调用ngx_signal_worker_processes()向所有 worker 进程发送SIGQUIT。 - Worker 进程收到
SIGQUIT后,停止接受新连接,并开始处理当前正在处理的请求。 - 当 worker 进程处理完所有请求后,关闭所有连接并退出。
- Master 进程等待所有 worker 进程退出后,清理资源并退出。
6. 总结
Nginx 的信号处理机制是其高可用和运维灵活性的基石。通过精心设计的信号处理函数和 master-worker 进程通信模型,Nginx 实现了零停机时间的配置重载、平滑升级和优雅关闭。理解这些底层原理,不仅能帮助运维人员更自信地执行操作,还能在遇到问题时快速定位和解决。