驱动篇:inux 电源管理的系统架构和驱动(二)
CPUFreq 的策略
SoC CPUFreq 驱动只是设定了 CPU 的频率参数,以及提供了设置频率的途径,但是它并不会管 CPU 自身究竟应该运行在哪种频率上。究竟频率依据的是哪种标准,进行何种变化,而这些完全由 CPUFreq 的策略( policy )决定,这些策略如表所示。
在 Android 系统中,则增加了 1 个交互策略,该策略适合于对延迟敏感的 UI 交互任务,当有 UI 交互任务的时候,该策略会更加激进并及时地调整 CPU 频率。
总而言之,系统的状态以及 CPUFreq 的策略共同决定了 CPU 频率跳变的目标, CPUFreq 核心层并将目标频率传递给底层具体 SoC 的 CPUFreq驱动,该驱动修改硬件,完成频率的变换
用户空间一般可通过 /sys/devices/system/cpu/cpux/cpufreq 节点来设置 CPUFreq 。譬如,我们要设置 CPUFreq 到 700MHz ,采用 userspace 策略,
则运行如下命令:
# echo userspace > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# echo 700000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed
CPUFreq 的性能测试和调优
Linux 3.1 以后的内核已经将 cpupower-utils 工具集放入内核的 tools/power/cpupower 目录中,该工具集当中的cpufreq-bench 工具可以帮助工程师分析采用 CPUFreq 后对系统性能的影响。
cpufreq-bench 工具的工作原理是模拟系统运行时候的 “ 空闲 → 忙 → 空闲 → 忙 ” 场景,从而触发系统的动态频率变化,然后在使用 ondemand 、 conservative 、 interactive 等策略的情况下,计算在做与 performance 高频模式下同样的运算完成任务的时间比例。
交叉编译该工具后,可放入目标电路板文件系统的 /usr/sbin/ 等目录下,运行该工具:
# cpufreq-bench -l 50000 -s 100000 -x 50000 -y 100000 -g ondemand -r 5 -n 5 -v
会输出一系列的结果,我们提取其中的 Round n 这样的行,它表明了 -g ondemand 选项中设定的 ondemand 策略相对于 performance 策略的性能比例,假设值为:
Round 1 - 39.74%
Round 2 - 36.35%
Round 3 - 47.91%
Round 4 - 54.22%
Round 5 - 58.64%
这显然不太理想,我们在同样的平台下采用 Android 的交互策略,得到新的测试结果:
Round 1 - 72.95%
Round 2 - 87.20%
Round 3 - 91.21%
Round 4 - 94.10%
Round 5 - 94.93%
一般的目标是在采用 CPUFreq 动态调整频率和电压后,性能应该为 performance 这个高性能策略下的 90% 左右,这样才比较理想
CPUFreq 通知
CPUFreq 子系统会发出通知的情况有两种: CPUFreq 的策略变化或者 CPU 运行频率变化。
在策略变化的过程中,会发送 3 次通知:
·CPUFREQ_ADJUST :所有注册的 notifier 可以根据硬件或者温度的情况去修改范围(即 policy->min 和 policy-> max );
·CPUFREQ_INCOMPATIBLE :除非前面的策略设定可能会导致硬件出错,否则被注册的 notifier 不能改变范围等
设定;
·CPUFREQ_NOTIFY :所有注册的 notifier 都会被告知新的策略已经被设置。
在频率变化的过程中,会发送 2 次通知:
·CPUFREQ_PRECHANGE :准备进行频率变更;
·CPUFREQ_POSTCHANGE :已经完成频率变更。
notifier 中的第 3 个参数是一个 cpufreq_freqs 的结构体,包含 cpu ( CPU 号)、 old (过去的频率)和 new (现在的频率)这 3 个成员。发送 CPUFREQ_PRECHANGE 和 CPUFREQ_POSTCHANGE 的代码如下:
srcu_notifier_call_chain(&cpufreq_transition_notifier_list,
CPUFREQ_PRECHANGE, freqs);
srcu_notifier_call_chain(&cpufreq_transition_notifier_list,
CPUFREQ_POSTCHANGE, freqs);
如果某模块关心 CPUFREQ_PRECHANGE 或 CPUFREQ_POSTCHANGE 事件,可简单地使用 Linux notifier 机制监控。譬如, drivers/video/sa1100fb.c 在 CPU 频率变化过程中需对自身硬件进行相关设置,因此它注册了 notifier 并在CPUFREQ_PRECHANGE 和 CPUFREQ_POSTCHANGE 情况下分别进行不同的处理
CPUFreq notifier 案例:
fbi->freq_transition.notifier_call = sa1100fb_freq_transition;
cpufreq_register_notifier(&fbi->freq_transition, CPUFREQ_TRANSITION_NOTIFIER);
...
sa1100fb_freq_transition(structnotifier_block *nb, unsigned long val,void *data)
{
struct sa1100fb_info *fbi = TO_INF(nb, freq_transition);
struct cpufreq_freqs *f = data;
u_int pcd;
switch (val) {
case CPUFREQ_PRECHANGE:
set_ctrlr_state(fbi, C_DISABLE_CLKCHANGE);
break;
case CPUFREQ_POSTCHANGE:
pcd = get_pcd(fbi->fb.var.pixclock, f->new);
fbi->reg_lccr3 = (fbi->reg_lccr3& ~0xff) | LCCR3_PixClkDiv(pcd);
set_ctrlr_state(fbi, C_ENABLE_CLKCHANGE);
break;
}
return 0;
}
此外,如果在系统挂起 / 恢复的过程中 CPU 频率会发生变化,则 CPUFreq 子系统也会发出CPUFREQ_SUSPENDCHANGE 和 CPUFREQ_RESUMECHANGE 这两个通知。
值得一提的是,除了 CPU 以外,一些非 CPU 设备也支持多个操作频率和电压,存在多个 OPP 。 Linux 3.2 之后的内核也支持针对这种非 CPU 设备的 DVFS ,该套子系统为 Devfreq 。与 CPUFreq 存在一个 drivers/cpufreq 目录相似,在内核中也存在一个 drivers/devfreq 的目录。
CPUIdle 驱动
目前的 ARM SoC 大多支持几个不同的 Idle 级别, CPUIdle 驱动子系统存在的目的就是对这些 Idle 状态进行管理,并根据系统的运行情况进入不同的 Idle 级别。具体 SoC 的底层 CPUIdle 驱动实现则提供一个类似于 CPUFreq 驱动频率表的 Idle 级别表,并实现各种不同 Idle 状态的进入和退出流程。
对于 Intel 系列笔记本计算机而言,支持 ACPI ( Advanced Configuration and Power Interface ,高级配置和电源接口),一般有 4 个不同的 C 状态(其中 C0 为操作状态, C1 是 Halt 状态, C2 是 Stop-Clock 状态, C3 是 Sleep 状态),如表所示。
而对于 ARM 而言,各个 SoC 对于 Idle 的实现方法差异比较大,最简单的 Idle 级别莫过于将 CPU 核置于 WFI (等待中断发生)状态,因此在默认情况下,若 SoC 未实现自身的芯片级 CPUIdle 驱动,则会进入 cpu_do_idle (),对于 ARM V7 而言,其实现位于 arch/arm/mm/proc-v7.S 中:
ENTRY(cpu_v7_do_idle)
dsb @ WFI may enter a low-power mode
wfi
mov
pc, lr
ENDPROC(cpu_v7_do_idle)
与 CPUFreq 类似, CPUIdle 的核心层提供了如下 API 以用于注册一个 cpuidle_driver 的实例:
int cpuidle_register_driver(struct cpuidle_driver *drv);
并提供了如下 API 来注册一个 cpuidle_device :
int cpuidle_register_device(struct cpuidle_device *dev);
CPUIdle 驱动必须针对每个 CPU 注册相应的 cpuidle_device ,这意味着对于多核 CPU 而言,需要针对每个 CPU 注册一次。cpuidle_register_driver ()接受 1 个 cpuidle_driver 结构体的指针参数,该结构体是 CPUIdle 驱动的主体,其定义如下:
cpuidle_driver 结构体
struct cpuidle_driver {
const char *name;
struct module *owner;
int refcnt;
/* used by the cpuidle framework to setup the broadcast timer */
unsigned int bctimer:1;
/* states array must be ordered in decreasing power consumption */
struct cpuidle_state states[CPUIDLE_STATE_MAX];
int state_count;
int safe_state_index;
/* the driver handles the cpus in cpumask */
struct cpumask *cpumask;
};
该结构体的关键成员是 1 个 cpuidle_state 的表,其实该表就是用于存储各种不同 Idle 级别的信息
cpuidle_state 结构体:
struct cpuidle_state {
char name[CPUIDLE_NAME_LEN];
char desc[CPUIDLE_DESC_LEN];
unsigned int flags;
unsigned int exit_latency; /* in US */
int power_usage; /* in mW */
unsigned int target_residency; /* in US */
bool disabled; /* disabled on all CPUs */
int (*enter) (struct cpuidle_device *dev,
struct cpuidle_driver *drv,
int index);
int (*enter_dead) (struct cpuidle_device *dev, int index);
/*
* CPUs execute ->enter_s2idle with the local tick or entire timekeeping
* suspended, so it must not re-enable interrupts at any point (even
* temporarily) or attempt to change states of clock event devices.
*/
void (*enter_s2idle) (struct cpuidle_device *dev,
struct cpuidle_driver *drv,
int index);
};
name 和 desc 是该 Idle 状态的名称和描述, exit_latency 是退出该 Idle 状态需要的延迟,
enter ()是进入该 Idle 状态的实现方法。
忽略细节,一个具体的 SoC 的 CPUIdle 驱动实例可见于 arch/arm/mach-ux500/cpuidle.c (最新的内核已经将代码转移到了 drivers/cpuidle/cpuidle-ux500.c 中),它有两个 Idle 级别,即 WFI 和 ApIdle ,其具体实现框架如代码清单所示:
ux500CPUIdle 驱动案例
staticatomic_t master = ATOMIC_INIT(0);
static DEFINE_SPINLOCK(master_lock);
static DEFINE_PER_CPU(struct cpuidle_device, ux500_cpuidle_device);
static inline int ux500_enter_idle(struct cpuidle_device *dev,
struct cpuidle_driver *drv, int index)
{
...
}
staticstruct cpuidle_driver ux500_idle_driver = {
.name = "ux500_idle",
.owner = THIS_MODULE,
.en_core_tk_irqen = 1,
.states = {
ARM_CPUIDLE_WFI_STATE,
{
.enter= ux500_enter_idle,
.exit_latency = 70,
.target_residency = 260,
.flags= CPUIDLE_FLAG_TIME_VALID,
.name = "ApIdle",
.desc = "ARM Retention",
},
},
.safe_state_index = 0,
.state_count = 2,
};
/*
* For each cpu, setup the broadcast timer because we will
* need to migrate the timers for the states >= ApIdle.
*/
static void ux500_setup_broadcast_timer(void *arg)
{
intcpu = smp_processor_id();
clockevents_notify(CLOCK_EVT_NOTIFY_BROADCAST_ON, &cpu);
}
int __init ux500_idle_init(void)
{
...
ret = cpuidle_register_driver(&ux500_idle_driver);
...
for_each_online_cpu(cpu) {
device = &per_cpu(ux500_cpuidle_device, cpu);
device->cpu = cpu;
ret = cpuidle_register_device(device);
...
}
...
}
device_initcall(ux500_idle_init);
与 CPUFreq 类似,在 CPUIdle 子系统中也有对应的 governor 来抉择何时进入何种 Idle 级别的策略,这些 governor 包括CPU_IDLE_GOV_LADDER 、 CPU_IDLE_GOV_MENU 。 LADDER 在进入和退出 Idle 级别的时候是步进的,它以过去的 Idle 时间作为参考,而 MENU 总是根据预期的空闲时间直接进入目标 Idle 级别。前者适用于没有采用动态时间节拍的系统(即没有选择 NO_HZ 的系统),不依赖于 NO_HZ 配置选项,而后者依赖于内核的 NO_HZ 选项。
图中演示了 LADDER 步进从 C0 进入 C3 ,而 MENU 则可能直接从 C0 跳入 C3 :
CPUIdle 子系统还通过 sys 向 userspace 导出了一些节点:
· 一类是针对整个系统的 /sys/devices/system/cpu/cpuidle ,通过其中的 current_driver 、current_governor 、 available_governors 等节点可以获取或设置 CPUIdle 的驱动信息以及 governor 。
· 一类是针对每个 CPU 的 /sys/devices/system/cpu/cpux/cpuidle ,通过子节点暴露各个在线的 CPU 中每个不同 Idle 级别的 name 、 desc 、 power 、latency 等信息。
综合以上的各个要素,可以给出 Linux CPUIdle 子系统的总体架构: