P4 tutorials 实例分析—basic (2024)

环境

OS:Ubuntu16.04

behavioral-model:1.13.0

p4c:1.2.0

mininet:2.3.0d6

tutorials:p4lang/tutorials

P4编程环境安装完成的目录结构如下:

├── behavioral-model├── grpc├── mininet├── p4c├── PI├── protobuf└── tutorials

实例分析主要操作是tutorials目录,结构如下:

├── exercises├── LICENSE├── p4-cheat-sheet.pdf├── P4_tutorial.pdf├── Learning tracker├── utils└── vm

其中P4_tutorial.pdf 为P4语言教程可以打开学习一下,exercises目录存放的是P4程序学习示例,utils里面存放了一些用于调用各个组件(mininet, bmv2, PI, p4c)的脚本,有了这些脚本,我们可以专注于p4代码的开发,控制面的编写,以及拓扑的构建,而不需要去关心去了解bmv2的启动命令,p4c的调用选项等等。具体如何使用,也是非常的简单,我们进入一个具体的例子查看:

root@ubuntu:~/P4/tutorials/exercises/basic# tree -L 1.├── basic.p4├── build├── logs├── Makefile├── pcaps├── pod-topo├── README.md├── receive.py├── send.py├── solution└── triangle-topo6 directories, 5 files

具体操作通过查看README.md文件下面步骤进行

P4 tutorials 实例分析—basic (1)

可以看到,通过make命令执行Makefile文件,我们可以调用utils下Makefile文件并执行下面的脚本,让我们的p4代码跑起来:

P4 tutorials 实例分析—basic (2)
P4 tutorials 实例分析—basic (3)

调用make run,我们可以运行当前目录下(以basic目录为例)的代码,它将执行以下几个步骤:

  • 编译basic.p4 代码,生成basic.json;
  • 解析topology.json, 并且构建相应的mininet仿真拓扑,按照该拓扑启动一台或者多台BMv2交换机,以及一些host;
  • 启动BMv2的同时会将p4代码编译产生的json文件导入;
  • 启动BMv2后会解析 sN-runtime.json 文件,将其载入交换机sN流表之中;
  • 进入mininet命令行,同时开始记录log以及搜集pcap文件;

可以从下面执行过程看到以上描述的几个步骤:

root@ubuntu:~/P4/tutorials/exercises/basic# make runmkdir -p build pcaps logsp4c-bm2-ss --p4v 16 --p4runtime-files build/basic.p4.p4info.txt -o build/basic.json basic.p4sudo python ../../utils/run_exercise.py -t pod-topo/topology.json -j build/basic.json -b simple_switch_grpcReading topology file.Building mininet topology.Configuring switch s3 using P4Runtime with file pod-topo/s3-runtime.json - Using P4Info file build/basic.p4.p4info.txt... - Connecting to P4Runtime server on 127.0.0.1:50053 (bmv2)... - Setting pipeline config (build/basic.json)... - Inserting 5 table entries... - MyIngress.ipv4_lpm: (default action) => MyIngress.drop() - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.1.1', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:01:00, port=1) - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.2.2', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:01:00, port=1) - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.3.3', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:02:00, port=2) - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.4.4', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:02:00, port=2)Configuring switch s2 using P4Runtime with file pod-topo/s2-runtime.json - Using P4Info file build/basic.p4.p4info.txt... - Connecting to P4Runtime server on 127.0.0.1:50052 (bmv2)... - Setting pipeline config (build/basic.json)... - Inserting 5 table entries... - MyIngress.ipv4_lpm: (default action) => MyIngress.drop() - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.1.1', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:03:00, port=4) - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.2.2', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:04:00, port=3) - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.3.3', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:03:33, port=1) - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.4.4', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:04:44, port=2)Configuring switch s1 using P4Runtime with file pod-topo/s1-runtime.json - Using P4Info file build/basic.p4.p4info.txt... - Connecting to P4Runtime server on 127.0.0.1:50051 (bmv2)... - Setting pipeline config (build/basic.json)... - Inserting 5 table entries... - MyIngress.ipv4_lpm: (default action) => MyIngress.drop() - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.1.1', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:01:11, port=1) - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.2.2', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:02:22, port=2) - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.3.3', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:03:00, port=3) - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.4.4', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:04:00, port=4)Configuring switch s4 using P4Runtime with file pod-topo/s4-runtime.json - Using P4Info file build/basic.p4.p4info.txt... - Connecting to P4Runtime server on 127.0.0.1:50054 (bmv2)... - Setting pipeline config (build/basic.json)... - Inserting 5 table entries... - MyIngress.ipv4_lpm: (default action) => MyIngress.drop() - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.1.1', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:01:00, port=2) - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.2.2', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:01:00, port=2) - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.3.3', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:02:00, port=1) - MyIngress.ipv4_lpm: hdr.ipv4.dstAddr=['10.0.4.4', 32] => MyIngress.ipv4_forward(dstAddr=08:00:00:00:02:00, port=1)s1 -> gRPC port: 50051s2 -> gRPC port: 50052s3 -> gRPC port: 50053s4 -> gRPC port: 50054**********h1default interface: eth0 10.0.1.1 08:00:00:00:01:11********************h2default interface: eth0 10.0.2.2 08:00:00:00:02:22********************h3default interface: eth0 10.0.3.3 08:00:00:00:03:33********************h4default interface: eth0 10.0.4.4 08:00:00:00:04:44**********Starting mininet CLI======================================================================Welcome to the BMV2 Mininet CLI!======================================================================Your P4 program is installed into the BMV2 software switchand your initial runtime configuration is loaded. You can interactwith the network using the mininet CLI below.To inspect or change the switch configuration, connect toits CLI from your host operating system using this command: simple_switch_CLI --thrift-port <switch thrift port>To view a switch log, run this command from your host OS: tail -f /root/P4/tutorials/exercises/basic/logs/<switchname>.logTo view the switch output pcap, check the pcap files in /root/P4/tutorials/exercises/basic/pcaps: for example run: sudo tcpdump -xxx -r s1-eth1.pcapTo view the P4Runtime requests sent to the switch, check thecorresponding txt file in /root/P4/tutorials/exercises/basic/logs: for example run: cat /root/P4/tutorials/exercises/basic/logs/s1-p4runtime-requests.txtmininet>

