0%

cpuidle

逻辑 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 空闲多长时间。 这是空闲循环的工作。

空闲循环(idle loop)

​ 空闲循环在它的每次迭代中都需要两个主要步骤。首先,它调用属于 CPU 空闲时间管理子系统 CPUIdle 的称为 governor 的代码模块,为 CPU 选择一个空闲状态以请求硬件进入。其次,它调用来自 CPUIdle 子系统的另一个代码模块 driver,以实际要求处理器硬件进入由 governor 选择的空闲状态。

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

​ 该数组中的每个空闲状态有两个参数会成为 governor 考虑的对象:目标驻留(target residency)和(最坏情况)退出延迟(exit latency)。目标驻留时间是硬件必须在给定状态中花费的最短时间,包括进入它所需的时间(可能很长)。只有花费比目标驻留时间长,才能比通过进入较浅的空闲状态节省更多的能量。 [空闲状态的“深度”大致对应于处理器在该状态下消耗的功率。] 退出延迟是要求处理器硬件进入空闲状态的 CPU 从空闲状态被唤醒到开始执行第一条指令所需的最长时间。请注意,通常退出延迟还必须包含进入给定状态所需的时间。万一在硬件进入空闲状态时发生唤醒,必须完全进入空闲状态后才能退出。

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

​ 有四个可用的 CPUIdle governors:menu、TEO、ladder 和 haltpoll。默认情况下使用哪一个取决于内核的配置,特别是空闲循环是否可以停止调度程序 tick。可用的 governor 可以从 available_governors 中读取,并且governor 可以在运行时更改。内核当前使用的 CPUIdle governor的名称可以从 sysfs 中 /sys/devices/system/cpu/cpuidle/ 下的 current_governor_ro 或 current_governor 文件中读取。

​ 另一方面,使用哪个 CPUIdle driver 通常取决于内核运行的平台,但有些平台有多个匹配的驱动程序。例如,有两个驱动程序可以与大多数英特尔平台一起使用,intel_idle 和 acpi_idle,一个具有硬编码的空闲状态信息,另一个能够分别从系统的 ACPI 表中读取该信息。尽管如此,即使在这些情况下,在系统初始化时选择的驱动程序也不能在以后替换,因此必须尽早决定使用其中的一个。内核当前使用的CPUIdle driver 的名称可以从sysfs中/sys/devices/system/cpu/cpuidle/下的current_driver文件中读取。

空闲 CPU 和调度器 tick

​ CPU 调度程序的 tick 使这项工作变得特别困难。这是一个由 CPU 调度器运行的定时器,目的是分时 CPU:如果你要在单个 CPU 上运行多个任务,每个任务只能运行一段时间,然后定期搁置做另一份任务。tick 不需要在空闲的 CPU 上运行,因为没有应该共享 CPU 的任务。此外,如果允许 tick 在原本空闲的 CPU 上运行,它将通过限制 CPU 可能保持空闲的时间来禁止 governor 选择深度空闲状态。所以在内核 4.16 和更早版本中,调度程序在调用 governor 之前禁用 tick。当 CPU 被中断唤醒时,调度程序会决定是否有工作要做,如果有,则重新激活 tick。

​ 如果 governor 预测到长时间空闲,并且空闲时间也的确是长,则 governor 获胜:CPU 将进入深度空闲状态并节省电量。但是,如果 governor 预测长时间闲置而空闲时间很短,那么 governor 会失败,因为进入深度空闲状态的成本不会通过短时间空闲状态的节电来弥补。更糟糕的是,如果 governor 预测空闲时间较短,无论实际空闲状态持续时间多长,它都会失败:如果空闲状态实际持续时间很长,则错过了潜在的节能效果;如果时间很短,则停止和重新启动 tick 的成本又被不必要地支付,即因为停止和启动 tick 是有代价的,如果 governor 预测短暂的空闲,停止 tick 是没有意义的。

​ Wysocki 考虑尝试重新设计 governor 来解决这个问题,但得出的结论是,基本问题是在调用 governor 之前停止 tick,即在获得预测的空闲状态之前就停止了 tick。因此,他为内核 4.17 重新设计了空闲循环:governor 预测空闲状态之后再做出停止 tick 的决定。如果预测长时间空闲,则停止 tick,以免过早唤醒 CPU。如果建议是短时间空闲,tick 则保持开启状态以避免关闭它的成本。这意味着 tick 是一个 safety net,如果空闲时间比预期的长,它会唤醒 CPU,并给 governor 另一个机会来让它正确。

​ 当空闲的 CPU 被中断唤醒时,无论是从保持运行的 tick 还是其他事件,调度程序立即决定是否有工作要做。如果有,则在需要时重新启动滴答;但如果没有,则立即重新调用 governor。由于现在可以在 tick 运行和停止时调用 governor,因此必须重新设计调控器以考虑到这一点。

​ 重新检查之前的赢/输表,Wysocki 预计这次返工会改善情况。如果预测到长时间空闲,tick 仍然停止,所以没有任何变化:如果实际空闲时间长,我们就赢了,如果实际空闲时间短,我们就输了。但是如果预测到短空闲,我们会更好:如果实际空闲很短,我们就节省了停止和重新启动 tick 的成本;如果实际空闲很长,未停止的计时器将唤醒 governor,并再次预测。

​ Wysocki 在许多系统上对此进行了测试。上图是所有测试系统的特征,显示了空闲系统上的功耗与时间的关系。绿线是旧的空闲循环,红线是新的:新方案下的功耗更低,而且比以前更可预测。并非所有测试的 CPU 都显示绿线和红线之间的差距如此之大,但所有 CPU 都在凹凸不平的绿线下方显示了一条平坦的红线。正如 Wysocki 所说,与旧方案相比,这种新方案预测短的空闲时间的频率更低,但它们预测为短空闲时间正确的概率更高。

​ 内核可以配置为完全禁止在空闲循环中停止 tick。 这可以通过取消设置 CONFIG_NO_HZ_IDLE 配置选项或通过在命令行中传递 nohz=off 来完成。 在这两种情况下,由于 tick 的停止被禁用,空闲循环代码简单地忽略了governor 的决定,并且 tick 永远不会停止。

​ 运行配置为允许在空闲 CPU 上停止 tick 的内核的系统称为 tickless 系统,它们通常被认为比运行无法停止 tick 的内核的系统更节能。 如果给定的系统是 tickless 的,它将默认使用 menu,如果它不是 tickless,则其上的默认 CPUIdle 调控器将是 ladder。

​ menu governor 是 tickless 的默认 CPUIdle 系统。它相当复杂,但其设计的基本原理十分直接,当它为 CPU 选择空闲状态时,它会尝试预测空闲持续时间并使用预测值进行空闲状态选择。

​ menu governor 首先获取到最近的计时器事件的时间,并假设调度程序 tick 将停止。该时间,在下文中称为睡眠时长,是下一次 CPU 唤醒前的时间上限。它用于确定睡眠长度范围,进而获得睡眠长度校正因子。

​ menu governor 维护两个睡眠长度校正因子数组。其中一个用于先前在给定 CPU 上运行的任务正在等待某些 I/O 操作完成时使用,而另一个则用在其他情况。每个数组包含多个校正因子值,这些值对应于不同的睡眠长度范围,数组中表示的每个范围都比前一个范围宽大约 10 倍。

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

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

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

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

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

参考文献

https://lwn.net/Articles/767630/

https://www.kernel.org/doc/html/latest/admin-guide/pm/cpuidle.html