P4编程理论与实践(2)—快速上手(转发)
发布日期:2022-02-12 16:06:54 浏览次数:8 分类:技术文章

本文共 8248 字,大约阅读时间需要 27 分钟。

P4编程理论与实践(2)—快速上手

本文内容简介

本文首先向大家简单介绍在学习P4过程中需要用到的工具。本文的主要特色是让对P4感兴趣的大家不费吹灰之力的在工作,学习之余,快速搭建完善的P4实验环境并开始第一个P4实验。本文提供两种搭建环境的方法,它们的特点如下:

  • 虚拟机安装:一个完整的p4教学环境,无需手动搭建环境。
    • 优点: 方便,快捷,对操作系统没什么要求。
    • 缺点:运行较慢。
    • 如果工作环境没有Linux系统,建议使用VM。
  • 真机搭建:
    • 优点:运行速率快,环境较新。
    • 缺点:可能会遇到错误,需要一个ubuntu系统。
    • 不用担心费神的环境搭建,本文将提供一个一键搭建环境的脚本。这个脚本通过了我多次测试。

实验环境介绍

  • 操作系统: Ubuntu 18.04 LTS 64位 桌面版
  • 推荐内存: 4G 以上

各个组件简介

主要需要安装5个组件:

  • bmv2
  • p4c
  • mininet
  • p4-tutorial
  • PI

首先要明白他们各自的作用【图片来源于p4.org】:

如图,我们写好xxx.p4代码,通过 p4c 这个 p4 compiler 将p4代码编译成为p4交换机可以理解的各种”机器代码”。如果目标交换机是 bmv2 , 那么p4c将生成 .json文件。

  • p4c是一款 p4编译器。
  • BMv2是支持P4编程的软件交换机。
  • PI是P4 runtime的实现,用于Control Plane对数据平面的控制。
  • mininet的功能是构建一个虚拟的网络拓扑。 它通过linux内核的一些特性(net命名空间),在一个主机上划分出多个虚拟网络空间,各个网络空间之间相互隔离,有自己的端口, ip等等。mininet让一个或者多个vhost(虚拟主机), 软件交换机(如ovs, bmv2)等 以进程的状态分别绑定在这些网络空间之中,共同构成一个进程级别的虚拟网络拓扑。需要注意的是这些进程级别的主机和交换机他们只是网络上的隔离,而文件系统则是共享主机的文件系统。
  • p4 tutorials 提供了用于学习的实例代码,它提供了很多个带有方向性的实际场景,例如负载均衡,简单的隧道机制,源路由等。并且它事先写好了控制面代码,让p4的初学者可以集中注意力在数据面编程的学习之上。
  • scapy是一个python库,提供构建数据包,抓包,解析包等功能。它功能强大,但是效率很低。由于P4编程中经常会引入各种各样的数据包,有些甚至是开发者自定义的数据包格式。所以我们可以利用scapy进行便捷的组包,发包。如果需要高速率的发包和解析包就不能使用scapy了。

搭建环境

参考搭建环境链接:

进行第一个实验

进行实验之前

如果你下载的是本文提供的第二种虚拟机,或者通过脚本安装了P4环境,现在P4目录下面应该是这个样子:

p4├── behavioral-model  ## BMv2 软件交换机├── grpc              ## 作为BMv2的依赖├── mininet           ## mininet 网络仿真├── p4c			      ## p4c 编译器├── PI                ## PI P4 runtime库├── protobuf          ## 作为依赖└── tutorials         #### 教程目录,以及以后主要的学习,实验

我们主要的工作目录时tutorials,其余的都是被使用的工具组件。细看tutorials:

tutorials├── exercises   # 存放各种练习├── utils       # 工具脚本目录└── vm          # 用于vagrant构建虚拟机的目录,可以无视

我们切换进入 exercises/basic 这个例子

basic├── basic.p4   # 要编写的p4代码├── build      # 生成文件的目录├── logs       # 日志文件, 在调试的时候真的非常重要!├── Makefile   ### 通过Makefile 来调用utils下的脚本!├── pcaps      # 生成的pcap包,可以使用wireshark等工具来分析├── README.md  # 详细的指导├── receive.py ## 利用scapy写的抓取和分析数据包的工具├── s1-runtime.json  #├── s2-runtime.json  # 在运行同时加载入交换机的控制面代码,这里有争议,稍后再谈├── s3-runtime.json  #├── send.py    ## 利用scapy写的构建和发送数据包的工具├── solution   # 这里有这个例子的示例代码(答案)└── topology.json  # 描述拓扑的json文件

可以看到,通过Makefile,我们可以调用utils下的脚本,让我们的p4代码跑起来:

>make run  # 启动命令### ...启动过程中的输出mininet>  # mininet 命令行### ... 你的一些实验操作mininet> exit  # 退出mininet 命令行make clean # 清理上次运行留下的缓存文件和遗留的进程,重要,否则下次运行会可能使用旧的代码。

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

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

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

{      "table": "MyIngress.ipv4_lpm",      "match": {        "hdr.ipv4.dstAddr": ["10.0.1.1", 32]      },      "action_name": "MyIngress.ipv4_forward",      "action_params": {        "dstAddr": "00:00:00:00:01:01",        "port": 1      }}

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

开始第一个实验basic

查看要实现的功能

查看README,里面这样介绍这个实验:

The objective of this exercise is to write a P4 program thatimplements basic forwarding. To keep things simple, we will justimplement forwarding for IPv4....

可以看到这是一个实现转发功能的P4实例,文件剩余部分是进行实验具体的思路和指令,建议大家多多查阅README,以后就可以自己学习啦~

查看网络拓扑结构 topology.json:

{    "hosts": [        "h1",        "h2",        "h3"    ],    "switches": {        "s1": { "runtime_json" : "s1-runtime.json" },        "s2": { "runtime_json" : "s2-runtime.json" },        "s3": { "runtime_json" : "s3-runtime.json" }    },    "links": [        ["h1", "s1"], ["s1", "s2"], ["s1", "s3"],        ["s3", "s2"], ["s2", "h2"], ["s3", "h3"]    ]}

清晰明了:这个拓扑中有3个switch,3个host,构成一个三角形的拓扑,注意到定义switches的时候,会定义载入到交换机的流表项文件”sN-runtime.json”。

了解大概之后,我们开始编写basic.p4代码:

回忆代码要实现的功能:ip_v4转发。我们需要完成的是tutorials中的TODO部分。

在代码的开头, 我们得知该代码是P4_16版本,使用的是我们之前谈到的v1_model。

/* -*- P4_16 -*- */#include 
#include
//通过查看main回顾v1_model:V1Switch(MyParser(), // 解析数据包,提取包头 MyVerifyChecksum(), // 校验和验证MyIngress(), // 输入处理MyEgress(), // 输出处理MyComputeChecksum(), // 计算新的校验和MyDeparser() // 逆解析器) main;/*我们要需要完成以下几个基本的步骤,其余部分可以暂时省略。-定义相关数据结构:根据需求,我们需要定义 ipv4数据包头以及其下层的以太网包头结构。-解析数据包:我们在此提取ipv4包头。-MyIngress:得到了数据包头,我们定义一个用于转发的流表,然后定义匹配域和动作。--MyDeparser:逆解析器-写好控制面代码*///定义数据结构:header ethernet_t {
macAddr_t dstAddr; // macAddr_t 是 typedef bit<48> 的自定义类型 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 {
/* 这个例子用不到metadata */}/*Parser 解析数据包parser是一个有限状态机。从 start 状态开始,每一个状态便解析一种协议,然后根据低层协议的类型字段,选择解析高一层协议的状态,然后transition到该状态解析上层协议,最后transition到accept。具体如下:*/parser MyParser(packet_in packet, out headers hdr, inout metadata meta, inout standard_metadata_t standard_metadata) {
state start {
transition parse_ethernet; //转移到解析以太包头的状态 } state parse_ethernet {
packet.extract(hdr.ethernet); //根据我们定义的数据结构提取以太包头 transition select(hdr.ethernet.etherType) {
// 根据协议类型选择下一个状态 // 类似于switch 0x0800: parse_ipv4; //如果是0x0800,则转换到parse_ipv4状态 default: accept; // 默认是接受,进入下一步处理 } state parse_ipv4 {
packet.extract(hdr.ipv4); //提取ip包头 transition accept; } }}/*Ingress在Ingress中,我们要实现一个转发功能,因此需要定义一个用于转发的流表:*/table ipv4_lpm {
key = {
//流表拥有的匹配域 hdr.ipv4.dstAddr: lpm; // 匹配字段是数据包头的ip目的地址 // lpm 说明匹配的模式是 Longest Prefix Match,即最长前缀匹配 // 当然还有 exact(完全匹配), ternary(三元匹配) } actions = {
//流表拥有的动作类型集合 ipv4_forward; //我们需要一个转发动作,这个需要稍后自定义 drop; // 丢弃动作 NoAction; // 空动作 } size = 1024; //流表可以容纳多少流表项 default_action = drop(); // table miss 是丢弃动作}//我们需要自己实现以下几个动作: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; //ttl要减去1}/*Checksum 和 Deparser这两个部分都有高度抽象的内置函数直接完成:*/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); }}// Deparsercontrol MyDeparser(packet_out packet, in headers hdr) {
apply {
packet.emit(hdr.ethernet); // 这里要注意先后顺序 packet.emit(hdr.ipv4); }}

写好控制面代码 虽然说官方为了让大家专注于数据面编程,已经给好了控制面指令,但是我们有必要查看一下他们,从而有了更深入的理解, 查看 s[1,2,3]-runtime.json, 里面定义了很多流表项。以s1-runtime.json为例, 具体的一条流表项为:

{      "table": "MyIngress.ipv4_lpm",         "match": {        "hdr.ipv4.dstAddr": ["10.0.1.1", 32]      },      "action_name": "MyIngress.ipv4_forward",      "action_params": {        "dstAddr": "00:00:00:00:01:01",        "port": 1      }}{      "table": "MyIngress.ipv4_lpm",         "match": {        "hdr.ipv4.dstAddr": ["10.0.1.1", 32]      },      "action_name": "MyIngress.ipv4_forward",      "action_params": {        "dstAddr": "00:00:00:00:01:01",        "port": 1      }}

回想在p4代码中自定义的转发表。匹配域也依照了我们自定义的代码。而动作也按照我们编写的动作代码传入了相应的参数。将所有的流表项汇总一下, 我绘制了下面的图片:

看到这一步,我们便了然了。数据面定义了转发表,而控制面下发了具体匹配转发的流表项,使得这三个主机可以互通。而在控制面下发的流表项与我们p4代码中定义的流表结构息息相关。

运行代码

minint> pingall   ## 进行一次pingall 测试*** Ping: testing ping reachabilityh1 - h2 h3 h2 - h1 h3 h3 - h1 h2 *** Results: 0% dropped (6/6 received)

丢包率为0,说明转发功能实现了。这样我们完成了第一个实验。

一点建议

  • 初次接触mininet的朋友,建议先学习mininet官方的。
  • p4 tutorials exercises中的README写的非常详细,大家可以自己完成后面的一些练习。

总结

本文提供给大家最快捷的方式去体验,学习和使用P4。其中有方便的虚拟机直接使用,也有真机搭建的脚本。搭建好环境并且开始第一个实验后,大家可以自己专注于P4的学习啦。


本文转自 SDNLAB: ttps://www.sdnlab.com/22512.html

转载地址:https://blog.csdn.net/weixin_46563862/article/details/114491483 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:P4编程理论与实践——理论篇(转载)
下一篇:vue自定义指令--拖拽(代码示例)

发表评论

最新留言

路过按个爪印,很不错,赞一个!
[***.219.124.196]2024年04月15日 11时44分55秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章

转:【答学员问】- 该如何根据岗位学习相关技能 2019-04-27
转:【答学员问】有什么经验教训,是你在面试很多次之后才知道的? 2019-04-27
消息队列:解耦、异步、削峰,现有MQ对比以及新手入门该如何选择MQ? 2019-04-27
【奇技淫巧】-- 三角形最小路径和 2019-04-27
【小技巧】argc和argv的用法 2019-04-27
学不下去了怎么办? 2019-04-27
二叉树的前中后序遍历(迭代法)(带动画) 2019-04-27
【小技巧】【XShell】【Xftp】Windows桌面与Linux虚拟机互传文件 2019-04-27
【redis入门】Centos下安装redis 2019-04-27
【redis入门】redis安装后相关知识串讲 2019-04-27
【redis】来吧,展示一下redis 发布-订阅模式 2019-04-27
讲通C/C++预编译/条件编译指令 #ifdef,#ifndef,#endif,#define,… 2019-04-27
【redis6.0.6】redis源码慢慢学,慢慢看 -- 第二天:空间配置(zmalloc) 2019-04-27
当下热点词再学:redis缓存预热、更新、降级,限流 2019-04-27
【redis6.0.6】redis源码慢慢学,慢慢看 -- 第五天:adlist 2019-04-27
别抖,OK? 操作系统抖动现象、网络抖动与延迟、函数抖动之防抖与节流,串讲 2019-04-27
第六天:网络处理(anet部分)-- redis源码慢慢学,慢慢看【redis6.0.6】 2019-04-27
通过域名获取主机IP -- struct addrinfo 2019-04-27
【C++】算法集锦(8):从两数和问题拓展到一百数和问题 2019-04-27
【C++】算法集锦(9):背包问题 2019-04-27