Nginx 源码阅读笔记6 master 主循环

只看了 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_slots
接下来调用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进程收养,此时startuppid也是大于 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数组中找到相应的项,修改其statusexited,如果退出状态码为 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;
}

首次收到这个信号,会设置delaysigio的值,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_restartngx_reopenngx_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]

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值