这个函数核心的逻辑是进入给定 index 的 idle 状态
target_state->enter(dev, drv, index);
, 然后将这个状态返回;但是在实际的实现中,因为涉及到中断、tick、配置等逻辑,所以比较复杂。
下文是对代码中细节和原理的研究:
broadcast
broadcast = !!(target_state->flags & CPUIDLE_FLAG_TIMER_STOP);
这行代码的作用是设置一个广播标志,判断是否需要停止定时器。其中,target_state 是一个指向 cpuidle_state 结构体的指针,该结构体描述了 CPU 的空闲状态,包括状态标识、所需的底层硬件支持、进入和退出该状态所需的操作等信息。
具体来说,target_state->flags 是一个位掩码,用于描述该空闲状态标识的一些特定属性。&运算符将该位掩码与 CPUIDLE_FLAG_TIMER_STOP 逐位进行 AND 运算,结果非零则表示 target_state 的 flags 属性中存在 CPUIDLE_FLAG_TIMER_STOP 标志,即需要停止定时器。
!!
运算符则将结果转换为布尔类型,确保在任何情况下返回的都是 0 或 1 的布尔值,而不是整型数值(双重否定表示肯定)。
因此,这行代码最终会将广播标志设置为 true 或 false,表示需要或不需要停止定时器。在 CPU 进入空闲状态之前,内核会根据广播标志来决定是否向其他 CPU 广播空闲状态,并通知它们停止当前正在运行的定时器。
简单来说,如果 local timer 关闭的话(进入更深层次的 idle 状态),就需要使用 broadcast.
CPUIDLE_FLAG_TLB_FLUSHED
if (target_state->flags & CPUIDLE_FLAG_TLB_FLUSHED) {
leave_mm(dev->cpu);
这段代码的作用是,
在CPU进入指定的空闲状态之前检查该状态是否需要刷新TLB
,并在必要时执行离开当前进程的操作。(从实测的数据来看,基本上没有刷新 TLB 的操作,这段逻辑很少被执行)
其中,target_state 是一个指向 cpuidle_state 结构体的指针,描述了要进入的空闲状态,包括状态标识、所需的底层硬件支持和进入该状态所需的操作等信息。flags 字段表示该状态的一些特殊属性,如 CPUIDLE_FLAG_TLB_FLUSHED,表示在进入该状态前需要刷新 TLB(Translation Lookaside Buffer)。
如果检测到目标状态需要刷新 TLB,则调用
leave_mm(dev->cpu)
函数执行离开当前进程的操作。该函数的作用是在该 CPU 上的所有进程中暂停当前进程,并切换到空闲进程,以便操作系统在进入空闲状态之前刷新 TLB 高速缓存。
总之,这段代码的作用是确保在进入特定的空闲状态之前清除 TLB 以避免任何不必要的冲突,同时保证进程能够正确地切换。
sched_idle_set_state
sched_idle_set_state(target_state);
void sched_idle_set_state(struct cpuidle_state *idle_state)
idle_set_state(this_rq(), idle_state);
这段代码的作用是将当前 CPU 切换到指定的空闲状态,也就是进入一种较低功耗的状态以进行省电。注意到在我们的 cpuidle_enter_state 流程中,这个 sched_idle_set_state 函数被两次调用:
sched_idle_set_state(target_state);
sched_idle_set_state(NULL);
sched_idle_set_state
是一个内核函数,用于设置当前 CPU 的空闲状态,并让 CPU 进入相应的空闲状态。target_state 是一个指向 cpuidle_state 结构体的指针,描述了要进入的空闲状态,包括状态标识、所需的底层硬件支持和进入该状态所需的操作等信息。
this_rq()
表示
当前 CPU 所在的 CPU 运行队列
(runqueue),它的返回值是一个指向
struct rq
结构体的指针,该结构体描述了 CPU 调度器的运作情况和统计信息。
在调用该函数之前,内核通常会执行一些准备工作,如停止定时器、暂停当前进程、刷新 TLB 等。接着,调用该函数将当前 CPU 切换到目标状态,并执行目标状态所需的操作,如关闭某些设备、降低 CPU 主频等。
整个过程是由内核负责管理和控制的,程序员无法直接控制。当系统需要重新唤醒 CPU 时,内核会根据 CPU 的中断或事件触发来驱动 CPU 从空闲状态中返回,并恢复相关的设备和资源。
rcu_idle_enter
if (!(target_state->flags & CPUIDLE_FLAG_RCU_IDLE))
rcu_idle_enter();
这段代码用于判断当前进入空闲状态的目标状态是否需要进行
RCU(Read-Copy-Update)空闲处理
,如果需要,则调用
rcu_idle_enter
函数进行 RCU 空闲处理。
在Linux内核中,RCU是一种无锁机制,用于在多个进程间共享数据。当一个进程需要修改共享数据时,该进程会先创建出一个新的副本,对其进行修改,然后将新副本加入到RCU保护区中。此时,其他进程仍然可以访问旧的副本,不会受到影响。当所有对旧副本的访问都结束后,RCU保护区才会被清空,新副本才会变成有效的数据。
在进入空闲状态时,如果当前CPU正在进行RCU更新操作,就需要进行RCU空闲处理,即等待所有正在使用旧副本的进程完成访问后,再进行新副本的更新。这样一来,就可以避免数据的冲突和不一致性。
rcu_idle_enter函数用于启动RCU空闲处理,并进入RCU空闲状态。该函数会将当前CPU所在的调度器的状态设置为RCU空闲状态,然后等待所有正在使用旧副本的进程访问完毕,直到RCU保护区被清空。在此期间,该CPU不会执行任何其他任务,以避免对正在访问旧副本的进程产生干扰。
void rcu_idle_enter(void)
lockdep_assert_irqs_disabled();
rcu_eqs_enter(false);
rcu_idle_enter
函数是用于启动RCU空闲处理并进入RCU空闲状态的函数。
其中,
lockdep_assert_irqs_disabled
函数用于断言
当前中断已经被禁止
,在RCU空闲处理期间不会被重新打开。该函数会在执行时检查当前是否处于内核锁定状态,并通过锁依赖机制确保锁的正确性。如果当前存在锁冲突,则会抛出一个警告信息。
rcu_eqs_enter
函数则用于进入RCU空闲状态并等待所有正在使用旧副本的进程访问完毕。其中,参数false表示
不需要检查是否处于内核软件调试状态
(KDB或KGDB)。在该函数中,会调用rcu_prepare_for_idle函数进行RCU更新准备工作,并将当前CPU所在的调度器状态设置为RCU空闲状态。然后,该函数会启动一个RCU处理线程,在其中等待所有正在使用旧副本的进程访问完毕并结束。❓❓ 待处理线程结束之后,该函数会将当前CPU所在调度器状态设置为正常运行状态,并返回。
enter
entered_state = target_state->enter(dev, drv, index);
进入 state, 待深入研究。❌❌❌
rcu_idle_exit
if (!(target_state->flags & CPUIDLE_FLAG_RCU_IDLE))
rcu_idle_exit();
这段代码用于判断目标状态是否为RCU空闲状态,并在不是RCU空闲状态时退出RCU空闲处理。
如果目标状态的flags字段中包含CPUIDLE_FLAG_RCU_IDLE标志,说明此时需要进入RCU空闲状态,即需要等待所有正在访问旧副本的进程结束后再进行新副本的更新。此时,不需要退出RCU空闲处理,代码直接跳过。
但如果目标状态的flags字段中不包含CPUIDLE_FLAG_RCU_IDLE标志,则代表当前并不需要进行RCU空闲处理。此时,需要通过rcu_idle_exit函数退出RCU空闲处理。该函数会将当前CPU所在的调度器状态设置为正常运行状态,并唤醒所有等待RCU更新的进程(如等待RCU更新的进程队列等)。这样一来,RCU更新操作就可以继续进行,而不会被阻塞在RCU空闲状态中。
需要注意的是,只有在完成了RCU更新操作之后,才能调用rcu_idle_exit函数退出RCU空闲状态。否则,会引起数据访问冲突和不一致性,导致系统出现异常。因此,在使用该函数时,需要保证系统支持RCU机制,并遵循相关的使用原则和规范。
critical_timings
stop_critical_timings();
start_critical_timings();
stop_critical_timings
用于停止内核关键代码段的性能计数和统计,以便进行性能分析和优化。该函数通常在内核调试、性能测试等场景下使用,以了解内核关键代码段的耗时和执行情况,从而进行优化。
该函数会将内核当前的性能计数器(如TSC、APIC计数器等)停止,并记录下停止时的值,然后将所有的性能计数器状态都设置为暂停。这样一来,在停止之前和停止之后,所有关键代码段的执行时间就可以计算出来了。
start_critical_timings
的作用正好相反,就不过多赘述了。
idle polling
idle polling 是一个空闲轮询机制。
我们来看代码:
#ifdef CONFIG_GENERIC_IDLE_POLL_SETUP
static int __init cpu_idle_poll_setup(char *__unused)
cpu_idle_force_poll = 1;
return 1;
__setup("nohlt", cpu_idle_poll_setup);
static int __init cpu_idle_nopoll_setup(char *__unused)
cpu_idle_force_poll = 0;
return 1;
__setup("hlt", cpu_idle_nopoll_setup);
#endif
cpu_idle_poll_ctrl
可以作为一个调度器选项,对空闲轮询机制进行控制。其具体做法是:
在传统的CPU调度方案中,当CPU处于空闲状态时,调度器通常会挂起CPU或让CPU执行一些简单的指令,以避免CPU浪费过多的时间。而在空闲轮询机制下,调度器会在CPU空闲时,立即检查系统中是否有可运行的进程,如果有,则立即唤醒该进程并将CPU分配给它。
具体的 polling 实现可能会分成很多种,如 busy-waiting, 表示如果没有可以运行的进程的话,则调度器会继续等待下一次轮询;如 sleep, 当 CPU 空闲时,会将整个 CPU 设置为休眠状态,以节能。
DEFINE_PER_CPU
DEFINE_PER_CPU
是一个宏,用于定义一种特殊的变量类型,称为 "per-cpu 变量"。这种变量在 Linux 内核中广泛使用,用于跨多个 CPU 核心共享数据时保证数据的一致性。
使用
DEFINE_PER_CPU
宏定义的变量,会在每个 CPU 核心上都创建一个独立的副本,并通过内核提供的函数接口进行同步和访问。这样,在多核系统中,不同 CPU 核心上的代码可以同时访问该变量,而不会出现数据竞争或者锁竞争等问题。
具体来说,
DEFINE_PER_CPU
宏的语法如下:
DEFINE_PER_CPU(type, var);
其中,
type
指定了变量的类型,
var
是变量名。例如:
DEFINE_PER_CPU(int, my_var);
上述代码定义了一个名为
my_var
的 per-cpu 整型变量。在汇编层面,编译器会自动生成对应的代码,以便为每个 CPU 核心上创建一个独立的
int
类型的变量,并对其进行初始化和管理。在 C 代码层面,程序员可以通过
get_cpu_var()
和
put_cpu_var()
等函数来访问和修改 per-cpu 变量的值,例如:
int val = get_cpu_var(my_var);
put_cpu_var(my_var, val+1);
需要注意的是,per-cpu 变量仅适用于每个 CPU 核心独立使用的数据,并不适用于需要全局同步的数据结构。此外,需要注意内存分配和访问的开销,以避免影响系统的性能。
Idle Data Struct
Abstract
本章节主要针对性分析 Idle 中的数据结构。在 kernel 中的 cpuidle framework 主体包括三个模块:cpuidle core, cpudile governors 和 cpuidle drivers.
cpuidle core
: 负责整体框架,对接 sched 模块,调度器发现没有任务在执行的时候,就切换到 idle 进程,通知就会给 cpuidle core; cpuidle core 负责向 cpuidle driver/governors 模块提供统一的注册和管理接口,向用户空间提供 governor 选择的接口。
cpuidle governors
: 在该模块进行 cpuidle 的选择;
cpuidle driver
: 负责具体 idle 机制的实现。
总的来说,这几个的关系大致如下图所示:
cpuidle core
cpuidle core 是 cpuidle framework 的核心模块,负责抽象出 cpuidle device、cpuidle driver 和 cpuidle governor三个实体。
其负责的功能有:
-
(如上阐述)向底层的 cpuidle driver 模块提供 cpudile devic e和 cpuidle driver 的注册/注销接口;
-
(如上阐述)向 cpuidle governors 提供 governor 的注册接口;
-
提供全局的 puidle 机制的开、关、暂停、恢复等功能;
-
(如上阐述)向用户空间程序提供 governor 选择的接口;
-
向 kernel sched 中的 cpuidle entry 提供 cpuidle 的级别选择、进入等接口,以方便调用。
cpuidle device
在现在的 SMP 系统中,每个 cpu core 都会对应一个 cpuidle device, 内核通过
strcut cpuidle_device
抽象 cpuidle device.
cpuidle driver
cpuidle driver 是一个 “driver", 其驱动的对象是 cpuidle device, 也就是 CPU;注意到在 SMP 系统中,有多个 CPU,也就意味着有多个 cpuidle device; 在实现 idle 的时候,如果这些 cpuidle device 的功能、参数相同,则可以使用一个 cpuidle driver 驱动,具体而言,kernel 中的宏
CONFIG_CPU_IDLE_MULTIPLE_DRIVERS
可以用来使能是否使用多个 cpu driver. 在实际的应用场景中,这个开关是被使能的。
代码可见
https://github.com/torvalds/linux/blob/master/include/linux/cpuidle.h
open in new window
struct cpuidle_driver {
const char *name;
struct module *owner;
unsigned int bctimer:1;
struct cpuidle_state states[CPUIDLE_STATE_MAX];
int state_count;
int safe_state_index;
struct cpumask *cpumask;
const char *governor;
bctimer:1
: 一个标志,用于指示在 cpuidle driver 注册和注销时,是否需要设置一个 broadcast timer;
cpumask
: 一个 struct cpumask 结构的 bit map 指针,用于说明该 driver 支持哪些 cpu core;
states
,
state_count
:该 driver 所支持的 idle state 及其个数。cpuidle driver 的主要任务就是定义所支持的 cpuidle state; 需要注意注释中所提到的,
states
应该按照功耗大小降序排列。
cpuidle driver 的注册,我们在这边主演研究多个 cpu driver 的场景,主要是分为几个:
💄
__cpuidle_get_cpu_driver
:
static struct cpuidle_driver *__cpuidle_get_cpu_driver(int cpu)
return per_cpu(cpuidle_drivers, cpu);
💄
__cpuidle_set_driver
static inline int __cpuidle_set_driver(struct cpuidle_driver *drv)
int cpu;
for_each_cpu(cpu, drv->cpumask) {
struct cpuidle_driver *old_drv;
old_drv = __cpuidle_get_cpu_driver(cpu);
if (old_drv && (old_drv != drv))
return -EBUSY;
for_each_cpu(cpu, drv->cpumask)
per_cpu(cpuidle_drivers, cpu) = drv;
return 0;
-
对于每个 cpumask 的 cpu, 设置 drv; 条件是原有的 cpu 无 drv 或者是有 drv 并且和需要设置的不相等(不相等的话直接 return 不进行设置)
-
设置的方法如高亮行所示
Something Else
tickless
In the context of operating systems, "tickless" refers to a power management feature that allows the system to reduce power consumption by dynamically adjusting the frequency of timer interrupts.
About "tick" interrupt:
Traditionally, operating systems use a periodic timer interrupt, often called the "tick," to keep track of time and to perform various tasks such as updating the system clock, scheduling tasks, and handling interrupts. These timer interrupts are generated at a fixed frequency, regardless of whether there is any work to be done, which can consume a significant amount of power.
WFI
WFI 是英文 Wait for Interrupt 的缩写,意为等待中断。WFI 指令是 ARM 处理器提供的一种指令,用于将处理器置于等待状态,直到下一个中断事件发生后才会继续执行。
Arm64 提供了 WFI 指令,使得 CPU 一旦执行该指令就进入低功耗状态,该状态会关闭 CPU 时钟,从而降低动态功耗。
如果我们想实现一个简单的 idle 的话,需要以下的流程即可