P4 Network SDN

P4_16 流水线

P4_16 Pipeline

HERMITOUO
2021-12-13
8 min

# 先验知识

P4 解析数据包的基础功能被定义在名为 core.p4 的头文件里。

目标 Target 的提供商可以为自己的架构自定义库,例如 BMV2 使用 v1model.p4 ,Tofino 使用 TNA.p4

# Parser

Parser 是 P4 Pipeline 中的第一个阶段,负责解析 Packet 中的 Header 信息,用于高级的逻辑控制。

Untitled

为了满足实时的解析速度,在 P4 中,我们无法读取和更改 Packet 中的 Payload(数据载荷部分),只能对 Packet 中的 Header 进行处理,这也使我们在交换机中的任何操作都不会改变数据包中的内容,保证了数据的安全性和隐私性。

如下图所示,P4 通过一个状态机,将 Packer 中的 Header 解析为用户可读、程序可用的数据。

Untitled

P4 中实现如下:

/* core.p4 */
extern packet_in {
    void extract<T>(out T hdr);
    void extract<T>(out T variableSizeHeader,
    in bit<32> variableFieldSizeInBits);
    T lookahead<T>();
    void advance(in bit<32> sizeInBits);
    bit<32> length();
}

/* standard_metadata_t 定义 */
struct standard_metadata_t {
    bit<9> ingress_port;
    bit<9> egress_spec;
    bit<9> egress_port;
    bit<32> clone_spec;
    bit<32> instance_type;
    bit<1> drop;
    bit<16> recirculate_port;
    bit<32> packet_length;
    ...
}

/* 用户程序 */
const bit<16> TYPE_IPV4 = 0x800;

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

struct metadata {
    /* empty */
}

struct headers {
    ethernet_t   ethernet;
    ipv4_t       ipv4;
}

parser MyParser(packet_in packet,
    out headers hdr,
    inout metadata meta,
    inout standard_metadata_t std_meta) {
    
    state start {
        packet.extract(hdr.ethernet);
        transition accept;
    }
}

注意,Header 变量的定义一定要与真实网络包结构相对应,如以太网帧的头部包含:目标 MAC 地址(48 位)、源 MAC 地址(48 位)、以太网包类型(16 位)。

Untitled

在定义 ethernet_t 时,应按照上述顺序定义变量。IP 帧同理。

Untitled

packet_incore.p4 中被定义,包含 extract 方法,会从数据包的第一个字节开始,向定义好的 Header 中读入数据。

另外,Parser 是 P4 中唯一可以产生循环的阶段。

# Match-Action Pipeline

Match-Action Pipeline 是 P4 工作流中最重要的一部分,它决定了如何使用 Parser 提取到的 Header 信息进行计算,如何对各类数据包进行转发。主要可分为三个部分:

  • Tables
  • Actions
  • Control Flow

# Tables

Tables 主要的功能就是进行简单的查找。如下图所示,我们在收到一个数据包后,经过 Parser 可以从中提取到一个 Key,使用 Key 在 Match Table 中进行匹配,如果命中,则执行对应的 Action。

Untitled

我们来定义一个简单的路由表:

table ipv4_lpm {                // 定义 table 的名字为 ipv4_lpm 
	key = {
		hdr.ipv4.dstAddr: lpm;      // lpm 意为 longest prefix match
		hdr.ipv4.version: exact;    // exact 意为 完全匹配
	}
	atcion = {
		ipv4_forward;
		drop;
	}
	size = 1024;                  // 定义表里最多可以存放的条目(entries)
	default_action = drop();
}

P4 支持三种 Table 的匹配方式(match_kind),定义在 core.p4 头文件中:

  • exact :完全匹配
  • ternary :使用 mash 进行三元匹配
  • lpm :最长前缀匹配

同时,P4 还支持目标架构自定义匹配方式,但不允许用户对 match_kind 进行定义。

