- 前言
在计算机系统中,cpu的主要任务是执行程序,其核心步骤包括取指、译码和执行。然而,若无程序需要执行,cpu如何处理这一情况呢?有人可能会认为直接停止运行即可,但实际上,决定何时停止以及如何停止需要在复杂的软硬件环境中仔细考虑。
让我们转向linux内核,Linux系统中的CPU被两种程序所占用:一类是进程(或线程),即进程上下文;另一类是中断和异常的处理程序,即中断上下文。
进程负责处理事务,例如读取用户输入并在屏幕上显示。当事务处理完毕,如用户不再输入且无新内容需显示时,进程可以释放CPU,但随时准备重新占用(如用户突然按键)。同样,若系统无中断或异常事件,CPU不会在中断上下文中花费时间。
在Linux内核中,CPU这种无所事事的状态被称为idle状态,而cpuidle框架正是管理这种状态的工具。
注:cpuidle框架系列文章将以ARM64为例平台。由于ARM64发布时间较短,早期版本的内核中没有相关代码,因此我们选择了最新的3.18-rc4版本的内核。
- 功能概述
曾经,Linux内核的cpu idle框架非常简单,简单到driver工程师只需在“includeasm-armarch-xxxsystem.h”中定义一个名为arch_idle的内联函数,并在该函数中调用内核提供的cpu_do_idle接口即可,其余的实现内核会帮我们完成,如下:
static inline void arch_idle(void) { cpu_do_idle(); }
尽管简单,但这包含了idle处理的两个关键点:
1)idle进程
idle进程的存在是为了解决“何时idle”的问题。
我们知道,Linux系统运行的基础是进程调度。当所有进程都不再运行时,即为CPU idle状态。内核通过一个简单的方法来判断这种状态:在init进程(系统的第一个进程)完成初始化任务后,将其转变为idle进程。由于idle进程的优先级最低,当其被调度时,说明系统中其他进程已停止运行,即CPU已idle。最终,idle进程会调用idle指令(如WFI),使CPU进入idle状态。
“ARM WFI和WFE指令”中提到,WFI Wakeup events会将CPU从WFI状态唤醒,这些事件通常是一些中断事件。因此,CPU唤醒后会执行中断处理程序,处理程序中会唤醒某些进程。当处理程序返回时,进行调度,如果没有其他进程需要执行,调度器会恢复idle进程的运行,当然,idle进程不会做任何事情,继续进入idle状态,等待下一次唤醒。
2)WFI
WFI用于解决“如何idle”的问题。
通常情况下,ARM CPU在idle时可以使用WFI指令,将CPU置于等待中断状态。在这种状态下,至少会关闭ARM核的时钟,以节省功耗(具体实现取决于ARM核的设计,可参考“ARM WFI和WFE指令”)。
也许您会觉得,上述过程已经足够好,为什么还要开发cpuidle框架?我的理解是:
- 软件架构
在Linux内核中,cpuidle框架位于“drivers/cpuidle”文件夹中,包含cpuidle核心、cpuidle调控器和cpuidle驱动三个模块,再结合位于内核调度中的cpuidle入口,共同完成CPU的idle管理。软件架构如下图:
1)内核调度模块
位于kernelschedidle.c中,负责实现idle线程的通用入口(cpuidle入口)逻辑,包括idle模式的选择和idle的进入等。
2)cpuidle核心
cpuidle核心负责实现cpuidle框架的整体结构,主要功能包括:
cpuidle核心的代码主要包括:cpuidle.c、driver.c、governor.c、sysfs.c。
3)cpuidle驱动
不同的架构和CPU核会有不同的cpuidle驱动,平台驱动开发者可以在cpuidle核心提供的框架下开发自己的cpuidle驱动。代码主要包括:cpuidle-xxx.c。
4)cpuidle调控器
Linux内核的框架有两种比较固定的抽象模式:
模式2的解释可能有些抽象,但在cpuidle的场景中容易理解:
前面提到,许多CPU提供了多种idle级别(即所谓的“方案”),这些idle级别的主要区别在于“idle时的功耗”和“退出时的延迟”。cpuidle驱动(机制)负责定义这些idle状态(每个状态的功耗和延迟分别是多少),并实现进入和退出的相关操作。最终,cpuidle驱动会将这些信息传递给调控器,由调控器根据具体的应用场景决定选择哪种idle状态(策略)。
内核中的cpuidle调控器都位于governors/目录下。
- 软件流程
在阅读本章之前,请先阅读以下三篇文章:
Linux cpuidle framework(2)_cpuidle核心
Linux cpuidle framework(3)_ARM64通用CPU idle驱动
Linux cpuidle framework(4)_menu调控器
前面提到过,内核会在系统启动完成后,在init进程(或线程)中处理cpuidle相关事务。大致过程如下(内核启动相关的分析将在其他文章中详细介绍):
cpu_startup_entry流程:
具体代码比较简单,不再分析,但有一点需要特别说明:
使用cpuidle框架进入idle状态时,本地irq处于关闭状态,因此从idle返回时,只能继续执行,直到irq被打开,才能执行相应的中断处理程序,这与传统的cpuidle不同。同时,这也间接验证了“Linux cpuidle framework(4)_menu调控器”中提到的,为什么menu调控器在reflect接口中只是简单地设置一个标志。因为reflect是在关闭中断时被调用的,需要尽快返回,以便处理中断事件。