# 先验知识
P4 解析数据包的基础功能被定义在名为 core.p4
的头文件里。
目标 Target 的提供商可以为自己的架构自定义库,例如 BMV2 使用 v1model.p4
,Tofino 使用 TNA.p4
。
# Parser
Parser 是 P4 Pipeline 中的第一个阶段,负责解析 Packet 中的 Header 信息,用于高级的逻辑控制。
为了满足实时的解析速度,在 P4 中,我们无法读取和更改 Packet 中的 Payload(数据载荷部分),只能对 Packet 中的 Header 进行处理,这也使我们在交换机中的任何操作都不会改变数据包中的内容,保证了数据的安全性和隐私性。
如下图所示,P4 通过一个状态机,将 Packer 中的 Header 解析为用户可读、程序可用的数据。
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 位)。
在定义 ethernet_t
时,应按照上述顺序定义变量。IP 帧同理。
packet_in
在 core.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。
我们来定义一个简单的路由表:
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
在控制流中,我们通常会执行三种操作:
- 使用定义好的 Table:
table_name.apply()
。 - 查询到达的数据包是否和 Table 中表项匹配:
table_name.apply().hit()
。 - 执行某个 Action。
引用一个数据包转发的例子,数据包到达后首先进行目的 IP 匹配,获得下一跳的 ID,再进行下一跳 ID 匹配,获得出口端口号。
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
数据重新写回数据包。
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.p4
的extern
类型。提供了emit
方法,用以将 header 组装到数据报文中。headers
:用户定义的报头类型,类型为in
。