intel_idle CPU 空闲时间管理驱动

版权:

© 2020 英特尔公司

作者:

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

一般信息

intel_idle 是 Linux 内核中 CPU 空闲时间管理子系统 (CPUIdle) 的一部分。它是 Nehalem 及后续英特尔处理器系列的默认 CPU 空闲时间管理驱动,但其对特定处理器模型的支持程度取决于它是否识别该处理器模型,也可能取决于平台固件提供的信息。[要理解 intel_idle,有必要了解 CPUIdle 的一般工作原理,因此如果您尚未熟悉 CPU 空闲时间管理,现在是时候了。]

intel_idle 使用 MWAIT 指令通知处理器,执行该指令的逻辑 CPU 处于空闲状态,因此可以将处理器的一些功能模块置于低功耗状态。该指令接受两个参数(通过目标 CPU 的 EAXECX 寄存器传递),其中第一个参数被称为 提示,可供处理器用于确定可以执行的操作(详情请参阅英特尔软件开发人员手册 [1])。因此,intel_idle 拒绝在禁用了 MWAIT 指令支持(例如,通过平台固件配置菜单)或根本不支持该指令的处理器上工作。

intel_idle 不是模块化的,因此无法卸载,这意味着向其传递早期配置参数的唯一方法是通过内核命令行。

Sysfs 接口

intel_idle 驱动在 /sys/devices/system/cpu/cpuidle/ 中暴露以下 sysfs 属性:

intel_c1_demotion

启用或禁用系统中所有 CPU 的 C1 降级。此文件仅在支持 C1 降级功能并经过测试的平台上公开。值 0 表示 C1 降级已禁用,值 1 表示已启用。写入 0 或 1 可禁用或启用所有 CPU 的 C1 降级。

C1 降级功能涉及平台固件将来自操作系统的深度 C 状态请求(例如,C6 请求)降级到 C1。其思想是固件监控 CPU 唤醒速率,如果该速率高于平台特定阈值,固件会将深度 C 状态请求降级到 C1。例如,Linux 请求 C6,但固件检测到每秒唤醒次数过多,因此将 CPU 保持在 C1 状态。当 CPU 在 C1 中停留足够长时间后,平台会将其提升回 C6。这可能会提高某些工作负载的性能,但也可能增加功耗。

空闲状态枚举

每个 MWAIT 提示值都被处理器解释为以某种方式重新配置自身以节省能源的许可。由此产生的处理器配置(功耗降低)被称为 C 状态(在 ACPI 术语中)或空闲状态。有意义的 MWAIT 提示值和与其对应的空闲状态(即处理器的低功耗配置)列表取决于处理器型号,也可能取决于平台的配置。

为了创建 CPUIdle 子系统所需的可用空闲状态列表(参见 CPU 空闲时间管理 中的 空闲状态表示),intel_idle 可以使用两种信息来源:驱动程序本身中包含的不同处理器型号的空闲状态静态表,以及系统的 ACPI 表。如果当前处理器型号被 intel_idle 识别,则始终使用前者;如果给定处理器型号需要后者(intel_idle 识别的所有服务器处理器型号都是这种情况),或者处理器型号不被识别,则使用后者。[有一个模块参数可以用来使驱动程序对任何识别的处理器型号都使用 ACPI 表;参见下文。]

如果使用 ACPI 表来构建可用空闲状态列表,intel_idle 首先会在系统中对应 CPU 的 ACPI 对象下查找 _CST 对象(有关 _CST 及其输出包的描述,请参阅 ACPI 规范 [2])。由于 CPUIdle 子系统期望驱动程序提供的空闲状态列表适用于它所处理的所有 CPU,并且 intel_idle 被注册为系统中所有 CPU 的 CPUIdle 驱动,因此驱动程序会查找第一个返回至少一个有效空闲状态描述的 _CST 对象,并且其返回包中包含的所有空闲状态都属于 FFH(功能固定硬件)类型,这意味着预计将使用 MWAIT 指令告知处理器可以进入其中一个状态。然后,该 _CST 的返回包被假定适用于系统中所有其他 CPU,并从中提取的空闲状态描述存储在来自 ACPI 表的初步空闲状态列表中。[如果 intel_idle 配置为忽略 ACPI 表,则跳过此步骤;参见下文。]

接下来,可用空闲状态列表中的第一个(索引 0)条目被初始化为表示“轮询空闲状态”(一种伪空闲状态,其中目标 CPU 不断地获取和执行指令),随后的(真实)空闲状态条目按如下方式填充。