新版本的tutorials中,载入静态流表项时采用了runtime方法,而非之前的CLI方法,我们查看一下s1-runtime.json:

root@ubuntu:~/P4/tutorials/exercises/basic/pod-topo# cat s1-runtime.json{ "target": "bmv2", "p4info": "build/basic.p4.p4info.txt", "bmv2_json": "build/basic.json", "table_entries": [ { "table": "MyIngress.ipv4_lpm", "default_action": true, "action_name": "MyIngress.drop", "action_params": { } }, { "table": "MyIngress.ipv4_lpm", "match": { "hdr.ipv4.dstAddr": ["10.0.1.1", 32] }, "action_name": "MyIngress.ipv4_forward", "action_params": { "dstAddr": "08:00:00:00:01:11", "port": 1 } }, { "table": "MyIngress.ipv4_lpm", "match": { "hdr.ipv4.dstAddr": ["10.0.2.2", 32] }, "action_name": "MyIngress.ipv4_forward", "action_params": { "dstAddr": "08:00:00:00:02:22", "port": 2 } }, { "table": "MyIngress.ipv4_lpm", "match": { "hdr.ipv4.dstAddr": ["10.0.3.3", 32] }, "action_name": "MyIngress.ipv4_forward", "action_params": { "dstAddr": "08:00:00:00:03:00", "port": 3 } }, { "table": "MyIngress.ipv4_lpm", "match": { "hdr.ipv4.dstAddr": ["10.0.4.4", 32] }, "action_name": "MyIngress.ipv4_forward", "action_params": { "dstAddr": "08:00:00:00:04:00", "port": 4 } } ]}

这是一个json文件,可以看到,其作用是定义一个个具体的流表项,标明了流表项所处的位置,匹配域,匹配模式,动作名,以及动作参数。这些字段都依赖于我们P4代码中所自定义的流表,匹配域和动作。

通过在mininet中查看网络结构画出拓扑图

mininet> neth1 eth0:s1-eth1h2 eth0:s1-eth2h3 eth0:s2-eth1h4 eth0:s2-eth2s1 lo: s1-eth1:eth0 s1-eth2:eth0 s1-eth3:s3-eth1 s1-eth4:s4-eth2s2 lo: s2-eth1:eth0 s2-eth2:eth0 s2-eth3:s4-eth1 s2-eth4:s3-eth2s3 lo: s3-eth1:s1-eth3 s3-eth2:s2-eth4s4 lo: s4-eth1:s2-eth3 s4-eth2:s1-eth4
P4 tutorials 实例分析—basic (4)

