设备电源管理基础

版权:

© 2010-2011 Rafael J. Wysocki <rjw@sisk.pl>, Novell Inc.

版权:

© 2010 Alan Stern <stern@rowland.harvard.edu>

版权:

© 2016 英特尔公司 (Intel Corporation)

作者:

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

Linux 中的大部分代码是设备驱动程序,因此大部分 Linux 电源管理 (PM) 代码也是驱动程序特定的。大多数驱动程序只会做很少的工作;而其他驱动程序,尤其是用于电池容量小的平台(如手机)的驱动程序,则会做很多工作。

本文概述了驱动程序如何与系统范围的电源管理目标交互,重点介绍了连接到驱动程序模型核心的所有组件所共享的模型和接口。您可以将其作为对任何特定驱动程序进行领域特定工作的背景知识来阅读。

设备电源管理的两种模型

驱动程序将使用以下一种或两种模型来使设备进入低功耗状态

系统睡眠模型

驱动程序可以作为进入系统范围低功耗状态(如“挂起”(也称为“挂起到内存”))或(主要用于带磁盘的系统)“休眠”(也称为“挂起到磁盘”)的一部分,从而进入低功耗状态。

这是设备、总线和类驱动程序通过实现各种特定角色的挂起和恢复方法来协作完成的工作,以干净地关闭硬件和软件子系统电源,然后重新激活它们而不会丢失数据。

某些驱动程序可以管理硬件唤醒事件,这些事件使系统脱离低功耗状态。此功能可以使用相关的 /sys/devices/.../power/wakeup 文件启用或禁用(对于以太网驱动程序,也可以使用 ethtool 使用的 ioctl 接口);启用它可能会增加一些功耗,但可以让整个系统更频繁地进入低功耗状态。

运行时电源管理模型

设备也可以在系统运行时进入低功耗状态,原则上独立于其他电源管理活动。然而,设备通常不是相互独立的(例如,父设备不能被挂起,除非其所有子设备都已被挂起)。此外,根据设备所在的总线类型,可能需要为此目的对设备执行一些特定于总线的操作。在运行时进入低功耗状态的设备可能需要在系统范围的电源转换(挂起或休眠)期间进行特殊处理。

由于这些原因,不仅设备驱动程序本身,而且适当的子系统(总线类型、设备类型或设备类)驱动程序和 PM 核心都参与运行时电源管理。与系统睡眠电源管理情况一样,它们需要通过实现各种特定角色的挂起和恢复方法进行协作,以便硬件能够干净地关闭电源并重新激活,而不会丢失数据或服务。

关于这些低功耗状态,除了它们非常系统特定且通常设备特定之外,没有太多可说的。此外,如果足够的设备(在运行时)进入低功耗状态,其效果可能与进入某种系统范围的低功耗状态(系统睡眠)非常相似……并且存在协同效应,因此多个使用运行时 PM 的驱动程序可能使系统进入一个可以提供更深层次节能选项的状态。

大多数挂起的设备将停止所有 I/O:不再进行 DMA 或 IRQ(唤醒事件除外),不再读取或写入数据,并且不再接受来自上游驱动程序的请求。然而,特定的总线或平台可能具有不同的要求。

硬件唤醒事件的例子包括实时时钟的警报、网络远程唤醒 (wake-on-LAN) 数据包、键盘或鼠标活动,以及媒体插入或移除(对于 PCMCIA、MMC/SD、USB 等)。

进入系统睡眠状态的接口

为子系统(总线类型、设备类型、设备类)和设备驱动程序提供了编程接口,以允许它们参与其所关注设备的电源管理。这些接口涵盖系统睡眠和运行时电源管理。

设备电源管理操作

设备电源管理操作,无论是子系统级别还是设备驱动程序级别,都是通过定义和填充在 include/linux/pm.h 中定义的 struct dev_pm_ops 类型的对象来实现的。其中包含的方法的作用将在下文中解释。目前,只需记住最后三个方法是针对运行时电源管理的,而其余方法则用于系统范围的电源转换。

对于至少某些子系统,还存在一种已弃用的“旧式”或“传统”电源管理操作接口。这种方法不使用 struct dev_pm_ops 对象,并且仅适用于以有限方式实现系统睡眠电源管理方法。因此,本文档不对此进行描述,请直接查阅源代码以获取更多信息。

子系统级别方法

挂起和恢复设备的核心方法位于 struct dev_pm_ops 中,该结构由 struct dev_pm_domainops 成员指向,或由 struct bus_typestruct device_typestruct classpm 成员指向。它们主要与为平台和总线(如 PCI 或 USB)编写基础设施的人员,或设备类型和设备类驱动程序的编写者相关。它们也与其子系统(PM 域、设备类型、设备类和总线类型)未提供所有电源管理方法的设备驱动程序的编写者相关。

