1. 前言
也许大家会觉得奇怪:为什么Linux kernel把对ARM big·Lttile的支持放到了cpufreq的框架中?
众所周知,ARM的big·Little架构,也称作HMP(具体可参考“Linux CPU core的电源管理(2)_cpu topology”中相关的介绍),通过在一个chip中封装两种不同类型的ARM core的方式,达到性能和功耗的平衡。这两类ARM Core,以cluster为单位,一类为高性能Core(即big core),一类为低性能Core(即Little core),通过它们的组合,可以满足不同应用场景下的性能和功耗要求,例如:非交互式的后台任务、或者流式多媒体的解码,可以使用低功耗的Little core处理;突发性的屏幕刷新,可以使用高性能的big core处理。
那么问题来了,Linux kernel怎么支持这种框架呢?
注1:本文很多理论性的表述,或多或少的理解并翻译自:“http://lwn.net/Articles/481055/ ” , 感兴趣的读者可以自行阅读。
2. Linux kernel支持ARM big·Lttile框架的思路
以一个包含两个cluster,cluster0有4个A15 core,cluster1有4个A7 core为例,我们会很自然的想到,可以把这8个core统统开放给kernel,让kernel的调度器(scheduler)根据系统的实际情况,决定哪些任务应该在哪些core上执行。但这存在一个问题:
当前linux kernel的scheduler,都是针对SMP系统设计的,而SMP系统中所有CPU core的性能和功耗都是相同的。
换句话说:kernel中还没有适用于big·Little架构的scheduler。怎么办?等待这样的一个scheduler出现?芯片厂商当然等不及,市场上已经出现了很多这种架构的芯片。为了应对这种软件滞后于硬件的现象(当前这种现象比比皆是,可能是硬件的成本比软件的人力成本低吧),ARM公司提出了这样一种软件解决方案(这只是ARM提出的解决方案的一种,后续我们介绍PSCI时,还会接触其它方案,这里就不再提及):
使用一个hypervisor(可以参考“ARMv8-a架构简介”中有关hypervisor的介绍),利用虚拟化扩展,将8个A15 A7 core虚拟为4个A15 core,这样OS kernel就可以以SMP的方式,和这4个虚拟的core交互。当OS需要的时候,可以通过一个hypervisor指令,让某一个虚拟的core在A15和A7两种模式下切换,所有的切换动作,包括IRQ、timer等的迁移,都由hypervisor负责,对OS是透明的。
上面的方法有两个缺点:
1)这8个core不能任意的组合使用 2)必须存在hypervisor,会增加系统的复杂度
linux 借鉴了这个思路,将ARM hypervisor相关的实现逻辑,移植到kernel中,借助cpufreq framework,来实现上面的功能。之所以将hypervisor移植到kernel,是为了降低对ARM hypervisor code的耦合要求(解决上面的缺点2,当然,缺点1暂时无法解决)。而之所以使用cpufreq的框架,其中的思考如下:
1)抛开在big core和Little core之间切换的代价不谈,对scheduler来说,big core和Little core的区别仅仅在core的性能和功耗上,这恰恰和cpufreq framework的关注点一致:频率降低,性能降低,功耗降低;频率增大,性能增强,功耗增大。因此,可以将big、Little的切换,嵌入到CPU的频率调整中,低范围的频率段,对应Little core,高范围的频率段,对应big core。 2)对kernel scheduler而言,上面的8个core只有4个可见,这4个可见的core具有较宽的频率范围,并在big·Little switcher使能的情况下,根据频率需求的情况,自动在big和Little之间切换。 3)因此,使用cpufreq框架,巧妙的将不对称HMP架构,转化为了SMP架构,这样,kernel现有的scheduler就派上用场了。
3. ARM big·Little driver的软件框架
基于上面的思考,linux kernel使用如下的软件框架实现ARM big·Little切换功能:
arm big little cpufreq driver,位于drivers/cpufreq/目录中,负责和cpufreq framework对接,以CPU调频的形式,实现ARM big·Little的切换。
arm bL switcher,是一个arch-dependent的driver,提供实际的切换操作。
3.1 arm big little cpufreq driver
arm big little cpufreq driver位于drivers/cpufreq目录下,由三个文件组成:
代码语言:javascript复制arm_big_little.c
arm_big_little.h
arm_big_little_dt.c
主要提供如下的功能(以本文参考的“linux-3.18-rc4” kernel为准):
1)支持A15和A7两个cluster。
2)当bL switching处于disable状态(bL switching的状态由arm bL switcher driver控制,3.2节会介绍)时,该driver就是一个普通的cpufreq driver,并为每个cluster提供一个frequency table(保存在freq_table[0]、freq_table[1]中)。
此时所有的big、Little core对系统都可见,每个core都可以基于cpufreq framework调整频率。
3)当bL switching处于enable状态时,该driver变成一个特殊的cpufreq driver,在调整频率的时候,可以根据情况,切换core的cluster。以第2章所描述的8个core为例:
此时只有4个虚拟的core对系统可见(由arm bL switcher driver控制,3.2节会介绍),系统不关心这些core属于哪个cluster、是big core还是Little core; 确切的说,每一个虚拟的core,代表了属于两个cluster的CPU对,可以想象为big Little组合,只是同一时刻,只有一个core处于enable状态(big or Little); 该driver会搜集2个cluster的frequency table,并合并成一个(保存在freq_table[2]中)。合并后,找出这些frequency中big core最小的那个(clk_big_min)以及Little core最大的那个(clk_little_max); 基于cpufreq framework进行频率调整时,如果所要求的频率小于clk_big_min,则将该虚拟core所对应的Little core使能,如果所要求得频率大于clk_little_max,则将该虚拟core所对应的big core使能。
3.2 arm bL switcher driver
arm bL switcher driver是一个arch-dependent的driver,以ARM平台为例,其source code包括:
arch/arm/common/bL_switcher.c arch/arm/common/bL_switcher_dummy_if.c
该driver的功能如下:
1)提供bL switcher功能的enable和disable控制
2)通过sysfs,允许用户空间软件控制bL switcher的使能与否
接口文件位于: /sys/kernel/bL_switcher/active
读取可以获取当前的使能情况,写1 enable,写0 disable。
3)为每个虚拟的CPU core(big Little组合),创建一个线程,实现最终的cluster切换。该部分是平台相关的,后面将会以ARM平台为例介绍具体的过程。
4. 代码分析
本章以ARM平台为例,结合kernel source code,从初始化以及cluster切换两个角度,介绍ARM big·Little driver的核心功能。
4.1 初始化
和ARM big·Little driver有关的初始化过程主要分为三个部分:
1)CPU core的枚举和初始化,具体可参考“ Linux CPU core的电源管理(5)_cpu control及cpu hotplug ”中有关possible CPU、present CPU的描述。 2)arm big little cpufreq driver的初始化,为每个cluster创建一个frequency table,并主持相应的cpufreq driver。 3)arm bL switcher driver,初始化bL switcher,并使能bL switcher。 下面我们重点介绍步骤2和步骤3。
4.1.1 arm big little cpufreq driver的初始化
还是以包含两个cluster,每个cluster有4个CPU core(A15和A7)的系统为例,由“Linux CPU core的电源管理(5)_cpu control及cpu hotplug”得描述可知,start_kernel之后,系统的possible CPU包含所有的8个core。
然后arm big little cpufreq driver出场了,其init接口位于“drivers/cpufreq/arm_big_little_dt.c”中,如下:
代码语言:javascript复制1: static int generic_bL_probe(struct platform_device *pdev)
2: {
3: struct device_node *np;
4:
5: np = get_cpu_node_with_valid_op(0);
6: if (!np)
7: return -ENODEV;
8:
9: of_node_put(np);
10: return bL_cpufreq_register(&dt_bL_ops);
11: }
12:
13: static int generic_bL_remove(struct platform_device *pdev)
14: {
15: bL_cpufreq_unregister(&dt_bL_ops);
16: return 0;
17: }
18:
19: static struct platform_driver generic_bL_platdrv = {
20: .driver = {
21: .name = "arm-bL-cpufreq-dt",
22: .owner = THIS_MODULE,
23: },
24: .probe = generic_bL_probe,
25: .remove = generic_bL_remove,
26: };
27: module_platform_driver(generic_bL_platdrv);
kernel将arm big little cpufreq driver注册成了一个简单的platform driver,因此driver的入口就是其probe函数:generic_bL_probe。 注3:这里之所以把代码贴出,主要是看到“module_platform_driver”这个接口比较有趣。平时的经验,注册一个platform driver的固定格式是:定义一个platform driver变量,包含.probe()、.remove()等回调函数;定义两个函数,init和exit,在init函数中,调用platform_driver_register接口,注册该driver;使用module_init和module_exit声明init和exit函数。是不是很啰嗦?那就用这个接口吧,很省事!
1)generic_bL_probe
generic_bL_probe接口很简单,以dt_bL_ops为参数,调用bL_cpufreq_register接口,注册cpufreq driver。dt_bL_ops是一个struct cpufreq_arm_bL_ops类型的变量,提供两个回调函数,分别用于获取cluster切换之间的延迟,以及初始化opp table,后面用到的时候再介绍。
2)bL_cpufreq_register
bL_cpufreq_register位于“drivers/cpufreq/arm_big_little.c”中,主要负责如下事情:
a)执行一些初始化动作。 b)调用cpufreq_register_driver接口,注册名称为bL_cpufreq_driver的cpufreq driver。有关cpufreq driver以及cpufreq_register_driver的描述可参考“Linux cpufreq framework(2)_cpufreq driver”。 c)调用arm bL switcher driver提供的bL_switcher_register_notifier接口,向该driver注册一个notify,当bL switcher enable或者disable的时候,该driver会通知arm big little cpufreq driver,以完成相应的动作。
3)bL_cpufreq_driver
bL_cpufreq_driver代表了具体的cpufreq driver,提供了.init()、.verify()、.target_index()等回调函数,如下:
代码语言:javascript复制 1: static struct cpufreq_driver bL_cpufreq_driver = {
2: .name = "arm-big-little",
3: .flags = CPUFREQ_STICKY |
4: CPUFREQ_HAVE_GOVERNOR_PER_POLICY |
5: CPUFREQ_NEED_INITIAL_FREQ_CHECK,
6: .verify = cpufreq_generic_frequency_table_verify,
7: .target_index = bL_cpufreq_set_target,
8: .get = bL_cpufreq_get_rate,
9: .init = bL_cpufreq_init,
10: .exit = bL_cpufreq_exit,
11: .attr = cpufreq_generic_attr,
12: };
有关cpufreq driver以及相关的回调函数已经在“Linux cpufreq framework(2)_cpufreq driver”中有详细介绍,这里再稍作说明一下:
.init()是cpufreq driver的入口函数,当该driver被注册到kernel中后,cpufreq core就会调用该回调函数,一般在init函数中初始化CPU core有关的frequency table,并依据该table填充相应的cpufreq policy变量。 .verify()可用于校验某个频率是否有效。 .target_index()可将CPU core设置为某一个频率,在本文的场景中,可以在修改频率是进行cluster切换,后面会详细介绍。
4)bL_cpufreq_init
bL_cpufreq_driver被注册后,cpufreq core就会调用bL_cpufreq_init接口,完成后续的初始化任务,该接口比较重要,是arm big little cpufreq driver的精髓,其定义如下:
代码语言:javascript复制 1: /* Per-CPU initialization */
2: static int bL_cpufreq_init(struct cpufreq_policy *policy)
3: {
4: u32 cur_cluster = cpu_to_cluster(policy->cpu);
5: struct device *cpu_dev;
6: int ret;
7:
8: cpu_dev = get_cpu_device(policy->cpu);
9: if (!cpu_dev) {
10: pr_err("%s: failed to get cpu%d devicen", __func__,
11: policy->cpu);
12: return -ENODEV;
13: }
14:
15: ret = get_cluster_clk_and_freq_table(cpu_dev);
16: if (ret)
17: return ret;
18:
19: ret = cpufreq_table_validate_and_show(policy, freq_table[cur_cluster]);
20: if (ret) {
21: dev_err(cpu_dev, "CPU %d, cluster: %d invalid freq tablen",
22: policy->cpu, cur_cluster);
23: put_cluster_clk_and_freq_table(cpu_dev);
24: return ret;
25: }
26:
27: if (cur_cluster < MAX_CLUSTERS) {
28: int cpu;
29:
30: cpumask_copy(policy->cpus, topology_core_cpumask(policy->cpu));
31:
32: for_each_cpu(cpu, policy->cpus)
33: per_cpu(physical_cluster, cpu) = cur_cluster;
34: } else {
35: /* Assumption: during init, we are always running on A15 */
36: per_cpu(physical_cluster, policy->cpu) = A15_CLUSTER;
37: }
38:
39: if (arm_bL_ops->get_transition_latency)
40: policy->cpuinfo.transition_latency =
41: arm_bL_ops->get_transition_latency(cpu_dev);
42: else
43: policy->cpuinfo.transition_latency = CPUFREQ_ETERNAL;
44:
45: if (is_bL_switching_enabled())
46: per_cpu(cpu_last_req_freq, policy->cpu) = clk_get_cpu_rate(policy->cpu);
47:
48: dev_info(cpu_dev, "%s: CPU %d initializedn", __func__, policy->cpu);
49: return 0;
50: }
该接口根据当前bL switcher的使能情况(由cpu_to_cluster的返回值判断,如果等于MAX_CLUSTERS,bL switcher处于enable状态,否则,为disable状态),有两种截然不同行为。 bL switcher disable时(由于arm big little cpufreq driver先于arm bL switcher driver初始化,它初始化时,bL switcher处于disable状态): 15行,调用get_cluster_clk_and_freq_table接口,为当前CPU所在的cluster创建frequency table,结果保存在freq_table[cluster]中 27~33行,将和当前cpu以及同属于一个cluster的所有其它CPU都保存在policy->cpus中(具体意义请参考““Linux cpufreq framework(2)_cpufreq driver””),并将它们的physical_cluster设置为当前CPU的cluster 以上逻辑的背后思路是:如果bL switcher没有enable,arm big little cpufreq driver就是一个普通的cpufreq driver,此时每个cluster的所有CPU(如4个A15 core,或者4个A7 core),共享同一个frequency table、cpufreq policy,也就是说,一个cluster下的所有CPU core,共享同一个调频策略 bL switcher enable时(会复杂一些): 15行,调用get_cluster_clk_and_freq_table接口,搜集两个cluster下CPU的frequency信息,并以升序的形式合并到一个frequency table中(freq_table[MAX_CLUSTERS]),找出这些frequency中big core最小的那个(clk_big_min)以及Little core最大的那(clk_little_max) 36行,将当前CPU的physical_cluster变量设置为当前A15_cluster,默认初始化时为big core模式 以上逻辑背后的思路是:如果bL switcher enable,则所有的CPU core共用一个合并后的frequency table,并由一个调频策略统一调度,具体方法后面再详细介绍
bL_cpufreq_init的核心实现是get_cluster_clk_and_freq_table接口,该接口会根据bL switcher的使能情况,初始化不同的frequency table,并在bL switcher enable的时候,将不同cluster的frequency合并到一起。具体代码就不再详细分析,这里强调一下里面的一个小技巧:
为了让bL switcher逻辑顺利执行,有必要尽量准确的区分big core和Little core的频率,get_cluster_clk_and_freq_table使用了一个简单的方法: 对Little core来说,统一把frequency除以2,使用的时候再乘回来,这就基本上可以保证Little core的frequency位于合并后的频率表的前面位置,big core位于后面位置。
4.1.2 arm bL switcher driver的初始化
以ARM平台为例,arm bL switcher driver的初始化接口是bL_switcher_init,由于它使用late_init宏声明,因此会在靠后的时机初始化,该接口主要完成两个事情:
1)如果no_bL_switcher参数不为1(默认为0),则调用bL_switcher_enable接口,使能bL switcher。 2)调用bL_switcher_sysfs_init接口,初始化bL switcher模块提供的sysfs API,具体可参考3.2章节的介绍。
因此,arm bL switcher driver的初始化,就转移到bL_switcher_enable上面了。
4.2 enable/disable
bL switcher的使能与否,是由arm bL switcher driver控制的,以enable为例,enable的时机有两个:
1)arm bL switcher driver初始化的时候,调用bL_switcher_enable。
2)通过sysfs(/sys/kernel/bL_switcher/active)使能
4.2.1 bL_switcher_enable
bL_switcher_enable的实现如下:
代码语言:javascript复制 1: /* arch/arm/common/bL_switcher.c */
2: static int bL_switcher_enable(void)
3: {
4: int cpu, ret;
5:
6: mutex_lock(&bL_switcher_activation_lock);
7: lock_device_hotplug();
8: if (bL_switcher_active) {
9: unlock_device_hotplug();
10: mutex_unlock(&bL_switcher_activation_lock);
11: return 0;
12: }
13:
14: pr_info("big.LITTLE switcher initializingn");
15:
16: ret = bL_activation_notify(BL_NOTIFY_PRE_ENABLE);
17: if (ret)
18: goto error;
19:
20: ret = bL_switcher_halve_cpus();
21: if (ret)
22: goto error;
23:
24: bL_switcher_trace_trigger();
25:
26: for_each_online_cpu(cpu) {
27: struct bL_thread *t = &bL_threads[cpu];
28: spin_lock_init(&t->lock);
29: init_waitqueue_head(&t->wq);
30: init_completion(&t->started);
31: t->wanted_cluster = -1;
32: t->task = bL_switcher_thread_create(cpu, t);
33: }
34:
35: bL_switcher_active = 1;
36: bL_activation_notify(BL_NOTIFY_POST_ENABLE);
37: pr_info("big.LITTLE switcher initializedn");
38: goto out;
39:
40: error:
41: pr_warn("big.LITTLE switcher initialization failedn");
42: bL_activation_notify(BL_NOTIFY_POST_DISABLE);
43:
44: out:
45: unlock_device_hotplug();
46: mutex_unlock(&bL_switcher_activation_lock);
47: return ret;
48: }
主要完成如下事情:
16行,调用bL_activation_notify,向arm big little cpufreq driver发送BL_NOTIFY_PRE_ENABLE通知,cpufreq driver收到该通知后,会调用cpufreq_unregister_driver,将bL_cpufreq_driver注销(具体可参考drivers/cpufreq/arm_big_little.c中的bL_cpufreq_switcher_notifier接口)。
20行,调用bL_switcher_halve_cpus接口,将系统所有possible的CPU core配对,并关闭不需要的core。该接口是本文的精髓,后面会稍微详细的介绍。
26~33行,为每个处于online状态的CPU core(此处已经是虚拟的core了,该core是一个big/Little对,同一时刻只有一个core开启),初始化用于cluster switch的线程。
35~36行,将bL_switcher_active置1,此时bL switcher正式enable了,向arm big little cpufreq driver发送BL_NOTIFY_POST_ENABLE通知,cpufreq driver会重新注册bL_cpufreq_driver。
4.2.2 bL_switcher_halve_cpus
bL_switcher_halve_cpus是ARM big·Little driver灵魂式的存在,它负责把系统8个big Little core转化成4个虚拟的CPU core,例如,这8个core是这样排布的(以cpu的逻辑ID为索引):
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
Little | Little | Little | Little | big | big | big | big |
bL_switcher_halve_cpus会将不同cluster的core组成一对,最终的结果保存在bL_switcher_cpu_pairing数组中,形式如下:
代码语言:javascript复制bL_switcher_cpu_pairing[0] = 7
bL_switcher_cpu_pairing[1] = 6
bL_switcher_cpu_pairing[2] = 5
bL_switcher_cpu_pairing[3] = 4
配对之后,把core 4~7 disable掉,就保证系统当前只有4个虚拟的CPU core了。
4.2.3 bL_cpufreq_switcher_notifier
bL_switcher_enable前后,会通知arm big little cpufreq driver,该driver会把bL_cpufreq_driver注销之后再重新注册,整个过程又回到了“arm big little cpufreq driver的初始化”过程,具体可参考4.1.1。
4.3 cluster切换
最后,我们来看一下big core和Little core到底是怎么切换的。切换是由arm big little cpufreq driver的bL_cpufreq_set_target发起的,该接口的实现如下:
代码语言:javascript复制 1: static int bL_cpufreq_set_target(struct cpufreq_policy *policy,
2: unsigned int index)
3: {
4: u32 cpu = policy->cpu, cur_cluster, new_cluster, actual_cluster;
5: unsigned int freqs_new;
6:
7: cur_cluster = cpu_to_cluster(cpu);
8: new_cluster = actual_cluster = per_cpu(physical_cluster, cpu);
9:
10: freqs_new = freq_table[cur_cluster][index].frequency;
11:
12: if (is_bL_switching_enabled()) {
13: if ((actual_cluster == A15_CLUSTER) &&
14: (freqs_new < clk_big_min)) {
15: new_cluster = A7_CLUSTER;
16: } else if ((actual_cluster == A7_CLUSTER) &&
17: (freqs_new > clk_little_max)) {
18: new_cluster = A15_CLUSTER;
19: }
20: }
21:
22: return bL_cpufreq_set_rate(cpu, actual_cluster, new_cluster, freqs_new);
23: }
切换的要点包括:
1)由4.2.2的描述可知,在bL switcher处于enable状态时,对调度器而言,只有4个core可见,而且这4个core的logical map是不变的,例如都是0、1、2、3。每一个core在物理上和2个属于不同cluster的core对应,同一时刻只有一个物理core运行
2)这4个core所处的“状态”(哪个物理core处于运行状态,big or Little),记录在“physical_cluster”中。
3)当经由cpufreq framework进行频率调整的时候,根据当前的“状态”,以及要调整的目的频率,计算是否需要切换cluster(也即disable当前正在运行的物理core,enable另外一个物理core)。
4)最终,以当前“状态”(actual_cluster)、新“状态”(new_cluster)等为参数,调用bL_cpufreq_set_rate接口,设置频率。
bL_cpufreq_set_rate接口经过一番处理后,得到真实的频率值,调用clock framework提供的接口(clk_set_rate)修改频率。之后,如果old_cluster和new_cluster不同,则调用arm bL switcher driver提供的bL_switch_request接口,进行cluster切换。该接口会启动一个线程,完成切换动作。该线程的处理函数是bL_switcher_thread,它会以目的cluster为参数,调用bL_switch_to接口,完成最终的切换操作。
bL_switch_to接口的实现比较复杂,需要迁移当前物理core的中断、timer,并disable当前物理core,然后enable目的cluster所代表的物理core,具体过程就不再详细分析了。