CPU 空闲时间管理【ChatGPT】

发布时间 2023-12-12 11:28:51作者: 摩斯电码

CPU 空闲时间管理

版权 © 2018 Intel Corporation

作者 Rafael J. Wysocki rafael.j.wysocki@intel.com

概念

现代处理器通常能够进入一种状态,其中程序的执行被暂停,属于它的指令不会从内存中获取或执行。这些状态是处理器的空闲状态。

由于处理器硬件的一部分在空闲状态下未被使用,进入这些状态通常可以减少处理器的功耗,从而节省能源。

CPU 空闲时间管理是一个关注利用处理器的空闲状态来实现节能的能效特性。

逻辑 CPU

CPU 空闲时间管理在 CPU 调度器看到的 CPU 上运行(即内核负责在系统中分配计算工作的部分)。在它的视图中,CPU 是逻辑单元。也就是说,它们不一定是单独的物理实体,可能只是作为单个单核处理器出现在软件中。换句话说,CPU 是一个实体,它看起来从内存中获取属于一个序列(程序)的指令并执行它们,但在物理上不一定是这样工作的。一般来说,可以考虑三种不同的情况。

首先,如果整个处理器一次只能执行一个指令序列(一个程序),那么它就是一个 CPU。在这种情况下,如果硬件被要求进入空闲状态,那就适用于整个处理器。

其次,如果处理器是多核的,每个核至少能同时执行一个程序。这些核不一定完全独立(例如,它们可能共享缓存),但它们大部分时间在物理上是相互并行工作的,因此如果它们中的每一个只执行一个程序,那么这些程序大部分时间是相互独立地同时运行的。在这种情况下,整个核都是 CPU,如果硬件被要求进入空闲状态,那就适用于最初请求进入空闲状态的核,但也可能适用于包含该核的更大单元(例如“包”或“集群”),实际上,它可能适用于包含该核的整个更大单元的层次结构。换句话说,如果除了一个核之外的较大单元中的所有核都已经在“核级”处于空闲状态,并且剩下的核要求处理器进入空闲状态,那可能会触发将整个较大单元置于空闲状态,这也会影响该单元中的其他核。

最后,多核处理器中的每个核可能能够在同一时间框架内执行多个程序(即,每个核可能能够从内存中的多个位置获取指令并在同一时间框架内执行它们,但不一定完全并行)。在这种情况下,核向软件呈现为“捆绑”,每个捆绑由多个单独的单核“处理器”组成,称为硬件线程(或者在英特尔硬件上特别称为超线程),每个线程可以执行一个指令序列。然后,从 CPU 空闲时间管理的角度来看,硬件线程是 CPU,如果处理器被其中一个线程要求进入空闲状态,那么请求进入空闲状态的硬件线程(或 CPU)会停止,但除非同一核中的所有其他硬件线程也要求处理器进入空闲状态,否则不会发生其他任何事情。在这种情况下,核可以被单独置于空闲状态,或者包含它的更大单元可以作为整体被置于空闲状态(如果较大单元中的其他核已经处于空闲状态)。

空闲 CPU

在 Linux 内核中,当除了特殊的“空闲”任务之外,没有任务在 CPU 上运行时,逻辑 CPU 简称为“空闲 CPU”。

任务是 CPU 调度器对工作的表示。每个任务由一系列要执行的指令或代码、在运行该代码时需要操作的数据以及每次由 CPU 运行该任务的代码时需要加载到处理器中的一些上下文信息组成。CPU 调度器通过将任务分配给系统中存在的 CPU 来分配工作。

任务可以处于各种状态。特别是,如果没有特定条件阻止它们的代码被 CPU 运行,它们就是可运行的(例如,它们不在等待任何事件发生或类似情况)。当任务变为可运行时,CPU 调度器将其分配给一个可用的 CPU 运行,如果没有更多的可运行任务分配给它,CPU 将加载给定任务的上下文并运行其代码(可能是从到目前为止执行的最后一条指令的后续指令开始,可能是由另一个 CPU 执行的)。[如果有多个可运行任务同时分配给一个 CPU,它们将受到优先级和时间共享的影响,以便允许它们随着时间的推移取得一些进展。]

特殊的“空闲”任务如果没有其他可运行任务分配给给定的 CPU,那么它就变为可运行,然后 CPU 被视为空闲。换句话说,在 Linux 中,空闲 CPU 运行称为空闲循环的“空闲”任务的代码。如果支持的话,该代码可能会导致处理器进入其中的一个空闲状态以节省能源,但如果处理器不支持任何空闲状态,或者在下一次唤醒事件之前没有足够的时间进入空闲状态,或者有严格的延迟约束阻止使用任何可用的空闲状态,CPU 将简单地在循环中执行更多或更少无用的指令,直到被分配一个新的任务来运行。

空闲循环

空闲循环代码在每次迭代中执行两个主要步骤。首先,它调用一个称为“调度器”的代码模块,该模块属于 CPU 空闲时间管理子系统 CPUIdle,以选择一个空闲状态,以便请求处理器硬件进入该状态。其次,它调用 CPUIdle 子系统的另一个代码模块,称为驱动程序,以实际要求处理器硬件进入调度器选择的空闲状态。

调度器的作用是找到最适合当前条件的空闲状态。为此,可以通过逻辑 CPU 要求进入的空闲状态以与平台或处理器架构无关的抽象方式表示,并组织在一个一维(线性)数组中。该数组必须在初始化时由与 Linux 内核运行的平台匹配的 CPUIdle 驱动程序准备和提供。这允许 CPUIdle 调度器独立于底层硬件,并与 Linux 内核可以运行的任何平台一起工作。

该数组中的每个空闲状态都由调度器考虑的两个参数来描述,即目标驻留时间和(最坏情况下的)退出延迟。目标驻留时间是硬件必须在给定状态中花费的最短时间,包括进入该状态所需的时间(可能很长),以便节省比进入浅层空闲状态节省的能源更多的能源。[空闲状态的“深度”大致对应于处理器在该状态下的功耗。] 退出延迟则是 CPU 要求处理器硬件进入空闲状态后开始执行第一条指令所需的最长时间。请注意,一般来说,退出延迟还必须包括在处理器进入该状态时唤醒发生时所需的时间,并且必须以有序的方式完全进入该状态才能退出。

有两种信息可以影响调度器的决策。首先,调度器知道最接近的定时器事件的时间。这个时间是已知的,因为内核会编程定时器,并且它确切地知道它们何时会触发,它是给定 CPU 依赖的硬件在空闲状态下可以花费的最长时间,包括进入和退出它所需的时间。然而,CPU 可能会在任何时间(特别是在最接近的定时器触发之前)被非定时器事件唤醒,通常不知道何时会发生。调度器只能看到 CPU 在被唤醒后实际空闲的时间(从现在开始将称为空闲持续时间),并且它可以使用该信息以某种方式与最接近的定时器之间的时间估计未来的空闲持续时间。调度器如何使用该信息取决于它所实现的算法,这是 CPUIdle 子系统中存在多个调度器的主要原因。

有四种 CPUIdle 调度器可用,分别是 menu、TEO、ladder 和 haltpoll。默认使用哪种取决于内核的配置,特别是取决于调度器滴答是否可以被空闲循环停止。可用的调度器可以从 available_governors 中读取,并且可以在运行时更改调度器。内核当前使用的 CPUIdle 调度器的名称可以从 sysfs 中的 /sys/devices/system/cpu/cpuidle/ 下的 current_governor_ro 或 current_governor 文件中读取。

另一方面,使用的 CPUIdle 驱动程序通常取决于内核运行的平台,但是有些平台可能有多个匹配的驱动程序。例如,有两个驱动程序可以与大多数英特尔平台一起工作,分别是 intel_idle 和 acpi_idle,一个具有硬编码的空闲状态信息,另一个能够从系统的 ACPI 表中读取该信息。然而,即使在这些情况下,系统初始化时选择的驱动程序也不能在以后被替换,因此必须早早地做出使用哪一个的决定(如果由于某种原因禁用了 intel_idle 或者它无法识别处理器,则在英特尔平台上将使用 acpi_idle 驱动程序)。内核当前使用的 CPUIdle 驱动程序的名称可以从 sysfs 中的 /sys/devices/system/cpu/cpuidle/ 下的 current_driver 文件中读取。

空闲 CPU 和调度器滴答

调度器滴答是一个定时器,定期触发,以实现 CPU 调度器的时间共享策略。当然,如果有多个可运行的任务同时分配给一个 CPU,让它们在给定的时间段内合理地取得进展的唯一方法是让它们共享可用的 CPU 时间。换句话说,粗略地说,每个任务被分配一个 CPU 时间片来运行其代码,受调度类、优先级等的影响,当时间片用完时,CPU 应该切换到运行(代码的)另一个任务。当前运行的任务可能不愿意自愿放弃 CPU,但调度器滴答就是为了让切换发生而存在的。这不是滴答的唯一作用,但却是使用它的主要原因。

调度器滴答从 CPU 空闲时间管理的角度来看是有问题的,因为它定期触发,而且相对频繁(取决于内核配置,滴答周期的长度在 1 毫秒到 10 毫秒之间)。因此,如果允许滴答在空闲的 CPU 上触发,那么让它们请求硬件进入超过滴答周期长度的空闲状态就没有意义。此外,在这种情况下,任何 CPU 的空闲持续时间都不会超过滴答周期的长度,并且由于滴答在空闲 CPU 上的唤醒导致进入和退出空闲状态所使用的能量将被浪费。

幸运的是,实际上并不需要允许滴答在空闲 CPU 上触发,因为(根据定义)它们除了特殊的“空闲”任务外没有其他任务要运行。换句话说,从 CPU 调度器的角度来看,它们上的 CPU 时间唯一的使用者是空闲循环。由于空闲 CPU 的时间不需要在多个可运行的任务之间共享,如果给定的 CPU 是空闲的,那么使用滴答的主要原因就消失了。因此,原则上可以完全停止空闲 CPU 上的调度器滴答,尽管这可能并不总是值得付出努力。

是否停止空闲循环中的调度器滴答取决于调度器的期望。首先,如果在滴答范围内有另一个(非滴答)定时器即将触发,显然停止滴答将是一种浪费时间,尽管在这种情况下,定时器硬件可能不需要重新编程。其次,如果调度器期望在滴答范围内有一个非定时器唤醒,那么停止滴答是不必要的,甚至可能是有害的。换句话说,在这种情况下,调度器将选择一个目标驻留时间在预期唤醒之前的空闲状态,因此该状态将相对较浅。调度器实际上不能选择一个深度的空闲状态,因为那将与它自己对短期内唤醒的期望相矛盾。现在,如果唤醒确实很快发生,停止滴答将是一种浪费时间,在这种情况下,定时器硬件需要重新编程,这是昂贵的。另一方面,如果停止滴答而唤醒不会很快发生,硬件可能会在调度器选择的浅空闲状态中花费无限的时间,这将是一种能量的浪费。因此,如果调度器期望在滴答范围内有任何类型的唤醒,最好允许滴答触发。否则,调度器将选择一个相对较深的空闲状态,因此应该停止滴答,以免它过早地唤醒 CPU。

无论如何,调度器知道它期望什么,是否停止调度器滴答的决定属于它。但是,如果滴答已经被停止(在空闲循环的先前迭代中的一个),最好将其保持原样,调度器需要考虑到这一点。

内核可以配置为完全禁止在空闲循环中停止调度器滴答。可以通过构建时的配置(取消设置 CONFIG_NO_HZ_IDLE 配置选项)或通过在命令行中传递 nohz=off 来完成。在这两种情况下,由于禁止了停止调度器滴答,空闲循环代码简单地忽略了调度器对它的决定,滴答也永远不会停止。

运行允许在空闲 CPU 上停止调度器滴答的内核的系统被称为无滴答系统,它们通常被认为比运行无法停止滴答的内核的系统更节能。如果给定的系统是无滴答的,默认情况下将使用 menu 调度器,如果不是无滴答的,则在其上将使用默认的 CPUIdle 调度器。

菜单调度器

菜单调度器是无滴答系统的默认 CPUIdle 调度器。它非常复杂,但其设计的基本原则是简单明了的。即,当被调用以为 CPU 选择一个空闲状态(即 CPU 将要求处理器硬件进入的空闲状态)时,它试图预测空闲持续时间,并使用预测值进行空闲状态选择。

它首先获取最接近的定时器事件的时间,假设调度器滴答将被停止。这个时间,在接下来的内容中称为睡眠长度,是下一次 CPU 唤醒前的时间的上限。它用于确定睡眠长度范围,而睡眠长度范围又需要得到睡眠长度校正因子。

菜单调度器维护两个睡眠长度校正因子数组。其中一个用于先前在给定 CPU 上运行的任务正在等待某些 I/O 操作完成时,另一个用于不是这种情况。每个数组包含几个校正因子值,这些值对应于不同的睡眠长度范围,这些范围的组织方式使得数组中表示的每个范围大约比前一个范围宽 10 倍。

在选择 CPU 的空闲状态之前,给定睡眠长度范围(用于选择 CPU 的空闲状态)的校正因子将在 CPU 被唤醒后进行更新,睡眠长度与观察到的空闲持续时间越接近,校正因子就越接近 1(它必须在 0 和 1 之间)。睡眠长度乘以它所属的范围的校正因子得到预测的空闲持续时间的第一个近似值。

接下来,调度器使用一个简单的模式识别算法来改进其空闲持续时间的预测。即,它保存最近观察到的 8 个空闲持续时间值,并在下次预测空闲持续时间时计算它们的平均值和方差。如果方差很小(小于 400 平方毫秒)或者相对于平均值来说很小(平均值大于标准差的 6 倍),则平均值被视为“典型间隔”值。否则,被丢弃最长的保存的观察到的空闲持续时间值,并对剩下的值重复计算。同样,如果它们的方差很小(在上述意义上),则平均值被视为“典型间隔”值,依此类推,直到确定了“典型间隔”,或者被丢弃的数据点太多,这种情况下,“典型间隔”被假定等于“无穷大”(最大无符号整数值)。以这种方式计算的“典型间隔”与睡眠长度乘以校正因子相比较,取两者中的最小值作为预测的空闲持续时间。

然后,调度器计算额外的延迟限制,以帮助“交互式”工作负载。它利用这样一个观察:如果所选空闲状态的退出延迟与预测的空闲持续时间相当,那么在该状态中度过的总时间可能会非常短,通过进入该状态和退出它所节省的能量量可能相对较小,因此最好避免与进入该状态和退出它相关的开销。因此,选择一个较浅的状态可能是一个更好的选择。额外延迟限制的第一个近似值是预测的空闲持续时间本身,此外,它还被除以取决于先前在给定 CPU 上运行的任务的数量,现在它们正在等待 I/O 操作完成的值。这个除法的结果与来自功耗管理服务质量(PM QoS)框架的延迟限制进行比较,取两者中的最小值作为空闲状态的退出延迟的限制。

现在,调度器准备遍历空闲状态列表并选择其中一个。为此,它将每个状态的目标驻留时间与预测的空闲持续时间进行比较,并将其退出延迟与计算出的延迟限制进行比较。它选择目标驻留时间最接近但仍低于预测的空闲持续时间,并且退出延迟不超过限制的状态。

在最后一步,如果它没有决定停止调度器滴答,调度器可能仍然需要改进空闲状态的选择。如果它预测的空闲持续时间小于滴答周期,并且滴答在空闲循环的先前迭代中尚未被停止(在这种情况下),则在先前的计算中使用的睡眠长度可能不反映最接近的定时器事件的实际时间,如果它确实大于该时间,调度器可能需要选择一个具有合适目标驻留时间的较浅状态。

定时器事件导向(TEO)调度器

定时器事件导向(TEO)调度器是无滴答系统的另一种 CPUIdle 调度器。它遵循与菜单调度器相同的基本策略:它总是试图找到适合给定条件的最深的空闲状态。然而,它对这个问题采用了不同的方法。

这个调度器的想法是基于这样的观察:在许多系统上,定时器事件的频率比任何其他中断频率高两个或更多数量级,因此它们很可能是从空闲状态唤醒 CPU 的最重要原因。此外,关于(相对较近的)过去发生的事情的信息可以用来估计 CPU 的即将到来的空闲期间是否很可能比最接近的定时器事件的时间短,如果不是,那么选择哪个较浅的空闲状态而不是选择最深的空闲状态。

当然,非定时器唤醒源在某些用例中更重要,这可以通过考虑 CPU 最近的几个空闲时间间隔来实现。然而,即使在这种情况下,也不需要考虑大于睡眠长度的空闲持续时间值,因为最接近的定时器最终会唤醒 CPU,除非它被提前唤醒。

因此,这个调度器估计 CPU 的预期空闲持续时间是否很可能明显短于睡眠长度,并相应地为其选择一个空闲状态。

这个调度器进行的计算是基于使用与 CPUIdle 驱动程序提供的 CPU 空闲状态的目标驻留时间参数值按升序排列的边界对齐的箱。也就是说,第一个箱从 0 开始,但不包括第二个空闲状态(空闲状态 1)的目标驻留时间,第二个箱从空闲状态 1 的目标驻留时间开始,但不包括空闲状态 2 的目标驻留时间,第三个箱从空闲状态 2 的目标驻留时间开始,但不包括空闲状态 3 的目标驻留时间,依此类推。最后一个箱从驱动程序提供的最深的空闲状态的目标驻留时间开始,一直到无穷大。

与每个箱相关联的两个度量称为“命中”和“拦截”。它们在每次在给定 CPU 上选择空闲状态之前根据上次发生的情况进行更新。

“命中”度量反映了睡眠长度和 CPU 唤醒后测得的空闲持续时间落入同一个箱中的情况的相对频率(即,相对于睡眠长度,CPU 似乎“准时”唤醒)。反过来,“拦截”度量反映了测得的空闲持续时间比睡眠长度短得多,以至于它所属的箱对应于比睡眠长度所属的箱更浅的空闲状态(这些情况在下文中称为“拦截”)的相对频率。

除了上述描述的度量之外,调度器还为每个箱计算最近的拦截次数(即,在给定 CPU 的最近 NR_RECENT 次调用中发生的拦截次数)。

为了选择CPU的空闲状态,管理器采取以下步骤(除了必须考虑的可能的延迟约束):

  1. 找到最深的 CPU 空闲状态,其目标驻留时间不超过当前的睡眠长度(候选空闲状态),并计算以下 3 个总和:

    • 候选状态和所有更深空闲状态的“命中”和“拦截”指标的总和(表示 CPU 空闲时间足够长,以避免在睡眠长度等于当前长度时被拦截的情况)。
    • 所有比候选状态浅的空闲状态的“拦截”指标的总和(表示 CPU 空闲时间不足以避免在睡眠长度等于当前长度时被拦截的情况)。
    • 所有比候选状态浅的空闲状态的最近拦截次数的总和。
  2. 如果第二个总和大于第一个总和,或者第三个总和大于 NR_RECENT 的一半,CPU 可能会提前唤醒,因此需要寻找替代的空闲状态来选择。

    • 按降序遍历比候选状态浅的空闲状态。
    • 对于每个空闲状态,计算所有介于该状态和候选状态之间(包括前者但不包括后者)的空闲状态的“拦截”指标总和和最近拦截次数的总和。
    • 如果需要考虑的每个总和(因为与之相关的检查表明 CPU 可能会提前唤醒)都大于步骤 1 中计算的相应总和的一半(这意味着状态的目标驻留时间在超过一半的相关情况下没有超过空闲持续时间),则选择给定的空闲状态而不是候选状态。
  3. 默认情况下选择候选状态。

利用感知机制:

利用感知机制的想法是,CPU 有两种不同的情况,应该采用两种不同的空闲状态选择方法 - 被利用和未被利用。

在这种情况下,“被利用”意味着 CPU 的平均运行队列利用率高于某个阈值。

当 CPU 在进入空闲状态时被利用时,很可能会很快被唤醒进行更多工作,因此应选择较浅的空闲状态,以最小化延迟并最大化性能。当 CPU 未被利用时,应优先采用通常基于指标的方法来选择最深的可用空闲状态,以实现节能。

为了实现这一点,管理器使用了一个利用阈值。该阈值是根据每个 CPU 的容量的百分比通过位移容量值计算得出的。根据测试,位移值为 6(约 1.56%)似乎能够获得最佳结果。

在选择下一个空闲状态之前,管理器比较当前 CPU 利用率与预先计算的利用阈值。如果低于阈值,则默认采用 TEO 指标机制。如果高于阈值,则将选择最接近的较浅空闲状态,只要不是轮询状态。

空闲状态的表示

对于CPU空闲时间管理目的,处理器支持的所有物理空闲状态必须表示为一个结构体cpuidle_state对象的一维数组,每个对象允许单独的(逻辑)CPU请求处理器硬件进入具有特定属性的空闲状态。如果处理器中存在单元的层次结构,一个结构体cpuidle_state对象可以涵盖层次结构不同级别的单元支持的一组空闲状态。在这种情况下,其目标驻留时间和退出延迟参数必须反映出最深层级的空闲状态的属性(即包含所有其他单元的单元的空闲状态)。

例如,考虑一个具有两个核心的处理器,这两个核心位于一个称为“模块”的更大单元中,假设通过一个核心在“核心”级别请求硬件进入特定的空闲状态(比如“X”),如果另一个核心已经处于空闲状态“X”,那么模块将尝试进入自己的特定空闲状态(比如“MX”)。换句话说,在“核心”级别请求空闲状态“X”将使硬件有权深入到“模块”级别的空闲状态“MX”,但不能保证一定会发生(请求空闲状态“X”的核心可能最终会自行进入该状态)。因此,代表空闲状态“X”的结构体cpuidle_state对象的目标驻留时间必须反映出模块空闲状态“MX”中的最短空闲时间(包括进入该状态所需的时间),因为这是CPU需要空闲以节省能量的最短时间,假设硬件进入该状态。类似地,该对象的退出延迟参数必须涵盖模块空闲状态“MX”的退出时间(通常也包括其进入时间),因为这是唤醒信号和CPU开始执行第一个新指令之间的最大延迟时间(假设模块中的两个核心一旦整体运行起来就总是准备好执行指令)。

然而,有些处理器内部的层次结构单元之间没有直接协调。在这种情况下,例如,在“核心”级别请求空闲状态不会自动影响“模块”级别,CPUIdle驱动程序负责整个层次结构的处理。然后,空闲状态对象的定义完全由驱动程序决定,但是处理器硬件最终进入的空闲状态的物理属性必须始终遵循管理器用于选择空闲状态的参数(例如,所选空闲状态对象的实际退出延迟不能超过管理器选择的空闲状态对象的退出延迟参数)。

除了上述讨论的目标驻留时间和退出延迟空闲状态参数外,代表每个空闲状态的对象还包含描述空闲状态的其他几个参数以及指向运行函数的指针,以便请求硬件进入该状态。此外,对于每个结构体cpuidle_state对象,还有一个包含给定空闲状态的使用统计信息的相应的结构体cpuidle_state_usage。内核通过sysfs公开了这些信息。

对于系统中的每个CPU,在sysfs中都有一个/sys/devices/system/cpu/cpu<N>/cpuidle/目录,其中数字<N>是在初始化时分配给给定CPU的编号。该目录包含一组名为state0、state1等的子目录,直到给定CPU定义的空闲状态对象的数量减一。其中的每个目录对应一个空闲状态对象,其名称中的数字越大,表示其代表的(有效)空闲状态越深。每个目录包含一些文件(属性),表示与其对应的空闲状态对象的属性,如下所示:

属性文件说明:

  • above: 请求进入此空闲状态的总次数,但观察到的空闲持续时间肯定太短,无法匹配其目标驻留时间。
  • below: 请求进入此空闲状态的总次数,但肯定更深的空闲状态会更好地匹配观察到的空闲持续时间。
  • desc: 空闲状态的描述。
  • disable: 是否禁用此空闲状态。
  • default_status: 此状态的默认状态,“enabled” 或 “disabled”。
  • latency: 空闲状态的退出延迟(以微秒为单位)。
  • name: 空闲状态的名称。
  • power: 此空闲状态下硬件的功耗(以毫瓦为单位)(如果指定,则为 0)。
  • residency: 空闲状态的目标驻留时间(以微秒为单位)。
  • time: 给定 CPU 在此空闲状态下的总时间(由内核测量,以微秒为单位)。
  • usage: 给定 CPU 请求进入此空闲状态的总次数。
  • rejected: 在给定 CPU 上拒绝请求进入此空闲状态的总次数。

disable 属性是唯一可写的属性。如果它包含 1,则对于此特定 CPU,禁用了给定的空闲状态,这意味着管理器永远不会选择它,并且 CPUIdle 驱动程序永远不会要求硬件为该 CPU 进入该状态。然而,为一个 CPU 禁用一个空闲状态并不会阻止其他 CPU 请求该状态,因此必须对所有 CPU 禁用它,以便任何 CPU 都不会请求它。[需要注意的是,由于梯形管理器的实现方式,禁用一个空闲状态也会阻止管理器选择比禁用状态更深的任何空闲状态。]

如果 disable 属性包含 0,则对于此特定 CPU,启用了给定的空闲状态,但同时可能对系统中的其他一些或所有其他 CPU 禁用了它。将 1 写入它会导致此空闲状态对于此特定 CPU 被禁用,将 0 写入它会允许管理器考虑它,并允许驱动程序请求它,除非该状态在驱动程序中全局禁用(在这种情况下,它根本不能使用)。

power 属性通常没有很好地定义,特别是对于表示处理器内不同级别单元的空闲状态的组合的空闲状态对象,通常很难获得复杂硬件的空闲状态功耗数据,因此 power 属性通常包含 0(不可用),如果包含非零数,则该数可能不太准确,不应依赖它进行任何有意义的操作。

time 文件中的数字通常可能大于实际在给定 CPU 上花费的空闲状态时间,因为它是由内核测量的,可能不包括硬件拒绝进入此空闲状态并进入更浅状态(甚至根本不进入任何空闲状态)的情况。由于这些原因,了解硬件在不同空闲状态下花费了多少时间的唯一可靠方法是使用硬件中的空闲状态驻留计数器(如果可用)。

通常,尝试进入空闲状态时收到的中断会导致空闲状态进入请求被拒绝,在这种情况下,CPUIdle 驱动程序可能会返回错误代码,以指示情况。usage 和 rejected 文件报告了给定空闲状态成功进入或被拒绝的次数。

Linux内核中的CPU功耗管理服务质量(PM QoS)

Linux内核中的功耗管理服务质量(PM QoS)框架允许内核代码和用户空间进程对内核的各种能效特性设置约束,以防止性能下降到所需水平以下。

CPU空闲时间管理可以通过PM QoS以两种方式受到影响,通过全局CPU延迟限制和通过各个CPU的恢复延迟约束。内核代码(例如设备驱动程序)可以借助PM QoS框架提供的特殊内部接口来设置这两种约束。用户空间可以通过打开/dev/下的cpu_dma_latency特殊设备文件并向其中写入一个二进制值(解释为带符号的32位整数)来修改前者。反过来,可以通过向/sys/devices/system/cpu/cpu<N>/power/pm_qos_resume_latency_us中的文件写入一个字符串(表示带符号的32位整数)来从用户空间修改CPU的恢复延迟约束,其中CPU编号<N>是在系统初始化时分配的。在这两种情况下,负值都将被拒绝,并且在这两种情况下,写入的整数将被解释为以微秒为单位的请求的PM QoS约束。

然而,请求的值不会自动应用为新的约束,因为它可能比之前由其他人请求的约束更不严格(在这种特定情况下更大)。因此,PM QoS框架维护了迄今为止已经为全局CPU延迟限制和每个单独CPU请求的约束的请求列表,并对它们进行聚合,并将有效值(在这种特定情况下为最小值)应用为新的约束。

实际上,打开cpu_dma_latency特殊设备文件会导致创建一个新的PM QoS请求,并将其添加到CPU延迟限制请求的全局优先级列表中,而来自“打开”操作的文件描述符代表该请求。如果然后使用该文件描述符进行写入,则写入其中的数字将与其关联的PM QoS请求一起作为新的请求限制值。接下来,优先级列表机制将用于确定整个请求列表的新有效值,并将该有效值设置为新的CPU延迟限制。因此,请求新的限制值只会在实际“列表”值受其影响时改变真实限制,如果它是列表中请求值的最小值,则是这种情况。

通过打开cpu_dma_latency特殊设备文件获取的文件描述符的进程仅控制与该文件描述符关联的PM QoS请求,但仅控制该特定PM QoS请求。

关闭cpu_dma_latency特殊设备文件,或者更准确地说,关闭打开它时获取的文件描述符,会导致与该文件描述符关联的PM QoS请求从全局CPU延迟限制请求的优先级列表中被移除并销毁。如果发生这种情况,优先级列表机制将再次被使用,以确定整个列表的新有效值,并且该值将成为新的限制。

另一方面,对于每个CPU,都有一个与/sys/devices/system/cpu/cpu<N>/power/pm_qos_resume_latency_us中的文件相关联的恢复延迟PM QoS请求,并且对其进行写入会导致此单个PM QoS请求被更新,而不管哪个用户空间进程执行此操作。换句话说,此PM QoS请求由整个用户空间共享,因此需要对与其关联的文件的访问进行仲裁,以避免混淆。然而,它仍然只是一个请求。每次以这种或那种方式更新请求列表时,都会使用优先级列表来确定要设置为该CPU的恢复延迟约束的新有效值(该列表中可能还有其他来自内核代码的请求)。

CPU空闲时间管理器预期将全局(有效)CPU延迟限制的最小值和给定CPU的有效恢复延迟约束视为它们允许为该CPU选择的空闲状态的退出延迟的上限。它们不应选择任何退出延迟超过该限制的空闲状态。

通过内核命令行控制空闲状态

除了允许通过sysfs接口禁用单个CPU的个别空闲状态外,还有影响CPU空闲时间管理的内核命令行参数。

cpuidle.off=1内核命令行选项可用于完全禁用CPU空闲时间管理。它不会阻止空闲CPU上的空闲循环运行,但会阻止CPU空闲时间管理器和驱动程序的调用。如果将其添加到内核命令行,则空闲循环将通过预期提供此目的的CPU架构支持代码要求空闲CPU进入空闲状态。然而,该默认机制通常是实现该架构(即CPU指令集)的所有处理器的最低公共分母,因此相当粗糙且不太节能。因此,不建议在生产环境中使用。

cpuidle.governor=内核命令行开关允许指定CPUIdle管理器的使用。它必须附加一个与可用管理器名称匹配的字符串(例如cpuidle.governor=menu),那么将使用该管理器而不是默认的管理器。例如,可以通过这种方式强制在默认情况下使用阶梯管理器的系统使用菜单管理器。

与CPU空闲时间管理相关的其他内核命令行参数仅适用于x86架构,对intel_idle的引用仅影响Intel处理器。

x86架构支持代码识别与CPU空闲时间管理相关的三个内核命令行选项:idle=pollidle=haltidle=nomwait。前两者都会完全禁用acpi_idleintel_idle驱动程序,从而有效地导致整个CPUIdle子系统被禁用,并使空闲循环调用架构支持代码来处理空闲CPU。它的处理方式取决于将哪个参数添加到内核命令行。在idle=halt情况下,架构支持代码将使用CPU的HLT指令(通常暂停程序的执行并导致硬件尝试进入最浅的可用空闲状态)来实现此目的,而如果使用idle=poll,空闲CPU将在一个更或多或少“轻量级”的指令序列中执行一个紧密的循环。[请注意,在许多情况下使用idle=poll有些激进,因为阻止空闲CPU几乎不节省任何能量可能不是它的唯一影响。例如,在Intel硬件上,它实际上阻止CPU使用需要任意数量的CPU处于空闲状态的P状态(参见CPU性能缩放),因此它很可能会损害单线程计算性能以及能效。因此,出于性能原因使用它可能根本不是一个好主意。]

idle=nomwait选项阻止CPU使用MWAIT指令进入空闲状态。使用此选项时,acpi_idle驱动程序将使用HLT指令而不是MWAIT。在运行Intel处理器的系统上,此选项禁用intel_idle驱动程序并强制使用acpi_idle驱动程序。请注意,在任何情况下,只有当所有其所需的信息都在系统的ACPI表中时,acpi_idle驱动程序才会起作用。

除了影响CPU空闲时间管理的架构级内核命令行选项外,还有影响单个CPUIdle驱动程序的参数可以通过内核命令行传递给它们。具体来说,intel_idle.max_cstate=<n>processor.max_cstate=<n>参数,其中<n>是在sysfs中给定状态目录的名称中也使用的空闲状态索引,会导致intel_idleacpi_idle驱动程序分别丢弃所有比空闲状态<n>更深的空闲状态。在这种情况下,它们将永远不会请求任何这些空闲状态或将它们暴露给管理器。[对于<n>等于0的情况,这两个驱动程序的行为是不同的。将intel_idle.max_cstate=0添加到内核命令行会禁用intel_idle驱动程序并允许使用acpi_idle,而processor.max_cstate=0等效于processor.max_cstate=1。此外,acpi_idle驱动程序是可以单独加载的processor内核模块的一部分,当加载它时可以将max_cstate=<n>作为模块参数传递给它。]