总线驱动程序根据硬件和使用它的驱动程序,实现这些方法;PCI 的工作方式与 USB 不同,等等。编写子系统级别驱动程序的人不多;大多数驱动程序代码都是建立在总线特定框架代码之上的“设备驱动程序”。

有关这些驱动程序调用的更多信息,请参阅后面的描述;它们按阶段为每个设备调用,遵循驱动程序模型树中的父子顺序。

/sys/devices/.../power/wakeup 文件

驱动程序模型中的所有设备对象都包含控制系统唤醒事件(可强制系统脱离睡眠状态的硬件信号)处理的字段。这些字段由总线或设备驱动程序代码使用 device_set_wakeup_capable()device_set_wakeup_enable() 初始化,这些函数定义在 include/linux/pm_wakeup.h 中。

power.can_wakeup 标志仅记录设备(及其驱动程序)是否物理上支持唤醒事件。device_set_wakeup_capable() 例程会影响此标志。power.wakeup 字段是指向 struct wakeup_source 类型对象的指针,用于控制设备是否应使用其系统唤醒机制以及通知 PM 核心设备发出的系统唤醒事件。此对象仅存在于支持唤醒的设备(即 can_wakeup 标志已设置的设备)中,并由 device_set_wakeup_capable() 创建(或移除)。

设备是否能够发出唤醒事件是一个硬件问题,内核负责跟踪它。相比之下,支持唤醒的设备是否应该发出唤醒事件是一个策略决定,它由用户空间通过一个 sysfs 属性来管理:power/wakeup 文件。用户空间可以向其写入“enabled”或“disabled”字符串,分别表示设备是否应发出系统唤醒信号。此文件仅在给定设备存在 power.wakeup 对象时才存在,并由 device_set_wakeup_capable() 随该对象一起创建(或移除)。从文件中读取将返回相应的字符串。

power/wakeup 文件中的初始值对于大多数设备来说是“disabled”;主要例外是电源按钮、键盘以及通过 ethtool 设置了 WoL (wake-on-LAN) 功能的以太网适配器。对于不自行生成唤醒请求但仅将唤醒请求从一个总线转发到另一个总线(如 PCI Express 端口)的设备,它也应默认为“enabled”。

device_may_wakeup() 例程仅在 power.wakeup 对象存在且对应的 power/wakeup 文件包含“enabled”字符串时才返回 true。此信息被子系统(如 PCI 总线类型代码)用于判断是否启用设备的唤醒机制。如果设备唤醒机制直接由驱动程序启用或禁用,它们也应该使用 device_may_wakeup() 来决定在系统睡眠转换期间做什么。然而,在任何情况下,设备驱动程序都不应直接调用 device_set_wakeup_enable()

应该注意的是,系统唤醒在概念上与运行时电源管理使用的“远程唤醒”不同,尽管它们可能由相同的物理机制支持。远程唤醒是一种功能,允许处于低功耗状态的设备触发特定中断,以指示它们应进入全功耗状态的条件。这些中断可能用于也可能不用于发出系统唤醒事件信号,具体取决于硬件设计。在某些系统上,无法从系统睡眠状态触发它们。无论如何,对于所有支持远程唤醒的设备和驱动程序,都应始终为运行时电源管理启用远程唤醒。

/sys/devices/.../power/control 文件

驱动程序模型中的每个设备都有一个标志来控制其是否受运行时电源管理的约束。此标志 runtime_auto 由总线类型(或一般子系统)代码使用 pm_runtime_allow()pm_runtime_forbid() 初始化;默认情况下允许运行时电源管理。

用户空间可以通过向设备的 power/control sysfs 文件写入“on”或“auto”来调整设置。写入“auto”会调用 pm_runtime_allow(),设置标志并允许设备由其驱动程序进行运行时电源管理。写入“on”会调用 pm_runtime_forbid(),清除标志,如果设备处于低功耗状态,则将其恢复到全功耗状态,并阻止设备进行运行时电源管理。用户空间可以通过读取该文件来检查 runtime_auto 标志的当前值。

设备的 runtime_auto 标志对系统范围的电源转换处理没有影响。特别是,即使设备的 runtime_auto 标志未设置,设备也可以(并且在大多数情况下应该且将)在系统范围转换到睡眠状态期间进入低功耗状态。

有关运行时电源管理框架的更多信息,请参阅I/O 设备的运行时电源管理框架

调用驱动程序进入和退出系统睡眠状态

当系统进入睡眠状态时,会要求每个设备的驱动程序通过将设备置于与目标系统状态兼容的状态来挂起设备。这通常是某种形式的“关闭”状态,但具体细节因系统而异。此外,启用唤醒功能的设备通常会保持部分功能,以便唤醒系统。

当系统脱离低功耗状态时,会要求设备的驱动程序通过将其恢复到全功耗状态来唤醒它。挂起和恢复操作总是同时进行,并且两者都是多阶段操作。

