其他分享
首页 > 其他分享> > P4-16 Specifications(v1.2.2)学习笔记

P4-16 Specifications(v1.2.2)学习笔记

作者:互联网

本文是学习2021年5月最新的P416语言规范(P416 Language Specification)时所做的学习笔记,以下将P416语言规范简称为规范。同时,为了避免因翻译而导致的问题,文中用加粗的原英文来表达部分术语,翻译成中文的术语也将加粗标记。

目录

术语

P4简介

P4是一种语言,全称是Programming Protocol-independent Packet Processors编程协议无关的分组处理器),用于表示可编程转发元件(如硬件或软件交换机、网络接口卡、路由器或网络设备)的数据平面如何处理Packet。P4仅用于指定Target数据平面功能以及部分控制平面数据平面通信的接口,但P4不能用来描述Target控制平面功能。

P4可编程交换机与传统交换机的区别

核心抽象

用P4编程Target时的一个典型工具工作流如下图所示

Target制造商为Target提供硬件或软件实现框架、Architecture定义和P4编译器。P4程序员为特定的Architecture编写程序(图中绿色区块),该Architecture定义了Target上的一组P4可编程组件及其外部数据平面接口。

编译一组P4程序会产生两个工件:

BNF范式(巴科斯范式)

规范中使用BNF范式来给出P4的语法,因此,在介绍具体语法之前,先简单描述一下BNF范式。本部分内容不是规范中介绍的内容。

巴科斯范式是以美国人巴科斯(Backus)和丹麦人诺尔(Naur)的名字命名的一种形式化的语法表示方法,用来描述语法的一种形式体系,是一种典型的元语言,又称巴科斯-诺尔形式(Backus-Naur Form,BNF)。它不仅能严格地表示语法规则,而且所描述的语法是与上下文无关的。

BNF表示语法规则的方式为:

规范中,所使用的BNF表示与上述略有不同:

如上述例子表示的是,一个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 PipelineMatch-Action Pipeline输出到唯一的一个Deparser,在退出Deparser后,分组被传送到11个出端口(8个以太网出端口+3个特殊端口)中的某一个端口。3个特殊端口分别为:

图中白色的模块是可编程的部分,用户必须提供对应的P4程序来指定每个白色模块的行为。红色箭头表示用户自定义数据。青色区块是功能固定的组件。绿色箭头是数据平面接口,用来在功能固定的模块和可编程模块之间传递信息——在P4程序中作为Intrinsic Metadata

VSS Architecture 固定功能模块

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_inpacket_out外部对象的声明,这些对象在ParserDeparser中用于访问分组数据。同时,核心库还定义了一些标准数据类型和错误代码。

very_simple_switch_model.p4是VSS Architecture的声明文件,包含了对ParserDeparserPackage等不可缺少的重要组件的声明。very_simple_switch_model.p4的部分声明(VSS涉及到的)将附在文末。

与C++类似,关键字typedef可以给类型取一个别名:

typedef typeName newName;

上述语句给typeName类型起了一个别名newName。在使用上,newNametypeName完全一样,两者只是名字不一样,实际上是同一个东西。

typedef有明显区别的是关键字type,它用以引入一个全新的类型。

type typeName newType;

上述语句引入了一种全新的类型,注意newTypetypeName是两种不同的类型。如果这两种类型要互相赋值,则需要强制类型转换:

type bit<32> U32;
U32 x = (U32)0;

在描述需要通过通信信道(比如控制平面API或者要被送到控制平面的网络分组)与控制平面进行交换的P4值时,通常使用type关键字。例如:

type bit<9> PortId_t;

上述语句定义了一个全新的类型PortId_t,用以表示长度为 9 比特的端口号。这样可以避免端口号被进行算数运算,因为新引入的类型PortId_t不支持算术运算(尽管bit<9>类型支持算数运算,但他们是不同的两种类型)。

注意,并不是所有类型都支持typedeftype关键字:

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字段进行操作:

如上面的代码所示,VSS定义了两个首部的格式:以太帧首部IPv4协议首部bit<n>是基本数据类型中的Integer类型。Integer是整数类型的统称,不是一个标准的基本数据类型,根据有符号和无符号,定长和不定长,分为以下几种形式:

基础类型符号 说明
bit<w> 表示长度为 w 比特的无符号整数,也叫bit-string
int<w> 表示长度为 w 比特的有符号整数
varbit<n> 长度最多w 比特的不定长无符号整数
int 无限精度有符号整数
Integer的字面值

一个整数值可能包含某些表示整数类型的前缀:

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只支持以下操作:

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值为falsetrue
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 stackheader_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;

上例中,定义了一个名为mplsheader stack,它包含10个元素,每个元素都是Mpls_h类型的。

