驱动篇:inux 电源管理的系统架构和驱动(二)

驱动篇: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 子系统的总体架构:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值