Featured image of post P4基础编程语法

P4基础编程语法

新一代的SDN解决方案必须让数据转发平面也具有可编程能力,让软件能够真正定义网络和网络设备,而P4为用户提供了这种能力

P4概述

P4 作为一个编程语言,有两个版本:P4_14和P4_16,P4_16 实现的模型基本等同于 PISA,叫做v1model

  1. Parser:解析数据包

  2. Match-Action的流水线

    • Ingress:Match-Action的流水线

    • Switching Logic:交换逻辑(此处设计有crossbar、buffer等,存放刚被上一个环节处理完,准备给下一个环节处理的包)

    • Egress:Match-Action的流水线

  3. Deparser:把改写好的headers重新封装,然后发送出去

基础语言组件

P4程序主要由五个组件构成:Header、Parser、Table、Action和Controller

  1. 首部:定义报文头部格式

  2. 解析器:定义数据包解析流程的有限状态机

  3. 表:定义匹配域以及对应的执行动作

  4. 动作:动作指令集,包括构造查找键、根据查找键查表、执行动作等

  5. 流控制程序:控制程序,决定了数据包处理的流程,比如如何在不同表之间跳转等

main函数

1
2
3
4
5
6
7
8
V1Switch(  
    MyParser(),  
    MyVerifyChecksum(),  
    MyIngress(),  
    MyEgress(),  
    MyComputeChecksum(),  
    MyDeparser()  
) main;

Header分为两种:一种是包头(Packet Header),一种是元数据(Metadata)

Packet Header

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef bit<9>  egressSpec_t;  
typedef bit<48> macAddr_t;  
typedef bit<32> ip4Addr_t;  
header ethernet_t {  
    macAddr_t dstAddr;  
    macAddr_t srcAddr;  
    bit<16>   etherType;  
}  
header ipv4_t {  
    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;  
    ip4Addr_t srcAddr;  
    ip4Addr_t dstAddr;  
}

当收到了一个packet,要把它的Ethernet header解析出来,使用如下语句:

packet.extract(ethernetHeader);

P4定义的header中含有一个隐藏的field叫做validity:如果解析成功的话,validity 就会被自动设置为true

元数据

元数据用来携带数据和配置信息,元数据的声明与包头类似,但在实例化时不同,而且包头和元数据在字段值的约束上存在一定的差别

元数据分为两种:

  1. 一种是用来携带P4程序运行过程中产生的数据的用户自定义元数据(User-Defined Metadata),如首部字段的运算结果等

  2. 一种是固有元数据(Intrinsic Metadata),用于携带交换机自身的配置信息,如数据包进入交换机时的端口号等

有以下固定元数据

字段 描述
ingress_port 数据包的入端口,解析之前设置,只读
packet_length 数据包的字节数,当交换机在快速转发模式下,该元数据不能在动作(action)中匹配或引用,只读
egress_spec 在入端口流水线的匹配-动作过程之后设置,指定数据包出端口,可以是物理端口、逻辑端口或者多播组
egress_instance 用于区分复制后数据包实例的标识符,只读
instance_type 数据包实例类型:正常(Normal)、入端口复制(ingress clone)、出端口复制(egress clone)、再循环(recirculated)
parser_sratus 解析器解析结果,0表示无错误,其实数字代表了对应的错误类型
parser_error_location 指向P4程序错误发生处
(该表格可能过时,以实际为准)

注意standard_metadata使用了struct的数据结构,该结构类似于Python中的字段,其中的成员是无序的

实例化

在首部区域定义结构后,需要进行实例化操作

1
2
3
4
struct headers {  
    ethernet_t   ethernet;  
    ipv4_t       ipv4;  
}

Parser

==P4语言中解析器采用有限状态机的设计思路,每个解析器方法视为一种状态。==当解析器工作时,会将当前处理的数据包头字节的偏移量记录在首部实例中,并在状态迁移(调用另一个解析器)时指向包头中下一个待处理的有效字节

一个P4程序中往往定义了大量的首部和首部实例,但并不是所有的首部实例都会对数据包进行操作。解析器工作时会生成描述数据包进行哪些匹配+动作操作的中间表示,在P4中称之为==解析后表示==,这些解析后表示规定了对数据包生效的实例,是一组对数据包生效的实例的集合

parser函数的起始以start开始,以accept或reject结束。accept表示解析成功、reject表示解析失败

定义parser函数时,参数中至少有一个参数packet_in,用于表示处理的接收数据包

对于其中常用的关键字语法:

  • extract:将目前的packet以特定的header取出来,取出来的各部分长度以header定义的为主

  • select:类似于C语言中的switch-case语法,取决于参数通过transition关键字决定接下来的跳转方向(函数的选择操作按列表中的顺序比较字段值和左端列出的比特组对应的值,如果没有发现匹配项,则对应到default项)

  • transition:在不同的state之间切换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 parser MyParser(packet_in packet,  
                out headers hdr,  
                inout metadata meta,  
                inout standard_metadata_t standard_metadata) {  
    state start {  
        transition parse_ethernet;  
    }  
    state parse_ethernet {  
        packet.extract(hdr.ethernet);  
        transition select(hdr.ethernet.etherType) {  
            TYPE_IPV4: parse_ipv4;  
            default: accept;  
        }  
    }  
    state parse_ipv4 {  
        packet.extract(hdr.ipv4);  
        transition accept;  
    }  
}

Match-Action

Tables

匹配方式 功能 示例
exact core.p4 精确匹配 0x01020304
lpm core.p4 最长前缀匹配 0x01020304/24
ternary core.p4 与掩码与运算 0x01020304 & 0x0F0F0F0F
range v1model.p4 检查是否在范围内 0x01020304 ~ 0x010203FF

