免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
楼主: lenky0401

[C] nginx核心讲解 [复制链接]

论坛徽章:
0
发表于 2012-08-16 07:32 |显示全部楼层
第二章 跟踪调试
利用日志信息跟踪
优秀的程序都会带有自己的日志输出接口,并且一般会给出不同等级的输出,以便于重次信息的过滤,比如Linux内核的日志输出标准接口为printk,并且给出了KERN_EMERG、KERN_ALERT、KERN_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级,等级从低到高分别为debug、info、notice、warn、error、crit、alert、emerg,如果设置为error,则表示nginx内等级为error、crit、alert、emerg的4种日志将被输出到日志文件或标准终端,另外的debug、info、notice、warn这4种日志将被直接过滤掉而不会输出,因此如果我们只关注特别严重的信息则只需将日志等级设置为emerg即可大大减少nginx的日志输出量,这样就避免了在大量的日志信息里寻找重要信息的麻烦。
当我们利用日志跟踪nginx时,需要获取最大量的日志信息,所以此时可以把日志等级设置为最低的debug级,在这种情况下,如果觉得调试日志太多,nginx提供按模块控制的更细粒等级:debug_core、debug_alloc、debug_mutex、debug_event、debug_http、debug_imap,比如如果只想看http的调试日志,则需这样设置:
error_log logs/error.log debug_http
此时nginx将输出从info到emerg所有等级的日志信息,而debug日志则将只输出与http模块相关的内容。
error_log配置指令可以放在配置文件的多个上下文内,比如main、http、server、location,但同一个上下文中只能设置一个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.1和ip段192.168.10.0/24:
events {
    debug_connection 192.168.1.1;
    debug_connection 192.168.10.0/24;
}
nginx的日志功能仍在不断的改进,如能利用得好,对于我们跟着nginx还是非常有帮助的,至少我知道有不少朋友十分习惯于c库的printf打印调试,相比如此,nginx的ngx_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>”或gdb提示的当前正执行的代码行与源代码匹配不上而让人感觉莫名其妙,则多半是因为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"
第2、3两种方法是在我们已经执行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可以从nginx的main()函数开始调试,默认情况下调试的是监控进程的流程,如果要调试工作进程的流程需要在进入gdb后执行set follow-fork-mode child(此时,上面的daemon配置可不改),更简单的方法是直接设置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,进入到gdb后r即可;或者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,所以根据标题很容易区分出哪个是监控进程,哪些个是工作进程。对工作进程3906的gdb调试,可以利用gdb的-p命令行参数:
[root@localhost ~]# gdb -p 3906
或者是在进入gdb后执行:
(gdb) attach 3906
都可以。如果是要调试客户端发过来的请求处理过程,那么要注意请求是否被交给另外一个工作进程处理而导致绑定到gdb的这个工作进程实际没有动作,此时可以考虑开两个终端,运行两个gdb或干脆修改配置项worker_processes值为1而只运行一个工作进程。
将nginx特定进程绑定到gdb后,剩余的调试操作无非就是gdb的使用,这可以参考官方手册(http://www.gnu.org/software/gdb/documentation/),手册内容很多,因为gdb提供的功能非常丰富,而某些功能对于我们调试nginx也大有帮助,像Break conditions、Watchpoints等。以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.init(&shm_zone, NULL) != NGX_OK) {
476:                    goto failed;
477:                }
478:        …
这就说明这个函数指针一定是在其它某处被再次赋值,但具体是在哪里呢?搜索nginx全部源代码可能一下子没找到对应的代码行,那么此时可利用gdb的Watchpoints功能进行快速定位:
(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 value和New 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上,对于该配置项的配置值可以是stop或abort。当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状态为Ts(s代表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=0x7fffbd0a0c0 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.5242的core文件,利用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=0x7fffd5625f1 at src/core/nginx.c:406
(gdb) up 3
#3  0x00000000004035d9 in main (argc=1, argv=0x7fffd5625f1 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的开源工具cgdb,cgdb最大的好处是能在终端里运行并且原生具备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下有两个命令strace(http://sourceforge.net/projects/strace/)和ltrace(http://www.ltrace.org/)可以查看一个应用程序在运行过程中所发起的系统调用,这对作为标准应用程序的nginx自然可用。由于这两个命令大同小异,所以下面仅以strace为例做下简单介绍,大致了解一些它能帮助我们获取哪些有用的调试信息,关于strace/ltrace以及后面介绍的pstack更多的用法请Google。
        从strace的man手册可以看到几个有用的选项:
        -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非常简单,没有附带css、js、jpg等文件,所以看到的输出也比较简单。strace输出的每一行记录一次系统调用,等号左边是系统调用名以及调用参数,等号右边是该系统调用的返回值。
1.        epoll_wait返回值为1,表示有1个描述符存在可读/写事件,这里当然是可读事件。
2.        accept4接受该请求,返回的数字3表示socket的文件描述符。
3.        epoll_ctl把accept4建立的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工作进程实际执行起来,必然要向nginx发出http请求,除了采用真实浏览器(比如IE、Opera等)以外,还可以使用类似于linux下的wget或curl这样的工具,可以自定义http请求头部,查看响应头部等,非常的方便。关于这两个命令的详细用法,请查man手册或Google,另外可参考一下网址:http://lenky.info/?p=1841

获得nginx程序执行流程
利用strace能帮助我们获取到nginx在运行过程中所发起的所有系统调用,但是不能看到nginx内部各个函数的调用情况;利用gdb调试nginx能让我们很清晰的获得nginx每一步的执行流程,但是单步调试毕竟是非常麻烦的,有没有更为方便的方法一次性获得nginx程序执行的整个流程呢?答案是肯定的,我们利用gcc的一个名为“-finstrument-functions”的编译选项,再加上一些我们自己的处理,就可以达到既定目的。关于-finstrument-functions的具体介绍,请直接参考官网手册:http://gcc.gnu.org/onlinedocs/gc ... ml#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.h和my_debug.c引入到nginx源码里一起编译,所以继续修改/nginx-1.2.0/objs/Makefile文件,根据nginx的Makefile文件特点,修改的地方主要有如下几处:
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();函数是非常有必要的,不过我这里在获取到一定量的信息后就直接kill掉nginx进程了。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数组等)都比较独立,要调试它们只需提供少量的桩代码即可。
以nginx的slab机制为例,通过下面提供的一些桩代码来调试该功能的具体实现。nginx的slab机制用于对多进程共享内存的管理,不过单进程也是一样的执行逻辑,除了加/解锁直通以外(即加锁时必定成功),所以我们采取最简单的办法,直接在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调试这个程序的界面显示:


论坛徽章:
0
发表于 2012-08-16 07:33 |显示全部楼层
第三章 配置解析
配置文件格式
nginx的配置文件格式是nginx作者自己定义的,并没有采用像语法分析生成器LEMON那种经典复杂的LALR(1)语法来描述配置信息,而是采用类似于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上下文里可以设置不同的日志输出级别和输出文件路径。就http应用而言,目前nginx预定义的配置上下文主要包括main、http、server、location这四种(还有其他几种,比如event、upstream、if、mail等),下面是一个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还是off把ngx_core_conf_t的daemon字段置为1或者0,这样就完成了从配置项目信息到nginx内部实际值的转换过程。当然,这还有其它一些细节未说,下面再具体来看:
ngx_command_s结构体的type字段指定该配置项的多种相关信息,比如:
1.        该配置的类型:NGX_CONF_FLAG表示该配置项目有一个布尔类型的值,例如daemon就是一个布尔类型的配置项目,其值为on或者off;NGX_CONF_BLOCK表示该配置项目为复杂配置项,因此其有一个由大括号组织起来的多值块,比如配置项http、events等。
2.        该配置项目的配置值的token个数:NGX_CONF_NOARGS、NGX_CONF_TAKE1、NGX_CONF_TAKE2、……、NGX_CONF_TAKE7,分别表示该配置项的配置值没有token、一个、两个、……、七个token;NGX_CONF_TAKE12、NGX_CONF_TAKE123、NGX_CONF_1MORE等这些表示该配置项的配置值的token个数不定,分别为1个或2个、1个或2个或3个、一个以上。
3.        可以该配置项目可处在的上下文:NGX_MAIN_CONF(配置文件最外层,不包含其内的类似于http这样的配置块内部,即不向内延伸,其他上下文都有这个特性)、NGX_EVENT_CONF(event配置块)、NGX_HTTP_MAIN_CONF(http配置块)、NGX_HTTP_SRV_CONF(http的server指令配置块)、NGX_HTTP_LOC_CONF(http的location指令配置块)、NGX_HTTP_SIF_CONF(http的在server配置块内的if指令配置块)、NGX_HTTP_LIF_CONF(http的在location配置块内的if指令配置块)、NGX_HTTP_LMT_CONF(http的limit_except指令配置块)、NGX_HTTP_UPS_CONF(http的upstream指令配置块)、NGX_MAIL_MAIN_CONF(mail配置块)、NGX_MAIL_SRV_CONF(mail的server指令配置块),等等。
字段conf被NGX_HTTP_MODULE类型模块所用,该字段指定当前配置项所在的大致位置,取值为NGX_HTTP_MAIN_CONF_OFFSET、NGX_HTTP_SRV_CONF_OFFSET、NGX_HTTP_LOC_CONF_OFFSET三者之一;其它模块基本不用该字段,直接指定为0。
字段offset指定该配置项值的精确存放位置,一般指定为某一个结构体变量的字段偏移(利用offsetof宏),对于复杂配置项目,例如server,它不用保存配置项值,或者说它本身无法保存,亦可以说是因为它的值被分得更细小而被单个保存起来,此时字段offset指定为0即可。
字段post在大多数情况下都为NULL,但在某些特殊配置项中也会指定其值,而且多为回调函数指针,例如auth_basic、connection_pool_size、request_pool_size、optimize_host_names、client_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.conf。ngx_conf_parse函数是执行配置文件解析的关键函数,其原型申明如下:
char *ngx_conf_parse(ngx_conf_t *cf, ngx_str_t *filename);
它是一个间接递归函数,也就是说虽然我们在该函数体内看不到直接的对其本身的调用,但是它执行的一些其它函数(比如ngx_conf_handler)内又会调用到ngx_conf_parse函数,从而形成递归,这一般在处理复杂配置项和一些特殊配置指令时发生,比如指令include、events、http、server、location等。
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,        正要开始解析一个复杂配置项值:即此时配置文件已经打开并且也已经对文件部分进行了解析,当遇到复杂配置项比如events、http等时,这些复杂配置项的处理函数又会递归的调用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_BUFFER(4096)的缓存区(除了最后一次,即配置文件剩余内容本来就不够了),这个缓存区在函数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结构体类型变量conf的worker_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上下文,之所以说是按上下级别来区分是因为main、http、server、location之间存在严格的包含与被包含关系,比如http包含server、server包含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为例,从名字就可以看出这是一个特别基础且重要的核心模块,模块序号index为0,而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_CONF_UNSET这样的初始赋值,这很重要,根据名称就能猜出这些值可用来判断用户是否有在配置文件里对这些配置项做过设置,因为这些值都是特殊值-1(用户的合法设置不会有-1的情况),所以如果用户没做设置,那么在配置文件解析完后,对应的字段值仍然为-1,如果此时在其它配置设定下,正常运行nginx需要这些字段,那么就需给这些字段设置对应的默认值。设置默认值的处理在模块的回调函数init_conf()内,在配置文件解析完(有的只是对应的配置块解析完,比如http、events配置块,前面提到的create_conf()也是如此,比如http配置块的create_main_conf()、init_main_conf()等,但默认值的设定肯定是在对应的依赖配置内容已经全部解析完后才进行的)后就会调用该函数:
278:        Filename : ngx_cycle.c
279:            for (i = 0; ngx_modules[i]; i++) {
280:        …
286:                if (module->init_conf) {
287:                    if (module->init_conf(cycle, cycle->conf_ctx[ngx_modules[i]->index])
288:                        == NGX_CONF_ERROR)
289:        …
看看核心模块ngx_core_module的默认值设置ngx_core_module_init_conf()函数:
973:        Filename : nginx.c
974:        static char *
975:        ngx_core_module_init_conf(ngx_cycle_t *cycle, void *conf)
976:        {
977:            ngx_core_conf_t  *ccf = conf;
978:       
979:            ngx_conf_init_value(ccf->daemon, 1);
980:            ngx_conf_init_value(ccf->master, 1);
981:        …
229:        Filename : ngx_conf_file.h
230:        #define ngx_conf_init_value(conf, default)                           \
231:            if (conf == NGX_CONF_UNSET) {                            \
232:                conf = default;                                          \
233:            }
前后一连贯,这部分逻辑就应该很容易懂了,比如如果用户没有对daemon做设置,那么它的值就还是NGX_CONF_UNSET,进行就把它的设置为default默认值,也就是1。其它字段的默认值处理也与此类似。
回过头来接着看,前面提到的两段相关源码执行之后,我们目前所了解的配置信息最基本组织结构如下图所示:

        可以看到只有两个核心模块ngx_core_module和ngx_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模块的daemon和master_process等、ngx_openssl_module模块的ssl_engine、ngx_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的转换与存储过程,第1045与1051行结合起来找到master_process转换后值的存储位置,而1059到1063完成转换(on为1,off为0)与存储。
接着看ngx_conf_file.c源码的第383行,有哪些配置项被打了NGX_MAIN_CONF标签而又不是NGX_DIRECT_CONF的?http、mail、events、error_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_conf、loc_conf是因为这两个字段里存储的配置信息是针对server、location应用的http全局配置。这些配置信息在结构上的组织和cycle->conf_ctx类似,仍然是根据模块来划分,当然只是NGX_HTTP_MODULE类型的模块,如果要画个图示,那么就是这样:

NGX_HTTP_MODULE类型模块具有哪种范围域的配置信息就将申请的内存空间“挂载”在对应的数组元素下(如果它在http上下文环境里配置),虽然大多数模块都只有一种,比如ngx_http_auth_basic_module模块只有loc_conf配置项,但ngx_http_charset_filter_module模块却有main_conf和loc_conf两类配置项,如上图中显示的那样(在整个NGX_HTTP_MODULE类型模块中排序中,ngx_http_auth_basic_module模块序号为6、ngx_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上下文这一同级别的所有配置同样也是按照模块划分来组织:

        依旧是进行上下文的切换(第3007和3008行),然后调用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 },
配置项charset的type既不包含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:                    }
从配置项charset的type字段里还可以看出它可以在多个上下文里使用,但如前所述,不管当前是在哪个上下文里,cf->ctx指向的都是一个ngx_http_conf_ctx_t结构体,配置项charset的conf字段为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行根据模块序号获取对应的数组元素,这就和本节最开始讲述的情况统一起来了。


论坛徽章:
0
发表于 2012-08-16 07:33 |显示全部楼层
第四章 模块解析
Nginx模块综述
nginx的模块非常之多,可以认为所有代码都是以模块的形式组织,这包括核心模块和功能模块,针对不同的应用场合,并非所有的功能模块都要被用到,附录A给出的是默认configure(即简单的http服务器应用)下被连接的模块,这里虽说是模块连接,但nginx不会像apache或lighttpd那样在编译时生成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值得关注的几个字段分别为ctx、commands、type,其中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_MODULE,ctx指向的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_PHASE、NGX_HTTP_POST_ACCESS_PHASE与NGX_HTTP_POST_REWRITE_PHASE这三个阶段也是为了完成nginx特定的功能,就算给这几个阶段加上回调函数,也永远不会被调用。一般条件下,我们的自定义模块回调函数挂载在NGX_HTTP_CONTENT_PHASE阶段的情况比较多,毕竟大部分情况下的业务需求是修改HTTP响应数据,nginx自身的产生响应内容的模块,像ngx_http_static_module、ngx_http_random_index_module、ngx_http_index_module、ngx_http_gzip_static_module、ngx_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_module的postconfiguration回调函数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字段的使用,把原本的二维数组实现转化为可直接在一维函数数组内部跳动;一般来讲,二维数组的遍历需要两层循环,而遍历一维函数数组就只需一层循环了,所以加上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_ERROR、NGX_HTTP_FORBIDDEN、NGX_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_module、ngx_http_autoindex_module、ngx_http_index_module,而从前面的图中看到回调函数顺序却是ngx_http_index_handler、ngx_http_autoindex_handler、ngx_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 Server和Proxy 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执行流程更加简单且完整。而另外两个配置指令upstream和proxy_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地址外,还有诸如down、weight、max_fails、fail_timeout、backup这样的可选参数,所有这些都需要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);
上面片段代码里的第一个赋值语句给当前location的http处理设置回调函数,而第二个赋值语句则是查找(没有找到则会创建,比如如果配置文件中upstream指令出现在proxy_pass指令的后面)其对应的upstream配置,我们这里就一个名为load_balance的upstream,所以找到的配置就是它了:

前面曾提到,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重定向location或named命名location或if条件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与后端服务器通信协议(比如HTTP、Memcache),将客户端的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_request、process_header和input_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.get和r->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模块的功能逻辑里。


论坛徽章:
0
发表于 2012-08-16 07:33 |显示全部楼层
第五章 事件机制
I/O多路复用模型
        各种平台下支持的各种I/O事件处理机制在nginx内部都被进行了统一封装,这样不论nginx被用在何种平台都以最高效的方式运行,下表列出了nginx具体的支持情况:
名称        特点
select        标准的IO复用模型,几乎所有的类unix系统上都有提供,但性能相对较差。如果在当前系统平台找不到更优的IO复用模型,那么nginx默认编译并使用select复用模型,我们也可以通过使用--with-select_module或--without-select_module配置选项来启用或禁用select复用模型模块的编译。
poll        标准的IO复用模型,但理论上比select复用模型要优。同select复用模型类似,可以通过使用--with-poll_module或--without-poll_module配置选项来启用或禁用poll复用模型模块的编译。
epoll        系统Linux 2.6+上正式提供的性能更为优秀的IO复用模型。
kqueue        在系统FreeBSD 4.1+,OpenBSD 2.9+,NetBSD 2.0和MacOS X上特有的性能更优秀的IO复用模型。
eventport        在系统Solaris 10上可用的高性能IO复用模型。
/dev/poll        在系统Solaris 7 11/99+,HP/UX 11.22+ (eventport),IRIX 6.5.15+和Tru64 UNIX 5.1A+上可用的高性能IO复用模型。
rtsig        实时信号(real time signals)模型,在Linux 2.2.19+系统上可用。可以通过使用--with-rtsig_module配置选项来启用rtsig模块的编译。
aio        异步I/O(Asynchronous Input and Output)模型,通过异步IO函数,如aio_read、aio_write、aio_cancel、aio_error、aio_fsync、aio_return等实现。
上表给出的8种I/O事件处理机制中,前6种属于本节将介绍的I/O多路复用模型,而后两种机制,实时信号和异步I/O比较特殊,在此不做过多的描述,本文其它地方也不做考虑;
不论哪种I/O多路复用模型,基本的原理是相同的,它们都能让应用程序可以同时对多个I/O端口进行监控以判断其上的操作是否已经顺利完成,达到时间复用的目的。举个例子,如果要监控来之10根不同地方的水管(I/O端口)是否有水流出来(是否可读),那么需要10个人(10个线程或10处代码)来做这件事情;如果利用某种技术(比如摄像头)把这10根水管的状态情况统一传达到某个点,那么就只需要1人在那个点进行监控就行了,而类似于select()或epoll_wait()这样的系统调用就类似于摄像头的功能,应用程序将阻塞在这些系统调用上,而不是阻塞在某一处的I/O系统调用上。这个例子虽然粗糙,但应该是把I/O多路复用模型的基本特点给描述出来了。
不同的平台有支持不同的I/O多路复用模型,我认为对它们一个个进行讲解是不必要的,因为通过查man手册或Google都能找到更详细的资料,所以我们直接看nginx对这些I/O多路复用模型的封装与使用。在nginx源码里,I/O多路复用模型被封装在一个名为ngx_event_actions_t的结构体里,该结构体包含的字段主要就是回调函数,将各个I/O多路复用模型的功能接口进行统一:
ngx_event_actions_t接口        说明
init        初始化
add        将某描述符的某个事件(可读/可写)添加到多路复用监控里
del        将某描述符的某个事件(可读/可写)从多路复用监控里删除
enable        启用对某个指定事件的监控
disable        禁用对某个指定事件的监控
add_conn        将指定连接关联的描述符加入到多路复用监控里
del_conn        将指定连接关联的描述符从多路复用监控里删除
process_changes        监控的事件发生变化,只有kqueue会用到这个接口
process_events        阻塞等待事件发生,对发生的事件进行逐个处理
done        回收资源
由于I/O多路复用模型各自具体实现的不同,上表中列出的一些回调接口,在Nginx的各个I/O多路复用处理模块里可能并没有对应的处理,但几个最基本的接口,比如add/del/process_events肯定都会有实现。为了方便使用任何一种事件处理机制,nginx定义了一个类型为ngx_event_actions_t的全局变量ngx_event_actions,并且还定义了几个宏:
44:        Filename : ngx_event.c
45:        ngx_event_actions_t   ngx_event_actions;

447:        Filename : ngx_event.h
448:        #define ngx_process_changes  ngx_event_actions.process_changes
449:        #define ngx_process_events   ngx_event_actions.process_events
450:        #define ngx_done_events      ngx_event_actions.done
451:       
452:        #define ngx_add_event        ngx_event_actions.add
453:        #define ngx_del_event        ngx_event_actions.del
454:        #define ngx_add_conn         ngx_event_actions.add_conn
455:        #define ngx_del_conn         ngx_event_actions.del_conn
这样,nginx要将某个事件添加到多路复用监控里,只需调用ngx_add_event()函数即可,至于这个函数对应到哪个具体的I/O多路复用处理模块上,在这里可以毫不关心。
        当然,我们做分析还是要知道ngx_add_event()函数是怎么关联到具体的I/O多路复用处理模块的,而不难看出,关键点是全局变量ngx_event_actions的值。给全局变量ngx_event_actions进行赋值出现在各个事件处理模块的初始化函数内,比如epoll模块:
147:        Filename : ngx_epoll_module.c
148:        ngx_event_module_t  ngx_epoll_module_ctx = {
149:            &epoll_name,
150:            ngx_epoll_create_conf,               /* create configuration */
151:            ngx_epoll_init_conf,                 /* init configuration */
152:       
153:            {
154:                ngx_epoll_add_event,             /* add an event */
155:                ngx_epoll_del_event,             /* delete an event */
156:                ngx_epoll_add_event,             /* enable an event */
157:                ngx_epoll_del_event,             /* disable an event */
158:                ngx_epoll_add_connection,        /* add an connection */
159:                ngx_epoll_del_connection,        /* delete an connection */
160:                NULL,                            /* process the changes */
161:                ngx_epoll_process_events,        /* process the events */
162:                ngx_epoll_init,                  /* init the events */
163:                ngx_epoll_done,                  /* done the events */
164:            }
165:        };
166:        …
288:        static ngx_int_t
289:        ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer)
290:        {
291:        …
327:            ngx_event_actions = ngx_epoll_module_ctx.actions;
在其它事件处理模块的初始化函数内也可以找到这样的赋值语句,所以一旦设定nginx使用某个事件处理模块,经过事件处理模块的初始化后,就把全局变量ngx_event_actions指向了它的封装,比如从上面epoll模块的源代码来看,调用ngx_add_event()函数对应执行的就是ngx_epoll_add_event()函数。
设定nginx使用哪个事件处理机制是通过在event块里使用use指令来指定的,该配置指令对应的处理函数为ngx_event_use(),在经过相关验证(比如重复指定、对应的事件处理模块是否存在等)后,就会把对应的事件处理模块序号记录在配置变量ecf->use内。如果不进行主动指定,那么nginx就会根据当前系统平台选择一个合适的事件处理模块,并且同样把其序号记录在配置变量ecf->use内,其相关逻辑实现在函数ngx_event_core_init_conf()内。
在工作进程的初始化函数ngx_worker_process_init()内会调用事件核心模块的初始化函数ngx_event_process_init(),而在该函数内,根据配置变量ecf->use记录的值,进而调用到对应事件处理模块的初始化函数,比如epoll模块的ngx_epoll_init()函数:
582:        Filename : ngx_event.c
583:        static ngx_int_t
584:        ngx_event_process_init(ngx_cycle_t *cycle)
585:        {
586:        …
617:            for (m = 0; ngx_modules[m]; m++) {
618:                if (ngx_modules[m]->type != NGX_EVENT_MODULE) {
619:                    continue;
620:                }
621:       
622:                if (ngx_modules[m]->ctx_index != ecf->use) {
623:                    continue;
624:                }
625:       
626:                module = ngx_modules[m]->ctx;
627:       
628:                if (module->actions.init(cycle, ngx_timer_resolution) != NGX_OK) {
629:                    /* fatal */
630:                    exit(2);
631:                }
632:       
633:                break;
634:            }
至此,nginx内对I/O多路复用模型的整体封装,前后才真正衔接起来,下面是一个粗略的框图:


事件处理
Nginx内事件封装所对应的结构体为ngx_event_t,在该结构体内可以看到很多位域字段,凭经验即可知道它们都是用作旗标,即标记事件当前是否处在某种状态;除去这些旗标字段,与事件本身联系更为紧密的是回调接口handler字段,该字段直接指定了当事件发生时,nginx该如何进行处理。
我们所关注的事件基本都是依附在socket描述符上的,而随着处理流程的不断变化,在socket描述符上所关注的事件也会发生改变;比如,对于一个新建连接socket,一开始必定是关注其可读事件,以便从客户端获取请求信息,当读取完所有请求信息并且被nginx正常处理后,又将关注该socket的可写事件,从而可以将对应的响应信息顺利发送给客户端;即便是关注的同一个事件,根据当前处理阶段的不同,其事件处理回调函数也可能不同,这很容易理解,比如同是新建连接socket的可读事件,但处理客户端请求头的回调函数与处理客户端请求体的回调函数肯定不是同一个;下面就分析一个客户端请求/服务端响应的完整流程,看在这个过程中,关注事件如何变化,回调函数又如何变化。
这是一个非常简单的流程,客户端浏览器(比如ie)发送请求(请求某静态页面)到服务器,服务器也就是nginx程序,nginx从磁盘文件系统读取静态文件发送给客户端,流程结束。

当客户端浏览器发送请求到nginx时,nginx就将调用监听套接口对应的事件处理函数ngx_event_accept(),在该函数内将创建一个新的关联当前请求连接的套接口;在这个套接口上,nginx关注的事件以及回调函数列表(通过gdb的watch指令抓取)如下:
序号        关注事件类型        对应的回调函数
1        读        ngx_http_init_request()
2        写        ngx_http_empty_handler()
3        读        ngx_http_process_request_line()
4        读        ngx_http_process_request_headers()
5        读        ngx_http_request_handler()
6        写        ngx_http_request_handler()
7        写        ngx_http_empty_handler()
8        读        ngx_http_keepalive_handler()
accept()新建的套接口最先关注的当然是读事件,以便从客户端获取请求信息,其回调函数为ngx_http_init_request(),一旦读到客户端请求信息就开始进行初始化等准备工作;此时不关注写事件,所以写事件的回调函数为ngx_http_empty_handler(),什么也不做,仅打印一条日志;接下来对请求头、请求头处理依次进行,一旦处理结束就开始关注写事件,此时的写事件回调函数同为ngx_http_request_handler(),将响应数据全部发回给客户端后,将写事件的回调函数又置为ngx_http_empty_handler();最后,关注读事件等待客户端的下一个请求,此时的回调处理函数为ngx_http_keepalive_handler(),表示当前是在与客户端保持keepalive状态;如果客户端有新的请求数据发到,那么在ngx_http_keepalive_handler()函数内将读到对应的数据,并且调用ngx_http_init_request()做初始化,开始一个新的请求处理。如果此时客户端关闭了连接,那么nginx同样也将获得一个可读事件,调用ngx_http_keepalive_handler()函数处理却读取不到数据,于是关闭连接、回收资源,函数返回;这部分相关逻辑如下所示:
2662:        Filename : ngx_event.c
2663:        static void
2664:        ngx_http_keepalive_handler(ngx_event_t *rev)
2665:        {
2666:        …
2730:            n = c->recv(c, b->last, size);
2731:        …
2743:            if (n == 0) {
2744:                ngx_log_error(NGX_LOG_INFO, c->log, ngx_socket_errno,
2745:                        "client %V closed keepalive connection", &c->addr_text);
2746:                ngx_http_close_connection(c);
2747:                return;
2748:            }
2749:        …
2767:            ngx_http_init_request(rev);
2768:        }
Nginx对事件的处理耦合性太强,对上一步骤、当前处理步骤以及下一步骤都必须仔细把握,否则回调设置错了,一切就乱了;当然,这也可以说它灵活,只要你乐意,插入几个自编的模块到处理步骤里是非常简单的事情。

负载均衡
在一般情况下,配置nginx执行时,工作进程都会有多个,由于各个工作进程相互独立的接收客户端请求、处理、响应,所以就可能会出现负载不均衡的情况,比如1个工作进程当前有3000个请求等待处理,而另1个进程当前却只有300个请求等待处理,nginx采取了哪些均衡措施来避免这种情况就是本节将要讨论的内容。
从上一节内容可以看到,nginx工作进程的主要任务就是处理事件,而事件的最初源头来之监听套接口,所以一旦某个工作进程独自拥有了某个监听套接口,那么所有来之该监听套接口的客户端请求都将被这个工作进程处理;当然,如果是多个工作进程同时拥有某个监听套接口,那么一旦该监听套接口出现有某客户端请求,此时就将引发所有拥有该监听套接口的工作进程去争抢这个请求,能争抢到的肯定只有某一个工作进程,而其它工作进程注定要无获而返,这种现象即为惊群(thundering herd)。关于惊群是否已经被Linux内核所处理,这里不做深入考究,但可以肯定的是,要进行负载均衡,最基本的着手点也就是监听套接口,nginx是不是这样做的呢?下面来看。
在nginx源码里能看到这样一个名为ngx_use_accept_mutex的变量,可以说它就是nginx均衡措施的基本,该变量是整型类型,具体定义如下:
53:        Filename : ngx_event.c
54:        ngx_uint_t            ngx_use_accept_mutex;
该变量的赋值语句在函数ngx_event_process_init()内,也就是每个工作进程开始时的初始化函数,前后调用关系如下:
ngx_worker_process_cycle() -> ngx_worker_process_init() -> ngx_event_process_init()
在函数ngx_event_process_init()内,可以看到只有多进程模型下,并且工作进程数目大于1、用户设置开启负载均衡的情况下才设置该变量为1,否则为0:
596:        Filename : ngx_event.c
597:            if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) {
598:                ngx_use_accept_mutex = 1;
599:                ngx_accept_mutex_held = 0;
600:                ngx_accept_mutex_delay = ecf->accept_mutex_delay;
601:       
602:            } else {
603:                ngx_use_accept_mutex = 0;
604:            }
前两个条件很容易理解,只有有多个进程才有均衡的概念,而对于ecf->accept_mutex字段的判断主要是提供用户便利,可以关闭该功能,因为既然均衡策略也有相应的代码逻辑,难保在某些情况下其本身的消耗也许会得不偿失;当然,该字段默认为1,在配置初始化函数ngx_event_core_init_conf()内,有这么一句:ngx_conf_init_value(ecf->accept_mutex, 1);
一旦变量ngx_use_accept_mutex值为1,也就开启了nginx负载均衡策略,此时在每个工作进程的初始化函数ngx_event_process_init()内,所有监听套接口都不会被加入到其事件监控机制里,如下第828和829行的代码跳过了所有监听套接口的监听事件加入:
745:        Filename : ngx_event.c
746:            for (i = 0; i < cycle->listening.nelts; i++) {
747:        …
828:                if (ngx_use_accept_mutex) {
829:                    continue;
830:                }
831:        …
838:                    if (ngx_add_event(rev, NGX_READ_EVENT,0) == NGX_ERROR) {
839:                        return NGX_ERROR;
840:                    }
841:        …
845:            }
而真正将监听套接口(即客户端请求)加入到事件监控机制是在函数ngx_process_events_and_timers()里。在前面的进程模型一节,曾提到工作进程的主要执行体是一个无限for循序,而在该循环内最重要的函数调用就是ngx_process_events_and_timers(),所以可以想象在该函数内动态添加或删除监听套接口是一种很灵活的方式;如果当前工作进程负载比较小,就将监听套接口加入到自身的事件监控机制里,从而带来新的客户端请求;而如果当前工作进程负载比较大,就将监听套接口从自身的事件监控机制里删除,避免引入新的客户端请求而带来更大的负载;当然,并不是想加就加、想删就删,这需要利用锁机制来做互斥与同步,既避免监听套接口被同时加入到多个进程的事件监控机制里,又避免监听套接口在某一时刻没有被任何一个进程监控。
        看函数ngx_process_events_and_timers()源码,这里有一段至关重要的代码:
222:        Filename : ngx_event.c
223:            if (ngx_use_accept_mutex) {
224:                if (ngx_accept_disabled > 0) {
225:                    ngx_accept_disabled--;
226:       
227:                } else {
228:                    if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
229:                        return;
230:                    }
231:       
232:                    if (ngx_accept_mutex_held) {
233:                        flags |= NGX_POST_EVENTS;
234:       
235:                    } else {
236:                        if (timer == NGX_TIMER_INFINITE
237:                            || timer > ngx_accept_mutex_delay)
238:                        {
239:                            timer = ngx_accept_mutex_delay;
240:                        }
241:                    }
242:                }
243:            }
可以看到这段代码只有在开启负载均衡(即ngx_use_accept_mutex = 1;)后才生效,在该逻辑内,首先通过检测变量ngx_accept_disabled值是否大于0来判断当前进程是否已经过载,为什么可以这样判断需要理解变量ngx_accept_disabled值的含义,这在accept()接受新请求连接的处理函数ngx_event_accept()内可以看到:
17:        Filename : ngx_event_accept.c
18:        void
19:        ngx_event_accept(ngx_event_t *ev)
20:        {
21:        …
107:                ngx_accept_disabled = ngx_cycle->connection_n / 8
108:                                      - ngx_cycle->free_connection_n;
其中ngx_cycle->connection_n表示一个工作进程的最大可承受连接数,可以通过worker_connections指令配置,其默认值为512,在工作进程配置初始化函数ngx_event_core_init_conf()内有这样的语句:
12:        Filename : ngx_event.c
13:        #define DEFAULT_CONNECTIONS  512
1244:            ngx_conf_init_uint_value(ecf->connections, DEFAULT_CONNECTIONS);
1245:            cycle->connection_n = ecf->connections;
另外一个变量ngx_cycle->free_connection_n则表示当前可用连接数,假设当前活动连接数为x,那么该值为:ngx_cycle->connection_n - x;,故此ngx_accept_disabled的值为:
ngx_accept_disabled = x - ngx_cycle->connection_n * 7 / 8;
也就是说如果当前活动连接数(x)超过最大可承受连接数的7/8,则表示发生过载,变量ngx_accept_disabled值将大于0,并且该值越大表示负载越重。
回过头来看函数ngx_process_events_and_timers()内的代码,当进程处于过载状态时,所做的工作仅仅只是对变量ngx_accept_disabled自减1(第225行),这表示既然经过了一轮事件处理,那么负载肯定有所减小,所以也要相应的调整变量ngx_accept_disabled的值;经过一段时间,ngx_accept_disabled又会降到0以下,便可争用锁获取新的请求连接。
如果进程并没有处于过载状态,那么就会去争用锁(第228行),当然,实际上是争用监听套接口的拥有权,争锁成功就会把所有监听套接口(注意:是所有的监听套接口,它们总是作为一个整体被加入或删除,下同)加入到自身的事件监控机制里(如果原本不在),争锁失败就会把监听套接口从自身的事件监控机制里删除(如果原本就在);从函数ngx_trylock_accept_mutex()的内部实现可以看到这一点,代码非常容易理解,画个流程图表示(剔除了异常流程):

变量ngx_accept_mutex_held的值用于标识当前是否拥有锁,注意这一点很重要,因为接着看第232-241行的代码就是针对如此的处理;如果当前拥有锁,则给flags变量打个NGX_POST_EVENTS标记,这表示所有发生的事件都将延后处理(POST有表示在…之后的意思)。这是任何架构设计都必须遵守的一个约定,即持锁者必须尽量缩短自身持锁的时间,nginx的设计也不得例外,所以也照此把大部分事件延后到释放锁之后再去处理,缩短自身持锁的时间能让其它进程尽可能的有机会获取到锁;如果当前进程没有拥有锁,那么就把事件监控机制阻塞点(比如epoll_wait)的超时时间限制在一个比较短的范围内(即ngx_accept_mutex_delay,可通过指令accept_mutex_delay配置,默认值为500毫秒),超时更快,那么也就更频繁的从阻塞中跳出来,也就有更多的机会去争抢到互斥锁。
没有拥有锁的进程接下来的操作与无负载均衡情况没有什么不同,所以下面开始重点拥有锁的进程对事件的处理,这也就是前面提到的延迟处理。当一个事件发生时,一般处理(即不做延迟)会立即调用事件对应的回调函数,而延迟处理则会将该事件以链表的形式缓存起来,可以看epoll模型里的代码作为示例:
556:        Filename : ngx_epoll_module.c
557:        static ngx_int_t
558:        ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
559:        {
560:        …
672:                    if (flags & NGX_POST_EVENTS) {
673:                        queue = (ngx_event_t **) (rev->accept ?
674:                                   &ngx_posted_accept_events : &ngx_posted_events);
675:       
676:                        ngx_locked_post_event(rev, queue);
677:       
678:                    } else {
679:                        rev->handler(rev);
680:                    }
681:        …
706:                    if (flags & NGX_POST_EVENTS) {
707:                        ngx_locked_post_event(wev, &ngx_posted_events);
708:       
709:                    } else {
710:                        wev->handler(wev);
711:                    }
第679和710行是直接调用事件回调函数进行处理,而另外的代码是进行事件缓存,即加到ngx_posted_accept_events链表(新建连接事件,也就是监听套接口上的发生的可读事件)或ngx_posted_events链表。
回到我们讨论的最初函数ngx_process_events_and_timers(),看最后一点相关内容:
199:        Filename : ngx_event.c
200:        void
201:        ngx_process_events_and_timers(ngx_cycle_t *cycle)
202:        {
203:        …
247:            (void) ngx_process_events(cycle, timer, flags);
248:        …
254:            if (ngx_posted_accept_events) {
255:                ngx_event_process_posted(cycle, &ngx_posted_accept_events);
256:            }
257:       
258:            if (ngx_accept_mutex_held) {
259:                ngx_shmtx_unlock(&ngx_accept_mutex);
260:            }
261:        …
269:            if (ngx_posted_events) {
270:        …
274:                    ngx_event_process_posted(cycle, &ngx_posted_events);
275:        …
276:            }
在ngx_process_events()函数调用里已经将所有事件延迟保存,接下来先处理新建连接缓存事件ngx_posted_accept_events,此时还不能释放锁,因为我们还在处理监听套接口上的事件,还要读取上面的请求数据,所以必须独占,一旦缓存的新建连接事件全部被处理完就必须马上释放持有的锁。请求的具体处理与响应是最消耗时间的,不过在此之前已经释放了持有的锁后,所以即使慢一点也不会影响到其它进程。补充两点:第一,如果在处理新建连接事件的过程中,在监听套接口上又来了新的请求会怎么样?这没有关系,当前进程只处理已缓存的事件,新的请求将被阻塞在监听套接口上,会等到下一轮被哪个进程争取到锁并加到事件处理机制监控里时才会触发而被抓取出来。第二,第259行只是释放锁而并没有将监听套接口从事件处理机制监控里删除,所以有可能在接下来处理ngx_posted_events缓存事件的过程中,互斥锁被另外一个进程争抢到并且把所有监听套接口加入到它的事件处理机制监控里,因此严格来说,在同一时刻,监听套接口可能被多个进程拥有,但是,在同一时刻,监听套接口只可能被一个进程监控(也就是epoll_wait()这种),因此进程在处理完ngx_posted_events缓存事件后去争用锁,发现锁被其它进程占有而争用失败,会把所有监听套接口从自身的事件处理机制监控里删除,然后才去进行事件监控。
        最后,说一下nginx在多核平台上针对负载均衡所做的工作,也就是worker_cpu_affinity指令,利用该指令可以将各个工作进程固定在指定的CPU核上执行。关于多核平台的优化,说起来内容比较多,但最核心的思路就是per-cpu化处理,小到程序内部变量,大到架构设计都是如此,只有这样才有可能做到性能按CPU线性扩展,对于nginx用到的cpu_affinity,即cpu亲和性,也是如此。cpu affinity,简单点说就是让某一段代码/数据尽量在指定的某一个或几个cpu核心上长时间运行/计算的机制。nginx这里用到的把工作进程绑定到指定cpu是cpu affinity的其中一种应用,另外一种典型应用就是网卡收发包时硬中断的多cpu绑定,等等,这样做的最直观好处就是能够大大提高cpu cache的命中率,提高性能。关于cpu affinity的api使用介绍以及cpu cache对性能的影响,请参考:http://lenky.info/?p=1262http://lenky.info/?p=310http://lenky.info/?p=1784,以及相应系统的man手册。下面仅看一下nginx内cpu affinity的使用配置,其实非常简单,首先根据系统cpu个数设定工作进程数目,我这里只有两个核,所以就指定2个工作进程(也可以指定4、6、8等,一般情况下肯定是与cpu数成倍数),并且要让工作进程0运行在0号CPU上,工作进程1运行在1号CPU上(都是从0开始编号):
00:        Filename : nginx.conf
01:        worker_processes  2;
02:        worker_cpu_affinity 01 10;
worker_cpu_affinity指令的配置值是位图表示法,从前往后分别是0号工作进程、1号工作进程、…、n号工作进程的cpu二进制掩码(各个掩码之间用空格隔开),所以这里0号工作进程的cpu掩码为01,表示其使用0号cpu,1号工作进程的cpu掩码为10,表示其使用1号cpu;如果哪个工作进程的cpu掩码为11,则表示其既使用0号cpu,又使用1号cpu。
        使用这个配置文件来执行nginx,利用ps的-F选项查看:

        PSR列对应的就是进程所在cpu号,可以看到0号工作进程(即2224)的cpu号为0,而1号工作进程(即2225)的cpu号为1。
        将配置修改一下:worker_cpu_affinity 10 01;,重启nginx再看:


超时管理
        事件超时意味着等待的事件没有在指定的时间内到达,nginx有必要对这些可能发生超时的事件(下面统称为超时事件对象)进行统一管理,并在发生事件超时时做出相应的处理,比如回收资源,返回错误等。举个具体例子来说,当客户端对nginx发出请求连接后,nginx就会accept()并建立对应的连接对象connection、读取客户端请求的头部信息,而读取这个头部信息显然是要求在一定的时间内完成,如果在一个有限的时间内没有读取到头部信息或读取的头部信息不完整,那么nginx就无法进行正常处理,并且应该认为这是一个错误/非法的请求,直接返回错误信息("Request time out" (40)并释放相应的资源,如果nginx不这样做,那么针对如此的恶意攻击就很容易实施。当然,其它需要进行事件超时监控的地方还有很多,比如读取客户端请求数据、回写响应数据、管道通信等等,下面就看nginx是如何对这些超时事件对象进行统一超时管理的。
        对于超时管理,无非要解决两个问题:第一,超时事件对象的组织,nginx采用的是红黑树(本节如无特殊说明,提到红黑树就是指这颗树);第二,超时事件对象的超时检测,nginx提供了两种方案,一种是定时检测机制,通过设置定时器,争取在每过一定的时间就对红黑树管理的所有超时事件对象进行一次超时扫描检测。另一种方案是先计算出距离当前最快发生超时的时间是多久,假设时间为t秒,那么就等待t秒(其实是事件处理模型阻塞t秒)后去进行一次超时检测。
        先看超时事件对象的组织结构红黑树,我们知道nginx把事件封装在一个名为ngx_event_s的结构体内,而该结构体有几个字段与nginx的超时管理联系紧密:
37:        Filename : ngx_event.h
38:        struct ngx_event_s {
39:        …
67:            unsigned         timedout:1;
68:            unsigned         timer_set:1;
69:        …
134:            ngx_rbtree_node_t   timer;
其中timedout域字段用于标识当前事件是否已经超时,0为没有超时;timer_set域字段用于标识当前事件是否已经加入到红黑树管理,需要对其是否超时做监控,0为没有加入;而tmer字段,很容易看出它属于红黑树节点类型变量,红黑树就是通过该字段来组织所有的超时事件对象。
nginx设置了两个全局变量以便在程序的任何地方都能快速的访问到这颗红黑树:
17:        Filename : ngx_event_timer.c
18:        ngx_thread_volatile ngx_rbtree_t  ngx_event_timer_rbtree;
19:        static ngx_rbtree_node_t          ngx_event_timer_sentinel;
ngx_event_timer_rbtree封装了整棵红黑树树结构,而ngx_event_timer_sentinel属于红黑树节点类型变量,在红黑树的操作过程中被当作哨兵使用,同时注意到它是static的,所以作用域仅限于ngx_event_timer.c源文件内。
红黑树的初始化函数ngx_event_timer_init()是在ngx_event_process_init()函数内被调用,所以每个工作进程都会在自身的初始化时建立这颗红黑树:

        当需要对某个事件进行超时监控时,就会把它加入到这个红黑树内。仍以之前的例子来说,在nginx调用accept()接受到客户端请求并建立对应的连接对象connection后,在连接对象的初始化函数ngx_http_init_connection()内,可以找到这么一行代码:
224:        Filename : ngx_http_request.c
225:            ngx_add_timer(rev, c->listening->post_accept_timeout);
这也就是将rev事件(触发该事件即表示客户端传来请求头等信息)对象加入到红黑树内进行超时管理,同时给它指定的超时时限为c->listening->post_accept_timeout(该变量的值可由用户通过client_header_timeout指令进行配置,默认情况下是60000毫秒)。
函数ngx_add_timer()完成将一个超时事件对象加入到红黑树的具体逻辑,代码非常的简单,首先在对应树节点的key字段里记录超时时刻(在后续进行超时检测扫描时就需要该字段来进行时刻的先后比较),然后判断该超时事件对象是否已经加入到红黑树,如果是的话则需要先调用函数ngx_del_timer()将它从红黑树里移除,最后再调用ngx_rbtree_insert()函数将超时事件对象真正加入到红黑树。另外可以看到,这种加入是间接性的,根据前面的介绍可知,每个事件对象封装结构体都有一个tmer字段,并且其为ngx_rbtree_node_t类型变量,加入到红黑树的就是这个字段,而非事件对象结构体本身。当然,可以通过利用offsetof宏来根据该tmer字段快速方便的找到其所在的对应事件对象结构体,所以并不用为这种设计而担心。
        具有四个节点的红黑树描述如下图所示,从该图中可以看到两点:第一,可以通过全局变量ngx_event_timer_rbtree.root快速定位到该红黑树的根节点;第二,从该红黑树根节点从左或从右遍历下去,最后都将到达全局变量ngx_event_timer_sentinel指定的末端树节点,这也是前面称ngx_event_timer_sentinel为哨兵节点的原因所在。

通过红黑树,nginx对那些需要关注其是否超时的事件对象就有了统一的管理,nginx可以选择在合适的时机对事件计时红黑树管理的事件进行一次超时检测,对于超时了的事件对象就进行相应的处理,这在前面曾提到过nginx的超时检测方案有两种,下面就来分别介绍。
        Nginx具体使用哪种超时检测方案主要取决于一个配置指令timer_resolution,比如:
03:        Filename : nginx.conf
04:        timer_resolution  100ms;
反映到nginx内,也就是全局变量ngx_timer_resolution的值为100,再接下来分析,就又得看工作进程的核心处理函数ngx_process_events_and_timers():
199:        Filename : ngx_event.c
200:        void
201:        ngx_process_events_and_timers(ngx_cycle_t *cycle)
202:        {
203:        …
206:            if (ngx_timer_resolution) {
207:                timer = NGX_TIMER_INFINITE;
208:                flags = 0;
209:       
210:            } else {
211:                timer = ngx_event_find_timer();
212:                flags = NGX_UPDATE_TIME;
213:        …
247:            (void) ngx_process_events(cycle, timer, flags);
可以看到ngx_timer_resolution变量是否非0主要影响了两个变量的值:timer和flags。先看非0情况,也就是超时检测方案1,此时flags值为0,可以认为这表示对其它地方代码逻辑无附加影响,而timer为无限大(即:#define NGX_TIMER_INFINITE  (ngx_msec_t) -1),而该值在ngx_process_events()函数内将被用作事件处理机制可以等待的最长时间,那么将timer设置为无限大会使得工作进程在事件处理机制里会无限阻塞而导致超时事件得不到及时处理么?当然不会,先不说正常情况下,事件处理机制肯定会监控到某些I/O事件发生,即便是因为服务器太空闲,没有任何I/O事件发生,工作进程也不会无限阻塞,因为工作进程在一开始就设置好了一个定时器,这实现在初始化函数ngx_event_process_init()内,关于这个函数前面曾多次提到,所以下面直接看相关代码:
642:        Filename : ngx_event.c
643:                sa.sa_handler = ngx_timer_signal_handler;
644:        …
652:                itv.it_interval.tv_sec = ngx_timer_resolution / 1000;
653:                itv.it_interval.tv_usec = (ngx_timer_resolution % 1000) * 1000;
654:                itv.it_value.tv_sec = ngx_timer_resolution / 1000;
655:                itv.it_value.tv_usec = (ngx_timer_resolution % 1000 ) * 1000;
656:       
657:                if (setitimer(ITIMER_REAL, &itv, NULL) == -1) {
通过setitimer()函数设置的定时器会自动循环,所以每隔ngx_timer_resolution毫秒,工作进程就将收到一个定时事件,将其从事件处理机制的阻塞等待里唤醒出来(如果它正处于阻塞状态)。定时事件的回调函数为ngx_timer_signal_handler(),该函数简单扼要,仅设置一下标记:ngx_event_timer_alarm = 1;,这倒非常符合信号中断处理函数的特点。
只有在ngx_event_timer_alarm为1的情况下,工作进程才会更新它的时间,也就是工作进程的时间粒度为ngx_timer_resolution:
573:        Filename : ngx_epoll_module.c
574:            events = epoll_wait(ep, event_list, (int) nevents, timer);
575:       
576:            err = (events == -1) ? ngx_errno : 0;
577:       
578:            if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
579:                ngx_time_update();
580:            }
从上面代码可以看到,就算工作进程被I/O事件唤醒而执行到第578行,但只要ngx_event_timer_alarm不为1就不会(从前面可知,在这里讨论的超时检测方案1下,第578行前半句判断为假)执行时间更新函数ngx_time_update(),从而导致下面的第262行也为假,超时检测函数ngx_event_expire_timers()不会也不会被执行到:
244:        Filename : ngx_event.c
245:            delta = ngx_current_msec;
246:       
247:            (void) ngx_process_events(cycle, timer, flags);
248:       
249:            delta = ngx_current_msec - delta;
250:        …
262:            if (delta) {
263:                ngx_event_expire_timers();
264:            }
如果经过了ngx_timer_resolution毫秒,执行了定时函数ngx_timer_signal_handler(),设置了ngx_event_timer_alarm值为1,又更新了时间,那么第263行的超时检测函数ngx_event_expire_timers()自然会被执行到,这无需多说,下面再来看ngx_timer_resolution为0的情况,即超时检测方案2。
在超时检测方案2里,timer的值被设置为最快发生超时的事件对象的超时时刻与当前时刻的时间差。举个例子来说,比如红黑树管理着三个事件a、b、c,它们分别将在5000、6000、7000毫秒后超时,那么距离当前最快发生超时的就是事件a,而事件a的超时时刻与当前时刻的时间差为5000毫秒,因此变量timer的值就将被设置5000。timer值的具体计算实现在函数ngx_event_find_timer()内,该函数从红黑树内找到key值最小(key值记录的就是事件的超时时刻,那么该值最小的节点表示的也就是距离当前最快发生超时的事件)的节点,然后用该节点的key值减去当前时刻(ngx_current_msec,事实上由于该值并不是完全实时的,所以和精确的当前时刻会有一些偏差,不过不影响)即得到预期的timer值。预期的timer值可能为负数,这表示已经有事件超时了,因此直接将timer值设置为0,那么事件处理机制在开始监控I/O事件时会立即返回,以便能马上处理这些超时事件;另一个变量flags被标记为NGX_UPDATE_TIME,从前面第578行代码可以看到函数ngx_time_update()将被执行,时间被更新,也就是说事件处理机制每次返回都会更新时间,如果I/O事件比较多(比如客户端请求非常的多),那么将会导致比较频繁的调用gettimeofday()系统函数,这也可以说是超时检测方案2的最大缺点。
        回过头来看超时检测方案1,简单直观、容易理解,但有可能导致一些超时事件得不到及时的处理,不过这并不会造成多大问题,如果不放心则可以根据应用环境通过配置指令timer_resolution适当的调整一下ngx_timer_resolution值即可。
        来看最后一个需要讨论的问题,即对超时事件对象是否超时需进行的扫描检测以及对已超时事件对象的处理。由于工作进程利用红黑树来组织管理超时事件对象,因此检测是否有事件对象超时并不需要遍历扫描所有的超时事件对象,而直接找到最近的即将超时的超时事件对象,判断其是否超时,如果超时则将其移出红黑树、设置其超时标记(即将ev->timedout置为1)并调用该事件对应的回调处理函数进行处理,处理完了再判断第二近的即将超时的超时事件对象,如此反复,直到遇到某个超时事件对象还未超时或所有超时事件对象都已超时并处理完毕就结束检测;这整个逻辑具体实现都在函数ngx_event_expire_timers()内,流程图如下所示:



论坛徽章:
0
发表于 2012-08-16 07:34 |显示全部楼层
第六章 变量机制
初识变量
        前面曾讲过nginx配置文件的解析过程,也就是nginx如何在启动的过程中对用户设定的配置文件进行解析,并将配置文件中的各个配置项与配置值转换为对应的nginx内部变量值,从而能让nginx按照用户预想的情况去运行。       
如果只是一些比较简单并且确定的功能配置需求,那么nginx用户能够很方便的做出相应的设定,比如用户想要设置工作进程数为2个,那么配置文件中这样写即可:worker_processes 2;;与此同理,nginx也很容易做到按用户的配置要求去执行,比如这里nginx也就只需执行且仅执行2次fork()函数来生成工作进程即可,具体实现可利用for循环并通过控制上限值来做到:
360:        Filename : ngx_process_cycle.c
361:            for (i = 0; i < n; i++) {
362:        …
365:                ngx_spawn_process(cycle, ngx_worker_process_cycle, NULL,
366:                                  "worker process", type);
在上面的源代码里,for循环的条件判断上限值n(也就是ccf->worker_processes)即为2,它是通过解析配置项worker_processes时根据用户的具体设定而赋值的。
        如果是更高级一点的功能配置,比如当请求连接的客户端是ie浏览器时,nginx能自动将请求文件重定向到/msie目录下,那么nginx用户在配置文件里又该如何去表达这个逻辑呢?熟悉nginx的用户肯定知道要实现这个需求,我们可以这样配置(来之官方wiki文档示例:http://wiki.nginx.org/HttpRewriteModule#if):
49:        Filename : nginx.conf
50:                if ($http_user_agent ~ MSIE) {
51:                    rewrite ^(.*)$ /msie/$1 break;
52:                }
这样,我们用非ie浏览器访问该web站点时,请求的文件来之其根目录,而用ie浏览器访问该web站点时,请求的文件却来之其根目录下的msie文件夹(事实上,如果用ie浏览器做目录访问,即后面不带文件名,如果nginx配置了index模块,那么访问可能会出现这样的错误:2012/05/25 11:19:25 [error] 4274#0: *3 open() "/usr/local/nginx/web/msie//msie//index.html" failed (2: No such file or directory), client: 192.168.164.1, server: localhost, request: "GET / HTTP/1.1", host: "192.168.164.2",可以看到是因为被映射了两次,即首先根目录匹配,由/映射为/msie/,然后被index模块改为/msie//index.html后重定向,又匹配到if条件被再次映射为/msie//msie//index.html而导致路径错乱。关于这个错误以及官方提到的可以考虑用try_files替代if等暂不做过多讨论,本节仅以此作为示例讨论nginx变量)。
从上面的配置文件相关内容来看,对于稍懂一点编程知识的人来说,直观上这并没有什么难以理解的地方,无非先一个判断客户端是否为ie浏览器,是则将URI重定向到msie,否则继续原URI的操作,这看似非常简单的逻辑却至少需要一个东西的支撑,也就是必须要有一个符号(或别的什么)来代表客户端浏览器,nginx用户才能在配置文件里表达类似“当‘客户端浏览器’是什么,nginx就该怎么样,如果不是,nginx又该怎么样”这样的语义,而这个符号也就是本节将要重点介绍的nginx变量,如上面示例配置中的$http_user_agent就是一个nginx变量。
对于nginx而言,变量是指配置文件中以$开头的标识符(整个本章都不涉及SSI模块的变量,因为其比较独特,留待后面篇章专讲),这和编程语言PHP里的变量命名要求基本一致,当然,nginx变量的功能等各个方面肯定都要相对简单得多,这是不言而喻的,够用就好,毕竟nginx的主要功能不在这里。
和其它编程语言里的变量意义一致,nginx的变量也同样是指明有一块内存空间,其存放了会根据情况发生变化的动态值。比如,对于变量$http_user_agent所代表的一块内存空间而言,客户端用ie浏览器访问时,其内存放的值为MSIE,用非ie浏览器访问时,其内存放的值也许就变化为Opera或Safari等(根据客户端浏览器类型而定),但肯定就不是MSIE了,否则上下文中的if判断逻辑将失去它的作用,用户的设置也将失效。
不像PHP或C语言那样拥有众多的变量类型,nginx只有一种变量类型,即字符串,而且既然变量是用在配置文件中,那么根据曾在配置解析一章的讲解,变量值字符串加或者不加引号,加双引号或单引号都没有什么影响,除非字符串内包含有空格,需要利用引号或用转义字符(\)将它前后的字符连成一个字符串。
Nginx变量所代表的内存里存放的字符串当然不是凭空生成的,就像是在C语言里,我们定义一个变量后总会直接或间接的给它赋值,否则读取出来的就是垃圾数据,所以nginx变量也会被赋值,不过这种赋值大部分情况下是自动的,并且是延后的。
自动赋值的意思很简单,比如在上面的示例中,在整个配置文件内,我都没有对变量$http_user_agent进行赋值操作,但是却可以直接拿它来用,因为我知道在每一个客户端请求连接里,这个变量都会自动的被nginx赋值,要么为MSIE,或Opera、或Safari等,当然,这大家都知道原因,因为它是nginx内部变量。其实,我们实际使用中,大部分情况也就是使用内部变量,一方面在于nginx提供的内部变量非常的多,基本考虑了大多数使用场景,另一方面,如果你使用外部变量(或称之为自定义变量),那么就得给它赋值,如果是将一个确定的值(或内部变量)赋值给它,那么在使用这个变量的地方用这个确定的值(或内部变量)就行了,何必多此一举,除非是要根据特殊逻辑组织多个不同的确定值和(或)内部变量在一起成一个新的变量,不过这种情况一般也都比较少。
延后赋值,专业术语叫惰性求值(Lazy Evaluation),其实说清楚了也容易懂,它是从性能上的考虑。nginx光内部变量就有好几十个,如果每一个客户端请求,nginx都去给它们赋好值,但是配置文件里却有根本没用到,这岂不是大大的性能浪费?所以,对于大部分变量,只有真正去读它的值时,nginx才会临时执行一段代码先给它赋上相应的值,然后再将结果返回(当然还有其它细节,比如如果之前nginx已经给它赋好了值并且有效,就不用做第二次赋值直接返回即可,等),这种优化与编程中的另一种常见技术,即写时复制(Copy On Write)有异曲同工之妙。
内部变量意味着变量名是预先定义好的,Nginx目前具体提供有哪些预定义好的内部变量以及每个变量的含义在官方wiki文档(比如:http://wiki.nginx.org/HttpCoreModule#Variableshttp://wiki.nginx.org/HttpGeoipModule#geoip_country)上可以查看,也可以通过源代码(检索关键字:ngx_http_variable_t)根据变量名的英文单词猜测其代表的大致含义。除了http核心模块ngx_http_core_module提供了大量的内部变量之外,其它模块比如ngx_http_fastcgi_module、ngx_http_geoip_module等也有一些内部变量,如果我们自己开发nginx模块,自然也可以提供类似这样的内部变量供用户在nginx配置文件里使用。
除了内部变量之外,与之相对的就是外部变量(或称之为自定义变量),外部变量是nginx用户在配置文件里定义的变量,因此变量名可由用户随意设定,当然也是要以$开头,并且得注意不要覆盖内部变量名。目前nginx主要是通过ngx_http_rewrite_module模块的set指令来添加外部变量,当然也有其它模块比如ngx_http_geo_module来新增外部变量,这些在后面其它章节的分析中会看到其具体的实现。

支撑机制
        任意一个变量,都有其变量名和变量值,nginx与此对应的封装分别为结构体ngx_http_variable_s和ngx_variable_value_t:
16:        Filename : ngx_http_variables.h
17:        typedef ngx_variable_value_t  ngx_http_variable_value_t;
35:        struct ngx_http_variable_s {
36:            ngx_str_t                     name;   /* must be first to build the hash */
37:            ngx_http_set_variable_pt      set_handler;
38:            ngx_http_get_variable_pt      get_handler;
39:            uintptr_t                     data;
40:            ngx_uint_t                    flags;
41:            ngx_uint_t                    index;
42:        };
27:        Filename : ngx_string.h
28:        typedef struct {
29:            unsigned    len:28;
30:       
31:            unsigned    valid:1;
32:            unsigned    no_cacheable:1;
33:            unsigned    not_found:1;
34:            unsigned    escape:1;
35:       
36:            u_char     *data;
37:        } ngx_variable_value_t;
可以看到这两个结构体并非只是简单的包含其名与值,还有其它相关的辅助字段,甚至结构体ngx_http_variable_s本身就包含一个data字段,看似是用来存放变量值的地方,那为什么又还要一个专门的ngx_variable_value_t结构体来封装nginx变量值呢?关于这个问题,在本节后面的讲解中会逐步清晰,这里暂且不讲。
在进行配置解析之前,nginx会统计其支持的所有内部变量,也即在每个模块的回调函数module->preconfiguration内,将模块自身支持的内部变量统一加入到http核心配置ngx_http_core_main_conf_t的variables_keys字段内:
149:        Filename : ngx_http_core.module.h
150:        typedef struct {
151:        …
157:            ngx_hash_t                 variables_hash;
158:       
159:            ngx_array_t                variables;       /* ngx_http_variable_t */
160:        …
168:            ngx_hash_keys_arrays_t    *variables_keys;
169:        …
175:        } ngx_http_core_main_conf_t;
就以http核心模块ngx_http_core_module为例,其模块的preconfiguration回调函数为ngx_http_core_preconfiguration(),该函数就一条语句:调用ngx_http_variables_add_core_vars()函数,从而将自身支持的所有内部变量(组织在ngx_http_core_variables数组内)加入到cmcf->variables_keys变量内:
2014:        Filename : ngx_http_variables.c
2015:        ngx_int_t
2016:        ngx_http_variables_add_core_vars(ngx_conf_t *cf)
2017:        {
2018:        …
2022:            cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);
2023:       
2024:            cmcf->variables_keys = ngx_pcalloc(cf->temp_pool,
2025:                                               sizeof(ngx_hash_keys_arrays_t));
2026:        …
2039:            for (v = ngx_http_core_variables; v->name.len; v++) {
2040:                rc = ngx_hash_add_key(cmcf->variables_keys, &v->name, v,
2041:                                      NGX_HASH_READONLY_KEY);
2042:        …
上面代码中,函数ngx_hash_add_key()是实际执行往变量cmcf->variables_keys内进行新增操作的函数,除了http核心模块ngx_http_core_module以外,其它模块都会这么直接或间接的把自身支持的内部变量加到cmcf->variables_keys内,再比如ngx_http_proxy_module模块,其相关执行过程如下:
ngx_http_proxy_add_variables() -> ngx_http_add_variable() -> ngx_hash_add_key()
其中ngx_http_proxy_add_variables()是ngx_http_proxy_module模块的preconfiguration回调函数。不仅是内部变量,用户自定义的外部变量在配置文件的解析过程中也会被添加到cmcf->variables_keys内,这从外部变量的主要设置指令set的回调函数ngx_http_rewrite_set()的内部实现即可看出:
ngx_http_rewrite_set() -> ngx_http_add_variable() -> ngx_hash_add_key()
总之,当nginx解析配置正常结束时,所有的变量都被集中在cmcf->variables_keys内,那这有什么作用呢?继续来看。
Nginx在配置文件的解析过程中,会遇到用户使用变量的情况,如最前面的配置示例中使用了变量$http_user_agent,所有这些被用户在配置文件里使用的变量都会先通过ngx_http_get_variable_index()函数而被添加到cmcf->variables内(配置文件中出现:set $file t_a;,在这里这个$file变量既是定义,又是使用,先定义它,然后把字符串"t_a"赋值给它,这也是一种使用,所以它会被加入到cmcf->variables内,可以简单的认为nginx在解析配置文件的过程中遇到的所有变量都会被加入到cmcf->variables内;有些变量虽然没有出现在配置文件内,但是以nginx默认设置的形式出现在源代码里,比如ngx_http_log_module模块内的ngx_http_combined_fmt全局静态变量里就出现了一些nginx变量,也会被加入到cmcf->variables中;另外,有些变量是模块自身特有的,比如ngx_http_log_module模块内的$time_local变量,其模块自身具体专有逻辑来独自处理,从而没有加入到cmcf->variables内;nginx的哲学是怎么高效就怎么做,除非是对代码框架影响特别大,这也是我们在看源代码的过程中要注意的,所以我的描述也只能针对大多数情况,即便是我在叙述的过程中使用了“全”、“都”这样的字词也不代表就是绝对如此),这和我前面描述的一致,虽然nginx默认提供的变量有很多,但只需把我们在配置文件里真正用到了的变量给挑出来。当配置文件解析完后,所有用到的变量也被集中起来了,所有这些变量需要检查其是否合法,因为nginx不能让用户在配置文件里使用一个非法的变量,这就需要cmcf->variables_keys的帮忙。
这个合法性检测逻辑很简单,实现在函数ngx_http_variables_init_vars()内,其遍历cmcf->variables内收集的所有已使用变量,逐个去已定义变量cmcf->variables_keys集合里查找,如果找到则表示用户使用无误,如果没找到,则需要注意,这还只能说明它可能是一个非法变量,因为有一点之前一直没讲,那就是有一部分变量虽然没有包含在cmcf->variables_key内,但是它们却合法,这部分变量是以"http_"、"sent_http_"、"upstream_http_"、"cookie_"、"arg_"开头的五类变量,这些变量庞大并且不可预知,不可能提前定义并收集到cmcf->variables_keys内,比如以"arg_"开头代表的参数类变量会根据客户端请求uri时附带的参数不同而不同,一个类似于“http://192.168.164.2/?pageid=2”这样的请求就会自动生成变量$arg_pageid,因此还需判断用户在配置文件里使用的变量是否在这五类变量里,具体怎么判断也就是检测用户使用的变量名前面几个字符是否与它们一致(这也间接说明,用户自定义变量时不要以这些字符开头)。当然,如果用户在配置文件里使用了变量$arg_pageid,而客户端请求时却并没有带上pageid参数,此时也只不过是变量$arg_pageid值为空而已,但它总还算是合法,但如果提示类似如下这样的错误,请需检查配置文件内变量名是否书写正确:
nginx: [emerg] unknown "x_var_test" variable
函数ngx_http_variables_init_vars()在对已使用变量进行合法性检测的同时,对于合法的使用变量会将其对应的三个主要字段设置好,即get_handler()回调、data数据、flags旗标,从前面给出的结构体ngx_http_variable_s定义来看,name存储的是变量名字符串,index存储的是该变量在cmcf->variables内的下标(通过函数ngx_http_get_variable_index()获得),这两个都是不变的,而set_handlerr()回调目前只在使用set配置指令构造脚本引擎时才会用到,而那里直接使用cmcf->variables_keys里对应变量的该字段,并且一旦配置文件解析完毕,set_handlerr()回调也就用不上了,所以只有剩下的三个字段才需要做赋值操作,即从cmcf->variables_keys里对应变量的对应字段拷贝过来,或是另外五类变量就根据不同类别进行固定的赋值。
先看flags旗标字段,这里涉及到的旗标主要是两个:一个为NGX_HTTP_VAR_CHANGEABLE,表示该变量可重复添加,该标记影响的逻辑主要是变量添加函数ngx_http_add_variable()。比如如下配置不会出错,因为set指令新增的变量都是NGX_HTTP_VAR_CHANGEABLE的:
49:        Filename : nginx.conf
50:                set $file t_a;
51:                set $file t_b;
此时,set指令会重复添加变量$file(其实,第51行并不会新增变量$file,因为在新增的过程中发现已经有该变量了,并且是NGX_HTTP_VAR_CHANGEABLE的,所以就返回该变量使用),并且其最终值将为t_b。如果新增一个不是NGX_HTTP_VAR_CHANGEABLE的变量$t_var,那么nginx将提示the duplicate "t_var" variable后退出执行;
另一个标记为NGX_HTTP_VAR_NOCACHEABLE,表示该变量不可缓存,我们都知道,所有这些变量基本都是跟随客户端请求的每个连接而变的,比如变量$http_user_agent会随着客户端使用浏览器的不同而不同,但是在客户端的同一个连接里,这个变量肯定不会发生改变,即不可能一个连接前半个是IE浏览器而后半个是Opera浏览器,所以这个变量是可缓存的,在处理这个客户端连接的整个过程中,变量$http_user_agent值计算一次就行了,后续使用可直接使用其缓存。然而,有一些变量,因为nginx本身的内部处理会发生改变,比如变量$uri,虽然客户端发过来的请求连接URI是/thread-3760675-2-1.html,但通过rewrite一转换却变成了/thread.php?id=3760675&page=2&floor=1,也即是变量$uri发生了改变,所以对于变量$uri,每次使用都必须进行主动计算(即调用回调get_handler()函数),该标记影响的逻辑主要是变量取值函数ngx_http_get_flushed_variable()。当然,如果我们明确知道当前的细节情况,此时从性能上考虑,也不一定就非要去重新计算获取值,比如刚刚通过主动计算获取了变量$uri的值,接着马上又去获取变量$uri的值(这种情况当然有,例如连续将$uri变量的值赋值给另外两个不同变量),此时可使用另外一个取值函数ngx_http_get_indexed_variable(),直接取值而不考虑是否可缓存标记。
再来看data数据字段,这个字段指向存放该变量值的地方,具体点说是指向结构体ngx_http_request_t变量r中的某个字段。我们知道(或者将要知道,下文会讲到)一个nginx变量总是与具体的http请求绑定在一起的,一个http请求总有一个与之对应的ngx_http_request_t变量r,该变量r内存放有大量的与当前http请求相关的信息,而大部分nginx变量的值又是与http请求相关的,简而言之,nginx内置变量的值大部分直接或间接的来之变量r的某些字段内。举个例子,nginx内部变量$args表示的是客户端GET请求时uri里的参数,熟悉结构体ngx_http_request_t定义的人知道该结构体有一个ngx_str_t类型字段为args,其内存放的就是GET请求参数,所以内部变量$args的这个data字段就是指向变量r里的args字段,表示其数据来之这里。这是直接的情况,那么间接的情况呢?看nginx内部变量$remote_port,这个变量表示客户端端口号,这个值在结构体ngx_http_request_t内没有直接的字段对应,但是肯定同样也是来之ngx_http_request_t变量r里,怎么去获取就看get_handler()函数的实现,此时data数据字段没什么作用,值为0。
最后来看get_handler()回调字段,这个字段主要实现获取变量值的功能。前面讲了nginx内置变量的值都是有默认来源的,如果是简单的直接存放在某个地方(上面讲的内部变量$args情况),那么不要这个get_handler()回调函数倒还可以,通过data字段指向的地址读取;但是如果比较复杂,虽然知道这个值存放在哪儿,但是却需要比较复杂的逻辑获取(上面讲的内部变量$remote_port情况),此时就必须靠回调函数get_handler()来执行这部分逻辑。总之,不管简单或复杂,回调函数get_handler()帮我们去在合适的地方通过合适的方式,获取到该内部变量的值,这也是为什么我们并没有给nginx内部变量赋值,却又能读到值,因为有这个回调函数的存在。来看看这两个示例变量的data字段与get_handler()回调字段情况:
191:        Filename : ngx_http_variables.c
192:            { ngx_string("args"),
193:              ngx_http_variable_request_set,
194:              ngx_http_variable_request,
195:              offsetof(ngx_http_request_t, args),
196:              NGX_HTTP_VAR_CHANGEABLE|NGX_HTTP_VAR_NOCACHEABLE,0},
197:        …
555:        static ngx_int_t
556:        ngx_http_variable_request(ngx_http_request_t *r, ngx_http_variable_value_t *v,
557:            uintptr_t data)
558:        {
559:        …
561:            s = (ngx_str_t *) ((char *) r + data);
562:       
563:            if (s->data) {
564:        …
568:                v->data = s->data;
因为data字段的帮助,变量$args的get_handler()回调函数ngx_http_variable_request()的实现非常的简单。
155:        Filename : ngx_http_variables.c
156:            { ngx_string("remote_port"), NULL, ngx_http_variable_remote_port, 0, 0, 0 },
157:        ...
1039:        static ngx_int_t
1040:        ngx_http_variable_remote_port(ngx_http_request_t *r,
1041:            ngx_http_variable_value_t *v, uintptr_t data)
1042:        {
1043:            ngx_uint_t            port;
1044:        …
1059:            switch (r->connection->sockaddr->sa_family) {
1060:       
1061:        #if (NGX_HAVE_INET6)
1062:            case AF_INET6:
1063:                sin6 = (struct sockaddr_in6 *) r->connection->sockaddr;
1064:                port = ntohs(sin6->sin6_port);
1065:                break;
1066:        #endif
再看变量$remote_port的get_handler()回调函数ngx_http_variable_remote_port()的处理就比较麻烦了,上面只给出了部分代码,它根据不同的情况做不同的处理,此时data字段也没用了。
一并再来看下set_handler(),这个回调目前只被使用在set指令里,组成脚本引擎的一个步骤,提供给用户在配置文件里可以修改内置变量的值,带有set_handler()接口的变量非常的少,比如变量$args、$limit_rate,这类变量一定会带上NGX_HTTP_VAR_CHANGEABLE标记,否则这个接口毫无意义,因为既然不能修改,何必提供修改接口?也会带上NGX_HTTP_VAR_NOCACHEABLE标记,因为既然会被修改,自然也是不可缓存的。下面看看变量$args的set_handler()接口函数ngx_http_variable_request_set():
577:        Filename : ngx_http_variables.c
578:        static void
579:        ngx_http_variable_request_set(ngx_http_request_t *r,
580:            ngx_http_variable_value_t *v, uintptr_t data)
581:        {
582:            ngx_str_t  *s;
583:       
584:            s = (ngx_str_t *) ((char *) r + data);
585:       
586:            s->len = v->len;
587:            s->data = v->data;
588:        }
直接修改了结构体ngx_http_request_t变量r里的args字段(因为data会指向那里)。由此可以看到,不管从哪方面来讲,data字段都只是一个辅助get_handler()、set_handler()回调处理的指示字段,在调用这两个回调函数时,会把data指定传递进来,以明确指定变量值来源的地方,简化和统一这两个回调函数的逻辑,所以你能看到大多数变量的get_handler()回调字段都是指向ngx_http_variable_header()、ngx_http_variable_request()这样的通用函数。其实,如果你有必要,data字段完全可以设置其它值以便传到get_handler()、set_handler()这两个回调处理函数里,这就回答了前面的疑问:为什么结构体ngx_http_variable_s里已经包含有一个data字段了,nginx还要弄一个专门的ngx_variable_value_t结构体封装来nginx变量值,因为“这个”data字段不是我们设想的“那个”data字段。
是否可以把ngx_variable_value_t结构体的所有字段都移到结构体ngx_http_variable_s内,将变量值和变量名组织在一起呢?非要这样做(假设合并而成的结构体为ngx_http_variable_name_value_t,有些重复字段要改一下,比如ngx_variable_value_t里的data改为value_data等),当然可以,但是如果那样设计的话,以现在的代码逻辑,在nginx里使用nginx变量名时,所有ngx_variable_value_t这些字段是否都会浪费(即它们用不上)?而当使用nginx变量值时,那所有的ngx_http_variable_s那些字段又是多余(因为,此时那些字段也用不上)?举个例子,合并之后,对于变量$args,就有个对应的结构体变量ngx_http_variable_name_value_t来统一描述它的名称和值,而我们知道变量是与请求相关联的,这也就是说nginx工作进程当前有处理个客户端请求正在处理,就有多少份$args变量,假设当前有3个客户端请求在处理,从而变量$args也就有三份,对应结构体ngx_http_variable_name_value_t里的关于对变量名的描述就有三份,这岂不是大大内存浪费?这也违背高性能设计里同一份数据只存一份的设计原则(因为存放多份一样的数据,不管是生成、更新、维护都麻烦)。按照现在nginx对变量的设计,三个请求的$args变量如下所示,可以看到$args变量名只存一份,而$args变量值根据每个请求而存三份,虚线箭头是指各个$args变量值根据$args变量名的data字段与http请求对象的args字段关联起来(调用get_handler()、set_handler()回调函数时,会把当前http请求对象r传递进去):

如果合二结构体为一个,那么就是如下这样的情况,相比现在的设计,多次保存$args变量名就是对内存的一种浪费:

现在,我们知道在nginx内部,对于多个变量,其变量名只会保存一次,那么怎么把变量名和变量值对应起来呢?也就是说,比如要读取变量的值,该利用哪个变量名的get_handler()回调函数呢?关键点就在变量名里的index字段,关于这个字段在前面说过,它的值来之将变量添加cmcf->variables内时所对应的数组下标,比如假定cmcf->variables数组内当前已有6个nginx变量,如果此时再新增一个使用变量$a,那么$a的index就是6(注意下标的序号是从0开始)。当然,在这里,为什么说index字段很关键,下面继续来看就会理解了。
继续来看函数ngx_http_variables_init_vars()后面的逻辑,可以看到cmcf->variables_keys变量指NULL,其原本实际所占的内存空间因为在cf->temp_pool内(函数ngx_http_variables_add_core_vars()的第2024行),所以在初始化基本结束后也会被释放掉(函数ngx_init_cycle()的第717行):
41:        Filename : ngx_cycle.c
42:        ngx_cycle_t *
43:        ngx_init_cycle(ngx_cycle_t *old_cycle)
44:        {
45:        …
717:            ngx_destroy_pool(conf.temp_pool);
因此,关于nginx变量,到最后,我们就剩下了一个cmcf->variables数组,里面存放了所有用户用到的变量,但是要清楚cmcf->variables数组存放的只是有可能被用到的变量,因为在实际处理客户端请求的过程中,根据请求的不同(比如请求地址、传递参数等)执行的具体路径也不相同,所以实际用到的变量也不相同。另外,刚刚讲了,cmcf->variables数组存放的只是各个变量名(以及相关属性、回调字段),其变量值是通过另外一个结构体ngx_variable_value_t变量来存储的,所以必须为这个变量申请对应的内存空间。这在nginx处理每一个客户端请求时的初始化函数ngx_http_init_request()内创建了这个存储空间:
236:        Filename : ngx_http_request.c
237:        static void
238:        ngx_http_init_request(ngx_event_t *rev)
239:        {
240:        …
478:            r->variables = ngx_pcalloc(r->pool, cmcf->variables.nelts
479:                                                * sizeof(ngx_http_variable_value_t));
这个变量和cmcf->variables是一一对应的,形成var_name与var_value对,所以两个数组里的同一个下标位置元素刚好就是相互对应的变量名和变量值,而我们在使用某个变量时总会先通过函数ngx_http_get_variable_index()获得它在变量名数组里的index下标,也就是变量名里的index字段值,然后利用这个index下标进而去变量值数组里取对应的值,这就解释了前面所提到的疑问。
对于子请求,虽然有独立的ngx_http_request_t对象r,但是却没有额外创建的r->variables,和父请求(或者说主请求)是共享的,这在ngx_http_subrequest()函数里可以看到相应的代码:
2365:        Filename : ngx_http_core_module.c
2366:        ngx_int_t
2367:        ngx_http_subrequest(ngx_http_request_t *r,
2368:            ngx_str_t *uri, ngx_str_t *args, ngx_http_request_t **psr,
2369:            ngx_http_post_subrequest_t *ps, ngx_uint_t flags)
2370:        {
2371:        …
2373:            ngx_http_request_t            *sr;
2374:        …
2386:            sr = ngx_pcalloc(r->pool, sizeof(ngx_http_request_t));
2387:        …
2455:            sr->variables = r->variables;
针对子请求,虽然重新创建了ngx_http_request_t变量sr,但子请求的nginx变量值数组sr->variables却是直接指向父请求的r->variables,其实这并不难理解,因为父子请求的大部分变量值都是一样的,当然没必要申请另外的空间,而对于那些父子请求之间可能会有不同变量值的变量,又有NGX_HTTP_VAR_NOCACHEABLE标记的存在,所以也不会有什么问题。比如变量$args,在父请求里去访问该变量值时,发现该变量是不可缓存的,于是就调用get_handler()函数从main_req对象的args字段(即r->args)里去取,此时得到的是page=9999;而在子请求里去访问该变量值时,发现该变量是不可缓存的,于是也调用get_handler()函数从sub_req对象的args字段(即sr->args,注意对象sr与r之间是分割开的)里去取,此时得到的是id=12;因而,在获取父子请求之间可变变量的值时,并不会相互干扰:

关于nginx变量的基本支撑机制就大概是上面介绍的这些,另外值得说明的的是,函数ngx_http_variables_init_vars()里还有一些没提到的代码以及相关逻辑,这包括旗标NGX_HTTP_VAR_INDEXED、NGX_HTTP_VAR_NOHASH、变量cmcf->variables_hash以及取值函数ngx_http_get_variable()等,它们都是为SSI模块实现而设计的,所以本章暂且不讲,否则夹杂在一起反而搞混,这里仅提醒注意一下,在SSI模块专章时再回头来看这部分。

脚本引擎
有了对变量支撑机制的了解,下面就直接进入脚本引擎的主题,可通过“set $file t_a;”这个非常简单的实例来描述脚本引擎的大致情况。该实例虽然简单,但已包含脚本引擎处理的基本过程,更复杂一点的情况无非也就是回调处理多几重、相关数据多一点而已。
nginx在解析配置文件时遇到“set $file t_a;”这句配置项就会执行set指令相应的回调函数ngx_http_rewrite_set(),下面开始逐步分析。
首先,value字符串数组(其实它本身只是一个字符串指针,但因为它指向的是数组变量cf->args的elts字段,所以可以认为它是一个数组。类似于这种细节,后面都不再一一解释,请根据上下文环境自行理解)包含有三个元素,分别为set、$file、t_a,其中set是指令符号,抛开不管,所以第一个被处理的字符串为$file,我们知道set是用来设置自定义变量的,所以先判断变量名是否合法(即第一个字符是否为$符号),合法则利用函数ngx_http_add_variable()将它加入到变量集cmcf->variables_keys里,同时利用函数ngx_http_get_variable_index()将它也加入到已使用变量集cmcf->variables内并获取它的对应下标index,以便后续使用它。这些都是准备工作,其相关代码如下:
891:        Filename : ngx_http_rewrite_module.c
892:        static char *
893:        ngx_http_rewrite_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
894:        {
895:        …
905:            if (value[1].data[0] != '$') {
906:        …
914:            v = ngx_http_add_variable(cf, &value[1], NGX_HTTP_VAR_CHANGEABLE);
915:        …
919:            index = ngx_http_get_variable_index(cf, &value[1]);
接下来就是构建“set $file t_a;”所对应的脚本引擎,脚本引擎是一系列的回调函数以及相关数据(它们被组织成ngx_http_script_xxx_code_t这样的结构体,代表各种不同功能的操作步骤),被保存在变量lcf->codes数组内,而ngx_http_rewrite_loc_conf_t类型变量lcf是与当前location相关联的,所以这个脚本引擎只有当客户端请求访问当前这个location时才会被启动执行。如下配置中,“set $file t_a;”构建的脚本引擎只有当客户端请求访问/t目录时才会被触发,如果当客户端请求访问根目录时则与它毫无关系:
13:        Filename : nginx.conf
14:                location / {
15:                    root web;
16:                }
17:                location /t {
18:                    set $file t_a;
19:                }
这也可以说是nginx变量惰性求值特性的根本来源,没触发脚本引擎或没执行到的脚本引擎路径,自然不会去计算其相关变量的值。
在函数ngx_http_rewrite_set()接下来的逻辑里就如何去构建相对应的脚本引擎,“set $file t_a;”配置语句比较简单,略去过多无关重要的细节,仅关注与其相关的关键执行代码路径,第一个重点关注逻辑在函数ngx_http_script_value_code_t()内:
963:        Filename : ngx_http_rewrite_module.c
964:        static char *
965:        ngx_http_rewrite_value(ngx_conf_t *cf, ngx_http_rewrite_loc_conf_t *lcf,
966:            ngx_str_t *value)
967:        {
968:        …
976:                val = ngx_http_script_start_code(cf->pool, &lcf->codes,
977:                                               sizeof(ngx_http_script_value_code_t));
978:        …
988:                val->code = ngx_http_script_value_code;
989:                val->value = (uintptr_t) n;
990:                val->text_len = (uintptr_t) value->len;
991:                val->text_data = (uintptr_t) value->data;
函数ngx_http_script_start_code()利用ngx_array_push_n()在lcf->codes数组内申请了sizeof(ngx_http_script_value_code_t)个元素,注意每个元素的大小为一个字节,所以其实也就是为ngx_http_script_value_code_t类型变量val申请存储空间(很棒的技巧)。接着第988行开始为保存回调函数以及相关数据。
第二个重点关注的逻辑在函数ngx_http_rewrite_set()内,其继续保存ngx_http_script_xxx_code_t类结构体变量:
891:        Filename : ngx_http_rewrite_module.c
892:        static char *
893:        ngx_http_rewrite_set(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
894:        {
895:        …
933:            if (ngx_http_rewrite_value(cf, lcf, &value[2]) != NGX_CONF_OK) {
934:        …
951:            vcode = ngx_http_script_start_code(cf->pool, &lcf->codes,
952:                                               sizeof(ngx_http_script_var_code_t));
953:        …
957:            vcode->code = ngx_http_script_set_var_code;
958:            vcode->index = (uintptr_t) index;
逻辑很简单,利用函数ngx_http_script_start_code()为ngx_http_script_var_code_t类型变量vcode申请存储空间,然后保存回调函数以及相关数据。
上面具体代码执行路径被我略去了,总之,结果就是如下图示这样,nginx创建了两个结构体变量,并且设置好了字段值:

可以看到这两个结构体变量在地址空间上是连续存储的(图中,我特意把每个结构体字段的地址给标了出来),这一点非常重要,因为在脚本引擎实际执行时,回调函数前后的依次调用就靠这个来保证。到这里,关于配置项目set $file t_a;而言,整个set指令就已完成了它原本的功能,对应的回调函数ngx_http_rewrite_set()构建了这么一个脚本引擎的基础结构(每一个结构体变量代表脚本引擎的一个步骤),但这个脚本引擎还没‘跑’起来。要让这个脚本引擎跑起来,我们把这个配置项目放到配置文件的某一个location下,然后去请求这个location,此时nginx就会要执行这个配置语句,对应的脚本引擎自然也就‘跑’起来了。
为了判断脚本引擎‘跑’起来后的效果,我们需要查看变量$file的值,这可以借助互联网上提供的第三方开源模块,比如echo模块(http://wiki.nginx.org/HttpEchoModule),不过我们这里可以灵活利用一下rewrite指令即可,在配置文件里设定如下配置项:
13:        Filename : nginx.conf
14:                location / {
15:                    root web;
16:                }
17:                location /t {
18:                    set $file t_a;
19:                    rewrite ^(.*)$ /index.html?$file redirect;
20:                    root  html;
21:                }
这样,任何对t目录的访问都被无条件的重定向到根目录,并且将变量$file的内容(这里也就是"t_a")以参数的形式带过去。由于redirect指令会以http状态码302来指示浏览器重新请求新的URI,因此我们能在浏览器地址栏里间接的看到$file的值,比如wget看到的情况:

前面章节曾讲过,nginx将对客户端的连接请求响应处理分成11个阶段,每一个阶段可以有零个或多个回调函数进行专门处理,而在这里,当客户端对/t目录进行的请求访问时,nginx执行到NGX_HTTP_REWRITE_PHASE阶段的回调函数ngx_http_rewrite_handler()时,就会触发该location上脚本引擎的执行:
135:        Filename : ngx_http_rewrite_module.c
136:        static ngx_int_t
137:        ngx_http_rewrite_handler(ngx_http_request_t *r)
138:        {
139:        …
166:            e->sp = ngx_pcalloc(r->pool,
167:                                rlcf->stack_size * sizeof(ngx_http_variable_value_t));
168:        …
172:            e->ip = rlcf->codes->elts;
173:        …
178:            while (*(uintptr_t *) e->ip) {
179:                code = *(ngx_http_script_code_pt *) e->ip;
180:                code(e);
181:            }
脚本引擎的执行逻辑也非常的简单,因为刚提到脚本引擎各步骤在内存地址空间上连续,所以前一步骤的回调执行完后,指针偏移到下一步,然后判断是否有效,有效则接着执行,如此反复。由于每个步骤自身占据多大空间只有自己清楚,因此回调指针的偏移操作是由各个步骤来处理的,以这里的实例来看,第一个步骤对应的是结构体ngx_http_script_value_code_t变量,回调函数为ngx_http_script_value_code():
1650:        Filename : ngx_http_script.c
1651:        void
1652:        ngx_http_script_value_code(ngx_http_script_engine_t *e)
1653:        {
1654:            ngx_http_script_value_code_t  *code;
1655:       
1656:            code = (ngx_http_script_value_code_t *) e->ip;
1657:       
1658:            e->ip += sizeof(ngx_http_script_value_code_t);
1659:       
1660:            e->sp->len = code->text_len;
1661:            e->sp->data = (u_char *) code->text_data;
1662:        …
1666:            e->sp++;
1667:        }
很容易看出来,上面代码中的第1658行就是做回调指针偏移操作,加上当前结构体ngx_http_script_value_code_t变量大小即可。另外,这也隐含的默认所有的ngx_http_script_xxx_code_t结构体第一个字段必定为回调指针,如果我们添加自己的脚本引擎功能步骤,这点就需要注意。
第一步骤的回调函数ngx_http_script_value_code()处理完后,转到ngx_http_rewrite_handler()函数的第178行判断,为真,所以接着执行结构体ngx_http_script_var_code_t变量的回调函数ngx_http_script_set_var_code(),同样做相应的偏移,再判断就会进入到rewrite指令所对应的处理步骤里。先不管后面步骤,只看与set指令相关的两个步骤,我们知道set指令是让nginx用户给变量赋值,这里“set $file t_a;”即是将字符串"t_a"赋值给变量$file,所以这个逻辑也就是实现在刚才的那两个步骤里,具体来说是两个函数ngx_http_script_value_code()与ngx_http_script_set_var_code()。
在继续分析之前,需要先提一个变量e->sp,它是一个数组,在ngx_http_rewrite_handler()函数的第166行申请空间,就是通过它来在脚本引擎的各个步骤之间进行数据的传递。对于它的使用,有点类似于C语言函数调用栈帧,存入传递值就压栈,取传递值就退栈。比如看上面ngx_http_script_value_code()函数的实现代码,它是将用户设定的值(用户在配置文件里设定的字符串"t_a"以及长度在nginx解析配置文件时存在了ngx_http_script_value_code_t结构体变量的相关字段内)存起来,所以在第1660、1661以及1666行的代码,就是转存用户设定值并压栈(注意栈顶数据为空)。而函数ngx_http_script_set_var_code()就是取值退栈:
1669:        Filename : ngx_http_script.c
1670:        void
1671:        ngx_http_script_set_var_code(ngx_http_script_engine_t *e)
1672:        {
1673:        …
1676:            code = (ngx_http_script_var_code_t *) e->ip;
1677:       
1678:            e->ip += sizeof(ngx_http_script_var_code_t);
1679:        …
1682:            e->sp--;
1683:       
1684:            r->variables[code->index].len = e->sp->len;
1685:        …
1688:            r->variables[code->index].data = e->sp->data;
变量code->index表示nginx变量$file在cmcf->variables数组内的下标,对应每个请求的变量值存储空间就为r->variables[code->index],这里从栈中取出数据并进行变量实际赋值。
基本过程就是,利用ngx_http_script_value_code()函数将"t_a"存储到临时空间(e->sp栈),然后利用函数ngx_http_script_set_var_code()从临时空间(e->sp栈)取值放到变量$file内,整个set指令的逻辑工作得以完成。
更复杂一点的nginx配置被解析后生成的脚本引擎及其执行,与上面的介绍并无特别大的差异,只是在脚本引擎的具体生成过程中可能会涉及到正则式的处理,比如:
        # rewrite /download/*/mp3/*.any_ext to /download/*/mp3/*.mp3
rewrite ^/(download/.*)/mp3/(.*)\..*$ /$1/mp3/$2.mp3 break;
        前面的“^/(download/.*)/mp3/(.*)\..*$”就是一个正则匹配,^表示开头,$表示结尾,(download/.*)与(.*)分别对应后面的变量$1,$2,像这个路径:/download/20120805/mp3/sample.txt,其对应的变量$1的值为download/20120805,变量$2的值为sample,所以rewrite后的路径为/download/20120805/mp3/sample.mp3。关于这方面的更多内容不做过多介绍,对于复杂脚本引擎感兴趣的或遇到实际问题的,可自行查看MAN手册和nginx相关源代码,我相信有了前面介绍的基础知识,那不会太难理解,无非是细节代码繁琐一点。

执行顺序
        关于nginx变量(或者说是其所在的脚本引擎)的执行顺序,这是一个值得关注的话题,因为不理解它的内在原理,就容易让人在nginx配置文件里实际使用变量时出现困惑;但对于ngnix本身来说,这也是自然而然的事情,在前面的模块解析一章曾描述过nginx将对客户端请求的处理分成11个阶段,每一个阶段前后按序执行,那么与此对应的nginx变量也将受此影响,而出现貌似不合常理的异常情况。举个实例来说,假设在nginx配置文件里有这么一段配置(这段配置在实际使用中毫无用处,这里仅作问题描述):
49:        Filename : nginx.conf
50:                location / {
51:                    root   web;
52:                    set $file index1.html;
53:                    index $file;
54:        …
65:                    set $file index2.html;
66:        …
第52行设置变量$file的值为index1.html,第53行再通过index配置指令来指定根目录的首页文件为变量$file(也就是index1.html),这是我们原本的意图。在接下来的配置里,变量$file的值又被修改作为它用,比如也许被修改为logs/root_access.log,然后用户access_log配置指令来指定根目录的访问日志文件。这里为了作对比演示,我们就直接把它设置为index2.html,并且index1.html和index2.html的文件内容也非常简单,分别为:
[root@localhost web]# cat index1.html
<center><h1>1</h1></center>
[root@localhost web]# cat index2.html
<center><h1>2</h1></center>
利用这个配置文件执行nginx后,通过curl命令来请求访问该根目录:

奇怪的发现,nginx返回的内容来之文件index2.html,完全超出我们原本的设想,这是不是nginx的bug呢?当然不是,其真实原因正是由于受到变量执行顺序的影响。
前面已经说过nginx对客户端的请求是分阶段处理的,配置文件里使用到的nginx变量会跟随处理阶段的向前推进而逐个被执行到,而与它在配置文件里的具体前后位置并没有关系(当然,必须都在本次会执行到的路径上)。由于在nginx启动阶段,通过对配置文件的逐行解析,会把属于同一阶段的变量集中在一起。如在上面的实例中,虽然两条set指令使用的$file变量跨越了index指令使用的$file变量,但在配置文件解析后,其效果变成了类似于这样:

当一个客户端请求过来时,在REWRITE_PHASE阶段,将依次执行“set $file index1.html;”、“set $file index2.html;”,再到CONTENT_PHASE阶段执行ngx_http_index_module模块的逻辑时,$file变量的值已经是index2.html,所以nginx返回给客户端的才是文件index2.html的内容。
        上面给出的只是一个非常简单的例子,但是也较为清楚的说明了nginx变量的执行顺序及其内在原因。如果继续举例也没有太大必要,毕竟原理就这么简单,我们在实际进行nginx配置时,也就要特别注意配置文件里都使用了哪些nginx变量,每个nginx变量都使用在哪些配置指令里,避免出现受变量执行顺序的隐含影响,导致nginx工作不正常的情况。

论坛徽章:
0
发表于 2012-08-16 07:34 |显示全部楼层
第七章 请求处理
创建监听套接口
前面章节曾陆陆续续的提到过nginx对客户端请求的处理,但不甚连贯,所以本章就尝试把这个请求处理响应过程完整的描述一遍。下面先来看后http请求处理的前置准备工作,也就是监听套接口的创建以及组织等。
创建哪些监听套接口当然是由用户来指定的,nginx提供的配置指令为listen(仅关注http模块:http://wiki.nginx.org/HttpCoreModule#listen),该指令功能非常的丰富,不过在大部分情况下,我们都用得比较简单,一般是指定监听ip和端口号(因为http协议是基于tcp,所以这里自然也就是tcp端口),比如:listen 192.168.1.1:80;,这表示nginx仅监听目的ip是192.168.1.1且端口是80的http请求;如果主机上还有一个192.168.1.2的ip地址,那么客户端对该地址的80端口访问将被拒绝,要让该地址也正常访问需同样把该ip加入:listen 192.168.1.2:80;,如果有更多ip,这样逐个加入比较麻烦,因而另一种更偷懒的配置方法是只指定端口号:listen 80;,那么此时任意目的ip都可以访问到。不过,这两种不同的配置方式会影响到nginx创建监听套接口的数目,前一种方式nginx会对应的创建多个监听套接口,而后一种方式,由于listen 80;包含了所有的目标ip,所以创建一个监听套接口就足以,即便是配置文件里还有listen 192.168.1.1:80;这样的配置。看看实例,感性的认识一下:
30:        Filename : nginx.conf
15:            server {
16:                listen       80;
17:        ...
34:            server {
35:                listen       192.168.1.1:80;
36:       
上面配置中有两个server,第一个配置listen的目标ip为任意(只要是本主机有的),第二个配置listen的目标ip为192.168.1.1,但是nginx在创建监听套接口时却只创建了一个:
[root@localhost html]# netstat -ntap | grep nginx
tcp    0    0    0.0.0.0:80    0.0.0.0:*    LISTEN    13040/nginx
        如果将第16行配置改为listen 192.168.1.2:80;,那么此时的nginx将创建两个监听套接口:
[root@localhost nginx]# netstat -ntpa | grep nginx
tcp    0    0    192.168.1.1:80    0.0.0.0:*    LISTEN    13145/nginx
tcp    0    0    192.168.1.2:80    0.0.0.0:*    LISTEN    13145/nginx
再来看nginx代码的具体实现,配置指令listen的使用上下文为server,其对应的处理函数为ngx_http_core_listen(),该函数本身的功能比较单一,主要是解析listen指令并将对应的结果存到变量lsopt(可能有人注意到这是一个局部变量,不过没关系,在后面的函数调用里通过结构体赋值的方式,将它的值全部复制给另外一个变量addr->opt)内,最后调用函数ngx_http_add_listen(),这才是此处关注的核心函数,它将所有的listen配置以[port,addr]的形式组织在http核心配置ngx_http_core_main_conf_t下的ports数组字段内。另外,如果有一个server内没有配置监听端口,那么nginx会自动创建一个变量lsopt并给出一些默认值,然后调用函数ngx_http_add_listen()将其组织到ports数组字段内,这在server块配置的回调函数ngx_http_core_server()最后可以看到:
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:        …
2790:            if (rv == NGX_CONF_OK && !cscf->listen) {
2791:                ngx_memzero(&lsopt, sizeof(ngx_http_listen_opt_t));
2792:        …
2795:                sin->sin_family = AF_INET;
2796:        …
2799:                sin->sin_port = htons((getuid() == 0) ? 80 : 8000);
2800:        #endif
2801:                sin->sin_addr.s_addr = INADDR_ANY;
2802:        …
2816:                if (ngx_http_add_listen(cf, cscf, &lsopt) != NGX_OK) {
2817:        …
默认的设置是ipv4协议族、80或8000端口、任意目的ip,所以不管怎么样,一个server配置至少有一个监听套接口。回过头来看ngx_http_add_listen()函数以及相关逻辑,具体代码并没有什么难以理解的地方,我们直接看一个实例以及对应的图示,这样更直观且能把握全局。仍接着前面的实例,再加一个server配置:
00:        Filename : nginx.conf
15:            server {
16:                listen       80;
17:                server_name  www.other_all.com;
18:        ...
34:            server {
35:                listen       192.168.1.1:80;
36:                server_name  www.web_test1.com;
37:        …
53:            server {
54:                listen       192.168.1.2:8000;
55:                server_name  www.web_test2.com;
56:        …

当nginx的http配置块全部解析完后,所有的监听套接口信息(包括用户主动listen配置或nginx默认添加)都已被收集起来,先按port端口分类形成数组存储在cmcf->ports内,然后再在每一个port内按ip地址分类形成数组存储在port->addrs内,也就是一个[port, addr]的二维划分,如上图所示。附带说一下,其实一个[port, addr]可以对应有多个server配置块,但这里的实例中server配置块只有一个,所以也就是默认配置块default_server;对应server配置块的多少并不会影响到监听套接口的创建逻辑,因为创建监听套接口依赖的是[port, addr]本身,而非它对应的server配置块。回到刚才的思路上,在http配置指令的回调函数ngx_http_block()最后,也就是http配置块全部解析完后,将调用ngx_http_optimize_servers()函数‘创建’对应的监听套接口,之所以打上引号是因为这里还只是名义上的创建,也就只是创建了每个监听套接口所对应的结构体变量ngx_listening_s,并以数组的形式组织在全局变量cycle->listening内,具体的函数调用关系如下:
ngx_http_optimize_servers() -> ngx_http_init_listening() -> ngx_http_add_listening() -> ngx_create_listening()
先关注两点:第一,如果某端口上有任意目的ip的listen配置,那么在该端口上只会创建一个结构体变量ngx_listening_s,不管是否还有其他ip在该端口上的listen配置,进而在后面创建监听套接口描述符时也只创建一个,这在前面的演示实例里验证过这种情况,相关逻辑代码实现在函数ngx_http_init_listening()内,比如局部变量bind_wildcard,也包括前面函数中对ip地址排序的准备工作等。第二,ngx_listening_s结构体变量ls的回调字段handler被设置为ngx_http_init_connection,注意到这点可以帮助我们在看后面的逻辑时,代码回调该函数时,就能清楚它实际执行的是哪个函数。
另外要关注的是监听套接口与server配置块的关联,这是必须的,因为当监听套接口上一个客户端请求到达时,nginx必须知道它对应的server配置才能做进一步处理,这部分逻辑主要在函数ngx_http_add_addrs()内(以ipv4为例),调用关系如下:
ngx_http_optimize_servers() -> ngx_http_init_listening() -> ngx_http_add_addrs()
到此,在上面所举示例里,所有等待创建的监听套接口以及相关数据组织结构如下所示("www.web_test1.com"排到了"www.other_all.com"的前面,是因为ngx_sort()排序的缘故):

在所有配置解析完并且做了一些其它初始化工作后,就开始真正的监听套接口描述符创建以及特性设置,也就是调用诸如socket()、setsockopt()、bind()、listen()等这样的系统函数,相关逻辑实现在函数ngx_open_listening_sockets()和ngx_configure_listening_sockets()内,而这两个函数在nginx的初始化函数ngx_init_cycle()内靠结尾处被调用。从函数ngx_open_listening_sockets()内代码实现,可以看到就是遍历cycle->listening数组内每一个ls元素进行逐个创建,而ngx_configure_listening_sockets()内的描述符特性设置也是如此:
267:        Filename : ngx_connection.c
268:        ngx_int_t
269:        ngx_open_listening_sockets(ngx_cycle_t *cycle)
270:        {
271:        …
292:                ls = cycle->listening.elts;
293:                for (i = 0; i < cycle->listening.nelts; i++) {
294:                    …
312:                    s = ngx_socket(ls.sockaddr->sa_family, ls.type, 0);
313:        …
在这两个函数执行完之后,cycle->listening数组的每一个ls元素,其fd就不再是-1,而是一个可用的监听套接口描述符,并且该描述符根据用户设置赋予了不同的特性,比如收包缓存区大小,发包缓存区大小等。
关于监听套接口,nginx主进程的工作就做完了,接下来主进程通过fork()创建子进程,也就是工作进程,它们将全部继承这些已初始化好的监听套接口。在每个工作进程的事件初始化函数ngx_event_process_init()内,对每一个监听套接口创建对应的connection连接对象(为什么不直接用一个event事件对象呢?主要是考虑到可以传递更多信息到函数ngx_event_accept()内,并且这个连接对象虽然并没有对应的客户端,但可以与accept()创建的连接套接口统一起来,因为连接套接口对应的是connection连接对象),并利用该connection的read事件对象(因为在监听套接口上触发的肯定是读事件):
582:        Filename : ngx_event.c
583:        static ngx_int_t
584:        ngx_event_process_init(ngx_cycle_t *cycle)
585:        {
586:        …
745:            ls = cycle->listening.elts;
746:            for (i = 0; i < cycle->listening.nelts; i++) {
747:       
748:                c = ngx_get_connection(ls.fd, cycle->log);
749:                …
759:                rev = c->read;
760:                …
762:                rev->accept = 1;
763:                …
826:                rev->handler = ngx_event_accept;
read事件对象的回调处理函数为ngx_event_accept(),请记住它。一切都准备就绪,接下来就是对rev事件对象进行监控,即将监听套接口所对应的事件对象加入到nginx的事件处理模型里,那什么时候加入呢?如果没有启动accept_mutex,那么在函数ngx_event_process_init()末尾就会通过ngx_add_event()将它加入到事件监控机制内:
837:        Filename : ngx_event.c
838:                    if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
否则就需在核心执行函数ngx_process_events_and_timers()内进行竞争,抢占到accept_mutex锁后才会把它加入:
353:        Filename : ngx_event_accept.c
354:                    if (ngx_add_event(c->read, NGX_READ_EVENT,0)==NGX_ERROR){
在事件机制一章的负载均衡一节有对这两方面的详细描述,而在这里我们需特别注意的是,这些事件对象是以水平触发的方式加入到事件监控机制内的,这意味着一个进程一次没有处理完的客户端连接请求可以再次被捕获。
举个例子,我们知道在默认情况下,nginx一次只accept()一个请求(即multi_accept为off),如果某个监听套接口A上同时来了两个客户端请求连接,触发可读事件被工作进程捕获,工作进程accept()处理了一个请求后,重新阻塞在事件机制监控处(比如epoll_wait()),但事实上,监听套接口A上还有一个客户端请求连接没有被处理,如果监听套接口A不是以水平触发而是以边缘触发加入到事件监控机制,此时监听套接口A虽然可读却无法触发可读事件而让epoll_wait()返回,除非监听套接口A上又来了新的请求重新触发可读事件,但这无疑会导致连接请求得不到及时处理并逐渐累积,到最后该监听套接口A彻底失效。理解这个例子需要对epoll事件模型的水平触发LT与边缘触发ET两种模式特性有一定的了解,但这里我也不多详叙,不清楚的请翻阅man手册或Google。
另外,可以看到nginx主进程在创建完工作进程后并没有关闭这些监听套接口,但主进程却又并没有进行accept()客户端请求连接,那么是否会导致一些客户端请求失败呢?答案当然是否定的,虽然主进程也拥有那些监听套接口,并且它也的确能收到客户端的请求,但是主进程并没有监控这些监听套接口上的事件,没有去读取客户端的请求数据。既然主进程没有去读监听套接口上的数据,那么数据就阻塞在那里,等待任意一个工作进程捕获到对应的可读事件后,进而就可以去处理并响应客户端请求。至于主进程为什么要保留(不关闭)那些监听套接口,是因为再后续再创建新工作进程(比如某工作进程异常退出,主进程收到SIGCHLD信号)时,还要把这些监听套接口传承过去。

创建连接套接口
当有客户端发起请求连接,监控监听套接口的事件管理机制就会捕获到可读事件,工作进程便执行对应的回调函数ngx_event_accept(),从而开始连接套接口的创建工作。
函数ngx_event_accept()的整体逻辑都比较简单,但是有两个需要解析的处理。首先是每次处理调用accept()的次数,默认情况下也就是调用accept()一次,即工作进程每次捕获到监听套接口上的可读事件后,只接受一个服务请求,如果同时收到多个客户端请求,那么除第一个以外的请求需等到再一次触发事件才能被accept()接受。但是,如果用户配置有multi_accept on;,那么工作进程每次捕获到监听套接口上的可读事件后,将反复accept(),一次接受所有的客户端服务请求。这样看起来,似乎“一次接受所有的客户端服务请求”更高效,可为什么它却不是默认配置呢?
举个例子就懂了,假设两个工作进程A和B相互争用监听套接口并对其上的客户端服务请求进行处理,假定两者争用成功的概率都为50%,但是进程A的运气有点差,亦或者说运气有点好,反正不管怎么说,每次进程A对监听套接口争用成功时,总是同时有很多个客户端请求到达,而进程B却每次只有少数的几个请求,这样几个循环下来,进程A就非常繁忙了,工作进程之间的负载没有得到均衡,所以默认情况下,工作进程一次只accept()一个客户端服务请求。相关的逻辑代码如下所示(系统调用accept4()与accept()差别不大,请查man手册,下面代码以使用accept()为例):
17:        Filename : ngx_event_accept.c
18:        void
19:        ngx_event_accept(ngx_event_t *ev)
20:        {
21:        …
40:                ev->available = ecf->multi_accept;
41:        …
50:            do {
51:        …
61:                s = accept(lc->fd, (struct sockaddr *) sa, &socklen);
62:        …
64:                if (s == -1) {
65:        …
70:                        return;
71:        …
290:            } while (ev->available);
当配置文件中有multi_accept on;时,对应的解析值ecf->multi_accept为1,从而ev->available值为1,所以这个do{}while是一个死循环,直到accept()接受不到客户端请求时,即返回值s等于-1时,循环才得以退出。在未配置multi_accept或multi_accept为off的情况下,ev->available值为0,此时循环主体自然也就只执行一次。
函数accept()调用成功接受客户端请求后,就通过函数ngx_get_connection()申请对应的连接对象,做一些初始赋值等,简单明了而无需多说,但有一个需要解析的处理是deferred_accept,相关代码如下:
204:        Filename : ngx_event_accept.c
205:                if (ev->deferred_accept) {
206:                    rev->ready = 1;
207:        …
284:                ls->handler(c);
ev->deferred_accept值的最初影响设置是listen配置的附属项目里,前面曾讲过listen配置项非常的复杂,有大量的附属项目提供用户来指定这个监听套接口的相关属性,而deferred就是其中的一个,带有该附属项目的对应监听套接口描述符会被设置TCP_DEFER_ACCEPT特性,并且对应到这里的ev->deferred_accept值为1(前后是怎样的转换与逐步赋值略过不讲,翻下代码很容易理解)。TCP_DEFER_ACCEPT特性意味当工作进程accept()这个监听套接口上的客户端请求时,请求的数据内容已经到达了,所以这里第206行将rev->ready设置为1,表示数据准备就绪。最后执行的ls->handler回调也就是函数ngx_http_init_connection(),这是在很早之前赋值(还记得么?上一节提到过)上的。
181:        Filename : ngx_http_request.c
182:        void
183:        ngx_http_init_connection(ngx_connection_t *c)
184:        {
185:        …
206:            rev->handler = ngx_http_init_request;
207:        …
213:            if (rev->ready) {
214:                /* the deferred accept(), rtsig, aio, iocp */
215:       
216:                if (ngx_use_accept_mutex) {
217:                    ngx_post_event(rev, &ngx_posted_events);
218:                    return;
219:                }
220:       
221:                ngx_http_init_request(rev);
222:                return;
223:            }
224:       
225:            ngx_add_timer(rev, c->listening->post_accept_timeout);
226:       
227:            if (ngx_handle_read_event(rev, 0) != NGX_OK) {
228:        …
函数ngx_http_init_connection()很简单,但注意到rev->ready,如果它为0,则将事件对象rev加入到超时管理机制和事件监控机制,等超时或请求数据到达。如果rev->ready为1,也就是监听套接口描述符使用刚才讲到的TCP_DEFER_ACCEPT特性,accept()接受服务请求后,请求数据已经准备好了,当然是开始着手处理。第216行的if判断为真则意味着有加锁,所以先把该事件对象加到ngx_posted_events链表,返回解锁后再进行处理,否则在第221行就开始处理,这部分逻辑结合事件机制一章的描述应该容易理解,不过需注意在rev->ready为1的处理情况下,到此时为止,我们新建连接对象都还没有被加入到事件监控机制里,因为当前我们是知道有数据可读,如果运气好,需要的所有请求数据都已经全部到达了,读取数据处理请求然后响应即可,就没有必要把连接对象加到事件监控机制里,却什么作用都没起到又把它从事件监控机制里删除;只有当进行数据读取时,发现所需要的请求数据没有全部到达,此时才将连接对象加到事件监控机制里,等待进一步数据到达时以便获得事件通知,所以在后面的ngx_http_read_request_header()类似函数内能看到ngx_handle_read_event()这样的函数调用:
1139:        Filename : ngx_http_request.c
1140:        static ssize_t
1141:        ngx_http_read_request_header(ngx_http_request_t *r)
1142:        {
1143:        …
1164:            if (n == NGX_AGAIN) {
1165:        …
1167:                    ngx_add_timer(rev, cscf->client_header_timeout);
1168:        …
1170:                if (ngx_handle_read_event(rev, 0) != NGX_OK) {
如上所示,在读到NGX_AGAIN时,也就是需要的数据没有全部到达,于是将事件对象rev加入到超时管理机制和事件监控机制,以等待后续数据可读事件或超时。

HTTP请求处理
函数ngx_http_init_request(),正式开始对一个客户端服务请求进行处理与响应工作。该函数的主要工作仍然只是做处理准备:建立http连接对象ngx_http_connection_t、http请求对象ngx_http_request_t、找到对应的server配置default_server、大量的初始化赋值操作,最后执行回调函数ngx_http_process_request_line(),进入到http请求头的处理中:
236:        Filename : ngx_http_request.c
237:        static void
238:        ngx_http_init_request(ngx_event_t *rev)
239:        {
240:        …
276:                hc = ngx_pcalloc(c->pool, sizeof(ngx_http_connection_t));
277:        …
295:                r = ngx_pcalloc(c->pool, sizeof(ngx_http_request_t));
296:        …
380:                    addr = port->addrs;
381:                    addr_conf = &addr[0].conf;
382:        …
388:            /* the default server configuration for the address:port */
389:            cscf = addr_conf->default_server;
390:        …
395:            rev->handler = ngx_http_process_request_line;
396:        …
519:            rev->handler(rev);
520:        }
查找该请求该被端口上的哪个ip地址处理是通过对比目的ip地址来进行的(前面讲过,如果某端口上设置有任意ip监听,比如*:80,那么即便还有其他指定ip的监听,比如192.168.1.1:80,也只会创建一个监听套接口,所以对于该套接口上接收到的连接请求首先要进行目的ip匹配),这部分代码很容易理解,就是一个for循环遍历查找,要注意的是由于ipv4的地址只有32位可以直接比较,但ipv6的地址有128位,所以需采用memcmp()比较,上面没有显示这部分代码,给出的第380、381行代码是监听套接口上只有一个目的ip的情况,此时直接使用它,并且第389行取用该地址上的默认server配置。如果客户端请求对应的server不是这个默认的会怎么样?不用当心这种情况,因为在后面还会有处理,比如ngx_http_find_virtual_server()函数,具体请参见下一章内容。
在继续下面的内容讲解前,有必要先介绍一下HTTP协议,当然,关于HTTP协议方面的内容,如果展开来说一时半会说不完,也有专门的书籍,比如《O'Reilly - HTTP Pocket Reference》、《O'Reilly - HTTP The Definitive Guide》、《Sams - HTTP Developers Handbook》等,所以这里仅以RFC 2616(对应HTTP 1.1:http://www.ietf.org/rfc/rfc2616.txt)为依据简单介绍一下HTTP请求响应数据的格式。
根据RFC 2616内容可知,HTTP请求消息(也包括响应消息。消息,即message,可简单认为就是上面提到的请求响应数据的学术名称,我说数据是笼统说法,请勿拘泥这些名词概念,毕竟我不是在写学术论文,)是利用RFC 822(http://www.ietf.org/rfc/rfc822.txt)定义的常用消息格式来传输实体(消息的负载,即真正有价值的数据)。这种常用消息格式就是由开始行(start-line),零个或多个头域(经常被称作“头”)、一个指示头域结束的空行(一个仅包含CRLF的“空”行)以及一个可有可无的消息主体(message-body)。当然,RFC 2616文档里对HTTP请求响应消息格式描叙得更具体一点,其中请求消息格式的BNF(巴科斯诺尔范式)表示如下:
Request         = Request-Line              ; Section 5.1
                *(( general-header          ; Section 4.5
                 | request-header           ; Section 5.3
                 | entity-header ) CRLF)    ; Section 7.1
                CRLF
                [ message-body ]            ; Section 4.3
Request-Line    = Method SP Request-URI SP HTTP-Version CRLF
Method          = "OPTIONS"                 ; Section 9.2
                  | "GET"                   ; Section 9.3
                  | "HEAD"                  ; Section 9.4
                  | "POST"                  ; Section 9.5
                  | "PUT"                   ; Section 9.6
                  | "DELETE"                ; Section 9.7
                  | "TRACE"                 ; Section 9.8
                  | "CONNECT"               ; Section 9.9
                  | extension-method
extension-method    = token
Request-URI     = "*" | absoluteURI | abs_path | authority
可以看到,工作进程收到的客户端请求头部数据以Request-Line开始(GET / HTTP/1.0\r\n),接着是不定数的请求头部(User-Agent: Wget/1.12 (linux-gnu)\r\nAccept: */*\r\nHost: www.web_test2.com\r\nConnection: Keep-Alive\r\n),最后以一个空行结束(\r\n)。
而函数ngx_http_process_request_line()处理的数据就是客户端发送过来的http请求头中的Request-Line,这个过程可分为三步:读取Request-Line数据、解析Request-Line、存储解析结果并设置相关值。当然,这个过程实际执行时可能重复多次(比如Request-Line数据分多次到达监听套接口),所以函数内的实现是一个for ( ;; )循环。下面逐一简单来看下各个步骤:
第一步,读取Request-Line数据。通过函数ngx_http_read_request_header()将数据读到缓存区r->header_in内。比如执行wget www.web_test2.com请求时,调试nginx对应工作进程打印的数据如下:
(gdb) p r->header_in->pos
$8 = (u_char *) 0x98bdef8 "GET / HTTP/1.0\r\nUser-Agent: Wget/1.12 (linux-gnu)\r\nAccept: */*\r\nHost: www.web_test2.com\r\nConnection: Keep-Alive\r\n\r\n"
一次就把整个请求头部数据读到了,当然也就包括完整的Request-Line数据(即:GET / HTTP/1.0\r\n)。另外说一句,可以看到命令wget默认是以HTTP 1.0协议发送请求,不过不影响(下面仍以它的数据为例),nginx也支持HTTP 1.0协议,也可以用curl命令curl www.web_test2.com进行请求:
(gdb) p r->header_in->pos
$9 = (u_char *) 0x98bdef8 "GET / HTTP/1.1\r\nUser-Agent: curl/7.19.7 (i686-pc-linux-gnu) libcurl/7.19.7 NSS/3.12.7.0 zlib/1.2.3 libidn/1.18 libssh2/1.2.2\r\nHost: www.web_test2.com\r\nAccept: */*\r\n\r\n"
刚才提到,由于客户端请求头部数据可能分多次到达,所以缓存区r->header_in内可能还有一些上一次没解析完的头部数据,所以会存在数据的移动等操作,不过也都比较简单,仅提一下而略过不讲。
第二步,解析Request-Line。对读取到的Request-Line数据进行解析的工作实现在函数ngx_http_parse_request_line()内。由于Request-Line数据有严格的BNF对应,所以其解析过程虽然繁琐,但并无不好理解的地方。
第三步,存储解析结果并设置相关值。在Request-Line的解析过程中会有一些赋值操作,但更多的是在成功解析后,ngx_http_request_t对象r内的相关字段值都将被设置,比如uri(/)、method_name(GET)、http_protocol(HTTP/1.0)等。
Request-Line解析成功,即函数ngx_http_parse_request_line()返回NGX_OK,意味着这初步算是一个合法的http请求,接下来就开始解析其它请求头(general-header、request-header、entity-header):
706:        Filename : ngx_http_request.c
707:        static void
708:        ngx_http_process_request_line(ngx_event_t *rev)
709:        {
710:        …
732:            for ( ;; ) {
733:        …
735:                    n = ngx_http_read_request_header(r);
736:        …
742:                rc = ngx_http_parse_request_line(r, r->header_in);
743:       
744:                if (rc == NGX_OK) {
745:        …
896:                    if (ngx_list_init(&r->headers_in.headers, r->pool, 20,
897:                                      sizeof(ngx_table_elt_t))
898:        …
915:                    rev->handler = ngx_http_process_request_headers;
916:                    ngx_http_process_request_headers(rev);
函数ngx_http_process_request_headers()对每一个请求头的处理步骤与函数ngx_http_process_request_line()处理Request-Line的情况类似,也是分为三步:读取数据(对应函数ngx_http_read_request_header(),如果数据已经从监听套接口描述符读到缓存区了,那么无需再读)、解析数据(对应函数ngx_http_parse_header_line())、存储解析结果。
在第二步骤中,函数ngx_http_parse_header_line()解析的每一个请求头都会放到r->headers_in.headers内,看看gdb断点后捕获到的实例数据:
(gdb) p r->headers_in.headers.part
$34 = {elts = 0x98be5d4, nelts = 4, next = 0x0}
(gdb) p *(ngx_table_elt_t *)r->headers_in.headers.part.elts
$35 = {hash = 486342275, key = {len = 10, data = 0x98bdf08 "User-Agent"}, value = {len = 21, data = 0x98bdf14 "Wget/1.12 (linux-gnu)"}, ...}
(gdb) p *(ngx_table_elt_t *)(r->headers_in.headers.part.elts + sizeof(ngx_table_elt_t) * 1)
$36 = {hash = 2871506184, key = {len = 6, data = 0x98bdf2b "Accept"}, value = {len = 3,        data = 0x98bdf33 "*/*"}, ...}
(gdb) p *(ngx_table_elt_t *)(r->headers_in.headers.part.elts + sizeof(ngx_table_elt_t) * 2)
$37 = {hash = 3208616, key = {len = 4, data = 0x98bdf38 "Host"}, value = {len = 17, data = 0x98bdf3e "www.web_test2.com"}, ...}
(gdb) p *(ngx_table_elt_t *)(r->headers_in.headers.part.elts + sizeof(ngx_table_elt_t) * 3)
$38 = {hash = 3519315678, key = {len = 10, data = 0x98bdf51 "Connection"}, value = {len = 10, data = 0x98bdf5d "Keep-Alive"}, ...}
如果请求头有对应的回调处理函数还会被做进一步处理,所有可以被nginx识别并处理的请求头定义在数组ngx_http_headers_in内,比如:
80:        Filename : ngx_http_request.c
81:        ngx_http_header_t  ngx_http_headers_in[] = {
82:            { ngx_string("Host"), offsetof(ngx_http_headers_in_t, host),
83:                         ngx_http_process_host },
84:       
85:            { ngx_string("Connection"), offsetof(ngx_http_headers_in_t, connection),
86:                         ngx_http_process_connection },
87:        …
而在函数ngx_http_process_request_headers()内的具体实现如下:
955:        Filename : ngx_http_request.c
956:        static void
957:        ngx_http_process_request_headers(ngx_event_t *rev)
958:        {
959:        …
1038:                rc = ngx_http_parse_header_line(r, r->header_in,
1039:                                                cscf->underscores_in_headers);
1040:       
1041:                if (rc == NGX_OK) {
1042:        ..
1056:                    h = ngx_list_push(&r->headers_in.headers);
1057:        …
1085:                    hh = ngx_hash_find(&cmcf->headers_in_hash, h->hash,
1086:                                       h->lowcase_key, h->key.len);
1087:        …
1088:                    if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {
1089:        …
第1041行为真则表示一个请求头被成功解析,在1056行先把它加入到r->headers_in.headers内,然后在cmcf->headers_in_hash(该变量对应ngx_http_headers_in变量)内查找该请求头能否被nginx处理,比如"Host"请求头就能够被nginx处理,从而调用其对应的处理函数ngx_http_process_host()。
当函数ngx_http_parse_header_line()返回NGX_HTTP_PARSE_HEADER_DONE时,表示所有的请求头都已经处理完成(最后一个被处理的请求头为entity-header),客户端的具体请求已经基本被理解(可能还有请求体,比如POST时),nginx开始进入到内部处理,即开始执行各种模块Handler,不过在此之前,通过调用ngx_http_process_request_header()函数先做了一个简单的检查:
1099:        Filename : ngx_http_request.c
1100:                if (rc == NGX_HTTP_PARSE_HEADER_DONE) {
1101:        …
1110:                    rc = ngx_http_process_request_header(r);
1111:       
1112:                    if (rc != NGX_OK) {
1113:                        return;
1114:        …
1116:                    ngx_http_process_request(r);
函数ngx_http_process_request_header()的检查比较简单,比如如果客户端使用HTTP 1.1协议发送请求却没有带上"Host"请求头则直接返回错误(HTTP 1.1协议明确要求必须有"Host"请求头);客户端发送TRACE请求则也返回错误(TRACE请求用于调试跟踪,nginx不支持);等。
调用函数ngx_http_process_request()也就是开始执行各种模块Handler,也就是那个前面章节曾提到过的“状态机”:
ngx_http_process_request() -> ngx_http_handler() -> ngx_http_core_run_phases()
874:        Filename : ngx_http_core_module.c
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:            }
对一个客户端请求的处理,终于衔接到nginx的Handler模块来了,各个Handler模块的处理在前面章节已经描述过,所以这里不再多讲。对于一个访问静态页面的GET类型请求,最终会被ngx_http_static_module模块的ngx_http_static_handler()函数捕获,该函数组织待响应的数据,然后调用ngx_http_output_filter()经过nginx过滤链后将数据发送到客户端,此时一个请求的处理与响应也就完成,所以当回到ngx_http_process_request()函数的最末,调用到函数ngx_http_run_posted_requests()内时,因为c->destroyed为真而直接退出。
请求处理响应完后,调用函数ngx_http_finalize_request()进行清理工作,由于nginx对内存的使用采用内存池的方式,所以回收起来非常的简单,也不会出现内存泄露,还有一些其它清理工作,比如从事件超时红黑树里移除等也都比较简单,无需多说。

HTTP数据响应
http响应消息也分为head头部和body主体,和请求消息一致,也是头部信息先发送,然后才是主体信息。本节仍以简单的GET请求静态页面为例,来看看nginx如何对客户端做出数据响应。
前面提到,简单的GET请求静态页面会最终被ngx_http_static_module模块实际处理,执行的函数为ngx_http_static_handler(),该函数首先要做的当然是找到请求静态页面所对应的磁盘文件,这通过组合当前location配置的根目录与GET请求里的绝对URI即可得到该磁盘文件的绝对路径。
接着通过绝对路径打开该磁盘文件,并且通过文件属性来设置相关响应头,比如通过文件大小来设置Content-Length响应头(这里还只是设置对应的字段值,并非创建实际的响应头,下同),告诉客户端接收数据的长度;通过文件修改时间来设置Last-Modified响应头,那么客户端下次再请求该静态文件时可带上该时间戳,那时nginx就有可能直接返回304状态码,让客户端直接使用本地缓存,从而提高性能;等等。发送响应体需要一些内存资源,这会在发送响应头以前分配好,因为如果内存申请失败可提前异常返回,避免可能出现响应头已经发送出去后却发现发送响应体所需要的内存资源却没法成功申请的情况。当然,发送响应头还需要经过nginx的过滤链,这是通过函数:
ngx_http_send_header() -> ngx_http_top_header_filter()
逐步顺链调用下去,过滤链上的回调函数可能会对响应头数据进行检测、截获、新增、修改和删除等操作,不管怎样,一般情况下,执行流程会走到过滤链最末端的两个函数内:
ngx_http_header_filter() -> ngx_http_write_filter()
其中函数ngx_http_header_filter()完成响应头字符串数据的组织工作。该函数申请一个buf缓存块,然后根据最初设置以及经过过滤链的修改后的相关响应头字段值,组织响应头数据以字符串的形式存储在该缓存块内,下面是在该函数接近末尾的地方,用gdb捕获到的数据:
(gdb) p b->pos
$39 = (u_char *) 0x98be920 "HTTP/1.1 200 OK\r\nServer: nginx/1.2.0\r\nDate: Sun, 27 May 2012 13:58:31 GMT\r\nContent-Type: text/html\r\nContent-Length: 219\r\nLast-Modified: Fri, 25 May 2012 15:20:11 GMT\r\nConnection:keep-alive\r\nAccept-Ranges: bytes\r\n\r\n"
        该缓存块被接入到发送链变量out(注意这是一个局部变量)内,之后进入到函数ngx_http_write_filter()进行“写出”操作,打上引号是因为此处只有在满足某些条件的情况下才会执行实际的数据写出:
46:        Filename : ngx_http_write_filter_module.c
47:        ngx_int_t
48:        ngx_http_write_filter(ngx_http_request_t *r, ngx_chain_t *in)
49:        {
50:        …
172:            /*
173:             * avoid the output if there are no last buf, no flush point,
174:             * there are the incoming bufs and the size of all bufs
175:             * is smaller than "postpone_output" directive
176:             */
177:       
178:            if (!last && !flush && in && size < (off_t) clcf->postpone_output) {
179:                return NGX_OK;
180:            }
可以看到如果没有带最后一个缓存块(last)并且没有要求强制写出(flush)并且当前有新加缓存块(in为真)并且当前缓存块总数据大小小于设定值(clcf->postpone_output),此时可直接返回NGX_OK,这意味着会有数据马上跟来(所以该if语句为什么会有对in是否为真的判断就是因为这个原因,如果当前都没有新加入数据,那么也不要期待下一步会马上有数据加入,因此基于这种思路,从时延上考虑,就需要立即写出,从而这整个if判断为假),所以此次可以不写。之所以这么做,当然还是从性能上考虑,在其它章节我们可以看到不管是用哪种读/写方式,总还是要进行用户空间与内核空间的切换,性能损耗比较大,所以读/写操作能省一次就一次。
        在我们的示例里,或者说是客户端访问服务器静态页面的这种情况下,那么此时一般就是从低179行退出返回了,但是在该函数前面的逻辑里,我们的待发送缓存块(即包含响应头数据的字符串数据)被连接到r->out链内了,这样做是必须的,毕竟传入进来的out发送链是个局部变量。此时的情况如下:

        函数依次返回后到函数ngx_http_static_handler()内继续执行,看一下相关的完整代码:
47:        Filename : ngx_http_static_module.c
48:        static ngx_int_t
49:        ngx_http_static_handler(ngx_http_request_t *r)
50:        {
51:        …
235:            b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
236:        …
240:            b->file = ngx_pcalloc(r->pool, sizeof(ngx_file_t));
241:        …
245:            rc = ngx_http_send_header(r);
246:        …
255:            b->last_buf = (r == r->main) ? 1: 0;
256:        …
258:            b->file->fd = of.fd;
259:            b->file->name = path;
260:        …
266:            return ngx_http_output_filter(r, &out);
267:        }
代码第235、240、245行在前面已经描述过了,第255行的last_buf会被置1,即由于当前请求就是主请求,第258、259行在后面实现将静态文件写出到客户端时会要用到,第266行开始进入到body过滤链,最后也进入到函数ngx_http_write_filter()内,同样,该函数的前面逻辑把这个新缓存块也加入到r->out链内,但是在判断是否要实际写出的if判断时,由于last标记为真,所以此时的确需要做数据写出操作。此时的r->out链情况如下所示:

        在进行实际的数据写出操作时,会有一些其它与本节无关的细节,比如限速、一次没完全写完等需设置定时器再写,撇开这些而关注我们的重点函数:
241:        Filename : ngx_http_write_filter_module.c
242:            chain = c->send_chain(c, r->out, limit);
回调指针send_chain根据系统环境的不同而指向不同的函数,关于这点在其它章节会讲到,在我这里指向的是ngx_linux_sendfile_chain()函数,该函数遍历r->out链上的每一个缓存块,根据缓存块里的数据类型调用不同的系统接口函数将数据写出到客户端。比如这里,对于第一个缓存块,其内数据是存放在内存中的字符串数据(响应头),所以调用系统接口函数writev()将其写出;对于第二个缓存块,其相关联数据是磁盘上文件系统内某个文件(该文件已被打开,对应文件描述符存放在buf->file->fd内)的内容,对于这些数据的写出采用的是系统调用sendfile()。相关代码为:
36:        Filename : ngx_linux_sendfile_chain.c
37:        ngx_chain_t *
38:        ngx_linux_sendfile_chain(ngx_connection_t *c, ngx_chain_t *in, off_t limit)
39:        {
40:        …
78:            for ( ;; ) {
79:        …
92:                for (cl = in; cl && send < limit; cl = cl->next) {
93:        …
248:                if (file) {
249:        …
264:                    rc = sendfile(c->fd, file->file->fd, &offset, file_size);
265:        …
293:                } else {
294:                    rc = writev(c->fd, header.elts, header.nelts);
客户端需要的数据都发送出去了,那么剩下的工作也就是进行连接关闭和一些连接相关资源的清理,当然,如果需要与客户端进行keepalive,那么会执行函数ngx_http_set_keepalive()保留一些可重用的资源,这样在客户端新的请求到达时,处理能更快速。不过,对于一个客户端请求的处理与响应,到此就已经算是完满结束了。


论坛徽章:
0
发表于 2012-08-16 07:36 |显示全部楼层
从word文档里拷贝贴出来,以便被搜索引擎检索,具体还是建议看word文档,毕竟比较工整并且如果发现有问题会被我修正,在:http://lenky.info/ebook/

论坛徽章:
0
发表于 2012-08-16 13:48 |显示全部楼层
嗯。下载了不顶一下对不起你的劳动成果

论坛徽章:
0
发表于 2012-08-19 22:45 |显示全部楼层
楼主厉害。

怎么耐着性子看这么多代码的啊?


我怎么就耐不下心来看代码。唉。

论坛徽章:
0
发表于 2012-08-21 15:33 |显示全部楼层
支持楼主,加油哈!~~
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP