dpdk提升多少

dpdk提升多少,第1张

能提升40倍。

DPDK只是单纯的从驱动拿数据,然后组织成数据块给人用,跑在用户态。功能相当于linux的设备无关接口层,处于socket之下,驱动之上。只不过linux协议栈的这部分在核心态。包处理器,很多时候是不用linux内核协议栈的,而是用专用包处理程序,类似于DPDK加上层应用处理。通常会有些硬件加速,包处理效率更高些。缺点是一旦用不上某些功能,那些加速就白费了。而纯软件处理就非常灵活,不过代价就是功耗和性能。

纯DPDK性能非常高,intel自己给出的数据是,处理一个包80时钟周期。一个36Ghz的单核双线程至强,64字节小包,纯转发能力超过90Mpps,也就是每秒9千万包。如果加上linux

socket协议栈,比如跑个纯>

《深入浅出DPDK》(朱河清)电子书网盘下载免费在线阅读

6tx5

书名:深入浅出DPDK

作者:朱河清

豆瓣评分:71

出版社:机械工业出版社

出版年份:2016-5

页数:267

内容简介:

近年来,随着网络技术的不断创新和市场的发展,越来越多的网络设备基础架构开始向基于通用处理器平台的架构方向融合,期望用更低的成本和更短的产品开发周期来提供多样的网络单元和丰富的功能,如应用处理、控制处理、包处理、信号处理等。为了适应这一新的产业趋势, 英特尔公司联合第三方软件开发公司及时推出了基于Intel® x86架构DPDK (Data Plane Development Kit,数据平面开发套件) 实现了高效灵活的包处理解决方案。经过近6年的发展,DPDK已经发展成支持多种高性能网卡和多通用处理器平台的开源软件工具包,并在成为通用处理器平台上影响力最大的数据平面解决方案。

本书汇聚了最资深的DPDK技术专家精辟见解和实战体验,详细介绍了DPDK技术发展趋势,数据包处理,硬件加速技术,包处理和虚拟化 ,以及DPDK 技术在SDN,NFV ,网络存储等领域的实际应用。文中还使用大量的篇幅讲解各种核心软件算法,最先进的数据优化思想,大量详尽的实战心得和使用指南。作为国内第一本全面的阐述网络数据面的核心技术的书籍,本书主要面向IT 网络通讯行业的从业人员,以及大专院校的学生,用通俗易懂的文字打开了一扇通向新一代网络处理架构的大门。

华为云计算服务器(又称云服务器或云主机),是云计算服务体系中的一项主机产品,该产品有效的解决了传统物理租机与VPS服务中存在的管理难度大,业务扩展性弱的缺陷。在实际应用中的云主机具有三个方面的d性能力:主机服务配置与业务规模可灵活的进行调整;主机集群内d性可伸缩;计费方式灵活。

DPDK提供了三种classify算法:最长匹配LPM、精确匹配(Exact Match)和通配符匹配(ACL)。

其中的ACL算法,本质是步长为8的Multi-Bit Trie,即每次可匹配一个字节。一般来说步长为n时,Trie中每个节点的出边为2^n,但DPDK在生成run-time structures时,采用DFA/QRANGE/SINGLE这几种不同的方式进行数据结构的压缩,有效去除了冗余的出边。本文将为大家介绍ACL算法的基本原理,主要内容包括:trie树的构造、运行时的node array生成和匹配原理。对于ACL接口的使用,参考DPDK的官方文档即可。

ACL规则主要面向的是IP流量中的五元组信息,即IP/PORT/PROTO,算法在这个基础上进行了抽象,提供了三种类型的匹配区域:

熟悉这三种类型的使用后,完全可以用它们去匹配网络报文的其它区域,甚至将其应用到其它场景中。

具体来说,rte_acl_field_def有5个成员:type、size、field_index、input_index、offset。

如果要深入理解算法,可以思考这几个字段的意义,或者换个角度来看:

对于规则的定义,要注意如下两点:

比如定义了5个field,那么请给出每一个的具体定义:

像field[1]中IP和mask都为0,表示匹配所有的IP地址;field[3]中range由0到65535,表示匹配所有。类似这样的全匹配一定要显示的定义出来,因为如果不明确定义,这些字段的值取决于编译器的,最后编译的ACL规则很可能与原有设想存在偏差。

如果在规则中,对于某个field不进行限制,对于不同type的field,规则书写时有一定差异:

对于BITMASK和MASK类型,全0代表匹配所有,如上例中的field[0]、field[1];

对于RANGE,则按照上述field[3]中的形式定义。

规则定义好后,会转换为trie树并最终合并到一起。

实际处理过程中,build_trie函数会自底向上的将rule中的每个field转换为node,然后将这些node合并生成这条rule的trie,最后将这个trie与已有的trie进行merge,最终生成整个rule set的trie。

tire由node组成,其主要数据成员如下:

node中values成员用于记录匹配信息,ptrs则用于描述node的出边,用于指向转换后的node。

values采用bitmap进行压缩,其数据结构为struct rte_acl_bitset values; 一个byte取值范围是[0,255],可通过256个bit位来进行对应,并实现byte值的快速查找:即通过第x位的bit值是否为1来判断是否包含数值x(0 <= x < 256)。

num_ptrs用于描述出边数目,ptrs即为实际的出边,它记录了其匹配值values和匹配后的节点指针。

match_flag和mrt则用于记录匹配结果,trie树中叶子节点一定是记录匹配结果的节点。

trie树其详细结构比较复杂,这里将其结构进行简化,如下所示:

上图的trie树有4个node,通过ptrs进行指向,values字段为匹配值的bitmap表示,为了表述的简洁,后续会采用simple的方式进行描述。

在trie simple中,实心节点表示匹配节点,边上的数字代表匹配值(为便于阅读,采用实际值而不再是bitmap形式),…代表其它匹配值。

不同type的field,转换为node的方式会有所不同。

目前提供的3种类型:BITMASK描述一个byte的匹配,支持mask模式;MASK用于描述4个byte的匹配,支持mask模式;RANGE描述2个byte的匹配,此时mask表示上限。

field到node的转换,见build_trie中的for循环,具体转换函数则参考:

对于BITMASK,如{valueu8 = 6, mask_rangeu8 = 0xff,},它最后的转换形式如下:

构造field的node时,总会在结尾添加一个空的end节点,最后一个field除外(它是match node)。在for循环中每完成了一个field的解析后,会将其合并到root中,从而生成这个rule的trie。

合并前,也会先构造一个空的end node(见build_trie函数中,while循环下的root创建),让它与field构成的node头合并,因为不相交,所以merge时会将匹配信息合并到end node并释放原有的头,并将field链的end节点返回(保存到end_prev中),下次合并时,就用此end节点与新的node头合并。

循环遍历完所有的field后,这些node就串联起来了,构成这个rule的trie。

对于多个rule,每次构造完成后会merge到整体的trie中。

这里详细介绍下merge算法原理,其实仔细阅读acl_merge_trie函数的注释即可。

对于node A和node B的merge, acl_merge_trie函数返回一个节点,这个节点指向它们路径的交集。

这里给出三个例子用于展示merge前后的变化。为了减少状态点,构造rte_acl_field_def如下:

示例1:

acl_rules[1]为trie A,acl_rules[0]对应trie B,最终trie B合并到trie A上,具体如下:

1和1’合并时,因为level为0,所以1’直接合并到1中;

4和4’合并时,因为节点无交集,所以创建新节点c1(node 4的拷贝),并将4'上的边拷贝到c1中。

示例2,rule类别相同,但优先级不同:

acl_rules[1]为trie A,acl_rules[0]对应trie B,最终trie B合并到trie A上,具体如下:

6和6’是match node,类别相同,且6的优先级为2大于6’的优先级。

6和6’合并时,直接返回6。而前面创建的新节点,如d1,已包含5’的所有边(非ACL_INTERSECT_B),所以最终返回5,free d1。

同理依次往上回溯,a4,b3,c2,也依次被释放,最终merge的trie即为原来的trie A。

示例3,rule类别不同,优先级相同:

acl_rules[1]为trie A,acl_rules[0]对应trie B,最终trie B合并到trie A上,具体如下:

6和6’是match node,因为类别不同,所以最终创建了新node e1,这也导致示例3和示例2最终merge结果的不同。

合并是一个递归的过程,逆向思考构造过程会有助于理解算法。另外,在build_trie之前会sort_rule,匹配范围更大的rule会放到前面优先构造trie,个人为这样node A包含node B的概率更大,这可能也是merge时创建的node C是A的拷贝而不是B的拷贝的原因,因为这样出现ACL_INTERSECT_B的概率相对较低。

一些说明:

trie树构造完成后,会将其由指针跳转的形式转换为等效的数组索引形式,即node array,既可让匹配数据更紧凑,也可提高匹配算法的效率。

采用node array的方式进行状态点的压缩是很常见的优化方式,比如snort里面的ac算法(acsmxc):

笔者也曾经做过类似的优化,通过将出边由指针方式修改为索引方式,整个匹配tree的内存占用只需要原来的1/5。

将指针方式转换为node array形式是优化的第一步,对于Next[256]出边又可以采用多种压缩方式,比如snort中新的ac算法(acsmx2c),就采用了Sparse rows和Banded rows等多种压缩方式,但其原理是将出边进行映射转换,本质上还是做DFA状态跳转。

DPDK对边的压缩方式与上述类似,不过它优化的粒度更细,不同type的node有不同的压缩方式:

比如在示例三中,node 1为DFA节点(根节点强制使用DFA方式),2、3、a5、b4、c3、d2为QRANGE,4、5为SINGLE,6、e1为MATCH。

2、3、a5、b4虽然在图上仅有一条有效边,但它不为SINGLE,因为对于无效的匹配其实也会有出边,所以它的真实出边数目并不唯一,只有像4、5这类全匹配节点才是真正的SINGLE节点。

在构造node array前,会调用acl_calc_counts_indices函数更新node的node type,fanout等信息。

node type依据其fanout值决定,fanout计算见acl_count_fanout函数,其原理是:

比如对于示例3中的d2节点:

fanout计算完成后,若其值为1则为SINGLE节点,(1, 5]为QRANGE节点,(5, 256]为DFA节点。

注意:对于trie树的root节点,不论fanout值为多少,会强制将其构造为DFA节点,且其fanout值会重新计算。

type和fanout计算完成后,会统计各类节点数目,信息保存在acl_calc_counts_indices传入的counts参数中,随后rte_acl_gen依据这些信息将整块的node array内存分配出来,其布局大致如下:

Data indexes中用于保存在rte_acl_field_def中定义的offset;

Results对应match node,用于保存匹配结果。

Trans table包含整个匹配过程中的跳转点:

静态将整块node array分配完成后,就需要依据trie 树的node信息填充Trans table和Results了,具体过程见acl_gen_node函数;Data indexes的填充则在acl_set_data_indexes中完成。

22中的内存布局大致描绘了各种类型节点的分布情况,DFAs内部由一个一个的DFA节点组成,QUADs和SINGLEs也一样,都是由相同类型的节点构成。

对于每一个节点,其结构则类似如下形式:

DFA节点的fanout一般为4,出边数为fanoutRTE_ACL_DFA_GR64_SIZE;(图中画的为fanout为4的情况,256条出边)

QUAD节点的fanout不超过5,即为节点的出边数不超过5;(图中画的为fanout为4的情况)

SINGLE节点只有一个出边;

图中的trans即为这个节点的出边,它本质是一个uint64的数据结构,通过trans和input信息即可计算得到下一个节点的index,从而实现匹配跳转。trans中不同bit位包含着丰富的信息,具体见aclh中的说明即可。

高32位对于不同类型的节点有不同的解释:

低32位:

在实际处理过程中,通过高32位与input_byte计算得到index,与低32位中的addr,即可快速定位到下一个trans:trans_table + (addr+index)。

这里的处理其实与传统的DFA跳转差别很大,传统处理时,next = node[‘input’],跳转到下一个节点,然后采用next[‘input’]进行跳转和匹配,即使有数据结构的压缩,跳转目标仍是状态点。但DPDK中,跳转时直接采用trans_table + (addr+index),直接找到了状态点的边(trans),而不是到状态点。

跳转表具体构建时,采用acl_gen_node函数完成:

匹配的过程与跳转表的构建其实是互为一体的,如何构建跳转表就决定了如何进行匹配。

在23节,对于跳转的形式已进行了说明,具体可阅读rte_acl_classify_scalar函数:跳转时直接采用trans_table + (addr+index),直接找到了状态点的边(trans),而不是到状态点。

对于具体的匹配过程,还有一点需要注意,即GET_NEXT_4BYTES的使用,每次匹配时候都会去4BTYTES进行匹配,这也是为什么定义input fields时要求4字节连续。比如我在dpdk-dev邮件组中问的这个 问题 。

解决4字节连续,可以通过定义相同的input_index来解决,比如像邮件中提到的设置sport/dport的input_index相同,这是因为data indexes的构造取决于input_index,见acl_build_index函数;同时field_index不同、input_index相同时可避免对field区间的优化(如果优化,将某个field去掉了,这时4字节匹配会失效)。邮件中的问题,正是因为field[3]被优化掉后,4字节连续匹配出现问题。

在特定的场合还必须通过指定size为32来解决,即使type类型为BITMASK,见DPDK的ACL文档中关于 tos示例的说明 。

另外再说下field_index,前面提出一个问题:field_index是否多余?

答案是不多余,因为算法中会对field进行优化,如果不指定field_index字段,这个优化就无法进行了,具体的优化处理见acl_rule_stats函数。

优化过程中要进行input_index的判断,这是因为相同的input_index可以有多个field,但其中只有某个field是completely wild时应避免进行优化。只有相同input_index的所有field都是completely wild时,才应该将这个field优化掉。

上面的一系列说明,都是针对GET_NEXT_4BYTES每次匹配四个字节的匹配进行的补充说明。

匹配的具体过程,这里用图形的方式进行简要说明,为了能有多种类型的node,这里构造规则如下:

trie树如下所述:

对应的node array如下图所示:

假设输入数据为:proto 16, ip 1921288,则transition跳转方式如上图红线所示:

注意:node array中indexes、DFA0和idle省略了。

关于trie树相关的理论知识参考 这里 。

本文主要介绍了DPDK的ACL算法,详细描述了如何由规则生成trie,并将trie转换为node array的过程,在文末通过示例介绍了具体的匹配过程。文章旨在介绍ACL算法的基本思路,希望对大家能有所帮助。

下图展示了内部数据结构的细节。

在不同的CPU核上处理同一个发送端口的入队/出队 *** 作可能会对调度器的性能造成重大影响,因此不建议这样做。

端口入队/出队 *** 作需要共享对以下数据结构的访问:

以下 *** 作会造成预期的性能下降:

因此,调度程序的入队和出队 *** 作必须在同一个线程中运行,从而允许队列和Bitmap *** 作可以是非线程安全的,并保持调度程序的数据结构在同一个CPU核内部。 尽量将数据结构局部化,避免跨线程共享。

增加NIC端口的数量仅仅需要按比例增加用于流量调度的CPU核的数量。

包处理步骤如下:

需要注意的是,这些步骤之间有很强的依赖关系,在步骤1和步骤2的结果可用之前,步骤2和步骤3不能启动,因此没法利用处理器乱序执行引擎提供的任何性能优化。

当有大量的数据包和队列需要处理时,很有可能处理当前包所需要的数据结构不在L1和L2缓存中,因此上述3个步骤对内存的访问结果会造成L1/L2 cache miss。考虑到性能因素,每个数据包都造成3次L1/L2 cache miss是不可接受的。

解决方案是提前预取需要的数据结构。预取 *** 作有执行延迟,在此期间处理器不应该尝试访问当前正在预取的数据结构,因此处理器应该执行其他工作。唯一可做的其他工作是对其他输入包执行入队 *** 作流程的不同步骤,从而以流水线的方式处理入队 *** 作。

图2演示了入队 *** 作的流水线实现,其中有4个流水线阶段,每个阶段处理2个不同的输入包。在给定的时间内,任何输入包都不能成为多个流水线阶段的一部分。

上面描述了非常基本的入队流水线拥塞管理方案:将数据包塞入队列,当队列被填满时,所有到达同一队列的数据包将被丢弃,直到有数据包通过出队 *** 作被消费掉,才可以在队列中塞入新的数据包。如果用RED/WRED改进入队流水线,就可以通过查看队列占用率和包的优先级,为特定的包做出入队/丢弃决策(而不是不加选择地将所有包入队或者丢弃)。

在当前管道调度处理下一个包的步骤为:

以上 *** 作涉及到的数据结构(pipe, queue, queue array, mbufs)可以在被访问之前预取进缓存,从而避免cache miss。在对当前管道(管道A)的数据发出预取 *** 作后,可以立即切换到另一个管道(管道B)进行处理,等到管道A的预取 *** 作完成,再切换回去,从而利用预取本身的时延同步处理多个管道的数据。

出队状态机利用处理器缓存机制,尽可能同时处理多个活跃管道,从而发送更多的数据包。

输出端口就是一个被调度器不断的用待传输数据填充的队列。对于10 GbE,每秒有125亿个字节需要被端口调度器填充。如果调度器的速度不够快,不能填满队列(假设有足够的待传输数据包和credit),那么必然有一些带宽会被闲置,从而产生浪费。

原则上,分层调度器出队 *** 作应该由网卡发送器触发。通常情况下,一旦网卡发送队列的输入流量低于一个预定义的阈值,端口调度程序将被唤醒(基于中断或轮询的方式,通过持续监控队列占用决定是否被唤醒),填充更多的数据包进入队列。

调度器需要跟踪随着时间推移而变化的credit信息,这需要基于时间进行credit的更新(例如,子端口和管道流量整形、流量组上限的强制执行,等等)。

每次调度器决定向网卡发送器传递数据包时,调度器将相应地调整其内部时间信息。因为在物理接口上每发送一个字节所需的时间是固定的,所以可以以字节为单位作为内部时间的参考值,这样处理起来也会比较方便。当一个包准备传输时,时间以(n + h)的形式递增,其中n是以字节为单位的包长度,h是每个包的帧数据字节数(包括帧头、CRC等)。

调度器需要将其内部时间参考值与端口速率对齐,从而确保调度器不会填充超过网卡发送速率的数据,从而防止由于网卡发生队列已满而发生的丢包。

调度器需要读取每次出队调用的当前时间。CPU时间戳可以通过读取TSC寄存器(Time Stamp Counter)或HPET寄存器(High Precision Event Timer)来获得。当前CPU时间戳需要通过以下公式将CPU时钟转换为字节数:time_bytes = time_cycles / cycles_per_byte, cycles_per_byte是传输一个字节所消耗的CPU周期(例如,10GbE的端口以2GHz的CPU频率传输数据,cycles_per_byte = 2G/(10G/8) = 16)。

调度器会维护针对网卡时间的内部时间引用,网卡时间将随着被调度的数据包的长度(包含帧开销)的增加而增加。在每次出队调用时,调度程序会根据当前时间检查其对网卡时间的内部引用:

调度器往返延迟(SRTD,Scheduler Round Trip Delay)是调度器连续两次检查同一管道之间的时间(以CPU周期为单位)。

为了跟上发送端口的速率(即避免网卡带宽的浪费),调度器应该能够比网卡发送器实际传输速度更快地调度数据包。

假设没有发生端口带宽超卖,调度器需要根据配置的令牌桶,跟踪每个管道的速率。这意味着管道的令牌桶的大小应该设置得足够高,以防止它因较大的SRTD而溢出,从而导致管道的可用带宽损失。

当以下所有条件都满足时,调度器可以为(子端口S、管道P、流量组TC、队列Q)做出发送下一个包的合理调度决策:

如果以上条件都满足,则可以选择一个数据包进行传输,并从子端口S、子端口S的流量组TC、管道P、管道P的流量组TC中减去必要的credit。

由于所有数据包长度的最大公约数是一个字节,因此以字节作为credit的计量单位。传输n字节的数据包所需的credit数等于(n+h),其中h等于每个数据包的帧开销(以字节为单位)。

子端口和管道的流量整形是通过令牌桶来实现的,每个令牌桶使用一个饱和计数器(saturated counter)来实现,通过这个计数器跟踪credit的数值。

令牌桶泛型参数和 *** 作如下表所示:

为了实现上面描述的令牌桶通用 *** 作,当前设计使用下表中给出的持久数据结构:

可以用以下公式计算桶速率(单位为字节/秒):

式中,r = 端口线速(单位为字节/秒)。

令牌桶 *** 作的实现如下表所示:

同一管道内TC的优先级是严格定义的,其调度是由管道出队状态机实现的,该状态机按照优先级升序排列选择队列。因此,队列0(与TC 0相关,优先级最高的TC)在队列1(优先级低于TC 0的TC 1)之前被处理,队列1在队列2(优先级低于TC 1的TC 2)之前被处理,并继续下去,直到除最低优先级TC以外的所有TC队列都被处理。最后,队列12-15(尽力而为TC, 最低优先级TC)被处理。

管道和子端口级别的TC不支持流量整形(traffic shaping),所以在这两个级别的上下文里没有维护令牌桶。管道和子端口级别的TC上限被强制限定为周期性回填的credit,每次在管道/子端口级别处理包的时候,credit就会被消费掉,如下两表所示。

子端口/管道TC强制上限的持久化数据结构:

最低优先级TC(尽力而为TC,best effort TC)的WRR设计方案从简单到复杂的演变如下表所示。

子端口流量组X的超售是一个配置问题,当子端口的成员管道为流量组X分配的带宽大于在上层子端口级别为同一流量组分配的带宽时发生。

特定子端口和流量组的超售完全是管道和子端口级配置的结果,而不是由于运行时流量负载的动态演化(如拥塞)造成的。

如果当前子端口对流量组X的总体需求较低,超售的存在并不代表问题,因为所有成员管道对流量组X的需求已经完全可以满足。然而,当所有子端口的成员管道的流量组X的需求合在一起超过了在子端口级别配置的限制时,这将不再能够实现。

下表总结了处理这个问题的一些可能的方案,当前实现是基于第三种方案。

通常,子端口TC超售只对最低优先级的流量组启用,这通常用于尽力而为(best effort)的流量,管理平面需要防止其他(更高优先级)的TC发生这种情况。

为了便于实现,还假设子端口的最低优先级TC的上限设置为子端口速率的100%,并且对于所有子端口的成员管道,管道最低优先级TC的上限设置为管道速率的100%。

该算法会先计算一个容量(watermark),容量会根据子端口的成员管道当前的需求定期更新,其目的是限制每个管道允许发送的最低优先级(best effort)TC的流量。在每个TC上限计算周期开始时,在子端口级别上计算该容量,并在当前实施周期内在所有子端口的成员管道上使用相同的容量值。下面说明在每个周期开始时作为子端口级别计算的容量如何传播到所有子端口的成员管道的。

当前计算周期开始(和上一个计算周期结束的时间相同)时,容量值需要基于上一周期开始时分配给尽力而为TC但没有被子端口的成员管道消费掉的带宽值做出调整。

如果子端口有尽力而为TC带宽没用掉,就增加当前时段的容量值,以鼓励子端口的成员管道消耗更多带宽。否则,降低容量值,以强制子端口成员管道的尽力而为TC带宽的消耗相等。

容量值的增加或减少会以很小的增量进行,因此可能需要几个执行周期才能达到平衡状态。由于子端口成员管道对尽力而为TC的需求的变化,这种状态可以在任何时候发生改变,例如,由于需求增加(当需要降低容量值时)或需求减少(当可以增加容量值时)。

在需求较低时,为防止子端口成员管道占用太多带宽,需要设置高容量值。容量的最高值被选为子端口成员管道配置的最高速率。下表说明了容量 *** 作。

每个TC流量限制执行周期开始时,从子端口级别到成员管道的容量传递:

容量值计算:

为了调度一个报文,调度器必须在多个队列里查看报文和credit,当调度器需要查看的队列越多,其性能就越低。

调度器维护了活跃队列的Bitmap,从而可以直接跳过非活跃队列,但是为了检测特定的管道是否有足够的credit,必须使用管道发送状态机进行探测,无论最终调度结果如何(没有报文或者至少产生一个报文),总会消耗CPU时间。

这个场景强调了策略对调度器性能的重要性:如果管道没有足够的credit,其数据包应该尽快丢弃(在触发分层调度器之前),从而可以将管道队列标识为不活跃,发送端就可以跳过这个管道而不用消耗资源去查看管道credit。

端口调度器的性能针对大量队列进行了优化。但如果队列的数量很少,那么对于相同级别的活动流量,端口调度器的性能预期会比一小组消息传递队列的性能更差。

以上就是关于dpdk提升多少全部的内容,包括:dpdk提升多少、怎么使用dpdk的steup.sh安装、《深入浅出DPDK》pdf下载在线阅读全文,求百度网盘云资源等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zz/10632708.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-10
下一篇 2023-05-10

发表评论

登录后才能评论

评论列表(0条)

保存