nginx的奇幻冒险:master和worker模式

上一篇主要介绍了ngx_init_cycle这个函数,着重在讲常规的服务器如何启动、TCP怎样建立连接。本文继续跟着nginx启动步伐,把剩下的流程说完,主要内容包括进程分支操作和信号量处理。

判断需要执行的启动模式

nginx定义了两种服务器工作模式,一种为master和worker模式,将进程分为单个master和多个worker,master监管程序控制子进程,worker负责处理连接事件;另一个是single模式表示一个哦进程完成所有的工作。

  • 默认模式是master worker模式,下文主要介绍的也是这种模式。生产环境一般也会用这个模式,稳定性和性能都比较好。
1
2
3
4
5
6
// nginx.c
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);

if (ccf->master && ngx_process == NGX_PROCESS_SINGLE) {
ngx_process = NGX_PROCESS_MASTER;
}

初始化信号量

非window系统会进入这个部分,这里的信号量主要用来进行进程间通讯,一般是异步传输的,当进程收到信号之后会先中断当前的操作优先执行信号处理函数。信号可以在进程间相互传递,也可以系统内核向进程传递。
比如kill命令,kill的字面意思容易造成误会让人以为就是直接杀死进程,其实是向进程传输信号。系统默认会向对应进程发送一个SIGTERM信号,如果进程没有对这个信号注册对应的函数就会关闭进程。

  • kill可以选择传输信号类型 kill -s SIGNAL PID
  • 如果进程正在执行阻塞的系统调用,在没有信号屏蔽的情况下收到信号后,就会直接中断返回一个异常值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ngx_int_t
ngx_init_signals(ngx_log_t *log)
{
ngx_signal_t *sig;
struct sigaction sa;

for (sig = signals; sig->signo != 0; sig++) {
ngx_memzero(&sa, sizeof(struct sigaction));

if (sig->handler) {
sa.sa_sigaction = sig->handler;
sa.sa_flags = SA_SIGINFO;

} else {
sa.sa_handler = SIG_IGN;
}

sigemptyset(&sa.sa_mask);
if (sigaction(sig->signo, &sa, NULL) == -1) {
......
}
}

return NGX_OK;
}

signals是一个固定的数组代表了所有需要进行处理的信号量,遍历signals调用sigaction()为每个信号设置处理函数,大多数信号的处理函数会设置成void ngx_signal_handler,这个函数会根据信号类设置不同的全局变量,比如收到NGX_SHUTDOWN_SIGNAL就会把ngx_quit设置1,这些全局变量的作用后续会出现在master_cycle中。

  • sigaction()用于设置信号的处理函数。sig表示对应的信号标识号,act是设置信号的结构体,oldact用于备份旧的信号配置,如果不需要可以输入空。

    1
    2
    #include <signal.h>
    int sigaction(int sig, const struct sigaction *act,struct sigaction *oldact);
  • struct sigaction主要作为sigaction()的参数,该函数具备更加丰富的参数,因而取代了signal()sa_handlersa_sigaction都可以用来设置处理函数,不过两者参数不同。而且两者为一个联合体,共用内存只能取其一。sa_flag可以设置特殊的属性,比如SA_SIGINFO用于记载信号的详细信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct sigaction{
union
{
/* Used if SA_SIGINFO is not set. */
__sighandler_t sa_handler;
/* Used if SA_SIGINFO is set. */
void (*sa_sigaction) (int, siginfo_t *, void *);
} __sigaction_handler;
# define sa_handler __sigaction_handler.sa_handler
# define sa_sigaction __sigaction_handler.sa_sigaction
__sigset_t sa_mask;
int sa_flags;
void (*sa_restorer) (void);
}

设置daemon进程

守护进程是生存期长的一种进程。它们常常在系统引导装入时使用,仅在系统关闭时才终止。

—《UNIX环境高级编程第三版》

nginx作为一个服务器程序,必然要长时间地运行,比较适合作为守护进程启动,来看下nginx是如何将进程设置为守护进程的吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
ngx_int_t 
ngx_daemon(ngx_log_t *log)
{
int fd;

switch (fork()) {
case -1:
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "fork() failed");
return NGX_ERROR;

case 0:
break;

default:
exit(0);
}

ngx_parent = ngx_pid;
ngx_pid = ngx_getpid();

if (setsid() == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "setsid() failed");
return NGX_ERROR;
}
umask(0);
fd = open("/dev/null", O_RDWR);
if (fd == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
"open(\"/dev/null\") failed");
return NGX_ERROR;
}

if (dup2(fd, STDIN_FILENO) == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDIN) failed");
return NGX_ERROR;
}

