linux cpu运行错误的是什么意思,Linux CPU core的电源管理(1)_概述

Linux CPU core的电源管理(1)_概述

作者:wowo 发布于:2015-4-30 21:20

分类:电源管理子系统

1. 前言

在SMP(Symmetric Multi-Processing)流行起来之前的很长一段时间,Linux kernel的电源管理工作主要集中在外部设备上,和CPU core相关的,顶多就是CPU idle。但随着SMP的普及,一个系统中可用的CPU core越来越多,这些core的频率越来越高,处理能力越来越强,功耗也越来越大。因此,CPU core有关的电源管理,在系统设计中就成为必不可少的一环,与此有关的思考包括: 对消费者(一些专业应用除外)而言,这种暴增的处理能力,是一种极大的浪费,他们很少(或者从不)有如此高的性能需求。但商家对此却永远乐此不疲,原因无外乎:

1)硬件成本越来越低。

2)营销的噱头。

3)软件设计者的不思进取(臃肿的Android就是典型的例子),导致软件效率低下,硬件资源浪费严重。以至于优化几行代码的难度,甚至比增加几个cpu核还困难。

在这种背景下,CPU core的电源管理逻辑,就非常直接了:根据系统的负荷,关闭“多余的CPU性能”,在满足用户需求的前提下,尽可能的降低CPU的功耗。但CPU的控制粒度不可能无限小,目前主要从两个角度实现CPU core的电源管理功能:

1)在SMP系统中,动态的关闭或者打开CPU core(本文重点介绍的功能)。

2)CPU运行过程中,动态的调整CPU core的电压和频率(将在其它文章中单独分析)。

本文将以ARM64为例,介绍linux kernel CPU core相关的电源管理设计。

2. 功能说明

在linux kernel中,CPU core相关的电源管理实现,并不是单纯的电源管理行为,它会涉及到系统初始化、CPU拓扑结构、进程调度、CPU hotplug、memory  hotplug等知识点。总的来说,它主要完成如下功能: 1)系统启动时,CPU core的初始化、信息获取等。

2)系统启动时,CPU core的启动(enable)。

3)系统运行过程中,根据当前负荷,动态的enable/disable某些CPU core,以便在性能和功耗之间平衡。

4)CPU core的hotplug支持。所谓的hotplug,是指可以在系统运行的过程中,动态的增加或者减少CPU core(可以是物理上,也可以是逻辑上)。

5)系统运行过程中的CPU idle管理(具体可参考“Linux cpuidle framework系列文章”)。

6)系统运行过程中,根据当前负荷,动态的调整CPU core的电压和频率,以便在性能和功耗之间平衡。

3. 软件架构

为了实现上面的功能,linux kernel抽象出了下面的软件框架:

18864cac79c4f56d2a4eed3579194ca8.gif

软件框架包括arch-dependent和arch-independent两部分。

对ARM64而言,arch-dependent部分位于“arch\arm64\kernel”,负责提供平台相关的控制操作,包括: CPU信息的获取(cpuinfo);

CPU拓扑结构的获取(cpu topology);

底层的CPU操作(init、disable等)的实现,cpu ops(在ARM32中是以smp ops的形式存在的);

SMP相关的初始化(smp);

等等。

arch-independent负责实现平台无关的抽象,包括: CPU control模块,屏蔽底层平台相关的实现细节,提供控制CPU(enable、disable等)的统一API,供系统启动、进程调度等模块调用;

CPU subsystem driver,向用户空间提供CPU hotplug有关的功能;

cpuidle,处理CPU idle有关的逻辑,具体可参考“cpuidle framework”相关的文章;

cpufreq,处理CPU frequency调整有关的逻辑,具体可参考后续的文章;

等等。

4. 软件模块的功能及API描述

4.1 kernel cpu control

kernel cpu control位于“.\kernel\cpu.c”中,是一个承上启下的模块,负责屏蔽arch-dependent的实现细节,向上层软件提供CPU core控制的统一API。主要功能包括:

1)将CPU core抽象为possible、present、online和active四种状态,并以bitmap的形式,在模块内部维护所有CPU core的状态,同时以cpumask的形式向其它模块提供状态查询、状态修改的API。相关的API如下:

1: /* kernel/cpu.c */

2:

3: #ifdef CONFIG_INIT_ALL_POSSIBLE

4: static DECLARE_BITMAP(cpu_possible_bits, CONFIG_NR_CPUS) __read_mostly

5: = CPU_BITS_ALL;

6: #else

7: static DECLARE_BITMAP(cpu_possible_bits, CONFIG_NR_CPUS) __read_mostly;

8: #endif

9: const struct cpumask *const cpu_possible_mask = to_cpumask(cpu_possible_bits);

10: EXPORT_SYMBOL(cpu_possible_mask);

11:

12: static DECLARE_BITMAP(cpu_online_bits, CONFIG_NR_CPUS) __read_mostly;

13: const struct cpumask *const cpu_online_mask = to_cpumask(cpu_online_bits);

14: EXPORT_SYMBOL(cpu_online_mask);

15:

16: static DECLARE_BITMAP(cpu_present_bits, CONFIG_NR_CPUS) __read_mostly;

17: const struct cpumask *const cpu_present_mask = to_cpumask(cpu_present_bits);

18: EXPORT_SYMBOL(cpu_present_mask);

19:

20: static DECLARE_BITMAP(cpu_active_bits, CONFIG_NR_CPUS) __read_mostly;

21: const struct cpumask *const cpu_active_mask = to_cpumask(cpu_active_bits);

22: EXPORT_SYMBOL(cpu_active_mask);

bitmap的定义是:

#define DECLARE_BITMAP(name,bits)    unsigned long name[BITS_TO_LONGS(bits)]

其本质上是一个long型的数组,数组中每一个bit,代表一个CPU core的状态。例如long的长度为64位的系统中,如果有8个CPU core,则可以使用长度为1的数组的前8个bit,代表这个8个core的状态。

这里一共有4种状态需要表示:

cpu_possible_bits,系统中包含的所有的可能的CPU core,在系统初始化的时候就已经确定。对于ARM64来说,DTS中所有格式正确的CPU core,都属于possible的core;

cpu_present_bits,系统中所有可用的CPU core(具备online的条件,具体由底层代码决定),并不是所有possible的core都是present的。对于支持CPU hotplug的形态,present core可以动态改变;

cpu_online_bits,系统中所有运行状态的CPU core(后面会详细说明这个状态的意义);

cpu_active_bits,有active的进程正在运行的CPU core。

另外,在使用bitmap表示这4种状态的同时,还提供了4个cpumask,用于对外提供接口。cpumask的本质也是bitmap(多一层封装而已),只是kernel提供了一些方便的API,可以以CPU编号为单位,操作cpumask(具体可参考include/linux/cpumask.h)。

注1:这里有一个关于constant变量的经典例子,大家可以学习一下。

毫无疑问,CPU core的这些状态,是相当重要的一些状态,因此kernel希望它们对外(除cpu control外)是read only的,对内又是writeable的。怎么办呢?

这里的设计很巧妙,对内使用4个static的bitmap变量,因此是writeable的。而对外呢,使用4个constant类型的cpumask指针(指针readonly,值readonly),因此是readonly的。

外部模块read时,通过一层转换,从static的bitmap中获取实际的值。是不是很有意思?

下面是这几个变量有关的操作函数:

1: /* include/linux/cpumask.h */

2:

3: #define num_online_cpus() cpumask_weight(cpu_online_mask)

4: #define num_possible_cpus() cpumask_weight(cpu_possible_mask)

5: #define num_present_cpus() cpumask_weight(cpu_present_mask)

6: #define num_active_cpus() cpumask_weight(cpu_active_mask)

7: #define cpu_online(cpu) cpumask_test_cpu((cpu), cpu_online_mask)

8: #define cpu_possible(cpu) cpumask_test_cpu((cpu), cpu_possible_mask)

9: #define cpu_present(cpu) cpumask_test_cpu((cpu), cpu_present_mask)

10: #define cpu_active(cpu) cpumask_test_cpu((cpu), cpu_active_mask)

11:

12:

13: #define for_each_possible_cpu(cpu) for_each_cpu((cpu), cpu_possible_mask)

14: #define for_each_online_cpu(cpu) for_each_cpu((cpu), cpu_online_mask)

15: #define for_each_present_cpu(cpu) for_each_cpu((cpu), cpu_present_mask)

16:

17: /* Wrappers for arch boot code to manipulate normally-constant masks */

18: void set_cpu_possible(unsigned int cpu, bool possible);

19: void set_cpu_present(unsigned int cpu, bool present);

20: void set_cpu_online(unsigned int cpu, bool online);

21: void set_cpu_active(unsigned int cpu, bool active);

22: void init_cpu_present(const struct cpumask *src);

23: void init_cpu_possible(const struct cpumask *src);

24: void init_cpu_online(const struct cpumask *src);

2)提供CPU core的up/down操作,以及up/down时的notifier机制

通俗地讲,所谓的CPU core up,就是将某一个CPU core“运行”起来。何为运行呢?回忆一下单核CPU的启动,就是让该CPU core在指定的memory地址处取指执行。因此该功能只对SMP系统有效(使能了CONFIG_SMP)。而CPU core down,就是让CPU core保存现场(后面可以继续执行)后,停止取指,只有在CPU hotplug功能使能(CONFIG_HOTPLUG_CPU)时有效。这两个功能对应的API为:

1: /* include/linux/cpu.h */

2:

3: int cpu_up(unsigned int cpu);

4:

5: int cpu_down(unsigned int cpu);

同时,提供了CPU up/down时的通知API,具体请参考“include/linux/cpu.h"。

3)提供SMP PM有关的操作

系统休眠过程中,将noboot的CPU禁用,并在系统恢复时恢复(可参考“Linux电源管理(6)_Generic PM之Suspend功能”中的有关描述)。

1: #ifdef CONFIG_PM_SLEEP_SMP

2: extern int disable_nonboot_cpus(void);

3: extern void enable_nonboot_cpus(void);

4: #else /* !CONFIG_PM_SLEEP_SMP */

5: static inline int disable_nonboot_cpus(void) { return 0; }

6: static inline void enable_nonboot_cpus(void) {}

7: #endif /* !CONFIG_PM_SLEEP_SMP */

4.2 cpu subsystem driver

cpu subsystem driver位于“drivers/base/cpu.c”中,从设备模型的角度,抽象CPU core设备,并通过sysfs提供CPU core状态查询、hotplug控制等接口。具体如下:

1)注册一个名称为“bus”的subsystem(在sysfs中目录为“/sys/devices/system/cpu/”,有关subsystem的描述,可参考“Linux设备模型(6)_Bus”)。

2)使用struct cpu抽象CPU core device(见“include/linux/cpu.h”)。

4)从设备模型的角度,提供CPU core device的register/unregister等接口,并在系统初始化的时候根据CPU core的个数,将这些device注册到kernel中。同时根据kernel配置,注册相关的CPU attribute。

1: extern int register_cpu(struct cpu *cpu, int num);

2: extern struct device *get_cpu_device(unsigned cpu);

3: extern bool cpu_is_hotpluggable(unsigned cpu);

4: extern bool arch_match_cpu_phys_id(int cpu, u64 phys_id);

5: extern bool arch_find_n_match_cpu_physical_id(struct device_node *cpun,

6: int cpu, unsigned int *thread);

7:

8: extern int cpu_add_dev_attr(struct device_attribute *attr);

9: extern void cpu_remove_dev_attr(struct device_attribute *attr);

10:

11: extern int cpu_add_dev_attr_group(struct attribute_group *attrs);

12: extern void cpu_remove_dev_attr_group(struct attribute_group *attrs);

13:

14: #ifdef CONFIG_HOTPLUG_CPU

15: extern void unregister_cpu(struct cpu *cpu);

16: extern ssize_t arch_cpu_probe(const char *, size_t);

17: extern ssize_t arch_cpu_release(const char *, size_t);

18: #endif

最终在sysfs中的目录结构如下:

# ls /sys/devices/system/cpu/

autoplug/  cpu2/      cpuidle/   offline    power/

cpu0/      cpu3/      kernel_max online     present

具体会在后续的文章中详细说明。

4.3 smp

smp位于“arch/arm64/kernel/smp.c”,在arch-dependent代码中,承担承上启下的角色,主要提供两类功能:

1)arch-dependent的SMP初始化、CPU core控制等操作(本文需要关注的功能)。

2)IPI(Inter-Processor Interrupts)相关的支持(具体可参考本站“中断子系统”有关的文章)。