header stack的操作

假设有一个名为 hs,大小为 n 的header stack,则 hs 有如下操作

header union

header_union是多个header类型元素的共同体,所有元素共用存储资源,并且最多只能选择其中的一个元素。例如:

header_union IP_h {
	IPv4_h v4;
	IPv6_h v6;
}

上例定义了一个名为IP_h的共同体,包含两个协议首部IPv4和IPv6,但是在任何时刻,只有其中的一个协议生效。即,任何时刻,header_union里面的Header,只有一个Headervalidity字段是true,其余所有Headervalidity字段都是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程序还需要实现ParserMatch-Action管道和Deparser三个关键模块。在VSS中,这三个模块的功能如下:

基于上述功能描述,下面将介绍ParserMatch-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的一部分,是程序员提供的状态,而acceptreject逻辑上实在Parser之外的,与程序员提供的状态不同,如下图所示。一个Parser从一个start状态开始,直到到达一个accept或者reject状态为止。Architecture必须指定到达accept或者reject状态时的具体行为。

VSS的Parser声明如下:

parser Parser<H>(packet_in b, 
                out H parsedHeaders);

可以看到,该Parser接收两个参数,一个是packet_in类型的,表示输入分组。另一个是H类型的,表示一个已解析的HeaderH是一个泛型,不是一个具体的类型,程序员需要自己用自定义的Header类型替代。注意到在第二个参数的最前面有一个out关键字。P4中有三个方向关键字,用以指定参数的方向属性:

in:输入参数,该参数是只读的,不能作为左值

out:输出参数,该参数通常是未初始化的,并且必须是左值。在执行函数调用后,参数的值被复制到该左值的相应存储位置

inout:既可以是in也可以是out,必须是左值

从代码上可以看到,VSS自定义了两个状态startparse_ipv4。每个状态以state关键字标识,其后跟着状态名,然后是具体的状态体(用花括号包含)。不难发现,两个状态体中共有的关键字有extracttransitionselect,我们将逐一介绍。

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);
}

extractpacket_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的第二个变体:

前面提到,“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(字节)了。于是,我们可以按照以下步骤来做:

本例子讲述了如何通过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);

如果conditiontrue,该方法没有任何作用。如果conditionfalse,则会立即transitionreject状态,设置解析错误代码为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,其核心组件是tableaction

action控制平面可以动态影响数据平面行为的主要结构:

action actionName(parameterList){动作主体}

参数列表中,没有方向属性的参数称为“action data”,这些参数需要放在参数列表的末尾,并且这些参数值来自表项(table entries)(例如,由控制平面指定、默认的default_action属性或者const entries属性)。动作主体用花括号包含,是一些列的语句或者申明,但是不能有switch语句。

table描述了一个Match-Action Unit,执行以下步骤(如下图所示):

一个标准的table需要包含keyaction属性,可选的包含default_actionsize属性:

下面将分别介绍每个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字段值。

为了实现上述功能,首先定义了两个actionDrop_action通过把outCtrl.outPort设为DROP_PORT来指示解复用模块应该丢弃该分组Set_nhop接收两个参数,通过参数ipv4_dest来获取IPv4协议目的地址,通过参数port来指定输出端口号outCtrl.outPortSet_nhop用自定义的nexthop局部变量来表示下一跳的IPv4地址。同时,还应该将首部ttl字段值减 1。ipv4_destport没有方向属性,都是“action data”,由表项提供。

table的主体包含了keyactionssizedefault_action属性。key指定根据首部dstAddr字段来查表,匹配方式是lpm(最长前缀匹配)。actions指定匹配的结果有Drop_actionSet_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的主体包含了keyactionsdefault_action属性。key指定根据首部的ttl字段来查表,匹配方式是exact(完全匹配)。actions指定匹配的结果有Send_to_cpuNoAction两种。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的主体包含了keyactionssizedefault_action属性。key指定根据nexthop来查表,匹配方式是exact(完全匹配)。actions指定匹配的结果有Drop_actionSet_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的主体包含了keyactionssizedefault_action属性。key指定根据outCtrl.outPort来查表,匹配方式是exact(完全匹配)。actions指定匹配的结果有Drop_actionSet_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);
    } 
}

DeparserParser的逆过程,Deparser也是一个control块,至少包含一个packet_out参数,表示输出分组Deparser主体包含一个apply块,在apply中调用emit方法来构造分组emitpacket_out的一个方法:

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

1、P416 Language Specification

2、P4语言的特性、P4语言和P4交换机的工作原理和流程简介

3、Hello World in P4

4、巴科斯范式-百度百科

标签:P4,16,Specifications,packet,分组,action,table,bit
来源: https://www.cnblogs.com/jackinhu/p/14949751.html