P4学习笔记(一)初始P4

P4学习笔记(二)一个简单P4交换机实现


本节主要讲诉利用P4实现一个最简单的交换机。首先会讲一下交换机的架构,然后给出具体的P4代码实现。

1、 架构模型

简单P4交换机(VSS:very simple switch)它只是一个教学示例,说明了可编程交换机如何利用P4实现和代码编写。VSS具有许多固定功能块(在我们的示例中以浅蓝色显示),具体功能在下一小节描述。 白色块为P4代码实现模块(parse、match-action pipeline、 deparse)。
在这里插入图片描述
VSS通过8个输入以太网端口之一,通过再循环通道或从直接连接到CPU的端口接收数据包。 VSS有一个单一的Parser,解析后输出到match-action pipline,pipline处理后输出到Deparse模块,经过Deparser后,数据包通过8个输出以太网端口之一或3个“特殊”端口之一发出:

  • 发送到“ CPU端口”的数据包将发送到控制平面
  • 发送到“丢弃端口”的数据包将被丢弃
  • 发送到“再循环端口”的数据包通过特殊的输入端口重新注入到交换机中

图中的白色块是可编程的,用户必须提供相应的P4程序来指定每个此类块的行为。红色箭头指示用户定义的数据流。浅蓝色块是固定功能组件。绿色箭头是数据平面接口,用于在固定功能块和可编程块之间传递信息,这些功能在P4程序中作为内部元数据(译者注:类似于全局变量的概念)。

2、预定义模块详细描述

P4仅能实现部分模块功能,部分功能还是需要预定义模块来实现。

2.1 Arbiter 模块

  • 从物理输入以太网端口之一,控制平面或输入再循环端口接收数据包。
  • 对于从以太网端口接收的数据包,该模块计算以太网尾部校验和并进行验证。 如果校验和不匹配,则丢弃数据包。 如果校验和确实匹配,它已从数据包payload中删除。
  • 如果有多个数据包,则接收数据包涉及运行仲裁算法。(译者注:没有太明白啥意思)
  • 如果Arbiter 模块正在忙于处理先前的数据包,并且没有队列空间可用,则输入端口可能会丢弃到达的数据包,且不会提示。
  • 接收到数据包后,Arbiter 块将inCtrl、inputPort值设置为match-action pipline输入,该值是数据包起源的输入端口的标识。 物理以太网端口的编号为0到7,而输入再循环端口的编号为13,CPU端口的编号为14。

2.2 Parser runtime 模块

Parser runtime模块与Parser协同工作。 它基于Parser的结果match-action pipline提供错误代码,并且向demux 模块提供关于数据包payload的信息(例如,剩余payload数据的大小)。 parser完成数据包的处理后,将使用关联的元数据作为输入(数据包头和用户定义的元数据)调用match-action pipline。

2.3 Demux 模块

Demux模块的核心功能是从deparser接收输出数据包的包头,从parser接收数据包的payload,将它们组合成新的数据包,然后将结果发送到正确的输出端口。 输出端口由outCtrl.ouputPort的值指定,该值由match-action pipline设置。

  • 丢弃报文:将数据包发送到丢弃端口会让数据包消失。
  • 转发报文:将数据包发送到编号在0到7之间的输出以太网端口会导致它在相应的物理接口上发出。 如果输出接口已经在忙于发出另一个数据包,则可以将该数据包放入队列中。 发出数据包时,物理接口会计算正确的以太网校验和尾部并将其附加到数据包中。
  • 上升到CPU:将数据包发送到输出CPU端口会使数据包传输到控制平面。 在这种情况下,发送到CPU的数据包是原始输入数据包,而不是从Parser接收的数据包,从deparser接受后的包会被丢弃。
  • 循环处理包:将数据包发送到输出再循环端口会使它出现在输入再循环端口。 当无法单次完成数据包处理时,循环很有用。
  • 非法包: 如果outputPort的值非法(例如9),则数据包将被丢弃。
  • 业务繁忙丢包:如果Demux 模块正忙于处理先前的数据包,并且没有能力将来自deparser的数据包排队,则解复用器可能会丢弃该数据包,而与指示的输出端口无关

3、代码声明文件

(译者注:文中的语法是基于P4_16的)下面一段代码是VSS的定义文件,定义了VSS所要用到的结构体以及函数声明。

// File "very_simple_switch_model.p4"
// Very Simple Switch P4 declaration
// core library needed for packet_in and packet_out definitions
# include <core.p4>

/* Various constants and structure declarations */
/* ports are represented using 4-bit values */
typedef bit<4> PortId;