SMP初始化操作,主要负责从DTS中解析CPU core信息,并获取必要的信息以及操作函数集,由smp_init_cpus接口实现,并在setup_arch(arch\arm64\kernel\setup.c)中调用。

CPU core控制相关的接口包括:

1: /* arch/arm64/include/asm/smp.h */

2:

3: /*

4: * Called from the secondary holding pen, this is the secondary CPU entry point.

5: */

6: asmlinkage void secondary_start_kernel(void);

7:

8: /*

9: * Initial data for bringing up a secondary CPU.

10: */

11: struct secondary_data {

12: void *stack;

13: };

14: extern struct secondary_data secondary_data;

15: extern void secondary_entry(void);

16:

17: extern void arch_send_call_function_single_ipi(int cpu);

18: extern void arch_send_call_function_ipi_mask(const struct cpumask *mask);

19:

20: extern int __cpu_disable(void);

21:

22: extern void __cpu_die(unsigned int cpu);

23: extern void cpu_die(void);

secondary_start_kernel、secondary_entry,是那些 noboot CPU的入口,具体后面再详细介绍;

__cpu_disable、__cpu_die、cpu_die等函数负责disable某个CPU core,它们不会直接操作硬件,而是通过下层的cpu_ops控制具体的CPU core,具体请参考4.4小节的说明。

4.4 cpu ops

由于SMP架构比较复杂,特别是对ARM64而言,又涉及到虚拟化等安全特性,ARM便将CPU core的up/down等电源管理操作,封装起来(例如封装到secure mode下,特权级别的OS代码通过一些指令码与其交互,具体请参考后续的文章)。在ARM64中,这种封装便是通过cpu os结构(struct cpu_operations)体现的,如下:

1: /* arch/arm64/include/asm/cpu_ops.h */

2:

3: /**

4: * struct cpu_operations - Callback operations for hotplugging CPUs.

5: *

6: * @name: Name of the property as appears in a devicetree cpu node's

7: * enable-method property.

8: * @cpu_init: Reads any data necessary for a specific enable-method from the

9: * devicetree, for a given cpu node and proposed logical id.

10: * @cpu_init_idle: Reads any data necessary to initialize CPU idle states from

11: * devicetree, for a given cpu node and proposed logical id.

12: * @cpu_prepare: Early one-time preparation step for a cpu. If there is a

13: * mechanism for doing so, tests whether it is possible to boot

14: * the given CPU.

15: * @cpu_boot: Boots a cpu into the kernel.

16: * @cpu_postboot: Optionally, perform any post-boot cleanup or necesary

17: * synchronisation. Called from the cpu being booted.

18: * @cpu_disable: Prepares a cpu to die. May fail for some mechanism-specific

19: * reason, which will cause the hot unplug to be aborted. Called

20: * from the cpu to be killed.

21: * @cpu_die: Makes a cpu leave the kernel. Must not fail. Called from the

22: * cpu being killed.

23: * @cpu_kill: Ensures a cpu has left the kernel. Called from another cpu.

24: * @cpu_suspend: Suspends a cpu and saves the required context. May fail owing

25: * to wrong parameters or error conditions. Called from the

26: * CPU being suspended. Must be called with IRQs disabled.

27: */

28: struct cpu_operations {

29: const char *name;

30: int (*cpu_init)(struct device_node *, unsigned int);

31: int (*cpu_init_idle)(struct device_node *, unsigned int);

32: int (*cpu_prepare)(unsigned int);

33: int (*cpu_boot)(unsigned int);

34: void (*cpu_postboot)(void);

35: #ifdef CONFIG_HOTPLUG_CPU

36: int (*cpu_disable)(unsigned int cpu);

37: void (*cpu_die)(unsigned int cpu);

38: int (*cpu_kill)(unsigned int cpu);

39: #endif

40: #ifdef CONFIG_ARM64_CPU_SUSPEND

41: int (*cpu_suspend)(unsigned long);

42: #endif

43: };

ARM architecture提供多种可选的cpu ops实现,如spin-table、PSCI(Power State Coordination Interface)等(具体会在后续的文章中详细描述),开发者可以根据需求,选择一种。4.3节所描述的smp初始化时,会解析DTS,并填充cpu ops变量。

