本文是学习2021年5月最新的P416语言规范(P416 Language Specification)时所做的学习笔记,以下将P416语言规范简称为规范。同时,为了避免因翻译而导致的问题,文中用加粗的原英文来表达部分术语,翻译成中文的术语也将加粗标记。
目录- 术语
- P4简介
- 核心抽象
- BNF范式(巴科斯范式)
- very_simple_switch(VSS)
- VSS程序解析
- 代码附录
- References
术语
- 体系结构(Architecture):描述一组P4可编程组件以及他们之间的数据平面接口,由厂家提供。可以认为是P4程序跟Target之间的一种“约定”,P4程序员需要针对该“约定”编写程序。
- 数据平面(Data Plane):数据平面实现分组处理和转发逻辑。
- 控制平面(Control Plane):控制平面提供了数据平面处理和转发分组前所必须的各种网络信息和转发查询表项。
- 元数据(Metadata):P4程序的执行过程中生成的中间数据。
- 分组(Packet):分组是数据平面要处理和转发的对象。
- 分组首部(Packet Header): 分组最开始的一段格式化数据。一个给定的分组可能包含一系列代表不同网络协议的首部。
- 分组有效载荷(Packet Payload): 跟在分组首部后面的分组数据。
- 分组处理系统(Packet-processing system):为处理网络分组而设计的一种数据处理系统,分组处理系统实现控制平面和数据平面算法。
- 目标(Target):一种可以执行P4程序的分组处理系统。Target可以是一个交换机、路由器或者其它可编程转发元件,Target是一个总称。
P4简介
P4是一种语言,全称是Programming Protocol-independent Packet Processors(编程协议无关的分组处理器),用于表示可编程转发元件(如硬件或软件交换机、网络接口卡、路由器或网络设备)的数据平面如何处理Packet。P4仅用于指定Target的数据平面功能以及部分控制平面和数据平面通信的接口,但P4不能用来描述Target的控制平面功能。
P4可编程交换机与传统交换机的区别
- 协议无关性:P4交换机是协议无关的,交换机不与任何特定的网络协议绑定,程序员可以通过编程自定义各种数据平面协议和分组的处理转发逻辑。
- 目标无关性:用户不需要关心底层硬件的细节就可对分组的处理方式进行编程。这一特性通过P4前后端编译器实现,前端编译器将P4高级语言程序转换成中间表示,后端编译器将中间表示编译成设备配置,自动配置目标设备。
- 可重构性:在不更换硬件的前提下,允许用户随时改变分组的处理和转发方式 ,并在编译后重新配置交换机。
核心抽象
-
Header:描述在一个分组内的各个协议首部的格式。
-
Parser:描述接收到的分组中允许的首部序列、如何解析这些首部序列、以及要从分组中提取的首部和字段。
-
Tables:将用户定义的key与Action相关联。 Table泛化了传统的交换机表,可用于实现路由表、流表、访问控制列表和其它用户自定义的表。
-
Actions: 描述如何处理首部字段和Metadata。 Actions可以包含控制平面在运行时提供的数据。
-
Match-Action Units:执行以下操作序列:首先,根据分组字段或所计算出的Metadata来构造key;然后,使用构造的key在Table中查找,选择要执行的Action;最后,执行所选的Action。
-
Control Flow:描述分组在Target上的处理流程,包括Match-Action Units调用的数据依赖序列。也可以用来实现Deparser。
-
Extern Object:特定于Architecture的第三方库,可以由P4程序通过定义良好的API进行调用。
-
User-defined Metadata:用户自定义的数据结构,与分组相关联。
-
Intrinsic Metadata:由Architecture提供的元数据,例如,所接收到的分组的输入端口。
用P4编程Target时的一个典型工具工作流如下图所示
Target制造商为Target提供硬件或软件实现框架、Architecture定义和P4编译器。P4程序员为特定的Architecture编写程序(图中绿色区块),该Architecture定义了Target上的一组P4可编程组件及其外部数据平面接口。
编译一组P4程序会产生两个工件:
- 数据平面配置,用于在数据平面实现由 P4 程序指定的转发逻辑
- API,用于管理数据平面对象状态
BNF范式(巴科斯范式)
规范中使用BNF范式来给出P4的语法,因此,在介绍具体语法之前,先简单描述一下BNF范式。本部分内容不是规范中介绍的内容。
巴科斯范式是以美国人巴科斯(Backus)和丹麦人诺尔(Naur)的名字命名的一种形式化的语法表示方法,用来描述语法的一种形式体系,是一种典型的元语言,又称巴科斯-诺尔形式(Backus-Naur Form,BNF)。它不仅能严格地表示语法规则,而且所描述的语法是与上下文无关的。
BNF表示语法规则的方式为:
- 非终结符用尖括号括起
- 每条规则的左部是一个非终结符,右部是由非终结符和终结符组成的一个符号串,中间一般以“: :=”分开
- 具有相同左部的规则可以共用一个左部,各右部之间以直竖“|”隔开
- 双引号(" ")内包含的字符代表着这些字符本身,而双引号本身用double_quote来代表
- 在双引号外的字(有可能有下划线)代表着语法部分
- 尖括号(< >)内包含的为必选项
- 方括号([ ])内包含的为可选项
- 花括号( { } )内包含的为可重复0至无数次的项
- 竖线( | )表示在其左右两边任选一项,相当于"OR"的意思。
- ::= 是“被定义为”的意思
在规范中,所使用的BNF表示与上述略有不同:
-
字母大写的符号表示终结符,小写字母表示的符号为非终结符
-
“被定义为”用冒号(:)表示,而不是“: : =”
nonTypeName : IDENTIFIER | APPLY | KEY | ACTIONS | STATE | ENTRIES | TYPE ; name : nonTypeName | TYPE_IDENTIFIER ;
如上述例子表示的是,一个name
被定义为一个nonTypeName
或者一个TYPE_IDENTIFIER
,一个nonTypeName
被定义为一个IDENTIFIER
或者APPLY
...或者一个TYPE
。全部大写的符号是终结符,无须再定义。
very_simple_switch(VSS)
接下来将介绍规范中给出的very_simple_switch
例子,以下称该例子为VSS。
VSS Architecture 概览
如图所示,这是一个最简单的P4可编程Architecture。VSS可以从8个以太网入端口(最左边的三个蓝色箭头)、再循环通道(最下面的从右到左的蓝色长箭头)或者与CPU(控制平面)直接相连的端口(左上角的蓝色箭头)接收分组。VSS只有唯一的一个Parser,其输出到唯一的一个Match-Action Pipeline。Match-Action Pipeline输出到唯一的一个Deparser,在退出Deparser后,分组被传送到11个出端口(8个以太网出端口+3个特殊端口)中的某一个端口。3个特殊端口分别为:
-
被送到“CPU Port”的分组将被传送到控制平面
-
被送到“Drop Port”的分组将被丢弃
-
被传送到“Recirculate Port”的分组将通过再循环管道回到入端口
图中白色的模块是可编程的部分,用户必须提供对应的P4程序来指定每个白色模块的行为。红色箭头表示用户自定义数据。青色区块是功能固定的组件。绿色箭头是数据平面接口,用来在功能固定的模块和可编程模块之间传递信息——在P4程序中作为Intrinsic Metadata
VSS Architecture 固定功能模块
-
Arbiter Block(仲裁模块)
- 从11个端口(8个以太网入端口+3个特殊端口)中的某一个端口接收分组
- 对于从以太网入端口收到的分组,仲裁模块计算以太网尾部校验和并且进行验证。如果校验和不匹配,该分组将被丢弃。如果校验和匹配,将尾部校验和字段从分组有效荷载中移除。
- 如果有多个分组可用,仲裁模块需要运行仲裁算法来决定哪个分组先处理,其余的分组在等待队列中排队。
- 如果仲裁模块正在处理前一个分组,并且等待队列满了,则输入端口直接丢弃该分组。
- 在收到一个分组后,仲裁模块设置
inCtrl.inputPort
的值,该值将作为Match-Action Pipeline的一个输入,以标识该分组的源输入端口。物理以太网端口编号为0~7,再循环通道端口编号为13,CPU端口编号为14。
-
Parser Runtime Block(解析器运行时模块)
解析器运行时模块和Parser协同工作,它基于解析操作给Match-Action Pipeline提供一个错误代码,并且给解复用模块提供分组有效荷载的信息(比如剩余有效荷载的大小等)。从解析器运行时模块到解复用模块的箭头表示从Parser到解复用器的附加信息流:正在处理的分组以及解析结束的分组内的偏移量(即分组有效负载的开始位置)。一旦Parser处理完一个分组,Match-Action Pipeline就会被调用。Match-Action Pipeline以相关的Metadata(Packet Headers和User-defined Metadata)作为输入。
-
Demux Block(解复用模块)
解复用模块的核心功能是接收来自Deparser的新首部和来自Parser的有效荷载,将它们重组成一个新的分组后发送到正确的出端口。出端口由Match-Action Pipeline设置的
outCtrl.outPort
值来指定:- 送到Drop端口的分组将被丢弃
- 送到以太网端口(编号为0~7)的分组将会被发送到对应的物理接口。如果该输出接口正在发送另一个分组,则该分组会被放在等待队列里。当要发送该分组时,物理接口会计算一个正确的以太网校验和尾部并追加到分组上。
- 送到CPU端口的分组将会传送到控制平面。如果是这种情况,被传送到CPU的将是原始分组而不是重组后的新分组,重组后的新分组将会被丢弃。
- 送到再循环通道端口的分组将会从再循环通道返回到入端口。当一个分组无法通过单一通道完成处理时,再循环通道就非常有用。
- 如果
outCtrl.outPort
制定的出端口号是非法的值(比如端口号为9,前面设置的端口号只有0~7, 13和14),该分组将被丢弃。 - 最后,如果解复用模块正在处理前一个分组并且等待队列已经满了,那么该分组将会被直接丢弃。
VSS程序解析
在此部分将对VSS程序代码进行解析,同时介绍所涉及到的P4语法,完整代码将附在文末。
1. include、typedef 和 type
# include <core.p4>
# include "very_simple_switch_model.p4"
typedef bit<48> EthernetAddress;
typedef bit<32> IPv4Address;
与一个C++程序类似,P4通过# include
预处理命令包含一些外部文件。
core.p4
是P4的一个核心库,包含对大多数程序有用的声明,例如它包含预定义的packet_in
和packet_out
外部对象的声明,这些对象在Parser和Deparser中用于访问分组数据。同时,核心库还定义了一些标准数据类型和错误代码。
very_simple_switch_model.p4
是VSS Architecture的声明文件,包含了对Parser、Deparser、Package等不可缺少的重要组件的声明。very_simple_switch_model.p4
的部分声明(VSS涉及到的)将附在文末。
与C++类似,关键字typedef
可以给类型取一个别名:
typedef typeName newName;
上述语句给typeName
类型起了一个别名newName
。在使用上,newName
和typeName
完全一样,两者只是名字不一样,实际上是同一个东西。
与typedef
有明显区别的是关键字type
,它用以引入一个全新的类型。
type typeName newType;
上述语句引入了一种全新的类型,注意newType
和typeName
是两种不同的类型。如果这两种类型要互相赋值,则需要强制类型转换:
type bit<32> U32;
U32 x = (U32)0;
在描述需要通过通信信道(比如控制平面API或者要被送到控制平面的网络分组)与控制平面进行交换的P4值时,通常使用type
关键字。例如:
type bit<9> PortId_t;
上述语句定义了一个全新的类型PortId_t
,用以表示长度为 9 比特的端口号。这样可以避免端口号被进行算数运算,因为新引入的类型PortId_t
不支持算术运算(尽管bit<9>
类型支持算数运算,但他们是不同的两种类型)。
注意,并不是所有类型都支持typedef
和type
关键字:
2. header、基础数据类型和操作
header Ethernet_h {
EthernetAddress dstAddr; // 目的地址
EthernetAddress srcAddr; // 源地址
bit<16> etherType; // 上层协议类型
}
header IPv4_h {
bit<4> version; // 版本
bit<4> ihl; // 首部长度
bit<8> diffserv; // 区分服务
bit<16> totalLen; // 总长度
bit<16> identification; // 标识
bit<3> flags; // 标志
bit<13> fragOffset; // 片偏移
bit<8> ttl; // 生存时间
bit<8> protocol; // 协议
bit<16> hdrChecksum; // 首部校验和
IPv4Address srcAddr; // 源地址
IPv4Address dstAddr; // 目的地址
}
分组首部用关键字header
定义。header
是P4的派生类型,跟c++中的struct
关键字类似。header
定义了一个特定首部的所有字段。注意,header
类型除了显示定义的字段外,还有一个bool
类型的隐藏字段validity
,表示该首部是否有效,初始值为false
。可以使用以下 3 个方法来对validity
字段进行操作:
-
isValid()
方法: 返回validity
字段的值. -
setValid()
方法:将validity
字段值设为true
-
setInvalid()
方法:将validity
字段值设为false
如上面的代码所示,VSS定义了两个首部的格式:以太帧首部和IPv4协议首部。bit<n>
是基本数据类型中的Integer
类型。Integer
是整数类型的统称,不是一个标准的基本数据类型,根据有符号和无符号,定长和不定长,分为以下几种形式:
基础类型符号 | 说明 |
---|---|
bit<w> | 表示长度为 w 比特的无符号整数,也叫bit-string |
int<w> | 表示长度为 w 比特的有符号整数 |
varbit<n> | 长度最多为 w 比特的不定长无符号整数 |
int | 无限精度有符号整数 |
Integer的字面值
一个整数值可能包含某些表示整数类型的前缀:
-
表示数值进制的前缀:
- 0x 或者 0X 表示 16 进制
- 0o 或 0O 表示 8 进制,注意与C++区分,必须是数字0+字母o/O,而不是单独一个数字0。C++中 8 进制用单一数字 0 作为前缀
- 0d 或 0D 表示 10 进制
- 0b 或 0B 表示 2 进制
- 不加以上任何前缀,默认为 10 进制
-
表示长度和有符号无符号属性的前缀:
-
长度用一个数字表示,后面紧跟着字母s/w,表示有/无符号属性
-
nw 表示长度为 n 比特的无符号整数,等价于bit<n>类型
-
ns 表示长度为 n 比特的有符号整数,等价于int<n>类型
-
不带以上任何前缀的等价于int类型
-
-
在数值中间可能出现下划线( _ ),这在P4中也是合法的,计算具体数值时直接忽略下划线
-
长度为n比特的数字,每个比特从0~n-1编号。比特位0是最低有效位(最右边),比特位n-1是最高有效位(最左边)
Integer的操作符
P4中的所有二元操作符(除了移位操作)都要求两个操作数的类型和长度一样。通常来说,对于w比特的整数,若其值超出可表示范围,P4仅保留其低w位。此外,P4支持可选的饱和算术,与传统的取模算术不同。具体体现在溢出时采取的方式不同:假设一个长度为8比特的有符号运算结果的真实值为130,。我们都知道8比特长度能表示的有符号数值范围为-128~127,显然130不在范围内。对于饱和算术,该结果将会尽可能的接近真实数值,即该结果将被设为127;而对于取模算术,将130对128取模后得到-126(不同的取模运算实现方式得到的值不一样,这里是采用向上取整的方式,采用截断方式和向下取整结果是2)。同样的,对于8比特无符号数258,P4的结果为255,而不是2。
无符号整数bit<w>
类型
操作符 | 举例 | 功能描述 |
---|---|---|
负号(-) | -X | 与C++一样,其结果为2W-X |
正号(+) | +X | 等价于X |
减号(-) | a-b | 与C++一样,结果为无符号数,等价于a+(-b) |
加号(+)、乘号(*) | a+b、a*b | 与C++一样,溢出则只取低w位 |
饱和算术减 | |-| | 采用饱和算术进行减法 |
饱和算术加 | |+| | 采用饱和算术进行加法 |
按位与、或、异或、取反 | &、|、^、~ | 与C++一样 |
比较运算符 | ==、!=、>、<、>=、<= | 与C++一样 |
逻辑左移、右移 | X<<n、X>>n | 左操作数是无符号整数,右操作数必须是bit<w>类型或者一个非负整数,如果 n 大于 X 的长度,则 X 所有位都变为 0 |
提取 | X=E[L:R]、e[L:R]=x | w>L>=R>=0,将 E 的R~L(包括R和L)比特位提取出来赋值给X。X 结果是一个长度为L-R+1的无符号整数;做左值时表示将e的R~L(包括R和L)比特位设置为x,其他位不变。若x是有符号整数,则视为无符号整数。注意P4比特位编号是从右往左,从0到w-1。 |
拼接 | a++b | 将b拼接在a的后面,结果的总长度为a的长度和b的长度和,类型和符号取决于a |
有符号整数int<w>
类型
操作符 | 举例 | 功能描述 |
---|---|---|
负号(-) | -X | 与C++一样 |
正号(+) | +X | 等价于X |
加号(+)、减号(-)、乘号(*) | a+b、a-b、a*b | 与C++一样,溢出则只取低w位 |
饱和算术减 | |-| | 采用饱和算术进行减法 |
饱和算术加 | |+| | 采用饱和算术进行加法 |
按位与、或、异或、取反 | &、|、^、~ | 与C++一样 |
比较运算符 | ==、!=、>、<、>=、<= | 与C++一样 |
左移、右移 | X<<n、X>>n | 左操作数是有符号整数,右操作数必须是bit<w>类型或者非负整数,左移操作,与无符号移位操作一样;右移操作,高位补符号位 |
提取 | X=E[L:R]、e[L:R]=x | w>L>=R>=0,将 E 的R~L(包括R和L)比特位提取出来赋值给X。X 结果是一个长度为L-R+1的无符号整数;做左值时表示将e的R~L(包括R和L)比特位设置为x,其他位不变。若x是有符号整数,则视为无符号整数。注意P4比特位编号是从右往左,从0到w-1。 |
拼接 | a++b | 将b拼接在a的后面,结果的总长度为a的长度和b的长度和,类型和符号取决于a |
任意精度整数int
类型
操作符 | 举例 | 功能描述 |
---|---|---|
负号(-) | -X | 与C++一样 |
正号(+) | +X | 等价于X |
加号(+)、减号(-)、乘号(*) | a+b、a-b、a*b | 与C++一样,因为是无限精度,不会溢出,所以也不存在饱和算术 |
除号 | a/b | 正整数间的截断除法 |
取模 | a%b | 正整数间的取模运算 |
饱和运算操作和按位操作 | |+|、|-|、&、|、^、~ | 未定义 |
比较运算符 | ==、!=、>、<、>=、<= | 与C++一样 |
左移、右移 | X<<n、X>>n | 左操作数是有符号整数,右操作数必须是正整数,左移等价于X*2n,右移等价于X/2n |
提取 | X=E[L:R]、e[L:R]=x | w>L>=R>=0,将 E 的R~L(包括R和L)比特位提取出来赋值给X。X 结果是一个长度为L-R+1的无符号整数;做左值时表示将e的R~L(包括R和L)比特位设置为x,其他位不变。若x是有符号整数,则视为无符号整数。注意P4比特位编号是从右往左,从0到w-1。 |
拼接 | a++b | 将b拼接在a的后面,结果的总长度为a的长度和b的长度和,类型和符号取决于a |
不定长整数varbit<n>
类型
varbit
只支持以下操作:
- 赋值操作。a=b,a和b必须有一样的静态长度。在执行赋值的时候,a的动态长度会被设置为b的动态宽度。
- 比较是否相等。当且仅当a和b的长度和每一位的值都一样时,两者才相等。
P4的其他基础类型
基础类型 | 类型值(core.p4中定义的) |
---|---|
void | —— |
error | ParseError,PacketTooShort等 |
string | 仅允许用于表示编译时常量字符串值 |
match_kind | exact,ternary,lpm |
bool | true,false |
类型转化
显式转换
类型转换 | 说明 |
---|---|
bit<1> <-> bool | 0是false ,1为true ,反之亦然 |
int -> bool | 只有当int 的值为 0 或 1 时,对应的bool 值为false 或true
|
int<w> -> bit<w> | 所有比特位不变,把负数当做正数 |
bit<w> -> int<w> | 所有比特位不变,如果最高位为1,则视为负数 |
bit<w> -> bit<x> | 如果 w>x,保留低 x 位;如果 w<x,高位补 0 |
int<w> -> int<x> | 如果 w>x,保留低 x 位;如果 w<x,高位补符号位 |
bit<w> -> int | 所有比特位不变,结果永远为正 |
int<w> -> int | 所有比特位不变,结果可能为负 |
int -> bit<w> | 转为补码后,保留低 w 位 |
int -> int<w> | 转为补码后,保留低 w 位 |
隐式转化
为了保持语言简单并避免引入隐藏代价,P4只隐式地将int
类型转换为固定宽度类型,并将具有基础类型的enum
转换为基础类型。特别是,对int
类型的表达式和具有固定宽度类型的表达式使用二目操作,会将int
类型的操作数隐式转换为另一个操作数的类型。
Mask(&&&) 操作和 Range(..) 操作
&&&
:Mask
操作,接受两个bit<w>
类型的操作数,得到一个集合,集合中的元素类型为bit<w>
a &&& b={c | bit<w> c, c 满足a&b==c&b}
..
:Range
操作,接受两个类型为int<w>
或bit<w>
的操作数,得到一个集合,集合中的元素包括两个操作数之间的所有连续整数。
4w5..4w8 // 得到集合{4w5, 4w6, 4w7, 4w8}
3. struct、header stack 和 header union
struct Parsed_packet {
Ethernet_h ethernet;
IPv4_h ip;
}
struct
与header
类似,struct
也是一个派生类型,具体用法跟C++的结构体一样。没有任何字段的空结构体也是合法的。这里主要介绍另外两个跟header
密切相关的派生类型header stack
和header_unions
。
header stack
header stack
类似C++中的数组,该数组元素是header
类型的,例如:
header Mpls_h {
bit<20> label;
bit<3> tc;
bit bos;
bit<8> ttl;
}
Mpls_h[10] mpls;
上例中,定义了一个名为mpls
的header stack
,它包含10个元素,每个元素都是Mpls_h
类型的。
header stack的操作
假设有一个名为 hs,大小为 n 的header stack
,则 hs
有如下操作
-
hs[index]
:下标为index
的元素的引用,0<=index<n
-
hs.size
:返回hs
的大小 -
hs.nextIndex
:计数器,hs.next
初始值为 0,每当成功调用一次extract
,计数器会自动加 1。 -
hs.next
:返回下标为hs.nextIndex
的元素的引用,只能在Parser中使用。hs.next
初始指向 hs 的第一个元素,每当成功调用一次extract
,指针会自动往前推进。当hs.nextIndex
大于等于 n 时,访问hs.next
会出现error.StackOutOfBounds
的错误 -
hs.lastIndex
:返回hs.nextIndex-1
,只能在Parser中使用。 -
hs.last
:返回下标为hs.nextIndex-1
的元素的引用,只能在Parser中使用。当hs.nextIndex
等于 0 时,访问hs.last
会出现error.StackOutOfBounds
的错误 -
hs.push_front(int count)
:将hs
的元素依次右移count
个位置,原hs
的前count
个元素无效,后count
个元素被消除 -
hs.pop_front(int count)
:将hs
的元素依次左移count
个位置,原hs
的前count
个元素被消除,后count
个元素无效
header union
header_union
是多个header
类型元素的共同体,所有元素共用存储资源,并且最多只能选择其中的一个元素。例如:
header_union IP_h {
IPv4_h v4;
IPv6_h v6;
}
上例定义了一个名为IP_h
的共同体,包含两个协议首部IPv4和IPv6,但是在任何时刻,只有其中的一个协议生效。即,任何时刻,header_union
里面的Header,只有一个Header的validity
字段是true
,其余所有Header的validity
字段都是false
。
4. error
error {
IPv4OptionsNotSupported,
IPv4IncorrectVersion,
IPv4ChecksumError
}
自定义了三个错误代码,在core.p4
中也预定义了一些错误代码:
error {
NoError, // No error.
PacketTooShort, // Not enough bits in packet for 'extract'.
NoMatch, // 'select' expression has no matches.
StackOutOfBounds, // Reference to invalid element of a header stack.
HeaderTooShort, // Extracting too many bits into a varbit field.
ParserTimeout, // Parser execution time limit exceeded.
ParserInvalidArgument // Parser operation was called with a value
// not supported by the implementation.
}
除了上述用户自定义的数据结构外,通常一个P4程序还需要实现Parser、Match-Action管道和Deparser三个关键模块。在VSS中,这三个模块的功能如下:
-
Parser:需要识别的Header为Ethernet Header,其后跟着IPv4 Header。如果这两个Header有一个丢失,解析终止同时记录一个错误代码。否则,它会将这些Header中的信息提取到数据结构
Parsed_packet
中。 -
Match-Action管道:它包含四个Match-Action单元(即有四个Table),如下图所示:
-
如果有任何解析错误出现,该分组将被丢弃。实现方式为将
outCtrl.outPort
的值设置为DROP_PORT
-
第一个Table使用IPv4协议目的地址来决定
outCtrl.outPort
和下一跳的IPv4地址。如果查表失败,丢弃分组。此外,该表还减少IPv4的ttl字段值。 -
第二个Table检查ttl的值:如果ttl值为0,从CPU端口将分组发送到控制平面。
-
第三个Table使用下一跳的IPV4地址(第一个表计算的)来决定下一跳的以太网地址。
-
最后一个Table使用
outCtrl.outPort
来标识当前交换机的源以太网地址,该地址在输出分组中设置。
-
-
Deparser:通过重新组装Match-Action管道计算的以太帧Header和IPv4协议Header来构造输出分组
基于上述功能描述,下面将介绍Parser、Match-Action管道和Deparser三个关键模块的代码实现。
5. Parser
// Parser section
parser TopParser(packet_in b, out Parsed_packet p) {
Checksum16() ck; // instantiate checksum unit
state start {
b.extract(p.ethernet);
transition select(p.ethernet.etherType) {
0x0800: parse_ipv4;
// no default rule: all other packets rejected
}
}
state parse_ipv4 {
b.extract(p.ip);
verify(p.ip.version == 4w4, error.IPv4IncorrectVersion);
verify(p.ip.ihl == 4w5, error.IPv4OptionsNotSupported);
ck.clear();
ck.update(p.ip);
// Verify that packet checksum is zero
verify(ck.get() == 16w0, error.IPv4ChecksumError);
transition accept;
}
}
P4中的Parser实际上是一个有穷状态机(Finite State Machine,FSM),包含一个起始状态(start
)和两个终结状态(accept
,表示解析成功,和reject
,表示解析失败)。注意,start
状态是Parser的一部分,是程序员提供的状态,而accept
和reject
逻辑上实在Parser之外的,与程序员提供的状态不同,如下图所示。一个Parser从一个start
状态开始,直到到达一个accept
或者reject
状态为止。Architecture必须指定到达accept
或者reject
状态时的具体行为。
VSS的Parser声明如下:
parser Parser<H>(packet_in b,
out H parsedHeaders);
可以看到,该Parser接收两个参数,一个是packet_in
类型的,表示输入分组。另一个是H
类型的,表示一个已解析的Header。H
是一个泛型,不是一个具体的类型,程序员需要自己用自定义的Header类型替代。注意到在第二个参数的最前面有一个out
关键字。P4中有三个方向关键字,用以指定参数的方向属性:
in
:输入参数,该参数是只读的,不能作为左值
out
:输出参数,该参数通常是未初始化的,并且必须是左值。在执行函数调用后,参数的值被复制到该左值的相应存储位置
inout
:既可以是in
也可以是out
,必须是左值
从代码上可以看到,VSS自定义了两个状态start
和parse_ipv4
。每个状态以state
关键字标识,其后跟着状态名,然后是具体的状态体(用花括号包含)。不难发现,两个状态体*有的关键字有extract
、transition
和select
,我们将逐一介绍。
extract
extract
:前面提到在core.p4
中声明了一个内置的外部类型packet_in
,表示输入分组:
extern packet_in {
void extract<T>(out T headerLvalue);
void extract<T>(out T variableSizeHeader, in bit<32> varFieldSizeBits);
T lookahead<T>();
bit<32> length(); // This method may be unavailable in some architectures
void advance(bit<32> bits);
}
extract
是packet_in
中的一个方法。Parser通过调用extract
方法来抽取分组数据。可以看到extract
有两个变体:
void extract<T>(out T headerLeftValue);
该方法只有一个参数headerLeftValue
,用以提取长度固定的header
类型首部。如果该方法成功执行,headerLeftValue
的值将被从对应分组中提取的数据字段填充,并且validity
字段被设置为true
。例如,在VSS中的start
状态中,如果extract
方法执行成功,那么p.ethernet
的各个字段将被从分组b
中抽取的数据填充,并且p.ethernet
的隐含字段validity
将被置为true
此外,还有一种包含两个参数的变体:
void extract<T>(out T headerLvalue, in bit<32> variableFieldSize);
该方法中,headerLeftValue
必须是恰好包含一个varbit
类型的字段。第二个参数variableFieldSize
是一个bit<32>
类型的整数,表示该首部中唯一的varbit
字段的长度。下面介绍完lookahead
方法后,会举例说明如何使用具有两个参数的变体。
packet_in 中的其他方法介绍
lookahead
T lookahead<T>();
lookahead
方法与extract
类似,用以抽取数据,但不同的地方在于:
-
extract
抽取的数据填充在headerLeftValue
参数中,而lookahead
是返回所抽取的数据 -
extract
抽取完数据后,nextBitIndex
指针会往前推进,而使用lookahead
方法,nextBitIndex
指针不推进 - T 必须是固定长度的类型,即,T 不能包含
varbit
字段
穿插一点补充内容:
关于如何使用extract
的第二个变体:
前面提到,“extract
第二个变体需要两个参数,headerLeftValue
必须是恰好包含一个varbit
类型的字段。第二个参数variableFieldSize
是一个bit<32>
类型的整数,表示该首部中唯一的varbit
字段的长度”,那么varbit
字段的长度的长度该如何确定呢?换句话说,第二个参数如何确定?事实上,我们可以结合lookahead
方法,计算出该varbit
字段的长度,考虑一下例子:
header Tcp_option_sack_h {
bit<8> kind;
bit<8> length;
varbit<256> sack;
}
struct Tcp_option_sack_top {
bit<8> kind;
bit<8> length;
}
parser Tcp_option_parser(packet_in b, out Tcp_option_stack vec) {
state start {
. . . . . . .
}
. . . . . . . // 其他的state定义,不具体列出
state parse_tcp_option_sack {
bit<8> n = b.lookahead<Tcp_option_sack_top>().length;
b.extract(vec.next.sack, (bit<32>) (8 * n - 16));
transition start;
}
}
在上面的例子中,我们定义一个包含一个varbit<256> sack
字段的首部header Tcp_option_sack_h
,该首部还包含另外两个字段:kind
(表示类型)和length
(表示该首部的总长度,单位是字节)。现在如果能够确定首部Tcp_option_sack_h
的总长度 n字节,那么就可以计算出sack
的长度是n-2(字节)了。于是,我们可以按照以下步骤来做:
-
先定义一个
struct Tcp_option_sack_top
,仅包含Tcp_option_sack_h
的两个固定字段,然后用lookahead
方法提取分组中的Tcp_option_sack_top
数据。Tcp_option_sack_top
是固定长度,因此可以使用lookahead
方法。从而,我们可以得到length
字段的值,这样就得到了首部的总长度:bit<8> n = b.lookahead<Tcp_option_sack_top>().length;
-
基于此,再进一步调用
extract
方法,就可以提取出整个首部了。由于lookahead
方法不会使nextBitIndex
指针往前推进,所以对extract
方法不会产生影响。b.extract(vec.next.sack, (bit<32>) (8 * n - 16)); // n的单位是字节,第二个参数的单位是比特,因此需要进行转化
本例子讲述了如何通过lookahead
方法和extract
方法相结合的方式,提取包含varbit
字段的首部。
advance
advance
方法用以跳过bits
个比特的数据。由于上面提到extract
抽取完数据后,nextBitIndex
指针会往后推进,因此也可以使用extract
方法将数据抽取到下划线标识符上使指针往前推进,从而跳过接下来的一些比特:
b.extract<T>(_)
Parser中的 transition 和 select
transition
语句用控制状态转移,类似goto
语句。select
语句类似switch
,但是与之不同的是,在P4中,default
和_
标签后面的case是不可到达的。这意味着,select中的标签可能是可以重复的,如果重复的标签是在default
和_
标签之后,则后面的重复标签不可到达。
Parser中的 checksum 和 verify
checksum
是一个外部函数,用来计算检验和:
extern Checksum16 {
Checksum16(); // constructor
void clear(); // prepare unit for computation
void update<T>(in T data); // add data to checksum
void remove<T>(in T data); // remove data from existing checksum
bit<16> get(); // get the checksum for the data added since last clear
}
verify
也是一个外部函数,只能在Parser中调用:
extern void verify(in bool condition, in error err);
如果condition
是true
,该方法没有任何作用。如果condition
是false
,则会立即transition
到reject
状态,设置解析错误代码为err
(第二个参数)
6. Match-Action Pipeline
// Match-action pipeline section
control TopPipe(inout Parsed_packet headers,
in error parseError, // parser error
in InControl inCtrl, // input port
out OutControl outCtrl) {
IPv4Address nextHop; // local variable
/***************table 1***************/
action Drop_action() {
. . . . . .
}
action Set_nhop(IPv4Address ipv4_dest, PortId port) {
. . . . . .
}
table ipv4_match {
. . . . . .
}
/***************table 2***************/
action Send_to_cpu() {
. . . . . .
}
table check_ttl {
. . . . . .
}
/***************table 3***************/
action Set_dmac(EthernetAddress dmac) {
. . . . . .
}
table dmac {
. . . . . .
}
/***************table 4***************/
action Set_smac(EthernetAddress smac) {
. . . . . .
}
table smac {
. . . . . .
}
apply {
. . . . . .
}
}
Parser负责从分组中提取首部数据。这些首部(和其他的Metadata)可以在控制模块中进行操作和转换。实现控制模块功能的主体是Match-Action Units,其核心组件是table
和action
。
action
是控制平面可以动态影响数据平面行为的主要结构:
action actionName(parameterList){动作主体}
参数列表中,没有方向属性的参数称为“action data”,这些参数需要放在参数列表的末尾,并且这些参数值来自表项(table entries)(例如,由控制平面指定、默认的default_action
属性或者const entries
属性)。动作主体用花括号包含,是一些列的语句或者申明,但是不能有switch
语句。
table
描述了一个Match-Action Unit,执行以下步骤(如下图所示):
-
构造
Key
-
查表。表中通过
key
来查找表项(match的过程),查找的结果是一个action
。lookup table是一个有限映射,其内容由控制平面通过单独的控制平面API进行异步操作(读/写)。 -
执行对应的
action
一个标准的table需要包含key
和action
属性,可选的包含default_action
和size
属性:
-
key
是形如(expression:match_kind)
的一对二元组。expression
指定key
的内容,match_kind
指定key
的匹配方式。P4核心库定义了三种匹配方式:exact
(完全匹配)、lpm
(最长前缀匹配)和ternary
(三元匹配,即匹配某些指定的比特位,例如用mask
操作指定一些比特位,可参见下面例子中的注释)。 -
default_action
,当查表失败时,执行default_action
指定的动作。如果没有显示的指定default_action
,编译器会默认为每个table指定default_action
为NoAction
。 -
size
是一个整数,指定该table所需的大小。 -
const entries
。前面提到,table entries可以由const entries
指定。我们可以事先定义一些表项,用以在编译时初始化表项,下面这个例子分别使用exact
和ternary
方式进行两个key
匹配,同时用const entries
指定了一些常量表项。同时,在常量表项中用注释解释了三元匹配:header hdr { bit<8> e; bit<16> t; bit<8> l; bit<8> r; bit<1> v; } struct Header_t { hdr h; } struct Meta_t {} control ingress(inout Header_t h, inout Meta_t m, inout standard_metadata_t standard_meta) { action a() { standard_meta.egress_spec = 0; } action a_with_control_params(bit<9> x) { standard_meta.egress_spec = x; } table t_exact_ternary { key = { h.h.e : exact; h.h.t : ternary; } actions = { a; a_with_control_params; } default_action = a; const entries = { /** 以0x1111 &&& 0xF为例,解释一下三元匹配 * 0001 0001 0001 0001 & 0000 0000 0000 1111 = 0000 0000 0000 0001 * xxxx xxxx xxxx 0001 & 0000 0000 0000 1111 = 0000 0000 0000 0001 * 这意味着,只要低4位是0001,高12位不管是什么都可以匹配成功 * 这么匹配有可能会匹配到多个结果,用户需要为每个表项定一个优先级, * 当存在多个匹配时,根据用户定义好的优先级进行选择,最终只能匹配一个结果*/ (0x01, 0x1111 &&& 0xF ) : a_with_control_params(1); (0x02, 0x1181 ) : a_with_control_params(2); (0x03, 0x1111 &&& 0xF000) : a_with_control_params(3); (0x04, 0x1211 &&& 0x02F0) : a_with_control_params(4); (0x04, 0x1311 &&& 0x02F0) : a_with_control_params(5); (0x06, _ ) : a_with_control_params(6); _ : a; } } }
-
当定义好 Match-action Units后,还需要使用
apply
来调用
下面将分别介绍每个table以及所涉及到的action实现
7. the first table in VSS
/***************table 1***************/
action Drop_action() {
outCtrl.outputPort = DROP_PORT;
}
action Set_nhop(IPv4Address ipv4_dest, PortId port) {
nextHop = ipv4_dest;
headers.ip.ttl = headers.ip.ttl - 1;
outCtrl.outputPort = port;
}
table ipv4_match {
key = { headers.ip.dstAddr: lpm; } // longest-prefix match
actions = {
Drop_action;
Set_nhop;
}
size = 1024;
default_action = Drop_action;
}
第一个table使用IPv4协议目的地址来决定outCtrl.outPort
和下一跳的IPv4地址。如果查表失败,丢弃分组。此外,该表还减少IPv4的ttl字段值。
为了实现上述功能,首先定义了两个action
。Drop_action
通过把outCtrl.outPort
设为DROP_PORT
来指示解复用模块应该丢弃该分组。Set_nhop
接收两个参数,通过参数ipv4_dest
来获取IPv4协议目的地址,通过参数port
来指定输出端口号outCtrl.outPort
。Set_nhop
用自定义的nexthop
局部变量来表示下一跳的IPv4地址。同时,还应该将首部的ttl
字段值减 1。ipv4_dest
和port
没有方向属性,都是“action data”,由表项提供。
table
的主体包含了key
、actions
、size
和default_action
属性。key
指定根据首部的dstAddr
字段来查表,匹配方式是lpm(最长前缀匹配)。actions
指定匹配的结果有Drop_action
和Set_nhop
两种。size
指定table的大小为1024字节。default_action
指定默认动作为Drop_action
。
8. the second table in VSS
action Send_to_cpu() {
outCtrl.outputPort = CPU_OUT_PORT;
}
table check_ttl {
key = { headers.ip.ttl: exact; }
actions = {
Send_to_cpu;
NoAction;
}
const default_action = NoAction; // defined in core.p4
}
第二个Table检查ttl
字段的值:如果ttl
值为0,从CPU端口将分组发送到控制平面。
为了实现上述功能,首先定义了一个Send_to_cpu
动作,把outCtrl.outPort
设为CPU_OUT_PORT
来指示解复用模块应该将该分组发送到控制平面。
table
的主体包含了key
、actions
和default_action
属性。key
指定根据首部的ttl
字段来查表,匹配方式是exact(完全匹配)。actions
指定匹配的结果有Send_to_cpu
和NoAction
两种。NoAction
是核心库中定义的一个动作,没有做任何具体的操作。default_action
指定默认动作为NoAction
。
9. the third table in VSS
action Set_dmac(EthernetAddress dmac) {
headers.ethernet.dstAddr = dmac;
}
table dmac {
key = { nextHop: exact; }
actions = {
Drop_action;
Set_dmac;
}
size = 1024;
default_action = Drop_action;
}
第三个Table使用下一跳的IPV4地址(第一个表计算的)来决定下一跳的以太网地址。
为了实现上述功能,首先定义了一个Set_dmac
动作。Set_dmac
接收一个参数,通过参数dmac
来获取下一跳的以太网地址(ethernet首部的dstAddr
字段)。dmac
没有方向属性,是“action data”,由表项提供。
table
的主体包含了key
、actions
、size
和default_action
属性。key
指定根据nexthop
来查表,匹配方式是exact(完全匹配)。actions
指定匹配的结果有Drop_action
和Set_dmac
两种。size
指定table的大小为1024字节。default_action
指定默认动作为Drop_action
。
10. the last table in VSS
action Set_smac(EthernetAddress smac) {
headers.ethernet.srcAddr = smac;
}
table smac {
key = { outCtrl.outputPort: exact; }
actions = {
Drop_action;
Set_smac;
}
size = 16;
default_action = Drop_action;
}
最后一个Table使用outCtrl.outPort
来标识当前交换机的源以太网地址,该地址在输出分组中设置。
为了实现上述功能,首先定义了一个Set_smac
动作。Set_smac
接收一个参数,通过参数smac
来获取源以太网地址(ethernet首部的srcAddr
字段)。smac
没有方向属性,是“action data”,由表项提供。
table
的主体包含了key
、actions
、size
和default_action
属性。key
指定根据outCtrl.outPort
来查表,匹配方式是exact(完全匹配)。actions
指定匹配的结果有Drop_action
和Set_smac
两种。size
指定table的大小为16字节。default_action
指定默认动作为Drop_action
。
11. apply
apply {
if (parseError != error.NoError) {
Drop_action(); // invoke drop directly
return;
}
ipv4_match.apply(); // Match result will go into nextHop
if (outCtrl.outputPort == DROP_PORT)
return;
check_ttl.apply();
if (outCtrl.outputPort == CPU_OUT_PORT)
return;
dmac.apply();
if (outCtrl.outputPort == DROP_PORT)
return;
smac.apply();
}
在实现了上述Match-Action Units后,还需要使用apply
来调用。实现方式是通过apply
关键字和一个大括号包含的主体。在主体中,每个table各自调用apply
方法。
在VSS的apply
主体中,首先,如果有任何解析错误出现,该分组将被丢弃。实现方式为将outCtrl.outPort
的值设置为DROP_PORT
。然后,如果没有出现解析错误,则依次调用上述四个table。通过if
语句和return
语句来控制是否继续调用接下来的table。
12. Deparser
// deparser section
control TopDeparser(inout Parsed_packet p, packet_out b) {
Checksum16() ck;
apply {
b.emit(p.ethernet);
if (p.ip.isValid()) {
ck.clear(); // prepare checksum unit
p.ip.hdrChecksum = 16w0; // clear checksum
ck.update(p.ip); // compute new checksum.
p.ip.hdrChecksum = ck.get();
}
b.emit(p.ip);
}
}
Deparser是Parser的逆过程,Deparser也是一个control
块,至少包含一个packet_out
参数,表示输出分组。Deparser主体包含一个apply
块,在apply
中调用emit
方法来构造分组。emit
是packet_out
的一个方法:
- 当
emit
应用于一个首部时,如果首部是有效的,则把首部数据加到分组里。否则,不进行任何操作。 - 当
emit
应用于一个header stack
时,emit
对header stack
内的每个元素递归地调用自身。 - 当应用于一个
struct
或者header union
时,emit
对每个字段递归调用自身。注意struct
不能包含error
或enum
字段。
13. package
// Instantiate the top-level VSS package
VSS(TopParser(),
TopPipe(),
TopDeparser()) main;
最后,实例化一个package
。VSS的package
声明如下:
package VSS<H> (Parser<H> p,
Pipe<H> map,
Deparser<H> d);
代码附录
1. VSS Architecture的声明
very_simple_switch_model.p4
// File "very_simple_switch_model.p4"
// Very Simple Switch P4 declaration
// core library needed for packet_in and packet_out definitions
# include <core.p4>
/* Various constants and structure declarations */
/* ports are represented using 4-bit values */
typedef bit<4> PortId;
/* only 8 ports are "real" */
const PortId REAL_PORT_COUNT = 4w8; // 4w8 is the number 8 in 4 bits
/* metadata accompanying an input packet */
struct InControl {
PortId inputPort;
}
/* special input port values */
const PortId RECIRCULATE_IN_PORT = 0xD;
const PortId CPU_IN_PORT = 0xE;
/* metadata that must be computed for outgoing packets */
struct OutControl {
PortId outputPort;
}
/* special output port values for outgoing packet */
const PortId DROP_PORT = 0xF;
const PortId CPU_OUT_PORT = 0xE;
const PortId RECIRCULATE_OUT_PORT = 0xD;
/* Prototypes for all programmable blocks */
/*** Programmable parser.
* @param <H> type of headers; defined by user
* @param b input packet
* @param parsedHeaders headers constructed by parser
*/
parser Parser<H>(packet_in b,
out H parsedHeaders);
/*** Match-action pipeline
* @param <H> type of input and output headers
* @param headers headers received from the parser and sent to the deparser
* @param parseError error that may have surfaced during parsing
* @param inCtrl information from architecture, accompanying input packet
* @param outCtrl information for architecture, accompanying output packet
*/
control Pipe<H>(inout H headers,
in error parseError,// parser error
in InControl inCtrl,// input port
out OutControl outCtrl); // output port
/*** VSS deparser.
* @param <H> type of headers; defined by user
* @param b output packet
* @param outputHeaders headers for output packet
*/
control Deparser<H>(inout H outputHeaders,
packet_out b);
/*** Top-level package declaration - must be instantiated by user.
* The arguments to the package indicate blocks that
* must be instantiated by the user.
* @param <H> user-defined type of the headers processed.
*/
package VSS<H>(Parser<H> p,
Pipe<H> map,
Deparser<H> d);
// Architecture-specific objects that can be instantiated
// Checksum unit
extern Checksum16 {
Checksum16(); // constructor
void clear(); // prepare unit for computation
void update<T>(in T data); // add data to checksum
void remove<T>(in T data); // remove data from existing checksum
bit<16> get(); // get the checksum for the data added since last clear
}
2. 完整的 VSS 代码
complete VSS program
// Include P4 core library
# include <core.p4>
// Include very simple switch architecture declarations
# include "very_simple_switch_model.p4"
// This program processes packets comprising an Ethernet and an IPv4
// header, and it forwards packets using the destination IP address
typedef bit<48> EthernetAddress;
typedef bit<32> IPv4Address;
// Standard Ethernet header
header Ethernet_h {
EthernetAddress dstAddr;
EthernetAddress srcAddr;
bit<16> etherType;
}
// IPv4 header (without options)
header IPv4_h {
bit<4> version;
bit<4> ihl;
bit<8> diffserv;
bit<16> totalLen;
bit<16> identification;
bit<3> flags;
bit<13> fragOffset;
bit<8> ttl;
bit<8> protocol;
bit<16> hdrChecksum;
IPv4Address srcAddr;
IPv4Address dstAddr;
}
// Structure of parsed headers
struct Parsed_packet {
Ethernet_h ethernet;
IPv4_h ip;
}
// User-defined errors that may be signaled during parsing
error {
IPv4OptionsNotSupported,
IPv4IncorrectVersion,
IPv4ChecksumError
}
// Parser section
parser TopParser(packet_in b, out Parsed_packet p) {
Checksum16() ck; // instantiate checksum unit
state start {
b.extract(p.ethernet);
transition select(p.ethernet.etherType) {
0x0800: parse_ipv4;
// no default rule: all other packets rejected
}
}
state parse_ipv4 {
b.extract(p.ip);
verify(p.ip.version == 4w4, error.IPv4IncorrectVersion);
verify(p.ip.ihl == 4w5, error.IPv4OptionsNotSupported);
ck.clear();
ck.update(p.ip);
// Verify that packet checksum is zero
verify(ck.get() == 16w0, error.IPv4ChecksumError);
transition accept;
}
}
// Match-action pipeline section
control TopPipe(inout Parsed_packet headers,
in error parseError, // parser error
in InControl inCtrl, // input port
out OutControl outCtrl) {
IPv4Address nextHop; // local variable
/*** Indicates that a packet is dropped by setting the
* output port to the DROP_PORT
*/
action Drop_action() {
outCtrl.outputPort = DROP_PORT;
}
/*** Set the next hop and the output port.
* Decrements ipv4 ttl field.
* @param ivp4_dest ipv4 address of next hop
* @param port output port
*/
action Set_nhop(IPv4Address ipv4_dest, PortId port) {
nextHop = ipv4_dest;
headers.ip.ttl = headers.ip.ttl - 1;
outCtrl.outputPort = port;
}
/*** Computes address of next IPv4 hop and output port
* based on the IPv4 destination of the current packet.
* Decrements packet IPv4 TTL.
* @param nextHop IPv4 address of next hop
*/
table ipv4_match {
key = { headers.ip.dstAddr: lpm; } // longest-prefix match
actions = {
Drop_action;
Set_nhop;
}
size = 1024;
default_action = Drop_action;
}
/*** Send the packet to the CPU port
*/
action Send_to_cpu() {
outCtrl.outputPort = CPU_OUT_PORT;
}
/*** Check packet TTL and send to CPU if expired.
*/
table check_ttl {
key = { headers.ip.ttl: exact; }
actions = {
Send_to_cpu;
NoAction;
}
const default_action = NoAction; // defined in core.p4
}
/*** Set the destination MAC address of the packet
* @param dmac destination MAC address.
*/
action Set_dmac(EthernetAddress dmac) {
headers.ethernet.dstAddr = dmac;
}
/*** Set the destination Ethernet address of the packet
* based on the next hop IP address.
* @param nextHop IPv4 address of next hop.
*/
table dmac {
key = { nextHop: exact; }
actions = {
Drop_action;
Set_dmac;
}
size = 1024;
default_action = Drop_action;
}
/*** Set the source MAC address.
* @param smac: source MAC address to use
*/
action Set_smac(EthernetAddress smac) {
headers.ethernet.srcAddr = smac;
}
/*** Set the source mac address based on the output port.
*/
table smac {
key = { outCtrl.outputPort: exact; }
actions = {
Drop_action;
Set_smac;
}
size = 16;
default_action = Drop_action;
}
apply {
if (parseError != error.NoError) {
Drop_action(); // invoke drop directly
return;
}
ipv4_match.apply(); // Match result will go into nextHop
if (outCtrl.outputPort == DROP_PORT)
return;
check_ttl.apply();
if (outCtrl.outputPort == CPU_OUT_PORT)
return;
dmac.apply();
if (outCtrl.outputPort == DROP_PORT)
return;
smac.apply();
}
}
// deparser section
control TopDeparser(inout Parsed_packet p, packet_out b) {
Checksum16() ck;
apply {
b.emit(p.ethernet);
if (p.ip.isValid()) {
ck.clear(); // prepare checksum unit
p.ip.hdrChecksum = 16w0; // clear checksum
ck.update(p.ip); // compute new checksum.
p.ip.hdrChecksum = ck.get();
}
b.emit(p.ip);
}
}
// Instantiate the top-level VSS package
VSS(TopParser(),
TopPipe(),
TopDeparser()) main;
References
2、P4语言的特性、P4语言和P4交换机的工作原理和流程简介