CPU 空闲时间管理

版权:

© 2018 Intel Corporation

作者:

Rafael J. Wysocki <rafael.j.wysocki@intel.com>

概念

现代处理器通常能够进入这样的状态:程序的执行被挂起,属于程序的指令不会从内存中获取或执行。 这些状态是处理器的*空闲*状态。

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

CPU 空闲时间管理是一种节能特性,旨在为此目的使用处理器的空闲状态。

逻辑 CPU

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

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

其次,如果处理器是多核的,那么其中的每个核心一次至少能够遵循一个程序。 这些核心不必完全彼此独立(例如,它们可能共享缓存),但大多数时候它们仍然彼此物理并行工作,因此如果它们中的每一个只执行一个程序,那么这些程序就会大部分时间彼此独立地同时运行。 在这种情况下,整个核心都是 CPU,如果要求硬件进入空闲状态,则适用于首先要求它的核心,但也可能适用于核心所属的更大单元(例如“包”或“集群”)(事实上,它可能适用于包含该核心的整个更大的单元层次结构)。 也就是说,如果除了一个核心之外,更大的单元中的所有核心都已置于“核心级别”的空闲状态,并且剩余的核心要求处理器进入空闲状态,那么可能会触发它将整个更大的单元置于空闲状态,这也会影响该单元中的其他核心。

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

空闲 CPU

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

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

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

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

空闲循环

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

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

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

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

有四个 CPUIdle 调速器可用,分别是 menuTEOladderhaltpoll。 默认使用哪个取决于内核的配置,特别是调度程序节拍是否可以被 空闲循环停止。 可用的调速器可以从 available_governors 中读取,并且可以在运行时更改调速器。 内核当前使用的 CPUIdle 调速器的名称可以从 /sys/devices/system/cpu/cpuidle/ 中的 current_governor_rocurrent_governor 文件中读取,位于 sysfs 中。

