NGINX脚本语言原理及源码分析(三)
39 次浏览
发表于 2021-04-07 13:37

概述

上两篇文章,我们分别介绍了NGINX变量的基本特性实现原理。本篇,我们继续通过分析NGINX中复杂变量是如果通过NGINX脚本语言的原理实现求值的。

基本原理

语言编译

我们平常使用的计算机语言一般分为两类,一种是编译型语言比如C语言,一种是脚本语言比如lua

编译型语言都要经历"编译"这个阶段,而脚本语言一般会简化掉这个步骤,直接解释执行。

下图是一个程序语言从编写到运行的简要流程:

所述流程中,词法分析是把源代码分析成一个个独立的单词。然后语法分析是对词法分析中生成的单词流进行进一步的检查判断是否符合指令语法定义。比如对于C语言来讲,乘法指令的操作数只能是数字等,语法分析要对指令的合法性进行检查

对于编译型语言,编译器在编译源码时,会把源码中的语句翻译成机器指令。比如用C语言写的一条if语句, if (a == 1) { } , 编译器会把这条语句翻译成若干机器指令。每一条指令都有对应的指令和操作数。

NGINX的脚本语言执行过程也大体类似。对于每一条配置指令,逐个解析指令中的单词,然后按照指令的定义逐步分析每一个单词,判断指令格式是否正确。并且在这一过程中对于某一条指令生成指令集,这里所谓的指令集对于NGINX来说就是C语言函数和对应的参数组成的结构体。这些结构体在源码层面有一个特征就是名称都是以code_t结束,比如return指令对应的指令结构体是:

typedef struct {

ngx_http_script_code_pt     code;

uintptr_t                   status;

ngx_http_complex_value_t    text;

} ngx_http_script_return_code_t;

其中结构体的第一项code操作码,实际的实现是回调函数,用来执行具体的动作。后面两项statustext相当于操作码code的操作数。NGINX有一个代码实现上的规则,对于所有的指令结构,其结构第一项一定是一个结构为ngx_http_script_code_pt的回调函数。

简单说来NGINX的脚本语言执行的原理,就是把配置脚本中的配置指令编译成各个指令结构然后依次执行的过程。

执行单元

在计算机语言中,把源码编译完成以后,计算机需要一个执行单元来执行编译后的指令,这个执行单元就是CPU。对于计算过程中产生的临时数据,有对应的寄存器和栈内存区域来存储。

与此对应,NGINX定义了一个ngx_http_script_engine_t结构来模拟计算机的CPU。每当要执行一段指令序列时,都需要创建一个脚本引擎结构用来执行指令。

typedef struct {

u_char                     *ip;

u_char                     *pos;

ngx_http_variable_value_t  *sp;

ngx_str_t                   buf;

ngx_str_t                   line;

/* the start of the rewritten arguments */

u_char                     *args;

unsigned                    flushed:1;

unsigned    skip:1;

unsigned                    quote:1;

unsigned                    is_args:1;

unsigned                    log:1;

ngx_int_t                   status;

ngx_http_request_t         *request;

} ngx_http_script_engine_t;

结构中的ip变量实际存放的是每个指令结构体的起始地址,因为其实地址的第一个元素是对应的指令的执行回调函数,所以,通过ip可以方便地获取解析函数。

结构中的sp变量实际指向模拟的栈地址。NGINX中的栈只存ngx_http_variable_value_t这一种数据类型。NGINX中,模拟的栈的大小是固定的。每一个栈的大小都是10,表示最多可以存放10个类型为ngx_http_variable_value_t的临时结果。这个栈大小是源码层面限定而且是不可配置的,对于实际的应用来讲,栈大小为10已经够用了。

指令执行流程

把配置编译成指令后的执行流程是:

1.     然后在执行这些指令集之前,需要先创建一个脚本引擎结构体:

e = ngx_pcalloc(r->pool,  sizeof(ngx_http_script_engine_t));

2.     然后,通过下面语句创建栈空间的分配:

e->sp = ngx_pcalloc(r->pool, rlcf->stack_size*sizeof(ngx_http_variable_value_t));

其中e->sp用来存放栈,rlcf->stack_size就是我们之前说的栈大小(可存变量的个数)

3.     把要执行的指令序列的首地址赋值给指针变量:

e->ip = rlcf->codes->elts;

其中rlcf->codes->elts中存放的就是这次要执行的整个指令序列。

4.     执行指令:

while (*(uintptr_t *) e->ip) {

code = *(ngx_http_script_code_pt *) e->ip;

code(e);

}