如果当前处理器型号被 intel_idle 识别,则驱动程序中有一个(静态)空闲状态描述表。在这种情况下,“内部”表是空闲状态信息的主要来源,其信息被复制到可用空闲状态的最终列表中。如果不需要使用 ACPI 表进行空闲状态枚举(取决于处理器型号),则所有列出的空闲状态默认启用(因此它们都将在 CPU 空闲状态选择期间被 CPUIdle 管理器考虑)。否则,如果来自 ACPI 表的初步空闲状态列表中没有匹配的条目,则某些列出的空闲状态可能默认不启用。在这种情况下,用户空间仍然可以稍后(基于每个 CPU)借助 sysfs 中的 disable 空闲状态属性来启用它们(参见 CPU 空闲时间管理 中的 空闲状态表示)。这基本上意味着,如果平台固件(通过 ACPI 表)未公开,则驱动程序“已知”的空闲状态可能默认不启用。

如果给定处理器型号不被 intel_idle 识别,但它支持 MWAIT,则来自 ACPI 表的初步空闲状态列表将用于构建最终列表,该列表将在驱动程序注册期间提供给 CPUIdle 核心。对于该列表中的每个空闲状态,其描述、MWAIT 提示和退出延迟都将复制到最终空闲状态列表中的相应条目。其所代表的空闲状态的名称(由 sysfs 中的 name 空闲状态属性返回)为“CX_ACPI”,其中 X 是该空闲状态在最终列表中的索引(请注意,X 的最小值为 1,因为 0 保留给“轮询”状态),其目标驻留时间基于退出延迟值。具体来说,对于 C1 类型的空闲状态,退出延迟值也用作目标驻留时间(为了与 intel_idle 识别的各种处理器型号的多数“内部”空闲状态表兼容),对于其他空闲状态类型(C2 和 C3),目标驻留时间值是退出延迟的 3 倍(同样,这是因为在 intel_idle 识别的处理器型号的大多数情况下,它反映了目标驻留时间与退出延迟的比率)。在这种情况下,最终列表中的所有空闲状态默认启用。

初始化

intel_idle 的初始化始于检查内核命令行选项是否禁止使用 MWAIT 指令。如果是这种情况,则立即返回错误代码。

下一步是检查驱动程序是否识别处理器型号,这决定了空闲状态枚举方法(参见上文),以及处理器是否支持 MWAIT(如果不支持,初始化将失败)。然后,通过 CPUID 枚举处理器中的 MWAIT 支持,如果支持级别不符合预期(例如,返回的 MWAIT 子状态总数为 0),则驱动程序初始化失败。

接下来,如果驱动程序未配置为忽略 ACPI 表(参见下文),则从其中提取平台固件提供的空闲状态信息。

然后,为所有 CPU 分配 CPUIdle 设备对象,并按照上文所述创建可用空闲状态列表。

最后,借助 cpuidle_register_driver()intel_idle 被注册为系统中所有 CPU 的 CPUIdle 驱动,并通过 cpuhp_setup_state() 注册了一个用于配置单个 CPU 的 CPU 上线回调(其中,这会导致当时系统中所有存在的 CPU 都调用该回调例程,每个 CPU 执行自己的回调例程实例)。该例程为运行它的 CPU 注册一个 CPUIdle 设备(从而使 CPUIdle 子系统能够操作该 CPU),并可选地执行给定处理器型号可能需要的一些 CPU 特定初始化操作。

内核命令行选项和模块参数

x86 架构支持代码识别三个与 CPU 空闲时间管理相关的内核命令行选项:idle=pollidle=haltidle=nomwait。如果内核命令行中存在其中任何一个,则不允许使用 MWAIT 指令,因此 intel_idle 的初始化将失败。

除此之外,intel_idle 自身识别五个模块参数,这些参数可以通过内核命令行设置(它们不能通过 sysfs 更新,因此这是更改其值的唯一方法)。

max_cstate 参数值是驱动程序注册期间提供给 CPUIdle 核心的空闲状态列表中的最大空闲状态索引。它也是 intel_idle 可以使用的常规(非轮询)空闲状态的最大数量,因此在找到该数量的可用空闲状态后,空闲状态的枚举就会终止(如果 max_cstate 更大,则可能已使用的其他空闲状态根本不予考虑)。设置 max_cstate 可以阻止 intel_idle 将因某种原因被视为“过深”的空闲状态暴露给 CPUIdle 核心,但它通过使它们在系统关闭并重新启动之前有效地不可见来做到这一点,这可能并非总是合乎需要的。实际上,只有当相关空闲状态无法在系统启动期间启用时,才真正需要这样做,因为在系统工作状态下,CPU 电源管理服务质量 (PM QoS) 功能可以用于阻止 CPUIdle 接触那些空闲状态,即使它们已经被枚举(参见 CPU 空闲时间管理 中的 CPU 的电源管理服务质量)。将 max_cstate 设置为 0 会导致 intel_idle 初始化失败。

