转载请保持文档完全或保留出处()欢迎访问Lenky个人网站:核心讲解《上篇》第零章慕名对nginx的源码进行学习研究是早在2009年的事情,当时还在学校,整天呆在实验室里看动漫,时间一久就心感愧疚,觉得还是要趁有空学点东西,恰当时不知从哪里得知高性能服务器是一个很有“前途”的方向,几经搜索又机缘偶合的得识lighttpd与nginx,从此开始在动漫与代码之间来回穿梭,直到毕业。关于lighttpd与nginx,无需多说,当时lighttpd比nginx要火,所以我先看的lighttpd源码,后看的nginx源码,也因此lighttpd的文档在我读书的时候就写完(虽然写得很矬)了,但nginx的文档写了一些放在电脑里,后来离开学校开始工作后,就把这件事情和这些文档都给搁在那了,直到近一年前,我建了一个个人博客站点(),为了凑文章数目,才又把它们给找了出来,并且根据最新的nginx源码重新整理了一下,也就是现在你看到的这篇文档。当然,这只是一部分,所以标题才叫《上篇》。重新整理主要是因为注意到以前写的文档过细的去逐行注释代码(网上很多nginx源码分析的文章也大多有这个缺点),而此次希望能从比较高一点的角度去解析nginx,让读者尽快的把握全局而不是陷入细节;为了达到这个目标,文档里就尽量的少贴代码多画图,当然,一些必要的代码是不可缺少的,所以你还是会在本文档里看到源代码。虽然我的个人期望比较好,可惜水平比较差,目前写出来的文档也就这个样了。:)最后,说一下本文档基于的相关环境,虽然列了一个表格如下,其实没那么复杂,我安装的是一个centos6.2的32位虚拟机,其它开发软件包都是centos6.2里所对应提供的,而nginx版本为1.2.0。软件包版本nginx1.2.0osCentOSrelease6.2(Final)/kernel-2.6.32/32bitgccgccversion4.4.620110731(RedHat4.4.6-3)(GCC)gdbGNUgdb(GDB)RedHatEnterpriseLinux(7.2-50.el6)makeGNUMake3.81文档版本(更新地址:):版本号修订时间0.12012-7-20转载请保持文档完全或保留出处()欢迎访问Lenky个人网站:第一章进程模型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循环,这也是一个无限循环,直到进程终止才退出,服务进程基本都是这种写法,所以不用详述,下面先看看这个模型的图示:转载请保持文档完全或保留出处()欢迎访问Lenky个人网站:上图中表现得很明朗,监控进程和工作进程各有一个无限for循环,以便进程持续的等待和处理自己负责的事务,直到进程退出。监控进程的无限for循环内有一个关键的sigsuspend()函数调用,该函数的调用使得监控进程的大部分时间都处于挂起等待状态,直到监控进程接收到信号为止,当监控进程接收到信号时,信号处理函数ngx_signal_handler()就会被执行,我们知道信号处理函数一般都要求足够简单(关于信号处理函数的实现准则请Google),所以在该函数内执行的动作主要也就是根据当前信号值对相应的旗标变量做设置,而实际的处理逻辑必须放在主体代码里来处理,所以该for循环接下来的代码就是判断有哪些旗标变量被设置而需要处理的,比如ngx_reap(有子进程退出?)、ngx_quit或ngx_terminate(进行要退出或终止?注意:虽然两个旗标都是表示结束nginx,不过ngx_quit的结束更优雅,它会让nginx监控进程做一些清理工作且等待子进程也完全清理并退出之后才终止,而ngx_terminate更为粗暴,不过它通过使用SIGKILL信号能保证在一段时间后必定被结束掉)、ngx_reconfigure(重新加载配置?)等。当所有信号都处理完时又挂起在函数sigsuspend()调用处继续等待新的信号,如此反复,构成监控进程的主要执行体。82:Filename:ngx_process_cycle.c83:void84:ngx_master_process_cycle(ngx_cycle_t*cycle)85:{86:…146:for(;;){147:…170:sigsuspend(&set);171:…177:if(ngx_reap){178:…184:if(!live&&(ngx_terminate||ngx_quit)){185:…188:if(ngx_terminate){189:…210:if(ngx_quit){211:…212:}213:…工作进程的执行主体与监控进程类似,不过工作进程既名之为工作进程,那么它的主要关注点就是与客户端或后端真实服务器(此时nginx作为中间代理)之间的数据可读/可写等交互事件,而不是进程信号,所以工作进程的阻塞点是在像select()、epoll_wait()等这样的I/O多路复用函数调用处,以等待发生数据可读/可写事件,当然,也可能被新收到的进程信号中断。关于I/O多路复用的更多细节,请参考其他章节。721:Filename:ngx_process_cycle.c转载请保持文档完全或保留出处()欢迎访问Lenky个人网站::staticvoid723:ngx_worker_process_cycle(ngx_cycle_t*cycle,void*data)724:{725:…780:for(;;){781:782:if(ngx_exiting){783:…806:ngx_process_events_and_timers(cycle);807:808:if(ngx_terminate){809:…810:}811:…整体架构如前面介绍的那样,正常执行起来后的Nginx会有多个进程,最基本的有master_process和worker_process,还可能会有cache相关进程(这在后面会具体讲到)。除了自身进程之间的相互通信,Nginx还凭借强悍的模块功能与外界四通八达,比如通过upstream与webserver通信、依靠fastcgi与applicationserver通信等等。一个较为完整的整体架构框图如下所示:转载请保持文档完全或保留出处()欢迎访问Lenky个人网站:进程通信运行在多进程模型的nginx在正常工作时,自然就会有多个进程实例,比如下图是在配置“worker_processes4;”情况下的显示,nginx设置的进程title能很好的帮助我们区分监控进程与工作进程,不过带上选项f的ps命令以树目录的形式打印各个进程信息也能帮助我们做这个区分。多进程联合工作必定要牵扯到进程之间的通信问题,下面就来看看nginx是如何做的。采用socketpair()函数创造一对未命名的UNIX域套接字来进行Linux下具有亲缘关系的进程之间的双向通信是一个非常不错的解决方案。nginx就是这么做的,先看fork生成新工作进程的ngx_spawn_process()函数以及相关代码:21:Filename:ngx_process.h22:typedefstruct{23:ngx_pid_tpid;24:intstatus;25:ngx_socket_tchannel[2];26:…27:}ngx_process_t;28:…47:#defineNGX_MAX_PROCESSES102435:Filename:ngx_process.c36:ngx_process_tngx_processes[NGX_MAX_PROCESSES];37:86:ngx_pid_t87:ngx_spawn_process(ngx_cycle_t*cycle,ngx_spawn_proc_ptproc,void*data,88:char*name,ngx_int_trespawn)89:{90:…117:if(socketpair(AF_UNIX,SOCK_STREAM,0,ngx_processes[s].channel)==-1)118:…186:pid=fork();187:…在该函数进行fork()之前,先调用了socketpair()创建一对socket描述符存放在变量ngx_processes[s].channel内(其中s标志在ngx_processes数组内第一个可用元素的下标,比如最开始产生第一个工作进程时,可用元素的下标s为0),而在fork()之后,由于子进程继承了父进程的资源,那么父子进程就都有了这一对socket描述符,而nginx将channel[0]给父转载请保持文档完全或保留出处()欢迎访问Lenky个人网站:进程使用,channel[1]给子进程使用,这样分别错开的使用不同socket描述符,即可实现父子进程之间的双向通信:除此之外,对于各个子进程之间,也可以进行双向通信。如前面所述,父子进程的通信channel设定是自然而然的事情,而子进程之间的通信channel设定就涉及到进程之间文件描述符(socket描述符也属于文件描述符)的传递,因为虽然后生成的子进程通过继承的channel[0]能够往前生成的子进程发送信息,但前生成的子进程无法获知后生成子进程的channel[0]而不能发送信息,所以后生成的子进程必须利用已知的前生成子进程的channel[0]进行主动告知,下面来看看这个具体是怎样的。在子进程的启动初始化函数ngx_worker_process_init()里,会把ngx_channel(也就是channel[1])加入到读事件监听集里,对应的回调处理函数为ngx_channel_handler():834:Filename:ngx_process_cycle.c835:staticvoid836:ngx_worker_process_init(ngx_cycle_t*cycle,ngx_uint_tpriority)837:{838:…994:if(ngx_add_channel_event(cycle,ngx_channel,NGX_READ_EVENT,995:ngx_channel_handle