3. P4语言核心抽象
P4 的核心价值不在于“能写转发逻辑”,而在于它把数据平面拆成一组边界清晰、职责明确的抽象对象。理解这些抽象,关键不是背定义,而是弄清它们在报文处理流水线中的位置、生命周期和相互约束。一个 P4 程序通常由 Parser、Ingress、Egress、Deparser 四个控制块,以及 Header、Metadata、Table 三类数据与匹配抽象共同构成。它们不是并列的语法元素,而是围绕“报文如何被解析、如何被决策、如何被重新封装”形成的一条完整处理链。
一、Header 与 Metadata:两类状态,两种生命周期
Header 表示报文中真实存在的协议字段,例如以太网目的 MAC、IPv4 源地址、TCP 目的端口。它的工程语义是“可被解析、可被修改、可被重新序列化”。在 P4 中,Header 必须先声明类型,再在 Parser 中按协议顺序提取。例如:
header ethernet_t {
bit<48> dstAddr;
bit<48> 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;
bit<32> srcAddr;
bit<32> dstAddr;
}
Metadata 则不同,它不来自报文,而是流水线内部产生的临时状态。典型如入端口号、出端口号、队列优先级、时间戳、哈希结果、是否命中某条表项等。Metadata 的生命周期通常跨越 Parser、Ingress、Egress,甚至可被 Deparser 读取,但它不会自动出现在最终报文中,除非显式写入某个 Header 字段。工程上要特别注意:Header 是“报文的一部分”,Metadata 是“处理过程的上下文”。混淆二者,会导致校验和错误、字段错位或出端口决策失效。
二、Parser:把字节流变成有类型的字段
Parser 的职责是依据协议格式,从输入报文中逐层提取 Header,并填充 Metadata。它本质上是一个状态机:起始状态通常解析以太网,根据 etherType 决定下一状态是 IPv4、IPv6 还是其他协议;IPv4 中再根据 protocol 决定是否进入 TCP、UDP 或 ICMP。Parser 必须处理“解析到哪一层停止”的问题,常见做法是遇到未知协议或非首片分片时转入 accept 或 reject 状态。
工程语义上,Parser 不是“尽量多解析”,而是“按需解析”。如果后续 Table 只匹配 IPv4 五元组,就不必解析 TCP 载荷;如果要做带内网络遥测,则可能需要解析自定义的 INT 头。Parser 的深度直接影响流水线级数和资源占用。一个常见的教学示例是:以太网 → IPv4 → TCP,解析后得到 ethernet、ipv4、tcp 三个 Header 实例,以及标准元数据 standard_metadata。若 etherType 为 0x0800,则进入 IPv4 解析;若为 0x86DD,则进入 IPv6 分支;否则直接 accept,保留已解析部分。
三、Ingress 与 Table:匹配—动作的决策核心
Ingress 是数据平面决策的主战场。它读取 Parser 产出的 Header 和 Metadata,通过 Table 执行匹配—动作。Table 的工程语义由三部分决定:匹配字段、匹配类型、动作。匹配字段来自 Header 或 Metadata;匹配类型常见有 exact、l2、l3、ternary、range;动作则可以是转发、丢弃、改写字段、加封装、跳转到另一张表或发送到控制平面。
例如一张 IPv4 路由表可以这样定义:
table ipv4_routing {
key = {
hdr.ipv4.dstAddr: lpm;
}
actions = {
ipv4_forward;
drop;
NoAction;
}
size = 1024;
default_action = drop();
}
其中 lpm 表示最长前缀匹配,ipv4_forward 动作会设置出端口、改写目的 MAC 和源 MAC、递减 TTL。这里的关键工程点是:Table 不直接“执行转发”,它只负责“选择动作”;真正的转发行为由动作函数完成。Ingress 中还可以有多个 Table 串联,通过 apply 顺序执行,形成策略链。例如先做 ACL 过滤,再做路由查找,最后做流量统计。
四、Egress 与 Deparser:出口处理与重新封装
Egress 位于出口侧,通常用于出口队列选择、出口过滤、组播复制、出口隧道封装等。它与 Ingress 共享部分 Metadata,但也能访问 Egress 专属元数据,例如出端口、队列深度、拥塞标记。Egress 的 Table 往往比 Ingress 少,但并非可有可无:在数据中心场景中,Egress 常用于实现基于拥塞反馈的标记或出口 ACL。
Deparser 是流水线的最后一站,负责把 Header 按协议顺序重新序列化为字节流。它的工程语义是“逆解析”:Parser 按顺序提取,Deparser 按顺序输出。如果 Parser 中解析了 ethernet、ipv4、tcp,Deparser 通常也要按 ethernet、ipv4、tcp 的顺序 emit。若中间修改了 ipv4.hdrChecksum 或 tcp 校验和,Deparser 之前必须完成校验和更新,否则接收端会丢弃报文。Deparser 不负责决策,只负责“把当前 Header 状态写回报文”。
五、工程语义中的关键约束与常见误区
第一,Parser 与 Deparser 的顺序必须一致。Parser 先解析 ethernet,再解析 ipv4,Deparser 就必须先 emit ethernet,再 emit ipv4。若顺序颠倒,接收端会把 IPv4 头当成以太网头解析,导致协议错乱。第二,Header 字段修改后必须更新校验和。IPv4 的 hdrChecksum 和 TCP/UDP 校验和不会自动重算,需要在 Ingress 或 Egress 中显式调用校验和更新逻辑,否则报文会被对端静默丢弃。第三,Metadata 不能直接出现在报文中。若要把入端口号带到远端,必须把它写入某个 Header 字段,例如自定义的 INT 头或 VLAN 标签。第四,Table 的 default_action 决定未命中行为。若默认动作是 drop,则所有未匹配流量都会被丢弃;若默认动作是 NoAction,则报文会继续走后续流水线,但不会执行任何转发动作,通常需要配合其他表或出口逻辑。
工程检查清单:编写 P4 程序时,可按以下顺序自查:① Header 类型是否与协议格式逐字段对齐;② Parser 状态转移是否覆盖所有预期分支;③ Table 的 key 是否包含足够区分流量的字段;④ 动作是否设置了出端口和必要的字段改写;⑤ Deparser 的 emit 顺序是否与 Parser 一致;⑥ 校验和是否在 Deparser 之前完成更新;⑦ Metadata 是否被误当作 Header 使用。
把 Parser、Ingress、Egress、Deparser、Metadata、Header 与 Table 放在一起看,P4 的工程语义就清晰了:Parser 负责“看懂”,Ingress/Egress 负责“决策”,Table 负责“匹配与选择动作”,Deparser 负责“写回”,Header 是报文状态,Metadata 是处理上下文。理解这条链,才能在实际编写 P4 程序时知道每一步该做什么、不该做什么,以及出错时应该从哪个环节排查。