20. 深入 Nginx 信号处理

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),处理网络事件。它们也会处理信号,但通常只处理 SIGTERMSIGQUITSIGUSR1 等少数信号,用于优雅关闭或日志重开。

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.csrc/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->exitingngx_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(),但逻辑更简单,主要处理 SIGTERMSIGQUITSIGUSR1 等信号,用于优雅关闭或日志重开。

5. 实战:热更新、回滚与 Reload

5.1 配置重载(Reload)

命令nginx -s reloadkill -HUP <master_pid>

流程

  1. Master 进程收到 SIGHUP 信号,设置 ngx_reconfigure = 1
  2. Master 进程主循环检测到 ngx_reconfigure,开始解析新的配置文件。
  3. 如果新配置解析成功,master 进程会 fork 出一组新的 worker 进程。
  4. 新的 worker 进程开始使用新配置处理新连接。
  5. Master 进程向旧的 worker 进程发送 SIGQUIT 信号,让它们优雅退出(处理完当前请求后关闭连接)。
  6. 旧的 worker 进程退出后,reload 完成。

源码关键点ngx_reconfigure 标志位和 ngx_start_worker_processes() 函数。

5.2 热更新(平滑升级)

命令kill -USR2 <master_pid>

流程

  1. Master 进程收到 SIGUSR2 信号,设置 ngx_change_binary = 1
  2. Master 进程主循环检测到 ngx_change_binary,调用 ngx_exec_new_binary() 函数。
  3. ngx_exec_new_binary() 函数会 fork 一个子进程,并使用 execve() 系统调用执行新的 Nginx 二进制文件(/usr/local/nginx/sbin/nginx 或指定路径)。
  4. 新的 master 进程(由新二进制启动)会继承旧的 master 进程的监听 socket 和进程 ID。
  5. 新的 master 进程启动一组新的 worker 进程。
  6. 此时,新旧两套 Nginx 进程同时运行,共享监听端口(通过 SO_REUSEPORTSO_REUSEADDR 实现)。
  7. 新的 worker 进程开始处理新连接,旧的 worker 进程继续处理旧连接。
  8. 向旧的 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;
}

关键原理剖析

  1. 文件描述符继承fork() 后子进程会继承父进程的所有文件描述符,包括监听 socket 的 fd。execve() 执行新二进制时,这些 fd 默认保持打开(除非设置了 FD_CLOEXEC)。Nginx 在创建监听 socket 时不会 设置 FD_CLOEXEC,因此新二进制启动后能直接使用这些 fd 继续监听。

  2. 环境变量传递 :旧 master 通过 NGINX_VAR 环境变量将监听 socket 的 fd 编号列表传递给新 master。新 master 启动时,在 ngx_init_cycle() 中会读取该环境变量,将对应的 fd 作为已打开的监听 socket 使用,而不再重新 bind()listen()

  3. 新旧 master 共存fork() 后新旧两个 master 进程同时运行,共享同一组监听 socket。内核的 TCP 协议栈会将新连接分发给其中一个进程(取决于 SO_REUSEPORTaccept() 的竞争)。新 master 启动后立即 fork() 自己的 worker 进程,开始处理新连接。

  4. 旧 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 quitkill -QUIT <master_pid>

流程

  1. Master 进程收到 SIGQUIT 信号,设置 ngx_quit = 1
  2. Master 进程主循环检测到 ngx_quit,调用 ngx_signal_worker_processes() 向所有 worker 进程发送 SIGQUIT
  3. Worker 进程收到 SIGQUIT 后,停止接受新连接,并开始处理当前正在处理的请求。
  4. 当 worker 进程处理完所有请求后,关闭所有连接并退出。
  5. Master 进程等待所有 worker 进程退出后,清理资源并退出。

6. 总结

Nginx 的信号处理机制是其高可用和运维灵活性的基石。通过精心设计的信号处理函数和 master-worker 进程通信模型,Nginx 实现了零停机时间的配置重载、平滑升级和优雅关闭。理解这些底层原理,不仅能帮助运维人员更自信地执行操作,还能在遇到问题时快速定位和解决。

相关推荐
数据知道7 小时前
邮件安全实战:SPF/DKIM/DMARC 配置与钓鱼绕过
网络·安全·web安全·网络安全·邮件安全
王莎莎-MinerU7 小时前
MCP 解决的是工具接入,科研 Agent 还缺的是科学证据接口标准化
开发语言·网络·人工智能·深度学习·pdf·c#·php
就不掉头发7 小时前
计算机的虚拟内存
linux·服务器·windows
暴力求解7 小时前
Linux ---线程控制(二)
linux·运维·服务器·操作系统
smartvxworks7 小时前
Linux 实时内核(Linux Real-Time Kernel)详解:原理、实践与优化
java·linux·服务器
Chloeis Syntax8 小时前
JAVAEE初阶 --- 构造HTTP请求
网络·网络协议·http·postman
砚凝霜8 小时前
软考网络工程师|第 1 章 计算机网络基础 完整备考笔记
网络·笔记·计算机网络
梦痕长情8 小时前
等保三级三权账户
linux·运维·服务器
rcms152702692188 小时前
NIKON 4S019-394 接口模块
网络