对于简单的驱动程序,挂起可能会使用类代码停止设备,然后在 suspend_noirq 期间尽可能地“关闭”其硬件。匹配的恢复调用将完全重新初始化硬件,然后重新激活其类 I/O 队列。

更具电源意识的驱动程序可能会准备设备以触发系统唤醒事件。

调用序列保证

为了确保当设备挂起或恢复时,需要与设备通信的桥接器和类似链接可用,设备层次结构以自底向上的顺序遍历以挂起设备。恢复这些设备时则使用自顶向下的顺序。

设备层次结构的顺序由设备注册的顺序定义:子设备永远不能在其父设备之前注册、探测或恢复;也不能在其父设备之后移除或挂起。

策略是设备层次结构应与硬件总线拓扑匹配。[或者至少是控制总线,对于使用多个总线的设备。] 特别是,这意味着如果设备的父设备正在挂起(即已被 PM 核心选为下一个要挂起的设备)或已经挂起,以及所有其他设备都已挂起之后,设备注册可能会失败。设备驱动程序必须准备好应对这种情况。

系统电源管理阶段

挂起或恢复系统分几个阶段进行。不同的阶段用于空闲挂起 (suspend-to-idle)、浅层(待机)和深度(“挂起到内存”)睡眠状态以及休眠状态(“挂起到磁盘”)。每个阶段在下一阶段开始之前,涉及为每个设备执行回调。并非所有总线或类都支持所有这些回调,也并非所有驱动程序都使用所有回调。各个阶段总是在任务被冻结之后和解冻之前运行。此外,*_noirq 阶段在 IRQ 处理程序被禁用时运行(除了那些标记有 IRQF_NO_SUSPEND 标志的)。

所有阶段都使用 PM 域、总线、类型、类或驱动程序回调(即,定义在 dev->pm_domain->opsdev->bus->pmdev->type->pmdev->class->pmdev->driver->pm 中的方法)。PM 核心认为这些回调是互斥的。此外,PM 域回调总是优先于所有其他回调,例如,类型回调优先于总线、类和驱动程序回调。具体来说,以下规则用于确定在给定阶段执行哪个回调:

  1. 如果 dev->pm_domain 存在,PM 核心将选择由 dev->pm_domain->ops 提供回调执行。

  2. 否则,如果 dev->typedev->type->pm 都存在,将选择由 dev->type->pm 提供回调执行。

  3. 否则,如果 dev->classdev->class->pm 都存在,将选择由 dev->class->pm 提供回调执行。

  4. 否则,如果 dev->busdev->bus->pm 都存在,将选择由 dev->bus->pm 提供回调执行。

这允许 PM 域和设备类型在必要时覆盖由总线类型或设备类提供的回调。

PM 域、类型、类和总线回调反过来可以调用存储在 dev->driver->pm 中的设备或驱动程序特定方法,但它们并非必须这样做。

如果选择执行的子系统回调不存在,PM 核心将转而执行 dev->driver->pm 集合中相应的(如果存在)方法。

进入系统挂起状态

