1小时学会P4-16编程基础

举报
荷包蛋好吃 发表于 2021/07/29 19:23:24 2021/07/29
【摘要】 本文主要讲述一些P4-16的基本元素,以及相关基础架构,旨在帮助初学者快速上手P4-16。

本文通过chatgpt代理站(支持gpt4):gptschools.cn整理

image.png

本文主要讲述一些P4-16的基本元素,以及相关基础架构,旨在帮助初学者快速上手P4-16。

P4开源项目

P4项目源码可以在github上直接获取(https://github.com/p4lang)。

项目关系

项目关系如下:
image.png

  • 高级语言层:高级抽象的P4语言编写程序
  • 前端编译器:对高级语言进行与目标无关的语义分析并生产中间表示
  • 中间表示层:高级语言中间表示,可转换成多种其他语言
  • 后端编译器:将中间表示转换为目标平台机器码
  • 目标对象层:受控制硬件/软件设备

项目功能

P4项目由很多个单独的模块组成,每个模块就是一个子项目,各子项目功能介绍如下:

  • p4-hlir: 前端编译器。将P4代码转换成高级中间表示。目前,高级中间表示的展示形式与Python对象的层次结构相同。作用是: 使后端编译器不用关心语法分析和目标无关的语义检查

  • p4c-bm:后端编译器。可以将高级语言或高级中间表示转为JSON格式或PD格式的配置文件。

  • behavivoral-model: 又称bmv2,P4软件交换机。使用C++语言编写。该模块主要实现三个目标,最重要的是simple_switch,即P4语言标准中抽象交换机模型。另外两个目标是simple_router, l2_switch

  • p4-build: 需要手动生成的基础设施库,为执行P4程序,编译安装PD库。

  • switch: switch示例,基本完成交换机的绝大部分功能。

  • p4factory: 快速开始,内含6个可快速启动的项目,包括basic_routing, copy_to_cpu, l2_switch, sai_p4, simple_router, switch

  • ntf (Network Test Framework): 网络测试框架。内含用以执行bmv2应用的网络测试用例。该框架中集成了mininet和docker。

  • ptf:Python测试框架,基于unittest框架实现,该框架中的大部分代码从floodlight项目中的OFTest框架移植而来。

  • tutorials:示例教程。包括Basic Forwarding, Basic Tunneling, P4Runtime, Explicit Congestion Notification, Multi-Hop Route Inspection, Source Routing, Calculator, Load Balancing。

  • scapy-vxlan: 扩展Vxlan和ERSPAN-like协议包头处理。

P4语言基础(P4-16)

相关术语

  • PISA(Protocol-Independent Switch Architecture):协议无关交换机架构。
  • P4 Target:特定的硬件实现。
  • P4 Architecture:通过一组P4可编程、外部(externs)与固定组件,提供对P4 Target进行编程的接口。

P4语言与核心库由社区发展而来。外部(extern)库以及P4架构定义由设备上提供。

PISA

image.png

  • Parser:根据程序员声明的数据报头,解析数据包,生产独立的数据包头与中间信息(MetaData);
  • Match-Action Pipeline:根据解析的报头与元信息,匹配流表,对报头进行修改、添加、删除等操作;
  • Deparser:数据包重新被序列化。

P4架构

image.png

除了V1Model,目前还有Portable Switch Architecture和Tofino native等架构,目标是tofino native的架构比v1model有更多的功能,PSA的架构可以被更多的target (FPGA,ASIC,Software)所支持,提供从V1model到其他两个架构的translation。所以可以按照v1model的架构来写程序,之后compiler会帮助把v1model的程序转换成其他的架构的程序。

P4-16语言元素

  • Parser:通过一系列状态(State)转换,提取报头字段,与一些元数据;
  • Control:Tables, Actions, control flow声明;
  • Expressions:基本的操作与运算符;
  • Data Type:BiStrings,header, Struct,array等数据类型。
  • Architecture Description:目标硬件提供的可编程的组件与接口;
  • Extern Libraries:目标硬件支持的特定组件。

image.png

为一个目标硬件编写P4程序

image.png

整个过程由几部分组成,如下:

  • 用户提供: P4程序与控制面;
  • 厂家提供:P4架构模型与目标硬件;
  • 运行时操作:控制流表,外部控制,数据包上报控制面等。

数据类型

基础类型

  • bit<n>: n位的无符号整型(bitstring);
  • bit: 等同于bit<1>
  • int<n>:n(n >=2)位的有符号整型;
  • varbit<n>:最大为n位的变长bitstring。

示例:

bit<4> version;
bit<4> ihl;
bit<8> diffserv;
bit<16> totalLen;

typedef 定义类型别名

设置类型别名,如:

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

header 类型

header类型为有序的程序集合,特点如下:

  • 可以包含bit<n>、int<n>、varbit<n>基础数据类型;
  • 必须字节对齐
  • 可以是validinvalid,并提供几种操作测试或设置有效位:isValid()/setValid()/setInvalid()

示例:

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 类型

struct类型为无序数据类型集合,且不需字节对齐。可用于元数据的组织。

示例:

/* Architecture */
struct standard_metadata_t {
    bit<9> ingress_port;
    bit<9> egress_spec;
    bit<9> egress_port;
    bit<32> clone_spec;
    bit<32> instance_type;
    bit<1> drop;
    bit<16> recirculate_port;
    bit<32> packet_length;
    ...
}
/* User program */
struct metadata {
...
}

parser

类似于C语言的function声明关键字,可以有循环。通过一系列状态(state)执行与转换,提取报头字段与元数据。

示例如下:

/* From core.p4 */
extern packet_in {
    void extract<T>(out T hdr);
    void extract<T>(out T variableSizeHeader,
    in bit<32> variableFieldSizeInBits);
    T lookahead<T>();
    void advance(in bit<32> sizeInBits);
    bit<32> length();
}

/* Architecture */
struct standard_metadata_t {
    bit<9> ingress_port;
    bit<9> egress_spec;
    bit<9> egress_port;
    bit<32> clone_spec;
    bit<32> instance_type;
    bit<1> drop;
    bit<16> recirculate_port;
    bit<32> packet_length;
    ...
}


/* User program */
struct metadata_t {
    ...
}

struct headers_t {
    ethernet_t ethernet;
    ipv4_t ipv4;
}

parser MyParser(packet_in packet,
    out headers hdr,
    inout metadata meta,
    inout standard_metadata_t std_meta) {
    
    state start {
        packet.extract(hdr.ethernet);
        transition accept;
    }
}

parser的输入与输出

parser MyParser(packet_in packet,
    out headers hdr,
    inout metadata meta,
    inout standard_metadata_t std_meta) {...}

parser的输入与输出模型如下:
image.png

  • parser输入:
    • packet_in类型数据包,是由目标设备提供的extern类型数据。
    • metadata: 用户自定义元数据;
    • standard_metadata_t:**设备产生提供的元数据。
  • parser输出:
    • header:用户定义的数据报头;
    • metadata: 用户自定义元数据;
    • standard_metadata_t:设备产生提供的元数据。

注意extern packet_in提供几个接口,说明如下:

  • void extract<T>(out T hdr): 从数据报文指针开始位置,抽取T类型数据大小的报头,存储在hdr中**,并将数据报文指针前移。可能触发PacketTooShort or StackOutOfBounds错误,T类型的数据的大小必须为固定值
  • void extract<T>(out T variableSizeHeader, in bit<32> variableFieldSizeInBits):读取数据报头到可变大小的报头变量variableSizeHeader中,variableSizeHeader必须包含一个及以上varbit字段。数据报指针前移
  • T lookahead<T>():读取T类型数据大小的报头,但不前移数据报指针
  • void advance(in bit<32> sizeInBits):前移数据报头指针。
  • bit<32> length():返回数据报的字节数。

parser的状态与转换

状态

parser有三种预设状态state,为:

  • start:进入parser功能块后的第一个状态,自动进入;
  • accept:若进入accept状态,则表示数据报进入后续control流程;
  • reject:数据包被抛弃。

其他states,用户可以自行定义。每个state执行0次或以上,然后转换为其他state。(允许循环

转换

  1. 使用transition关键字使parser在状态之间转换。

示例:

parser MyParser(...) {
    state start {
        packet.extract(hdr.ethernet);
        transition accept;
    }
}
  1. 使用transition select(data){...}根据data的值转换到不同状态。
    select声明类似C语言中的switch...case,但是其没有fall-through行为,不用break进行中断后续选择。
state start {
    transition parse_ethernet;
}

state parse_ethernet {
    packet.extract(hdr.ethernet);
    transition select(hrd.ethernet.etherType) {
        0x800: parse_ipv4;
        default: accept;
    }
}

state parse_ipv4 {
    packet.extract(hdr.ipv4);
    transition accept;
}

value_set

只能用在parser 语句块中。

emit

pkt.emit 不支持条件emit,mirror.emit支持单bit的条件emit

header stacks

头堆栈有两个属性 nextlast,其可以在解析器中解析时使用。

举例如下,使用如下mpls定义表示10个MPLS头:

header Mpls_h {
    bit<20> label;
    bit<3> tc;
    bit bos;
    bit<8> ttl;
}

Mpls_h[10] mpls;

mpls.next 表示 mpls 堆栈中的一个元素的值。初始时,mpls.next 指向堆栈的第一个元素,当成功调用extract方法后,mpls.next将自动向前偏移,指向下一个元素。
mpls.last指向 next 前面的那个元素(如果元素存在),即最近 extract 出来的那个元素。

注意:通过 next 或 last 属性访问头堆栈中不存在的元素将引发 transition reject 状态转换,并设置错误到 error.StackOutOfBounds

struct Pkthdr {
    Ethernet_h ethernet;
    Mpls_h[3] mpls;
    // other headers omitted
}

parser P(packet_in b, out Pkthdr p) {
    state start {
        b.extract(p.ethernet);
        transition select(p.ethernet.etherType) {
            0x8847: parse_mpls;
            0x0800: parse_ipv4;
        }
    }

    state parse_mpls {
        b.extract(p.mpls.next);
        transition select(p.mpls.last.bos) {
            0: parse_mpls; // This creates a loop
            1: parse_ipv4;
        }
    }

    // other states omitted
}

control

对于固定硬件,SwitchIngress 控制流程的参数固定,如下:

control SwitchIngress(
        inout header_t hdr,
        inout metadata_t ig_md,
        in ingress_intrinsic_metadata_t ig_intr_md,
        in ingress_intrinsic_metadata_from_parser_t ig_prsr_md,
        inout ingress_intrinsic_metadata_for_deparser_t ig_dprsr_md,
        inout ingress_intrinsic_metadata_for_tm_t ig_tm_md) {
        ...
}
  • 类似于C语言的function声明关键字,不能有循环
  • if 语句中表达式<, >两侧只能有一个变量,==两侧可以都为变量,但判断条件产生的PHV不能超过 44 bits,否则报如下错误
if (eg_md.lkp.outer_dst_mac == eg_md.lkp.outer_src_mac) {
/home/lihaifeng/test/casteni-gw/p4src/casteni_gw.p4(314): error: : condition too complex, limit of 4 bytes + 12  bits of PHV input exceeded

/home/lihaifeng/test/casteni-gw/p4src/casteni_gw.p4(314): [--Werror=legacy] error: condition too complex, one operand of > must be constant
  • 在control中,可以申明变量、创建tables,以及实例化externs等。
  • 在control中,通过apply声明功能;
  • 代表可表达为DAG的所有类型的处理,包括:
    • Match-Action Pipelines;
    • Deparsers;
    • 其他的数据报处理,如更新checksums。
  • 由用户与特定架构指定的接口,如headers和metadata。

示例:

control MyIngress(inout headers hdr,
    inout metadata meta,
    inout standard_metadata_t std_meta) {
    /* Declarations region */
    bit<48> tmp;
    apply {
        /* Control Flow */
        tmp = hdr.ethernet.dstAddr;
        hdr.ethernet.dstAddr = hdr.ethernet.srcAddr;
        hdr.ethernet.srcAddr = tmp;
        std_meta.egress_spec = std_meta.ingress_port;
    }
}

上述程序的功能是:将源MAC与目的MAC对换,然后从输入口转发出去。

action

上小结的功能可以通过简单的action语句重新实现:

control MyIngress(inout headers hdr, 
    inout metadata meta,
    inout standard_metadata_t std_meta) {
    action swap_mac(inout bit<48> src, 
        inout bit<48> dst) {
        bit<48> tmp =src;
        src = dst;
        dst = tmp;
   }
    
    apply {
        swap_mac(hdr.ethernet.srcAddr,
            hdr.ethernet.dstAddr);
        std_meta.egress_spec = std_meta.ingress_port;
    }
}

action关键字的特点:

  • 类似C语言中的function
  • action可以在control内声明,也可以在control外全局定义;
  • 参数需要有类型与方向:有方向的参数来自数据面;没方向的参数来自控制面。有方向的参数的action可以直接调用。没有方向参数的action通常在table中被调用。
  • 支持在action内部实例化变量;
  • 支持标准的算术操作,如-,+,*(注意:无除与取模运算),位运算~, &, |, ^, >>, <<,比较运算==, !=, >, >=, <, <=
  • 支持非标准操作符:1)位切片,[m:l]; 2)自增运算,++

table

table主要由keyactions两部分组成。
可选的,可以指定table其他属性,如:

  • size:表项大小
  • default_action = NoAction:默认动作
  • const entries = {}

示例:

table ipv4_lpm {
    key = {
        hdr.ipv4.srcAddr: ternary; 
	    hdr.ipv4.dstAddr: ternary; 
    }
    actions = {
        ipv4_forward;
        drop;
        NoAction;
    }
    size = 1024;
    default_action = NoAction();
    const enries = {
    }
}

一个table表项包含如下几部分:

  • 用于匹配的特定key
  • 当数据报匹配这个表项时,一个action将被执行;
  • action data(可能为空)

match-action 处理流程

image.png

大致过程主要分为两部分:

  • 数据面P4程序:

    • 定义table的格式,包括key字段、可能产生的所有actionaction data
    • 根据header与metadata,执行表查找
    • 根据匹配结果,执行相应的action
  • 控制面

    • 下发特定的流表项,下发流表的方法有:1)基于配置;2)基于自动发现;3)基于协议计算。

标准的匹配种类有:exact,ternary, lpm, index, range, valid

exact

精确匹配

ternary

基于三态内容寻址器的匹配,匹配表的每个表项都有一个掩码,将掩码和字段值进行逻辑与运算,再执行匹配。为了避免导致多条表项匹配成功,每条表项都需要设定一个优先级。对于每一个ternary字段,表项的值由两部分组成:value &&& mask,先value后mask,顺序一定不能错!mask表示在这个字段上希望匹配哪些bits,1表示匹配,0表示忽略。若 匹配字段 & mask == value 则匹配成功。例如:

const entries = {
    // 该表项表示匹配那些ip src address前24位为10.0.1的数据包,而忽略了ip src 
    // address的最后8位以及ip dst address
    (0x0a000100 &&& 0xffffff00, 0x00000000 &&& 0x00000000): your_action(0x66, 0x8888);

    // 该表项表示匹配ip src address为10.0.2.x,且ip dst address为10.0.1.1的数据包
    // 如果mask全为1,则可以省略mask而只写value
    (0x0a000200 &&& 0xffffff00, 0x0a000101): your_action(0x11, 0x2222);

    // 最后,由于是ternary匹配,一个数据包可能匹配多条表项,因而表项之间需要有优先级
    // 以上述方式书写表项时,越早出现的表项优先级越高。
    // 当使用控制器动态的加入表项时,应该显式指定插入表项的优先级以确保正确性。
}

