QNX-Adaptive Partition

背景

本文主要是对QNX官方文档中Adaptive Partitaon 内容的一个概括,原文请移步QNX Adaptive Partation

在设计大型分布式系统时,本身就十分复杂,典型的系统具有大量子系统、进程和线程,彼此隔离开发。一旦所有这些不同的子系统都集成到一个共同的运行时间环境中,系统的所有部分都需要抢占资源,保证自己的操作在所有情况(正常,高负载,系统异常)下都可以响应。

鉴于平常的系统中,系统问题在集成产品时总是会出现。通常,一旦系统运行,就会发现导致严重性能退化的不可预见的相互影响。当出现此类情况
时,解决方案通常需要大量修改(经常通过试验和错误)才能找到。这影响了系统稳定性,影响了系统集成和发布。

这主要是因为这些组之间没有"预算"CPU使用的有效方法。线程优先级提供了确保关键任务运行的方法,但不为重要非关键任务提供有保证的 CPU 时间,而这些任务在正常操作中可能处于饥饿状态。此外,建立线程优先级的常见方法很难在大型开发团队中扩展。

QNX的Adaptive Partitioning主要就是用来解决这一问题:

  • 当系统负载高时,保证可获得一个最小CPU运行时间。
  • 防止不重要的或者非可信应用独占系统资源。
  • 提供安全性。防止资源被占用导致功能无响应。
  • 使嵌入式系统在开发或部署过程中的调试更加容易。
    在这里插入图片描述
    如上图,就是做了资源分区后的例子,DOS攻击一个分区导致分区CPU满载,不会影响到其他分区的任务执行。

内容

系统高负载问题解决方案

通常遇到系统中负载过高的情况时,首先能想到的是将CPU算力划分,保证进程在使用时能有自己预留的算力。

为了解决这个问题,一些系统使用虚拟墙,称为分区,围绕一组应用程序,以确保每个分区被赋予一套工程资源。考虑的主要资源是 CPU 时间,但也可以包括任何共享资源,如内存和文件空间(磁盘或闪存)。
这种方案有静态分区和自适应分区两种。能够为系统带来如下收益:

  • 提升产品性能。确保系统中可用的任何空闲时间提供给其他分区。

  • 处理设计复杂性。

静态分区

通过使用多个分区,可以避免出现单个故障点,比如单个分区进程异常,占用了所有该分区资源,但不会影响其他分区的使用,分区提供:

  • 内存保护 -每个分区是离散的,由内存管理单元 (MMU) 控制
  • 超载保护 - 每个分区都保证一段执行时间

在这里插入图片描述
但是这种分区方式不灵活,在单个分区没有使用资源时,这部分资源不能被其他分区使用。如果大多数分区处于空闲状态,并且一个分区非常繁忙,则繁忙的分区不会收到任何额外的执行资源。

自适应分区

与静态分区一样,自适应分区有分配给它的预算,以保证其在 CPU 资源的最小份额。与静态分区不同,自适应分区:

  • 可以根据需要动态添加和配置自适应分区。
  • 在正常负载下可以做系统调度器,动态分配资源,但即使在超载条件下仍能提供最小的执行资源。
  • 通过在系统未加载时需要额外资源的分区之间分配分区未使用的预算,最大限度地利用 CPU 的资源。

最多可有 32 个分区。在QNX 中微子中,是线程,而不是分区,是预定的。

自适应分区是一组命名的规则。当一个进程或线程与特定的调度器分区相关联时,其操作受当时该分区规则的约束。
例如,自适应分区类似于属于俱乐部的人。每个人都可以加入几个不同的俱乐部。他们有时甚至可以从一个俱乐部转会到另一个俱乐部。然而,当他们在一个特定的俱乐部,他们同意遵守该特定俱乐部的规则。

系统要求

在 x86 系统上,关闭可能导致处理器进入系统管理模式 (SMM) 的任何 BIOS 配置。防止SMM的一个典型原因是它以不可预知的间隔引入约100微秒的中断延迟。

自适应分区使用方法

看下系统中是否有自适应分区

aps show

创建自适应分区

如果没有信息则需要自己创建自适应分区,步骤如下:

  1. 进入包含系统启动图像构建文件的目录。

  2. 创建生成文件的副本。

    cp my_buildfile.build apsdma.build
    
  3. 编辑副本并搜索开始procnto 的行。行可能看起来像这样:

    PATH=/proc/boot:/bin:/usr/bin:/opt/bin \
    LD_LIBRARY_PATH=/proc/boot:/lib:/usr/lib:/lib/dll:/opt/lib \
    procnto-smp-instr
    
  4. 添加到行的开头:[module=aps]

[module=aps] PATH=/proc/boot:/bin:/usr/bin:/opt/bin \
LD_LIBRARY_PATH=/proc/boot:/lib:/usr/lib:/lib/dll:/opt/lib \
procnto-smp-instr
  1. 保存对构建文件的更改。

  2. 生成新的引导图像:

mkifs apsdma.build apsdma.ifs
  1. 将新图像传输到目标系统。
  2. 重新启动目标系统。

添加一些额外的分区

一个叫做分区,预算为CPU时间的10%
另一个称为分区B,预算为30%

aps create -b10 partitionA
aps create -b30 partitionB

使用aps实用程序列出系统上的分区:

$ aps show -l
                    +-------- CPU Time ------+-- Critical Time --
Partition name   id | Budget |  Max |   Used | Budget |      Used
--------------------+------------------------+-------------------
System            0 |    60% | 100% |  0.21% |  100ms |   0.000ms
partitionA        1 |    10% | 100% |  0.00% |    0ms |   0.000ms
partitionB        2 |    30% | 100% |  0.00% |    0ms |   0.000ms
--------------------+------------------------+-------------------
Total               |   100% |      |  0.21% |

使用命令在分区中启动进程:

on -Xaps=partitionA ./looper A 5
on -Xaps=partitionB ./looper B 5

运行耗时程序在分区B,在那里它会消耗尽可能多的CPU

on -Xaps=partitionB ./greedy &

您可能会惊讶地发现,循环器的两种情况继续以大致相同的速度运行。来自aps实用程序的输出可以解释原因:

                   +-------- CPU Time ------+-- Critical Time --
Partition name   id | Budget |  Max |   Used | Budget |      Used
--------------------+------------------------+-------------------
System            0 |    60% | 100% |  0.29% |  100ms |   0.000ms
partitionA        1 |    10% | 100% |  2.26% |    0ms |   0.000ms
partitionB        2 |    30% | 100% | 97.46% |    0ms |   0.000ms
--------------------+------------------------+-------------------
Total               |   100% |      |100.00% |

分区B的使用量超过其30%的预算。这是因为其他分区没有使用其预算。调度器没有在其他分区中运行空闲线程,而是给需要它的分区未使用的时间。

在分区中开始另一个耗时应用实例:

on -Xaps=partitionA ./greedy &
                   +-------- CPU Time ------+-- Critical Time --
Partition name   id | Budget |  Max |   Used | Budget |      Used
--------------------+------------------------+-------------------
System            0 |    60% | 100% |  0.17% |  100ms |   0.000ms
partitionA        1 |    10% | 100% | 26.89% |    0ms |   0.000ms
partitionB        2 |    30% | 100% | 72.94% |    0ms |   0.000ms
--------------------+------------------------+-------------------
Total               |   100% |      |100.00% |

系统分区未使用的时间被分配给其他两个分区,根据它们在 CPU 时间中所分的份额。

在系统分区中开始另一个贪婪应用实例:

on -Xaps=System ./greedy &

系统中现在没有空闲时间,因此每个分区只能获得其最少的保证 CPU 时间。

                    +-------- CPU Time ------+-- Critical Time --
Partition name   id | Budget |  Max |   Used | Budget |      Used
--------------------+------------------------+-------------------
System            0 |    60% | 100% | 55.67% |  100ms |   0.000ms
partitionA        1 |    10% | 100% | 11.84% |    0ms |   0.000ms
partitionB        2 |    30% | 100% | 32.49% |    0ms |   0.000ms
--------------------+------------------------+-------------------
Total               |   100% |      |100.00% |

线程调度器

自适应分区的核心是线程调度器,他是一个可配置的线程调度器,可保证 CPU 吞吐量的最低百分比到线程、进程或应用程序组。分配给分区的 CPU 时间百分比称为预算。

线程调度器详细规则

自适应分区线程调度器是一个可选的线程调度器,可保证 CPU 吞吐量的最低百分比到线程、流程或应用程序组。
我们称我们的分区具有自适应性,因为它们的内容是动态的:

  • 您可以动态地将应用程序启动到分区中。
  • 子线程和子进程自动运行父级线程/进程所在相同的分区。

CPU使用率计算

自适应分区线程调度器通过测量每个分区的平均 CPU 使用量来限制 CPU 的使用。平均值在一定时间(通常为 100 毫秒)上计算,该值可配置。
时间定义了线程调度器试图平衡分区与保证 CPU 限制的平均时间。可以将计算时间大小设置为任何值,从 8 毫秒到 400 毫秒不等。
不同选择会影响负载平衡的准确性,在极端情况下,即用线程会产生最大的延迟。

CPU如何划分时间分区

对于每个调度操作,线程调度器在选择要运行的线程之前检查每个分区。如果分区超出预算(这意味着过去 100 毫秒的 CPU时间消耗超过分区大小,其他分区也要求时间)并且螺纹想要运行,则线程调度器不会运行线程。

负载不足

当分区在计算时间内比其定义预算需要更少的 CPU 时间时,就会出现负载不足。例如,如果我们有三个分区:
系统分区,预算为 70%
分区Pa,预算为20%
分区Pb,预算为10%

                    +-------- CPU Time -------+-- Critical Time --
Partition name   id | Budget |  Max |    Used | Budget |      Used
--------------------+-------------------------+-------------------
System            0 |    70% | 100% |   6.23% |  200ms |   0.000ms
Pa                1 |    20% | 100% |  15.56% |    0ms |   0.000ms
Pb                2 |    10% | 100% |   5.23% |    0ms |   0.000ms
--------------------+------------------+--------------------------
Total               |   100% |      |  27.02% |

在这种情况下,所有三个分区的需求都低于其预算。

在这里插入图片描述

每当分区需求低于其预算时,线程调度器都会通过选择最优先的线程来执行。

闲暇

该状态发生在一个分区不运行时。然后,线程调度器将该分区的时间给其他运行分区。如果其他运行分区需要足够的时间,则允许它们超过预算。
例如,假设我们有这些分区:
系统分区,预算为 70%,但没有运行
分区Pa, 预算 20%, 运行无限循环优先 9
分区Pb,预算为10%,在优先级10运行无限循环
由于系统分区不需要时间,因此其预算时间成为空闲时间。如果使用默认调度策略(SCHED_APS_SCHEDPOL_DEFAULT),则线程调度器将剩余时间分配给系统中最高优先级的线程。在这种情况下,这是分区Pb中的无限循环线程。因此,aps 显示命令的输出可能是:

                    +-------- CPU Time -------+-- Critical Time --
Partition name   id | Budget |  Max |    Used | Budget |      Used
--------------------+-------------------------+-------------------
System            0 |    70% | 100% |   0.11% |  200ms |   0.000ms
Pa                1 |    20% | 100% |  20.02% |    0ms |   0.000ms
Pb                2 |    10% | 100% |  79.83% |    0ms |   0.000ms
--------------------+-------------------------+-------------------
Total               |   100% |      |  99.95% |

在此示例中,分区Pa获得其保证的最低 20%,但所有空闲时间都给予分区Pb。这是因为线程调度器严格按照优先级在分区之间进行选择,只要没有分区仅限于其预算。此策略确保最实时的行为。

满负荷

当所有分区都要求其全额预算时,就会出现满负荷。

                    +-------- CPU Time -------+-- Critical Time --
Partition name   id | Budget |  Max |    Used | Budget |      Used
--------------------+-------------------------+-------------------
System            0 |    70% | 100% |  69.80% |  200ms |   0.000ms
Pa                1 |    20% | 100% |  19.99% |    0ms |   0.000ms
Pb                2 |    10% | 100% |   9.81% |    0ms |   0.000ms
--------------------+-------------------------+-------------------
Total               |   100% |      |  99.61% |

在此示例中,满足分区保证预算的要求优先于优先级。

即使在满负荷时,线程调度器也可以为一组可工程的关键线程提供实时延迟