/* only 8 ports are "real" */
const PortId REAL_PORT_COUNT = 4w8; // 4w8 is the number 8 in 4 bits

/* metadata accompanying an input packet */
struct InControl {
	PortId inputPort;
}

/* special input port values */
const PortId RECIRCULATE_IN_PORT = 0xD;
const PortId CPU_IN_PORT = 0xE;

/* metadata that must be computed for outgoing packets */
struct OutControl {
	PortId outputPort;
}

/* special output port values for outgoing packet */
const PortId DROP_PORT = 0xF;
const PortId CPU_OUT_PORT = 0xE;
const PortId RECIRCULATE_OUT_PORT = 0xD;

/* Prototypes for all programmable blocks */
/**
* Programmable parser.
* @param <H> type of headers; defined by user
* @param b input packet
* @param parsedHeaders headers constructed by parser
*/
parser Parser<H>(packet_in b,
				out H parsedHeaders);

/**
* Match-action pipeline
* @param <H> type of input and output headers
* @param headers headers received from the parser and sent to the deparser
* @param parseError error that may have surfaced during parsing
* @param inCtrl information from architecture, accompanying input packet
* @param outCtrl information for architecture, accompanying output packet
*/
control Pipe<H>(inout H headers,
				in error parseError,// parser error
				in InControl inCtrl,// input port
				out OutControl outCtrl); // output port

/**
* VSS deparser.
* @param <H> type of headers; defined by user
* @param b output packet
* @param outputHeaders headers for output packet
*/
control Deparser<H>(inout H outputHeaders,
					packet_out b);
					
/**
* Top-level package declaration - must be instantiated by user.
* The arguments to the package indicate blocks that
* must be instantiated by the user.
* @param <H> user-defined type of the headers processed.
*/
package VSS<H>(Parser<H> p,
				Pipe<H> map,
				Deparser<H> d);
				
// Architecture-specific objects that can be instantiated
// Checksum unit
extern Checksum16 {
	Checksum16(); // constructor
	void clear(); // prepare unit for computation
	void update<T>(in T data); // add data to checksum
	void remove<T>(in T data); // remove data from existing checksum
	bit<16> get(); // get the checksum for the data added since last clear
}
  • 引入的 core.p4 是P4语言的内置库,定义了标准数据类型和错误类型

  • bit<4> 是具有4位的位字符串的类型。

  • 语法4w0xF表示使用4位表示值15。 另一种表示法是4w15。在许多情况下,可以省略width修饰符,只写15。

  • error是用于保存错误代码的内置P4类型

  • Parser 声明

    parser Parser<H>(packet_in b, out H parsedHeaders);
    

    该声明描述了解析器的接口,但尚未描述其实现,该接口将由程序员负责实现。 解析器从packet_in读取其输入,packet_in是在core.p4库中声明的P4 extern对象。 解析器将其输出(关键字out)写入parsedHeaders参数。 此参数的类型为H,但是具体类型不知道,需要程序员提供。

  • Match-Action pipeline 声明

    control Pipe<H>(inout H headers,
    			in error parseError,// parser error
    			in InControl inCtrl,// input port
    			out OutControl outCtrl); // output port
    

    该声明需要输入3个参数: 1、解析后的数据包头,2、解析器错误parseError 3、inCtrl控制数据。上图指出了这些信息的不同来源。 pipeline将其输出写入outCtrl,并且它必须在适当位置更新要由Deparser使用的包头。

  • Package 定义

    package VSS<H>
    

    类型变量H表示尚待用户稍后提供的类型,但未知。 在这种情况下,H是用户程序将要处理的包头的类型。 Parser将生成这些包头的解析表示,并且match-action管道将在适当位置更新输入包头以生成输出包头。

4、代码实现文件

在这里插入图片描述
上图展示了VSS交换机数据处理流程。下面用代码来实现上图中的逻辑。

