本節主要講訴利用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;