调度器的开销不会随着线程数的增加而增加。但是,它可能会随着分区数目的增加而增加,因此应该尽可能少地使用分区。

修改调度策略

可以通过SchedCtl ()或aps来修改规则。
SCHED_APS_SCHEDPOL_FREETIME_BY_RATIO
aps modify -S freetime_by_ratio

根据繁忙分区的预算比率划分空闲时间。在我们的示例中,自由时间按比率模式可能会导致aps 显示命令显示:

                   +-------- CPU Time -------+-- Critical Time --
Partition name   id | Budget |  Max |    Used | Budget |      Used
--------------------+-------------------------+-------------------
System            0 |    70% | 100% |   0.04% |  200ms |   0.000ms
Pa                1 |    20% | 100% |  65.96% |    0ms |   0.000ms
Pb                2 |    10% | 100% |  33.96% |    0ms |   0.000ms
--------------------+-------------------------+-------------------
Total               |   100% |      |  99.96% |

这表明,在自由时间按比例模式下,线程调度器将空闲时间在分区Pa和Pb之间以大约 2:1 的比例划分,即其预算比率。

SCHED_APS_SCHEDPOL_PARTITION_LOCAL_PRIORITIES
aps modify -S partition_local_priorities
纯粹按预算比率安排,不会超预算
此选项包括SCHED_APS_SCHEDPOL_FREETIME_BY_RATIO的行为。
关键线程不受此策略的影响。

SCHED_APS_SCHEDPOL_LIMIT_CPU_USAGE
aps modify -S limit_cpu_usage

强制执行参数,限制分区在系统加载不足时可能超支的正常预算金额。

小结

下表总结了线程调度器如何以正常和自由时间按比率模式划分时间:

分区状态正常自由时间按比例
使用<预算按优先级按优先级
使用>预算,有空闲时间按优先级按预算比率
满负荷按预算比率按预算比率
分区在任何负载下运行关键线程按优先级按优先级

分区继承

在这里插入图片描述
概括下就是进程的子进程或者线程继承相同的规则。

关键线程

关键线程,即使其分区超过预算(前提是分区有关键时间预算)也是允许运行的。创建分区时可指定关键分区预算和优先级。
关键线程始终是实时响应的,即使系统已满载,或在同一分区中的其他线程被限制以满足预算的任何时间。
要使此正常工作,系统中不得存在许多关键线程。如果关键线程占多数,则线程调度器将很少能够保证所有分区的最低 CPU 预算,并且系统降级为基于优先级的线程调度器。

关键线程唯一不会运行的时间是当他们的分区已经用尽其关键预算。当向分区开出的关键 CPU 时间超过其关键预算时,就会发生bankrupt。

系统分区的关键预算是无限的:这个分区永远不会bankrupt。

线程调度器的设计考虑(系统架构)

通常使用线程调度器:

  • 设计一个系统,使其在满载时以可预测或可定义的方式工作
  • 防止不重要或不可信的应用程序垄断系统

无论哪种情况,需要在考虑整个系统的情况下配置调度器的参数。基本决定是:

  • 应该创建多少调度器分区,每个分区应该进入哪些软件?
  • 每个调度器分区应获得哪些保证 CPU 百分比?
  • 每个调度器分区的关键预算(如果有的话)应该是什么?
  • 以毫秒为限,时间窗口应是多少尺寸?

确定调度器分区的数量及其内容

将功能相关软件放入同一调度器分区似乎很合理,而且通常这是正确的选择。但是,自适应分区线程调度是决定何时不运行软件的结构化方法。因此,实际的方法是将软件分离成不同的调度器分区,如果它应该缺乏CPU时间在不同的情况下。
例如,如果系统是数据包路由器,则:
路线包
收集和记录数据包路由
处理与对等路由器的路由拓扑协议
收集和记录路由拓扑指标
可能有两个调度器分区似乎是合理的:一个用于路由,一个用于拓扑。当然,记录路由指标在功能上与数据包路由相关。
但是,当系统超载时,这意味着比机器可能完成的更出色的工作,您需要决定要慢慢完成哪些工作。在此示例中,当路由器中超载传入的包时,路由器路由仍然很重要。但是,您可能会决定,如果您不能做所有事情,您宁愿路由包,也不愿收集路由指标。通过同样的分析,您可能会得出结论,路由拓扑协议应该继续运行,使用机器的使用比路由本身少得多,但在需要时快速运行。

