CPU空闲时间管理
版权 © 2019 Intel Corporation
作者 Rafael J. Wysocki rafael.j.wysocki@intel.com
CPU空闲时间管理子系统
系统中的每个逻辑CPU(看起来获取和执行指令的实体:如果存在的话,硬件线程或处理器核心)在中断或等效唤醒事件后处于空闲状态时,这意味着除了与其关联的特殊的“空闲”任务之外,没有其他任务可以在其上运行,这为其所属的处理器节省能量提供了机会。可以通过使空闲的逻辑CPU停止从内存中获取指令,并将其所依赖的一些处理器功能单元置于空闲状态,从而减少功耗。
然而,原则上可能存在多种不同的空闲状态可用于这种情况,因此可能需要找到最合适的(从内核的角度)并要求处理器使用(或“进入”)该特定的空闲状态。这就是内核中CPU空闲时间管理子系统(称为CPUIdle)的作用。
CPUIdle的设计是模块化的,并且基于避免代码重复的原则,因此其中原则上不需要依赖硬件或平台设计细节的通用代码与与硬件交互的代码是分开的。它通常分为三类功能单元:负责选择要求处理器进入空闲状态的调度器、将调度器的决策传递给硬件的驱动程序以及为它们提供一个共同框架的核心。
CPU空闲时间调度器
CPU空闲时间(CPUIdle)调度器是在系统中的任何逻辑CPU处于空闲状态时调用的一组策略代码。其作用是选择一个空闲状态,要求处理器进入以节省一些能量。
CPUIdle调度器是通用的,每个调度器都可以在Linux内核可以运行的任何硬件平台上使用。因此,它们操作的数据结构也不能依赖于任何硬件架构或平台设计细节。
调度器本身由一个struct cpuidle_governor对象表示,其中包含四个回调指针(enable、disable、select、reflect)、一个下面描述的rating字段和一个用于标识它的名称(字符串)。
要使调度器可用,需要通过调用cpuidle_register_governor()并将指向它的指针作为参数传递来向CPUIdle核心注册该对象。如果成功,这将导致核心将调度器添加到可用调度器的全局列表中,并且如果它是列表中唯一的调度器(即在此之前列表为空),或者其rating字段的值大于当前使用的调度器的rating字段的值,或者新调度器的名称被作为cpuidle.governor=命令行参数的值传递给内核,那么从那时起将使用新的调度器(一次只能使用一个CPUIdle调度器)。此外,用户空间可以通过sysfs在运行时选择要使用的CPUIdle调度器。
一旦注册,CPUIdle调度器就无法取消注册,因此将它们放入可加载的内核模块中是不切实际的。
CPUIdle调度器与核心之间的接口由四个回调函数组成:
- enable
int (*enable) (struct cpuidle_driver *drv, struct cpuidle_device *dev);
这个回调函数的作用是为处理由参数dev指向的struct cpuidle_device对象表示的(逻辑)CPU做准备。参数drv指向的struct cpuidle_driver对象代表了要与该CPU一起使用的CPUIdle驱动程序(除其他外,它应该包含代表处理给定CPU的处理器可以进入的空闲状态的struct cpuidle_state对象列表)。
如果失败,它应该返回一个负的错误代码,这将导致内核在再次为该CPU调用->enable()调度器回调之前,在相关CPU上运行特定架构的默认空闲CPU代码。 - disable
void (*disable) (struct cpuidle_driver *drv, struct cpuidle_device *dev);
调用此函数使得调度器停止处理由参数dev指向的struct cpuidle_device对象表示的(逻辑)CPU。
期望它撤销上次为目标CPU调用时由->enable()回调所做的任何更改,释放该回调分配的所有内存等。 - select
int (*select) (struct cpuidle_driver *drv, struct cpuidle_device *dev, bool *stop_tick);
调用此函数为持有由参数dev指向的struct cpuidle_device对象表示的(逻辑)CPU选择一个空闲状态。
要考虑的空闲状态列表由参数drv指向的struct cpuidle_driver对象(代表要与手头的CPU一起使用的CPUIdle驱动程序)的states数组中的struct cpuidle_state对象表示。此回调返回的值被解释为该数组的索引(除非它是负的错误代码)。
stop_tick参数用于指示是否停止调度器滴答声,然后再要求处理器进入所选的空闲状态。当它指向的布尔变量(在调用此回调之前设置为true)被清除为false时,将要求处理器在不停止给定CPU上的调度器滴答声的情况下进入所选的空闲状态(但是,如果在该CPU上已经停止了滴答声,那么在要求处理器进入空闲状态之前不会重新启动滴答声)。
此回调是强制性的(即struct cpuidle_governor中的select回调指针在注册调度器时不能为NULL)。 - reflect
void (*reflect) (struct cpuidle_device *dev, int index);
调用此函数允许调度器评估由上次调用->select()回调时所做的空闲状态选择的准确性,并可能利用该结果来改善将来的空闲状态选择的准确性。
此外,CPUIdle调度器在选择空闲状态时需要考虑处理器唤醒延迟的电源管理服务质量(PM QoS)约束。为了获取给定CPU的当前有效PM QoS唤醒延迟约束,CPUIdle调度器需要将CPU的编号传递给cpuidle_governor_latency_req()。然后,调度器的select()回调不能返回exit_latency值大于该函数返回的值的空闲状态的索引。
CPU空闲时间管理驱动程序
CPU空闲时间管理(CPUIdle)驱动程序提供了CPUIdle的其他部分与硬件之间的接口。
首先,CPUIdle驱动程序必须填充包含在表示它的struct cpuidle_driver对象中的struct cpuidle_state对象的states数组。从现在开始,该数组将表示处理器硬件可以被要求进入的可用空闲状态的列表,该列表由该驱动程序处理的所有逻辑CPU共享。
states数组中的条目应按照struct cpuidle_state中target_residency字段的值的升序进行排序(即索引0应对应具有最小target_residency值的空闲状态)。[由于target_residency值预计反映了struct cpuidle_state对象所代表的空闲状态的“深度”,因此这种排序顺序应与空闲状态“深度”的升序排序顺序相同。]
现有CPUIdle调度器用于与空闲状态选择相关的计算的struct cpuidle_state中的三个字段:
- target_residency:在此空闲状态中花费的最短时间,包括进入它所需的时间(可能很长),以节省比在相同时间内停留在较浅的空闲状态中节省的更多能量,单位为微秒。
- exit_latency:处理器请求处理器进入此空闲状态后,CPU开始执行第一条指令所需的最长时间,单位为微秒。
- flags:表示空闲状态属性的标志。目前,调度器仅使用CPUIDLE_FLAG_POLLING标志,如果给定对象不代表真正的空闲状态,而是可以用于避免要求处理器进入任何空闲状态的软件“循环”的接口,则设置该标志。[CPUIdle核心在特殊情况下使用其他标志。]
struct cpuidle_state中的enter回调指针,它不能为NULL,指向要执行的例程,以便要求处理器进入特定的空闲状态:
void (*enter) (struct cpuidle_device *dev, struct cpuidle_driver *drv, int index);
它的前两个参数分别指向运行此回调的逻辑CPU所代表的struct cpuidle_device对象和表示驱动程序本身的struct cpuidle_driver对象,最后一个是驱动程序的states数组中表示要求处理器进入的空闲状态的索引。
struct cpuidle_state中的类似的->enter_s2idle()回调仅用于实现系统范围的挂起到空闲(suspend-to-idle)系统管理功能。它与->enter()的区别在于,它不能在任何时候重新启用中断(甚至是临时的),也不能尝试更改时钟事件设备的状态,而->enter()回调有时可能会这样做。
一旦states数组已经被填充,有效条目的数量必须存储在表示驱动程序的struct cpuidle_driver对象的state_count字段中。此外,如果states数组中的任何条目表示“耦合”空闲状态(即只有在多个相关的逻辑CPU处于空闲状态时才能要求的空闲状态),则struct cpuidle_driver中的safe_state_index字段需要是一个非“耦合”(即只有在一个逻辑CPU处于空闲状态时才能要求的)空闲状态的索引。
此外,如果给定的CPUIdle驱动程序只处理系统中的逻辑CPU子集,那么其struct cpuidle_driver对象中的cpumask字段必须指向将由其处理的CPU的集合(掩码)。
只有在注册了CPUIdle驱动程序之后,才能使用它。如果驱动程序的states数组中没有“耦合”空闲状态条目,可以通过将驱动程序的struct cpuidle_driver对象传递给cpuidle_register_driver()来实现。否则,应该使用cpuidle_register()来实现此目的。
但是,还需要使用cpuidle_register_device()注册由给定CPUIdle驱动程序处理的所有逻辑CPU的struct cpuidle_device对象,这与cpuidle_register()不同,后者不会自动执行。因此,使用cpuidle_register()推荐在所有情况下使用CPUIdle驱动程序注册。
注册struct cpuidle_device对象会创建CPUIdle sysfs接口,并为其所代表的逻辑CPU调用调度器的->enable()回调,因此必须在注册将处理该CPU的驱动程序之后进行。CPUIdle驱动程序和struct cpuidle_device对象在不再需要时可以取消注册,这允许释放与它们关联的一些资源。由于它们之间存在依赖关系,因此必须先取消注册由给定CPUIdle驱动程序处理的所有struct cpuidle_device对象(使用cpuidle_unregister_device()),然后才能调用cpuidle_unregister_driver()取消注册驱动程序。或者,可以调用cpuidle_unregister()来取消注册CPUIdle驱动程序以及由其处理的所有struct cpuidle_device对象。
CPUIdle驱动程序可以响应导致修改可用处理器空闲状态列表的运行时系统配置更改(例如,当系统的电源源从交流电切换到电池或反之时)。在收到此类更改的通知后,CPUIdle驱动程序应调用cpuidle_pause_and_lock()临时关闭CPUIdle,然后对受到影响的所有struct cpuidle_device对象调用cpuidle_disable_device()。接下来,它可以根据系统的新配置更新其states数组,对所有相关的struct cpuidle_device对象调用cpuidle_enable_device(),并调用cpuidle_resume_and_unlock()以允许再次使用CPUIdle。