nginx核心讲解《上篇》

第零章

       慕名对nginx的源码进行学习研究是早在2009年的事情,当时还在学校,整天呆在实验室里看动漫,时间一久就心感愧疚,觉得还是要趁有空学点东西,恰当时不知从哪里得知高性能服务器是一个很有“前途”的方向,几经搜索又机缘偶合的得识lighttpdnginx,从此开始在动漫与代码之间来回穿梭,直到毕业。

       关于lighttpdnginx,无需多说,当时lighttpdnginx要火,所以我先看的lighttpd源码,后看的nginx源码,也因此lighttpd的文档在我读书的时候就写完(虽然写得很矬)了,但nginx的文档写了一些放在电脑里,后来离开学校开始工作后,就把这件事情和这些文档都给搁在那了,直到近一年前,我建了一个个人博客站点(http://lenky.info/),为了凑文章数目,才又把它们给找了出来,并且根据最新的nginx源码重新整理了一下,也就是现在你看到的这篇文档。当然,这只是一部分,所以标题才叫《上篇》。

       重新整理主要是因为注意到以前写的文档过细的去逐行注释代码(网上很多nginx源码分析的文章也大多有这个缺点),而此次希望能从比较高一点的角度去解析nginx,让读者尽快的把握全局而不是陷入细节;为了达到这个目标,文档里就尽量的少贴代码多画图,当然,一些必要的代码是不可缺少的,所以你还是会在本文档里看到源代码。虽然我的个人期望比较好,可惜水平比较差,目前写出来的文档也就这个样了。:)

       最后,说一下本文档基于的相关环境,虽然列了一个表格如下,其实没那么复杂,我安装的是一个centos 6.232位虚拟机,其它开发软件包都是centos 6.2里所对应提供的,而nginx版本为1.2.0

软件包

版本

nginx

1.2.0

os

CentOS release 6.2 (Final)/kernel-2.6.32/32bit

gcc

gcc version 4.4.6 20110731 (Red Hat 4.4.6-3) (GCC)

gdb

GNU gdb (GDB) Red Hat Enterprise Linux (7.2-50.el6)

make

GNU Make 3.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_quitngx_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:  static void

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_processworker_process,还可能会有cache相关进程(这在后面会具体讲到)。除了自身进程之间的相互通信,Nginx还凭借强悍的模块功能与外界四通八达,比如通过upstreamweb server通信、依靠fastcgiapplication server通信等等。一个较为完整的整体架构框图如下所示:

 

进程通信

运行在多进程模型的nginx在正常工作时,自然就会有多个进程实例,比如下图是在配置“worker_processes  4;”情况下的显示,nginx设置的进程title能很好的帮助我们区分监控进程与工作进程,不过带上选项fps命令以树目录的形式打印各个进程信息也能帮助我们做这个区分。多进程联合工作必定要牵扯到进程之间的通信问题,下面就来看看nginx是如何做的。

采用socketpair()函数创造一对未命名的UNIX域套接字来进行Linux下具有亲缘关系的进程之间的双向通信是一个非常不错的解决方案。nginx就是这么做的,先看fork生成新工作进程的ngx_spawn_process()函数以及相关代码:

21:      Filename : ngx_process.h

22:      typedef struct {

23:          ngx_pid_t           pid;

24:          int                 status;

25:          ngx_socket_t        channel[2];

26:     

27:      } ngx_process_t;

28:     

47:      #define NGX_MAX_PROCESSES         1024

 

35:      Filename : ngx_process.c

36:      ngx_process_t    ngx_processes[NGX_MAX_PROCESSES];

37:       

86:      ngx_pid_t

87:      ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,