这种分析导致三个分区:
路由包的分区,占很大份额,说 80%
拓扑协议的分区,比如 15%,但最大线程优先级高于数据包路由
用于记录路由指标和拓扑协议指标的分区
在这种情况下,我们选择将路由和路由指标的功能相关组件分开,因为如果我们被迫饿死某些东西,我们宁愿只挨饿一个。同样,我们选择将两个功能无关的组件(路由指标的伐木和拓扑指标的记录)分组,因为我们希望在相同的情况下饿死它们。

为每个分区选择 CPU 的百分比
通常,获得分区预算正确组合的关键是尝试它们:
离开安全关闭。
加载具有逼真负载的测试机。
使用 IDE 的系统分析器工具检查时间敏感线程的延迟。
尝试不同的预算模式,您可以轻松地在运行时间使用aps命令更改这些模式。

不能删除分区,但可以删除其所有相应的过程,然后将特定分区的预算更改为 0%。

选择窗口时间大小

实际限制
API 允许窗口尺寸短至 8 毫秒,但实际窗口尺寸可能需要更大。
超载未报告给用户。自适应分区调度器确实检测过载,并限制某些分区以保证其他分区的百分比份额,但它没有通知内核以外的任何内容,即检测到超载。问题是,每次调度操作都可能发生(或可能不会发生)超载,每秒可能发生数千次。
SCHED_RR线程可能不会在平均窗口的一部分小于一个倍片的分区中循环。例如,当时间切片为 4 ms(默认值),自适应分区调度器的窗口大小为 100 ms(默认值)时,则 4% 分区中的SCHED_RR线程可能无法正确循环。?
如果您使用自适应分区和绑定多处理 (BMP),则可能无法满足某些预算组合。只有当所有其他非零预算分区处于闲置状态时,零预算分区中的线程才应运行。在 SMP 机器上,当其他分区需要时间时,零预算分区可能会运行不正确。在任何时候,所有分区的最低预算仍然得到保证,如果所有非零预算分区都准备好运行,零预算分区将不会运行。

调度器分区之间的不受控制的交互
中断处理程序

调度器分区的安全性

默认情况下,系统中的任何人都可以添加分区并修改其属性。建议使用SCHED_APS_ADD_SECURITY命令到SchedCtl()或aps命令来指定适合的系统的安全级别。

下表显示了主要安全选项(包括aps命令的-s选项的安全策略和相应的SchedCtl()标志),以增加安全顺序。

apsSchedCtl()Description
noneSCHED_APS_SEC_OFF系统中的任何人都可以添加分区并修改其属性。
basicSCHED_APS_SEC_BASIC只有启用PROCMGR_AID_APS_ROOT并在系统分区中运行的进程才能更改总体调度参数。启用PROCMGR_AID_APS_ROOT并在任何分区中运行的流程可以设置关键预算。
flexibleSCHED_APS_SEC_FLEXIBLE只有启用PROCMGR_AID_APS_ROOT并在系统分区中运行的进程才能更改调度参数。但是,在任何分区中启用和运行PROCMGR_AID_APS_ROOT的进程可以创建子分区、将线程连接到自己的子分区、修改子分区和更改关键预算。这样,应用程序可以从自己的预算中创建自己的本地分区。预算百分比不得为零。
recommendedSCHED_APS_SEC_RECOMMENDED只有启用PROCMGR_AID_APS_ROOT并在系统分区中运行的进程才能创建分区或更改参数。这创建了分区的两级层次:系统分区及其子级。只有启用PROCMGR_AID_APS_ROOT并在系统分区中运行的进程才能将自己的线程连接到分区。预算百分比不得为零。

除非正在测试分区方面,并希望在不重新启动的情况下更改所有参数,否则至少应该设置基本安全性。

设置调度器分区后,可以使用SCHED_APS_SEC_PARTITIONS_LOCKED来防止进一步未经授权的更改。例如:

sched_aps_security_parms p;

APS_INIT_DATA( &p );
p.sec_flags = SCHED_APS_SEC_PARTITIONS_LOCKED;
SchedCtl( SCHED_APS_ADD_SECURITY, &p, sizeof(p));

