nginx核心讲解.docx
- 文档编号:3092042
- 上传时间:2023-05-05
- 格式:DOCX
- 页数:116
- 大小:1.66MB
nginx核心讲解.docx
《nginx核心讲解.docx》由会员分享,可在线阅读,更多相关《nginx核心讲解.docx(116页珍藏版)》请在冰点文库上搜索。
nginx核心讲解
nginx核心讲解《上篇》
第零章
慕名对nginx的源码进行学习研究是早在2009年的事情,当时还在学校,整天呆在实验室里看动漫,时间一久就心感愧疚,觉得还是要趁有空学点东西,恰当时不知从哪里得知高性能服务器是一个很有“前途”的方向,几经搜索又机缘偶合的得识lighttpd与nginx,从此开始在动漫与代码之间来回穿梭,直到毕业。
关于lighttpd与nginx,无需多说,当时lighttpd比nginx要火,所以我先看的lighttpd源码,后看的nginx源码,也因此lighttpd的文档在我读书的时候就写完(虽然写得很矬)了,但nginx的文档写了一些放在电脑里,后来离开学校开始工作后,就把这件事情和这些文档都给搁在那了,直到近一年前,我建了一个个人博客站点(http:
//lenky.info/),为了凑文章数目,才又把它们给找了出来,并且根据最新的nginx源码重新整理了一下,也就是现在你看到的这篇文档。
当然,这只是一部分,所以标题才叫《上篇》。
重新整理主要是因为注意到以前写的文档过细的去逐行注释代码(网上很多nginx源码分析的文章也大多有这个缺点),而此次希望能从比较高一点的角度去解析nginx,让读者尽快的把握全局而不是陷入细节;为了达到这个目标,文档里就尽量的少贴代码多画图,当然,一些必要的代码是不可缺少的,所以你还是会在本文档里看到源代码。
虽然我的个人期望比较好,可惜水平比较差,目前写出来的文档也就这个样了。
:
)
最后,说一下本文档基于的相关环境,虽然列了一个表格如下,其实没那么复杂,我安装的是一个centos6.2的32位虚拟机,其它开发软件包都是centos6.2里所对应提供的,而nginx版本为1.2.0。
软件包
版本
nginx
1.2.0
os
CentOSrelease6.2(Final)/kernel-2.6.32/32bit
gcc
gccversion4.4.620110731(RedHat4.4.6-3)(GCC)
gdb
GNUgdb(GDB)RedHatEnterpriseLinux(7.2-50.el6)
make
GNUMake3.81
文档版本(更新地址:
http:
//lenky.info/ebook/):
版本号
修订时间
0.1
2012-7-20
第一章
进程模型
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()调用处继续等待新的信号,如此反复,构成监控进程的主要执行体。
82:
Filename:
ngx_process_cycle.c
83:
void
84:
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
722:
staticvoid
723:
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通信等等。
一个较为完整的整体架构框图如下所示:
进程通信
运行在多进程模型的nginx在正常工作时,自然就会有多个进程实例,比如下图是在配置“worker_processes4;”情况下的显示,nginx设置的进程title能很好的帮助我们区分监控进程与工作进程,不过带上选项f的ps命令以树目录的形式打印各个进程信息也能帮助我们做这个区分。
多进程联合工作必定要牵扯到进程之间的通信问题,下面就来看看nginx是如何做的。
采用socketpair()函数创造一对未命名的UNIX域套接字来进行Linux下具有亲缘关系的进程之间的双向通信是一个非常不错的解决方案。
nginx就是这么做的,先看fork生成新工作进程的ngx_spawn_process()函数以及相关代码:
21:
Filename:
ngx_process.h
22:
typedefstruct{
23:
ngx_pid_tpid;
24:
intstatus;
25:
ngx_socket_tchannel[2];
26:
…
27:
}ngx_process_t;
28:
…
47:
#defineNGX_MAX_PROCESSES1024
35:
Filename:
ngx_process.c
36:
ngx_process_tngx_processes[NGX_MAX_PROCESSES];
37:
86:
ngx_pid_t
87:
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]给父进程使用,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.c
835:
staticvoid
836:
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_handler)
996:
==NGX_ERROR)
997:
{
998:
…
而在父进程fork()生成一个新子进程后,就会立即通过ngx_pass_open_channel()函数把这个子进程的相关信息告知给其前面已生成的子进程:
430:
Filename:
ngx_process_cycle.c
431:
staticvoid
432:
ngx_pass_open_channel(ngx_cycle_t*cycle,ngx_channel_t*ch)
433:
{
434:
436:
for(i=0;i 437: … 453: ngx_write_channel(ngx_processes[i].channel[0], 454: ch,sizeof(ngx_channel_t),cycle->log); 455: } 456: } 其中参数ch里包含了刚创建的新子进程(假定为A)的pid、进程信息在全局数组里存储下标、socket描述符channel[0]等信息,这里通过for循环遍历所有存活的其它子进程,然后调用函数ngx_write_channel()通过继承的channel[0]描述符进行信息主动告知,而收到这些消息的子进程将执行设置好的回调函数ngx_channel_handler(),把接收到的新子进程A的相关信息存储在全局变量ngx_processes内: 1066: Filename: ngx_process_cycle.c 1067: staticvoid 1068: ngx_channel_handler(ngx_event_t*ev) 1069: { 1070: … 1126: caseNGX_CMD_OPEN_CHANNEL: 1127: … 1132: ngx_processes[ch.slot].pid=ch.pid; 1133: ngx_processes[ch.slot].channel[0]=ch.fd; 1134: break; 1135: … 这样,前后子进程都有了对方的相关信息,相互通信也就没有问题了,这其中还有一些没讲到的具体实现细节,请以关键字“进程之间文件描述符传递”进行Google搜索。 直接看一下实例,就以上面显示的各个父子进程为例: ngx_processes 父-8706 子-8707 子-8708 子-8709 子-8710 [0]-8707-channel {3,7}* {-1,7}** {3,-1} {3,-1} {3,-1} [1]-8708-channel {8,9} {3,0} {-1,9} {8,-1} {8,-1} [2]-8709-channel {10,11} {9,0} {7,0} {-1,11} {10,-1} [3]-8710-channel {12,13} {10,0} {8,0} {7,0} {-1,13} 上表格中,{a,b}分别表示channel[0]和channel[1]的值,带*的{3,7}表示如果父进程8706向子进程8707发送消息,需使用channel[0],即描述符3;而带**的{-1,7}表示如果子进程8707向父进程8706发送消息,需使用channel[1],即描述符7,它的channel[0]为-1表示已经close()关闭掉了(nginx某些地方调用close()时并没有设置对应变量为-1,我这里为了好说明,对已经close()掉的描述符全部标记为-1了); 越是后生成的子进程,其channel[0]与父进程的对应channel[0]值相同的越多,因为基本都是继承而来,但前面生成的子进程的channel[0]是通过传递获得的,所以与父进程的对应channel[0]不一定相等。 比如如果子进程8707向子进程8710发送消息,需使用channel[0],即描述符10,而对应的父进程channel[0]却是12,虽然它们在各自进程里却表现为不同的整型数字,但在内核里表示同一个描述符结构,即不管是子进程8707往描述符10写数据还是父进程8706往描述符12写数据,子进程8710都能通过描述符13正确读取到这些数据。 最后,就目前nginx代码来看,子进程并没有往父进程发送任何消息,子进程之间也没有相互通信的逻辑,也许是因为nginx有其它一些更好的进程通信方式,比如共享内存等,所以这种channel通信目前仅做为父进程往子进程发送消息使用,但由于有这个基础在这,如果未来要使用channel做这样的事情,的确是可以的。 共享内存 共享内存是Linux下进程之间进行数据通信的最有效方式之一,而nginx就为我们提供了统一的操作接口来使用共享内存。 在nginx里,一块完整的共享内存以结构体ngx_shm_zone_t来封装表示,这其中包括的字段有共享内存的名称(shm_zone[i].shm.name)、大小(shm_zone[i].shm.size)、标签(shm_zone[i].tag)、分配内存的起始地址(shm_zone[i].shm.addr)以及初始回调函数(shm_zone[i].init)等: 24: Filename: ngx_cycle.h 25: typedefstructngx_shm_zone_sngx_shm_zone_t; 26: … 27: structngx_shm_zone_s{ 28: void*data; 29: ngx_shm_tshm; 30: ngx_shm_zone_init_ptinit; 31: void*tag; 32: }; 这些字段大都容易理解,只有tag字段需要解释一下,因为看上去它和name字段有点重复,而事实上,name字段主要用作共享内存的唯一标识,它能让nginx知道我想使用哪个共享内存,但它没法让nginx区分我到底是想新创建一个共享内存,还是使用那个已存在的旧的共享内存。 举个例子,模块A创建了共享内存sa,模块A或另外一个模块B再以同样的名称sa去获取共享内存,那么此时nginx是返回模块A已创建的那个共享内存sa给模块A/模块B,还是直接以共享内存名重复提示模块A/模块B出错呢? 不管nginx采用哪种做法都有另外一种情况出错,所以新增一个tag字段做冲突标识,该字段一般也就指向当前模块的ngx_module_t变量即可。 这样在上面的例子中,通过tag字段的帮助,如果模块A/模块B再以同样的名称sa去获取模块A已创建的共享内存sa,模块A将获得它之前创建的共享内存的引用(因为模块A前后两次请求的tag相同),而模块B则将获得共享内存已做它用的错误提示(因为模块B请求的tag与之前模块A请求时的tag不同)。 当我们要使用一个共享内存时,总会在配置文件里加上该共享内存的相关配置信息,而nginx在进行配置解析的过程中,根据这些配置信息就会创建对应的共享内存,不过此时的创建仅仅只是代表共享内存的结构体ngx_shm_zone_t变量的创建,这具体实现在函数shared_memory_add()内。 另外从这个函数中,我们也可以看到nginx使用的所有共享内存都以list链表的形式组织在全局变量cf->cycle->shared_memory下,在创建新的共享内存之前会先对该链表进行遍历查找以及冲突检测,对于已经存在且不存在冲突的共享内存可直接返回引用。 以ngx_http_limit_req_module模块为例,它需要的共享内存在配置文件里以limit_req_zone配置项出现: limit_req_zone$binary_remote_addrzone=one: 10mrate=1r/s; nginx在进行配置解析时,遇到limit_req_zone配置项则调用其对应的处理函数ngx_http_limit_req_zone(),而在该函数内又将继续调用函数shared_memory_add()创建对应的ngx_shm_zone_t结构体变量并加入到全局链表内: ngx_http_limit_req_zone()->ngx_shared_memory_add()->ngx_list_push() 共享内存的真正创建是在配置文件全部解析完后,所有代表共享内存的结构体ngx_shm_zone_t变量以链表的形式挂接在全局变量cf->cycle->shared_memory下,nginx此时遍历该链表并逐个进行实际创建,即分配内存、管理机制(比如锁、slab)初始化等: 398: Filename: ngx_cycle.c 399: /*createsharedmemory*/ 400: 401: part=&cycle->shared_memory.part; 402: shm_zone=part->elts; 403: 404: for(i=0;/*void*/;i++){ 405: … 467: if(ngx_shm_alloc(&shm_zone[i].shm)! =NGX_OK){ 468: … 471: if(ngx_init_zone_pool(cycle,&shm_zone[i])! =NGX_OK){ 472: … 475: if(shm_zone[i].init(&shm_zone[i],NULL)! =NGX_OK){ 476: ... 477: } 其中函数ngx_shm_alloc()是共享内存的实际分配,针对当前系统可提供接口,可以是mmap或shmget等;而ngx_init_zone_pool()函数是共享内存管理机制的初始化,因为共享内存的使用涉及到另外两个主题,第一,既然是共享内存,那么必然是多进程共同使用,所以必须考虑互斥问题;第二,nginx既以性能著称,那么对于共享内存自然也有其独特的使用方式,虽然我们可以不用(在马上要介绍到的init回调函数里做覆盖处理即可),但在这里也默认都会以这种slab的高效访问机制进行初始化。 关于这两点,这里暂且略过,待后续再做讨论。 回调函数shm_zone[i].init()是各个共享内存所特定的,根据使用方的自身需求不同而不同,这也是我们在使用共享内存时需特别注意的函数。 继续看实例ngx_http_limit_req_module模块的init函数ngx_http_limit_req_init_zone(): 398: Filename: ngx_http_limit_req_module.c 399: staticngx_int_t 400: ngx_http_limit_req_init_zone(ngx_shm_zone_t*shm_zone,void*data) 401: { 402: ngx_http_limit_req_ctx_t*octx=data; 403: … 398: if(octx){ 399: … 608: ctx->shpool=octx->shpool; 609: … 608: returnNGX_OK; 609: } 610: 611: ctx->shpool=(ngx_slab_pool_t*)shm_zone->shm.addr; 612: … 608: ctx->sh=ngx_slab_alloc(ctx->shpool,sizeof(ngx_ht
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- nginx 核心 讲解