worker 主循环位于ngx_worker_process_cycle
函数,函数一开头出现了几个赋值
ngx_int_t worker = (intptr_t) data;
ngx_process = NGX_PROCESS_WORKER;
ngx_worker = worker;
这个data
就是 master 进程生成子进程时传入的,表示这是第几个子进程,不一定与ngx_processes
中的下标相等,这个值存放在ngx_worker
中,这个变量可以用于设置 CPU 亲缘性,此外如果配置文件中对某个监听端口开启了reuseport
,也需要使用这个变量进行一些判断,具体等之后遇到了再说吧
接下来是一个很长的初始化函数ngx_worker_process_init
,开头调用了一个和环境变量相关的函数
if (ngx_set_environment(cycle, NULL) == NULL) {
/* fatal */
exit(2);
}
这个东西说实话我看不懂,感觉好像不影响局势,所以跳过了
接下来根据配置文件配置进程优先级、文件描述符数量上限与核心转储文件大小上限
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
if (worker >= 0 && ccf->priority != 0) {
if (setpriority(PRIO_PROCESS, 0, ccf->priority) == -1) {
... // ngx_log_error
}
}
if (ccf->rlimit_nofile != NGX_CONF_UNSET) {
rlmt.rlim_cur = (rlim_t) ccf->rlimit_nofile;
rlmt.rlim_max = (rlim_t) ccf->rlimit_nofile;
if (setrlimit(RLIMIT_NOFILE, &rlmt) == -1) {
... // ngx_log_error
}
}
if (ccf->rlimit_core != NGX_CONF_UNSET) {
rlmt.rlim_cur = (rlim_t) ccf->rlimit_core;
rlmt.rlim_max = (rlim_t) ccf->rlimit_core;
if (setrlimit(RLIMIT_CORE, &rlmt) == -1) {
... // ngx_log_error
}
}
然后根据配置文件设置用户组 ID 与有效用户 ID,此外还获取了用户所在的所有组,当然只有特权用户才能进行这些设置
if (geteuid() == 0) {
if (setgid(ccf->group) == -1) {
... // ngx_log_error
/* fatal */
exit(2);
}
if (initgroups(ccf->username, ccf->group) == -1) {
... // ngx_log_error
}
if (setuid(ccf->user) == -1) {
... // ngx_log_error
exit(2);
}
}
然后设置根据worker
的值设置 CPU 亲缘性,配置文件中需包含worker_cpu_affinity
指令,指令后面可以跟数个mask
代表第几个 CPU,也可以直接使用auto
,让 nginx 来决定, 具体可以查看man CPU_SET
和man cpuset_setaffinity
if (worker >= 0) {
cpu_affinity = ngx_get_cpu_affinity(worker);
if (cpu_affinity) {
ngx_setaffinity(cpu_affinity, cycle->log);
}
}
接下来设置工作目录和清空信号掩码,然后根据时间和 pid 来设置随机数种子
if (ccf->working_directory.len) {
if (chdir((char *) ccf->working_directory.data) == -1) {
... // ngx_log_error
/* fatal */
exit(2);
}
}
sigemptyset(&set);
if (sigprocmask(SIG_SETMASK, &set, NULL) == -1) {
... // ngx_log_error
}
tp = ngx_timeofday();
srandom(((unsigned) ngx_pid << 16) ^ tp->sec ^ tp->msec);
根据man sigprocmask
的描述,fork
会继承父进程的信号掩码,因为 worker 进程不使用 master 进程的方式等待信号,而是使用诸如epoll_wait
这类函数,所以这里需要清空掩码
接下来是将listening
数组中的previous
设置为NULL
,这个值是在ngx_init_cycle
中初始化listening
数组时设置的,若其中的ngx_listening_t
对象是重用的(比如 reload 时,配置文件中某些listen
指令没有改变),则会将previous
指向之前的ngx_listening_t
对象,目的是之后删掉其中的旧事件,而 master 模式下,worker 进程都是重新启动的,即不存在旧事件,所以previous
没有意义,只有在单进程模式下才有意义
/*
* disable deleting previous events for the listening sockets because
* in the worker processes there are no events at all at this point
*/
ls = cycle->listening.elts;
for (i = 0; i < cycle->listening.nelts; i++) {
ls[i].previous = NULL;
}
接着调用各模块的init_process
函数
for (i = 0; cycle->modules[i]; i++) {
if (cycle->modules[i]->init_process) {
if (cycle->modules[i]->init_process(cycle) == NGX_ERROR) {
/* fatal */
exit(2);
}
}
}
最后关闭了用不到的channel
,并将自己的channel
读事件加入事件模块,目的是从 master 进程接收命令
for (n = 0; n < ngx_last_process; n++) {
if (ngx_processes[n].pid == -1) {
continue;
}
if (n == ngx_process_slot) {
continue;
}
if (ngx_processes[n].channel[1] == -1) {
continue;
}
if (close(ngx_processes[n].channel[1]) == -1) {
... // ngx_log_error
}
}
if (close(ngx_processes[ngx_process_slot].channel[0]) == -1) {
... // ngx_log_error
}
if (ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT,
ngx_channel_handler) == NGX_ERROR) {
/* fatal */
exit(2);
}
接下来回到ngx_worker_process_cycle
中,首先设置进程标题,然后又是个无限for
循环
ngx_setproctitle("worker process");
for ( ;; ) {
...
}
循环内首先是处理了需要退出的情况
if (ngx_exiting) {
if (ngx_event_no_timers_left() == NGX_OK) {
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
ngx_worker_process_exit(cycle);
}
}
ngx_exiting
标志位是在后面接收到退出命令或信号时设置的,表示正在优雅地退出,优雅的体现之一在于ngx_event_no_timers_left
函数,这个函数遍历定时器红黑树,若遇到不可取消的事件则返回NGX_AGAIN
等待其过期,若所有事件都可以取消则返回NGX_OK
,然后调用ngx_worker_process_exit
退出进程
接下来是最核心的函数,这个函数用于处理事件
ngx_process_events_and_timers(cycle);
那么老规矩依旧来看一下这个函数,首先开头设置了timer
和flags
,这两个变量都用于 epoll 模块(事件模块我只看了 epoll 模块)
if (ngx_timer_resolution) {
timer = NGX_TIMER_INFINITE;
flags = 0;
} else {
timer = ngx_event_find_timer();
flags = NGX_UPDATE_TIME;
}
ngx_timer_resolution
表示时间精度,在配置文件的timer_resolution
指令中指定,可以看到,如果设置了时间精度则将timer
的值设置NGX_TIMER_INFINITE
(值为 -1),否则设置为定时器事件中最近一次事件的过期时间,而timer
之后用于epoll_wait
的第四个参数,表示超时时间, -1 则意味着无限阻塞
那么为什么设置了时间精度就要求无限阻塞呢?这里涉及到了事件模块的内容,核心事件模块ngx_event_core_module
的init_process
函数(之前统一调用过)会利用setitimer
系统调用,根据时间精度来设置一个定时器,而定时器到期后会向进程发送SIGALRM
,如果此时阻塞在epoll_wait
则会被其打断,epoll_wait
函数后进行的第一个动作就是调用ngx_time_update
更新时间(其实还需要做一些判断),以此达到控制时间精度的目的
这里还有一个flag
参数,这个参数作用之一为决定是否需要更新时间,结合之前说的,来看一下 epoll 模块中的一小段代码
events = epoll_wait(ep, event_list, (int) nevents, timer);
if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
ngx_time_update();
}
由此可以知道,如果没有设置时间精度,则每次epoll_wait
返回都会更新时间,如果设置了时间精度,则只有当接收到SIGALRM
信号(此时会设置ngx_event_timer_alarm
标志位),才会更新时间
接下来的部分与 accept 锁有关,主要用于解决 accept 的惊群问题,而 nginx 在1.11.3
后就默认关闭了 accept 锁,因为有更好的方案来解决惊群问题,比如reuseport
和1.11.3
版本加入的EPOLLEXCLUSIVE
标志位,所以这里不过多涉及 accept 锁,粗略地写一下
if (ngx_use_accept_mutex) {
if (ngx_accept_disabled > 0) {
ngx_accept_disabled--;
} else {
if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
return;
}
if (ngx_accept_mutex_held) {
flags |= NGX_POST_EVENTS;
} else {
if (timer == NGX_TIMER_INFINITE || timer > ngx_accept_mutex_delay) {
timer = ngx_accept_mutex_delay;
}
}
}
}
首先有一个ngx_accept_disabled
,这个值由连接数量和空闲连接数量计算得出,主要用于负载均衡
ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;
根据其判断是否需要尝试获取锁,如果获取到锁则将listening
数组的 accept 事件添加到 epoll 中,同时需要设置flags
的NGX_POST_EVENTS
位,用于延迟执行事件处理函数,因为如果在持有锁的时间内处理事件可能会导致其他进程长时间获取不到锁,如果没有获取到锁,则设置一个延迟,决定下次尝试获取锁的时间,当然也可能会提前
之后是调用ngx_process_events
,这个函数具体做了什么留到 epoll 模块时再写吧,之前那一小段 epoll 代码也在其中
delta = ngx_current_msec;
(void) ngx_process_events(cycle, timer, flags);
delta = ngx_current_msec - delta;
这里还计算了函数前后的时间差,有变化则代表过程中更新了时间,用于判断之后是否需要检查定时器事件
接着处理 accept 延迟队列中的事件,处理完后就可以将 accept 锁释放掉了,那些非 accept 事件则留到最后执行
ngx_event_process_posted(cycle, &ngx_posted_accept_events);
if (ngx_accept_mutex_held) {
ngx_shmtx_unlock(&ngx_accept_mutex);
}
最后,如果过程中更新了时间则检查是否有过期的定时器事件,然后执行延迟队列中的事件
if (delta) {
ngx_event_expire_timers();
}
ngx_event_process_posted(cycle, &ngx_posted_events);
那么回到 worker 的主循环中,后面还需要处理的就是强制中止、优雅退出和重新打开日志文件的情况了
if (ngx_terminate) {
ngx_worker_process_exit(cycle);
}
if (ngx_quit) {
ngx_quit = 0;
ngx_setproctitle("worker process is shutting down");
if (!ngx_exiting) {
ngx_exiting = 1;
ngx_set_shutdown_timer(cycle);
ngx_close_listening_sockets(cycle);
ngx_close_idle_connections(cycle);
}
}
if (ngx_reopen) {
ngx_reopen = 0;
ngx_reopen_files(cycle, -1);
}
强制中止和重新打开日志文件的情况比较简单,主要看下优雅退出的情况,ngx_set_shutdown_timer
根据配置文件中的shutdown_timeout
设置了一个定时器事件,用于延迟关闭正在使用的连接,ngx_close_listening_sockets
顾名思义,最后ngx_close_idle_connections
用于也关闭连接,与第一个函数不同的是这里没有延迟且关闭的是 idle 连接,例如设置了keepalive
而当前空闲的连接