88:          char *name, ngx_int_t respawn)

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数组内第一个可用元素的下标,比如最开始产生第一个工作进程时,可用元素的下标s0),而在fork()之后,由于子进程继承了父进程的资源,那么父子进程就都有了这一对socket描述符,而nginxchannel[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:  static void

836:  ngx_worker_process_init(ngx_cycle_t *cycle, ngx_uint_t priority)

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:  static void

432:  ngx_pass_open_channel(ngx_cycle_t *cycle, ngx_channel_t *ch)

433:  {

434:   

436:      for (i = 0; i < ngx_last_process; 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:             static void

1068:             ngx_channel_handler(ngx_event_t *ev)

1069:             {

1070:            

1126:                     case NGX_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:      typedef struct ngx_shm_zone_s  ngx_shm_zone_t;

26:     

27:      struct ngx_shm_zone_s {

28:          void                     *data;

29:          ngx_shm_t                 shm;

30:          ngx_shm_zone_init_pt      init;

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_addr zone=one:10m rate=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:      /* create shared memory */

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()是共享内存的实际分配,针对当前系统可提供接口,可以是mmapshmget等;而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:  static ngx_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:         return NGX_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_http_limit_req_shctx_t));

609: 

函数ngx_http_limit_req_init_zone()的第二个参数data表示‘旧’数据,在进行重新加载配置时(即nginx收到SIGHUP信号)该值将不为空,如果旧数据可继续使用,那么可直接返回NGX_OK;否则,需根据自身模块逻辑对共享内存的使用做相关初始化,比如ngx_http_limit_req_module模块,在第634642行直接使用默认已初始化好的slab机制,进行内存的分配等。当函数ngx_http_limit_req_init_zone()正确执行结束,一个完整的共享内存就已创建并初始完成,接着要做的就是共享内存的使用,这即回到前面提到的两个主题:互斥与slab

要解决互斥问题,无非就是利用锁机制,强制同一时刻只能有一个进程在访问共享内存,其基本原理就是利用共享的简单资源(比如最简单的原子变量)来代表复杂资源,一个进程在需要操作复杂资源之前先获得对简单资源的使用权限;因为简单资源足够简单,对它的使用权限的获取往往只有一步或几步,所以更容易避免冲突;这个应该是容易理解的,比如一个需要100步的操作肯定比一个只需要3步的操作更容易发生冲突(每一步需要的复杂度相同),因为前一种情况可能会一个进程在进行了99步后却因另外一个进程发出动作而失败,而后一种情况的进程执行完3步后就已经获得完全使用权限了。

要讲清楚nginx互斥锁的实现,如果不结合具体的代码恐怕是不行的,因为都是一些细节上的考量,比如处理各种不同的CPU架构、使用不同的共享简单资源(原子变量或文件描述符),并没有什么特别难以理解的地方,由于我并不想在这上半篇里出现大量的代码,所以具体实现这里暂且不讲,还好nginx互斥锁的使用非常简单,提供的接口函数以及含义如下:

函数

含义

ngx_shmtx_create()

创建

ngx_shmtx_destory()

销毁

ngx_shmtx_trylock()

尝试加锁(加锁失败则直接返回,不等待)

ngx_shmtx_lock()

加锁(持续等待,直到加锁成功)

ngx_shmtx_unlock()

解锁

ngx_shmtx_force_unlock()

强制解锁(可对其它进程进行解锁)

ngx_shmtx_wakeup()

唤醒等待加锁进程(系统支持信号量的情况下才可用)

 

slab机制

nginxslab机制与linuxslab机制在基本原理上并没有什么特别大的不同(当然,相比而言,linuxslab机制要复杂得多),简单来说也就是基于两点:缓存与对齐。缓存意味着预分配,即提前申请好内存并对内存做好划分形成内存池,当我们需要使用一块内存空间时,nginx就直接从已经申请并划分好的内存池里取出一块合适大小的内存即可,而内存的释放也是把内存返还给nginx的内存池,而不是操作系统;对齐则意味着内存的申请与分配总是按2的幂次方进行,即内存大小总是为8163264等,比如,虽然只申请33个字节的内存,但也将获得实际64字节可用大小的内存,这的确存在一些内存浪费,但对于内存性能的提升是显著的(关于内存对齐对性能的影响,可以参考:http://lenky.info/?p=310),更重要的是把内部碎片也掌握在可控的范围内。

nginxslab机制主要是和共享内存一起使用,前面提到对于共享内存,nginx在解析完配置文件,把即将使用的共享内存全部以list链表的形式组织在全局变量cf->cycle->shared_memory下之后,就会统一进行实际的内存分配,而nginxslab机制要做的就是对这些共享内存进行进一步的内部划分与管理,关于这点,从函数ngx_slab_init()的逻辑即可初见端倪,不过在此之前,先看看ngx_init_zone_pool()函数对它的调用:

916:  Filename : ngx_slab.c

917:  static ngx_int_t

918:  ngx_init_zone_pool(ngx_cycle_t *cycle, ngx_shm_zone_t *zn)

919:  {

920:      u_char           *file;

921:      ngx_slab_pool_t  *sp;

922:   

923:      sp = (ngx_slab_pool_t *) zn->shm.addr;

924: 

937:      sp->end = zn->shm.addr + zn->shm.size;

938:      sp->min_shift = 3;

939:      sp->addr = zn->shm.addr;

940: 

960:      ngx_slab_init(sp);

961: 

函数ngx_init_zone_pool()是在共享内存分配好后进行的初始化调用,而该函数内又调用了本节介绍的重点对象slab的初始化函数ngx_slab_init();,此时的情况图示如下:

可以看到此时共享内存的开始部分内存已经被用作结构体ngx_slab_pool_t的存储空间,这相当于是slab机制的额外开销(overhead),后面还会看到其他额外开销,任何一种管理机制都有自己的一些控制信息需要存储,所以这些内存使用是无法避免的。共享内存剩下的部分才是被管理的主体,slab机制对这部分内存进行两级管理,首先是page页,然后是page页内的slab块(通过slot对相等大小的slab块进行管理,为了区分slab机制,下面以slot块来指代这些slab块),也就是说slot块是在page页内存的再一次管理。

在继续对slab机制分析之前,先看看下面这个表格里记录的一些变量以及其对应的值,因为它们可以帮助我们对后面内容的理解。这些变量会根据系统环境的不同而不同,但一旦系统环境确定,那么这些值也就将都是一些常量值,下面表格基于的系统环境在本书最开始有统一介绍,这里不再累述:

变量名

描述

ngx_pagesize

4096

系统内存页大小,Linux下一般情况就是4KB

ngx_pagesize_shift

12

对应ngx_pagesize4096),即是4096 = 1 << 12;

ngx_slab_max_size

2048

slots分配和pages分配的分割点,大于等于该值则需从pages里分配。

ngx_slab_exact_size

128

正好能用一个uintptr_t类型的位图变量表示的页划分;比如在4KB内存页、32位系统环境下,一个uintptr_t类型的位图变量最多可以对应表示32个划分块的状态,所以要恰好完整的表示一个4KB内存页的每一个划分块状态,必须把这个4KB内存页划分为32块,即每一块大小为:

ngx_slab_exact_size = 4096 / 32 = 128

ngx_slab_exact_shift

7

对应ngx_slab_exact_size128),即是128 = 1 << 7;

pool->min_shift

3

固定值为3

pool->min_size

8

固定值为8,最小划分块大小,即是1 << pool->min_shift;

好,再来看slab机制对page页的管理,初始结构示意图如下:

slab机制对page页的静态管理主要体现在ngx_slab_page_t[K]page[N] 这两个数组上,需要解释几点:

第一,虽然是一个页管理结构(即ngx_slab_page_t元素)与一个page内存页相对应,但因为有对齐消耗以及slot块管理结构体的占用(图中的ngx_slab_page_t[n]数组),所以实际上页管理结构体数目比page页内存数目要多,即图中的ngx_slab_page_t[N]ngx_slab_page_t[K-1],这些结构体完全被忽视,我们也不用去管它们,只是需要知道有这些东西的存在。

第二,如何根据页管理结构page获得对应内存页的起始地址p?计算方法如下:

384:  Filename : ngx_slab.c

385:              p = (page - pool->pages) << ngx_pagesize_shift;

386:              p += (uintptr_t) pool->start;

对照前面图示来看这很明显,无需过多解释;相反,根据内存页的起始地址p也能计算出其对应的页管理结构page

第三,对齐是指实际page内存页按ngx_pagesize大小对齐,从图中看就是原本的start是那个虚线箭头所指的位置,对齐后就是实线箭头所指的位置,对齐能提高对内存页的访问速度,但这有一些内存浪费,并且末尾可能因为不够一个page内存页而被浪费掉,所以在ngx_slab_init()函数的最末尾有一次最终可用内存页的准确调整:

75:      Filename : ngx_cycle.c

76:      void

77:      ngx_slab_init(ngx_slab_pool_t *pool)

78:      {

79:     

130:      m = pages - (pool->end - pool->start) / ngx_pagesize;

131:      if (m > 0) {

132:          pages -= m;

133:          pool->pages->slab = pages;

134:      }

135: 

130行计算的m值如果大于0,说明对齐等操作导致实际可用内存页数减少,所以后面的if语句进行判断调整。

page页的静态管理结构基本就是如此了,再来看page页的动态管理,即page页的申请与释放,这就稍微麻烦一点,因为一旦page页被申请或释放,那么就有了相应的状态:使用或空闲。先看空闲页的管理,nginx对空闲page页进行链式管理,链表的头节点pool->free,初始状态下的链表情况如下:

这是一个有点特别的链表,它的节点可以是一个数组,比如上图中的ngx_slab_page_t[N] 数组就是一个链表节点,这个数组通过第0号数组元素,即ngx_slab_page_t[0],接入到这个空闲page页链表内,并且整个数组的元素个数也记录在这个第0号数组元素的slab字段内。

如果经历如下几步内存操作:子进程1从共享内存中申请1页,子进程2接着申请了2页,然后子进程1又释放掉刚申请的1页,那么空闲链表各是一个什么状态呢?逐步来看。

子进程1从共享内存中申请1页:

子进程2接着申请了2页:

然后子进程1又释放掉刚申请的1页:

释放的page页被插入到链表头部,如果子进程2接着释放其拥有的那2页内存,那么空闲链表结构将如下图所示:

可以看到,nginx对空闲page页的链式管理不会进行节点合并,不过关系不大,毕竟page页既不是slab机制的最小管理单元,也不是其主要分配单元。对处于使用状态中的page页,也是采用的链式管理,在介绍其详细之前,需先来看看slab机制的第二级管理机制,即slot块,这样便于前后的连贯理解。

slot块是对每一页page内存的内部管理,它将page页划分成很多小块,各个page页的slot块大小可以不相等,但同一个page页的slot块大小一定相等。page页的状态通过其所在的链表即可辨明,而page页内各个slot块的状态却需要一个额外的标记,在nginx的具体实现里采用的是位图方式,即一个bit位标记一个对应slot块的状态,1为使用,0为空闲。

根据slot块的大小不同,一个page页可划分的slot块数也不同,从而需要的位图大小也不一样。前面提到过,每一个page页对应一个名为ngx_slab_page_t的管理结构,该结构体有一个uintptr_t类型的slab字段。在32位平台上(也就是本书讨论的设定平台),uintptr_t类型占4个字节,即slab字段有32bit位。如果page页划分的slot块数小于等于32,那么nginx直接利用该字段充当位图,这在nginx内叫exact划分,每个slot块的大小保存在全局变量ngx_slab_exact_size以及ngx_slab_exact_shift内。比如,14KBpage页,如果每个slot块大小为128字节,那么恰好可划分成32块。下图是这种划分下的一种可能的中间情况:

如果划分的每个slot块比ngx_slab_exact_size还大,那意味着一个page页划分的slot块数更少,此时当然也是使用ngx_slab_page_t结构体的slab字段作为位图。由于比ngx_slab_exact_size大的划分可以有很多种,所以需要把其具体的大小也记录下来,这个值同样也记录在slab字段里。这样做是可行的,由于划分总是按2次幂增长,所以比ngx_slab_exact_size还大的划分至少要减少一半的slot块数,因此利用slab字段的一半bit位即可完整表示所有slot块的状态。具体点说就是:slab字段的高端bit用作位图,低端bit用于存储slot块大小(仅存其对应的移位数)。代码表现为:

378:  Filename : ngx_slab.c

379:              page->slab = ((uintptr_t) 1 << NGX_SLAB_MAP_SHIFT) | shift;

如果申请的内存大于等于ngx_slab_max_sizenginx直接返回一个page整页,此时已经不在slot块管理里,所有无需讨论。下面来看小于ngx_slab_exact_size的情况,此时slot块数目已经超出了slab字段可表示的容量。比如假设按8字节划分,那么14KBpage页将被划分为512块,表示各个slot块状态的位图也就需要512bit位,一个slab字段明显是不足够的,所以需要为位图另找存储空间,而slab字段仅用于存储slot块大小(仅存其对应的移位数)。

另找的位图存储空间就落在page页内,具体点说是其划分的前面几个slot块内。接着刚才说的例子,512bit位的位图,即64个字节,而一个slot块有8个字节,所以就需要占用page页的前8slot块用作位图。即,一个按8字节划分slot块的page页初始情况如下图所示:

由于前几个slot块一开始就被用作位图空间,所以必须把它们对应的bit位设置为1,表示其状态为使用。

不论哪种情况,都有了slot块的大小以及状态,那对slot块的分配与释放就水到渠成了。下面回到slab机制的最后一个话题,即对处于使用状态中的page页的链式管理。其实很简单,首先,根据每页划分的slot块大小,将各个page页加入到不同的链表内。在我们这里设定的平台上,也就是按816326412825651210242048一共9条链表,在ngx_slab_init()函数里有其初始化:

102:  Filename : ngx_slab.c

103:      n = ngx_pagesize_shift - pool->min_shift;

104:   

105:      for (i = 0; i < n; i++) {

106:          slots[i].slab = 0;

107:          slots[i].next = &slots[i];

108:          slots[i].prev = 0;

109:      }

假设申请一块8字节的内存,那么slab机制将分配一共page页,将它按8字节做slot划分,并且接入到链表slots[0]内,相关示例(表示这只是其中一处实现)代码:

352:  Filename : ngx_slab.c

353:              page->slab = shift;

354:              page->next = &slots[slot];

355:              page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_SMALL;

356:   

357:              slots[slot].next = page;

page->prev4字节对齐,所以末尾两位可以用做它用,这里用于标记当前slot划分类型为NGX_SLAB_SMALL,图示如下:

继续申请8字节的内存不会分配新的page页,除非刚才那页page(暂且称之为页A)被全是使用完,一旦页A被使用完,它会被拆除出链表,相关示例代码:

232:  Filename : ngx_slab.c

233:          prev = (ngx_slab_page_t *)

234:                      (page->prev & ~NGX_SLAB_PAGE_MASK);

235:          prev->next = page->next;

236:          page->next->prev = page->prev;

237:   

238:          page->next = NULL;

239:          page->prev = NGX_SLAB_SMALL;

234行是过滤掉末尾的标记位,以获得正确的前节点的地址,此时的图示如下:

如果仍然继续申请8字节的内存,那么nginxslab机制必须分配新的page页(暂且称之为页B),类似于前面介绍的那样,页B会被加入到链表内,此时链表中只有一个节点,但如果此时页A释放了某个slot块,它又会被加入到链表中,终于形成了具有两个节点的链表,相关示例代码(变量page指向页A)以及图示如下:

455:  Filename : ngx_slab.c

456:          page->next = slots[slot].next;

457:          slots[slot].next = page;

458:   

459:          page->prev = (uintptr_t) &slots[slot] | NGX_SLAB_SMALL;

460:          page->next->prev = (uintptr_t) page | NGX_SLAB_SMALL;

 

第二章

利用日志信息跟踪

优秀的程序都会带有自己的日志输出接口,并且一般会给出不同等级的输出,以便于重次信息的过滤,比如Linux内核的日志输出标准接口为printk,并且给出了KERN_EMERGKERN_ALERTKERN_DEBUG等这样的输出等级,nginx与此类似,下面具体来看。

为了获取最丰富的日志信息,我们在编译nginx前进行configure配置时,需要把--with-debug选项加上,这样能生成一个名为NGX_DEBUG的宏,而在nginx源码内,该宏被用作控制开关,如果没有它,那么很多日志逻辑代码将在make编译时直接跳过,比如对单连接的debug_connection调试指令、分模块日志调试debug_http功能等:

00:      Filename : ngx_auto_config.h

01:      #define NGX_CONFIGURE " --with-debug"

02:       

03:      #ifndef NGX_DEBUG

04:      #define NGX_DEBUG  1

05:      #endif

 

620:  Filename : nginx.c

621:  #if (NGX_DEBUG)

622:      {

623:      char  **e;

624:      for (e = env; *e; e++) {

625:          ngx_log_debug1(NGX_LOG_DEBUG_CORE, cycle->log, 0, "env: %s", *e);

626:      }

627:      }

628:  #endif

有了上面这个编译前提条件之后,我们还想在配置文件里做恰当的设置,关于这点nginx提供的主要配置指令为error_log,该配置项的默认情况(默认值定义在objs/ngx_auto_config.h文件内)为:

error_log logs/error.log error;

表示日志信息记录在logs/error.log(如果没改变nginx的工作路径的话,那么默认父目录为/usr/local/nginx/)文件内,而日志记录级别为error

在实际进行配置时,可以修改日志信息记录文件路径(比如修改为/dev/null,此时所有日志信息将被输出到所谓的linux黑洞设备)或直接输出到标准终端(此时指定为stderr),而nginx提供的日志记录级别一共有8级,等级从低到高分别为debuginfonoticewarnerrorcritalertemerg,如果设置为error,则表示nginx内等级为errorcritalertemerg4种日志将被输出到日志文件或标准终端,另外的debuginfonoticewarn4种日志将被直接过滤掉而不会输出,因此如果我们只关注特别严重的信息则只需将日志等级设置为emerg即可大大减少nginx的日志输出量,这样就避免了在大量的日志信息里寻找重要信息的麻烦。

当我们利用日志跟踪nginx时,需要获取最大量的日志信息,所以此时可以把日志等级设置为最低的debug级,在这种情况下,如果觉得调试日志太多,nginx提供按模块控制的更细粒等级:debug_coredebug_allocdebug_mutexdebug_eventdebug_httpdebug_imap,比如如果只想看http的调试日志,则需这样设置:

error_log logs/error.log debug_http

此时nginx将输出从infoemerg所有等级的日志信息,而debug日志则将只输出与http模块相关的内容。

error_log配置指令可以放在配置文件的多个上下文内,比如mainhttpserverlocation,但同一个上下文中只能设置一个error_log,否则nginx将提示类似如下这样的错误:

nginx: [emerg] "error_log" directive is duplicate in /usr/local/nginx/conf/nginx.conf:9

但在不同的配置文件上下文里可以设置各自的error_log配置指令,通过设置不同的日志文件,这是nginx提供的又一种信息过滤手段:

00:      Filename : example.conf

01:      ...

02:      error_log  logs/error.log error;

03:      ...

04:      http {

05:          error_log  logs/http.log debug;

06:          ...

07:          server {

08:              ...

09:              error_log  logs/server.log debug;

10:      ...

nginx提供的另一种更有针对性的日志调试信息记录是针对特定链接的,这通过debug_connection配置指令来设置,比如如下设置调试日志仅针对ip地址192.168.1.1ip192.168.10.0/24

events {

    debug_connection 192.168.1.1;

    debug_connection 192.168.10.0/24;

}

nginx的日志功能仍在不断的改进,如能利用得好,对于我们跟着nginx还是非常有帮助的,至少我知道有不少朋友十分习惯于c库的printf打印调试,相比如此,nginxngx_log_xxx要强大得多。

 

利用gdb调试

一般来说,默认./configure 生成的makefile文件都将带上-g选项,这对于利用gdb调试nginx是非常必要的,但如果在使用gdb调试nginx时提示“No symbol table is loaded.  Use the "file" command.”,则需检查objs/Makefile文件里的CFLAGS标记是否带上了-g选项;另一个值得关注的编译选项是-O0,如果在gdb内打印变量提示“<value optimized out>”,则是因为gcc优化导致,我们可以加上-O0选项禁止gcc的编译优化。如何把“-g -O0应用在nginx上可以有三种方法:

1,  在进行configure配置时,按如下方式执行:

[root@localhost nginx-1.2.0]# CFLAGS="-g -O0" ./configure

2,  直接修改文件objs/Makefile给其加上“-g -O0

3,  在执行make时,按如下方式执行:

[root@localhost nginx-1.2.0]# make CFLAGS="-g -O0"

23两种方法是在我们已经执行configure之后进行的,如果之前已经执行过make,那么还需刷新所有源文件的时间戳,以便重新编译nginx

[root@localhost nginx-1.2.0]# find . -name "*.c" | xargs touch

nginx默认以daemon形式运行,并且默认包含有监控进程和多个工作进程,所以如果要直接在gdb内执行nginx并让gdb捕获nginx的监控,则需要在nginx的配置文件里做如下设置:

daemon off;

这样nginx不再是daemon进程,此时利用gdb可以从nginxmain()函数开始调试,默认情况下调试的是监控进程的流程,如果要调试工作进程的流程需要在进入gdb后执行set follow-fork-mode child,更简单的方法是直接设置master_process off;,将监控进程逻辑和工作进程逻辑全部合在一个进程里调试。这些设置对于调试像配置信息解析流程这一类初始逻辑是非常重要的,因为nginx的这些逻辑是在nginx启动时进行的。总之,不管怎么样做,你都必须让gdb attach到你想要调试的进程上,需特别注意fork()这样的函数调用,因为一调用该函数,程序就一分为二,而gdb默认是继续attach父进程,如果父进程后续动作是直接退出(比如ngx_daemon()函数),那么就导致gdb跟丢了。

gdb带参数运行nginx有很多种方法,比如:Shell里执行gdb --args ./objs/nginx -c /usr/local/nginx/conf/nginx.conf,进入到gdbr即可;或者Shell里执行gdb ./objs/nginx,进入到gdb后执行r -c /usr/local/nginx/conf/nginx.conf,等等。

而另外的一般情况下,如果我们调试的是nginx的中间执行主过程,那么我们可以先执行nginx,然后根据nginx进程的进程号进行gdb绑定来做调试。首先,需要找到对应的进程号:

[root@localhost ~]# ps -efH | grep nginx

root      3971 24701  0 12:20 pts/4    00:00:00         grep nginx

root      3905     1  0 12:16 ?        00:00:00   nginx: master process ./nginx

nobody    3906  3905  0 12:16 ?        00:00:00     nginx: worker process

nobody    3907  3905  0 12:16 ?        00:00:00     nginx: worker process

[root@localhost ~]#

nginx代码还给nginx进程加上了title,所以根据标题很容易区分出哪个是监控进程,哪些个是工作进程。对工作进程3906gdb调试,可以利用gdb-p命令行参数:

[root@localhost ~]# gdb -p 3906

或者是在进入gdb后执行:

(gdb) attach 3906

都可以。

nginx特定进程绑定到gdb后,剩余的调试操作无非就是gdb的使用,这可以参考官方手册(http://www.gnu.org/software/gdb/documentation/),手册内容很多,因为gdb提供的功能非常丰富,而某些功能对于我们调试nginx也大有帮助,像Break conditionsWatchpoints等。以Watchpoints(监视点)为例,它可以监视某个变量在什么时候被修改,这对于我们了解nginx的程序逻辑是非常有帮助的,比如在理解nginx的共享内存逻辑时,看到ngx_shared_memory_add()函数内初始化的shm_zone->init回调为空:

1256:             Filename : ngx_cycle.c

1257:             ngx_shm_zone_t *

1258:             ngx_shared_memory_add(ngx_conf_t *cf, ngx_str_t *name, size_t size, void *tag)

1259:             {

1260:            

1318:                 shm_zone->init = NULL;

1319:            

而在ngx_init_cycle()函数里对该回调函数却是直接执行而并没有做前置判空处理:

41:      Filename : ngx_cycle.c

42:      ngx_cycle_t *

43:      ngx_init_cycle(ngx_cycle_t *old_cycle)

44:      {

45:     

475:          if (shm_zone[i].init(&shm_zone[i], NULL) != NGX_OK) {

476:              goto failed;

477:          }

478: 

这就说明这个函数指针一定是在其它某处被再次赋值,但具体是在哪里呢?搜索nginx全部源代码可能一下子没找到对应的代码行,那么此时可利用gdbWatchpoints功能进行快速定位:

(gdb) b ngx_cycle.c:1318

Breakpoint 1 at 0x805d7ce: file src/core/ngx_cycle.c, line 1318.

(gdb) r

Starting program: /home/gqk/nginx-1.2.0/objs/nginx -c /usr/local/nginx/conf/nginx.conf.upstream.sharedmem

[Thread debugging using libthread_db enabled]

 

Breakpoint 1, ngx_shared_memory_add (cf=0xbffff39c, name=0xbfffeed8, size=134217728, tag=0x80dbd80) at src/core/ngx_cycle.c:1318

1318           shm_zone->init = NULL;

Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.47.el6.i686 nss-softokn-freebl-3.12.9-11.el6.i686 openssl-1.0.0-20.el6.i686 pcre-7.8-3.1.el6.i686 zlib-1.2.3-27.el6.i686

(gdb) p &shm_zone->init

$1 = (ngx_shm_zone_init_pt *) 0x80eba68

(gdb) watch *(ngx_shm_zone_init_pt *) 0x80eba68

Hardware watchpoint 2: *(ngx_shm_zone_init_pt *) 0x80eba68

(gdb) c

Continuing.

Hardware watchpoint 2: *(ngx_shm_zone_init_pt *) 0x80eba68

 

Old value = (ngx_shm_zone_init_pt) 0

New value = (ngx_shm_zone_init_pt) 0x809d9c7 <ngx_http_file_cache_init>

ngx_http_file_cache_set_slot (cf=0xbffff39c, cmd=0x80dc0d8, conf=0x0) at src/http/ngx_http_file_cache.c:1807

1807           cache->shm_zone->data = cache;

shm_zone->init = NULL;代码对应的第1318行先下一个Breakpoint,执行nginx后将在此处停止程序,通过p打印获取shm_zone->init的地址值,然后直接给shm_zone->init对应的地址下Breakpoint进行监视,这样即便是跑出shm_zone->init变量所在的作用域也没有关系,执行c命令继续执行nginx,一旦shm_zone->init被修改,那么就停止在进行修改的代码的下一行,Old valueNew value也被gdb抓取出来,可以看到修改逻辑在第1806行(我这里是以proxy_cache所用的共享内存作为示例,而在其它实例情况下,将可能与此不同):

1084:             Filename : ngx_http_file_cache.c

1085:            

1086:                 cache->shm_zone->init = ngx_http_file_cache_init;

1087:                 cache->shm_zone->data = cache;

其实,nginx本身对于gdb也有相关辅助支持,这表现在配置指令debug_points上,对于该配置项的配置值可以是stopabort。当nginx遇到严重错误时,比如内存超限或其他不可意料的逻辑错误,就会调用ngx_debug_point()函数(类似于assert这样的断言,只是函数ngx_debug_point()本身不带判断),该函数根debug_points据配置指令的设置做相应的处理。

如果将debug_points设置为stop,那么ngx_debug_point()函数的调用将nginx进程进入到暂停状态,以便我们通过gdb接入查看相关进程上下文信息:

[root@localhost ~]# ps aux | grep nginx

root      4614  0.0  0.0  24044   592 ?        Ts   12:48   0:00 ./nginx

root      4780  0.0  0.1 103152   800 pts/4    S+   13:00   0:00 grep nginx

[root@localhost ~]#

注意上面的./nginx状态为Tss代表nginx进程为一个会话首进程session leader),其中T就代表nginx进程处在TASK_STOPPED状态,此时我们用gdb连上去即可看到问题所在(我这里只是一个测试,在main函数里主动调用ngx_debug_point()而已,所以下面看到的bt堆栈很简单,实际使用时,我们当然要把该函数放在需要观察的代码点):

[root@localhost ~]# gdb -q -p 4614

Attaching to process 4614

Reading symbols from /usr/local/nginx/sbin/nginx...done.

...

openssl-1.0.0-4.el6.x86_64 pcre-7.8-3.1.el6.x86_64 zlib-1.2.3-25.el6.x86_64

(gdb) bt

#0  0x0000003a9ea0f38b in raise () from /lib64/libpthread.so.0

#1  0x0000000000431a8a in ngx_debug_point () at src/os/unix/ngx_process.c:603

#2  0x00000000004035d9 in main (argc=1, argv=0x7fffbd0a0c08) at src/core/nginx.c:406

(gdb) c

Continuing.

 

Program received signal SIGTERM, Terminated.

执行c命令,nginx即自动退出。

如果将debug_points设置为“debug_points abort;”,此时调用ngx_debug_point()函数将直接abort崩溃掉,如果对OS做了恰当的设置,那么将获得对应的core文件,这就非常方便我们进行事后的慢慢调试,延用上面的直接在main函数里主动调用ngx_debug_point()的例子:

[root@localhost nginx]# ulimit -c

0

[root@localhost nginx]# ulimit -c unlimited

[root@localhost nginx]# ulimit -c

unlimited

[root@localhost nginx]# ./sbin/nginx

[root@localhost nginx]# ls

client_body_temp  core.5242     html  proxy_temp  scgi_temp

conf              fastcgi_temp  logs  sbin        uwsgi_temp

[root@localhost nginx]#

生成了名为core.5242core文件,利用gdb调试该core文件:

[root@localhost nginx]# gdb sbin/nginx core.5242 -q

Reading symbols from /usr/local/nginx/sbin/nginx...done.

[New Thread 5242]

...

(gdb) bt

#0  0x0000003a9de329a5 in raise () from /lib64/libc.so.6

#1  0x0000003a9de34185 in abort () from /lib64/libc.so.6

#2  0x0000000000431a92 in ngx_debug_point () at src/os/unix/ngx_process.c:607

#3  0x00000000004035d9 in main (argc=1, argv=0x7fffd5625f18) at src/core/nginx.c:406

(gdb) up 3

#3  0x00000000004035d9 in main (argc=1, argv=0x7fffd5625f18) at src/core/nginx.c:406

406         ngx_debug_point();

(gdb) list

401          }

402      }

403 

404      ngx_use_stderr = 0;

405 

406         ngx_debug_point();

407 

408      if (ngx_process == NGX_PROCESS_SINGLE) {

409          ngx_single_process_cycle(cycle);

410 

(gdb)

对于调试工具,我很乐意推荐另外一个封装gdb的开源工具cgdbcgdb最大的好处是能在终端里运行并且原生具备gdb的强大调试功能,关于cgdb的相关使用可以参考官网:http://cgdb.sourceforge.net/lenky个人网站上的粗略介绍:http://lenky.info/?p=1409

cgdb在远程ssh里执行的界面如下图所示,如果上面类vi窗口没有显示对应的代码或下面gdb窗口提示No such file or directory.,那么需要利用directory命令把nginx源码增加到搜索路径即可:

 

利用strace/pstack调试

Linux下有两个命令stracehttp://sourceforge.net/projects/strace/)和ltracehttp://www.ltrace.org/)可以查看一个应用程序在运行过程中所发起的系统调用,这对作为标准应用程序的nginx自然可用。由于这两个命令大同小异,所以下面仅以strace为例做下简单介绍,大致了解一些它能帮助我们获取哪些有用的调试信息,关于strace/ltrace以及后面介绍的pstack更多的用法请Google

       straceman手册可以看到几个有用的选项:

Ø         -p pid:通过进程号来指定被跟踪的进程。

Ø         -o filename:将跟踪信息输出到指定文件。

Ø         -f:跟踪其通过frok调用产生的子进程。

Ø         -t:输出每一个系统调用的发起时间。

Ø         -T:输出每一个系统调用消耗的时间。

首先利用ps命令查看到系统当前存在的nginx进程,然后用strace命令的-p选项跟踪nginx工作进程:

       为了简化操作,我这里只设定了一个工作进程,该工作进程会停顿在epoll_wait系统调用上,这是合理的,因为在没有客户端请求时,nginx就阻塞于此(除非是在争用accept_mutex锁),在另一终端执行wget命令向nginx发出http请求后,在来看strace的输出:

       [root@localhost ~]# wget 127.0.0.1

       通过strace的输出可以看到nginx工作进程在处理一次客户端请求过程中发起的所有系统调用。我这里测试请求的html非常简单,没有附带cssjsjpg等文件,所以看到的输出也比较简单。strace输出的每一行记录一次系统调用,等号左边是系统调用名以及调用参数,等号右边是该系统调用的返回值。

1.         epoll_wait返回值为1,表示有1个描述符存在可读/写事件,这里当然是可读事件。

2.         accept4接受该请求,返回的数字3表示socket的文件描述符。

3.         epoll_ctlaccept4建立的socket套接字(注意参数3)加入到事件监听机制里。

4.         recv从发生可读事件的socket文件描述符内读取数据,读取的数据存在第二个参数内,读取了107个字节。

5.         stat64判断客户端请求的html文件是否存在,返回值为0表示存在。

6.         open/fstat64打开并获取文件状态信息。open文件返回的文件描述符为9,后面几个系统调用都用到这个值。

7.         writev把响应头通过文件描述符3代表的socket套接字发给客户端。

8.         sendfile64把文件描述符9代表的响应体通过文件描述符3代表的socket套接字发给客户端。

9.         再往文件描述符4代表的日志文件内write一条日志信息。

10.     recv看客户端是否还发了其它待处理的请求/信息。

11.     最后关闭文件描述符3代表的socket套接字。

由于strace能够提供nginx执行过程中的这些内部信息,所以在出现一些奇怪现象,比如nginx启动失败、响应的文件数据和预期不一致、莫名其妙的Segment Fault段错误、存在性能瓶颈(利用-T选项跟踪各个函数的消耗时间),利用strace也许能提供一些相关帮助。最后,要退出strace跟踪,按ctrl+c即可。

命令strace跟踪的是系统调用,对于nginx本身的函数调用关系无法给出更为明朗的信息,如果我们发现nginx当前运行不正常,想知道nginx当前内部到底在执行什么函数,那么命令pstack就是一个非常方便实用的工具。

pstack的使用也非常简单,后面跟进程id即可,比如在无客户端请求的情况下,nginx阻塞在epoll_wait系统调用处,此时利用pstack查看到的nginx函数调用堆栈关系如下:

main()函数到epoll_wait()函数的调用关系一目了然,和在gdb内看到的堆栈信息一摸一样,因为命令pstack本身就是一个利用gdb实现的shell脚本,关于这点,感兴趣的自己看看即可。

 

获得nginx程序执行流程

利用strace能帮助我们获取到nginx在运行过程中所发起的所有系统调用,但是不能看到nginx内部各个函数的调用情况;利用gdb调试nginx能让我们很清晰的获得nginx每一步的执行流程,但是单步调试毕竟是非常麻烦的,有没有更为方便的方法一次性获得nginx程序执行的整个流程呢?答案是肯定的,我们利用gcc的一个名为“-finstrument-functions”的编译选项,再加上一些我们自己的处理,就可以达到既定目的。关于-finstrument-functions的具体介绍,请直接参考官网手册:http://gcc.gnu.org/onlinedocs/gcc/Code-Gen-Options.html#Code-Gen-Options,我就不再累述,下面看看具体操作。

首先,我们准备两个文件,文件名和文件内容分别如下:

00:      Filename : my_debug.h

01:      #ifndef MY_DEBUG_LENKY_H

02:      #define MY_DEBUG_LENKY_H

03:      #include <stdio.h>

04:       

05:      void enable_my_debug( void ) __attribute__((no_instrument_function));

06:      void disable_my_debug( void ) __attribute__((no_instrument_function));

07:      int get_my_debug_flag( void ) __attribute__((no_instrument_function));

08:      void set_my_debug_flag( int ) __attribute__((no_instrument_function));

09:      void main_constructor( void ) __attribute__((no_instrument_function, constructor));

10:      void main_destructor( void ) __attribute__((no_instrument_function, destructor));

11:      void __cyg_profile_func_enter( void *,void *) __attribute__((no_instrument_function));

12:      void __cyg_profile_func_exit( void *, void *) __attribute__((no_instrument_function));

13:       

14:      #ifndef MY_DEBUG_MAIN

15:      extern FILE *my_debug_fd;

16:      #else

17:      FILE *my_debug_fd;

18:      #endif

19:      #endif

 

00:      Filename : my_debug.c

01:      #include "my_debug.h"

02:      #define MY_DEBUG_FILE_PATH "/usr/local/nginx/sbin/mydebug.log"

03:      int _flag = 0;

04:       

05:      #define open_my_debug_file()  \

06:          (my_debug_fd = fopen(MY_DEBUG_FILE_PATH, "a"))

07:       

08:      #define close_my_debug_file()  \

09:          do {  \

10:              if (NULL != my_debug_fd) {  \

11:                  fclose(my_debug_fd);  \

12:              }  \

13:          }while(0)

14:       

15:      #define my_debug_print(args, fmt...) \

16:          do{  \

17:              if (0 == _flag) {  \

18:                  break;  \

19:              }  \

20:              if (NULL == my_debug_fd && NULL == open_my_debug_file()) {  \

21:                  printf("Err: Can not open output file.\n");  \

22:                  break;  \

23:              }  \

24:              fprintf(my_debug_fd, args, ##fmt);  \

25:              fflush(my_debug_fd);  \

26:          }while(0)

27:       

28:      void enable_my_debug( void )

29:      {

30:          _flag = 1;

31:      }

32:      void disable_my_debug( void )

33:      {

34:          _flag = 0;

35:      }

36:      int get_my_debug_flag( void )

37:      {

38:          return _flag;

39:      }

40:      void set_my_debug_flag( int flag )

41:      {

42:          _flag = flag;

43:      }

44:      void main_constructor( void )

45:      {

46:          //Do Nothing

47:      }

48:      void main_destructor( void )

49:      {

50:          close_my_debug_file();

51:      }

52:      void __cyg_profile_func_enter( void *this, void *call )

53:      {

54:          my_debug_print("Enter\n%p\n%p\n", call, this);

55:      }

56:      void __cyg_profile_func_exit( void *this, void *call )

57:      {

58:          my_debug_print("Exit\n%p\n%p\n", call, this);

59:      }

将这两个文件放到/nginx-1.2.0/src/core/目录下,然后编辑/nginx-1.2.0/objs/Makefile文件,给CFLAGS选项增加-finstrument-functions选项:

02:      Filename : Makefile

03:      CFLAGS =  -pipe  -O0 -W -Wall -Wpointer-arith -Wno-unused-parameter -Wunused-function -Wunused-va        riable -Wunused-value -Werror -g -finstrument-functions

接着,需要将my_debug.hmy_debug.c引入到nginx源码里一起编译,所以继续修改/nginx-1.2.0/objs/Makefile文件,根据nginxMakefile文件特点,修改的地方主要有如下几处:

00:      Filename : Makefile

01:     

18:      CORE_DEPS = src/core/nginx.h \

19:              src/core/my_debug.h \

20:     

84:      HTTP_DEPS = src/http/ngx_http.h \

85:              src/core/my_debug.h \

86:     

102:  objs/nginx:     objs/src/core/nginx.o \

103:          objs/src/core/my_debug.o \

104: 

211:          $(LINK) -o objs/nginx \

212:          objs/src/core/my_debug.o \

213: 

322:  objs/src/core/my_debug.o: $(CORE_DEPS) \

323:          src/core/my_debug.c

324:          $(CC) -c $(CFLAGS) $(CORE_INCS) \

325:                  -o objs/src/core/my_debug.o \

326:                  src/core/my_debug.c

327: 

为了在nginx源码里引入my_debug,这需要在nginx所有源文件都包含有头文件my_debug.h,当然没必要每个源文件都去添加对这个头文件的引入,我们只需要在头文件ngx_core.h内加入对my_debug.h文件的引入即可,这样其它nginx的源文件就间接的引入了这个文件:

37:      Filename : ngx_core.h

38:      #include "my_debug.h"

在源文件nginx.c的最前面加上对宏MY_DEBUG_MAIN的定义,以使得nginx程序有且仅有一个my_debug_fd变量的定义:

06:      Filename : nginx.c

07:      #define MY_DEBUG_MAIN 1

08:       

09:      #include <ngx_config.h>

10:      #include <ngx_core.h>

11:      #include <nginx.h>

最后就是根据我们想要截取的执行流程,在适当的位置调用函数enable_my_debug();和函数disable_my_debug();,这里仅作测试,直接在main函数入口处调用enable_my_debug();,而disable_my_debug();函数就不调用了:

200:  Filename : nginx.c

201:  main(int argc, char *const *argv)

202:  {

203: 

208:  enable_my_debug();

至此,代码增补工作已经完成,重新编译nginx,如果之前已编译过nginx,那么如下的第一步源文件时间戳刷新步骤很重要:

[root@localhost nginx-1.2.0]# find . -name "*" | xargs touch

[root@localhost nginx-1.2.0]# make

make -f objs/Makefile

make[1]: Entering directory `/home/gqk/nginx-1.2.0'

gcc -c -pipe  -O0 -W -Wall -Wpointer-arith -Wno-unused-parameter -Wunused-function -Wunused-variable -Wunused-value -Werror -g -finstrument-functions -I src/core -I src/event -I src/event/modules -I src/os/unix -I objs \

              -o objs/src/core/nginx.o \

              src/core/nginx.c

...

       -lpthread -lcrypt -lpcre -lcrypto -lcrypto -lz

make[1]: Leaving directory `/home/gqk/nginx-1.2.0'

make -f objs/Makefile manpage

make[1]: Entering directory `/home/gqk/nginx-1.2.0'

make[1]: Nothing to be done for `manpage'.

make[1]: Leaving directory `/home/gqk/nginx-1.2.0'

[root@localhost nginx-1.2.0]#

以单进程模式运行nginx,并且在配置文件里将日志功能的记录级别设置低一点,否则将有大量的日志函数调用堆栈信息,经过这样的设置后,我们才能获得更清晰的nginx执行流程,即配置文件里做如下设置:

00:      Filename : nginx.c

01:      master_process  off;

02:      error_log  logs/error.log  emerg;

正常运行后的nginx将产生一个记录程序执行流程的文件,这个文件会随着nginx的持续运行迅速增大,所以在恰当的地方调用disable_my_debug();函数是非常有必要的,不过我这里在获取到一定量的信息后就直接killnginx进程了。mydebug.log的内容类似于如下所示:

[root@localhost sbin]# head -n 20 mydebug.log

Enter

0x804a5fc

0x806e2b3

Exit

0x804a5fc

0x806e2b3

这记录的是函数调用关系,不过这里的函数还只是以对应的地址显示而已,利用另外一个工具addr2line可以将这些地址转换回可读的函数名。addr2line工具在大多数linux发行版上默认有安装,如果没有那么在官网http://sourceware.org/binutils/下载即可,其具体用法也可以参考官网手册:http://sourceware.org/binutils/docs/binutils/addr2line.html,这里直接使用,写个addr2line.sh脚本:

00:      Filename : addr2line.sh

01:      #!/bin/sh

02:       

03:      if [ $# != 3 ]; then

04:          echo 'Usage: addr2line.sh executefile addressfile functionfile'

05:          exit

06:      fi;

07:       

08:      cat $2 | while read line

09:      do

10:          if [ "$line" = 'Enter' ]; then

11:              read line1

12:              read line2

13:      #      echo $line >> $3

14:              addr2line -e $1 -f $line1 -s >> $3

15:              echo "--->" >> $3

16:              addr2line -e $1 -f $line2 -s | sed 's/^/    /' >> $3

17:              echo >> $3

18:          elif [ "$line" = 'Exit' ]; then

19:              read line1

20:              read line2

21:              addr2line -e $1 -f $line2 -s | sed 's/^/    /' >> $3

22:              echo "<---" >> $3

23:              addr2line -e $1 -f $line1 -s >> $3

24:      #      echo $line >> $3

25:              echo >> $3

26:          fi;

27:      done

执行addr2line.sh进行地址与函数名的转换,这个过程挺慢的,因为从上面的shell脚本可以看到对于每一个函数地址都调用addr2line进行转换,执行效率完全没有考虑,不过够用就好,如果非要追求高效率,直接写个c程序来做这个转换工作也是可以的。

[root@localhost sbin]# vi addr2line.sh

[root@localhost sbin]# chmod a+x addr2line.sh

[root@localhost sbin]# ./addr2line.sh nginx mydebug.log myfun.log

[root@localhost sbin]# head -n 12 myfun.log

main

nginx.c:212

--->

    ngx_strerror_init

    ngx_errno.c:47

 

    ngx_strerror_init

    ngx_errno.c:47

<---

main

nginx.c:212

 

[root@localhost sbin]#

关于如获得nginx程序执行流程的方法大体就是上面描述的这样了,当然,这里介绍得很粗略,写的代码都也仅只是作为示范以抛砖引玉,关于gcc以及相关工具的更深入研究以不在本书的范围之内,如感兴趣可查看上文中提供的相关链接。

 

加桩调试

如果我们对代码做过单元测试,那么肯定知道加桩的概念,简单点说就是为了让一个模块执行起来,额外添加的一些支撑代码。比如,我要简单测试一个实现某种排序算法的子函数的功能是否正常,那么我也许需要写一个main()函数,设置一个数组,提供一些乱序的数据,然后利用这些数据调用排序子函数(假设它提供的接口就是对数组的排序,等),然后printf打印排序后的结果,看是否排序正常,所有写的这些额外代码(main()函数、数组、printf打印)就是桩代码。

上面提到的这种用于单元测试的方法,同样也可以用来深度调试nginx内部逻辑,而且nginx很多的基础实现(比如slab机制、红黑树、chain链、array数组等)都比较独立,要调试它们只需提供少量的桩代码即可。

nginxslab机制为例,通过下面提供的一些桩代码来调试该功能的具体实现。nginxslab机制用于对多进程共享内存的管理,不过单进程也是一样的执行逻辑,除了加/解锁直通以外(即加锁时必定成功),所以我们采取最简单的办法,直接在nginx本身的main()函数内插入我们的桩代码。当然,必须根据具体情况把桩代码放在合适的调用位置,比如这里的slab机制就依赖一些全局变量(像ngx_pagesize等),所以需要把桩代码的调用位置放在这些全局变量的初始化之后:

197:  Filename : nginx.c

198:  void ngx_slab_test()

199:  {

200:      ngx_shm_t shm;

201:      ngx_slab_pool_t *sp;

202:      u_char *file;

203:      void *one_page;

204:      void *two_page;

205:      

206:      ngx_memzero(&shm, sizeof(shm));

207:      shm.size = 4 * 1024 * 1024;

208:      if (ngx_shm_alloc(&shm) != NGX_OK) {

209:          goto failed;

210:      }

211:      

212:      sp = (ngx_slab_pool_t *) shm.addr;

213:      sp->end = shm.addr + shm.size;

214:      sp->min_shift = 3;

215:      sp->addr = shm.addr;

216:   

217:  #if (NGX_HAVE_ATOMIC_OPS)

218:      file = NULL;

219:  #else

220:      #error must support NGX_HAVE_ATOMIC_OPS.

221:  #endif

222:      if (ngx_shmtx_create(&sp->mutex, &sp->lock, file) != NGX_OK) {

223:          goto failed;

224:      }

225:      

226:      ngx_slab_init(sp);

227:      

228:      one_page = ngx_slab_alloc(sp, ngx_pagesize);

229:      two_page = ngx_slab_alloc(sp, 2 * ngx_pagesize);

230:   

231:      ngx_slab_free(sp, one_page);

232:      ngx_slab_free(sp, two_page);

233:      

234:      ngx_shm_free(&shm);

235:      

236:      exit(0);

237:  failed:

238:      printf("failed.\n");

239:      exit(-1);

240:  }

241: 

353:      if (ngx_os_init(log) != NGX_OK) {

354:          return 1;

355:      }

356:      

357:      ngx_slab_test();

358: 

上面是修改之后的nginx.c源文件,直接make后生成新的nginx,不过这个可执行文件不再是一个web server,而是一个简单的调试slab机制的辅助程序。可以看到,程序在进入main()函数后先做一些初始化工作,然后通过ngx_slab_test()函数调入到桩代码内执行调试逻辑,完成既定目标后便直接exit()退出整个程序。

正常运行时,nginx本身对内存的申请与释放是不可控的,所以直接去调试nginx内存管理的slab机制的代码逻辑比较困难,利用这种加桩的办法,ngx_slab_alloc()申请内存和ngx_slab_free()释放内存都能精确控制,对每一次内存的申请与释放后,slab机制的内部结构是怎样一个变化都能进行把握,对其相关逻辑的理解起来也就没那么困难了。下面是利用cgdb调试这个程序的界面显示:

 

第三章

配置文件格式

nginx的配置文件格式是nginx作者自己定义的,并没有采用像语法分析生成器LEMON那种经典复杂的LALR1)语法来描述配置信息,而是采用类似于ini这种普通却又简单的name-value对来描述配置信息,不过nginx对此做了扩展,以提供更为灵活的用户配置。

对于这种自定义格式的配置文件,好处就是自由、灵活,而坏处就是对于nginx的每一项配置信息都必须去针对性的解析和设置,因此我们很容易的看到nginx源码里有大量篇幅的配置信息解析与赋值代码。

类似于ini文件,nginx配置文件也是由多个配置项组成的,每一个配置项都有一个项目名和对应的项目值,项目名又被称为指令(Directive),而项目值可能是简单的字符串(以分号结尾),也可能是由简单字符串和多个配置项组合而成配置块的复合结构(以大括号}结尾),我们可以将配置项归纳为两种:简单配置项和复杂配置项。

上图只是一个示例,而实际的简单配置项与复杂配置项会更多样化,要区分简单配置项与复杂配置项却很简单,不带大括号的就是简单配置项,反之则反,比如:

error_log  /var/log/nginx.error_log  info;

因为它不带大括号,所以是一个简单配置项;而

location ~ \.php$ {

    fastcgi_pass   127.0.0.1:1025;

}

带大括号,所以是一个复杂配置项。为什么要做这种看似毫无意义的区分?因为后面会看到对于复杂配置项而言,nginx并不做具体的解析与赋值操作,一般只是申请对应的内容空间、切换解析状态,然后递归调用(因为复杂配置项本身含有递归的思想)解析函数,而真正将用户配置信息转换为nginx内控制变量的值,还是依靠那些简单配置项所对应的处理函数来做。

不管是简单配置项还是复杂配置项,它们的项目名和项目值都是由标记(token:这里是指一个配置文件字符串内容中被空格、引号、分号、tab号、括号,比如‘{’、换行符等分割开来的字符子串)组成的,配置项目名就是一个token,而配置项目值可以是一个、两个和多个token组成。

比如简单配置项:

daemon off;

其项目名daemon为一个token,项目值off也是一个token;简单配置项:

error_page  404  /404.html;

其项目值就包含有两个token,分别为404/404.html

对于复杂配置项:

location /gqk {

index    index.html index.htm index.php;

try_files  $uri $uri/ @gqk;

}

其项目名location为一个token,项目值是一个token/gqk)和多条简单配置项(通过大括号)组成的复合结构(后续称之为配置块)。上面几个例子中的taken都是被空格分割出来的,事实上下面这样的配置也是正确的:

"daemon" "off";

'daemon' 'off';

daemon 'off';

"daemon" off;

当然,一般情况下没必要画蛇添足似的去加些引号,除非我们需要在token内包含空格而又不想使用转义字符(\)的话就可以利用引号,比如:

log_format   main '$remote_addr - $remote_user [$time_local]  $status '

    '"$request" $body_bytes_sent "$http_referer" '

    '"$http_user_agent" "$http_x_forwarded_for"';

但是像下面这种格式就会有问题,这对于我们来说很容易理解,不多详叙:

"daemon "off";

最后值得提一下的是,nginx配置文件里的注释信息以井号(#)作为开头标记。

直观上看到的配置文件格式大概就是上面介绍的这些,但根据nginx应用本身的特定,我们可以对配置文件做上下文识别和区分,或者说是配置项的作用域,因为虽然某项配置项在同一个上下文里只能设置一次,但却可以在不同的上下文里设置多次,以便达到更细粒的控制,比如配置项error_log就是如此,在不同的server上下文里可以设置不同的日志输出级别和输出文件路径。目前nginx预定义的配置上下文主要包括mainhttpserverlocationmail这几种,下面是一个http服务器示例配置的上下文情况:

配置项目解析准备

前面提到对于配置文件里的每一项配置,程序都必须去针对性的解析并转化为内部控制变量的值,因此对于所有可能出现的配置项,nginx都会提供有对应的代码去做它的解析转换工作,如果配置文件内出现了nginx无法解析的配置项,那么nginx将报错并直接退出程序。

举例来说,对于配置项daemon,在模块ngx_core_module的配置项目解析数组内的第一元素就是保存的对该配置项进行解析所需要的信息,比如daemon配置项的类型,执行实际解析操作的回调函数,解析出来的配置项值所存放的地址等:

static ngx_command_t  ngx_core_commands[] = {

    { ngx_string("daemon"),

        NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_FLAG,

        ngx_conf_set_flag_slot,

        0,

        offsetof(ngx_core_conf_t, daemon),

        NULL },

……

而如果我在配置文件中加入如下配置内容:

lenky on;

nginx启动后将直接返回如下提示错误,这是因为对于“lenky on”这个配置项,nginx根本就没有对应的代码去解析它:

[emerg]: unknown directive “lenky” in /usr/local/nginx/conf/nginx.conf:2

如果你在使用nginx的过程中也遇到类似的错误提示,那么请立即检查配置文件是否不小心敲错了字符。

为了统一配置项目的解析,nginx利用ngx_command_s数据类型对所有的nginx对配置项进行了统一的描述:

struct ngx_command_s {

    ngx_str_t             name;

    ngx_uint_t            type;

    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);

    ngx_uint_t            conf;

    ngx_uint_t            offset;

    void                 *post;

};

这是一个结构体数据类型,它包含多个字段,其中几个主要字段的含义为:字段name指定与其对应的配置项目的名称,字段set指向一个回调函数,而字段offset指定转换后控制值的存放位置。

以上面的daemon配置项目为例,当遇到配置文件里的daemon项目名时,nginx就调用ngx_conf_set_flag_slot回调函数对其项目值进行解析,并根据其是on还是offngx_core_conf_tdaemon字段置为1或者0,这样就完成了从配置项目信息到nginx内部实际值的转换过程。当然,这还有其它一些细节未说,下面再具体来看:

ngx_command_s结构体的type字段指定该配置项的多种相关信息,比如:

1.         该配置的类型:NGX_CONF_FLAG表示该配置项目有一个布尔类型的值,例如daemon就是一个布尔类型的配置项目,其值为on或者offNGX_CONF_BLOCK表示该配置项目为复杂配置项,因此其有一个由大括号组织起来的多值块,比如配置项httpevents等。

2.         该配置项目的配置值的token个数:NGX_CONF_NOARGSNGX_CONF_TAKE1NGX_CONF_TAKE2、……、NGX_CONF_TAKE7,分别表示该配置项的配置值没有token、一个、两个、……、七个tokenNGX_CONF_TAKE12NGX_CONF_TAKE123NGX_CONF_1MORE等这些表示该配置项的配置值的token个数不定,分别为1个或2个、1个或2个或3个、一个以上。

3.         可以存在该配置项目的上下文:NGX_MAIN_CONFNGX_HTTP_MAIN_CONFNGX_EVENT_CONFNGX_HTTP_SRV_CONFNGX_HTTP_LOC_CONFNGX_HTTP_UPS_CONF等等。

字段confNGX_HTTP_MODULE类型模块所用,该字段指定当前配置项所在的大致位置,取值为NGX_HTTP_MAIN_CONF_OFFSETNGX_HTTP_SRV_CONF_OFFSETNGX_HTTP_LOC_CONF_OFFSET三者之一;其它模块基本不用该字段,直接指定为0

字段offset指定该配置项值的精确存放位置,一般指定为某一个结构体变量的字段偏移(利用offsetof宏),对于复杂配置项目,例如server,它不用保存配置项值,或者说它本身无法保存,亦可以说是因为它的值被分得更细小而被单个保存起来,此时字段offset指定为0即可。

字段post在大多数情况下都为NULL,但在某些特殊配置项中也会指定其值,而且多为回调函数指针,例如auth_basicconnection_pool_sizerequest_pool_sizeoptimize_host_namesclient_body_in_file_only等配置项。

每个模块都把自己所需要的配置项目的对应ngx_command_s结构体变量组成一个数组,并以ngx_xxx_xxx_commands的形式命名,该数组以元素ngx_null_command作为结束哨兵。

 

配置文件解析流程

下面开始对nginx配置信息的整个解析流程进行描述,假设我们以命令:

nginx -c /usr/local/nginx/conf/nginx.conf

启动nginx,而配置文件nginx.conf也比较简单,如下所示:

06:      Filename : nginx.conf

07:      worker_processes  2;

08:      error_log  logs/error.log  debug;

09:      events {

10:          use  epoll;

11:          worker_connections  1024;

12:      }

13:      http {

14:          include  mime.types;

15:          default_type  application/octet-stream;

16:          server {

17:              listen  8888;

18:              server_name  localhost;

19:              location / {

20:                  root  html;

21:                  index  index.html  index.htm;

22:              }

23:              error_page  404  /404.html;

24:              error_page  500  502  503  504  /50x.html;

25:              location = /50x.html {

26:                  root  html;

27:              }

28:          }

29:      }

 

00:      Filename : mime.types

01:      types {

02:          text/html  html htm shtml;

03:          text/css  css;

04:          text/xml  xml;

05:          image/gif  gif;

06:          image/jpeg  jpeg jpg;

07:          application/x-javascript  js;

08:     

09:      }

首先,抹掉一些前枝末节,我们直接跟着nginx的启动流程进入到与配置信息相关的函数调用处:

main -> ngx_init_cycle -> ngx_conf_parse

267:  Filename : ngx_cycle.c

268:  if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) {

269:      environ = senv;

270:      ngx_destroy_cycle_pools(&conf);

271:      return NULL;

272:  }

此处调用ngx_conf_parse函数传入了两个参数,第一个参数为ngx_conf_s变量,关于这个变量我们在他处再讲,而第二个参数就是保存的配置文件路径的字符串/usr/local/nginx/conf/nginx.confngx_conf_parse函数是执行配置文件解析的关键函数,其原型申明如下:

char *ngx_conf_parse(ngx_conf_t *cf, ngx_str_t *filename);

它是一个间接递归函数,也就是说虽然我们在该函数体内看不到直接的对其本身的调用,但是它执行的一些其它函数(比如ngx_conf_handler)内又会调用到ngx_conf_parse函数,从而形成递归,这一般在处理复杂配置项和一些特殊配置指令时发生,比如指令includeeventshttpserverlocation等。

ngx_conf_parse函数体代码量不算太多,但是它照样也将配置内容的解析过程分得很清楚,总体来看分成三个步骤:

1,  判断当前解析状态;

2,  读取配置标记token

3,  当读取了合适数量的标记token之后对其进行实际的处理,也就是将配置值转换为nginx内对应控制变量的值。

当进入到ngx_conf_parse函数时,首先做的第一步是判断当前解析过程处在一个什么样的状态,这有三种可能:

a,  正要开始解析一个配置文件:即此时的参数filename指向一个配置文件路径字符串,需要函数ngx_conf_parse打开该文件并获取相关的文件信息以便下面代码读取文件内容并进行解析,除了在上面介绍的nginx启动时开始主配置文件解析时属于这种情况,还有当遇到include指令时也将以这种状态调用ngx_conf_parse函数,因为include指令表示一个新的配置文件要开始解析。状态标记为type = parse_file;

b,  正要开始解析一个复杂配置项值:即此时配置文件已经打开并且也已经对文件部分进行了解析,当遇到复杂配置项比如eventshttp等时,这些复杂配置项的处理函数又会递归的调用ngx_conf_parse函数,此时解析的内容还是来自当前的配置文件,因此无需再次打开它,状态标记为type = parse_block;

c,  正要开始解析命令行参数配置项值,这在对用户通过命令行-g参数输入的配置信息进行解析时处于这种状态,如:nginx -g 'daemon on;'nginx在调用ngx_conf_parse函数对命令行参数配置信息'daemon on;'进行解析时就是这种状态,状态标记为type = parse_param;

在判断好当前解析状态之后就开始读取配置文件内容,前面已经说到配置文件都是有一个个token标记组成的,因此接下来就是循环从配置文件里读取标记,而ngx_conf_read_token函数就是做这个事情的:

rc = ngx_conf_read_token(cf);

函数ngx_conf_read_token对配置文件内容逐个字符扫描并解析为单个的token,当然,该函数并不会频繁的去读取配置文件,它每次将从文件内读取足够多的内容以填满一个大小为NGX_CONF_BUFFER4096)的缓存区(除了最后一次,即配置文件剩余内容本来就不够了),这个缓存区在函数ngx_conf_parse内申请并保存引用到变量cf->conf_file->buffer内,函数ngx_conf_read_token反复使用该缓存区,该缓存区可能有如下一些状态:

初始状态,即函数ngx_conf_parse内申请后的初始状态:

处理过程中的中间状态,有一部分配置内容已经被解析为一个个token并保存起来,而有一部分内容正要被组合成token,还有一部分内容等待处理:

已解析字符和已扫描字符都属于已处理字符,但它们又是不同的,已解析字符表示这些字符已经被作为token额外的保存起来了,所以这些字符已经完全没用了;而已扫描字符表示这些字符还未组成一个完整的token,所以它们还不能被丢弃。

当缓存区里的字符都处理完时,需要继续从打开的配置文件中读取新的内容到缓存区,此时的临界状态为:

前面图示说过,已解析字符已经没用了,因此我们可以将已扫描但还未组成token的字符移动到缓存区的前面,然后从配置文件内读取内容填满缓存区剩余的空间,情况如下:

如果最后一次读取配置文件内容不够,那么情况就是下面这样:

函数ngx_conf_read_token在读取了合适数量的标记token之后就开始下一步骤,即对这些标记进行实际的处理,那多少才算是读取了合适数量的标记呢?区别对待,对于简单配置项则是读取其全部的标记,也就是遇到配置项结束标记分号;为止,此时一条简单配置项的所有标记都已经被读取并存放在cf->args数组内,因此可以开始下一步骤进行实际的处理;对于复杂配置项则是读完其配置块前的所有标记,即遇到大括号{为止,此时复杂配置项处理函数所需要的标记都已读取到,而对于配置块{}内的标记将在接下来的函数ngx_conf_parse递归调用中继续处理,这可能是一个反复的过程。当然,函数ngx_conf_read_token也可能在其它情况下提前返回,比如配置文件格式出错、文件处理完(遇到文件结束)、块配置处理完(遇到大括号}),这几种返回情况的处理都很简单,不多详叙。

ngx_conf_read_token函数如何识别并将token缓存在cf->args数组中的逻辑还是比较简单的。首先是对配置文件临时缓存区内容的调整(如有必要),这对应前面几个图示的缓存区状态;接着通过缓存区从前往后的扫描整个配置文件的内容,对每一个字符与前面已扫描字符的组合进行有效性检查并进行一些状态旗标切换,比如d_quoted旗标置1则表示当前处于双引号字符串后,last_space旗标置1则表示前一个字符为空白字符(包括空格、回车、tab等),……,这些旗标能大大方便接下来的字符有效性组合检查,比如前面的nginx.conf配置文件的第5行末尾多加了个分号(即有2个分号),那么启动nginx将报错:

nginx: [emerg] unexpected ";" in /usr/local/nginx/conf/nginx.conf:5

再接下来就是判断当前已扫描字符是否能够组成一个token标记,两个双引号、两个单引号、两个空白字符之间的字符就能够组成一个token标记,此时就在cf->args数组内申请对应的存储空间并进行token标记字符串拷贝,从而完成一个token标记的解析与读取工作;此时根据情况要么继续进行下一个token标记的解析与读取,要么返回到ngx_conf_parse函数内进行实际的处理。

列表看一下ngx_conf_parse函数在解析nginx.conf配置文件时每次调用ngx_conf_read_token后的cf->args里存储的内容是什么(这通过gdb调试nginx时在ngx_conf_file.c:185处加断点就很容易看到这些信息),这会大大帮助对后续内容的理解:

次数

返回值rc

cf->args存储内容

1

NGX_OK

(gdb) p (*cf->args)->nelts

$43 = 2

(gdb) p *((ngx_str_t*)((*cf->args)->elts))

$44 = {len = 16, data = 0x80ec0c8 "worker_processes"}

(gdb) p *(ngx_str_t*)((*cf->args)->elts + sizeof(ngx_str_t))

$45 = {len = 1, data = 0x80ec0da "2"}

2

NGX_OK

(gdb) p (*cf->args)->nelts

$46 = 3

(gdb) p *((ngx_str_t*)((*cf->args)->elts))

$47 = {len = 9, data = 0x80ec0dd "error_log"}

(gdb) p *(ngx_str_t*)((*cf->args)->elts + sizeof(ngx_str_t))

$48 = {len = 14, data = 0x80ec0e8 "logs/error.log"}

(gdb) p *(ngx_str_t*)((*cf->args)->elts + 2*sizeof(ngx_str_t))

$49 = {len = 5, data = 0x80ec0f8 "debug"}

3

NGX_CONF_BLOCK_START

(gdb) p (*cf->args)->nelts

$52 = 1

(gdb) p *((ngx_str_t*)((*cf->args)->elts))

$53 = {len = 6, data = 0x80ec11f "events"}

第…次

……

……

6

NGX_CONF_BLOCK_DONE

(gdb) p (*cf->args)->nelts

$58 = 0

第…次

……

……

n

NGX_CONF_BLOCK_START

(gdb) p (*cf->args)->nelts

$74 = 2

(gdb) p *((ngx_str_t*)((*cf->args)->elts))

$75 = {len = 8, data = 0x80f7392 "location"}

(gdb) p *(ngx_str_t*)((*cf->args)->elts + sizeof(ngx_str_t))

$76 = {len = 1, data = 0x80f739c "/"}

第…次

……

……

第末次

NGX_CONF_FILE_DONE

(gdb) p (*cf->args)->nelts

$65 = 0

ngx_conf_read_token函数的返回值决定了ngx_conf_parse函数接下来的进一步处理:

情况

返回值rc

ngx_conf_parse函数一般情况处理

情况1

NGX_ERROR

解析异常,return NGX_CONF_ERROR;

情况2

NGX_CONF_BLOCK_DONE

NGX_CONF_FILE_DONE

解析正常,return NGX_CONF_OK

情况3

NGX_OK

NGX_CONF_BLOCK_START

调用ngx_conf_handler进行配置文件配置到nginx内部控制变量的转换;继续下一轮for循环处理。

讨论情况3,我们知道此时解析转换所需要token都已经保存到cf->args内,那么接下来就将这些token转换为nginx内控制变量的值,执行此逻辑的主要是ngx_conf_handler函数,不过在此之前会首先判断cf->handler回调函数是否存在,该回调函数存在的目的是针对类似于“text/html  html htm shtml;”和“text/css  css;”这样的types配置项,这些配置项的主要特点是众多且变化不定(一般可被用户自由配置)但格式又基本统一,往往以key/values的形式存在,更重要的是对于这些配置项,nginx的处理也很简单,只是拷贝到对应的变量内,所以这时一般会提供一个统一的cf->handler回调函数做这个工作。比如types指令的处理函数ngx_http_core_types内就对cf->handler赋值为ngx_http_core_type,这些里面的mime.types设置全部由该函数统一处理。

配置转换核心函数ngx_conf_handler的调入被传入了两个参数,ngx_conf_t类型的cf包含有不少重要的信息,比如转换所需要token就保存在cf->args内,而第二个参数无需多说,记录的是最近一次token解析函数ngx_conf_read_token的返回值。

前面说过nginx的每一个配置指令都对应一个ngx_command_s数据类型变量,记录着该配置指令的解析回调函数、转换值存储位置等,而每一个模块又都把自身所相关的所有指令以数组的形式组织起来,所有函数ngx_conf_handler首先做的就是查找当前指令所对应的ngx_command_s变量,这通过循环遍历各个模块的指令数组即可,由于nginx的所有模块也是以数组的形式组织起来的,所有在ngx_conf_handler函数体内我们可以看到有两个for循环的遍历查找:

for (i = 0; ngx_modules[i]; i++) {

    ...

    cmd = ngx_modules[i]->commands;

    for ( /* void */ ; cmd->name.len; cmd++) {

        ...

    }

}

两个for循环的结束判断之所以可以这样写,是因为这些数组都带有对应的末尾哨兵。具体代码里面还有一些有效性判断(比如当前模块类型、指令名称、项目值个数、指令位置)等操作,虽然繁琐但并没有难点所以忽略不讲,直接看里面的函数调用:

393:  Filename : ngx_conf_file.c

394:  rv = cmd->set(cf, cmd, conf);

当代码执行到这里,所以nginx已经查找到配置指令所对应的ngx_command_s变量cmd,所以这里就开始调用回调函数进行处理,以配置项目“worker_processes  2;”为例,对应的ngx_command_s变量为:

69:      Filename : nginx.c

70:      { ngx_string("worker_processes"),

71:          NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_TAKE1,

72:          ngx_conf_set_num_slot,

73:          0,

74:          offsetof(ngx_core_conf_t, worker_processes),

75:          NULL },

那么其回调函数为ngx_conf_set_num_slot,这是一个比较公共的配置项处理函数,也就是那种数字的配置项目都可以使用该函数进行转换,该函数的内部逻辑非常简单,首先找到转换后值的存储位置,然后利用ngx_atoi函数把字符串的数字转换为整型的数字,存储到对应位置;这就完成了从配置文件里的“worker_processes  2;”到nginx ngx_core_conf_t结构体类型变量confworker_processes字段控制值的转换。

worker_processes指令的回调处理函数比较简单,对于复杂配置项,比如server指令的回调处理函数ngx_http_core_server就要复杂得多,比如它会申请内存空间(以便存储其包含的简单配置项的控制值)、会调用ngx_conf_parse等,这些就留在需要的时候再做阐述吧。

对于nginx配置文件的解析流程基本就是如此,上面的介绍忽略了很多细节,前面也说过,事实上对于配置信息解析的代码(即各种各样的回调函数cmd->set的具体实现)占去了nginx大量的源代码,而我们这里并没有做过多的分析,仅例举了worker_processes配置指令的简单解析过程。虽然对于不同的配置项,解析代码会根据自身应用不同而不同,但基本框架就是如此了。最后,看一个nginx配置文件解析的流程图,如下:

 

配置信息组织结构

       这里讲的配置信息已不再是配置文件里的内容(比如daemon off;),而是指在nginx的执行环境里作为特定变量值的存在(比如ngx_flag_t daemon;)。虽然前面已经描述了从各个配置项到特定变量值的转换过程,但并没有详细阐明这些控制变量的整体组织结构,下面就尝试描述这部分内容。

       nginx内部对配置信息的组织首先是根据上下级别来区分的,也就是所谓的配置上下文,以http服务为例,最外层是main上下文、http指令的block块内为http上下文、接着是server上下文、location上下文,之所以说是按上下级别来区分是因为mainhttpserverlocation之间存在严格的包含与被包含关系,比如http包含serverserver包含location,这个无需累述;配置信息的组织还是按模块来划分的,这体现在每一平行级别上,也就是说对于所有main上下文里的配置,是根据模块来划分组织的,这是自然而然的事情,因为nginx代码本身也进行了模块化划分,而用户传递进来的配置信息说到底要被这些模块代码使用,为了让模块更方便的找到与自己相关的配置信息,那么直接根据模块来组织配置信息是合理的。会不会出现多个模块共用一个配置值的情况呢?按理不会,如果出现这种情况就说明模块的划分不恰当导致模块之间耦合性太强。看具体实现,首先是:

187:  Filename : ngx_cycle.c

188:  cycle->conf_ctx = ngx_pcalloc(pool, ngx_max_module * sizeof(void *));

189: 

215:  for (i = 0; ngx_modules[i]; i++) {

216:      if (ngx_modules[i]->type != NGX_CORE_MODULE) {

217:          continue;

218:      }

219:   

220:      module = ngx_modules[i]->ctx;

221:   

222:      if (module->create_conf) {

223:          rv = module->create_conf(cycle);

224:          if (rv == NULL) {

225:              ngx_destroy_pool(pool);

226:              return NULL;

227:          }

228:          cycle->conf_ctx[ngx_modules[i]->index] = rv;

229:      }

230:  }

231: 

251:  conf.ctx = cycle->conf_ctx;

252: 

262:  if (ngx_conf_param(&conf) != NGX_CONF_OK) {

263:  ...

268:  if (ngx_conf_parse(&conf, &cycle->conf_file) != NGX_CONF_OK) {

269:  ...

188行代码申请存储模块配置信息的内存空空间,可以看到这是一个指针数组,数组元素个数为ngx_max_module,刚好一个指针元素可以对应一个模块,后续这些指针就指向其对应模块配置信息的具体存储位置。

       215行的for循环主要是为了调用核心模块的create_conf()函数,创建实际的配置信息存储空间。为什么先只处理核心模块呢?因为核心模块才是基本模块,它们的配置空间必须首先创建,以便作为其它非核心模块的依赖。对于for循环内两个if判断的理解,是因为不是所有的模块都是核心模块,也不是所有的核心模块都有create_conf()函数,比如虽然模块ngx_http_module和模块ngx_mail_module都是核心模块,但它们却并没有create_conf()函数,因为这两个模块是否真正使用依赖于具体的配置文件,如果配置文件里并没有配置http,但nginx代码却先在这里把http的配置信息存储空间申请出来而后面又完全不用,那岂不是多此一举?所以,这两个核心模块的配置信息存储空间会在配置文件的解析过程中根据需要申请。第223行的存储空间若创建成功,那么第228行就把它赋值给对应的指针元素,完成前面所说的那样。

以核心模块ngx_core_module为例,从名字就可以看出这是一个特别基础且重要的核心模块,模块序号index0,而create_conf回调指针指向函数ngx_core_module_create_conf()

924:  Filename : nginx.c

925:  static void *

926:  ngx_core_module_create_conf(ngx_cycle_t *cycle)

927:  {

928:      ngx_core_conf_t  *ccf;

929:   

930:      ccf = ngx_pcalloc(cycle->pool, sizeof(ngx_core_conf_t));

931:      

945:      ccf->daemon = NGX_CONF_UNSET;

946:      ccf->master = NGX_CONF_UNSET;

947:      

970:      return ccf;

971:  }

这个函数主要做了一件事情,申请内存空间、初始内存空间并返回内存空间的指针引用。上面两段相关源码执行之后,我们目前所了解的配置信息最基本组织结构如下图所示:

       可以看到只有两个核心模块ngx_core_modulengx_regex_module有对应的create_conf回调函数,申请的配置存储空间“挂载”在对应的数组元素下。当然,这只是我这里的nginx模块情况(请参考附录A),也许你那因为configure编译设置不同而有所不同,不过可以肯定结构都是这样了。

       再来看源文件ngx_cycle.c的第251行和第268行(第262行是对通过nginx命令行传过来的配置信息的处理,和第268行将执行的逻辑一样,而且应该是更简单一点,所以略过),因为cycle->conf_ctx是唯一能正确找到配置存储空间的指针,不能把它弄乱,所以把它赋值给conf.ctx供后续使用,conf.ctx也就是类似于一个临时变量,不管后续代码怎样修改它(这个值也的确会随着配置文件的解析、配置上下文的切换而变化),我们的cycle->conf_ctx不变,如第268行所看到得那样,ngx_conf_parse()的第一个参数就是conf的引用,该函数再通过函数调用,把conf又传递到函数ngx_conf_handler()内:

101:  Filename : ngx_conf_file.c

102:  char *

103:  ngx_conf_parse(ngx_conf_t *cf, ngx_str_t *filename)

104:  {

105: 

244:          rc = ngx_conf_handler(cf, rc);

277:  }

278:   

279:   

280:  static ngx_int_t

281:  ngx_conf_handler(ngx_conf_t *cf, ngx_int_t last)

282:  {

283: 

376:              /* set up the directive's configuration context */

377:   

378:              conf = NULL;

379:   

380:              if (cmd->type & NGX_DIRECT_CONF) {

381:                  conf = ((void **) cf->ctx)[ngx_modules[i]->index];

382:   

383:              } else if (cmd->type & NGX_MAIN_CONF) {

384:                  conf = &(((void **) cf->ctx)[ngx_modules[i]->index]);

385:   

386:              } else if (cf->ctx) {

387:                  confp = *(void **) ((char *) cf->ctx + cmd->conf);

388:   

389:                  if (confp) {

390:                      conf = confp[ngx_modules[i]->ctx_index];

391:                  }

392:              }

393:   

394:              rv = cmd->set(cf, cmd, conf);

395: 

431:  }

378-392行的代码为我们关注的重点,看第380行的if判断,什么样的配置项类型是NGX_DIRECT_CONF的?搜索一下nginx的所有代码,发现只有核心模块的配置项才可能是这个类型,比如ngx_core_module模块的daemonmaster_process等、ngx_openssl_module模块的ssl_enginengx_regex_module模块的pcre_jit。从前面分析,我们已经知道这些核心模块的配置存储空间已经申请了,所有其配置项的转换后值已有存储的地方,看第381行给conf赋值语句,以ngx_core_module模块为例,那么conf指针的指向当前如下所示:

41:      Filename : nginx.c

42:          { ngx_string("master_process"),

43:            NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_FLAG,

44:            ngx_conf_set_flag_slot,

45:            0,

46:            offsetof(ngx_core_conf_t, master),

47:            NULL },

 

1041:             Filename : ngx_conf_file.c

1042:             char *

1043:             ngx_conf_set_flag_slot(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)

1044:             {

1045:                 char  *p = conf;

1046:            

1048:                 ngx_flag_t       *fp;

1049:            

1051:                 fp = (ngx_flag_t *) (p + cmd->offset);

1052:            

1059:                 if (ngx_strcasecmp(value[1].data, (u_char *) "on") == 0) {

1060:                     *fp = 1;

1061:              

1062:                 } else if (ngx_strcasecmp(value[1].data, (u_char *) "off") == 0) {

1063:                     *fp = 0;

1064:            

上面两段代码显示了配置项master_process的转换与存储过程,第10451051行结合起来找到master_process转换后值的存储位置,而10591063完成转换(on1off0)与存储。

接着看ngx_conf_file.c源码的第383行,有哪些配置项被打了NGX_MAIN_CONF标签而又不是NGX_DIRECT_CONF的?httpmaileventserror_log等,其中前面三个的处理比较类似,以http配置项的处理为例,我们知道ngx_http_module虽然是核心模块,但是其配置存储空间是还没有实际申请的,所以看第384行给conf进行赋值的语句右值是数组元素的地址,由于ngx_http_module模块对应7号数组元素,所以conf指针的指向当前如下所示:

83:      Filename : ngx_http.c

84:          { ngx_string("http"),

85:            NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS,

86:            ngx_http_block,

87:            0,

88:            0,

89:            NULL },

90:     

118:  static char *

119:  ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)

120:  {

121: 

125:      ngx_http_conf_ctx_t         *ctx;

126: 

132:      ctx = ngx_pcalloc(cf->pool, sizeof(ngx_http_conf_ctx_t));

133: 

137:      *(ngx_http_conf_ctx_t **) conf = ctx;

138:   

132行申请了内存空间,而第137行通过conf参数间接的把这块内存空间“挂载”在7号数组元素下。对于多级指针,大多数人都容易搞混乱,如果没有理解,请仔细思考一下上面的指针操作。经过ngx_http_block的处理,我们能看到的配置信息最基本组织结构如下图所示:

对于ngx_http_module模块的内部配置,除了main_conf配置外,为什么还有srv_confloc_conf是因为这两个字段里存储的配置信息是针对serverlocation应用的http全局配置。这些配置信息在结构上的组织和cycle->conf_ctx类似,仍然是根据模块来划分,当然只是NGX_HTTP_MODULE类型的模块,如果要画个图示,那么就是这样:

NGX_HTTP_MODULE类型模块具有哪种范围域的配置信息就将申请的内存空间“挂载”在对应的数组元素下(如果它在http上下文环境里配置),虽然大多数模块都只有一种,比如ngx_http_auth_basic_module模块只有loc_conf配置项,但ngx_http_charset_filter_module模块却有main_confloc_conf两类配置项,如上图中显示的那样(在整个NGX_HTTP_MODULE类型模块中排序中,ngx_http_auth_basic_module模块序号为6ngx_http_charset_filter_module模块序号为31)。继续看ngx_http_block函数的处理:

117:  Filename : ngx_http.c

118:  static char *

119:  ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)

120:  {

121: 

218:      pcf = *cf;

219:      cf->ctx = ctx;

220: 

235:      /* parse inside the http{} block */

236:   

237:      cf->module_type = NGX_HTTP_MODULE;

238:      cf->cmd_type = NGX_HTTP_MAIN_CONF;

239:      rv = ngx_conf_parse(cf, NULL);

240: 

325:      *cf = pcf;

326: 

218行把cf值(注意指针取值符号*,所以这里是进行的结构体赋值操作)保存起来,而第325行进行恢复,前面曾说过在配置文件解析的过程中,cf->ctx会随着上下文的切换而改变,第219行就可以看到这点,此时cf->ctx和上图中蓝色箭头指向一致。第239行调入到ngx_conf_parse后,当前配置上下文环境就从main切换到http,如果在接下来的解析过程中遇到server指令,其指令处理函数ngx_http_core_server(),类似于http指令的处理,对于server上下文这一同级别的所有配置同样也是按照模块划分来组织的:

       server上下文里不再有http全局配置,所以其main_conf字段直接指向http上下文的main_conf即可。

2700:             Filename : ngx_http_core_module.c

2701:             static char *

2702:             ngx_http_core_server(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)

2703:             {

2704:            

2780:                 /* parse inside server{} */

2781:              

2782:                 pcf = *cf;

2783:                 cf->ctx = ctx;

2784:                 cf->cmd_type = NGX_HTTP_SRV_CONF;

2785:              

2786:                 rv = ngx_conf_parse(cf, NULL);

2787:              

2788:                 *cf = pcf;

2789:            

2786行调入到ngx_conf_parse()后,当前配置上下文环境就从http切换到server,如果在接下来的解析过程中遇到location指令,其指令处理函数ngx_http_core_location (),类似于http指令、server指令的处理,对于location上下文这一同级别的所有配置同样也是按照模块划分来组织:

       依旧是进行上下文的切换(第30073008行),然后调用ngx_conf_parse()函数继续处理:

2824:             Filename : ngx_http_core_module.c

2825:             static char *

2826:             ngx_http_core_location(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)

2827:             {

2828:            

3007:                 save = *cf;

3008:                 cf->ctx = ctx;

3009:                 cf->cmd_type = NGX_HTTP_LOC_CONF;

3010:              

3011:                 rv = ngx_conf_parse(cf, NULL);

3012:              

3013:                 *cf = save;

3014:            

可以看到不管是http上下文还是server上下文、location上下文,调入到ngx_conf_parse()函数内后,cf->ctx指向的都是一个ngx_http_conf_ctx_t结构体,如果此时从ngx_conf_parse()函数再调入到ngx_conf_handler()函数,此时情况是怎么样呢?回过头来看ngx_conf_file.c源码的第386行,这是第三种情况,在前面两个if都不匹配的情况下再来进行这个判断,通过查看http模块配置项的type字段发现这些配置项的ngx_conf_handler()函数处理都会进入到这个判断里,看个实例:

138:  Filename : ngx_http_charset_filter_module.c

139:      { ngx_string("charset"),

140:        NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF

141:                          |NGX_HTTP_LIF_CONF|NGX_CONF_TAKE1,

142:        ngx_http_set_charset_slot,

143:        NGX_HTTP_LOC_CONF_OFFSET,

144:        offsetof(ngx_http_charset_loc_conf_t, charset),

145:        NULL },

配置项charsettype既不包含NGX_DIRECT_CONF旗标又不包含NGX_MAIN_CONF旗标,所以进入到第386行的判断里:

385:  Filename : ngx_conf_file.c

386:              } else if (cf->ctx) {

387:                  confp = *(void **) ((char *) cf->ctx + cmd->conf);

388:   

389:                  if (confp) {

390:                      conf = confp[ngx_modules[i]->ctx_index];

391:                  }

392:              }

从配置项charsettype字段里还可以看出它可以在多个上下文里使用,但如前所述,不管当前是在哪个上下文里,cf->ctx指向的都是一个ngx_http_conf_ctx_t结构体,配置项charsetconf字段为NGX_HTTP_LOC_CONF_OFFSET,也就是:

51:      Filename : ngx_http_config.h

52:      #define NGX_HTTP_LOC_CONF_OFFSET   offsetof(ngx_http_conf_ctx_t, loc_conf)

即取ngx_http_conf_ctx_t结构体的字段loc_conf偏移量,那么第387行代码也就是获取指针字段loc_conf所指向的数组,再由第390行根据模块序号获取对应的数组元素,这就和本节最开始讲述的情况统一起来了。

 

第四章

Nginx模块综述

nginx的模块非常之多,可以认为所有代码都是以模块的形式组织,这包括核心模块和功能模块,针对不同的应用场合,并非所有的功能模块都要被用到,附录A给出的是默认configure(即简单的http服务器应用)下被链接的模块,这里虽说是模块链接,但nginx不会像apachelighttpd那样在编译时生成so动态库而在程序执行时再进行动态加载,nginx模块源文件会在生成nginx时就直接被编译到其二进制执行文件中,所以如果要选用不同的功能模块,必须对nginx做重新配置和编译。对于功能模块的选择,如果要修改默认值,需要在进行configure时进行指定,比如新增http_flv功能模块(默认是没有这个功能的,各个选项的默认值可以在文件auto/options内看到):

[root@localhost nginx-1.2.0]# ./configure --with-http_flv_module

执行后,生成的objs/ngx_modules.c文件内就包含有对ngx_http_flv_module模块的引用了,要再去掉http_flv功能模块,则需要重新configure,即不带--with-http_flv_module配置后再编译生成新的nginx执行程序。通过执行./configure –help,我们可以看到更多的配置选项。

虽然Nginx模块有很多,并且每个模块实现的功能各不相同,但是根据模块的功能性质,可以将它们分为四个类别:

1,  handlers:处理客户端请求并产生待响应内容,比如ngx_http_static_module模块,负责客户端的静态页面请求处理并将对应的磁盘文件准备为响应内容输出。

2,  filters:对handlers产生的响应内容做各种过滤处理(即是增删改),比如模块ngx_http_not_modified_filter_module,对待响应内容进行过滤检测,如果通过时间戳判断出前后两次请求的响应内容没有发生任何改变,那么可以直接响应 304 Not Modified”状态标识,让客户端使用缓存即可,而原本待发送的响应内容将被清除掉。

3,  upstream:如果存在后端真实服务器,nginx可利用upstream模块充当反向代理(Proxy)的角色,对客户端发起的请求只负责进行转发(当然也包括后端真实服务器响应的回转),比如ngx_http_proxy_module就为标准的代理模块。

4,  load-balance:在nginx充当中间代理时,由于后端真实服务器往往多于一个,对于某一次客户端的请求,如何选择对应的后端真实服务器来进行处理,这就有类似于ngx_http_upstream_ip_hash_module这样的模块来实现不同的负载均衡算法(Load Balance)。

对于这几类模块,我们马上会分别进行详细介绍并分析各自典型代表模块,不过在此之前先从nginx模块源码上来进行直观认识。前面讲过nginx的所有代码都是以模块形式进行组织,而封装nginx模块的结构体为ngx_module_s,定义如下:

110:  Filename : ngx_conf_file.h

111:  struct ngx_module_s {

112:      ngx_uint_t            ctx_index;     //当前模块在同类模块中的序号

113:      ngx_uint_t            index;            //当前模块在所有模块中的序号

114:      

120:      ngx_uint_t            version;         //当前模块版本号

121:   

122:      void                 *ctx;             //指向当前模块特有的数据

123:      ngx_command_t        *commands;      //指向当前模块配置项解析数组

124:      ngx_uint_t            type;             //模块类型

125:         //以下为模块回调函数,回调时机可根据函数名看出

126:      ngx_int_t           (*init_master)(ngx_log_t *log);

127:      

128:  };

11:      Filename : ngx_core.h

12:      typedef struct ngx_module_s      ngx_module_t;

结构体ngx_module_s值得关注的几个字段分别为ctxcommandstype,其中commands字段表示当前模块可以解析的配置项目,这在配置文件解析一章做过详细描述;表示模块类型的type值只有5种可能的值,而同一类型模块的ctx指向的数据类型也相同:

序号

type

ctx指向数据类型

1

NGX_CORE_MODULE

ngx_core_module_t

2

NGX_EVENT_MODULE

ngx_event_module_t

3

NGX_CONF_MODULE

NULL

4

NGX_HTTP_MODULE

ngx_http_module_t

5

NGX_MAIL_MODULE

ngx_mail_module_t

上表中第三列里的数据类型非常重要,它们的字段基本都是一些回调函数,这些回调函数会在其模块对应的配置文件解析过程前//后会适时的被调用,做一些内存准备、初始化、配置值检查、初始值填充与合并、回调函数挂载等初始工作,以ngx_http_core_module模块为例,该模块type类型为NGX_HTTP_MODULEctx指向的ngx_http_module_t结构体变量ngx_http_core_module_ctx

785:  Filename : ngx_http_core_module.c

786:  static ngx_http_module_t  ngx_http_core_module_ctx = {

787:      ngx_http_core_preconfiguration,        /* preconfiguration */

788:      NULL,                                  /* postconfiguration */

789:   

790:      ngx_http_core_create_main_conf,        /* create main configuration */

791:      ngx_http_core_init_main_conf,          /* init main configuration */

792:   

793:      ngx_http_core_create_srv_conf,         /* create server configuration */

794:      ngx_http_core_merge_srv_conf,          /* merge server configuration */

795:   

796:      ngx_http_core_create_loc_conf,         /* create location configuration */

797:      ngx_http_core_merge_loc_conf           /* merge location configuration */

798:  };

根据上面代码注释,可以很明显的看出各个回调函数的回调时机,比如函数ngx_http_core_preconfiguration()将在进行http块配置解析前被调用,所以在ngx_http_block()函数里可以看到这样的代码:

117:  Filename : ngx_http.c

118:  static char *

119:  ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)

120: 

228:          if (module->preconfiguration) {

229:              if (module->preconfiguration(cf) != NGX_OK) {

230:                  return NGX_CONF_ERROR;

231:              }

232:          }

233: 

239:      rv = ngx_conf_parse(cf, NULL);

240: 

309:          if (module->postconfiguration) {

310:              if (module->postconfiguration(cf) != NGX_OK) {

311:                  return NGX_CONF_ERROR;

312:              }

313:          }

314: 

至于这些回调函数内的具体逻辑,如前所述一般是一些初始或默认值填充工作,但也有回调函数挂载的设置,比如ngx_http_static_module模块的postconfiguration字段回调函数ngx_http_static_init()就是将自己的处理函数ngx_http_static_handler()挂载在http处理状态机上,但总体来看这毕竟都只是一些简单的初始准备工作,不多累述。

 

Handler模块

       对于客户端http请求的处理过程,为了获得更强的控制能力,Nginx将其细分为多个阶段,每一个阶段可以有零个或多个回调函数进行专门处理,当我们在编写自己的handlers类型模块时,必须把模块功能处理函数挂载在正确的阶段点上,如前面所述的模块ngx_http_static_module就将自己的模块功能处理函数ngx_http_static_handler()挂载在NGX_HTTP_CONTENT_PHASE阶段。这在提供很大灵活性的同时,也极大的增加了编写自定义模块的困难,不过在详细了解每一个处理阶段之后,这种困难也许没有想象中的那么大。

Http请求处理过程一共分为11个阶段,每一个阶段对应的处理功能都比较单一,这样能尽量让nginx模块代码更为内聚:

序号

阶段宏名

阶段描述

0

NGX_HTTP_POST_READ_PHASE

读取请求内容阶段

1

NGX_HTTP_SERVER_REWRITE_PHASE

Server请求地址重写阶段

2

NGX_HTTP_FIND_CONFIG_PHASE

配置查找阶段

3

NGX_HTTP_REWRITE_PHASE

Location请求地址重写阶段

4

NGX_HTTP_POST_REWRITE_PHASE

请求地址重写提交阶段

5

NGX_HTTP_PREACCESS_PHASE

访问权限检查准备阶段

6

NGX_HTTP_ACCESS_PHASE

访问权限检查阶段

7

NGX_HTTP_POST_ACCESS_PHASE

访问权限检查提交阶段

8

NGX_HTTP_TRY_FILES_PHASE

配置项try_files处理阶段 

9

NGX_HTTP_CONTENT_PHASE

内容产生阶段

10

NGX_HTTP_LOG_PHASE

日志模块处理阶段

并非每一个阶段都能去挂载自定义的回调函数,比如NGX_HTTP_TRY_FILES_PHASE阶段就是针对配置项try_files的特定处理阶段,而NGX_HTTP_FIND_CONFIG_PHASENGX_HTTP_POST_ACCESS_PHASENGX_HTTP_POST_REWRITE_PHASE这三个阶段也是为了完成nginx特定的功能,就算给这几个阶段加上回调函数,也永远不会被调用。一般条件下,我们的自定义模块回调函数挂载在NGX_HTTP_CONTENT_PHASE阶段的情况比较多,毕竟大部分情况下的业务需求是修改HTTP响应数据,nginx自身的产生响应内容的模块,像ngx_http_static_modulengx_http_random_index_modulengx_http_index_modulengx_http_gzip_static_modulengx_http_dav_module等都是挂载在这个阶段。

大多数情况下,功能模块会在其对应配置解析完后的回调函数,也就是ngx_http_module_t结构体的postconfiguration字段指向的函数内将当前模块的回调功能函数挂载到这11个阶段的其中一个上,看个示例:

16:      Filename : ngx_http_static_module.c

17:      ngx_http_module_t  ngx_http_static_module_ctx = {

18:          NULL,                                  /* preconfiguration */

19:          ngx_http_static_init,                  /* postconfiguration */

20:     

270:  static ngx_int_t

271:  ngx_http_static_init(ngx_conf_t *cf)

272:  {

273:      ngx_http_handler_pt        *h;

274:      ngx_http_core_main_conf_t  *cmcf;

275:   

276:      cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

277:   

278:      h=ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);

279:      if (h == NULL) {

280:          return NGX_ERROR;

281:      }

282:   

283:      *h = ngx_http_static_handler;

284:   

285:      return NGX_OK;

286:  }

在模块ngx_http_static_modulepostconfiguration回调函数ngx_http_static_init()内,将ngx_http_static_module模块的核心功能函数ngx_http_static_handler()挂载在Http请求处理流程中的NGX_HTTP_CONTENT_PHASE阶段。这样,当一个客户端的http静态页面请求发送到nginx服务器,nginx就能够调用到我们这里注册的ngx_http_static_handler()函数,具体怎么做呢?接着看。

各个功能模块将其自身的功能函数挂载在cmcf->phases后,内部的情况如下图所示:

回调函数会根据选用模块的不同而不同,上图中显示的是在如附录A所示的模块选用下的情况。这些回调函数的调用是有条件的,调用后也要做一些根据返回值的结果处理,比如某次处理能否进入到阶段NGX_HTTP_CONTENT_PHASE的回调函数中处理,这需要一个事前判断,所以在函数ngx_http_init_phase_handlers()里对所有这些回调函数进行一次重组:

这里不过多描述ngx_http_init_phase_handlers()函数如何对这些回调函数进行的重组,因为对照上图并利用gdb跟踪一下也就清楚了,但从上图中可以看到,该函数只把有回调函数的处理阶段给提取了出来,同时利用ngx_http_phase_handler_t结构体数组对这些回调函数进行重组,不仅加上了进入回调函数的条件判断checker函数,而且通过next字段的使用,把原本的二维数组实现转化为可直接在一维函数数组内部跳动即可。

再来看对http请求进行分阶段处理核心函数ngx_http_core_run_phases

863:  Filename : ngx_http_core_module.c

864:  void

865:  ngx_http_core_run_phases(ngx_http_request_t *r)

866:  {

867:      ngx_int_t                   rc;

868:      ngx_http_phase_handler_t   *ph;

869:      ngx_http_core_main_conf_t  *cmcf;

870:   

871:      cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);

872:   

873:      ph = cmcf->phase_engine.handlers;

874:   

875:      while (ph[r->phase_handler].checker) {

876:   

877:          rc = ph[r->phase_handler].checker(r, &ph[r->phase_handler]);

878:   

879:          if (rc == NGX_OK) {

880:              return;

881:          }

882:      }

883:  }

注意while循环代码并结合前面的分析,可以看到这是一个超简单的遍历处理。r->phase_handler标志当前处理的序号,对一个客户端请求处理的最开始时刻,该值当然就是0了,while循环判断如果存在checker函数(末尾数组元素的checker函数为NULL),那么就调用该checker函数并有可能进而调用对应的回调函数,以NGX_HTTP_ACCESS_PHASE阶段的ngx_http_core_access_phase()函数为例:

1087:             Filename : ngx_http_core_module.c

1088:             ngx_int_t

1089:             ngx_http_core_access_phase(ngx_http_request_t*r,ngx_http_phase_handler_t*ph)

1090:             {

1091:            

1094:                 if (r != r->main) {

1095:                     r->phase_handler = ph->next;

1096:                     return NGX_AGAIN;

1097:                 }

1098:            

1102:                 rc = ph->handler(r);

1103:              

1104:                 if (rc == NGX_DECLINED) {

1105:                     r->phase_handler++;

1106:                     return NGX_AGAIN;

1107:                 }

1108:              

1109:                 if (rc == NGX_AGAIN || rc == NGX_DONE) {

1110:                     return NGX_OK;

1111:                 }

1112:              

1113:            

1142:                 /* rc == NGX_ERROR || rc == NGX_HTTP_...  */

1143:              

1144:                 ngx_http_finalize_request(r, rc);

1145:                 return NGX_OK;

1146:             }

1094行是一个回调函数准入判断,如果当前是子请求,那么第1095行代码让状态机直接进入到下一个处理阶段;第1102行进行回调处理,也就是执行功能模块的功能函数,如果第1104行判断成功则表示当前回调拒绝处理或者说是不符合它的处理条件,那么第1105行将处理移到一下回调函数(注意:处理阶段可能会发生迁移,比如当前回调函数已经是当前阶段的最后一个回调函数,那么调用下一个回调函数时就进入到下一个阶段);如果第1109行判断成功则表示当前回调需要再次调用或已经成功处理,但此处与前两处返回不同,首先并没有进行自增phase_handler变量,其次是这里返回NGX_OK会导致ngx_http_core_run_phases()函数里的循环处理会退出,这表示状态机的继续处理需要等待更进一步的事件发生,这可以能是子请求结束、socket描述符变得可写、超时发生等,并且再进入到状态机处理函数时,仍将从当前回调开始;第1142行后表示发生错误(比如NGX_ERRORNGX_HTTP_FORBIDDENNGX_HTTP_UNAUTHORIZED等)后的处理流程。

可以看到,一个功能模块的handler函数可以返回多种类型的值,并且这些值有其固有的含义:

序号

返回值

含义

1

NGX_OK

当前阶段已经被成功处理,必须进入到下一个阶段

2

NGX_DECLINED

当前回调不处理当前情况,进入到下一个回调处理

3

NGX_AGAIN

当前处理所需资源不足,需要等待所依赖事件发生

4

NGX_DONE

当前处理结束,仍需等待进一步事件发生后做处理

5

NGX_ERROR, NGX_HTTP_…

当前回调处理发生错误,需要进入到异常处理流程

       值得说明的是,上表只是一般情况下的含义,针对具体的阶段,我们最好仔细对照它的checker函数,看checker函数内对回调函数返回值的具体处理是怎样的。

由于回调函数的返回值会影响到同一阶段的后续回调函数的处理与否,而nginx又采用先进后出的方案,即先注册的模块,其回调函数反而后执行,所以回调函数或者说模块的前后顺序非常重要。以NGX_HTTP_CONTENT_PHASE阶段的三个回调函数为例,在附录A显示的模块列表里可以看到三个相关模块的注册顺序是ngx_http_static_modulengx_http_autoindex_modulengx_http_index_module,而从前面的图中看到回调函数顺序却是ngx_http_index_handlerngx_http_autoindex_handlerngx_http_static_handler,这个顺序是合理的,当我们打开nginx服务器时,如果直接访问的是一个目录,那么nginx先是查看当前目录下是否存在index..html/index.htm/index.php等这样的默认显示页面,这是回调函数ngx_http_index_handler()的工作;如果不存在默认显示页面,那么就看是否允许生成类似于下图这样的列表页面:

这又是属于ngx_http_autoindex_handler()函数的工作,而ngx_http_static_handler()回调函数则是根据客户端静态页面请求查找对应的页面文件并组成待响应内容;可以看到这三个回调函数虽然都挂载在NGX_HTTP_CONTENT_PHASE阶段,但各自实现的功能本身就存在有先后关系,如果函数ngx_http_autoindex_handler()ngx_http_index_handler()函数之前,那么对于本就存在默认显示页面的目录进行列表显示,这就是非常明显的逻辑错误。

 

Filter模块

对于Http请求处理handlers产生的响应内容,在输出到客户端之前需要做过滤处理,这些过滤处理对于完整功能的增强实现与性能的提升是非常有必要的,比如如果没有过滤模块ngx_http_chunked_filter_module,那么就无法支持完整的HTTP 1.1协议的chunk功能;如果没有ngx_http_not_modified_filter_module过滤模块,那么就无法让客户端使用本地缓存来提高性能;诸如这些都需要过滤模块的支持。由于响应数据包括响应头和响应体,所以与此相对应,任一filter模块必须提供处理响应头的header过滤功能函数(比如ngx_http_not_modified_filter_module模块提供的ngx_http_not_modified_header_filter()函数)或处理响应体的body过滤功能函数(比如ngx_http_copy_filter_module模块提供的ngx_http_copy_filter()函数)或两者皆有(比如ngx_http_chunked_filter_module模块提供的ngx_http_chunked_header_filter()函数和ngx_http_chunked_body_filter()函数)。

所有的header过滤功能函数和body过滤功能函数会分别组成各自的两条过滤链,如下图所示(使用附录A所列模块):

这两条过滤链怎么形成的呢?在源文件ngx_http.c里,可以看到定义了这样的两个函数指针变量:

71:      Filename : ngx_http.c

72:      ngx_int_t  (*ngx_http_top_header_filter) (ngx_http_request_t *r);

73:      ngx_int_t  (*ngx_http_top_body_filter) (ngx_http_request_t *r, ngx_chain_t *ch);

这是整个nginx范围内可见的全局变量;然后在每一个filter模块内,我们还会看到类似于这样的定义(如果当前模块只有header过滤功能函数或只有body过滤功能函数,那么如下定义也就只有相应的那个变量):

52:      Filename : ngx_http.c

53:      static ngx_http_output_header_filter_pt  ngx_http_next_header_filter;

54:      static ngx_http_output_body_filter_pt    ngx_http_next_body_filter;

注意到static修饰符,也就是说这两个变量是属于模块范围内可见的局部变量。有了这些函数指针变量,再在各个filter模块的postconfiguration回调函数(该函数会在其对应配置解析完后被调用做一些设置工作,前面已经描述过)内,全局变量与局部变量的巧妙赋值使得最终行成了两条过滤链。以header过滤链为例,通过附录A的模块列表ngx_modules变量,可以看到ngx_http_header_filter_module是具有header过滤功能函数的序号最小的过滤模块,其postconfiguration回调函数如下:

616:  Filename : ngx_http_header_filter_module.c

617:  static ngx_int_t

618:  ngx_http_header_filter_init(ngx_conf_t *cf)

619:  {

620:      ngx_http_top_header_filter = ngx_http_header_filter;

621:   

622:      return NGX_OK;

623:  }

ngx_http_top_header_filter指向其header过滤功能函数ngx_http_header_filter,此时header过滤链表现为如下形式:

接着nginx初始化再继续执行到下一序号的带有header过滤功能函数的过滤模块的postconfiguration回调函数:

231:  Filename : ngx_http_chunked_filter_module.c

232:  static ngx_int_t

233:  ngx_http_chunked_filter_init(ngx_conf_t *cf)

234:  {

235:      ngx_http_next_header_filter = ngx_http_top_header_filter;

236:      ngx_http_top_header_filter = ngx_http_chunked_header_filter;

237: 

无需对上面两行代码做过多解释,此时header过滤链表现为如下形式:

其它过滤模块的类此加入,逐步形成最终的完整header过滤链;当然,body过滤链的形成过程也与此类似。两条过滤链形成后,其对应的调用入口分别在函数ngx_http_send_header()和函数ngx_http_output_filter()内:

1888:             Filename : ngx_http_core_module.c

1889:             ngx_int_t

1890:             ngx_http_send_header(ngx_http_request_t *r)

1891:             {

1892:            

1897:                 return ngx_http_top_header_filter(r);

1898:             }

1899:              

1901:             ngx_int_t

1902:             ngx_http_output_filter(ngx_http_request_t *r, ngx_chain_t *in)

1903:             {

1904:            

1912:                 rc = ngx_http_top_body_filter(r, in);

1913:            

1919:                 return rc;

1920:             }

这两个函数非常简单,主要是通过过滤链的链头函数指针全局变量进入到两条过滤链内,进而依次执行链上的各个函数。比如这里ngx_http_top_header_filter指向的是ngx_http_not_modified_header_filter()函数,因此进入到该函数内执行,而在该函数的执行过程中又会根据情况,继续通过当前模块内的函数指针局部变量ngx_http_next_header_filter间接的调用到header过滤链的下一个过滤函数,这对保证过滤链的前后承接是非常必要的,除非我们遇到无法继续处理的错误,此时只有返回NGX_ERROR这样的值:

51:      Filename : ngx_http_not_modified_filter_module.c

52:      static ngx_int_t

53:      ngx_http_not_modified_header_filter(ngx_http_request_t *r)

54:      {

55:     

70:          return ngx_http_next_header_filter(r);

71:      }

根据HTTP协议具备的响应头影响或决定响应体内容的特点,所以一般是先对响应头进行过滤,根据头过滤处理返回值再对响应体进行过滤处理,如果在响应头过滤处理中出错或某些特定情况下,响应体过滤处理可以不用再进行。

 

Upstream模块

upstream模块的典型应用是反向代理,这里就以ngx_http_proxy_module模块为例。假定我们有如下这样的实例环境,客户端对服务器80端口的请求都被Nginx Proxy Server转发到另外两个真实的Nginx Web Server实例上进行处理(下图是实验环境,Web ServerProxy Server都只是Nginx进程,并且运行在同一台服务器):

       那么,Nginx Proxy Server的核心配置多半是这样:

00:      Filename : nginx.conf.upstream

01:     

02:      http {

03:     

04:             upstream load_balance {

05:                    server localhost:8001;

06:                    server localhost:8002;

07:             }

08:            

09:             server {

10:                    listen 80;

11:                    location / {

12:                           proxy_buffering off;

13:                           proxy_pass http://load_balance;

14:                    }

15:             }

16:      }

上面的proxy_buffering off;配置是为了禁用nginx反向代理的缓存功能,保证客户端的每次请求都被转发到后端真实服务器,以便我们每次跟踪分析的nginx执行流程更加简单且完整。而另外两个配置指令upstreamproxy_pass在此处显得更为重要,其中upstream配置指令的回调处理函数为ngx_http_upstream(),该函数除了申请内存、设置初始值等之外,最主要的动作就是切换配置上下文并调用ngx_conf_parse()函数继续进行配置解析:

4160:             Filename : ngx_http_upstream.c

4161:                 pcf = *cf;

4162:                 cf->ctx = ctx;

4163:                 cf->cmd_type = NGX_HTTP_UPS_CONF;

4164:              

4165:                 rv = ngx_conf_parse(cf, NULL);

4166:            

4173:                 if (uscf->servers == NULL) {

进入到upstream配置块内,最主要的配置指令也就是server,其对应的处理函数为ngx_http_upstream_server(),对于每一个后端真实服务器,除了其uri地址外,还有诸如downweightmax_failsfail_timeoutbackup这样的可选参数,所有这些都需要ngx_http_upstream_server()函数来处理。

       ngx_http_upstream.c的第4173行下个断点,我们可以看到这里给出示例的解析结果:

       另外一个重要配置指令proxy_pass主要出现在location配置上下文中,而其对应的处理函数为ngx_http_proxy_pass(),抹去该函数内的众多细节,我们重点关注两个赋值语句:

3336:             Filename : ngx_http_proxy_module.c

3337:             static char *

3338:             ngx_http_proxy_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)

3339:             {

3340:            

3356:                 clcf->handler = ngx_http_proxy_handler;

3357:            

3425:                 plcf->upstream.upstream = ngx_http_upstream_add(cf, &u, 0);

上面片段代码里的第一个赋值语句给当前locationhttp处理设置回调函数,而第二个赋值语句则是查找(没有找到则会创建,比如如果配置文件中upstream指令出现在proxy_pass指令的后面)其对应的upstream配置,我们这里就一个名为load_balanceupstream,所以找到的配置就是它了:

前面曾提到,Nginx将对客户端的http请求处理分为多个阶段,而其中有个NGX_HTTP_FIND_CONFIG_PHASE阶段主要就是做配置查找处理,如果当前请求location设置了upstream,即回调函数指针clcf->handler不为空,则表示对该location的请求需要后端真实服务器来处理:

949:  Filename : ngx_http_core_module.c

950:  ngx_int_t

951:  ngx_http_core_find_config_phase(ngx_http_request_t *r,

952:      ngx_http_phase_handler_t *ph)

953:  {

954: 

981:      ngx_http_update_location_config(r);

982: 

1439:             void

1440:             ngx_http_update_location_config(ngx_http_request_t *r)

1441:             {

1442:            

1519:                 if (clcf->handler) {

1520:                     r->content_handler = clcf->handler;

1521:                 }

1522:             }

在其它有location更新的情况下,比如redirect重定向locationnamed命名locationif条件location等,此时也会调用ngx_http_update_location_config()函数进行location配置更新。我们知道upstream模块的主要功能是产生响应数据,虽然这些响应数据来自后端真实服务器,所以在NGX_HTTP_CONTENT_PHASE 阶段的checker函数ngx_http_core_content_phase()内,我们可以看到在r->content_handler不为空的情况下会优先对r->content_handler函数指针进行回调:

1385:             Filename : ngx_http_core_module.c

1386:             ngx_int_t

1387:             ngx_http_core_content_phase(ngx_http_request_t *r,

1388:                 ngx_http_phase_handler_t *ph)

1389:             {

1390:            

1394:                 if (r->content_handler) {

1395:                     r->write_event_handler = ngx_http_request_empty_handler;

1396:                     ngx_http_finalize_request(r, r->content_handler(r));

1397:                     return NGX_OK;

1398:                 }

1399:            

1394行,如果r->content_handler不为空,即存在upstream,那么进入处理,注意第1397行直接返回NGX_OK,也即不再调用挂在该阶段的其它模块回调函数,所以说upstream模块的优先级是最高的。根据前面的回调赋值,调用r->content_handler()指针函数,实质上就是执行函数ngx_http_proxy_handler(),直到这里,我们才真正走进upstream代理模块的处理逻辑里。

对于任何一个Upstream模块而言,最核心的实现主要是7个回调函数,upstream代理模块自然也不例外,它实现并注册了这7个回调函数:

回调指针

函数功能

upstream代理模块

create_request

根据nginx与后端服务器通信协议(比如HTTPMemcache),将客户端的HTTP请求信息转换为对应的发送到后端服务器的真实请求。

ngx_http_proxy_create_request

由于nginx与后端服务器通信协议也为HTTP,所以直接拷贝客户端的请求头、请求体(如果有)到变量r->upstream->request_bufs内。

process_header

根据nginx与后端服务器通信协议,将后端服务器返回的头部信息转换为对客户端响应的HTTP响应头。

ngx_http_proxy_process_status_line

此时后端服务器返回的头部信息已经保存在变量r->upstream->buffer内,将这串字符串解析为HTTP响应头存储到变量r->upstream->headers_in内。

input_filter_init

根据前面获得的后端服务器返回的头部信息,为进一步处理后端服务器将返回的响应体做初始准备工作。

ngx_http_proxy_input_filter_init

根据已解析的后端服务器返回的头部信息,设置需进一步处理的后端服务器将返回的响应体的长度,该值保存在变量r->upstream->length内。

input_filter

正式处理后端服务器返回的响应体。

ngx_http_proxy_non_buffered_copy_filter

本次收到的响应体数据长度为bytes,数据长度存储在r->upstream->buffer内,把它加入到r->upstream->out_bufs响应数据链等待发送给客户端。

finalize_request

正常结束与后端服务器的交互,比如剩余待取数据长度为0或读到EOF等,之后就会调用该函数。由于nginx会自动完成与后端服务器交互的清理工作,所以该函数一般仅做下日志,标识响应正常结束。

ngx_http_proxy_finalize_request

记录一条日志,标识正常结束与后端服务器的交互,然后函数返回。

reinit_request

对交互重新初始化,比如当nginx发现一台后端服务器出错无法正常完成处理,需要尝试请求另一台后端服务器时就会调用该函数。

ngx_http_proxy_reinit_request

设置初始值,设置回调指针,处理比较简单。

abort_request

异常结束与后端服务器的交互后就会调用该函数。大部分情况下,该函数仅做下日志,标识响应异常结束。

ngx_http_proxy_abort_request

记录一条日志,标识异常结束与后端服务器的交互,然后函数返回。

       上表格中前面5个函数执行的先后次序如下图所示,由于在Client/Proxy/Server之间,一次请求/响应数据可以发送多次(下图中只画出一次就发送完毕的情况),所以下图中对应的函数也可能被执行多次,不过一般情况下,这5个函数执行的先后次序就是这样了。

       这些回调函数如何夹杂到nginx中被调用并不需要完全搞清楚,要写一个Upstream模块,我们只要实现上面提到的这7个函数即可,当然,可以看到最主要的也就是create_requestprocess_headerinput_filter这三个回调,它们实现从HTTP协议到Nginx与后端服务器之间交互协议的来回转换,使得在用户看来,他访问的就是一台功能完整的Web服务器,而也许事实上,显示在他面前的数据来自Memcache或别的什么服务器。

      

Load-balance模块

Load-balance模块可以称之为辅助模块,与前面介绍的以处理请求/响应数据为目标的三种模块完全不同,它的目标明确且单一,即如何从多台后端服务器中选择出一台合适的服务器来处理当前请求。

       要实现一个具体的Load-balance模块,依旧只需实现如下4个回调函数即可:

回调指针

函数功能

round_robin模块

ip_hash模块

uscf->peer.init_upstream

解析配置文件过程中被调用,根据upstream里各个server配置项做初始准备工作,另外的核心工作是设置回调指针us->peer.init。配置文件解析完后就不再被调用。

ngx_http_upstream_init_round_robin

设置:us->peer.init = ngx_http_upstream_init_round_robin_peer;

ngx_http_upstream_init_ip_hash

设置:us->peer.init = ngx_http_upstream_init_ip_hash_peer;

us->peer.init

在每一次nginx准备转发客户端请求到后端服务器前都会调用该函数,该函数为本次转发选择合适的后端服务器做初始准备工作,另外的核心工作是设置回调指针r->upstream->peer.getr->upstream->peer.free等。

ngx_http_upstream_init_round_robin_peer

设置:r->upstream->peer.get = ngx_http_upstream_get_round_robin_peer;

    r->upstream->peer.free = ngx_http_upstream_free_round_robin_peer;

ngx_http_upstream_init_ip_hash_peer

设置:r->upstream->peer.get = ngx_http_upstream_get_ip_hash_peer;

    r->upstream->peer.free为空。

r->upstream->peer.get

在每一次nginx准备转发客户端请求到后端服务器前都会调用该函数,该函数实现具体的为本次转发选择合适后端服务器的算法逻辑,即完成选择获取合适后端服务器的功能。

ngx_http_upstream_get_round_robin_peer

加权选择当前权值最高(即从各方面综合比较更有能力处理当前请求)的后端服务器。

ngx_http_upstream_get_ip_hash_peer

根据ip哈希值选择后端服务器。

r->upstream->peer.free

在每一次nginx完成与后端服务器之间的交互后都会调用该函数。如果选择算法有前后依赖性,比如加权选择,那么需要做一些数值更新操作;如果选择算法没有前后依赖性,比如ip哈希,那么该函数可为空;

ngx_http_upstream_free_round_robin_peer

更新相关数值,比如rrp->current等。

       Nginx默认采用round_robin加权算法,如果要选择其它负载均衡算法,必须在upstream的配置上下文中明确指定。比如采用ip_hash算法的upstream配置如下所示:

00:      Filename : nginx.conf

01:     

20:              upstream load_balance {

21:                      ip_hash;

22:                      server localhost:8001;

23:     

在配置项ip_hash的处理函数里,会给uscf->peer.init_upstream函数指针赋值上ip_hash模块提供的回调函数,这样在Nginx后续处理过程中才能调到ip_hash模块的功能逻辑里。

 

附录A

00:      Filename : objs/ngx_modules.c

01:       

02:      #include <ngx_config.h>

03:      #include <ngx_core.h>

04:       

05:       

06:       

07:      extern ngx_module_t  ngx_core_module;

08:      extern ngx_module_t  ngx_errlog_module;

09:      extern ngx_module_t  ngx_conf_module;

10:      extern ngx_module_t  ngx_events_module;

11: