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管理。软件架构如下图:cpuidle framework

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

Linux cpuidle framework(3)_ARM64 generic CPU idle driver

Linux cpuidle framework(4)_menu governor

前面提到过,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

标签: Linux framework cpuidle

评论:

wowo
2016-07-01 21:11
@Foreveryoung,schedule的东西还没有深入研究呢。
zozowit
2015-11-26 11:11
请问,“由于该进程的优先级是最低的,所以当idle进程被调度到时,则说明系统的其它进程不再运行了,也即CPU idle了”,这个进程的优先级是最低的,是在哪里有设置吗?
wowo
2015-11-27 16:46
@zozowit:抱歉,评论太多把您这个漏了……
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];                                
}
zozowit
2015-12-01 17:53
@wowo:感谢解答,还有一点请您赐教,SCHED_IDLE是在哪里设定的?
wowo
2015-12-02 10:34
@zozowit:抱歉,我为上面的回复以及文中的表述道歉,关于idle进程的优先级,应该是这样的(对进程管理的理解有限,可能还有错误):
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的调度器貌似没有使用的。

进程管理的东西太复杂了,看着战战巍巍的,希望高手指正!也希望有高手把进程管理的东西给大家分析一下。
zozowit
2015-12-02 10:43
@wowo:您好,恕我愚笨。请问“ idle进程并不会参与调度,因此当无进程调度的时候执行idle进程,是调度器保证的”这个是否有代码可以阅读。另外之前您列出的代码,看上去确实是把这个优先级调低诶?
/*                                                                      
         * SCHED_IDLE tasks get minimal weight:                                
         */                                                                    
        if (p->policy == SCHED_IDLE) {                                          
                load->weight = scale_load(WEIGHT_IDLEPRIO);                    
                load->inv_weight = WMULT_IDLEPRIO;                              
                return;                                                        
        }
wowo
2015-12-02 10:52
@zozowit:调度器的行为,我也没有细看,太复杂了。
我之前贴出的这一段代码,确实是这个逻辑,但我找了现在kernel的代码,没有发现哪个进程使用SCHED_IDLE这个policy,可能是kernel提供的一种方法吧,只是现在没有人用。
zozowit
2015-12-02 11:29
@wowo:您好,我看了一下,是在start_kernel-->init_idle中执行了rq->curr = rq->idle = idle
schedule函数中,按照rt ,cfs,idle这三种调度方式顺序,寻找各自的运行任务,那么如果rt和cfs都未找到运行任务,那么最后会调用idle schedule的idle进程,作为schedule函数调度的下一个任务。
wowo
2015-12-02 11:42
@zozowit:确实,idle作为一个单独的schedule class了,
const struct sched_class idle_sched_class
至于schedule class的含义,我就不了解了。
Foreveryoung
2016-07-01 17:40
@wowo:crash64> sym _sched_class
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,大神清楚分别对应什么么?
pingchangxin
2015-10-26 20:39
请教一个问题,现在内核支持supend to RAM,这个流程会在pm.c中有***_pm_enter,里面有PM_SUSPEND_STANDY或者PM_SUPEND_MEM,而现在cpuidle,应该也可以在**_enter_idle里面可以根据此时的硬件状态,来做现在是WFI,还是进deep sleep,总感觉这两个有点重合,不知道,你理解的这两者是怎么结合起来的,谢谢!
wowo
2015-10-26 20:49
@pingchangxin:我的理解是:
STR和cpuidle,不是同一个事物,它们的目标不同,不具有可比性。
如果碰巧,它们通过相同的手段达到目的,这只是一个假象而已。
例如,系统进入STR状态时,就不会有idle的概念(idle线程被冻结了)。
所以,您的问题:怎么结合的?我觉得应该不会结合。
pingchangxin
2015-10-27 13:50
@wowo:谢谢你,可能我被这个假象糊弄了,总觉得他们直接是不是有什么猫腻,呵呵
cym
2015-10-19 09:38
hi wowo,每个cpu都有一个idle进程(或叫线程),他们的pid是一样的吗?或者叫做一个线程组更为合适?
linuxer
2015-10-19 10:37
@cym:系统有多少个cpu core,就有多少个idle进程,BSP的idle进程的pid是0,其他的cpu的idle进程是在init进程(pid=1)中初始化的,因此secondary cpu的idle进程和BSP 的idle进程的pid是不同的。
wowo
2015-10-19 11:06
@linuxer:关于CPU的idle线程的创建过程,可以参考“http://www.wowotech.net/pm_subsystem/cpu_hotplug.html”中有关的章节。

idle thread的创建过程是:fork_idle-->copy_process

我对进程管理看的不多,不知道问题的答案(是,或者不是),但根据这个过程,我觉得所有的idle thread的pid应该是一个,即init_struct_pid。linuxer,是不是这样呢?
linuxer
2015-10-19 12:05
@wowo:我也没有仔细看,我的直觉是觉得fork一个进程当然要分配一个pid了,要不还是等晚上有时间仔细看看代码好了
linuxer
2015-10-19 12:27
@wowo:你说的是对的,我又看进去copy_process的代码,其中有一段:

    if (pid != &init_struct_pid) {
        pid = alloc_pid(p->nsproxy->pid_ns_for_children);
        if (IS_ERR(pid)) {
            retval = PTR_ERR(pid);
            goto bad_fork_cleanup_io;
        }
    }

看起来如果是init_struct_pid就不再分配pid了。
cym
2015-10-19 13:50
@linuxer:谢谢众大虾!
Foreveryoung
2016-07-01 17:46
@cym:crash64> ps |grep "swapper"
>     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线程。
wowo
2016-07-01 21:12
@Foreveryoung:是的。
firo
2015-08-30 20:01
因而framework需要从对上(consumer)和对下(consumer)两个角度进行软件抽象;
两个consumer?
wowo
2015-08-31 09:02
@firo:抱歉,笔误,等下会改正,谢谢~~
tim
2015-03-03 11:07
如果 cpu 进入的 idle 状态是 WFI(C0 state),则退出的流程是,先是 cpu 退出 WFI 状态,此时中断是被关闭的,所以不会响应中断,代码继续执行,当然执行的代码是 idle 线程,之后打开中断,执行中断处理函数。
如果 cpu 进入是其他状态(C1/C2 state),退出时,则是 cpu 会重新上电,然后从 BROM 运行,之后是 mcpm_entry_point 函数 -> cpu_resume 函数。此时 cpu 的上下文已经恢复,上下文还是之前的 idle 线程,之后的流程与 C0 类似。
linuxer
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
wowo
2014-12-18 12:30
@linuxer:传统的使用WFI的idle,确实应该是这样子啊,还是你总结的好,哈哈,等等我把这段话抄上去。
对于后面比较复杂的idle framework,是否会存在像suspend/resume那样的场景,可以在idle时suspend,然后返回时继续往下执行,这个还不太清楚,要往下继续看。

PS:WFE才会有SEV指令,WFI没有。
linuxer
2014-12-18 13:08
@wowo:呵呵~~我看错了,WFE是用在spin lock中, 我刚才还想在idle的时候使用WFE怪怪的,原来是自己看错了。
wowo
2015-01-20 23:31
@linuxer:我更新了这篇文章的最后一节,结果还真和我们想象的不一样,在cpuidle framework中,进入idle时,是关中断的,因此从idle回来后,只能先回到idle thread,打开中断后,才能执行中断handler。
Y
2015-07-15 22:54
@wowo:从C1被中断唤醒后,是继续执行 enter(C1)的下一条指令,不需要软件参与吗? 我看到楼上有兄弟说从BROM开始重新执行? 到底哪个是对的
thanks
wowo
2015-07-16 08:44
@Y:idle指令执行之后,CPU要做那些事情,idle回来之前,CPU要做哪些事情,都是和具体的平台有关的。不同平台,有不同的实现。
因此,回到您的问题,哪个都有可能是对的。这也是这篇文章没有涉及这部分内容的原因。
但有一点是可以保证的,对cpuidle framework本身,它“看到的”,是一致的,即:从哪里进去,就从哪里出来。
shoujixiaodao
2015-07-22 11:12
@wowo:进入idle时,是关中断的,因此从idle回来后,只能先回到idle thread,打开中断后,才能执行中断handler
   ----有点小疑惑,既然关中断了。中断还能唤醒WFI状态吗?多谢!
wowo
2015-07-22 13:17
@shoujixiaodao:可以的,具体可参考linuxer同学在这里的回复:http://www.wowotech.net/?post=81#2191
shoujixiaodao
2015-07-22 13:48
@wowo:学习力,涨姿势。多谢!
Allen
2017-07-15 21:40
@shoujixiaodao:4种方法
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.

发表评论:

Copyright @ 2013-2015 蜗窝科技 All rights reserved. Powered by emlog