当系统进入冻结 (freeze)、待机 (standby) 或内存睡眠 (memory sleep) 状态时,阶段包括:preparesuspendsuspend_latesuspend_noirq

  1. prepare 阶段旨在通过阻止新设备注册来防止竞争条件;如果新子设备可以随意注册,PM 核心将永远无法知道设备的所有子设备是否已挂起。[相比之下,从 PM 核心的角度来看,设备可以随时注销。] 与其他挂起相关阶段不同,在 prepare 阶段,设备层次结构是自顶向下遍历的。

    ->prepare 回调方法返回后,不能在该设备下注册新的子设备。该方法也可以以某种方式为即将到来的系统电源转换准备设备或驱动程序,但不应将设备置于低功耗状态。此外,如果设备支持运行时电源管理,->prepare 回调方法不得更新其状态,以防以后需要从运行时挂起中恢复它。

    对于支持运行时电源管理的设备,prepare 回调的返回值可以用于指示 PM 核心,在设备的后代也都在运行时挂起的情况下,PM 核心可以安全地将设备保持在运行时挂起状态(如果已经处于运行时挂起)。即,如果 prepare 回调返回一个正数,并且设备的所有后代也出现这种情况,并且它们(包括设备本身)都处于运行时挂起状态,则 PM 核心将跳过 suspendsuspend_latesuspend_noirq 阶段以及所有这些设备后续设备恢复的所有相应阶段。在这种情况下,->complete 回调将是 ->prepare 回调之后下一个被调用的,并完全负责将设备置于适当的一致状态。

    请注意,即使设备已禁用运行时 PM,此直接完成过程也适用;只有运行时 PM 状态才重要。因此,如果设备具有系统睡眠回调但不支持运行时 PM,则其 prepare 回调绝不能返回正值。这是因为所有此类设备在最初都设置为运行时挂起且运行时 PM 已禁用。

    此功能也可以通过使用 DPM_FLAG_NO_DIRECT_COMPLETEDPM_FLAG_SMART_PREPARE 驱动程序电源管理标志来控制。[通常,它们是在驱动程序针对特定设备进行探测时,通过将它们传递给 dev_pm_set_driver_flags() 辅助函数来设置的。] 如果第一个标志已设置,PM 核心将不会对给定设备及其任何祖先应用上述直接完成过程。第二个标志(当设置时)通知中间层代码(总线类型、设备类型、PM 域、类)它应考虑驱动程序提供的 ->prepare 回调的返回值,并且只有当驱动程序的回调也返回正值时,它才能从其自己的 ->prepare 回调返回正值。

  2. ->suspend 方法应停止设备的 I/O 操作。它们还可以保存设备寄存器并将其置于适当的低功耗状态,具体取决于设备所在的总线类型,并且它们可以启用唤醒事件。

    然而,对于支持运行时电源管理的设备,子系统(特别是总线类型和 PM 域)提供的 ->suspend 方法必须遵循一项附加规则,即在其驱动程序的 ->suspend 方法被调用之前可以对设备执行的操作。具体来说,如果必要,它们可以通过为设备调用 pm_runtime_resume() 来使设备从运行时挂起中恢复,但它们此时不得以任何其他方式更新设备的状态(以防驱动程序需要在其 ->suspend 方法中使设备从运行时挂起中恢复)。实际上,PM 核心通过在发出 ->prepare 回调之前调用 pm_runtime_get_noresume()(并在发出 pm_runtime_put() 回调之后调用 ->complete 回调)来阻止子系统或驱动程序在这些时候将设备置于运行时挂起状态。

  3. 对于许多设备来说,将挂起操作分为“停止设备”和“保存设备状态”两个阶段很方便,在这种情况下,suspend_late 旨在执行后者。它总是在相关设备的运行时电源管理被禁用之后执行。

  4. suspend_noirq 阶段发生在 IRQ 处理程序被禁用之后,这意味着在回调方法运行时,驱动程序的中断处理程序将不会被调用。->suspend_noirq 方法应保存设备先前未保存的寄存器值,并最终将设备置于适当的低功耗状态。

    大多数子系统和设备驱动程序不需要实现此回调。然而,允许设备共享中断向量的总线类型(如 PCI)通常需要它;否则,驱动程序在挂起阶段可能会遇到错误,因为它在自己的设备设置为低功耗后,收到了由其他设备生成的共享中断。

在这些阶段结束时,驱动程序应已停止所有 I/O 事务(DMA、IRQs),保存了足够的(硬件所需)状态以便重新初始化或恢复先前状态,并将设备置于低功耗状态。在许多平台上,它们将关闭一个或多个时钟源;有时它们还会关闭电源或降低电压。[支持运行时 PM 的驱动程序可能已经执行了其中一些或所有步骤。]

如果 device_may_wakeup() 返回 true,则设备应准备好生成硬件唤醒信号,以便在系统处于睡眠状态时触发系统唤醒事件。例如,enable_irq_wake() 可能会识别连接到开关或其他外部硬件的 GPIO 信号,而 pci_enable_wake() 对 PCI PME 信号执行类似操作。

如果任何这些回调返回错误,系统将不会进入所需的低功耗状态。相反,PM 核心将通过恢复所有已挂起的设备来撤销其操作。

退出系统挂起状态

从冻结 (freeze)、待机 (standby) 或内存睡眠 (memory sleep) 中恢复时,阶段包括:resume_noirqresume_earlyresumecomplete

  1. ->resume_noirq 回调方法应在驱动程序中断处理程序被调用之前执行任何必要的操作。这通常意味着撤销 suspend_noirq 阶段的操作。如果总线类型允许设备共享中断向量(如 PCI),则该方法应将设备及其驱动程序置于一种状态,在该状态下,驱动程序可以识别设备是否是传入中断的来源(如果有),并正确处理它们。

    例如,PCI 总线类型的 ->pm.resume_noirq() 将设备置于全功耗状态(在 PCI 术语中为 D0)并恢复设备的标准配置寄存器。然后它调用设备驱动程序的 ->pm.resume_noirq() 方法来执行设备特定的操作。

  2. ->resume_early 方法应准备设备以执行恢复方法。这通常涉及撤销前面 suspend_late 阶段的操作。

  3. ->resume 方法应将设备恢复到其操作状态,以便它可以执行正常的 I/O。这通常涉及撤销 suspend 阶段的操作。

  4. complete 阶段应撤销 prepare 阶段的操作。因此,与其他恢复相关阶段不同,在 complete 阶段,设备层次结构是自底向上遍历的。

    然而请注意,一旦 ->resume 回调发生,新的子设备就可以在该设备下注册;无需等到 complete 阶段运行。

    此外,如果之前的 ->prepare 回调返回正数,设备可能在整个系统挂起和恢复过程中都保持在运行时挂起状态(其 ->suspend->suspend_late->suspend_noirq->resume_noirq->resume_early->resume 回调可能已被跳过)。在这种情况下,->complete 回调完全负责在系统挂起后(如有必要)将设备置于一致状态。[例如,它可能需要为此目的为设备排队一个运行时恢复请求。] 要检查是否是这种情况,->complete 回调可以查询设备的 power.direct_complete 标志。如果在 ->complete 回调运行时该标志已设置,则表示使用了直接完成机制,可能需要特殊操作才能使设备之后正常工作。