Actions

Action会有输入、输出的值,因此会传入带有方向性的参数

  1. 有向参数
  •   in:在一个action里是只读的,类似于Python中的函数输入值
  •   out:在一个action里是可以被改写的,类似于Python中的函数返回值
  •   inout:同时作为输入和输出值
  1. 无向参数:不像上面三类都是有方向性的,还有一种是从Table里查询得到的结果,这个参数不需要定义
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 有向参数示例  
// 数据包的镜像:数据包从来的 就送回哪去  
action reflect_packet(  
  inout bit<48> src,  
  inout bit<48> dst,  
  in bit<9> inPort,  
  out bit<9> outPort  
) {  
  // 交换源地址和目标地址  
  bit<48> tmp = src;   
  src = dst;  
  dst = tmp;  
  // 设置出口  
  outPort = inPort;  
}  
reflect_packet(  
  hdr.ethernet.srcAddr,  
  hdr.ethernet.dstAddr,  
  standard_metadata.ingress_port,  
  standard_metadata.egress_spec  
)  
// 无向参数示例  
// port是从Table里查到的 就不需要标明方向  
action set_egress_port(bit<9> port) {  
  standard_metadata.egress_spec = port;  
}

Control Flow

控制流指:要是在Table中查询到目标,就执行一个动作。主要有三类操作:

  1. 使用定义好的表:ipv4_lpm.apply();

  2. 查询是否hit:if (ipv4_lpm.apply().hit) {...} else {...}

  3. 查看执行的是哪一个动作:switch (ipv4_lpm.apply().action_run) { ipv4_forward: {...} }

以网络层转发为例,一个网络包被转发的过程,可以用两个表来实现:第一个表 ipv4_lpm 用来把IP地址映射到下一跳的ID,第二个表 forward用来把下一跳的ID映射到出口端口号

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
control MyIngress(...) {  
  /* 函数部分 */  
  action drop() {...}               // 定义丢掉packet的动作  
  action set_nhop_index(...) {...}  // 定义设置下一跳对应ID的动作  
  action _forward(...) {...}        // 定义转发的动作  
  /* 表 */  
  table ipv4_lpm {  
    key = {                         
      hdr.ipv4.dstAddr: lpm;        // 设置最长前缀匹配  
    }  
    actions = { // 设置用到的动作集  
      set_nhop_index;  
      drop;  
      NoAction;  
    }  
    size = 1024;     // 表中能容纳的最大entries数量  
    default_action = NoAction();     // 定义默认的动作为无动作  
  }   
  table forward {  
    key = {  
      meta.nhop_index: exact;  
    }  
    actions = {  
      _forward;  
      NoAction;  
    }  
    size = 64;  
    default_action = NoAction();  
  }  
  /* 控制逻辑 */  
  apply {  
    // header数据类型自带隐藏参数 用于判断header格式是否正确  
    if (hdr.ipv4.isValid()) {  
      // 应用ipv4_lpm这个表 并且检查是否hit  
      if (ipv4_lpm.apply().hit) {  
        // 若匹配成功 则应用forward这个表  
        forward.apply();  
      }  
    }  
  }  
}

Deparser

Deparser是Parser的逆过程,使用emit函数

1
2
3
4
5
6
7
control MyDeparser(...) {  
  apply {  
    packet.emit(hdr.ethernet);  
    packet.emit(hdr.ipv4);  
    packet.emit(hdr.tcp);  
  }  
}

附录

基本数据类型

P4不支持float与string

1
2
3
4
5
6
7
bool        // Boolean value  
bit<W>      // Bit-string of width W  
int<W>      // Signed integer of width W  
varbit<W>   // Bit-string of dynamic length <= W  
match_kind  // Describes ways to match table keys  
error       // Used to signal errors  
void        // No values, used in few restricted circumstances

高级数据类型

Header

(在上文已说明)

Header stack

例如MPLS可能有多个label

1
2
3
4
5
6
7
8
9
header Mpls_h {  
  bit<20> label;  
  bit<3>  tc;  
  bit     bos;  
  bit<8>  ttl;  
}  
  
// 10个MPLS headers  
Mpls_h[10] mols;

Header union

例如IP header要么是v4,要么是v6

1
2
3
4
5
// 只用其中的一种  
header_union IP_h {  
  IPv4_h v4;  
  IPv6_h v6;  
} 

Header struct

(在上文已说明)

P4 的基本语法

P4 支持的操作类型如下:

  • 算术运算(arithmetic operations):+, -, *

  • 逻辑运算(logical operations):~, &, |, ^, », «

  • 数列的切割(bit slicing):[m:l]

  • 比特的叠加(bit concatenation):++

P4 不支持除法(division)和取余(modulo)运算

P4对于变量和常量的声明和实例化和C语言基本一致,变量不能用于维护不同网络数据包之间的状态,只能用于暂时存储数据。如果想要进行有状态的存储操作,需要用到tables或者extern objects

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* variable */  
bit<8> x = 123;  
  
typedef bit<8> MyType;  
MyType x;  
x = 123;  
  
  
/* constant */  
const bit<8> x = 123;  
  
typedef bit<8> MyType;  
const MyType x = 123;

P4 的逻辑方面只有if-else和switch语句,并支持两个等级的终止命令:return终止当前运行的部分,exit则终止所有运行的模块

1
2
3
4
5
6
7
8
// Conditions - not in parsers  
if (x == 123) {...} else {...}  
  
// Switch - only in control blocks  
switch (t.apply().action_run) {  
  action1: {...}  
  action2: {...}  
}

对变长变量的支持

Licensed under CC BY-NC-SA 4.0


使用 Hugo 构建
主题 StackJimmy 设计