如 P4_16 的 v1model.p4 中定义了 range 方式,检查 Key 是否在一个范围中。

# Actions

我们可以将 Actions 类比为 C 语言中的 Function,一个 Action 由名称、输入、动作体、输出四部分定义。然而,和 C 不同,Action 的输入和输出均作为参数出现:

  • 有向参数
    • in :在一个 Action 中作为只读输入,只能被访问,不能被更改。类比 C 语言中的函数输入值(值传递)。
    • out :在一个 Action 中作为输出值,需要赋值后才能访问。类比 C 语言中的返回值。
    • inout :在一个 Action 中同时作为输入值和输出值,即可访问,又可更改。类比 C 语言中的引用。
  • 无向参数:从 Table 中查询到的值,无需标明方向

使用 Action 时,我们要先定义,再调用。下面是一个回传数据包的例子(有向参数),交换机将收到的数据包从相同的端口原路返回。

// 定义 reflect_packet Action
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 Action
reflect_packet(
  hdr.ethernet.srcAddr,
  hdr.ethernet.dstAddr,
  standard_metadata.ingress_port,
  standard_metadata.egress_spec
)

无向参数通常为 Table 的查询结果:

action set_egress_port(bit<9> port) {
  standard_metadata.egress_spec = port;
}

# Control Flow

在控制流中,我们通常会执行三种操作:

  1. 使用定义好的 Table:table_name.apply()
  2. 查询到达的数据包是否和 Table 中表项匹配:table_name.apply().hit()
  3. 执行某个 Action。

引用一个数据包转发的例子,数据包到达后首先进行目的 IP 匹配,获得下一跳的 ID,再进行下一跳 ID 匹配,获得出口端口号。

Untitled

P4 实现:

control MyIngress(...) {

  /* 动作定义 */
  action drop() {...}               // 定义一下丢掉 packet 的动作
  action set_nhop_index(...) {...}  // 定义一下设置下一跳对应 ID 的动作
  action _forward(...) {...}        // 定义一下转发的动作

  /* 表定义 */
  table ipv4_lpm {
    key = {                       
      hdr.ipv4.dstAddr: lpm;        // 要求 longest prefix match
    }
    actions = {
      set_nhop_index;
      drop;
      NoAction;
    }
    size = 1024;
    default_action = NoAction();     // 定义默认的动作,就是无动作
  } 

  table forward {
    key = {
      meta.nhop_index: exact;
    }
    actions = {
      _forward;
      NoAction;
    }
    size = 64;
    default_action = NoAction();
  }

  /* 开始控制逻辑 */
  apply {
    if (hdr.ipv4.isValid()) {         // header 数据类型自带的隐藏参数,判断一个 header 格式是否正确
      if (ipv4_lpm.apply().hit) {     // 应用 ipv4_lpm 这个表,并且检查有没有 hit
        forward.apply();              // 应用 forward 这个表
      }
    }
  }
}

此外,控制流中还支持许多高阶操作,如:

  • 完整地复制一个 Packet 包。
  • 把 Packet 发给 Control Plane。
  • 让 Packet 再循环(recirculating),重新经过一次 Pipeline。

详情可参考 P4_16 语言定义

# Deparser

Deparser 是 Parser 阶段的逆过程,把 P4 程序中的存储的 Header 数据重新写回数据包。

Untitled

P4 实现如下:

/* core.p4 */
extern packet_out {
    void emit<T>(in T hdr);
}

/* 用户程序 */
control MyDeparser(
	packet_out packet,
  in headers hdr
) {
  apply {
    packet.emit(hdr.ethernet);
    packet.emit(hdr.ipv4);
    packet.emit(hdr.tcp);
  }
}

Deparser Control 的参数由两部分构成:

  • packet_out :定义在 core.p4extern 类型。提供了 emit 方法,用以将 header 组装到数据报文中。
  • headers :用户定义的报头类型,类型为 in
Last Updated: 1/17/2022, 3:45:29 AM