nginx源代码分析--框架设计 & master-worker进程模型

时间:2022-01-21 12:34:02

Nginx的框架设计—进程模型

在这之前,我们首先澄清几点事实:

nginx作为一个高性能server的特点。事实上这也是全部的高性能server的特点,依赖epoll系统调用的高效(高效是相对select/poll这些系统调用的,底层有一个链表和红黑树,避免了轮询,降低了用户空间和系统空间之间的数据传递等)。非堵塞(全部的操作都是非堵塞,这样)。多进程(master-slave进程模型),这些事实使得nginx成为一个高性能server的前提条件。

既然作为一个软件(httpserver),相对于一个网络库而言肯定有更完好的功能。它能够在不须要关闭的前提下实现更新系统;能够接受外部的信号,依据不同的信号做对应的处理;可定制。用户能够开发第三方的模块,依据自我须要将模块加入到7个处理阶段(HTTP请求本来是有11个阶段,可是当中的4个阶段是不支持加入用户自己定义的模块的)。它能够作为反向代理。它是能够支持高并发的。它的工作模式是基于配置文件的,一个配置文件决定了nginx的工作模式。配置文件解析完毕也就完毕了初始化的全部工作,配置文件的解析在nginx中占了举足轻重的地位。

基于上述几点事实慢慢阐述nginx的设计理念。

首选从大的框架说起。既然是多进程,那么怎样分配各个进程的任务,针对CPU的核数来开启进程数目(当然是否为多进程能够通过配置文件来配置的)。分为master-worker模型,master负责开启worker进程,master进程负责和worker进程通信,master进程负责接收外部的信号。master进程负责将某些信号(总共8个信号,可是相应着worker进程关注的三个变量。三个变量分别为ngx_quit/ngx_reopen/ngx_terminate等)传递给worker进程,所以worker进程是不须要关注外部信号的,master进程须要关注worker进程的工作情况。比方接受到了SIGCHLD信号。也就是worker进程退出。在须要避免worker进程成为僵尸进程外还须要检查worker进程是否是正常死亡,假设非正常死亡还须要重新启动这个worker进程。那么worker进程做了什么呢?进程间通信就这些么?

事实上master进程和worker进程时间是有通信,worker进程之间也是能够有通信的,仅仅只是这里没有通信。通信的方式是通过socketpair,也有共享内存。两者各有不同的作用。Worker做了什么呢!

负责监听,接受连接,接受完以后就负责和这个连接进程交互,直到这个连接终于结束。事实上进程模型到这里就已经结束了。仅仅不多worker进程怎么做就是还有一回事了。

以下从代码上来分析上面提到的一切:

在src/core/nginx.c中有入口Main函数,大体了解main函数都做了些什么。

1)、ngx_get_options函数是依据用户输入的參数来设置一些全局变量,比方用户输入了-s,那么就设置ngx_signal标志,这个标志说明用户须要给ngxin输入一个信号。诸如此类的全局变量。往下再依据全局变量再进行处理。

2)、接下来的if(ngx_show_version)就是在步骤1)来设置的。表示用户是否想看版本号信息。(当然这是第一个在ngx_get_options中被设置的全局变量)

3)ngx_time_init()函数设置了表示时间的初始化。函数中的最后一行ngx_time_update是一个非常重要的函数。是用来更新时间的函数,依据配置文件,决定了更新时间的方式。以后再讨论,因为每次调用gettimeofday()这个系统调用非常浪费时间,那么就须要一个时间缓存,更新时间的有两个, ngx_time_sigsafe_update和ngx_time_update。前者指负责err_log的时间,后者负责更新非常多时间

4)接下来就是ngx_getpid(),为master进程创建一个文件。单独保存master进程的PID.

5)接下来就是准备初始化全局变量ngx_cycle,这个一个大块头。先创建一个内存池,保存相关用户传递进来的參数ngx_save_argv,

6)接下来就是处理选择项的问题。就是看看步骤5中依据用户输入的參数都保存了什么參数。

7)ngx_add_inherited_sockets函数是处理集成的套接字,这些套接字是依据NGINX这个环境变量来设置的,假设没有这个环境变量。那么就不进行操作,这些套接字是须要保存到cycle中的listening中,作为监听套接字。

8)ngx_init_cycle是初始化全局大块头,这里做了非常多非常多的配置,尤其是当中的ngx_conf_parse()函数,这个函数解析配置文件。

9)接下来就是处理步骤1中依据用户输入的參数都设置了什么全局变量,比方ngx_test_config和ngx_signal,假设是信号就须要对应的操作ngx_signal_process.

10)假设不是信号处理,那么就须要初始化master进程须要关注的信号。

11)推断是否须要设置为daemon进程。最后进入开启worker进程的模型。

ngx_single_process_cycle和ngx_master_process_cycle。

到此为止。main函数已经分析完了,当中有三点比較重要。一个ngx_init_cycle()

Ngx_master_process_cycle和ngx_init_signals()函数。咱们从简单的来看吧

Nginx中信号的处理方式:

在ngx_init_signals函数中,遍历signals全局数组,这个数组中保存着nginx须要关注的所用信号。为每个信号赋值相应的信号处理函数。全部的信号都相应同一个信号处理函数ngx_signal_handler()函数,在这个函数有看到了前面提到的一个函数,和更新时间有关。ngx_time_sigsafe_update,这个函数是更新错误时间的,也就是说。在信号处理中有一个更新时间的操作。虽说仅仅更新错误信息的时间。看看信号处理的方式。首先依据ngx_process来推断当前的传递信号的方式,是给master进程传递的?还是给全部的进程来传的?

总之有一句话,master进程从外部接受到信号,那么master进程就会被激活。全部的信号都相应同一个信号处理函数ngx_signal_handler处理,这个处理仅仅是处理相应的全局标志位,那么master进程再依据标志位来做相应的处理,大部分信号处理就是ngx_signal_worker_process函数,也就是须要将master进程接收到的信号也给worker进程传递一份,这里仅仅有三个信号须要传递给worker进程。也就是说worker进程仅仅需关注三个信号(事实上不是三个信号。可能是好几个信号。仅仅只是有多个信号相应着同一个处理。终于worker进程仅仅须要关注ngx_reopen/ngx_quit/ngx_terminate这三个变量就可以)。传递信号的方式可能是用socketpait,也可能是用kill系统调用。

再简单点介绍信号的处理就是:既然nginx是多进程的。那么不可能全部进程都能够接受外部信号的,那么这个任务交给master进程算了。master进程就须要注冊自己关注的信号,同一时候相应的信号处理函数,那么在fork出子进程后,子进程就须要清空集成的信号(由于外部信号处理交给master进程了),父子进程的通信通过socketpair来处理,那么子进程在这个通道上的读取事件处理就是ngx_channel_handler,在这个函数中,依据master进程发送的命令来改动全局变量。注意进程被信号激活接下来的操作都是推断全局变量,那么全局变量的改动对于不同的进程又不同样。master进程是通过信号处理函数(ngx_signal_handler处理),worker进程是通过socketpair创造的套接字相应的函数(ngx_channel_handler)

父子进程再被信号激活以后,都是依据全局变量来推断做什么处理的。

那么谁来改动全局变量呢?那么对于master进程来说。改动全局变量的任务交给了ngx_signal_handler函数,对于子进程来说,这个任务就是ngx_channel_handler来处理的。并且master进程关注的信号比子进程关注的信号多,全部有的信号是不须要给子进程来传递的,所以有了ngx_process这个变量。比方在ngx_get_options函数中。假设发现了-s选项。那么说明用户是准备给nginx发送信号。假设信号是stop之类。就须要将ngx_process设置为NGX_PROCESS_SIGNALLER,说明这个信号须要给子进程发送。

Ngx_process被赋值的地方不多,在nginx.c中,ngx_cycle.c中都有被赋值的操作。Ngx_process在ngx_get_options中被赋值,假设用户输入的信号是stop之类的信号等。在main函数中。假设有master配置,那么就被赋值为NGX_PROCESS_MASTER。