这里以PSCI为例,简单了解一下这些接口的含义(具体说明请参考后续的文章)。

cpu_boot:   Boots a cpu into the kernel.  其实就是将启动函数(secondary_entry)的物理地址,给到CPU core执行,具体要看spec

cpu_disable: Prepares a cpu to die.

cpu_die: Makes a cpu leave the kernel.

cpu_suspend: Suspends a cpu and saves the required context.

4.5 cpu topology

本文提到了很多诸如SMP、CPU core之类的字眼,相应读者可能看的不太明白,这和CPU的拓扑结构有关。进程调度、cpufreq等模块,可能需要根据具体的拓扑结构,制定相应的策略。

ARM64的topology在“./arch/arm64/include/asm/topology.h”中定义,如下:

1: struct cpu_topology {

2: int thread_id;

3: int core_id;

4: int cluster_id;

5: cpumask_t thread_sibling;

6: cpumask_t core_sibling;

7: };

8:

9: extern struct cpu_topology cpu_topology[NR_CPUS];

10:

11: #define topology_physical_package_id(cpu) (cpu_topology[cpu].cluster_id)

12: #define topology_core_id(cpu) (cpu_topology[cpu].core_id)

13: #define topology_core_cpumask(cpu) (&cpu_topology[cpu].core_sibling)

14: #define topology_thread_cpumask(cpu) (&cpu_topology[cpu].thread_sibling)

15:

16: void init_cpu_topology(void);

17: void store_cpu_topology(unsigned int cpuid);

18: const struct cpumask *cpu_coregroup_mask(int cpu);

topology的实现位于“arch/arm64/kernel/topology.c ”中,负责在系统初始化的时候,由boot cpu读取DTS,填充每个CPU core的struct cpu_topology变量。同时,该文件提供一些通用的宏定义,用于获取执行CPU core的信息,例如该CPU core的package id、core id等。

topology的具体描述,请参考下一篇文章。

4.6 cpu info及其它

cpu info位于“arch/arm64/kernel/cpuinfo.c”,负责在初始化的时候将ARM CPU core有关的信息,从寄存器中读出,缓存在struct cpuinfo_arm64类型的变量中,以便后面使用。具体不再详细描述。

5. 结束语

本文简单的介绍了CPU core电源管理相关的软件组成,并认识了cpu ops、cpu topology等概念,后续将通过以下的文章进行更为详细的分析:

Linux CPU core的电源管理(2)_cpu topology,认识并理解ARM处理器的拓扑结构,以及cluster(socket)、core、thread等处理器结构相关的概念;

Linux CPU core的电源管理(4)_PSCI,分析ARM PSCI(Power State Coordination Interface)接口;

Linux CPU core的电源管理(5)_cpu control,从系统的角度,分析系统初始化、进程调度、CPU hotplug等场景下,CPU up/down等操作的流程;

Linux cpufreq framework系列文章,分析CPU动态频率/电压调整相关的实现。

原创文章,转发请注明出处。蜗窝科技,www.wowotech.net。c6a6308114f401be7df747ae46f2b4db.png

评论:

2016-05-27 09:13

hi,wowo,

我们经常所说的DVFS,狭隘地是指interactive这个governor吗?还有其他地方有涉及吗?

啥时候有空把interactive governor分析一下啊?

谢谢

2016-05-27 10:55

@koala:可以这么理解,不过你也可以设计自己的策略。

有时间可以分析一下interactive governor,多谢关注~

2016-01-27 19:56

写的很好,才发现有这么好的一个地方,谢谢! 希望能快点学习,以后我也能在这里写一篇文章。 :)

2016-01-28 08:57

@archer:希望不止有一篇哈,等着你:-)

YANG23

2015-09-17 15:35

非常期待 电源管理PSCI相关的文章,希望作者尽快发表啊,另外能不能对比一下 PSCI和SMP SPIN TABLE优劣主要对功耗的影响呢  期待作者

2015-09-18 12:20

@YANG23:多谢关注,其实一般情况下,不会深入到PSCI的,会用就可以了。

PSCI主要的特点,就是支持CPU hotplug。对功耗而言,PSCI的ops可能会单独core的power,因而肯定比SPIN table好。

2015-08-30 20:25

非常期待 Linux CPU core的电源管理(5)_cpu control

这篇文章.

