Nginx 源码阅读笔记6 master 主循环
来源:互联网 发布:高性能php架构 编辑:程序博客网 时间:2024/06/06 19:54
只看了 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; // 平滑升级时新进程的 piddelay = 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_tngx_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]
- Nginx 源码阅读笔记6 master 主循环
- nginx源码阅读(三).master进程的工作循环
- Nginx 源码阅读笔记7 worker 主循环
- Nginx学习笔记(十七):master进程的循环工作
- nginx 源码分析阅读笔记-进程管理
- Nginx 源码阅读笔记1 内存池
- Nginx 源码阅读笔记3 时间管理
- Nginx 源码阅读笔记4 启动流程
- Nginx 源码阅读笔记5 初始化 cycle
- Nginx 源码阅读笔记8 epoll 模块
- nginx源码阅读(五).worker进程的工作循环
- Nginx源码分析--master进程
- nginx源码阅读(一)
- 阅读 Nginx 源码
- 阅读nginx源码_win32
- nginx源码阅读
- Nginx 源码阅读笔记2 原子变量与互斥锁
- Nginx 源码阅读笔记9 http 模块初始化
- ACM ArabellaCPC 2015F题 并查集
- 清华镜像下载Android源码
- 笔试数理题总结
- java线程学习
- 给kali的Metasploit下添加一个新的exploit
- Nginx 源码阅读笔记6 master 主循环
- React Native之Modal组件实现遮罩层效果
- DataFrame筛选数据与loc用法
- windows下搭建Java环境
- 使用jquery判断是否为数字
- CDN及Nginx反向代理
- thinkPhp中 VOlist和 foreach 的区别
- leetcode [Implement Queue using Stacks]//待整理多种解法
- 企业级私有Docker仓库Harbor搭建