Linux cpuidle framework(1)_概述和软件架构
作者:wowo 发布于:2014-12-17 23:04 分类:电源管理子系统
1. 前言
在计算机系统中,CPU的功能是执行程序,总结起来就是我们在教科书上学到的:取指、译码、执行。那么问题来了,如果没有程序要执行,CPU要怎么办?也许您会说,停掉就是了啊。确实,是要停掉,但何时停、怎么停,却要仔细斟酌,因为实际的软硬件环境是非常复杂的。
我们回到Linux kernel上,Linux系统中,CPU被两类程序占用:一类是进程(或线程),也称进程上下文;另一类是各种中断、异常的处理程序,也称中断上下文。
进程的存在,是用来处理事务的,如读取用户输入并显示在屏幕上。而事务总有处理完的时候,如用户不再输入,也没有新的内容需要在屏幕上显示。此时这个进程就可以让出CPU,但会随时准备回来(如用户突然有按键动作)。同理,如果系统没有中断、异常事件,CPU就不会花时间在中断上下文。
在Linux kernel中,这种CPU的无所事事的状态,被称作idle状态,而cpuidle framework,就是为了管理这种状态。
注:cpuidle framework系列文章会以ARM64作为示例平台,由于ARM64刚刚发布不久,较早版本的kernel没有相关的代码,因此选用了最新的3.18-rc4版本的kernel。
2. 功能概述
曾经有过一段时间,Linux kernel的cpu idle框架是非常简单的,简单到driver工程师只需要在“include\asm-arm\arch-xxx\system.h”中定义一个名字为arch_idle的inline函数,并在该函数中调用kernel提供的cpu_do_idle接口,就Okay了,剩下的实现kernel全部帮我们做了,如下:
1: static inline void arch_idle(void)
2: {
3: cpu_do_idle();
4: }
以蜗蜗之前使用过的一个ARM926的单核CPU为例(内核版本为Linux2.6.23),cpuidle的处理过程是:
B start_kernel(arch\arm\kernel\head-common.S)
start_kernel->rest_init(init\main.c)
;系统初始化完成后,将第一个进程(init)变为idle进程,
;以下都是在进程的循环中,周而复始…
cpu_idle->default_idle(arch\arm\kernel\process.c)
arch_idle(include\asm-arm\arch-xxx\system.h)
cpu_do_idle(include/asm-arm/cpu-single.h)
cpu_arm926_do_idle(arch/arm/mm/proc-arm926.S)
mcr p15, 0, r0, c7, c0, 4 @ Wait for interrupt ;WFI指令
虽然简单,却包含了idle处理的两个重点:
1)idle进程
idle进程的存在,是为了解决“何时idle”的问题。
我们知道,Linux系统运行的基础是进程调度,而所有进程都不再运行时,称作cpu idle。但是,怎么判断这种状态呢?kernel采用了一个比较简单的方法:在init进程(系统的第一个进程)完成初始化任务之后,将其转变为idle进程,由于该进程的优先级是最低的,所以当idle进程被调度到时,则说明系统的其它进程不再运行了,也即CPU idle了。最终,由idle进程调用idle指令(这里为WFI),让CPU进入idle状态。
“ARM WFI和WFE指令”中介绍过,WFI Wakeup events会把CPU从WFI状态唤醒,通常情况下,这些events是一些中断事件,因此CPU唤醒后会执行中断handler,在handler中会wakeup某些进程,在handler返回的时候进行调度,当没有其他进程需要调度执行的时候,调度器会恢复idle进程的执行,当然,idle进程不做什么,继续进入idle状态,等待下一次的wakeup。
2)WFI
WFI用于解决“怎么idle”的问题。
一般情况下,ARM CPU idle时,可以使用WFI指令,把CPU置为Wait for interrupt状态。该状态下,至少(和具体ARM core的实现有关,可参考“ARM WFI和WFE指令”)会把ARM core的clock关闭,以节省功耗。
也许您会觉得,上面的过程挺好了,为什么还要开发cpuide framework?蜗蜗的理解是:
ARM CPU的设计越来越复杂,对省电的要求也越来越苛刻,因而很多CPU会从“退出时的延迟”和“idle状态下的功耗”两个方面考虑,设计多种idle级别。对延迟较敏感的场合,可以使用低延迟、高功耗的idle;对延迟不敏感的场合,可以使用高延迟、低功耗的idle。
而软件则需要根据应用场景,在恰当的时候,选择一个合适的idle状态。而选择的策略是什么,就不是那么简单了。这就是cpuidle framework的存在意义(我们可以根据下面cpuidle framework的软件架构,佐证这一点)。
3. 软件架构
Linux kernel中,cpuidle framework位于“drivers/cpuidle”文件夹中,包含cpuidle core、cpuidle governors和cpuidle drivers三个模块,再结合位于kernel sched中的cpuidle entry,共同完成cpu的idle管理。软件架构如下图:
1)kernel schedule模块
位于kernel\sched\idle.c中,负责实现idle线程的通用入口(cpuidle entry)逻辑,包括idle模式的选择、idle的进入等等。
2)cpuidle core
cpuidle core负责实现cpuidle framework的整体框架,主要功能包括:
根据cpuidle的应用场景,抽象出cpuidle device、cpuidle driver、cpuidle governor三个实体;
以函数调用的形式,向上层sched模块提供接口;
以sysfs的形式,向用户空间提供接口;
向下层的cpuidle drivers模块,提供统一的driver注册和管理接口;
向下层的governors模块,提供统一的governor注册和管理接口。
cpuidle core的代码主要包括:cpuidle.c、driver.c、governor.c、sysfs.c。
3)cpuidle drivers
负责idle机制的实现,即:如何进入idle状态,什么条件下会退出,等等。
不同的architecture、不同的CPU core,会有不同的cpuidle driver,平台驱动的开发者,可以在cpuidle core提供的框架之下,开发自己的cpuidle driver。代码主要包括:cpuidle-xxx.c。
4)cpuidle governors
Linux kernel的framework有两种比较固定的抽象模式:
模式1,provider/consumer模式,interrupt、clock、timer、regulator等大多数的framework是这种模式。它的特点是,这个硬件模块是为其它一个或多个模块服务的,因而framework需要从对上(consumer)和对下(provider)两个角度进行软件抽象;
模式2,driver/governor模式,本文所描述的cpuidle framework即是这种模式。它的特点是:硬件(或者该硬件所对应的驱动软件)可以提供多种可选“方案”(这里即idle level),“方案”的实现(即机制),由driver负责,但是到底选择哪一种“方案”(即策略),则由另一个模块负责(即这里所说的governor)。
模式2的解释可能有点抽象,把它放到cpuidle的场景里面,就很容易理解了:
前面讲过,很多CPU提供了多种idle级别(即上面所说的“方案”),这些idle 级别的主要区别是“idle时的功耗”和“退出时延迟”。cpuidle driver(机制)负责定义这些idle状态(每一个状态的功耗和延迟分别是多少),并实现进入和退出相关的操作。最终,cpuidle driver会把这些信息告诉governor,由governor根据具体的应用场景,决定要选用哪种idle状态(策略)。
kernel中,cpuidle governor都位于governors/目录下。
4. 软件流程
在阅读本章之前,还请读者先阅读如下三篇文章:
“Linux cpuidle framework(2)_cpuidle core”
前面提到过,kernel会在系统启动完成后,在init进程(或线程)中,处理cpuidle相关的事情。大致的过程是这样的(kernel启动相关的分析,会在其它文章中详细介绍):
首先需要说明的是,在SMP(多核)系统中,CPU启动的过程是:
1)先启动主CPU,启动过程和传统的单核系统类似:stext-->start_kernel-->rest_init-->cpu_startup_entry
2)启动其它CPU,可以有多种方式,例如CPU hotplug等,启动过程为:secondary_startup-->__secondary_switched-->secondary_start_kernel-->cpu_startup_entry
上面的代码位于./arch/arm64/kernel/head.S、init/main.c等等,感兴趣的读者可以自行参考。最终都会殊途同归,运行至cpu_startup_entry接口,该接口位于kernel/sched/idle.c中,负责处理CPU idle的事情,流程如下(暂时忽略一些比较难理解的分支,如cpu idle poll等)。
cpu_startup_entry流程:
cpu_startup_entry
arch_cpu_idle_prepare,进行idle前的准备工作,ARM64中没有实现
cpu_idle_loop,进入cpuidle的主循环
如果系统当前不需要调度(!need_resched()),执行后续的动作
local_irq_disable,关闭irq中断
arch_cpu_idle_enter,arch相关的cpuidle enter,ARM64中没有实现
cpuidle_idle_call,main idle function
cpuidle_select,通过cpuidle governor,选择一个cpuidle state
cpuidle_enter,通过cpuidle state,进入该idle状态
…
中断产生,idle返回(注意,此时irq是被禁止的,因此CPU不能响应产生中断的事件)
cpuidle_reflect,通知cpuidle governor,更新状态
local_irq_enable,使能中断,响应中断事件,跳转到对应的中断处理函数
…
arch_cpu_idle_exit,和enter类似,ARM64没有实现
具体的代码比较简单,不再分析了,但有一点,还需要着重说明一下:
使用cpuidle framework进入idle状态时,本地irq是处于关闭的状态,因此从idle返回时,只能接着往下执行,直到irq被打开,才能执行相应的中断handler,这和之前传统的cpuidle不同。同时也间接证实了“Linux cpuidle framework(4)_menu governor”中所提及的,为什么menu governor在reflect接口中只是简单的置一个标志。因为reflect是在关中断时被调用的,需要尽快返回,以便处理中断事件。
原创文章,转发请注明出处。蜗窝科技,www.wowotech.net。

