一,nginx进程模型
nginx的进程模型和大多数后台服务程序一样,按职责将进程分成监控进程和工作进程两类,启动nginx的主进程充当监控进程,
而由主进程fork出来的子进程则充当工作进程。工作进程的任务自然是完成具体的业务逻辑,而监控进程充当整个进程组的对外
接口,同时对工作进程进行监护,比如如果某工作进程意外退出,监控进程将重新fork生成一个新的工作进程。nginx也可以单进
程模型执行,在这种进程模型下,主进程就是工作进程,此时没有监控进程,单进程模型比较简单且官方建议仅供测试使用,所
以下面主要分析多进程模型。
分析nginx多进程模型的入口函数为主进程的ngx_master_process_cycle()函数,在该函数做完信号处理设置等之后就会调用一个名
为ngx_start_worker_processes()的函数用于fork产生出子进程(子进程数目通过函数调用的第二个实参指定),子进程作为一个新
的实体开始充当工作进程的角色执行ngx_worker_process_cycle()函数,该函数主体为一个无限for循环,持续不断的处理客户端的服
务请求,而主进程继续执行ngx_master_process_cycle()函数,也就是作为监控进程执行主体for循环,这也是一个无限循环,直到进
程终止才退出,服务进程基本都是这种写法,所以不用详述,下面先看看这个模型的图示:
上图中表现得很明朗,监控进程和工作进程各有一个无限for循环,以便进程持续的等待和处理自己负责的事务,直到进程退出。
监控进程的无限for循环内有一个关键的sigsuspend()函数调用,该函数的调用使得监控进程的大部分时间都处于挂起等待状态,直到监控进程
接收到信号为止,当监控进程接收到信号时,信号处理函数ngx_signal_handler()就会被执行,我们知道信号处理函数一般都要求足够简单(关于
信号处理函数的实现准则请Google),所以在该函数内执行的动作主要也就是根据当前信号值对相应的旗标变量做设置,而实际的处理逻辑必须放
在主体代码里来处理,所以该for循环接下来的代码就是判断有哪些旗标变量被设置而需要处理的,比如ngx_reap(有子进程退出?)、ngx_quit或
ngx_terminate(进行要退出或终止?注意:虽然两个旗标都是表示结束nginx,不过ngx_quit的结束更优雅,它会让nginx监控进程做一些清理工作
且等待子进程也完全清理并退出之后才终止,而ngx_terminate更为粗暴,不过它通过使用SIGKILL信号能保证在一段时间后必定被结束掉)、
ngx_reconfigure(重新加载配置?)等。当所有信号都处理完时又挂起在函数sigsuspend()调用处继续等待新的信号,如此反复,构成监控进程的主
要执行体。
二,整体架构
正常执行起来后的Nginx会有多个进程,最基本的有master_process和worker_process,还可能会有cache相关进程(这在后面会具体讲到)。除了自身
进程之间的相互通信,Nginx还凭借强悍的模块功能与外界四通八达,比如通过upstream与web server通信、依靠fastcgi与application server通信等等。一
个较为完整的整体架构框图如下所示:
三,进程通信
运行在多进程模型的nginx在正常工作时,自然就会有多个进程实例,比如下图是在配置“worker_processes 4;”情况下的显示,
nginx设置的进程title能很好的帮助我们区分监控进程与工作进程。
采用socketpair()函数创造一对未命名的UNIX域套接字来进行Linux下具有亲缘关系的进程之间的双向通信是一个非常不错的解决方案。
在nginx中master和worker的通信是通过socketpair来实现的,每次fork完一个子进程之后,将这个子进程的socketpaire句柄传递给前
面已经存在的子进程,这样子进程之间也就可以通信了。
nginx中fork子进程是在ngx_spawn_process中进行的:
第一个参数是全局的配置,第二个参数是子进程需要执行的函数,第三个参数是proc的参数。第四个类型
ngx_pid_t
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
char *name, ngx_int_t respawn)
这个函数主要的任务就是:
1 有一个ngx_processes全局数组,包含了所有的存货的子进程,这里会fork出来的子进程放入到相应的位置。并设置这个进程的相关属性。
2 创建socketpair,并设置相关属性。
3 在子进程中执行传递进来的函数。
我们先来看几个主要的数据结构:
首先是进程结构,这个结构体表示了一个进程。包含了它的id状态,channel等等。
Filename : ngx_process.h
typedef struct {
///进程id
ngx_pid_t pid;
///进程的退出状态(主要在waitpid中进行处理).
int status;
///进程channel(也就是通过socketpair创建的两个句柄)
ngx_socket_t channel[2];
///进程的执行函数(也就是每次spawn,子进程所要执行的那个函数).
ngx_spawn_proc_pt proc;
void *data;
char *name;
///进程的几个状态。
unsigned respawn:1;
unsigned just_respawn:1;
unsigned detached:1;
unsigned exiting:1;
unsigned exited:1;
} ngx_process_t;
#define NGX_MAX_PROCESSES 1024
下面我们来看详细的代码。Filename : src/os/unix/ngx_process.c
//全局的进程表,保存了存活的子进程。
ngx_process_t ngx_processes[NGX_MAX_PROCESSES];
ngx_pid_t
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
char *name, ngx_int_t respawn)
{
u_long on;
ngx_pid_t pid;
///表示将要fork的子进程在ngx_processes中的位置,
ngx_int_t s;
///首先,如果传递进来的类型大于0,则就是已经确定这个进程已经退出,我们就可以直接确定slot。
if (respawn >= 0) {
s = respawn;
} else {
///遍历ngx_processess,从而找到空闲的slot,从而等会fork完毕后,将子进程信息放入全局进程信息表的相应的slot。
for (s = 0; s < ngx_last_process; s++) {
if (ngx_processes[s].pid == -1) {
break;
}
}
///到达最大进程限制报错。
if (s == NGX_MAX_PROCESSES) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
"no more than %d processes can be spawned",
NGX_MAX_PROCESSES);
return NGX_INVALID_PID;
}
}
if (respawn != NGX_PROCESS_DETACHED) {
/* Solaris 9 still has no AF_LOCAL */
if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1)
{
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"socketpair() failed while spawning \"%s\"", name);
return NGX_INVALID_PID;
}
...
// ngx_nonblocking
// ioctl
// fcntl
ngx_channel = ngx_processes[s].channel[1];
} else {
ngx_processes[s].channel[0] = -1;
ngx_processes[s].channel[1] = -1;
}
ngx_process_slot = s;
pid = fork();
switch (pid) {
...
}
...
return pid;
}
在该函数进行fork()之前,先调用了socketpair()创建一对socket描述符存放在变量ngx_processes[s].channel内(其中s标志在ngx_processes数组内第一个可用元素的下标,比如最开始产生第一个工作进程时,可用元素的下标s为0),而在fork()之后,由于子进程继承了父进程的资源,那么父子进程就都有了这一对socket描述符,而nginx将channel[0]给父进程使用,channel[1]给子进程使用,这样分别错开的使用不同socket描述符,即可实现父子进程之间的双向通信:
除此之外,对于各个子进程之间,也可以进行双向通信。如前面所述,父子进程的通信channel设定是自然而然的事情,而子进程之间的通信channel设定就涉及到进程之间文件描述符(socket描述符也属于文件描述符)的传递,因为虽然后生成的子进程通过继承的channel[0]能够往前生成的子进程发送信息,但前生成的子进程无法获知后生成子进程的channel[0]而不能发送信息,所以后生成的子进程必须利用已知的前生成子进程的channel[0]进行主动告知。
..
参考资料<深入剖析Nginx>