首页 > nginx, 源码分析 > nginx变量机制

nginx变量机制

2012年8月8日 发表评论 阅读评论 9,323 次浏览

初识变量
前面曾讲过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#Variables、http://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.len = e->sp->len;
1685: …
1688: r->variables.data = e->sp->data;
变量code->index表示nginx变量$file在cmcf->variables数组内的下标,对应每个请求的变量值存储空间就为r->variables[/code],这里从栈中取出数据并进行变量实际赋值。
基本过程就是,利用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工作不正常的情况。

更多内容请关注:http://lenky.info/ebook/

转载请保留地址:http://www.lenky.info/archives/2012/08/1849http://lenky.info/?p=1849


备注:如无特殊说明,文章内容均出自Lenky个人的真实理解而并非存心妄自揣测来故意愚人耳目。由于个人水平有限,虽力求内容正确无误,但仍然难免出错,请勿见怪,如果可以则请留言告之,并欢迎来讨论。另外值得说明的是,Lenky的部分文章以及部分内容参考借鉴了网络上各位网友的热心分享,特别是一些带有完全参考的文章,其后附带的链接内容也许更直接、更丰富,而我只是做了一下归纳&转述,在此也一并表示感谢。关于本站的所有技术文章,欢迎转载,但请遵从CC创作共享协议,而一些私人性质较强的心情随笔,建议不要转载。

法律:根据最新颁布的《信息网络传播权保护条例》,如果您认为本文章的任何内容侵犯了您的权利,请以Email或书面等方式告知,本站将及时删除相关内容或链接。