当然,除了以const entries的形式写入表项,还可以通过P4 Controller 来写入。以下为如何构造一个新的表项(Python)

table_entry = p4info_helper.buildTableEntry(
            table_name="MyIngress.ipv4_ternary",
            match_fields={
				"hdr.ipv4.srcAddr": (0x0a000200, 0xffffff00), # (value, mask)
				"hdr.ipv4.dstAddr": (0x0b000100, 0xffffff00)
			},
            action_name="MyIngress.your_action",
            action_params={
				"param1": 0x66,
				"param2": 0x1234
			},
            priority=1
)

通过控制器写入表项时要注意一点,如果你不关心某个字段,应该直接在match_fields中直接不写该字段而不是将mask全设为0,这与const entries是不一样的。

table_entry = p4info_helper.buildTableEntry(
            table_name="MyIngress.ipv4_ternary",
            match_fields={
				"hdr.ipv4.srcAddr": (0x0a000200, 0xffffff00)
			},
            action_name="MyIngress.your_action",
            action_params={
				"param1": 0x66,
				"param2": 0x1234
			},
            priority=1
)

lpm

lpm match: 最长前缀匹配,这是三重匹配的一种特殊情况,当多个表项匹配成功时,选择掩码最长的最为最高优先级进行匹配。

上述匹配方法定义在core.p4中:

