2. P4语言核心语法与数据平面抽象
P4 的核心思想是让数据平面的转发行为由程序显式描述,而不是由固定流水线隐式决定。要写出第一个可编译的 P4 程序,必须先理解它的六个基本构件:程序结构、头类型、解析器、匹配动作表、控制流、元数据,以及它们所依附的标准架构 PSA 与 PNA。本节以 PSA 为目标架构,从零编写一个“识别 IPv4 并做二层转发”的最小程序,并完成编译。
一、P4 程序的基本骨架
一个 P4_16 程序通常由以下部分组成:架构声明、头类型定义、解析器、入口控制块、若干匹配动作表与动作、以及反解析器(deparser)。PSA 架构要求程序实现 Ingress 和 Egress 两个控制块,并显式声明 main 包。最小骨架如下:
#include <core.p4>
#include <psa.p4>
typedef bit<48> macAddr_t;
typedef bit<32> ipv4Addr_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;
ipv4Addr_t srcAddr;
ipv4Addr_t dstAddr;
}
struct headers_t {
ethernet_t ethernet;
ipv4_t ipv4;
}
struct metadata_t { }
parser IngressParser(packet_in buffer,
out headers_t hdr,
inout metadata_t meta,
inout standard_metadata_t std_meta) {
state start {
buffer.extract(hdr.ethernet);
transition select(hdr.ethernet.etherType) {
0x0800: parse_ipv4;
default: accept;
}
}
state parse_ipv4 {
buffer.extract(hdr.ipv4);
transition accept;
}
}
control Ingress(...) { apply { } }
control Egress(...) { apply { } }
parser IngressDeparser(packet_out buffer,
in headers_t hdr,
in metadata_t meta,
in standard_metadata_t std_meta) {
state start {
buffer.emit(hdr.ethernet);
buffer.emit(hdr.ipv4);
transition accept;
}
}
PSA_Switch(IngressParser(), Ingress(), Egress(), IngressDeparser()) main;
这段代码已经可以编译通过,但还没有任何转发逻辑。它展示了 P4 的“声明式”风格:先定义数据长什么样(头类型),再定义如何识别(解析器),最后定义如何处理(控制块与表)。
二、头类型与元数据的分工
头类型(header)描述报文中真实存在的字段,例如以太网目的 MAC、IPv4 源地址。每个 header 在运行时有一个“有效位”(validity bit),只有被 extract 成功解析后才为 valid,deparser 也只会 emit valid 的头。元数据(metadata)则描述报文之外、流水线内部传递的状态,例如入端口号、时间戳、自定义标记。PSA 提供了 standard_metadata_t,其中包含 ingress_port、egress_port、packet_path 等标准字段,用户自定义元数据则放在自己的 metadata_t 中。
区分二者的实用判据是:会出现在线上报文里的字段放 header,只在芯片内部流转的放 metadata。把入端口写进 header 会导致 deparser 多发出字节,破坏报文格式。
三、解析器:状态机驱动的报文识别
解析器本质是一个有限状态机。每个 state 执行 extract 或 emit,然后用 transition select 根据刚解析出的字段决定下一状态。上面的例子中,start 状态先取出以太网头,再根据 etherType 判断:0x0800 进入 parse_ipv4,其他值直接 accept。这种“边解析边分支”的能力,正是 P4 相对固定流水线的关键优势——它天然支持可变长、可嵌套的协议栈。
需要注意:extract 会按 header 定义的字段顺序和位宽从报文中连续取位,因此头类型的字段顺序必须与真实报文一致,否则解析结果会整体错位。
四、匹配动作表与控制流
匹配动作表(table)是 P4 的转发决策核心,由三部分组成:key(匹配字段)、action(可执行的操作)、默认动作。下面给 Ingress 增加一张基于目的 MAC 的转发表:
action forward(macAddr_t dst, PortId_t port) {
std_meta.egress_port = port;
}
table l2_fwd {
key = { hdr.ethernet.dstAddr : exact; }
actions = { forward; drop; NoAction; }
default_action = drop();
size = 64;
}
control Ingress(...) {
apply {
l2_fwd.apply();
}
}
这里 exact 表示精确匹配,PSA 还支持 lpm(最长前缀)、ternary(三态)、range 等匹配类型。default_action = drop() 保证未命中表项时不会误转发。size = 64 声明表容量,编译器和目标设备据此分配资源。
控制流由 apply 块内的语句顺序决定。P4 允许 if/else、switch 以及表的顺序调用,但要注意:在硬件流水线上,多个表的执行顺序会影响可实现的并行度,因此应尽量让互不依赖的表并列,而不是串成一条长链。
五、PSA 与 PNA 的差异
PSA(Portable Switch Architecture)面向交换机/网卡转发场景,流水线为 Ingress → Traffic Manager → Egress,强调端口、队列、组播等交换语义。PNA(Portable NIC Architecture)面向主机网卡场景,流水线为 Main → (可选) Timestamp/Checksum 等辅助块,更贴近“收包—处理—回主机或转发”的网卡行为,元数据中会出现 host_metadata 等主机侧字段。选择架构时,若目标是 SmartNIC 上做主机侧卸载,应优先考虑 PNA;若做的是交换机式转发,则用 PSA。两者的头类型、解析器写法基本一致,差异主要体现在控制块签名和标准元数据字段上。
六、编译与结果判读
把上述代码保存为 l2fwd.p4,使用开源编译器 p4c 编译:
p4c --target psa --arch psa l2fwd.p4 -o build/
若编译成功,build/ 下会生成中间表示文件(如 l2fwd.p4i)和目标相关产物。常见报错及含义:
error: unknown type standard_metadata_t:缺少#include <psa.p4>或架构声明不匹配。error: table l2_fwd: key field not a header field:key 中引用了未解析或非 header 的字段。error: action forward: parameter type mismatch:动作参数类型与 key 字段类型不一致,例如把bit<48>写成bit<32>。
编译通过只说明语法与类型正确,并不代表逻辑正确。下一步应把生成的产物加载到支持 PSA 的软件交换机(如 BMv2)或目标 SmartNIC 上,用真实报文验证:发送目的 MAC 为已配置表项的帧,观察是否从预期端口转发;发送未配置的 MAC,观察是否被丢弃。只有“编译通过 + 行为符合预期”,才算完成第一个 P4 程序的闭环。
p4c --target psa --arch psa --pp l2fwd.p4 输出预处理后的代码,确认 psa.p4 被正确包含;再逐步加入表项和动作,每加一步编译一次,避免一次性堆叠大量语法错误难以定位。
standard_metadata_t 当作普通 header 去 extract。它由架构自动填充,不应出现在解析器的 extract 调用中;解析器只负责从报文中提取真实存在的头。