如果内核配置了 ACPI 支持,intel_idle 会识别 no_acpiuse_acpino_native 模块参数。在未配置 ACPI 的情况下,这些标志对功能没有影响。

no_acpi - 完全不使用 ACPI。只提供原生模式,不提供 ACPI 模式。

use_acpi - 在 ACPI 模式下为空操作,在原生模式下驱动程序将查询 ACPI 表以获取 C 状态的开启/关闭状态。

no_native - 仅在 ACPI 模式下工作,不提供原生模式(忽略所有自定义表)。

states_off 模块参数的值(默认为 0)表示一个空闲状态列表,以位掩码的形式默认禁用。

具体来说,states_off 值中已设置的位的位置是默认要禁用的空闲状态的索引(如 sysfs 中相应空闲状态目录的名称所示,state0state1 ... state<i> ...,其中 <i> 是给定空闲状态的索引;参见 CPU 空闲时间管理 中的 空闲状态表示)。

例如,如果 states_off 等于 3,驱动程序将默认禁用空闲状态 0 和 1;如果等于 8,将默认禁用空闲状态 3,依此类推(超出最大空闲状态索引的位位置将被忽略)。

通过这种方式禁用的空闲状态可以通过用户空间经由 sysfs 启用(基于每个 CPU)。

ibrs_off 模块参数是一个布尔标志(默认为 false)。如果设置,它用于控制当 CPU 进入空闲状态时,IBRS(间接分支限制推测)是否应关闭。此标志不影响使用增强型 IBRS 的 CPU,后者可以保持开启状态而对性能影响很小。

对于某些 CPU,IBRS 将默认作为 Spectre v2 和 Retbleed 安全漏洞的缓解措施。在空闲时保持 IBRS 模式开启可能会对其兄弟 CPU 的性能产生影响。IBRS 模式在 CPU 进入深度空闲状态时将默认关闭,但在某些较浅的空闲状态中则不会。设置 ibrs_off 模块参数将强制 CPU 在任何可用空闲状态下关闭 IBRS 模式。这可能有助于提高兄弟 CPU 的性能,但代价是空闲 CPU 的唤醒延迟略高。

空闲状态的核级和包级

通常,在支持 MWAIT 指令的处理器中,存在(至少)两个级别的空闲状态(或 C 状态)。一个级别称为“核 C 状态”,涵盖处理器中的单个核;而另一个级别称为“包 C 状态”,涵盖整个处理器封装,并且可能还涉及系统的其他组件(GPU、内存控制器、I/O 集线器等)。

一些 MWAIT 提示值允许处理器仅使用核 C 状态(最重要的是,对应于 C1 空闲状态的 MWAIT 提示值就是这种情况),但大多数提示值都允许处理器将目标核(即包含执行 MWAIT 且具有给定提示值的逻辑 CPU 的核)置于特定的核 C 状态,然后(如果可能)进入更深层次的特定包 C 状态。例如,表示 C3 空闲状态的 MWAIT 提示值允许处理器将目标核置于称为“核 C3”(或 CC3)的低功耗状态,这发生在如果该核中所有逻辑 CPU(SMT 同级)都已执行了具有 C3 提示值(或代表更深空闲状态的提示值)的 MWAIT 的情况下,并且除此之外(在大多数情况下),它还允许处理器将整个封装(可能包括 GPU 或内存控制器等非 CPU 组件)置于称为“包 C3”(或 PC3)的低功耗状态,这发生在所有核都已进入 CC3 状态且(可能)满足某些附加条件的情况下(例如,如果 GPU 包含在 PC3 中,则可能要求它处于特定的 GPU 特定低功耗状态才能达到 PC3)。

通常,没有简单的方法可以在满足进入相应包 C 状态的条件时,强制处理器仅使用核 C 状态,因此,执行具有非核级专用(例如 C1)提示值的 MWAIT 的逻辑 CPU 必须始终假定这可能导致处理器进入包 C 状态。[这就是为什么 intel_idle 中空闲状态“内部”表中大多数 MWAIT 提示值对应的退出延迟和目标驻留时间值反映的是包 C 状态的特性。] 如果完全不希望使用包 C 状态,则必须使用 PM QoS上文所述的 intel_idlemax_cstate 模块参数,以将允许的空闲状态范围限制为仅具有核级 MWAIT 提示值(如 C1)的状态。

参考资料