if (dup2(fd, STDOUT_FILENO) == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "dup2(STDOUT) failed");
return NGX_ERROR;
}

if (fd > STDERR_FILENO) {
if (close(fd) == -1) {
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "close() failed");
return NGX_ERROR;
}
}
return NGX_OK;

创建一个子进程,关闭父进程,设置子进程的属性,重定向子进程的标准输入输出为空地址等。

  • fork()会创建一个子进程。子进程返回0,父进程返回子进程id,错误为-1。子进程会和父进程一样执行后面的代码,执行顺序是未知的由系统决定。。
  • unix系统中每一个进程都有一个父进程,当父进程退出而子进程还在运行时,系统就会修改该子进程的父进程,一般都由init进程担任。
  • umask(0)的作用则是将进程的权限掩码设置为零,这样就充值了该进程创建文件的权限。
  • 打开并获取/dev/null文件的描述符,使用dup2()把标准输入输出描述符设置为null的描述符,这个操作会把定向到标准输入和输出的数据全部忽略。也就是说这个进程无法直接和用户进行操作。

如此一来守护进程会忽略标准的输入输出,并且运行在后台不会收到用户终端的影响。

master_process_cycle

  1. 设置信号屏蔽集

1
2
3
4
5
6
7
sigset_t           set;
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
......
if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
......
}

设置信号屏蔽集,屏蔽不需要的信号以免干扰。

  • sigset_t表示信号集合,sigemptyset()将信号置空,sigaddset()将特定的信号加入set中。 sigprocmask(SIG_BLOCK, &set, NULL)基于set对当前进程设置屏蔽的信号,第三个参数类型是sigset_t用于保存旧的信号屏蔽集合,置空表示不需要保存。
  • 子进程会继承父进程的信号屏蔽集。
  1. 创建工作进程

调用ngx_spawn_process()创建n个子进程,子进程创建数量可以在配置文件中设置。这个函数设计地十分巧妙,ngx_spawn_proc_pt是函数指针,data表示函数proc中的参数,name表示开启的进程名称,respawn表示启动的类型,我们来看下它的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
typedef void (*ngx_spawn_proc_pt) (ngx_cycle_t *cycle, void *data);

ngx_pid_t
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data, char *name, ngx_int_t respawn){
ngx_int_t s;

if (respawn >= 0) {
s = respawn;
}else{
......
}

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)
......
}

pid = fork();

switch (pid) {
......
case 0:
ngx_parent = ngx_pid;
ngx_pid = ngx_getpid();
proc(cycle, data);
break;
}

ngx_processes[s].pid = pid;
ngx_processes[s].exited = 0;
.......
}

子进程最终会调proc()也就是传入的函数指针,这就给这个函数极大的自由度,除了创建工作进程以外还可以创建其他的进程。这里的函数指针指向ngx_worker_process_cycle(),而父进程会更新ngx_processes里的内容然后退出函数。

  • socketpair()可以设置两个可以进行双向通信的unix域套接字,在此先不纠结这个操作,等到用到的时候再看。
  • nginx的代码很严谨,判断上一步没出错才能会执行下一步,这个编程习惯值得学习。
  1. master_cycle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for ( ;; ) {
......
sigsuspend(&set);

if (ngx_reap) {
......
}

if (!live && (ngx_terminate || ngx_quit)) {
......
}

if (ngx_terminate) {
......
}

}

sigsuspend()会忽略之前屏蔽的信号集并会一直阻塞等待信号。如果收到信号则执行信号处理函数并返回,上文对信号处理函数的注册简单说过。然后下面就依次判断相关变量并执行相关操作。总之,master进程会循环等待并处理信号。

  1. worker cycle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for ( ;; ) {

if (ngx_exiting) {
......
}

ngx_process_events_and_timers(cycle);

if (ngx_terminate) {
......
}

if (ngx_quit) {
......
}
}

worker进程主要在循环中执行ngx_process_events_and_timers进行事件处理,这个函数是nginx能够处理大量连接的关键所在。

后话

  1. 这次分析main函数中剩余的部分,涉及到信号处理、守护进程等,以及把整个启动的流程简单过了一遍。下回会分析nginx的事件驱动。
  2. 作为练手的小项目Minix还会同步更新。这次增加了后台进程的代码,GitHub不太稳定最终决定把代码托管在gitee上。我尽量每次写完文章之后更新相关部分的代码。我认为学习编程的出发点一定是能够设计出实用的程序,纯粹看代码而完全不动手实践是严重脱离编程实质的。

nginx的奇幻冒险:master和worker模式
http://www.sjblg.com/nginx-3/
作者
Jay Shen
许可协议