Overview
P4 workflow.
上一篇P4 学习笔记(二)- 基础语法和 Parser里面我们已经看到了 P4 workflow 中第一个部分—— Deparser。这一篇文章我们一起学习一下剩下的两个部分。其中 P4 里最主要的部分就是 Match-Action Pipeline 了,这部分是实现各种不同 protocol 花式转发逻辑的精髓。我们从三个方面学习一下,也就是:
最后我们会看一下最后一个 Deparser 的过程。这样 P4 完整的 workflow 我们就学完了。
Tables
先说 Tables,它的功能就是简单的查表(不是水表)。像下面这幅图画的,我们收到了一个 packet 之后,从它的 header 里面提取出一个 Key(键值),比如我们之前说过的 5-tuple 的 hash,也就对应了一个 network flow。用这个键值,去中间那个表查询,如果查到了,我们就可以去完成相应的动作了,比如改写 packet 里面的目标 IP 地址。
Table in P4
上面这个图其实还包含了很多其他的信息,比如 Action Code 的部分,我们会有输入(Headers, Metadata, ID, Data)和输出值(Headers, Metadata),也会发现有的值又作为输入值又作为输出值(Headers & Metadata)。同时,我们会注意到图里有个 Control Plane,可以增删改查 Table 里面每一个 entry 的值。这些细节我们稍后都会再一次看到。
一个简单的例子就是网络层的路由表,这个表用 P4 的实现方式如下:
table ipv4_lpm { // 表的名字叫 ipv4_lpm
key = { // 定义键值们
hdr.ipv4.dstAddr: lpm; // 要求 longest prefix match
hdr.ipv4.version: exact; // 要求完全一致
}
actions = { // 放上所有可能的动作们
ipv4_forward;
drop;
}
size = 1024; // 定义表里最多可以有多少个 entries (size matters ;)
default_action = drop(); // 定义默认的动作,就是丢掉 packet
}
键值对应的 match 方式有很多种,上面写了两种,都是 core.p4
的库里的,还有其他的 match 方式,比如:
- ternary (core): 把值和一个 mask 比较,比如 0x01020304 符合 mask 0x0F0F0F0F
- range (v1model.p4): 检查是否值在一个范围里,比如取 0x01020304 - 0x010203FF 之间的值
Actions
Actions 类似 c 语言里的函数,就是很多段可能对 packet 进行更改的代码。我们刚刚说到过 Action 会有输入、输出的值,是带有方向性的参数,其实实际的参数的类型有四种:
-
有向参数:
-
- in: 在一个 action 里是只读的,就是常见的程序语言里函数的输入值;
- out: 在一个 action 里是可以被改写的(非初始化),就是常见的程序语言里函数的返回值;
- inout: 同时作为输入和输出值,类似 c++ 里面的引用;
- in: 在一个 action 里是只读的,就是常见的程序语言里函数的输入值;
-
无向参数:不像上面三类都是有方向性的,还有一种是从 Table 里查询得到的结果,这个参数不需要定义。
举个例子:
// 有向参数示例 - packet 的镜像,也就是从哪里来的,就送回哪里去,简单的交换一下源地址和目标地址就好
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
)
// 无向参数示例 - 如果是从 Table 里查到的 port,就不需要标明方向
action set_egress_port(bit<9> port) {
standard_metadata.egress_spec = port;
}
Control Flow
控制流的部分看起来可能比较抽象,但大体的思想就是,我们有一个表,要是查询到有我们想要的,我们就执行一个动作。所以主要的操作也就三类:
- 使用一个我们定义好了的表:
ipv4_lpm.apply()
; - 查询有没有对应的信息,也就是有没有 hit:
if (ipv4_lpm.apply().hit) {...} else {...}
- 查看我们执行的动作是哪一个:
switch (ipv4_lpm.apply().action_run) { ipv4_forward: {...} }
还是用网络层转发的例子来说明,一个网络包被转发过程,可以用两个表来实现,也就是如下图所示。其中第一个表 ipv4_lpm
用来把 IP 地址映射到下一跳的 ID,第二个表 forward
用来把下一跳的 ID 映射到出口端口号。
L3 Forwarding with 2 tables.
用 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 这个表
}
}
}
}
还有一个很重要的过程也是用类似的控制逻辑来实现的,那就是 checksum(数据校验)的过程。这个过程又可以一分为二:
- 第一个部分,就是我们每次在收到一个 packet 的时候,都要验证 checksum,来确定这个 packet 的内容是完整的没被修改过的(当然也有中间人连着 checksum 一起修改的可能性,但这里不做更深入的讨论);
- 第二个部分,就是我们在转发 packet 出去之前,要更新 packet 的checksum。
这两个部分都在 v1model.p4
这个库里实现了:
extern void verify_checksum<T, O>(
in bool condition,
in T data,
inout O checksum,
HashAlgorithm algo
);
extern void update_checksum<T, O>(
in bool condition,
in T data,
inout O checksum,
HashAlgorithm algo
);
比如在计算 IP header 的 checksum 的时候,我们会用到这样的控制逻辑:
control MyComputeChecksum(...) {
apply {
update_checksum(
hdr.ipv4.isValid(), // 前提条件是 header 格式正确
{ // 数据是 header 中所有的 field
hdr.ipv4.version,
hdr.ipv4.ihl,
hdr.ipv4.diffserv,
hdr.ipv4.totalLen,
hdr.ipv4.identification,
hdr.ipv4.flags,
hdr.ipv4.flagOffset,
hdr.ipv4.ttl,
hdr.ipv4.protocol,
hdr.ipv4.srcAddr,
hdr.ipv4.dstAddr
},
hdr.ipv4.hdrChecksum, // 即将被更新的 checksum
HashAlgorithm.csum16 // 使用 csum16 哈希算法
);
}
}
这一部分确实有很多细节,包括数据部分都要用到哪些 field,好在这些都是标准化的流程,P4 也为大家提供了参考[1],大家就当是复习一下计算 checksum 的过程吧!
类似与 Parser 部分,Match-Action Pipeline 里面也有很多更高阶的(骚)操作,比如:
- 完整的复制一个 packet,搞个 clone;
- 把 packet 发到控制层(control plane);
- 把 packet 重新送回 Match-Action Pipeline,再过一次流水线,也叫 recirculating。
具体的细节可以参考官方手册[2]。
Deparser
P4 workflow.
到此为止,P4 workflow 就只剩下最后一个部分了,也就是 Deparser。但这一部分其实很简单,就是 Parser 的逆过程,如下图所示:
Deparser
代码实现也很简单, 三个 headers 的话,只需要三行 emit
就可以解决:
control MyDeparser(...) {
apply {
packet.emit(hdr.ethernet);
packet.emit(hdr.ipv4);
packet.emit(hdr.tcp);
}
}
小结
这篇文章我们学习了完整的 P4 的 workflow,并且用一些示例介绍了更多 P4 语法上的规范。下一篇我们开始配置环境,实战一下。