在这些阶段结束时,驱动程序应与挂起之前一样具有完整功能:可以使用 DMA 和 IRQ 执行 I/O,并且相关时钟已启用。

然而,这里的细节可能再次是平台特定的。例如,某些系统支持多种“运行”状态,恢复结束时生效的模式可能不是挂起之前的模式。这意味着某些时钟或电源的可用性发生了变化,这很容易影响驱动程序的工作方式。

驱动程序需要能够处理自所有挂起方法调用后已复位的硬件,例如通过完全重新初始化。这可能是最困难的部分,也是受 NDA 文档和芯片勘误表保护最多的部分。如果硬件状态自挂起执行以来没有改变,那是最简单的,但这只能在目标系统睡眠状态是 suspend-to-idle 时才能保证。对于其他系统睡眠状态,情况可能并非如此(对于 ACPI 定义的系统睡眠状态,如 S3,通常不是)。

驱动程序还必须准备好注意到在系统断电期间设备已被移除,只要物理上可能。PCMCIA、MMC、USB、Firewire、SCSI,甚至 IDE 都是常见总线类型的例子,在这些总线上,常见的 Linux 平台会看到此类移除。驱动程序如何注意到和处理此类移除的细节目前是总线特定的,并且通常涉及一个单独的线程。

这些回调可能会返回错误值,但 PM 核心将忽略此类错误,因为它除了在系统日志中打印它们之外,无法做任何事情。

进入休眠状态

系统休眠比进入睡眠状态更复杂,因为它涉及创建和保存系统映像。因此,休眠有更多的阶段,并使用不同的回调集。这些阶段总是在任务被冻结并且释放了足够的内存之后运行。

休眠的一般过程是停止所有设备(“freeze”),在一切稳定时创建系统内存映像,重新激活所有设备(“thaw”),将映像写入永久存储,最后关闭系统(“power off”)。用于完成此过程的阶段有:preparefreezefreeze_latefreeze_noirqthaw_noirqthaw_earlythawcompletepreparepoweroffpoweroff_latepoweroff_noirq

  1. prepare 阶段已在上面的“进入系统挂起状态”一节中讨论。

  2. ->freeze 方法应停止设备以使其不生成 IRQ 或 DMA,并且可能需要保存设备寄存器的值。然而,设备不必置于低功耗状态,为了节省时间最好不要这样做。此外,设备不应准备好生成唤醒事件。

  3. freeze_late 阶段类似于前面描述的 suspend_late 阶段,只是设备不应置于低功耗状态,也不应允许生成唤醒事件。

  4. freeze_noirq 阶段类似于前面讨论的 suspend_noirq 阶段,但同样设备不应置于低功耗状态,也不应允许生成唤醒事件。

此时创建系统映像。所有设备都应处于非活动状态,并且在此过程中内存内容应保持不受干扰,以便映像形成系统状态的原子快照。

  1. thaw_noirq 阶段类似于前面讨论的 resume_noirq 阶段。主要区别在于其方法可以假定设备与 freeze_noirq 阶段结束时处于相同状态。

  2. thaw_early 阶段类似于上面描述的 resume_early 阶段。其方法应(如有必要)撤销前面 freeze_late 阶段的操作。

  3. thaw 阶段类似于前面讨论的 resume 阶段。其方法应将设备恢复到操作状态,以便(如有必要)用于保存映像。

  4. complete 阶段已在上面的“退出系统挂起状态”一节中讨论。

此时,系统映像已保存,设备需要为即将到来的系统关机做好准备。这很像在将系统置于空闲挂起 (suspend-to-idle)、浅层或深度睡眠状态之前挂起它们,并且阶段也相似。

  1. prepare 阶段已在上面讨论。

  2. poweroff 阶段类似于 suspend 阶段。

  3. poweroff_late 阶段类似于 suspend_late 阶段。

  4. poweroff_noirq 阶段类似于 suspend_noirq 阶段。

->poweroff->poweroff_late->poweroff_noirq 回调应分别执行与 ->suspend->suspend_late->suspend_noirq 回调基本相同的事情。一个显著的区别是它们不需要存储设备寄存器值,因为这些寄存器应该已经在 freezefreeze_latefreeze_noirq 阶段存储过了。此外,在许多机器上,固件将关闭整个系统的电源,因此回调不必将设备置于低功耗状态。

退出休眠状态

从休眠中恢复,再次比从主内存内容保留的睡眠状态中恢复更复杂,因为它需要将系统映像加载到内存中,并在控制权传回映像内核之前恢复休眠前的内存内容。

尽管原则上映像可以由引导加载程序加载到内存中并恢复休眠前的内存内容,但实际上这无法完成,因为引导加载程序不够智能,并且没有用于传递必要信息的既定协议。因此,引导加载程序会将一个全新的内核实例(称为“恢复内核”)加载到内存中,并以常规方式将控制权传递给它。然后,恢复内核读取系统映像,恢复休眠前的内存内容,并将控制权传递给映像内核。因此,从休眠中恢复涉及两个不同的内核实例。实际上,恢复内核可能与映像内核完全不同:不同的配置甚至不同的版本。这对设备驱动程序及其子系统具有重要影响。

为了能够将系统映像加载到内存中,恢复内核需要至少包含一部分设备驱动程序,使其能够访问包含映像的存储介质,尽管它不需要包含映像内核中存在的所有驱动程序。映像加载后,由引导内核管理的设备需要准备好将控制权传回映像内核。这与创建系统映像的初始步骤非常相似,并且以相同的方式完成,使用 preparefreezefreeze_noirq 阶段。然而,受这些阶段影响的设备仅是那些在恢复内核中具有驱动程序的设备;其他设备将仍然处于引导加载程序留下的任何状态。

如果休眠前内存内容的恢复失败,恢复内核将按照上述“解冻”过程,使用 thaw_noirqthaw_earlythawcomplete 阶段,然后继续正常运行。这种情况很少发生。大多数情况下,休眠前内存内容会成功恢复,控制权被传递给映像内核,然后由映像内核负责将系统恢复到工作状态。

为此,映像内核必须恢复设备的休眠前功能。此操作很像从睡眠状态中唤醒(内存内容已保留),尽管它涉及不同的阶段:restore_noirqrestore_earlyrestorecomplete

  1. restore_noirq 阶段类似于 resume_noirq 阶段。

  2. restore_early 阶段类似于 resume_early 阶段。

  3. restore 阶段类似于 resume 阶段。

  4. complete 阶段已在上面讨论。

resume[_early|_noirq] 的主要区别在于 restore[_early|_noirq] 必须假定设备已被引导加载程序或恢复内核访问和重新配置。因此,设备的状态可能与 freezefreeze_latefreeze_noirq 阶段记住的状态不同。设备甚至可能需要重置并完全重新初始化。在许多情况下,这种差异无关紧要,因此 ->resume[_early|_noirq]->restore[_early|_norq] 方法指针可以设置为相同的例程。然而,在实际存在差异的情况下,会使用不同的回调指针。

电源管理通知器

有些操作无法通过上面讨论的电源管理回调执行,因为回调发生得太晚或太早。为了处理这些情况,子系统和设备驱动程序可以注册电源管理通知器,这些通知器在任务冻结之前和解冻之后被调用。一般来说,PM 通知器适用于执行需要用户空间可用,或者至少不会干扰用户空间的操作。

详情请参阅挂起/休眠通知器

设备低功耗(挂起)状态

设备低功耗状态不是标准化的。一个设备可能只处理“开”和“关”,而另一个设备可能支持十几种不同版本的“开”(有多少引擎处于活动状态?),再加上一个比从完全“关”状态更快恢复到“开”状态的模式。

某些总线定义了不同挂起状态的含义。PCI 提供了一个示例:挂起序列完成后,非传统 PCI 设备不得执行 DMA 或发出 IRQ,并且其发出的任何唤醒事件都将通过 PME# 总线信号发出。此外,还有几种 PCI 标准设备状态,其中一些是可选的。

相比之下,集成的片上系统处理器通常使用 IRQ 作为唤醒事件源(因此驱动程序会调用 enable_irq_wake()),并且可能能够将 DMA 完成视为唤醒事件(有时 DMA 也可以保持活动状态,只有 CPU 和某些外设进入睡眠)。

这里的一些细节可能是平台特定的。系统可能具有在某些睡眠状态下可以完全活动的设备,例如在系统大部分处于轻度睡眠时使用 DMA 刷新的 LCD 显示屏……并且其帧缓冲区甚至可能由 DSP 或其他非 Linux CPU 更新,而 Linux 控制处理器则保持空闲。

此外,所采取的具体行动可能取决于目标系统状态。一个目标系统状态可能允许给定设备非常活跃;另一个可能需要硬关机并在恢复时重新初始化。两个不同的目标系统可能会以不同的方式使用同一设备;上述 LCD 可能在一个产品的“待机”状态下处于活动状态,但使用相同 SOC 的不同产品可能工作方式不同。

设备电源管理域