现在移植一个cpuidle功能到高版本内核, bootloader一走完, 就hang住了.

我猜和PM与SMP初始化哪里出错了. 很多代码不是我写的, 现在正在看.

请教lz, PM和smp初始化相关的代码有哪些呢?

2015-08-31 09:05

@firo:解决问题的时候,看代码不是最有效的方法,你可以用一些收到,查一查hang在哪里了。

另外,这个时候和PM没有关系吧?SMP的话,可以去“arch/xxx/kernel/smp.c”看看。

2015-08-31 10:42

@wowo:感谢.

内核什么信息都没打印出来,就hang了.

因为我是只把上个版本cpuidle & suspend相关的代码和进来.系统就不能启动了. 所以和PM关系非常大.

2015-09-01 09:45

@firo:可以把选项CONFIG_DEBUG_LL(kernel low-level debugging functions)打开,即使console还没有初始化,也可以用printascii等函数加入打印信息,这样就可以定位具体挂在哪里了?这个功能非常实用。

2015-09-01 11:13

@snail:感谢!

昨天用printascii看到4个core都走到了cpu_startup_entry.看上去系统正常启动了.

1. 可能是console没有正常启动所以看不到printk之类的信息, 概率较小, 因为支部分代码没动过.

2. wowo知道用什么方法可以, 把printk在系统启动时打到console里面的信息用printascii 或者其他方法输出出来, 看看用户态是否起来了? 排除是否只是console有问题.

3. 还有一个问题, arm多核cpu_do_idle是用宏定义成processor._do_idle(), 我没有找到这个_do_idle是在哪里初始化的.

在setup_processor只看到了processor的整体赋值语句

list = lookup_processor_type(read_cpuid_id());

...

processor = *list->proc;

我昨天用printascii打印的时候只打印到

static void default_idle(void)

{

if (arm_pm_idle)

arm_pm_idle();

else

cpu_do_idle(); //这是个宏

local_irq_enable();

}接下来不知道怎么走了.

2015-09-01 12:26

@firo:2. 很久以前我们做过,方法有多种,比如重写printk,调用printascii 输出。

3. processor的_do_idle,一般都是发指令到CPU core中了,不同平台不一样。一般情况下,跟到这里面,就没有方法接着跟了。

2015-08-20 14:51

cpu_online_bits,系统中所有运行状态的CPU core(后面会详细说明这个状态的意义);

系统是如果确定运行状态呢?

cpu_active_bits,有active的进程正在运行的CPU core。

这个online 与active 如何区别呢?

2015-08-20 17:11

@tigger:不好意思,一直空接着写这一块的东西:

cpu_online_bits比较容易理解,CPU启动的话(也即CPU运行了kernel代码,secondary_start_kernel),就认为是online的,会通过set_cpu_online接口设置这个bit mask。

而cpu_active_bits,则表示有任务在这个CPU上调度,由kenrel sched模块,调用set_cpu_active设置。

online和active的区别是,只要active,一定是online,因此online更多的代码了CPU的“物理状态”,而active,更多的是OS调度层面的抽象,CPU本身没有这一个状态。

buhui

2015-08-08 15:19

好文章,读这样的文章,就像饥饿的人扑到了面包上。比陈浩强的文章有天壤之别。

2015-05-10 14:57

高端手机平台现在都是cpuidle + scheduler(HMP) 做省功耗方案。hotplug只在thermal控制中使用了。

2015-05-11 10:29

@schedule:多谢分享,第一次听说HMP的概念,要了解了解。

2015-05-24 22:42

@wowo:1、是的,64bit 4核以上arm cpu方案基本都采用HMP(GTS) + Interactive(DVFS)组合,但功耗调试需要花时间。

2015-05-25 11:05

@zzq57683968:原来HMP就是big·little模式的multi-processor,我对HMP这个词不太了解。big·little是ARM为了性能和功耗平衡搞出来的一个东西,和CPU topology有关。

DVFS还没有深入了解,就是我们项目加上去过一段时间,后来又去掉了,据说不太稳定,呵呵。

2015-05-10 14:38

CPU/GPU 绝对耗电大户啊

发表评论:

昵称

邮件地址 (选填)

个人主页 (选填)

d4e3789769c8ad44c7e403863bfc3822.png

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值