/* core.p4 */
match_kind {
    exact,
    ternary,
    lpm
}

v1model架构还支持range与selector匹配。

有些架构还支持regexp与fuzzy匹配。

apply

apply执行action,或将table应用。

示例:

control MyIngress(inout headers hdr,
inout metadata meta,
inout standard_metadata_t standard_metadata) {
    table ipv4_lpm {
        ...
    }
    action update() {
        ...
    }
    apply {
        ...
        ipv4_lpm.apply();
        update();
        ...
    }
}

deparser

deparser 将报头组装到数据报中,其不需要额外的新组建,使用control功能进行表示即可。

示例:

/* From core.p4 */
extern packet_out {
    void emit<T>(in T hdr);
}

/* User Program */
control DeparserImpl(packet_out packet,
    in headers hdr) {
    apply {
        ...
        packet.emit(hdr.ethernet);
        ...
    }
}

deparser的参数由两部分组成:

  • packet_out类型:其为定义在core.p4extern类型。其void emit<T>(in T hdr)方法,将hdr组装到数据报中,并移动数据报指针。
  • hearders: 用户定义的报头类型,方法为in

表达式

集合运算

通用集合

default 或者 _ 表示通用集合,包含指定类型的所有可能。

select (hdr.ipv4.version) {
    4: continue;
    _: reject;
}

debug

查看bmv 日志

/tmp/p4s.s1.log
/tmp/p4s.s2.log

使用debug流表

使用table读取headers或metadata信息。如:

control Myingress(...) {
    table debug {
        key = {
            std_meta.egress_spec: exact;
        }
        action = {}
    }

    apply {
        debug.apply();
    }
}

打印结果如下:

[15:16:48.145] [bmv2] [D]
[thread 4090] [96.0] [cxt 0]
Looking up key:
* std_meta.egress_spec : 2

常用技巧

分段IP识别

使用hdr.ipv4.frag_offset字段区分,如果为0,则解析,否则若数据报文分段,则不解析。

    state parse_ipv4_no_options {
        ig_md.flags.ipv4_checksum_err = ipv4_checksum.verify();
        transition select(hdr.ipv4.protocol, hdr.ipv4.frag_offset) {
            (IP_PROTOCOLS_ICMP, 0) : parse_icmp;
            (IP_PROTOCOLS_IGMP, 0) : parse_igmp;
            (IP_PROTOCOLS_TCP, 0) : parse_tcp;
            (IP_PROTOCOLS_UDP, 0) : parse_udp;
            // Do NOT parse the next header if IP packet is fragmented.
            default : accept;
        }
    }

P4Runtime

本节关注红框内的部分。

image.png

运行时控制方法比较

  • P4编译器自动生成的runtime APIs:依赖程序,很难再不重启控制面的情况下添加新的P4程序
  • BMv2 CLI:程序独立,但是目标依赖,控制面可移植性差
  • OpenFlow:目标独立,但依赖协议。openflow协议头与actions在协议规范中定义死了。
  • OCP Switch Abstraction Interface (SAI):目标独立,但协议依赖。

各运行时控制API优缺点总结如下:
image.png

