CPU 空闲时间管理

版权:

© 2018 英特尔公司

作者:

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

概念

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

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

CPU 空闲时间管理是一项关于使用处理器空闲状态以达到此目的的节能功能。

逻辑 CPU

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 被视为空闲。换句话说,在 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 驱动程序的名称可以从 /sys/devices/system/cpu/cpuidle/ 下的 current_driver 文件中读取 sysfs

空闲 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 将请求处理器硬件进入的空闲状态),它会尝试预测空闲持续时间,并使用预测值进行空闲状态选择。

它首先获得直到最近的定时器事件的时间,并假设调度器节拍将停止。该时间(在下文中称为睡眠长度)是下一次 CPU 唤醒之前的时间上限。它用于确定睡眠长度范围,而睡眠长度范围又需要用于获得睡眠长度校正因子。

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

给定睡眠时长范围的校正因子(在为 CPU 选择空闲状态之前确定)在 CPU 被唤醒后更新,并且睡眠时长越接近观察到的空闲持续时间,校正因子越接近 1(它必须落在 0 到 1 之间,包括 0 和 1)。睡眠时长乘以它所属范围的校正因子,以获得预测空闲持续时间的第一个近似值。

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

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

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

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

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

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

此调速器的想法基于以下观察:在许多系统中,定时器事件的频率比任何其他中断高两到三个数量级,因此它们很可能是 CPU 从空闲状态唤醒的最重要原因。此外,有关过去(相对较近)发生的事情的信息可用于估计具有目标驻留时间的深度空闲状态(在已知的)直到最近定时器事件的时间内(称为睡眠时长)是否可能适合即将到来的 CPU 空闲周期,如果不是,则选择哪个较浅的空闲状态代替它。

当然,非定时器唤醒源在某些用例中更重要,这些用例可以通过考虑 CPU 的最近几个空闲时间间隔来涵盖。然而,即使在这种情况下,也没有必要考虑大于睡眠时长的空闲持续时间值,因为除非它更早被唤醒,否则最近的定时器最终会唤醒 CPU。

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

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

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

“命中”指标反映了睡眠时长和 CPU 唤醒后测量的空闲持续时间落入同一容器的情况的相对频率(也就是说,相对于睡眠时长,CPU 似乎“按时”唤醒)。反过来,“拦截”指标反映了测量的空闲持续时间比睡眠时长短得多,以至于它落入的容器对应于比睡眠时长落入的容器的空闲状态更浅的空闲状态的情况的相对频率(这些情况在下面称为“拦截”)。

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

  1. 找到目标驻留时间不超过当前睡眠时长(候选空闲状态)的最深 CPU 空闲状态,并按如下方式计算 2 个总和

    • 候选状态和所有更深空闲状态的“命中”和“拦截”指标的总和(它表示 CPU 空闲时间足够长,如果睡眠时长等于当前睡眠时长,则可以避免被拦截的情况)。

    • 所有比候选状态浅的空闲状态的“拦截”指标的总和(它表示 CPU 空闲时间不足以避免被拦截,如果睡眠时长等于当前睡眠时长,则会发生这种情况)。

  2. 如果第二个总和大于第一个总和,则 CPU 很可能会提前唤醒,因此请寻找要选择的备用空闲状态。

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

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

    • 如果需要考虑的每个总和(因为与之相关的检查表明 CPU 很可能会提前唤醒)大于在步骤 1 中计算的相应总和的一半(这意味着在超过一半的相关情况下,有问题的状态的目标驻留时间没有超过空闲持续时间),则选择给定的空闲状态而不是候选状态。

  3. 默认情况下,选择候选状态。

空闲状态的表示

为了实现 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 定义的空闲状态对象数减一。这些目录中的每一个都对应一个空闲状态对象,并且其名称中的数字越大,它所代表的(有效)空闲状态越深。每个目录都包含许多文件(属性),这些文件表示与其对应的空闲状态对象的属性,如下所示:

above

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

below

请求此空闲状态的总次数,但肯定更深的空闲状态会更适合观察到的空闲持续时间。

desc

空闲状态的描述。

disable

是否禁用此空闲状态。

default_status

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

latency

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

name

空闲状态的名称。

power

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

residency

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

time

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

usage

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

rejected

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

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

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

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

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,在 /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 选择的空闲状态的退出延迟上限。它们绝不应选择任何退出延迟超过该限制的空闲状态。

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

除了允许 sysfs 接口 为单个 CPU 禁用各个空闲状态外,还有影响 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 使用 P 状态(请参阅 CPU 性能缩放),这需要一个软件包中的任意数量的 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> 更深的空闲状态。在这种情况下,它们将永远不会要求任何这些空闲状态或将其暴露给调速器。[对于 <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> 可以在加载时作为模块参数传递给它。]