有时设备共享参考时钟或其他电源资源。在这些情况下,通常不可能单独将设备置于低功耗状态。相反,可以通过关闭共享电源资源,将共享同一电源资源的一组设备同时置于低功耗状态。当然,它们也需要通过打开共享电源资源,一起置于全功耗状态。具有此属性的一组设备通常被称为电源域。电源域也可以嵌套在另一个电源域中。嵌套域被称为父域的子域。

电源域的支持通过 struct devicepm_domain 字段提供。此字段是指向在 include/linux/pm.h 中定义的 struct dev_pm_domain 类型对象的指针,该对象提供一组电源管理回调,类似于在所有电源转换期间为给定设备执行的子系统级别和设备驱动程序回调,而不是各自的子系统级别回调。具体来说,如果设备的 pm_domain 指针不为 NULL,则将执行其指向对象中的 ->suspend() 回调,而不是其子系统(例如总线类型)的 ->suspend() 回调,所有剩余回调也类似。换句话说,如果为给定设备定义了电源管理域回调,它们总是优先于设备子系统(例如总线类型)提供的回调。

设备电源管理域的支持仅与需要使用相同设备驱动程序电源管理回调的多种不同电源域配置的平台相关,并且希望避免将电源域的支持整合到子系统级别回调中,例如通过修改平台总线类型。其他平台无需实现或以任何方式考虑它。

设备可以定义为 IRQ-safe,这向 PM 核心表明它们的运行时 PM 回调可以在中断禁用时被调用(有关更多信息,请参阅I/O 设备的运行时电源管理框架)。如果一个 IRQ-safe 设备属于一个 PM 域,那么该域的运行时 PM 将被禁止,除非该域本身被定义为 IRQ-safe。然而,只有当域中的所有设备都是 IRQ-safe 时,将 PM 域定义为 IRQ-safe 才具有意义。此外,如果一个 IRQ-safe 域有一个父域,那么只有当父域本身也是 IRQ-safe 时,才允许父域的运行时 PM,并附加限制是 IRQ-safe 父域的所有子域也必须是 IRQ-safe。

运行时电源管理

许多设备能够在系统运行时动态断电。此功能对于不使用的设备很有用,并且可以在运行中的系统上显着节省电量。这些设备通常支持一系列运行时电源状态,可能使用“关闭”、“睡眠”、“空闲”、“活动”等名称。在某些情况下(如 PCI),这些状态将受到设备所用总线的限制,并且通常会包含在系统睡眠状态中使用的硬件状态。

在某些设备由于运行时电源管理而处于低功耗状态时,可以启动系统范围的电源转换。系统睡眠 PM 回调应识别此类情况并适当地对其作出反应,但必要的行动是子系统特定的。

在某些情况下,决策可能在子系统级别做出,而在其他情况下,设备驱动程序可能被留待决定。在某些情况下,在系统范围的电源转换期间,可能希望将挂起的设备保持在该状态,但在其他情况下,设备必须暂时恢复到全功耗状态,例如,以便禁用其系统唤醒功能。这一切都取决于硬件以及所涉子系统和设备驱动程序的设计。

如果在于系统范围转换为睡眠状态期间需要从运行时挂起恢复设备,可以通过从设备的驱动程序或其子系统(例如,总线类型或 PM 域)的 ->suspend 回调(或与休眠相关的转换的 ->freeze->poweroff 回调)中调用 pm_runtime_resume() 来完成。然而,子系统不得以其他方式在调用设备驱动程序的 ->suspend 回调(或等效回调)*之前*,从其 ->prepare->suspend 回调(或等效回调)中更改设备的运行时状态。

DPM_FLAG_SMART_SUSPEND 驱动程序标志

某些总线类型和 PM 域的策略是在其 ->suspend 回调中预先从运行时挂起恢复所有设备,但如果设备的驱动程序能够处理运行时挂起的设备,则可能没有这个必要。驱动程序可以在探测时,在 dev_pm_set_driver_flags() 辅助例程的帮助下,通过在 power.driver_flags 中设置 DPM_FLAG_SMART_SUSPEND 来指示这一点。

设置该标志会导致 PM 核心和中间层代码(总线类型、PM 域等)跳过驱动程序提供的 ->suspend_late->suspend_noirq 回调,前提是设备在整个系统范围挂起(以及系统休眠的“freeze”和“poweroff”部分)的这些阶段中仍处于运行时挂起状态。[否则,同一个驱动程序回调可能会对同一个设备连续执行两次,这通常是无效的。] 如果设备的中间层系统范围 PM 回调存在,则它们负责跳过这些驱动程序回调;如果不存在,则由 PM 核心跳过它们。子系统回调例程可以通过测试 dev_pm_skip_suspend() 辅助函数的返回值来确定是否需要跳过驱动程序回调。

