《p4规范》解析器部分详解
p4解析器是根据有限状态机的思想来设计的。
解析器中解析的过程可以被一个解析图(parser graph)所表示,解析图中所表示的某一个状态(或者说,在P4语言中的某一个解析函数)看做是一个状态节点,每一个状态转换等同于跨越状态节点之间的边界。
(图2)
图2展示了一个非常简单的例子。请注意,在图二中的每一个状态节点都明确识别了一个首部实例。
虽然P4支持这种图表的形式,但是并不是非它不可。
在解析图中的某一个状态节点,也许只是一个单纯的、没有绑定任何特定首部实例的选择节点,也有可能是一个处理了多个首部实例的操作节点。
下面是一个P4代码示例,是mTag解析器内容中的部分解析函数(注:每一个状态函数等同于状态转移中的状态节点)。开始解析所需要调用的解析函数直接命名为ethernet。
parser ethernet {
extract(ethernet); //从以太网首部字段开始
return select(latest.etherType) {
0x8100: vlan;
0x800: ipv4;
default: ingress;
}
}
parser vlan {
extract(vlan);
return select(latest.etherType) {
0xaaaa: mtag;
0x800: ipv4;
default: ingress;
}
}
parser mtag {
extract(mtag);
return select(latest.etherType) {
0x800: ipv4;
default: ingress;
}
}
mtag的解析图:
(图三)
在解析过程结束之后,进入Ingress过程,并且调用Ingress流水线的流控制函数。
解析表示(Parsed Representation)
解析器根据P4程序解析实际的数据包。解析过程之后的匹配-动作流水线上的相关操作,将在解析生成的结果上执行。解析生成的结果叫做这个数据包的解析表示(Parsed Representation,在下文中用简称PR表示),它是一个合法首部实例的集合。
解析器生成未经操作的PR。在解析操作之后,匹配-动作流水线有可能会通过修改PR中的合法首部实例字段值来不断更新PR,也有可能从PR中删除或者增添某些首部实例。
总的来说,解析表示PR是底层解析处理之后的产物,是一系列合法首部实例的集合;首部实例的合法性体现在它是否在底层解析状态转移的过程中被解析状态函数操作(在P4程序中体现为,extract函数的函参)。后续的流水线过程将针对PR中的首部实例来进行操作。
对于某些特殊的操作(比如复制数据报),在数据报的处理流程中将会保存原始的数据报信息;相关内容在规范的后续章节会论述。
元数据实例被认为是PR的一部分;一般来说,元数据实例等同于普通的合法首部实例。
解析操作
解析器根据数据包中第一个字节的反馈进行调控。解析器在数据包中保存了一个当前指向该包中特殊单位字节的指针(current offset),并从该指针所指向的字节位置中提取出首部实例,然后标记这些实例是合法的,同时为这些实例分配资源来组成PR。处理之后,解析器移动该指针,使其指向下一个即将处理的字节;然后状态进行转移,进入下一个解析状态。
在做状态转移的决定时,P4程序可能会检查元数据的某些字段,将这些信息作为状态转移的参考;但运行P4的物理设备可能对该操作有所限制。
例如,位于元数据中的ingress端口信息可能会被用于确定初始解析状态节点(上文P4程序中的start解析函数)所允许解析的数据报格式。另外,服务于数据报复制和循环的元数据,在解析过程中会影响处理这些特殊数据报的操作。
在P4中,每一个状态节点代表着一个解析函数,会以以下四种方式中的一种结束该解析函数的相关操作。
(1) return语句声明了下一个将被执行的解析函数名,该解析函数为当前状态转移的目的状态。
eg:return ipv4; //ipv4为已定义的解析函数名
(2) return语句声明了即将执行的控制函数名,意味着解析过程的结束。
eg:return ingress;
(3) 使用关键字
parse_error
明确说明解析过程发生了错误。
(4) 未知的错误发生了。
需要注意的是,由于上文中头两个结束方法,解析函数名、控制函数名是共享在同一个命名空间里的。
倘若存在某一解析函数与某一控制函数重名的情况,编译器必须报错。
return语句中的选择操作,根据首部实例中特定字段的值或者数据报信息,为状态转移选择不同的状态分支。
在某状态节点中,通过调用extract方法来明确指明将被解析的首部实例;若一个解析函数需要声明extract语句来告知底层对数据包进行解析的操作 该声明应在解析函数定义的首部。
value sets
在某些情况下,在运行时(run time)需要确定一些能够决定解析过程状态转移的数值,比如说,MPLS标签字段的值用于确定遵循MPLS标签的包头,这种匹配也许会在运行时动态的改变。
P4通过支持解析器值集(Parser Value Set)来支持这种决定状态转移的功能。这个是一个已命名的值集,通过运行时API(run time API)来对该值集进行操作(加入或者删除)。在解析状态转移的过程中的某些情况,会参考该值集中的值。
解析器值集仅包含数值,没有包含任何其他的首部类型或者状态转移的信息。在同一个值集中,所有的数值必须对应于相同的转移。例如,所有对应于一个IPv4相关状态转移的MPLS标签值,存在于同一个值集;而对应于一个IPv6相关状态转移的MPLS标签值则组成了另外一个值集。
在P4语言中,属于最高级别(top level)的值集定义位于解析函数的定义部分之外。值集有一个独立的全局命名空间。
只有已定义的值集才能在解析函数中被引用。
//定义 parser_value_set value_set_name;
从值集被引用的位置可以推断出值集中数值的宽度。但如果在很多地方都使用了某一个相同的值集,那么就会出现推断出的值集数值宽度不一的情况,这种情况下编译器必须报错。
用于更新解析器值集的运行时API,必须支持同时定义值集中的数值和掩码对的情况。
在实际的P4程序中,一般会在某一个解析状态节点涉及到解析器值集的相关内容,比如:
parser parser_name {
return select(latest.field_name) {
value_set_name: default; //引用了名为value_set_name的值集
}
}
parser function BNF
(图)
extract函数只能对数据包中的包头进行提炼操作,无法操作元数据。
select函数是目的转移状态选择逻辑的主体,类比于C语言中的switch-case操作;该函数采用了逗号分隔的字段列表(a comma-separated list of fields,类似的概念可以参考CSV,这里的逗号指的是
;
),并且通过左端列出的具有重要意义的比特组(bits)(对应于特定字段值),比如下文中对应于field_name
字段值的0xaaaa
比特组,和组成该列表的成员字段值建立联系。
P4程序中的select函数体:
return select(latest.field_name) {
0xaaaa : next_parser_name;
default : ingress;
}
select函数的选择操作按列表中的顺序比较字段值和左端列出的比特组对应的值(一般为16进制值),如果没有发现与select函数的参数字段值相匹配的表项,则对应到default表项(如果有定义的话)。
根据上文给出的select函数体代码进行理解:select函数将字段field_name
的值与第一个表项中的0xaaaa
进行匹配,如果匹配成功则选择进入next_parser_name
状态节点;若不匹配,执行default
表项,结束解析过程,进入ingress流水线。
操作符mask用于三元匹配(ternary match),该类型的匹配需要定义确定的掩码值。
基于select函数的参数字段值和列表中的成员字段值的匹配受限于掩码的值,在进行比较之前,需要将参数字段的值与成员字段的值分别和掩码进行与运算。
在允许掩码匹配和将解析器值集引入匹配的情况下,可能会出现匹配到多个表项的情况;因此,表项的顺序决定了匹配的优先级:若出现该情况,则采用在列表中符合匹配条件的第一个表项。
select函数的参数中,经常出现
latest.xxx
,这个latest
代表了在该解析函数中最近一次解析的首部实例(即最近一次调用extract函数所操作的对象)。在同一个解析函数中,在使用latest
之前如果没有进行extract语句的操作,会出现错误。
[待定]另外一种情况是
select(current( const_value , const_value ))
,current函数用于引用在数据包中尚未被解析为字段的区域,其第一个参数是指向该区域的指针(offset),第二个参数是区域的宽度(单位为bit)。根据给出的宽度值得到的调用结果视作是一个无符号的字段值。
注:实际上,current函数的调用结果是根据转换规则转换成的元数据字段,具体的转换规则请参考规范附录17.5节。
在解析函数主体中,有可能会出现设置状态语句(上文BNF中的set_statement)。在设置元数据状态语句中,调用set_metadata函数如下:
set_metadata( field_ref , metadata_expr );
作为函数参数
field_ref
的字段,必须存在于一个已声明的元数据实例之中。如果metadata_expr
(该函数参数可以是一个字段的值,也可以是对数据或者字段的引用)与该字段值不一,则会发生值的替换。
extract 函数
extract函数将首部实例作为其函数参数,该首部实例不能是元数据实例。
提取操作将当前指针(current offset)所指向的数据包位置上的数据拷贝到首部实例中。在操作结束之后,将该指针移向下一个待解析的数据包首部。
注意,当对下一个可用的空间进行提取的时候,对于作为extract函参的首部实例栈而言,需要使用标识符
next
而不是last
来指向该空间;也可以通过偏移值将首部实例栈中的某一个实例作为extract函参:
return extract(instance_name[const_value]) {···}
或者
return extract(instace_name[next]) {···}
解析器异常处理机制
当解析过程中出现错误时,解析器有两种处理的方法:(1)丢包(drop),(2)加工(process)。
若解析器使用了丢包作为处理解析错误的方法,那么一旦在解析过程中遇到错误,解析器马上将包丢弃;该数据包不会进行任何的后续匹配-动作操作。
在一次执行的过程中,需要启用一个或者多个计数器来记录丢包事件的发生次数。
另一方面,如果解析器使用了加工处理的方法,它在遇到异常错误的时候会立即停止解析的过程,并设置一个特殊的元数据来详细说明该解析错误;随后该包被交给匹配-动作流水线的控制函数来进行处理。
就像其它正常的数据包一样,该数据包根据已安装至底层设备的匹配-动作规则来进行相关操作处理,但这些规则可能会根据该数据包所携带的元数据来检查解析错误,并采取诸如将包送至控制层面的策略。
在P4中,有很多可能会暗中触发的错误情况,这些情况列举在下文的表格中。
另外,程序员可以在解析函数中通过parser_error
异常处理关键字来设置对于错误的处理方法:
parser_error parser_exception_name;
程序员们可以依据下文的BNF逻辑,来明确定义关于解析器异常处理机制的操作。
错误处理指令能够调用一些元数据集来协助处理操作,该指令要么将包交给了后续流程的控制函数,要么是丢包指令。
注意,只有在执行了调用操作之后,上文提到的为说明解析错误而设置的特殊元数据才实际有效。
标准的解析器异常处理机制
在下表中列举了标准的解析器异常处理机制名称。
前缀pe
代表解析器异常处理机制(parser exception)。
(Table)
当一个异常处理机制将数据包交给了后续的匹配-动作流水线,那么可以用元数据来表示该处理机制的类型。
默认的异常处理方法
在定义了
p4_pe_default
默认处理机制的前提下,倘若出现了一个解析错误没有匹配到对应解析处理机制的情况,选择调用已定义的默认处理机制。
如果发生了上面的情况,同时也没有定义任何的默认处理机制,那么解析器选择丢包。
2016/10/12