接下来我们开始解读basic.p4代码,basic目录下的只有TODO部分没有具体实现,代码的具体实现在basic/solution目录下面。make run时需要把文件复制到basic目录运行。

P4是一种高级数据面编程语言,既然是高级语言,那么其设计本身就有着很高的抽象程度。我们先来看一种图:

P4 tutorials 实例分析—basic (5)

这是P4中提供的最简单最易理解的编程结构,V1Model。可以看到它由5个模块组成,他们的名字分别是(从左到右):

  • Parser: 解析器, 解析并且提取数据包头的各个字段。
  • Ingress: Ingress处理,在这里定义Ingress流水线。
  • TM: Traffic manager,有一些队列,用于流量控制(一些队列相关的metadata在此更新)。
  • Egress: Egress, 在这里定义Egress流水线。
  • Deparser:用于重组数据包,因为数据包在处理过程中经历了分解和处理。所以最后转发的时候需要重组一下。

P4为我们提供了上述抽象,我们就可以把所有的交换机理解为上述的模型,然后按照上述模型进行开发就可以了。所以,按照上述模型,分析basic用例的代码结构:

/* -*- P4_16 -*- */#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;}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 */}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) { //根据etherType, 选择转移到其他状态,直到转移到accept; TYPE_IPV4: parse_ipv4; //如果是0x0800,则转移到parse_ipv4状态 default: accept; //默认是接受,进入下一步处理 } } state parse_ipv4 { packet.extract(hdr.ipv4); //提取ip包头 transition 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(standard_metadata); //内置函数,将当前数据包标记为即将丢弃数据包 } 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; //ttl要减去1 } table ipv4_lpm { //定义一张表 key = { //流表匹配域关键字 hdr.ipv4.dstAddr: lpm; //匹配模式(lpm是最长前缀匹配,exact是精准匹配,ternary是三元匹配) } 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 ********************************************************************************************************//*将上述代码中定义的各个模块组装起来,有点像C/C++中的main函数*/V1Switch(MyParser(), //解析数据包,提取包头MyVerifyChecksum(), //校验和验证MyIngress(), //输入处理MyEgress(), //输出处理MyComputeChecksum(), //计算新的校验和MyDeparser() //逆解析器) main;

上例中几个模块是用函数来封装的。

parser解析数据包:parser是一个有限状态机。从 start 状态开始,每一个状态便解析一种协议,然后根据低层协议的类型字段,选择解析高一层协议的状态,然后transition到该状态解析上层协议,最后transition到accept;

Ingress:Ingress中,要实现一个转发功能,因此需要定义一个用于转发的流表;

Checksum 和 Deparser:这两个部分都有高度抽象的内置函数直接完成;

控制面代码在pod-topo目录下面s1-runtime.json、s2-runtime.json、s3-runtime.json、s4-runtime.json,定义好了每个交接机的流表项。

测试

  1. 根据README.md的步骤进行测试结果如下截图
P4 tutorials 实例分析—basic (6)

根据测试结果看丢包率为0,说明转发功能实现了。

2.连接到交换机查看流表

通过simple_switch_CLI --thrift-port 9090命令可以连接到s1交换机进行操作。--thrift-port默认端口是9090,后面依此类推。可以通这help查看所有操作命令。

P4 tutorials 实例分析—basic (7)

3.在s1上删除10.0.1.1对应的流表(命令:table_delete MyIngress.ipv4_lpm 0)进行测试

P4 tutorials 实例分析—basic (8)
P4 tutorials 实例分析—basic (9)

从测试结查可以看出删除到h1的流表项后,到h1都不通,h2, h3, h4三台主机之间是可以互相ping通。

至此所有分析都完成。

P4 tutorials 实例分析—basic (2024)
Top Articles
Latest Posts
Article information

Author: Van Hayes

Last Updated:

Views: 5747

Rating: 4.6 / 5 (66 voted)

Reviews: 81% of readers found this page helpful

Author information

Name: Van Hayes

Birthday: 1994-06-07

Address: 2004 Kling Rapid, New Destiny, MT 64658-2367

Phone: +512425013758

Job: National Farming Director

Hobby: Reading, Polo, Genealogy, amateur radio, Scouting, Stand-up comedy, Cryptography

Introduction: My name is Van Hayes, I am a thankful, friendly, smiling, calm, powerful, fine, enthusiastic person who loves writing and wants to share my knowledge and understanding with you.