此外,在设置了 DPM_FLAG_SMART_SUSPEND 的情况下,如果设备在整个前面的“freeze”转换期间一直处于运行时挂起状态,则驱动程序的 ->thaw_noirq->thaw_early 回调在休眠期间会被跳过。同样,如果设备存在中间层回调,则它们负责执行此操作,否则由 PM 核心处理。

DPM_FLAG_MAY_SKIP_RESUME 驱动程序标志

在系统从睡眠状态进行系统范围恢复时,最简单的方法是将设备置于全功耗状态,如I/O 设备的运行时电源管理框架中所述。[有关此特定问题以及设备运行时电源管理框架的一般信息,请参阅该文档。] 然而,在系统转换为工作状态后,通常希望将设备保持在挂起状态,特别是如果这些设备在之前的系统范围挂起(或类似)转换之前处于运行时挂起状态。

为此,设备驱动程序可以使用 DPM_FLAG_MAY_SKIP_RESUME 标志来向 PM 核心和中间层代码表明,如果设备在系统范围 PM 转换到工作状态后可以保持挂起状态,则允许跳过其“noirq”和“early”恢复回调。这是否可行通常取决于设备在给定系统挂起-恢复周期之前的状态以及正在进行的系统转换类型。特别是,与休眠相关的“thaw”和“restore”转换完全不受 DPM_FLAG_MAY_SKIP_RESUME 的影响。[在“restore”转换期间,无论标志设置如何,所有回调都会发出;而在“thaw”转换期间是否跳过任何驱动程序回调,则取决于 DPM_FLAG_SMART_SUSPEND 标志是否设置(参见上文)。此外,如果设备的任何子设备将恢复到全功耗状态,则不允许设备保持运行时挂起状态。]

DPM_FLAG_MAY_SKIP_RESUME 标志结合 PM 核心在挂起类型转换的“suspend”阶段设置的 power.may_skip_resume 状态位一起考虑。如果驱动程序或中间层有理由阻止驱动程序的“noirq”和“early”恢复回调在随后的系统恢复转换期间被跳过,它应该在其 ->suspend->suspend_late->suspend_noirq 回调中清除 power.may_skip_resume。[请注意,设置 DPM_FLAG_SMART_SUSPEND 的驱动程序需要在其 ->suspend 回调中清除 power.may_skip_resume,以防另外两个回调被跳过。]

设置 power.may_skip_resume 状态位以及 DPM_FLAG_MAY_SKIP_RESUME 标志对于跳过驱动程序的“noirq”和“early”恢复回调是必要的,但通常不充分。是否应该跳过它们可以通过评估 dev_pm_skip_resume() 辅助函数来确定。

如果该函数返回 true,则应跳过驱动程序的“noirq”和“early”恢复回调,并且设备的运行时 PM 状态将被 PM 核心设置为“suspended”。否则,如果设备在之前的系统范围挂起转换期间处于运行时挂起状态并且其 DPM_FLAG_SMART_SUSPEND 已设置,则其运行时 PM 状态将由 PM 核心设置为“active”。[因此,未设置 DPM_FLAG_SMART_SUSPEND 的驱动程序不应期望其设备的运行时 PM 状态在系统范围恢复类型转换期间由 PM 核心从“suspended”更改为“active”。]

如果设备的 DPM_FLAG_MAY_SKIP_RESUME 标志未设置,但 DPM_FLAG_SMART_SUSPEND 已设置且驱动程序的“late”和“noirq”挂起回调被跳过,则其系统范围的“noirq”和“early”恢复回调(如果存在)将照常调用,并且设备的运行时 PM 状态在为其启用运行时 PM 之前由 PM 核心设置为“active”。在这种情况下,驱动程序必须准备好应对其系统范围恢复回调与 ->runtime_suspend 回调背靠背调用(没有中间的 ->runtime_resume 和系统范围挂起回调)的情况,并且设备的最终状态在这种情况下必须反映“active”的运行时 PM 状态。[请注意,如果驱动程序的 ->suspend_late 回调指针指向与其 ->runtime_suspend 回调相同的函数,并且其 ->resume_early 回调指针指向与其 ->runtime_resume 回调相同的函数,而驱动程序的其他系统范围挂起-恢复回调都不存在,例如,这完全不是问题。]

同样,如果设备的 DPM_FLAG_MAY_SKIP_RESUME 已设置,其驱动程序的系统范围“noirq”和“early”恢复回调可能被跳过,而其“late”和“noirq”挂起回调可能已执行(原则上,无论 DPM_FLAG_SMART_SUSPEND 是否设置)。在这种情况下,驱动程序需要能够应对其 ->runtime_resume 回调与其“late”和“noirq”挂起回调背靠背调用的情况。[例如,如果驱动程序同时设置了 DPM_FLAG_SMART_SUSPENDDPM_FLAG_MAY_SKIP_RESUME,并对运行时 PM 和系统范围挂起/恢复使用相同的挂起/恢复回调函数对,则这不是一个问题。]