P4-编程语言入门
P4 官网
P4 Github 地址
P4 官方 Tutorials Github 地址
介绍
P4 (Programming Protocol-independent Packet Processors, 四个 P 开头的单词) 是一门用于可编程设备的特定语言, 旨在实现网络设备数据平面的可编程性, 允许用于通过编程定义如何处理网络流量, 相比之下, 传统的数据平面限于实现特定的协议, 灵活性差, 比较固化.
通过 P4 配置, 可以无需更换硬件来更改路由器或交换机数据处理的逻辑, 且协议与平台无关.
为什么说 P4 对 data plane 的掌控比较强
因为其遵循 Top-down 设计, 数据平面的交换机在硬件层面就是可编程的, 上层的设计改变, 下层的行为也改变 (也就是 Top 决定 Down). 相比于传统 Fixed-function ASIC 设备, 若想改变上层, 需要先更改下层硬件 (Down 决定 Top).
相关概念
PISA
PISA, Protocol-Independent Switch Architecture, 协议无关交换机架构.
- 数据包进入时, 由 programmable parser 处理成中间码 (其实叫 individual headers, 一种 parsed representation, 可用于 matching 和 actions)
- 中间码经 programmable match-action pipline 处理 (比如 modify, add, remove)
- 中间码在处理后, 经 programmable deparser 又转换为数据包发出
P4 target
P4 target 指运行 P4 程序的具体硬件或软件平台, 这个概念强调的是, P4 程序并不是为某一种特定设备编写的, 而是面向一种通用的数据平面描述.
P4 Architecture
P4 Architecture (P4 架构) 指的是一个标准化的接口和组件集, 这些接口和组件允许开发者在一个具体的 目标设备 (Target) 上使用 P4 编程. 也就是说 P4 Architecture 在 P4 程序和目标设备 (如交换机, 模拟器, 网络处理器) 之间充当桥梁.
P4 Architecture 还定义了哪些部分可以通过 P4 进行编程 (比如定义如何解析, 处理和转发数据包), 以及哪些部分是固定的 (指无法通过 P4 程序修改的部分, 通常是设备固有的功能) 或外部实现的 (externs, 指一些目标设备特定的功能, 不能直接用 P4 代码描述, 但可以通过 P4 API 调用), 从而为 P4 程序提供一个开发框架.
示例:
这里左边是 Architecture, 右边是 target.
Architecture 大致都包含有四个阶段:
- Parse, 分析并解析数据包头, 将数据包内容分解成不同的字段 (代码里一般是 headers 和 metadata), 以便后续阶段处理
- Ingress, 将字段放入 Match-Action pipeline, 匹配相应规则并采取对应动作, 读修改, 转发或丢弃数据包
- Traffic Manager (TM, 流量管理器), 负责数据包的队列 (根据不同流量的优先级放入不同的队列中), 调度以及流量管理 (控制传输速率以满足带宽或 QoS), 确保数据包按优先级顺序或服务质量要求发送
- Egress, 和 Ingress 类似, 在数据包出站前进行一些操作, 如标记, 封装, 或进一步修改头字段
- Deparser, 将字段封装为数据包, 以便正确转发
Programming a P4 Target 的流程
- P4 程序根据 P4 Architecture Model 提供的接口来编写
- P4 Runtime 是 Control Plane 和 Data plane 间的接口
Behavioral Model
Behavioral Model, 行为模型, 是 P4 语言模拟网络设备行为的一个实现框架.
BMV2 software switch
BMv2, Behavioral Model version2, 是 P4.org
提供的一个软件交换机实现, 旨在支持 P4 语言编写的程序. 它是一个开源项目, 主要用于开发, 测试和验证 P4 程序 (并不用于实际使用). 相关资料如下:
BMV2 由 C++ 编写, 用 P4 compiler 编译 P4 程序产生的 JSON 文件作为输入, 解释得到要对 packet 做的操作.
V1Model architecture
V1Model 是 P4 程序语言中使用的一个标准架构模型,专门为基于软件的 BMv2 (Behavioral Model v2) 交换机而设计。
Mininet
Mininet 是一个网络模拟工具, 常用于创建, 测试和研究软件定义网络 (SDN). 它允许用户在一台物理计算机上模拟大规模的网络拓扑, 包括交换机, 主机, 链路和控制器, 且能够运行标准的网络协议和应用程序.
P4Runtime
P4Runtime 是 P4 语言生态中的一个重要组件, 它提供了一种控制平面与数据平面之间的通信接口, 用于在和控制器之间进行实时的控制和管理. 通过 P4Runtime, 控制器可以动态下发, 更新或删除 P4 数据平面中的匹配表项和状态, 实现灵活的网络管理.
1 |
|
流程:
- 编译阶段: P4 程序首先使用 P4 编译器 (如
p4c
) 编译, 生成数据平面的二进制文件 (如 BMv2 程序), 并生成 P4Runtime API 文件 (如.p4info
文件). P4Info 文件描述了数据平面中的表结构, 动作, 元数据 等信息 - 运行阶段: 控制器通过 P4Runtime 读取 P4Info 文件, 了解设备支持的表项和元数据. 然后控制器可以动态与交换机交互, 如下发表项, 添加流规则, 或接收数据包事件
Ethernet 和 IPv4
Ethernet 是链路层协议, 位于 OSI 模型的第二层, 负责在同一局域网内的设备之间传输数据帧, 处理物理地址 (MAC 地址).
IPv4 是网络层协议, 位于 OSI 模型的第三层, 负责在不同网络之间路由数据包, 处理逻辑地址 (IP地址).
P4 程序基本结构
P4 的组成元素如下:
对于不同的 Model, 似乎程序的结构也有所不同.
V1Model 示例
这里分开写, 完整的程序合在一起就好
1 |
|
core.p4
是 P4 语言的核心库, 通常包含基础的类型定义, 数据结构, 操作符和功能v1model.p4
是 P4 语言的标准模型, 通常用于定义一个标准的 P4 数据平面模型, 包含了一些典型的网络设备行为和数据结构
1 |
|
metadata
(一般存储一些标志位或控制信息) 和headers
两个结构体都是自定义名称的, 但一般还是遵循这两个命名规范parser
关键字表明 “解析器定义”, 用于描述如何解析数据包中的各种协议头packet_in packet
, 指代流入的数据包,packet_in
是 P4 中的特殊类型, 用于表示输入的数据包流, 不需要额外的方向修饰符 (毕竟语义已经很明确了)out headers hdr
,out
表明这时一个 输出参数, 在解析的过程中,headers
类型的结构体hdr
会被填充数据input metadata meta
,input
表明其为一个 只读输入参数, 即该参数在解析器中不会被修改inout standard_metadata_t smeta
,inout
表示该参数既可以作为输入, 也可以作为输出, 即它的值可以在解析器中被修改;standard_metadata_t
, 是一个标准元数据结构, 存储与交换机端口, 流量控制等相关的信息
1 |
|
control
定义一个 控制块, 描述数据包处理的具体逻辑
1 |
|
- 用
V1Switch
模型创建一个名为main
的交换机实例 V1Switch
的参数是传递的各组件 (注意加上()
并不是执行函数, 而是表明其为一个组件), 需要按顺序
在 v1switch.p4
文件中, 可以看到 V1Switch
的定义为:
1 |
|
(暂不解释)
Hello World
1 |
|
P4 的解析器是由多个状态组成的, start
通常是解析器的初始状态.
state start
, 定义了start
状态的行为transition accept
, 表示切换到accept
状态, 而accept
状态是一个内置的状态, 表示解析器完成了数据包的解析过程, 并且将数据包传递给后续处理
1 |
|
apply {}
块是控制块中需要执行的部分, 这里表示不执行任何操作
1 |
|
- 这里通过数据包进入的端口来设置流出的端口
1 |
|
- 这里在 Parser 中没有对数据包做处理, 因此 Deparser 也为空
Basic Forwarding
这里仅修改了 MyIngress
control 块, 其他部分同上面 hello world:
1 |
|
action
关键词用于定义一个动作, 感觉类似一个待调用的函数, 接受一个bit<9>
的参数table
关键词用于创建一个流表, 存储匹配条件以及相应动作, 而forward
是该流表的名称 (就是变量名啦),key
定义流表的键, 查找哪个字段, 以及如何查找key = { standard_metadata.ingress_port: exact; }
, 表示查找standard_metadata.ingress_port
字段, 采用exact
的匹配原则 (可能就是完全一致的以配, 这里的意思就是只处理standard_metadata.ingress_port
字段)action
表示对匹配项执行的操作, 包含多条 action (前面用action
关键字定义的)size
指定流表的容量, 即可以存储的条目数量default_action
定义当没有找到匹配条目时的默认行为NoAction
是一个内置的特殊 action, 表示不对数据包做处理
forward.apply()
, 这里的apply()
是流表的内置方法, 表示启用该流表的查找和动作处理
Parser
解析器的作用是: 把 packets 映射到 headers 和 metadata 中 (也就是主要负责提取信息). 其基于状态机运行, 每个 parser 都有 3 个预定义的状态:
start
accept
reject
(其他的状态也可以自定义)
在 state 中, 可以执行 action, 或者过渡 (transition) 到下一个 state.
比如:
1 |
|
packet.extract(hdr.ethernet)
从packet
中提取数据, 将其解析为ethernet
头部, 并将其赋值给hdr.ethernet
Controls
Controls 就可以理解为执行一次的函数. 具体的行为在 apply { }
块中执行. 主要包括:
- Match-Action Pipelines
- Deparsers (也就是说没有专门的
deparser
关键字, 其也定义在control
里) - 额外的处理, 如 checksums
示例:
1 |
|
- 这里吧 src 和 dst 的 MAC 地址交换, 然后从接收包的 port 再发送出去
(也就是哪里出来就哪里回去)
Tables
Tables 是 Match-Action pipeline 的基本单位 (都是操作 table 中的数据), 定义 table 时需要指定:
- 要 match 什么
- 怎么 match
- 要执行的 actions 列表 (都列在 table 定义里)
- table 的基本属性, 如
size
,default action
,static entries
定义好 table 后, 其需要包含多条 entries (也就是 rules), 每一条 entry 需要包含:
- 用于 match 的 key
- 当 match 时的 action
- Action data (可以没有, 是与指定动作相关的参数, 通常用于执行该动作所需的额外信息)
Match-Action 的过程可以描述如下:
- packets 被 parser 处理后, 得到 headers and metadata
- Headers 和 metadata 经 control plane 处理时, 用 table 进行 match
- 如果 match hit, 则执行对应的 action code, 不然执行 default action
- 输出最终的 headers and metadata
一个 P4 Table 的示例:
1 |
|
(都用 =
赋值, 用 ;
结尾, 块后头不用加 ;
)
1pm
指 “最长前缀匹配” (Longest Prefix Match)
1pm
, exact
这些都称做 match_kind
, 不同的 model (architecture) 都有定义不同的 match_kind
, 可以在头文件中找到如下:
1 |
|
这里, 在 P4 程序中, 定义 table 的格式, 要执行的 match-action 操作, 处于 Data Plane, 而填充 table entries 属于 Control Plane 的工作.
启用 table 需要调用 apply()
方法:
1 |
|
Action
Action 和 C 的函数类似, 可以在一个 control 块中定义, 也可以全局定义. 传入 action 的参数也有方向和类型.
一个示例:
1 |
|
- 可以看出, action 会直接对传入的参数修改 (就像 C 中传入指针修改一样)
NoAction
在 core.p4
文件中的定义如下:
1 |
|
(可以看到确实是空)
Action 接受两种类型的参数:
- 直接调用 (在 Data Plane 直接传参)
- 非直接调用 (在 Control Plane table match 时调用时自动传参)
Deparsing
Deparsing 负责把 headers 重新转换为 packet, 其具体逻辑编写在 control
块中.
示例:
1 |
|
core.p4
中定义的packet_out
类型, 有emit
方法, 其能够将 valid header 序列化并保存到指定变量中extern
表明导出该类型外部文件可用
语法
结构体声明
1 |
|
和 C/C++ 一样, 用 struct
关键字.
类型声明
1 |
|
bit<9>
是一种 “位宽类型声明”, 用于精确控制字段的位宽, 这与 C/C++ 中的int
类型名指代长度不同. 这里是声明 9 位无符号整数字段 (P4 不允许符号位, 因此所有的bit<N>
类型都是无符号的)ingress_port
, 指数据包进入的端口egress_spec
, 指预期数据包流出端口, 是由 Ingress Pipeline 决定和设置egress_port
, 指实际数据包流出的端口, 通常和egress_spec
的值相同, 但可能由于重定向, 多播等原因不同
header
类型, 其成员是有序的 (在内存中顺序排列, 读取的时候有顺序, 这里也是字节对齐的), 可以包含 bit<n>
, int<n>
, varbit<n>
的成员, 如:
1 |
|
header
类型的一个好处在于, 其可以被标记为有效或无效 (用 setValid()
和 setInvalid()
函数), 可以用 isValid()
来检测, 从而方便处理 (有些 invalid 就跳过).
typedef
同 C/C++:
1 |
|
返回值?
P4 中没有传统意义的函数返回值, 解析器利用状态机来解析数据包, 并将解析结果存储在传入的参数中 (比如 headers
结构体), 而非通过返回值传递.
Select 语句
select
类似 C 中的 switch
, 但是不会自动 fall-through
(也就是说不需要 break 也能跳出), 其匹配成功后返回键值, 如:
1 |
|
hdr.ethernet.etherType
指示了以太网帧中承载的上层协议, 例如, 如果etherType
为0x0800
, 则表示帧承载的是 IPv4 数据包; 如果为0x0806
, 则表示 ARP 协议, 这里的话如果值是0x800
则跳转到parse_ipv4
, 不然到accept
.
运算符
大多 C 中的运算符都能使用, 但没有 division 和 modulo, 如下:
+
,-
,*
~
,&
,|
,^
,>>
,<<
==
,!=
,>
,>=
,<
,<=
- bit-slicing:
[m:l]
- 拼接:
++
Tutorial
Tutorial 环境准备
首先确保安装的 virtualbox 以及其内核版本正确, 不然会报错:
在 Archlinux
上, 可以用 downgrade
命令降低软件版本:
1 |
|
选择 7.0.20
版本即可, 同理对内核降版本:
1 |
|
选择和 virtualbox
相同的版本.
之后:
1 |
|
等待虚拟机启动, 配置脚本会将主机名改为 p4
, 有两个用户可登录:
- vagrant, 密码: vagrant
- p4, 密码: p4
环境构建所花的时间较长, 其取决于网速的电脑速度. (大概1个多小时吧)
构建结束如:
之后可用:
1 |
|
连接到虚拟机器. 这里默认登录到 vagrant
用户, 但我们实际需要 p4
用户, 可以选择进入虚拟机后切换用户, 也可以在 Vagrantfile
中指定 ssh 默认连接的用户:
1 |
|
此时需要密码登录. 若要在 vagrant ssh
时免密, 可以先用 vagrant ssh
登录到 vagrant
用户, 复制 ~/.ssh/authorized_keys
文件里的内容到 p4
用户的 ~/.ssh/authorized_keys
中即可.
若想在 vagrant share 将其暴露出来后免密, 配置如下:
1 |
|
若启用时不想打开 gui, 则可以修改 Vagrantfile, 把 vb.gui
设为 false
:
之后:
1 |
|
Tutorial 使用指南
下面都以 basic
为例.
要做的内容是补全 basic.p4
中 TODO
注释的部分.
每个题其实都有示例答案可以参考:
1 |
|
这里的 solution
目录下有示例答案.
Basic Forwarding
要实现 IPv4 forwarding, 交换机需要对每个 packet 执行下面操作:
- 更新 source (当前 switch 吧) 和 destination (下一跳 switch 吧) MAC 地址
- IP 报头中的 TTL (Time-To-Live) 值减一
- 从合适的 port 将 packet 发出
在补全代码前的现象为:
此时 h1 ping h2
没有返回值, 网络不通.
完整代码如下:
1 |
|
之后:
1 |
|
输出:
为什么要修改 standard_metadata
的值
因为该值存储 packet 流入流出的端口信息.
emit 的顺序为什么先是 ethernet 后是 ipv4
数据包的构建是从底层到顶层进行的, 这里的 emit
是用 appending 的方式构建数据包, 因此先封装底层的 Ethernet 帧, 然后是 IPv4 头部信息.
Basic Tunneling
Debugging
可以用 debug
table 来输出调试信息到 log 文件:
1 |
|