P4语言编程详解
1.源码目录结构
P4项目源码可以在github上直接获取(https://github.com/p4lang)。P4项目由很多个单独的模块组成,每个模块就是一个子项目,下面分别简单介绍一下各模块的功能。
(1)behavioral-model
模拟P4数据平面的用户态软件交换机,使用C++语言编写,简称bmv2。P4程序首先经过p4c-bm模块编译成JSON格式的配置文件,然后将配置文件载入到bmv2,转化成能实现交换机功能的数据结构。
behavioral-model模块是架构无关的,可以实现各种P4编程目标。该模块主要实现三个目标,其中最重要的是 simple_switch,即实现P4语言标准中抽象交换机模型。另外两个目标是(simple_router,l2_switch),这两个目标是作 为教学示例。
(2)p4-hlir
将P4代码转换成高级中间表示的前端编译器,目前的高级中间表示的展示形式与python对象的层次结构相同。该编译器的目的是使得后端编译器开发者从语法分析和目标无关的语义检查的负担中解放出来。
(3)p4c-bm
behavioral modal的后端编译器,建立在p4-hilr的顶部,该模块以P4程序作为输入,输出一个可以载入到behavioral model的JSON配置文件。
(4)p4-build
需要手动生成的基础设施库,为执行P4程序编译、安装PD库。
(5)switch
内含switch.p4程序样例以及通过SAI、SwitchAPI和Switchlink操作交换机所需的所有库,可独立于p4factory运行 。
(6)ntf(Network Test Framework)
网络测试框架,内含用以执行bmv2上应用的网络测试样例。该框架中集成了mininet和docker,方便用户进行测试。
(7)p4factory
内含整套用以运行和开发基于behavioral model的P4程序环境的代码,帮助用户快速开发P4程序。
(8)ptf
数据平面测试框架,基于unittest框架实现,内含标准Python版本。该框架中的大部分代码从floodlight项目中的OFTest框架移植而来,框架的实现和开发可参考OFTest框架文档。
(9)scapy-vxlan
基于Scapy项目,barefoot对其进行了定制,支持更多协议的数据包包头的伪造和解析,目前支持 VXLAN和ERSPAN-like(Scapy本身并不支持)。
(10)tutorials
P4语言教程,内含8个教程,覆盖了P4语言中的解析器、动作、状态存储、匹配-动作表、等基础组件。
1)cpoy_to_cpu:基本动作clone_ingress_to_egress教程
2)meter:计量表教程
3)TLV_parsing:IPv4数据包解析教程
4)register:寄存器读写状态教程
5)counter:计数器教程
6)action_profile:ECMP动作摘要教程
7)resubmit:数据包冲提交到入端口流水线教程
8)simple_nat:TCP流量的完全圆锥形NAT网络教程
注:P4语言项目库中的SAI、mininet及thrift是从其他开源项目完全fork而来,这里不展开讨论。
2.P4语言标准
当前P4语言标准的最新版本为《The P4 Language Specification Version1.1》(以下简称V1.1),目前版本的P4语言编译器已经基本实现了P4语言标准中的绝大部分特性 ,部分特性尚在开发之中。
2.1 基础数据类型及操作
P4语言中定义了5种基础数据类型,分别是:bool、bit、int、varbit、int。(注:此处W代表长度,通常使用十进制数字表示,如 bit)通常情况下,不同的数据类型之间可以相互转换,并且所有的二目运算符都要求数据类型保持一致,除了位移操作符(shifts)。
(1)布尔型(bool)
布尔型(Boolean),值为true或false,非整数型。布尔类型数据可进行如表1所示运算。
运算符 |
描述 |
and |
二目运算符,操作数必须都为布尔型,运算结果为布尔型。 |
or |
二目运算符,操作数必须都为布尔型,运算结果为布尔型。 |
not |
单目运算符,操作数必须为布尔型,运算结果为布尔型。 |
==,!= |
测试是否相等或不等,运算结果为布尔型。 |
表1 布尔型支持的运算
(2)无符号整型(bit)
无符号整型(unsigned
integers)也叫位串(bit-string)。位串是以比特位形式表示的任意长度的数(如:bit<127>,表示长度为127比特
的位串),但如果需要对位串进行某些数学运算时,位串长度必须是8的整数倍(如:16、32、64bit)。无符号整型支持如表2所示运算。
运算符 |
描述 |
==,!= |
测试是否相等或不等,运算结果为布尔型。 |
<,>,<=,>= |
无符号数比较,操作数的长度(W)要求相同,运算结果为布尔型。 |
&,|,^ |
按位运算符,操作数的长度(W)要求相同,运算结果为无符号整型。 |
~ |
运算结果为操作数的补码。 |
<<,>> |
左移运算符操作数为无符号整型,右移运算符操作数必须是无符号数或非负整数。此运算符为逻辑位移。 |
+(单目) |
单目加运算,效果同no-op。 |
-(单目) |
单目减运算,计算结果为2W减去操作数,W为操作数长度。 |
+(双目) |
二目加运算,操作数的长度(W)要求相同。计算结果为操作数的算术和,且运算结果长度也必须为W,超过则截断。 |
-(双目) |
二目减运算,操作数的长度(W)要求相同。计算结果为操作数的算术差。 |
* |
无符号乘法运算,操作数的长度(W)要求相同,计算结果为无符号数且长度与操作数相等。 |
表2 无符号整型支持的运算
(3)有符号整型(int(W))
有符号整型(signed integers)支持如表3所示运算。
运算符 |
描述 |
==,!= |
测试是否相等或不等,运算结果为布尔型。 |
<,>,<=,>= |
有符号数比较,操作数的长度(W)要求相同,运算结果为布尔型。 |
&,|,^ |
按位运算符,操作数的长度(W)要求相同,运算结果为无符号整型。 |
~ |
运算结果为操作数的补码。 |
<<,>> |
左移运算符操作数为有符号整型,右移运算符操作数必须是无符号数或非负整数。此运算符为逻辑位移。 |
+(单目) |
单目加运算,效果同no-op。 |
-(单目) |
单目减运算,运算结果有符号整型,且长度与操作数相等。 |
+(双目) |
二目加运算,操作数数据类型必须相同,运算结果也为同类型。 |
-(双目) |
二目减运算,操作数数据类型必须相同,运算结果也为同类型。 |
* |
有符号乘法运算,操作数的长度(W)要求相同,计算结果为有符号数且长度与操作数相等。 |
表3 有符号整型支持的运算
(4)变长位串(varbit)
变长位串(dynamically-sized bit-strings)不支持算术、比较、按位运算,甚至不支持类型转换。该数据类型在定义时会指定一个静态的最大宽度值,解析器会提取变长位串数据并设置一个值作为长度。
(5)无限精度整型(int)
无限精度整数(infinite-precision integers)支持如表4所示运算。
运算符 |
描述 |
==,!= |
测试是否相等或不等,操作数必须都是整型(int)运算结果为布尔型。 |
<,>,<=,>= |
有符号数比较,操作数类型都必须是整形,运算结果为布尔型。 |
<<,>> |
右移运算符操作数必须为正整数;左移运算结果和操作数相同。a<<b等价于ax2b,a>>b等价于a/2b。 |
+(单目) |
单目加运算,效果同no-op。 |
-(单目) |
单目减运算,运算结果为整型,且该运算不会导致溢出。 |
+(双目) |
二目加运算,操作数类型都必须是整型,运算结果为整型,且该运算不会导致溢出。 |
-(双目) |
二目减运算,操作数类型都必须是整型,计算结果为整型,且该运算不会导致溢出。 |
* |
无符号乘法运算,操作数必须都是整形,计算结果为整形,该运算不会导致溢出。 |
/,% |
二目有符号除法和取模运算,操作数必须是正整数,运算结果为正整数。 |
表4无限精度整型支持的运算
2.2 数据类型转换
再P4预研中,对数据进行运算时大多时候都要保证操作数数据类型的一致性,P4也提供了基础的数据类型转换功能,表5中列出了所有合法的数据类型转换。
From |
To |
描述 |
bit<1> |
bool |
0代表fasle,1代表true。 |
bool |
bit<1> |
0代表fasle,1代表true。 |
bit<W> |
int<W> |
保留所有比特位不变。 |
int<W> |
bit<W> |
保留所有比特位不变。 |
bit<W> |
bit<W1> |
当W>W1时,保留低位W1位长度的数据,当W<W1时新增位补0. |
int<W> |
int<W1> |
当W>W1时,保留低位W1长度的数字,当W<W1时新增位补符号位. |
int |
bit<W> |
将整型转化为位串,保留地位W位长度数据,溢出需要发出警告并转化为负数。 |
int |
int<W> |
将整型转化为位串,保留地位W位长度数据,溢出需要发出警告。 |
表5 合法数据类型转换
在P4程序中对数据进行运算时,除了用户在编写程序是手动转换数据类型,P4编译器在某些情况下也会自动将数据进行类型转换,这种转换是强制的、自动的的隐式类型转换。如表6所示,例举了P4程序中常见的几种隐式类型转换的情况。
bit<8> x; |
|
表达式 |
实际实现 |
x+1 |
x+(bit<8>)1 |
z<0 |
z<(int<8>)0 |
x<<13 |
0;//溢出时发出警告 |
x|0xFFF |
x|(bit<8>)0xFF;//溢出警告 |
表6 隐式类型转换
2.3 基础语言组件
P4程序中有5个语言组件:首部(Headers)、解析器(parsers)、表(Tables)、动作(Action)、流控制程序。
(1)首部
首部类型是由成员字段组成的有序列表,每个字段都有其名称和长度,每一种首部类型都有对应的首部实例来存储具体的数据。首部分为两种,一种是包头(Packet Headers),另一种是元数据(Metadata)。
包头用以描述数据包结构,以IPv4协议为例,图1为 IPv4报文头部结构,IPv4报头有20字节固定长度部分和可选字段、填充字段的可变部分,每个字段的作用这里不再赘述。
图1 IPv4协议报头结构
图2 IPv4 包头定义
对照图1中IPv4报头结构可以比较容易理解上述P4语言代码——按照IPv4报头格式,定义了一个包头并实例化。
这里需要区分“包头”,“报头”的关系。如果没有特殊指出,本文中的“包头(Packet Header)”指的是P4语言中的术语,而“报头”指的是数据包的报文头部。
元数据用来携带数据和配置信息,元数据的申明与包头类似,但在实例化时不同,而且包头和元数据在字段值的约束上存在一定的差别。元数据分为两种,一
种是用来携带P4程序运行过程中产生的数据的用户自定义元数据(User-Defined
Metadata),如首部字段的运算结果等。另一种是固有元数据(Intrinsic
Metadata),用于携带交换机自身的配置信息,如数据包进入交换机时的端口号等。
图3 元数据定义
用户可以使用自定义的元数据来携带任意数据,但固有元数据在编译器中具有特定的意义。V1.1中定义了8种固有元数据,这些元数据携带了数据包相关的状态信息,表7中展示8种标准固有元数据及其作用。
字段 |
描述 |
ingress_port |
数据包的入端口,解析之前设置。只读。 |
packet_length |
数据包的字节数,当交换机在快速转发模式下,该元数据不能在动作(action)中匹配或引用。只读。 |
egress_spec |
在入端口流水线的匹配-动作过程之后设置,指定数据包出端口,可以是物理端口、逻辑端口或者多播组。 |
egress_port |
指定数据包的物理出端口,区别于egress_spec,只能应用于物理端口。只读。 |
egress_instance |
用于区分复制后数据包实例的标识符。只读。 |
instance_type |
数据包实例类型:正常(Normal)、入端口复制(ingress clone)、出端口复制(egress clone)、再循环(recirculated)。 |
parser_status |
解析器解析结果,0表示无错误,其实数字代表了对应的错误类型。 |
parser_error_loaction |
指向P4程序错误发生处。 |
表7 固有元数据
在P4语言中定义首部类型有以下几点需要注意:
1)包头类型的长度需要字节对齐,即长度必须是8bit的整数倍。
2)包头中字段长度可以是可变值(该特性在P4语言规范中规定,但当前编译器版本并为实现,后续版本会支持)也可以是首部中其他字段值计算后的值。而元数据中的字段长度只能是定值。
3)只有包头能够实例化成数组,元数据则不行。
4)实例化时,首部中已定义名称的字段的值会被初始化成程序中的指定值,如果首部中只定义字段名称而未指定值,字段的值将会被初始化成0。
(3)解析器
一个P4程序中往往定义了大量的首部和首部实例,但并不是所有的首部实例都会对数据包进行操作。解析器工作时会生成描述数据包进行哪些匹配+动作操作的中
间表示( Intermediate Representation),在P4中称之为解析后表示(Parsed
Representation),这些解析后表示规定了对数据包生效的实例,是一组对数据包生效的实例的集合。
P4语言中解析器采用有限状态机的设计思路,每个解析器方法视为一种状态。当解析器工作时,会将当前处理的数据包头字节的偏移量记录在首部实例中,
并在状态迁移(调用另一个解析器)时指向包头中下一个待处理的有效字节。以以太网帧的解析器为例,用数据包类型代对应解析器,将每个解析器作为一种状态,
用箭头表示状态迁移,则可以构建出如图2 所示的以太网帧的解析器的状态迁移图。
图4 以太网帧解析器状态迁移图
图5 解析器定义
一个解析方法/状态可以以下四种方式结束:
1)return 一个流控制程序名
2)return一个解析器名
3)发生显式错误
4)发生隐式错误
P4语言中流控制程序和解析器的命名空间是共用的,所以在定义解析器和流控制程序的时候需要注意不能重名,否则会导致P4程序错误。
(3)动作
P4语言中的动作主要分为两种,基本动作(Primitive Actions)和复合动作(Compound
Actions)。基本动作包括:数据包处理运算符(如添加、删除或修改包头)、基本的算术运算符、哈希运算符和统计跟踪运算符(如计量、测量)。复合动
作由基本动作组合而成,由用户自行定义。表8中展示了P4中定义的基本动作。
动作 |
描述 |
no_op |
占位符动作,不做任何操作。 |
drop |
在入口流水线中将数据包丢弃。 |
modify_field |
修改解析后表示中的包头字段值。 |
modify_field_with_hash_based_index |
使用字段列表索引计算一个值并使用该值生成偏移量。 |
add_header |
为数据包的解析后表示添加包头。 |
remove_header |
为数据包的解析后表示删除包头。 |
copy_header |
复制首部实例。 |
push |
将所有首部实例压入一个数组,并在顶部添加一个新首部。 |
pop |
将实例数组顶部的元素弹出,后续元素向顶部移位。 |
count |
更新计数器。 |
meter |
执行计量操作。 |
generate_digest |
生成一个报文摘要并发送到接收机。 |
truncate |
在出口处截断数据包。 |
resubmit |
将原始数据包和元数据重新发送到解析器。 |
Recirculate |
在数据包完成出口修改操作后重新发送。 |
clone_ingress_pkt_to_ingress |
复制原始数据包并发送到解析器。 |
clone_egress_pkt_to_ingress |
复制出口数据包并发送到解析器。 |
clone_ingress_pkt_to_egress |
复制原始数据包并发送到缓存区。 |
clone_egress_pkt_to_egress |
复制出口数据包并发送的缓存区。 |
表8 基本动作
这些动作高度抽象且与协议无关,以实现P4语言处理数据的协议无关性。同时,复杂的操作及流程可以通过组合不同基本操作(即复合操作)完成,从而保障了P4语言对各种协议的支持以及扩展性。图6展示了P4中复合动作定义的示例。
图6 复合动作定义
(4)匹配-动作表
P4语言中的匹配-动作表定义了匹配字段、动作及一些相关属性(如表容量),当匹配-动作表中定义的字段与数据包匹配成功时,则执行对应的动作;若匹配不成功则标记为“失配(miss)”,并执行默认操作。匹配动作表的定义如图7所示。
图7定义动作-匹配表
P4语言的匹配-动作表支持多种匹配类型,如精确匹配、最长前缀匹配、范围匹配等。如表9所示,展示了动作-匹配表支持的匹配类型。
匹配类型 |
描述 |
excat |
精确匹配。 |
ternary |
三重匹配,动作-匹配表的每个表项都有一个掩码,将掩码和字段值进行逻辑与运算,再执行匹配。为了避免导致多条表项匹配成功,每条表项都需要设定一个优先级。 |
lpm |
这是三重匹配的一种特殊情况,当多个表项匹配成功时,选择掩码最长的最为最高优先级进行匹配。 |
index |
字段值作为表项索引。 |
range |
表项中确定一个范围,字段值在此范围内皆能成功匹配。 |
valid |
仅用于包头字段匹配,表项值只能为true/false。 |
表9 匹配类型表
(4)流控制程序
P4语言中匹配-动作表中规定需要匹配的字段和需要执行的操作,流控制程序则用来规定匹配-动作表的执行顺序。
以P4语言定义二层转发流程为例,数据包首先进行L2转发表(l2_fwd)匹配,然后根据数据包的以太网目的地址是否匹配路由器自身的MAC地址
(通过查找所属的router_mac表)决定是否经过l3路由表(ipv4_fib_lpm和upv6_fib_lpm),再根据IP包头类型
(IPv4或IPv6),数据包匹配不同的L3路由表,最后通过访问控制列表来控制数据包是否通过。
图8 流控制程序定义
2.4 状态存储
包头和元数据实例中的数据只能存在对某个数据包解析的过程中,解析下一个数据包时,这些实例会重新初始化。而计数器、计量器和寄存器中的数据在整个流水线中长期存在,所以称之为状态存储。
(1) 计数器
计数器附加在每个表项之后,并在完成一次匹配并执行对应操作后自增1。计数器中定义了7种属性,下图展示了V1.1中计数器的定义方式。
图9 计数器定义
1)Name
计数器名称,指向该计数器,P4编译器中通过名称+索引的方式确定一个计数器实例。
2)min_width
编译P4程序时,编译器分配给计数器的大小并不是完全固定的,该属性指定了分配给计数器的最小长度。
3)saturating
如果计数器中设定了该属性,则当计数器到达上限时停止计数,否则计数器将清零并重新开始计数。
4)direct
如果计数器中设定了该属性,则计数器绑定的匹配-动作表中无需指定count动作来更新计数器,计数器会自动更新。若在匹配动作表调用count动作更新计数器,则编译器报错。
5)static
如果计数器中设定了该属性,则必须在匹配-动作表中调用count动作更新计数器。
6)instance_count
该属性用以记录计数器实例数,如果计数器设定了direct属性,则无法在计数器中设定该属性;如果计数器中未设定direct属性,则该属性必须设定。
7)type
V1.1中的计数器类型有3种: bytes、packets、bytes_and_packets。
(2) 计量器
计量器的定义与计数器类似,计量器中定义了6种属性,下图展示了V1.1中计数器的定义方式。
图10 计量器定义
1)name
计量器名称,指向该计量器。
2)direct
如果计量器中设定了该属性,则计量器绑定的匹配-动作表中无需指定execute_meter动作来更新计量器,计数器会自动更新。若在匹配动作表调用execute_meter动作更新计量器,则编译器报错。
3)static
如果计数器中设定了该属性,则必须在匹配-动作表中调用execute_meter动作更新计数器。
4)instance_count
该属性用以记录计量器实例数,如果计量器设定了direct属性,则无法在计量器中设定该属性;如果计量器中未设定direct属性,则该属性必须设定。
5)type
V1.1中的计量器类型有2种: bytes、packets。
6)result
V1.1中的计量器的输出结果有3种,分别用三种颜色标记:红色(P4_METER_COLOR_RED)、黄色(P4_METER_COLOR_YELLO)和绿色(P4_METER_COLOR_GREEN),输出结果存在一个2bit长度的字段中。
(3) 寄存器
寄存器定义了5种属性,下图展示了V1.1中寄存器的定义方式。
图11 寄存器的定义
1)name
寄存器名称,指向该寄存器,P4编译器中通过名称+索引的方式确定一个计量器实例。
2)width_or_layout
width和layout属性二选一,width为指定一个确定的长度,而 layout是直接通过名称引用已定义的包头结构。
3)direct_or_static
与计数器和计量器中的定义类似,虽然寄存器不能直接在匹配过程中使用,但是作为modify_field动作的数据源,将当前寄存器中的数据复制到数据包的元数据中,并在后续的匹配中使用。
4)instance_count
该属性用以记录寄存器实例数,如果寄存器设定了direct属性,则无法在寄存器中设定该属性;如果寄存器中未设定direct属性,则该属性必须设定。
3 结语
以上是参考P4语言规范标准并结合个人的理解所写,希望能让不了解P4的人能有个基本的认识,同时起到抛砖引玉的作用。对P4感兴趣的同学可以联系笔者加入到P4微信交流群中与大牛们一起讨论。
邮箱:yangshuai@sdnlab.com 微信号:Redmaple_