评论:
2019-05-13 00:09
看了您的cpuidle framework系列,受益匪浅。现在有个实际场景:我们的线程运行结束后进入某个特定的cpuidle state时(或从该cpuidle state恢复时,具体未知),性能会跑得异常差,其他idle state均正常。请问:
1. 目前想到的解决方案是将sys/devices/system/cpu/cpux/cpuidle下的disable写成1。这种方法在实际中是否有明显已知的问题?
2. 内核是否有提供某个API来控制某个cpuidle state的使能状态?
3. 是否有其他解决该问题的思路?
2018-05-07 15:04
有遇到过偶发关机时无缘无故死机的问题,没有panic等异常log,多次测试发现取消CONFIG_CPU_IDLE内核配置项就不会出现,有试过把kernel hacking的一些调试选项打开,问题也不会出现。
1)遇到这种问题该如何分析定位?
2)关闭该内核选项会对产品造成什么后果?
2018-05-08 09:11
如果连idle都没法开,这个cpu就太水了。
至于后果,就是功耗、发热之类的可能会上去吧。
2018-05-08 15:06
例如这种:
[ 110.908373] sunxi-ehci 1c1c000.ehci2-controller: shutdown
[ 110.914589] drivers/usb/host/ehci_sunxi.c sunxi_ehci_
或者这种:
[ 177.662155] [ohci2-controller]: ohci shutdown end
[ 177.667322] sunxi-ehci 1c1c000.
或者这种:
[ 47.538096] drivers/usb/host/sunxi_hci.c close_clock 406
[ 47.544242] drivers/usb/host/ohci_sunxi.c sunxi_ohci_hcd_
这问题还偶发,自动化测试一两个小时才出现一次。实在找不到好的对策或者继续排查的办法。后来对比原厂的改动才动到IDEL的配置的,他们的代码里面早就取消了这个config
2015-11-26 11:11
2015-11-27 16:46
Kernel调度器对idle thread有特殊照顾,具体可参考kernel/sched/core.c:
static void set_load_weight(struct task_struct *p)
{
int prio = p->static_prio - MAX_RT_PRIO;
struct load_weight *load = &p->se.load;
/*
* SCHED_IDLE tasks get minimal weight:
*/
if (p->policy == SCHED_IDLE) {
load->weight = scale_load(WEIGHT_IDLEPRIO);
load->inv_weight = WMULT_IDLEPRIO;
return;
}
load->weight = scale_load(prio_to_weight[prio]);
load->inv_weight = prio_to_wmult[prio];
}
2015-12-02 10:34
1. idle进程的优先级并不是最低的,实际上,它的用户优先级(user nice)是0,正常情况下,算得上系统中最高的了。
2. idle进程并不会参与调度,因此当无进程调度的时候执行idle进程,是调度器保证的,这一点并不依赖它的优先级。
3. idle进程的优先级怎么来的?
a)start_kernel后kernel执行的第一个进程,不是由do_fork动态生成的,而是静态定义的(也就是传说中的init_task),init_task的优先级为(include/linux/init_task.h):
.prio = MAX_PRIO-20, \
.static_prio = MAX_PRIO-20, \
.normal_prio = MAX_PRIO-20,
也就是说,全部是120,转换为user nice为0,user nice的范围是-20~19,值越大优先越低,因此init_task的优先级算是比较高的了。
b)随后的kernel运行过程中,都是在init_task的上下文中,最后init_task会变成boot cpu的idle线程,因此boot cpu的idle线程的优先级就继承了init_task的了。
c)对于其它CPU来说,执行如下操作(具体可参考”http://www.wowotech.net/pm_subsystem/cpu_hotplug.html“):rest_init-->kernel_init-->kernel_init_freeable-->smp_init—>idle_threads_init—>idle_init-->fork_idle-->copy_process-->dup_task_struct(current)
这个过程会两次task copy:第一次是在reset_init中,调用kernel_thread fork kernel_init线程;第二次是fork_idle时将current task copy到idle task中,因此,最终非boot cpu的idle task的优先级,也是继承自init_task。
4. 关于这个标记--SCHED_IDLE,现在kernel的调度器貌似没有使用的。
进程管理的东西太复杂了,看着战战巍巍的,希望高手指正!也希望有高手把进程管理的东西给大家分析一下。
2015-12-02 11:29
schedule函数中,按照rt ,cfs,idle这三种调度方式顺序,寻找各自的运行任务,那么如果rt和cfs都未找到运行任务,那么最后会调用idle schedule的idle进程,作为schedule函数调度的下一个任务。
2016-07-01 17:40
symbol not found: _sched_class
possible alternatives:
ffffffc0009cfd28 (r) fake_sched_class
ffffffc0009d08d8 (R) idle_sched_class
ffffffc0009d0ac8 (R) fair_sched_class
ffffffc0009d0d00 (R) rt_sched_class
ffffffc0009d0ed8 (R) dl_sched_class
ffffffc0009d1000 (R) stop_sched_class
crash64> struct sched_class ffffffc0009d08d8
struct sched_class {
next = 0x0,
enqueue_task = 0x0,
dequeue_task = 0xffffffc0000d7d08 <dequeue_task_idle>,
yield_task = 0x0,
yield_to_task = 0x0,
check_preempt_curr = 0xffffffc0000d7cdc <check_preempt_curr_idle>,
pick_next_task = 0xffffffc0000d7b90 <pick_next_task_idle>,
put_prev_task = 0xffffffc0000d7cb0 <put_prev_task_idle>,
select_task_rq = 0xffffffc0000d7b64 <select_task_rq_idle>,
migrate_task_rq = 0x0,
post_schedule = 0x0,
task_waking = 0x0,
task_woken = 0x0,
set_cpus_allowed = 0x0,
rq_online = 0x0,
rq_offline = 0x0,
set_curr_task = 0xffffffc0000d7be8 <set_curr_task_idle>,
task_tick = 0xffffffc0000d7bd0 <task_tick_idle>,
task_fork = 0x0,
task_dead = 0x0,
switched_from = 0x0,
switched_to = 0xffffffc0000d7c70 <switched_to_idle>,
prio_changed = 0xffffffc0000d7c34 <prio_changed_idle>,
get_rr_interval = 0xffffffc0000d7c00 <get_rr_interval_idle>,
update_curr = 0xffffffc0000d7c1c <update_curr_idle>,
task_move_group = 0x0
}
整个体系好像有六种sched_class,芯片架构为arm64 V8,大神清楚分别对应什么么?
2015-10-26 20:39
2015-10-26 20:49
STR和cpuidle,不是同一个事物,它们的目标不同,不具有可比性。
如果碰巧,它们通过相同的手段达到目的,这只是一个假象而已。
例如,系统进入STR状态时,就不会有idle的概念(idle线程被冻结了)。
所以,您的问题:怎么结合的?我觉得应该不会结合。
2015-10-19 10:37
2015-10-19 11:06
idle thread的创建过程是:fork_idle-->copy_process
我对进程管理看的不多,不知道问题的答案(是,或者不是),但根据这个过程,我觉得所有的idle thread的pid应该是一个,即init_struct_pid。linuxer,是不是这样呢?
2016-07-01 17:46
> 0 0 0 ffffffc000f80160 RU 0.0 0 0 [swapper/0]
> 0 0 1 ffffffc13ca00c00 RU 0.0 0 0 [swapper/1]
> 0 0 2 ffffffc13ca01800 RU 0.0 0 0 [swapper/2]
> 0 0 3 ffffffc13ca02400 RU 0.0 0 0 [swapper/3]
> 0 0 4 ffffffc13ca03000 RU 0.0 0 0 [swapper/4]
> 0 0 5 ffffffc13ca03c00 RU 0.0 0 0 [swapper/5]
> 0 0 6 ffffffc13ca04800 RU 0.0 0 0 [swapper/6]
> 0 0 7 ffffffc13ca05400 RU 0.0 0 0 [swapper/7]
pid都是0,每个cpu上独立的线程,相互之间也扯不上什么关系吧,谈不上线程组,这个swapper线程就是所谓的idle线程。
2015-03-03 11:07
如果 cpu 进入是其他状态(C1/C2 state),退出时,则是 cpu 会重新上电,然后从 BROM 运行,之后是 mcpm_entry_point 函数 -> cpu_resume 函数。此时 cpu 的上下文已经恢复,上下文还是之前的 idle 线程,之后的流程与 C0 类似。
2014-12-18 10:12
你文章中说到:“当任意事件把CPU从idle状态唤醒时,接着运行idle进程,idle进程会判断是否有其它进程需要运行,如果有则发起一次调度,将CPU让给其它进程。”
当ARM处理器调用WFI指令进入idle状态的时候,可以唤醒它的至少应该包括下面的场景:
(1)来自其他处理器的SEV指令
(2)送达本处理器上的中断
当是中断唤醒的场景的时候,是否应该立刻执行中断handler?难道还是要先运行idle进程?我猜想应该先执行中断handler,在handler中会wakeup某些进程,在handler返回的时候进行调度(而不是idle进程判断是否要有其他进程需要运行),当没有其他进程需要调度执行的时候,调度器会恢复idle进程的执行,当然,idle进程不做什么,继续进入idle状态,等待下一次的wakeup
2014-12-18 12:30
对于后面比较复杂的idle framework,是否会存在像suspend/resume那样的场景,可以在idle时suspend,然后返回时继续往下执行,这个还不太清楚,要往下继续看。
PS:WFE才会有SEV指令,WFI没有。
2015-01-20 23:31
2015-07-15 22:54
thanks
2015-07-22 11:12
----有点小疑惑,既然关中断了。中断还能唤醒WFI状态吗?多谢!
2017-07-15 21:40
The WFI instruction has the effect of suspending execution until the core is woken up by one of the following conditions:
• An IRQ interrupt, even if the CPSR I-bit is set.
• An FIQ interrupt, even if the CPSR F-bit is set.
• An asynchronous abort.
• A Debug Entry request, even if JTAG Debug is disabled.
2020-08-20 17:24
Interrupts which are pending would not prevent processor to suspend execution.
When the processor is in WFI and an interrupt occurs:
This wakes up the processor and the processor jumps to the interrupt handler associated with that interrupt.
But when CPSR.I is disabled this just wakes up the processor and the processor starts executing the next instruction from the program counter.
功能
最新评论
- wangjing
写得太好了 - wangjing
写得太好了! - DRAM
圖面都沒辦法顯示出來好像掛點了。 - Simbr
bus至少是不是还有个subsystem? - troy
@testtest:只要ldrex-modify-strex... - gh
Linux 内核在 sparse 内存模型基础上实现了vme...
文章分类
随机文章
文章存档
- 2025年4月(5)
- 2024年2月(1)
- 2023年5月(1)
- 2022年10月(1)
- 2022年8月(1)
- 2022年6月(1)
- 2022年5月(1)
- 2022年4月(2)
- 2022年2月(2)
- 2021年12月(1)
- 2021年11月(5)
- 2021年7月(1)
- 2021年6月(1)
- 2021年5月(3)
- 2020年3月(3)
- 2020年2月(2)
- 2020年1月(3)
- 2019年12月(3)
- 2019年5月(4)
- 2019年3月(1)
- 2019年1月(3)
- 2018年12月(2)
- 2018年11月(1)
- 2018年10月(2)
- 2018年8月(1)
- 2018年6月(1)
- 2018年5月(1)
- 2018年4月(7)
- 2018年2月(4)
- 2018年1月(5)
- 2017年12月(2)
- 2017年11月(2)
- 2017年10月(1)
- 2017年9月(5)
- 2017年8月(4)
- 2017年7月(4)
- 2017年6月(3)
- 2017年5月(3)
- 2017年4月(1)
- 2017年3月(8)
- 2017年2月(6)
- 2017年1月(5)
- 2016年12月(6)
- 2016年11月(11)
- 2016年10月(9)
- 2016年9月(6)
- 2016年8月(9)
- 2016年7月(5)
- 2016年6月(8)
- 2016年5月(8)
- 2016年4月(7)
- 2016年3月(5)
- 2016年2月(5)
- 2016年1月(6)
- 2015年12月(6)
- 2015年11月(9)
- 2015年10月(9)
- 2015年9月(4)
- 2015年8月(3)
- 2015年7月(7)
- 2015年6月(3)
- 2015年5月(6)
- 2015年4月(9)
- 2015年3月(9)
- 2015年2月(6)
- 2015年1月(6)
- 2014年12月(17)
- 2014年11月(8)
- 2014年10月(9)
- 2014年9月(7)
- 2014年8月(12)
- 2014年7月(6)
- 2014年6月(6)
- 2014年5月(9)
- 2014年4月(9)
- 2014年3月(7)
- 2014年2月(3)
- 2014年1月(4)
2019-06-05 01:01