其中code就是一个ngx_http_script_code_pt类型的函数指针,另外根据NGINX的潜规 则,每个指令结构体的第一个字段一定是ngx_http_script_code_pt类型,所以这里拿到e->ip后可以强制转换成类型为ngx_http_script_code_pt的回调函数,然后再由此回调函数来完成对应指令的动作,并更改指令指针值(e->ip),直到e->ip指向为空。

指令执行阶段

NGINX中有两种指令形式,一种是NGINX的复杂变量,NGINX复杂变量的定义如下。

“A complex value, despite its name, provides an easy way to evaluate expressions which can contain text, variables, and their combination.”

意思是说它是一个纯文本、变量、或两者的组合的表达式。并且NGINX内部提供了一种计算该表达式的简易方式。

比如proxy_pass指令,后面可以跟一个多个变量和常量字符串组成的URL

proxy_pass ${host}/test/${path}, 在执行proxy_pass指令的解析函数时,会把复杂变量${host}/test/${path}的求值过程编译成一些指令集合,这些指令组合起来用来计算${host}/test/${path}的具体数值。这些指令是在proxy_pass指令对应的阶段来执行的。

另外一种是显式地通过在配置脚本中配置的指令,比如NGINX rewrite模块或者geo模块定义的指令。NGINX在启动阶段,会把这些指令编译成一系列指令集,并且存放到每一个location中的ngx_http_rewrite_loc_conf_t结构体中。

typedef struct {

ngx_array_t  *codes;//保存着所属location下的所有编译后脚本

ngx_uint_t    stack_size;//变量值栈sp的大小

……

} ngx_http_rewrite_loc_conf_t;

这个结构其实就是rewrite模块在location级别下面的配置结构体。如果匹配的location下面没有脚本语言配置,则codes成员是空的,否则codes成员就会承载着解析后的脚本指令的结构体。这样每当一个HTTP请求到来时就可以在这个请求本身的上下文执行对应的指令。

这些指令对应的执行阶段是在HTTPrewrite阶段也就是函数ngx_http_rewrite_handler中执行的。当函数执行时,首先创建一个ngx_http_script_engine_t结构,然后开始执行ngx_http_rewrite_loc_conf_t结构中codes数组中的指令。

源码分析

实现方案

还是以proxy_pass https://${host}:${port}/${path} 作为例子,来分析复杂变量https://${host}:${port}/${path} 是如何被解析成指令集以及如何被求值的。

NGINX有两种用于变量求值的实现。

第一种方案是:

使用结构体ngx_http_script_compile_t 来存放原始和编译后的值。而实际编译过程则由

ngx_http_script_compile方法负责,最后的结果值由函数ngx_http_script_run 计算得出并且存放到ngx_str_t类型的变量中。

第二种方案是:

使用结构体ngx_http_compile_complex_value_t来存放原始值,然后使用结构体ngx_http_complex_value_t来存放编译后的值。而实际编译过程则由

ngx_http_compile_complex_value方法负责,最后的结果值由函数ngx_http_complex_value计算得出并且存放到ngx_str_t类型的变量中。

第二种方案实现了对第一种方案的包装,使用起来更方便一些。在我们这个proxy_pass变量求值的例子中是使用了第一种方案,这也是整个NGINX变量求值的核心。

基本原理

在启动阶段,NGINX在解析复杂变量值时,会用到如下的几个数据结构用来表示复杂变量中变量名,常量字符串以及变量值等不同元素。每一种元素都对应一个指令结构。

1.     编译变量名的结构体

typedef struct {

         ngx_http_script_code_ptcode; //code指向获取变量名的脚本指令方法

        uintptr_tindex;     //r-variables数组中的索引号

      } ngx_http_script_var_code_t;

2.     变量常量字符串的结构体

        typedef struct {

        ngx_http_script_code_ptcode; //code指向获取变量字符串的方法

        uintptr_tlen;     //常量字符串长度

        } ngx_http_script_var_code_t;

3.     编译变量值的结构体

typedef struct {

        ngx_http_script_code_ptcode;   //code指向获取变量值的脚本指令

         /*外部变量值如果为整数,则转为整数后赋值给value,否则value0*/

        uintptr_tvalue;

        uintptr_ttext_len;  //外部变量值(set的第二个参数)的长度

        uintptr_ttext_data; //外部变量值的起始地址

} ngx_http_script_value_code_t;

4.     编译复杂变量值的结构体

typedef struct {

        ngx_http_script_code_pt code;//code指向编译复杂变量值的脚本指令方法

        ngx_array_t*lengths; //lengths存放的是复杂变量值中内嵌变量的值长度

} ngx_http_script_complex_value_code_t;

NGINX会为复杂变量的编译准备三个容器,分别是flusheslengthsvalues其中:

容器flushes:用来存放复杂值中每个变量的索引值,比如我们的例子:

https://${host}:${port}/${path}如果变量${host}${port}${port}在容器(cmcf->variables)中的索引值是5,67,那么容器flushes中就会顺序的包含5,67这三个数字。

容器lengths:该数组存放的是编译好的指令集,该指令集用来计算混合复杂值的字符长度。比如我们的例子https://${host}:${port}/${path},在不同的请求根据请求中${host}, ${port}以及 ${path}变量的长度加上https://:以及/部分的长度求出整个变量的长度。

容器values:该数组存放的也是指令集,主要用来计算复杂值的最终值,同样拿例子中的https://${host}:${port}/${path}为例子,需要在不同的请求中,根据各个变量的具体数值合并成一个字符串。

NGINX复杂变量编译工作主要就是填充这三个容器,这主要是靠ngx_http_script_compile函数和结构体ngx_http_script_compile_t结构体来完成的。

代码解析

指令proxy_pass对应的解析函数是ngx_http_proxy_pass

1.     首先通过函数ngx_http_script_variables_count判断指令后面url参数包含的变量的个数。原理就是通过字符串中$符号的个数进行判断。

2.     如果参数中包含变量,函数进入复杂变量求值流程。

3.     首先初始化ngx_http_script_compile_t结构对应的sc变量,保存指令proxy_pass配置的url原始值,并且把的lengths数组和values数组关联到ngx_http_core_loc_conf_t结构中的lengthsvalues数组。其中lengths数组用来存放获取url各个元素的长度的指令结构。数组values用来存放获取url各个元素数值的指令结构。

4.     调用ngx_http_script_compile函数进行复杂变量编译。编译的主要目的是生成上述sc结构中的lengthsvalues数组中的元素。函数ngx_http_script_compile主要进行复杂变量的编译工作,流程为:

a.     通过函数ngx_http_script_init_arrays根据变量的个数初始化sc结构体中的flushes,lengthsvalues数组。

b.    扫描url中的每个字符把url分成多个部分。对于某一部分,如果是形式为“$字符的变量,则通过ngx_http_script_add_var_code添加对应变量的指令结构。如果是“$数字形式的arg变量,则通过ngx_http_script_add_args_code添加对应的arg指令结构。如果是常量字符串,则通过ngx_http_script_add_copy_code添加对应的常量字符串指令结构。

对我们的例子https://${host}:${port}/${path}会依次生成“https://”常量字符串,“${host}”变量,“:“常量字符串,${port}变量,”/”常量字符串以及${path}变量对应的指令结构。

函数ngx_http_script_add_var_codengx_http_script_add_args_code以及ngx_http_script_add_copy_code会操作flushes,lengthsvalues数组,并且在其中添加对应的元素。

c.     函数执行完毕以后,对于我们的例子https://${host}:${port}/${path}sc结构中的flusheslengthsvalues数组的内容如下:

变量解析完成以后,其求值是在模块proxy passhandler函数ngx_http_proxy_handler中完成的。流程为:

1.     判断location结构plcfproxy_lengths数组是否为空。如果不空,说明对应的url是复杂变量。则通过函数ngx_http_proxy_eval来对复杂变量求值。

2.     函数ngx_http_proxy_eval调用函数ngx_http_script_run进行计算。函数ngx_http_script_run的流程为:

a.     初始化请求结构中的变量。

b.    逐个执行数组lengths中的指令,求出所有变量的长度并得到最后变量值的总长度。

c.     根据计算的变量总长度生成ngx_str_t 类型的变量value,用来存储最终的变量结果。

d.    逐个执行数组values中的指令,求出每一部分的数值并且合并到上面的变量value中。

至此,复杂变量https://${host}:${port}/${path}的最终结果就完成了计算。

结语

通过三篇文章分析了NGINX的变量以及复杂变量求值实现原理。这些变量和脚本语言的支持方便和丰富了NGINX的配置和使用。下一篇,我们讲继续分析NGINX中的rewrite模块定义的指令比如if/set/break/last/rewrite等指令的实现原理。

发表评论
发表者

皮皮鲁

暂无个人介绍

  • 26

    文章

  • 19

    关注

  • 19

    粉丝

活动推荐
版权所有©F5 Networks,Inc.保留所有权利。京ICP备16013763号-5