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 程序的闭环。

PSA 架构下第一个 P4 程序的数据流与编译路径 入端口报文 Eth + IPv4 IngressParser extract ethernet select etherType headers_t ethernet / ipv4 validity bit Ingress 控制块 l2_fwd.apply() key: dstAddr l2_fwd 表 exact / size=64 default: drop Egress 控制块 egress_port 队列调度 IngressDeparser emit ethernet emit ipv4 出端口报文 转发或丢弃 metadata_t standard_metadata_t ingress_port / egress_port packet_path 元数据随流水线传递 编译路径: l2fwd.p4 → p4c --target psa --arch psa → build/l2fwd.p4i → 目标产物 验证:加载到 BMv2 / SmartNIC,发送目的 MAC 命中表项的帧 → 预期端口转发;未命中 → 丢弃
实践提示:第一次编译时,建议先用 p4c --target psa --arch psa --pp l2fwd.p4 输出预处理后的代码,确认 psa.p4 被正确包含;再逐步加入表项和动作,每加一步编译一次,避免一次性堆叠大量语法错误难以定位。
常见误区:把 standard_metadata_t 当作普通 header 去 extract。它由架构自动填充,不应出现在解析器的 extract 调用中;解析器只负责从报文中提取真实存在的头。