Nginx的框架设计—进程模型
在这之前,我们首先澄清几点事实:
nginx作为一个高性能服务器的特点,其实这也是所有的高性能服务器的特点,依赖epoll系统调用的高效(高效是相对select/poll这些系统调用的,底层有一个链表和红黑树,避免了轮询,减少了用户空间和系统空间之间的数据传递等),非阻塞(所有的操作都是非阻塞,这样),多进程(master-slave进程模型),这些事实使得nginx成为一个高性能服务器的前提条件。
既然作为一个软件(http服务器),相对于一个网络库而言肯定有更完善的功能。它可以在不需要关闭的前提下实现更新系统;可以接受外部的信号,根据不同的信号做相应的处理;可定制,用户可以开发第三方的模块,根据自我需要将模块添加到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。