// Include P4 core library
# include <core.p4>
// Include very simple switch architecture declarations
# include "very_simple_switch_model.p4"
// This program processes packets comprising an Ethernet and an IPv4
// header, and it forwards packets using the destination IP address
typedef bit<48> EthernetAddress;
typedef bit<32> IPv4Address;
// Standard Ethernet header
header Ethernet_h {
	EthernetAddress dstAddr;
	EthernetAddress srcAddr;
	bit<16> etherType;
}
// IPv4 header (without options)
header IPv4_h {
	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;
	IPv4Address srcAddr;
	IPv4Address dstAddr;
}
// Structure of parsed headers
struct Parsed_packet {
	Ethernet_h ethernet;
	IPv4_h ip;
}
// Parser section
// User-defined errors that may be signaled during parsing
error {
	IPv4OptionsNotSupported,
	IPv4IncorrectVersion,
	IPv4ChecksumError
}
parser TopParser(packet_in b, out Parsed_packet p) {
	Checksum16() ck; // instantiate checksum unit
	
	state start {
		b.extract(p.ethernet);
		transition select(p.ethernet.etherType) {
			0x0800: parse_ipv4;
			// no default rule: all other packets rejected
		}
	}
	
	state parse_ipv4 {
		b.extract(p.ip);
		verify(p.ip.version == 4w4, error.IPv4IncorrectVersion);
		verify(p.ip.ihl == 4w5, error.IPv4OptionsNotSupported);
		ck.clear();
		ck.update(p.ip);
		// Verify that packet checksum is zero
		verify(ck.get() == 16w0, error.IPv4ChecksumError);
		transition accept;
	}
}

// Match-action pipeline section
control TopPipe(inout Parsed_packet headers,
				in error parseError, // parser error
				in InControl inCtrl, // input port
				out OutControl outCtrl) {
	IPv4Address nextHop; // local variable
	
	/**
	* Indicates that a packet is dropped by setting the
	* output port to the DROP_PORT
	*/
	action Drop_action() {
		outCtrl.outputPort = DROP_PORT;
	}
	
	/**
	* Set the next hop and the output port.
	* Decrements ipv4 ttl field.
	* @param ivp4_dest ipv4 address of next hop
	* @param port output port
	*/
	action Set_nhop(IPv4Address ipv4_dest, PortId port) {
		nextHop = ipv4_dest;
		headers.ip.ttl = headers.ip.ttl - 1;
		outCtrl.outputPort = port;
	}
	
	/**
	* Computes address of next IPv4 hop and output port
	* based on the IPv4 destination of the current packet.
	* Decrements packet IPv4 TTL.
	* @param nextHop IPv4 address of next hop
	*/
	table ipv4_match {
		key = { headers.ip.dstAddr: lpm; } // longest-prefix match
		actions = {
			Drop_action;
			Set_nhop;
		}
		size = 1024;
		default_action = Drop_action;
	}
	
	/**
	* Send the packet to the CPU port
	*/
	action Send_to_cpu() {
		outCtrl.outputPort = CPU_OUT_PORT;
	}
	
	/**
	* Check packet TTL and send to CPU if expired.
	*/
	table check_ttl {
		key = { headers.ip.ttl: exact; }
		actions = { Send_to_cpu; NoAction; }
		const default_action = NoAction; // defined in core.p4
	}
	/**
	* Set the destination MAC address of the packet
	* @param dmac destination MAC address.
	*/
	action Set_dmac(EthernetAddress dmac) {
		headers.ethernet.dstAddr = dmac;
	}
	/**
	* Set the destination Ethernet address of the packet
	* based on the next hop IP address.
	* @param nextHop IPv4 address of next hop.
	*/
	table dmac {
		key = { nextHop: exact; }
		actions = {
			Drop_action;
			Set_dmac;
		}
		size = 1024;
		default_action = Drop_action;
	}
	/**
	* Set the source MAC address.
	* @param smac: source MAC address to use
	*/
	action Set_smac(EthernetAddress smac) {
		headers.ethernet.srcAddr = smac;
	}
	/**
	* Set the source mac address based on the output port.
	*/
	table smac {
		key = { outCtrl.outputPort: exact; }
		actions = {
			Drop_action;
			Set_smac;
		}
		size = 16;
		default_action = Drop_action;
	}
	
	apply {
			if (parseError != error.NoError) {
				Drop_action(); // invoke drop directly
				return;
			}
			
			ipv4_match.apply(); // Match result will go into nextHop
			if (outCtrl.outputPort == DROP_PORT) return;
			
			check_ttl.apply();
			if (outCtrl.outputPort == CPU_OUT_PORT) return;
			
			dmac.apply();
			if (outCtrl.outputPort == DROP_PORT) return;
			
			smac.apply();
	}
}
	
	// deparser section
control TopDeparser(inout Parsed_packet p, packet_out b) {
	Checksum16() ck;
	apply {
		b.emit(p.ethernet);
		if (p.ip.isValid()) {
			ck.clear(); // prepare checksum unit
			p.ip.hdrChecksum = 16w0; // clear checksum
			ck.update(p.ip); // compute new checksum.
			p.ip.hdrChecksum = ck.get();
		}
		b.emit(p.ip);
	}
}

// Instantiate the top-level VSS package
VSS(TopParser(),
	TopPipe(),
	TopDeparser()) main;
	
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