前言:这篇文章参考自SDNLAB上zenhox的P4编程理论与实践——理论篇,以及杨泽卫、李呈老师的«重构网络与SDN»一书,在这里对他们表示能写出非常高质量的SDN文章也表示衷心的感谢。

在SDN中,一个重要的概念就是数据平面和控制平面的分离。控制平面就是我们常说的控制器,如果采用openflow协议,数据平面就是支持openflow的交换机。控制平面与数据平面之间仅能通过openflow协议进行通讯。openflow协议有以下的特点:

  • 预先定义好的控制器与数据平面设备之间通信的报文格式
  • openflow是一种狭义的南向协议,是协议相关的,数据平面和控制平面都需要实现该协议。

虽然openflow为SDN奠定了基础,但在实际开发中,openflow还存在着比较大的局限。它无法做到协议不相关,由于必须遵循openflow协议来进行编程,而无法根据自己的需要进行额外扩展,很不灵活。openflow只能在已经固化的交换机数据处理逻辑上,通过流表、组表等指导数据流处理,却无法重新定义交换机处理数据的逻辑。此外,在openflow的版本迭代速度比较快,字段的变化也非常多。在openflow 1.0版本中仅支持12个匹配字段,1.3版本中支持40个匹配字段,到了1.5版本则可以支持45个匹配字段。随着版本的更迭需要对字段进行更改,如果是软件层面的控制器可以比较方便的修改,但是对于数据平面,特别是支持openflow的硬件交换机来说,每次修改协议都需要重新编写数据包的处理逻辑。可以说,openflow的这种可扩展性非常差的设计大大阻碍了openflow的推广和使用。

数据平面可编程与P4

既然之前提到了openflow的弊端,虽然实现了控制平面的可编程,但在数据平面仍然是使用固定的openflow。于是,最好能够有一种方式能够做到协议无关,从而在数据平面能够自己定义数据包的处理逻辑。于是,P4语言应运而生。

P4是一种协议无关的数据包处理编程语言,P4支持用户自定义匹配字段,协议解析过程和转发过程,从而能实现真正意义上的协议无关可编程网络数据平面。

不过,P4与openflow的定位完全不同。openflow是一种南向协议,它是控制平面与数据平面沟通的桥梁。而P4则是一门数据平面的编程语言,它关注的是数据平面的开放性,并不涉及到控制平面。

P4的抽象转发模型

V1Model

这是P4所提供的最简单易理解的编程结构,V1Model。从图上可以看出,数据包的处理过程像一条流水线一样,它由5个模块组成,分别是:

  1. Parser:解析器,对数据包根据预先定义好的格式进行解析,并提取头部的各个字段,在这里可能会根据不同的头部进行匹配。比如如果三层包头的协议字段是0x0800则是ip报文,如果是0x0806则是arp报文,则会进行不同的处理。
  2. Ingress:Ingress流水线部分,数据处理逻辑的入口。
  3. TM:Traffic Manager,在这里定义核心的处理逻辑,并且会对一些元数据进行更新。
  4. Egress:Egress流水线部分,数据处理逻辑的出口。
  5. Deparser:用于对数据包进行重组,因为数据包在处理过程中经历了分解和处理。所以最后转发的时候需要重组一下。

P4为我们提供了上述抽象,我们就可以把所有的交换机的内部处理逻辑理解为上述的模型,每次数据包进入交换机,都会通过这些一条流水线。你要做的只是去定义流水线上的各个模块罢了。所以,按照上述模型,P4语言的代码结构如下:

#include <core.p4>
#include <v1model.p4>

const bit<16> TYPE_IPV4 = 0x800;

/*************************************************************************
*********************** H E A D E R S  ***********************************
*************************************************************************/

typedef bit<9>  egressSpec_t;
typedef bit<48> macAddr_t;
typedef bit<32> ip4Addr_t;

/*定义二层数据包头*/
header ethernet_t {
    macAddr_t dstAddr;
    macAddr_t srcAddr;
    bit<16>   etherType;
}

/*定义ip数据包头*/
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;
    ip4Addr_t srcAddr;
    ip4Addr_t dstAddr;
}

struct metadata {
    /* empty */
}

/*定义了headers由二层头部与三层头部组成*/
struct headers {
    ethernet_t   ethernet;
    ipv4_t       ipv4;
}

/*************************************************************************
*********************** P A R S E R  ***********************************
*************************************************************************/

parser MyParser(packet_in packet,
                out headers hdr,
                inout metadata meta,
                inout standard_metadata_t standard_metadata) {

    state start {
        transition parse_ethernet; //转移到 parse_ethernet状态
    }

    state parse_ethernet {
        packet.extract(hdr.ethernet); //提取数据包头
        transition select(hdr.ethernet.etherType) {
            TYPE_IPV4: parse_ipv4; //转移到 parse_ipv4状态
            default: accept; 
        }
    }

    state parse_ipv4 {
        packet.extract(hdr.ipv4);
        transition accept; //根据 etherType, 转移到其他状态,直到转移到accept结束
    }

}

/*************************************************************************
************   C H E C K S U M    V E R I F I C A T I O N   *************
*************************************************************************/
/*校验和的计算可以空着*/
control MyVerifyChecksum(inout headers hdr, inout metadata meta) {   
    apply {  }
}


/*************************************************************************
**************  I N G R E S S   P R O C E S S I N G   *******************
*************************************************************************/

control MyIngress(inout headers hdr,
                  inout metadata meta,
                  inout standard_metadata_t standard_metadata) {
    /*定义了丢包的动作*/
    action drop() {
        mark_to_drop();
    }
    
    /*定义了三层转发的动作*/
    action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {
        standard_metadata.egress_spec = port;
        hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;
        hdr.ethernet.dstAddr = dstAddr;
        hdr.ipv4.ttl = hdr.ipv4.ttl - 1;
    }
    
    /*定义table ipv4_lpm*/
    table ipv4_lpm {
        key = {
            hdr.ipv4.dstAddr: lpm;
        }
        actions = {
            ipv4_forward;
            drop;
            NoAction;
        }
        size = 1024;
        default_action = drop();
    }
    
    //上述都是声明定义过程,这里是真正的数据处理逻辑
    apply {
        if (hdr.ipv4.isValid()) {
            ipv4_lpm.apply();
        }
    }
}

/*************************************************************************
****************  E G R E S S   P R O C E S S I N G   *******************
*************************************************************************/

control MyEgress(inout headers hdr,
                 inout metadata meta,
                 inout standard_metadata_t standard_metadata) {
    apply {  }
}

/*************************************************************************
*************   C H E C K S U M    C O M P U T A T I O N   **************
*************************************************************************/
/*校验和的计算部分,可以省略*/
control MyComputeChecksum(inout headers  hdr, inout metadata meta) {
     apply {
	update_checksum(
	    hdr.ipv4.isValid(),
            { hdr.ipv4.version,
	      hdr.ipv4.ihl,
              hdr.ipv4.diffserv,
              hdr.ipv4.totalLen,
              hdr.ipv4.identification,
              hdr.ipv4.flags,
              hdr.ipv4.fragOffset,
              hdr.ipv4.ttl,
              hdr.ipv4.protocol,
              hdr.ipv4.srcAddr,
              hdr.ipv4.dstAddr },
            hdr.ipv4.hdrChecksum,
            HashAlgorithm.csum16);
    }
}

/*************************************************************************
***********************  D E P A R S E R  *******************************
*************************************************************************/
/*数据包的重组阶段*/
control MyDeparser(packet_out packet, in headers hdr) {
    apply {
        packet.emit(hdr.ethernet);
        packet.emit(hdr.ipv4);
    }
}

/*************************************************************************
***********************  S W I T C H  *******************************
*************************************************************************/

V1Switch(
MyParser(),
MyVerifyChecksum(),
MyIngress(),
MyEgress(),
MyComputeChecksum(),
MyDeparser()
) main;

这个简单的P4语言程序定义了一个三层数据包的处理流程。该程序的主函数入口在最下方的main函数,在这里定义了一条完整的数据处理流水线。先是定义Parser,后计算校验和,再经过Ingress和Egress对数据包进行处理,之后计算校验和,再将数据包重组就完成了一次数据转发。

P4程序的核心部分是数据头部的定义以及Ingress中对数据包的处理。在数据头部的定义中,可以很明确看到定义了二层以太网包头以及IP包头这两种数据格式。当然如果你需要实现更加复杂的应用,你可以定义更多的数据包头例如MPLS包头,TCP包头等,甚至你也可以定义openflow的包头,从而在P4程序中实现openflow的数据平面。

Ingress中的数据包处理是P4的核心部分。示例程序中定义了表ipv4_lpm。在table中可以定义动作,ipv4_lpm表中的动作包括三层转发(ipv4_forward),丢包(drop)和NoAction,默认动作则是简单的丢包。ipv4_forward这个action的实现逻辑也非常清晰,首先修改元数据(metadata),将egress的端口设置为出端口。然后,将二层的源MAC地址修改为当前MAC地址,将目的MAC修改为需要发送出去的端口的MAC地址,之后将ttl减一,这个逻辑也与我们所知道的三层转发过程一致。

P4语言语法简单,程序结构也比较清晰,非常容易上手。一旦熟悉P4程序的工作模式,将能够非常方便的上手开发。在网上也有一些比较不错的P4学习资源,比如P4官方的示例教程,地址是tutorials。里面的应用包括了基于P4实现的tunnel、负载均衡、源路由、ecn等等。

P4语言目前仍然保持着良好的发展势头,且越来越火爆。Barefoot公司推出的基于P4语言的芯片 Tofino 已经开始逐渐在市场上使用,常见于一些复杂的云网络中。基于数据平面可编程思想的P4,正在推动着SDN领域走的越来越远。