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抽象出了下面的软件框架:
软件框架包括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:
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:
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:
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:
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
5: static inline int disable_nonboot_cpus(void) { return 0; }
6: static inline void enable_nonboot_cpus(void) {}
7: #endif
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:
2:
3:
6: asmlinkage void secondary_start_kernel(void);
7:
8:
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:
2:
3:
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。
评论:
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 绝对耗电大户啊
发表评论:
昵称
邮件地址 (选填)
个人主页 (选填)