测试和调试

使用示例

[compress=3]
[virtual=x86,bios] .bootstrap = {
    startup-x86

    # PATH is the *safe* path for executables (confstr(_CS_PATH...))
    # LD_LIBRARY_PATH is the *safe* path for libraries
    # (confstr(_CS_LIBPATH)). That is, it's the path searched for libs
    # in setuid/setgid executables.

    # The module=aps enables the adaptive partitioning scheduler.

    [module=aps] PATH=/proc/boot:/bin:/usr/bin:/sbin:/usr/sbin \
        LD_LIBRARY_PATH=/proc/boot:/lib:/lib/dll:/usr/lib \
        procnto-smp-instr 
}

# Start-up script

[+script] .script = {
    # Programs require the runtime linker (ldqnx.so) to be at a fixed
    # location. To save memory, make everyone use the libc in the boot
    # image. For speed (fewer symbolic lookups), we point to libc.so.4
    # instead of libc.so.
    procmgr_symlink ../../proc/boot/libc.so.4 /usr/lib/ldqnx.so.2

    # Create some adaptive partitions during system startup:
    #   - IOPKT with a 20% budget
    #   - QCONN with a 20% budget
    # NOTE: To specify a critical budget of 5 ms, use sched_aps as seen below
    #       when the filesystem on the disk is available.
    sched_aps IOPKT 20 5
    sched_aps QCONN 20 5

    # Start the system logger.
    slogger2 &
    dumper

    # Start the PCI server.
    pci-server &
    waitfor /dev/pci

    display_msg .
    display_msg Welcome to QNX Neutrino 7.0!
    display_msg BIOS system with APS enabled

    # Get the disk up and running.
    devb-eide blk automount=hd0t179:/ &

    waitfor /bin

    # Further commands can now be run from disk.

    # USB services
    display_msg "Start USB services..."
    io-usb-otg -dehci &
    waitfor /dev/usb/io-usb-otg 4
    waitfor /dev/usb/devu-hcd-ehci.so 4

    display_msg "Starting Input services..."
    io-hid -d ps2ser kbd:kbddev:ps2mouse:mousedev \
       -d usb /dev/usb/io-usb-otg &
    waitfor /dev/io-hid/io-hid 10

    # Start up some consoles.
    display_msg Starting consoles
    devc-pty &
    devc-con-hid -n4 &
    reopen /dev/con1

    display_msg Starting serial port driver
    devc-ser8250 -b115200 &

    # Start the networking manager in the IOPKT partition.
    on -X aps=IOPKT io-pkt-v4-hc -d /lib/dll/devnp-abc100.so

    # Start some services.
    pipe
    random -t

    if_up -p wm0
    dhclient -m wm0

    #waitfor /dev/dbgmem

    # Create an additional parition with services:
    aps create -b10 INETD
    on -X aps=INETD inetd &
    on -X aps=QCONN qconn &

    # Specify a critical budget for a partition created during startup:
    aps modify -B 10 IOPKT

    # Use the "recommended" security level for the partitions:
    aps modify -s recommended

    # These env variables are inherited by all the programs that follow:
    SYSNAME=nto
    TERM=qansi

    # Start some extra shells on other consoles:
    reopen /dev/con2
    [+session] sh &
    reopen /dev/con3
    [+session] sh &
    reopen /dev/con4
    [+session] sh &

    # Start the main shell
    reopen /dev/con1
    [+session] sh &
}

[perms=0777] 
# Include the current "libc.so". It will be created as a real file
# using its internal "SONAME", with "libc.so" being a symlink to it.
# The symlink will point to the last "libc.so.*" so if an earlier
# libc is needed (e.g. libc.so.4) add it before the this line.
libc.so
libelfcore.so.1
libslog2.so


#######################################################################
## uncomment for USB driver
#######################################################################
#libusbdi.so
devu-hcd-ehci.so

fs-qnx6.so
cam-disk.so
io-blk.so
libcam.so

devnp-abc100.so
#libsocket.so

devc-con
pci-server
devc-ser8250
dumper
devb-eide
io-pkt-v4-hc
mount
slogger2
sh
cat
ls
pidin
less
on
qconn

小结

QNX Adaptive
QNX aps

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值