可见,P4Runtime API可以做到目标与协议独立,那么,什么是P4Runtime?

  • P4目标的运行控制框架,开源了API的服务端实现(https://github.com/p4lang/PI)
  • 目前其草案v1.0已可以获得
  • 基于Protocol buffers 的API:p4runtime proto, gRPC transport
  • P4程序独立:不用随着P4程序而改变
  • 域可重配置:push 新P4程序时,无需重新编译目标交换机软件栈

Protocol Buffers基础

Protocol Buffers,简称protobuf,是一种序列化数据结构的协议。

gRPC基础

gRPC是一个高性能、通用的开源 RPC 框架,其由 Google 主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf(Protocol Buffers) 序列化协议开发,且支持众多开发语言。

P4Runtime Service

使本地或远程实体能够仲裁主控权,加载管道/程序,发送/接收数据包,以及读写转发表条目,计数器和其他P4实体。

FAQs

[–Werror=type-error] error: cast: Cannot unify bit<8> to int<16>

ig_md.ecmp_hash_int = (ecmp_hash_int_t) ig_md.ecmp_hash;

解析

Many arithmetic expressions that would be allowed in other languages are illegal in P4. To illustrate, consider the following declarations:

bit<8>  x;
bit<16> y;
int<8>  z;

The table below shows several expressions which are illegal because they do not obey the P4 typing rules. For each expression we provide several ways that the expression could be manually rewritten into a legal expression. Note that for some expression there are several legal alternatives, which may produce different results! The compiler cannot guess the user intent, so P4 requires the user to disambiguate.

image.png

解决办法:

ig_md.ecmp_hash_int = (int<16>)(bit<16>) ig_md.ecmp_hash;

Can I apply a table multiple time in my P4 program?

no (except via resubmit/recirculate)

Can I modify table entries from my P4 program?

No(except for direct counters)

What happens upon reaching the reject state of the parser?

architecture dependent

How much of the packet can I parse?

architecture dependent

一个流表可以配置多少个action,多个action对性能是否有影响?

应该不会影响性能,一个流表为一个logic stage,多个stage连接为一个pipeline,stage越少,时延越低。
vlan id -> vxlan id,写入metadata vpc级别
dst ip cidr -> sg mac 作为 inner src mac vpc子网级别
dst ip -> inner dst mac, outer dst ip,走系统路由,arp系统。 ip地址级别

匹配 ->
转发

怎么设计流表已节省流表项所占空间?

下行

  • 标志位放哪?
    vlan id -> vxlan id,写入metadata vpc级别
    dst ip cidr -> sg mac 作为 inner src mac vpc子网级别
    dst ip -> inner dst mac, outer dst ip,走系统路由,arp系统 ip地址级别

上行

is_keeping 2
flow的包过来,若is_keeping为1或2,置is_keeping为2,将数据报转发给vRouter。vRouter收到包后,vlan转vxlan,发送到pod,途径ToR。

若发送探测消息,怎么构造识别?
在vlan包中加入特殊字段,回来的是vxlan id,且ip 可选字段中有特殊字段,则若is_keeping为2,则刷为1。再发一个探测包出去?
直接发探测包,一批一个?

全部下完后,再切
上次下的流表,is_keeping为0

怎么知道流表下完成,有个开关流表,下下去后,开始发送探测包,若is_keeping全为0,则将开关流表关闭。
若到了下轮下完表时间,还是没有切完成怎么办?那就先不切,下开关流表,关闭切流。下新流表后,从新开始切,不会乱序。

到了后,构造探测包,探测包有标志,是一个普通flow的vlan包。

1: 这次下的流表
下完后,

st=>start: 数据包进来
not_probing=>condition: probing_packet_filter,不是探测包?
vlan2vxlan=>condition: mapping_table,不能执行vlan与vxlan的转换?
can_probe=>condition: can_probe_table,是否可以发探测包?
probing=>operation: probing_table,发送探测包,
若发送标志位为true,刷mapping_table,将发送标志位置为trueforward=>operation: forward_table,将数据包转发到相应的计算节点
ecmp_end=>end: ecmp_table,使用ecmp算法将数据包转到相应vRouter

st->not_probing
not_probing(yes)->vlan2vxlan(yes)->can_probe(yes)->probing->ecmp_end
vlan2vxlan(no)->forward
can_probe(no)->ecmp_end
not_probing(no)->can_probe(yes)

准备开发环境

P4-developer-day或者腾讯云盘下载虚拟机镜像,通过virtualbox导入,生成虚拟机。

**更多内容关注微信公众号:nfvschool **

参考

本文通过chatgpt代理站(支持gpt4):gptschools.cn整理

更多内容,请关注微信公众号: nfvschool
原文链接:www.nfvschool.cn

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。