另一方面,使用哪个 CPUIdle 驱动程序通常取决于内核运行的平台,但是有多个匹配驱动程序的平台。 例如,有两个驱动程序可以与大多数 Intel 平台一起工作,分别是 intel_idleacpi_idle,一个具有硬编码的空闲状态信息,另一个能够分别从系统的 ACPI 表中读取该信息。 尽管如此,即使在这些情况下,在系统初始化时选择的驱动程序也不能稍后更换,因此必须尽早做出使用哪一个的决定(在 Intel 平台上,如果由于某种原因禁用了 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 调速器将是 ladder

menu 调速器

menu 调速器是无节拍系统的默认 CPUIdle 调速器。 它非常复杂,但其设计的基本原则很简单。 也就是说,当被调用为 CPU 选择空闲状态时(即 CPU 将要求处理器硬件进入的空闲状态),它会尝试预测空闲持续时间,并使用预测值来选择空闲状态。

它首先使用一个简单的模式识别算法来获得空闲持续时间的初步预测。 也就是说,它保存最后 8 个观察到的空闲持续时间值,并且在下次预测空闲持续时间时,它会计算它们的平均值和方差。 如果方差很小(小于 400 平方毫秒),或者相对于平均值很小(平均值大于标准差的 6 倍),则该平均值被视为“典型间隔”值。 否则,放弃保存的观察到的空闲持续时间值中最长或最短的(取决于哪个离平均值更远),并对剩余的值重复计算。

同样,如果它们的方差很小(在上述意义上),则将平均值作为“典型间隔”值,依此类推,直到确定“典型间隔”或放弃的数据点太多。 在后一种情况下,如果仍在考虑的数据点集的大小足够大,则下一个空闲持续时间不太可能高于仍在该集中的最大空闲持续时间值,因此该值被视为预测的下一个空闲持续时间。 最后,如果仍在考虑的数据点集太小,则不会进行预测。

如果以这种方式计算出的下一个空闲持续时间的初步预测足够长,则调速器将在假设调度程序节拍将被停止的情况下,获得直到最近的计时器事件的时间。 在下文中,该时间称为*睡眠长度*,它是下一次 CPU 唤醒之前的时间上限。 它用于确定睡眠长度范围,而睡眠长度范围又需要用于获得睡眠长度校正因子。

menu 调速器维护一个包含多个校正因子值的数组,这些校正因子值对应于不同的睡眠长度范围,这些范围的组织方式使得数组中表示的每个范围都比前一个范围宽约 10 倍。

在 CPU 唤醒后更新给定睡眠长度范围的校正因子(在为 CPU 选择空闲状态之前确定),并且睡眠长度越接近观察到的空闲持续时间,校正因子越接近 1(它必须在 0 和 1 之间,包括 0 和 1)。 睡眠长度乘以它所属范围的校正因子,以获得预测的空闲持续时间的近似值,该近似值与先前确定的“典型间隔”进行比较,并将两者的最小值作为最终的空闲持续时间预测。

如果“典型间隔”值很小,这意味着 CPU 可能会很快被唤醒,则跳过睡眠长度计算,因为它可能很昂贵,并且简单地预测空闲持续时间等于“典型间隔”值。

现在,调速器已准备好遍历空闲状态列表并选择其中一个。 为此,它会将每个状态的目标驻留时间与预测的空闲持续时间进行比较,并将其退出延迟与来自电源管理服务质量或 PM QoS 框架的延迟限制进行比较。 它选择目标驻留时间最接近预测的空闲持续时间但仍低于它的状态,并且退出延迟不超过限制。

在最后一步中,如果调速器尚未决定停止调度程序节拍,则它可能仍然需要优化空闲状态选择。 如果它预测的空闲持续时间小于节拍周期,并且节拍尚未停止(在空闲循环的先前迭代中),则会发生这种情况。 那么,先前计算中使用的睡眠长度可能无法反映直到最近的计时器事件的真实时间,并且如果它真的大于该时间,则调速器可能需要选择具有合适目标驻留时间的较浅状态。

面向计时器事件 (TEO) 的调速器

面向计时器事件 (TEO) 的调速器是无节拍系统的替代 CPUIdle 调速器。 它遵循与 menu 调速器 相同的基本策略:它始终尝试找到适合给定条件的最深空闲状态。 然而,它对该问题应用了不同的方法。

该调速器的想法是基于观察到在许多系统中,计时器中断的频率比任何其他中断类型高两个或多个数量级,因此它们可能会主导 CPU 唤醒模式。 此外,原则上,可以在空闲状态选择时确定下一个计时器事件将发生的时间,尽管这样做可能很昂贵,因此可以将其视为空闲状态选择最可靠的信息来源。

当然,在某些用例中,非计时器唤醒源更重要,但即使如此,通常也没有必要考虑大于直到下一个计时器事件的时间的空闲持续时间值,这在下文中称为睡眠长度,因为最近的计时器最终会唤醒 CPU,除非它更早被唤醒。

然而,由于获得睡眠长度可能很昂贵,因此调速器首先检查它是否可以使用最近的唤醒模式信息来选择浅空闲状态,在这种情况下,它可以完全不知道睡眠长度。 为此,它会计算 CPU 唤醒事件的数量,并查找其目标驻留时间在大多数相关最近情况下没有超过空闲持续时间(在唤醒后测量)的空闲状态。 如果该状态的目标驻留时间足够小,则可以立即使用它,而无需确定睡眠长度。

该调速器执行的计算基于使用其边界与 CPUIdle 驱动程序按升序提供的 CPU 空闲状态的目标驻留时间参数值对齐的存储桶。 也就是说,第一个存储桶从 0 跨越到第二个空闲状态(空闲状态 1)的目标驻留时间,但不包括它,第二个存储桶从空闲状态 1 的目标驻留时间跨越到空闲状态 2 的目标驻留时间,但不包括它,第三个存储桶从空闲状态 2 的目标驻留时间跨越到空闲状态 3 的目标驻留时间,但不包括它,依此类推。 最后一个存储桶从驱动程序提供的最深空闲状态的目标驻留时间跨越到无穷大。

两个名为“命中”和“截获”的指标与每个存储桶相关联。 每次在为给定的 CPU 选择空闲状态之前,都会根据上次发生的情况来更新它们。

“命中”指标反映了睡眠长度和 CPU 唤醒后测量的空闲持续时间落入同一存储桶中的情况的相对频率(也就是说,相对于睡眠长度,CPU 似乎“按时”唤醒)。 反过来,“截获”指标反映了非计时器唤醒事件的相对频率,对于这些事件,测量的空闲持续时间落入与睡眠长度落入的存储桶对应的空闲状态更浅的存储桶中(这些事件也在下面称为“截获”)。

调速器还会计算其测量的空闲持续时间低于节拍周期长度的“截获”,并在决定是否停止调度程序节拍时使用此信息。

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

  1. 找到最深的已启用 CPU 空闲状态(候选空闲状态),并按如下方式计算 2 个总和:

    • 比候选状态浅的所有空闲状态的“命中”指标的总和(它表示 CPU 可能被计时器唤醒的情况)。

    • 比候选状态浅的所有空闲状态的“截获”指标的总和(它表示 CPU 可能被非计时器唤醒源唤醒的情况)。

  2. 如果在步骤 1 中计算的第二个总和大于候选状态存储桶和所有后续存储桶(如果有)的两个指标的总和的一半,则较浅的空闲状态可能更合适,因此请寻找它。

    • 按降序遍历比候选状态浅的已启用空闲状态。

    • 对于它们中的每一个,计算从它到候选状态(包括前者但不包括后者)的所有空闲状态上的“截获”指标的总和。

    • 如果该总和大于在步骤 1 中计算的第二个总和的一半,则使用给定的空闲状态作为新的候选状态。

  3. 如果当前候选状态是状态 0 或其目标驻留时间足够短,则返回它并阻止调度程序节拍停止。

  4. 获得睡眠长度值并检查它是否低于当前候选状态的目标驻留时间,在这种情况下,需要找到一个新的较浅的候选状态,因此请寻找它。

空闲状态的表示

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

例如,假设一个处理器在称为“模块”的更大单元中有两个核心,并且假设通过一个核心要求硬件在“核心”级别进入特定的空闲状态(例如“X”)将触发模块尝试进入其自身的特定空闲状态(例如“MX”),如果另一个核心已经处于空闲状态“X”。 换句话说,在“核心”级别请求空闲状态“X”会授予硬件许可证以深入到“模块”级别的空闲状态“MX”,但不能保证会发生这种情况(请求空闲状态“X”的核心可能最终会自行进入该状态)。 那么,表示空闲状态“X”的 struct cpuidle_state 对象的目标驻留时间必须反映模块在空闲状态“MX”中花费的最短时间(包括进入该状态所需的时间),因为这是 CPU 需要空闲的最短时间,以便在硬件进入该状态的情况下节省任何能量。 类似地,该对象的退出延迟参数必须包括模块空闲状态“MX”的退出时间(并且通常也包括其进入时间),因为这是唤醒信号与 CPU 将开始执行第一个新指令的时间之间的最大延迟(假设模块中的两个核心都将准备好在模块作为一个整体变得可操作后立即执行指令)。

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

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

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

上面

请求该空闲状态的总次数,但观察到的空闲持续时间肯定太短,无法与其目标驻留时间匹配。

以下

此空闲状态被请求的总次数,但显然更深层次的空闲状态更适合观察到的空闲持续时间。

描述

空闲状态的描述。

禁用

此空闲状态是否被禁用。

默认状态

此状态的默认状态,“启用”或“禁用”。

延迟

空闲状态的退出延迟,单位为微秒。

名称

空闲状态的名称。

功耗

此空闲状态下硬件消耗的功率,单位为毫瓦(如果指定,则为 0)。

驻留时间

空闲状态的目标驻留时间,单位为微秒。

时间

给定 CPU 在此空闲状态下花费的总时间(由内核测量),单位为微秒。

使用次数

给定 CPU 请求硬件进入此空闲状态的总次数。

拒绝次数

在给定的 CPU 上,进入此空闲状态的请求被拒绝的总次数。

descname 文件都包含字符串。它们之间的区别在于,name 期望更简洁,而 description 可能更长,并且可能包含空格或特殊字符。上面列出的其他文件包含整数。

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

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

power 属性没有明确定义,特别是对于代表处理器中不同级别单元层次结构中空闲状态组合的空闲状态对象而言,并且通常很难获得复杂硬件的空闲状态功耗数字,因此 power 通常包含 0(不可用),如果它包含非零数字,则该数字可能不是很准确,不应依赖于任何有意义的事情。

time 文件中的数字通常可能大于给定 CPU 在给定空闲状态下实际花费的总时间,因为它是由内核测量的,并且可能不包括硬件拒绝进入此空闲状态并进入较浅的空闲状态(甚至根本没有进入任何空闲状态)的情况。内核只能测量请求硬件进入空闲状态到 CPU 随后唤醒之间的时间跨度,并且它无法说明在此期间硬件级别上实际发生了什么。此外,如果所讨论的空闲状态对象代表处理器中不同级别单元层次结构中空闲状态的组合,则内核永远无法说明硬件在任何特定情况下在该层次结构中下降了多深。出于这些原因,找出硬件在支持的不同空闲状态下花费了多少时间的唯一可靠方法是使用硬件中的空闲状态驻留计数器(如果可用)。

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

CPU 的电源管理服务质量

Linux 内核中的电源管理服务质量 (PM QoS) 框架允许内核代码和用户空间进程设置内核各种节能功能的约束,以防止性能下降到低于要求的水平。

CPU 空闲时间管理可能受到 PM QoS 的两种影响,一种是通过全局 CPU 延迟限制,另一种是通过各个 CPU 的恢复延迟约束。内核代码(例如设备驱动程序)可以使用 PM QoS 框架提供的特殊内部接口来设置这两者。用户空间可以通过打开 /dev/ 下的 cpu_dma_latency 特殊设备文件并将二进制值(解释为带符号的 32 位整数)写入其中来修改前者。反过来,可以通过将字符串(表示带符号的 32 位整数)写入 sysfs/sys/devices/system/cpu/cpu<N>/ 下的 power/pm_qos_resume_latency_us 文件来从用户空间修改 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,都有一个与 sysfs/sys/devices/system/cpu/cpu<N>/ 下的 power/pm_qos_resume_latency_us 文件关联的恢复延迟 PM QoS 请求,并且写入该文件会导致此单个 PM QoS 请求被更新,无论哪个用户空间进程执行此操作。换句话说,此 PM QoS 请求由整个用户空间共享,因此需要仲裁对与之关联的文件的访问,以避免混淆。[可以说,实际上使用此机制的唯一合法方法是将进程固定到所讨论的 CPU,并让它使用 sysfs 接口来控制它的恢复延迟约束。] 然而,这仍然只是一个请求。 它是优先级列表中的一个条目,用于确定要设置为 CPU 的恢复延迟约束的有效值,每次以这种或其他方式更新请求列表时(该列表中可能还有来自内核代码的其他请求)。

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

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

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

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

cpuidle.governor= 内核命令行开关允许指定要使用的 CPUIdle 调控器。必须在后面附加一个与可用调控器的名称匹配的字符串(例如 cpuidle.governor=menu),并且将使用该调控器而不是默认调控器。例如,可以强制在默认情况下使用 ladder 调控器的系统上使用 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_idle 驱动程序只有在系统 ACPI 表中包含它所需的所有信息时才能正常工作。

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