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 状态写回报文”。

P4 数据平面核心抽象与报文处理链 输入报文 字节流 Parser 状态机解析 提取 Header Header + Metadata Header:报文真实字段 Metadata:流水线上下文 standard_metadata Ingress Table 匹配 动作执行 Egress 出口队列/过滤 出口封装 Deparser 按序 emit Header 校验和更新 输出报文 重新序列化 Table 工程语义:匹配字段 + 匹配类型 + 动作 匹配字段 hdr.ipv4.dstAddr meta.in_port 匹配类型 exact / lpm ternary / range 动作 forward / drop set_field / NoAction 生命周期 Header: Parser→Deparser Metadata: Ingress/Egress 可跨级传递 关键约束 Parser 顺序 = Deparser 顺序 改字段后 必须更新校验和 否则报文被丢弃

五、工程语义中的关键约束与常见误区

第一,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 程序时知道每一步该做什么、不该做什么,以及出错时应该从哪个环节排查。