Linux cpuidle framework(1)_概述和软件架构

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是在关中断时被调用的,需要尽快返回,以便处理中断事件。

### `cpuidle_enter_state` 的介绍 `cpuidle_enter_state` 是 Linux 内核中用于实现 CPU 空闲状态管理的关键函数之一,它负责将 CPU 进入指定的空闲状态。该函数通常由 cpuidle 框架调用,用于在系统无任务运行时,将 CPU 置于低功耗状态以节省能耗。 `cpuidle_enter_state` 函数的实现依赖于平台相关的 cpuidle 驱动程序。不同的架构平台可以提供自己的实现,以满足特定硬件的低功耗需求。在调用此函数时,会传入当前的 cpuidle 设备驱动结构体指针,以及目标空闲状态的索引值。函数会根据这些参数调用相应的底层实现,完成 CPU 进入空闲状态的操作。 在典型的调用链中,`cpuidle_enter_state` 会最终调用平台特定的进入函数,例如 `psci_cpu_suspend_enter`,并通过宏展开的方式进入底层实现。例如: ```c CPU_PM_CPU_IDLE_ENTER_PARAM(psci_cpu_suspend_enter, idx, state); ``` 该调用链最终会调用到 `__CPU_PM_CPU_IDLE_ENTER`,其中 `low_level_idle_enter` 是实际负责进入低功耗状态的函数 [^1]。 ### 用途 `cpuidle_enter_state` 的主要用途是: 1. **实现 CPU 的低功耗管理**:当系统没有可运行的任务时,通过调用此函数将 CPU 进入空闲状态,从而降低功耗。 2. **与调度器协作**:当调度器检测到当前 CPU 无任务可运行时,会调用 `cpuidle_enter_state` 进入空闲状态,直到有新的任务需要调度。 3. **支持多种空闲状态**:允许平台定义多个空闲状态(如 C0、C1、C2 等),并根据当前系统负载时间约束选择合适的空闲状态。 4. **支持 tickless 模式**:在进入空闲状态时,可以关闭或暂停本地定时器,避免不必要的中断唤醒。 在 `do_idle()` 函数中,`arch_cpu_idle_enter()` 会调用 `cpuidle_enter_state` 来实现进入空闲状态的操作 [^3]。 ### 典型调用流程 在系统空闲时,调用流程通常如下: 1. 调度器调用 `cpu_idle_loop` 或 `do_idle()`。 2. `do_idle()` 调用 `arch_cpu_idle_enter()`。 3. `arch_cpu_idle_enter()` 调用 `cpuidle_enter_state()`。 4. `cpuidle_enter_state()` 调用平台特定的 enter 函数(如 `psci_cpu_suspend_enter`)。 5. CPU 进入指定的空闲状态,直到中断发生或有新的任务需要调度。 ### 代码结构示例 `cpuidle_enter_state` 函数的原型如下: ```c int cpuidle_enter_state(struct cpuidle_device *dev, struct cpuidle_driver *drv, int index); ``` - `dev`:指向当前 CPU 的 cpuidle 设备。 - `drv`:指向 cpuidle 驱动。 - `index`:要进入的空闲状态索引。 在平台驱动中,每个空闲状态会定义一个 `enter` 回调函数,用于实际执行进入低功耗状态的操作: ```c void (*enter)(struct cpuidle_device *dev, struct cpuidle_driver *drv, int index); ``` 例如,某些平台可能定义如下: ```c static struct cpuidle_state my_cpuidle_states[] = { { .name = "C1", .desc = "ARM WFI", .flags = CPUIDLE_FLAG_TIME_VALID, .exit_latency = 1, .target_residency = 1, .enter = my_enter_c1, }, }; ``` 其中 `my_enter_c1` 是平台特定的进入函数,最终由 `cpuidle_enter_state` 调用。 ### 相关特性 - **支持冻结模式**:在某些系统挂起场景中,`cpuidle_enter_state` 会调用 `enter_freeze` 函数进入冻结状态,此时时间系统被暂停,不能重新启用中断或修改时钟设备 [^4]。 - **与电源管理协同**:`cpuidle_enter_state` 是电源管理(如 suspend、hibernate)的重要组成部分,确保 CPU 在空闲时进入最低功耗状态。 ### 总结 `cpuidle_enter_state` 是 Linux 内核中 CPU 空闲状态管理的核心函数之一,负责将 CPU 进入指定的低功耗状态。它与调度器、电源管理子系统紧密协作,支持多种空闲状态,并允许平台开发者根据硬件特性实现自定义的低功耗策略。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值