关于低功耗状态,除了它们非常依赖于系统和设备之外,很难有更多的说法。此外,如果足够多的设备在运行时进入低功耗状态,其效果可能与进入某种系统范围的低功耗状态(系统休眠)非常相似...而且存在协同效应,因此使用运行时电源管理的几个驱动程序可能会使系统进入更深层次的节能选项。
大多数挂起的设备将停止所有I/O操作:不再进行DMA或IRQ操作(除了唤醒事件),不再读取或写入数据,并且不再接受来自上游驱动程序的请求。但是,不同的总线或平台可能具有不同的要求。
硬件唤醒事件的示例包括来自实时时钟的闹钟、网络的唤醒LAN数据包、键盘或鼠标活动,以及介质的插入或拔出(适用于PCMCIA、MMC/SD、USB等)。
设备电源管理操作在子系统级别和设备驱动程序级别都是通过定义和填充在include/linux/pm.h中定义的structdev_pm_ops类型的对象来实现的。接下来将解释其中包含的方法的作用。目前,只需记住最后三个方法是特定于运行时电源管理,而其余方法用于系统范围的电源转换。
此外,至少对于某些子系统,还存在一种已弃用的“旧”或“传统”接口用于电源管理操作。这种方法不使用structdev_pm_ops对象,仅适用于以有限方式实现系统休眠电源管理方法。因此,本文档中未对其进行描述,请直接参考源代码以获取更多信息。
总线驱动程序根据硬件和使用它的驱动程序适当地实现这些方法;PCI与USB的工作方式不同,等等。很少有人编写子系统级别的驱动程序;大多数驱动程序代码是构建在特定总线框架代码之上的“设备驱动程序”。
有关这些驱动程序调用的更多信息,请参阅后面的描述;它们按照驱动程序模型树中的父子顺序为每个设备调用不同的阶段。
驱动模型中的所有设备对象都包含控制系统唤醒事件处理的字段(硬件信号,可以强制系统退出睡眠状态)。这些字段由总线或设备驱动程序代码使用device_set_wakeup_capable()和device_set_wakeup_enable()进行初始化,这些函数定义在include/linux/pm_wakeup.h中。
power.can_wakeup标志仅记录设备(及其驱动程序)是否可以物理支持唤醒事件。device_set_wakeup_capable()例程会影响此标志。power.wakeup字段是指向structwakeup_source类型对象的指针,用于控制设备是否应该使用其系统唤醒机制,并通知PM核心设备发出的系统唤醒事件。此对象仅适用于具有唤醒能力的设备(即设置了can_wakeup标志的设备),并且由device_set_wakeup_capable()创建(或移除)。
设备是否能够发出唤醒事件是一个硬件问题,内核负责跟踪。相比之下,唤醒能力设备是否应该发出唤醒事件是一个策略决定,由用户空间通过sysfs属性进行管理:即power/wakeup文件。用户空间可以向其写入“enabled”或“disabled”字符串,分别表示设备是否应该发出系统唤醒信号。如果给定设备的power.wakeup对象存在,该文件也存在,并且由device_set_wakeup_capable()与该对象一同创建(或移除)。从文件中读取将返回相应的字符串。
对于大多数设备,power/wakeup文件的初始值为“disabled”;主要例外是已使用ethtool设置了WoL(唤醒LAN)功能的电源按钮、键盘和以太网适配器。对于那些不会自行生成唤醒请求,而只是从一个总线转发唤醒请求到另一个总线的设备(如PCIExpress端口),它也应默认为“enabled”。
device_may_wakeup()例程仅在power.wakeup对象存在且相应的power/wakeup文件包含“enabled”字符串时返回true。此信息由子系统(如PCI总线类型代码)使用,以查看是否启用设备的唤醒机制。如果设备唤醒机制由驱动程序直接启用或禁用,它们也应使用device_may_wakeup()来决定在系统睡眠转换期间该做什么。但是,无论如何,不希望设备驱动程序直接调用device_set_wakeup_enable()。
需要注意的是,系统唤醒在概念上与运行时电源管理中使用的“远程唤醒”是不同的,尽管它可能由相同的物理机制支持。远程唤醒是一种功能,允许处于低功耗状态的设备触发特定中断,以信号应将其置于全功率状态的条件。这些中断可能会或可能不会用于发出系统唤醒事件,这取决于硬件设计。在某些系统中,不可能从系统睡眠状态触发它们。无论如何,对于所有支持它的设备和驱动程序,远程唤醒应始终对运行时电源管理启用。
有关运行时电源管理框架的更多信息,请参考《I/O设备的运行时电源管理框架》。
当系统离开低功耗状态时,会要求设备的驱动程序将其恢复到全功率状态。挂起和恢复操作总是一起进行的,并且都是多阶段操作。
对于简单的驱动程序,挂起操作可能会使用类代码使设备静止,并在suspend_noirq期间尽可能关闭其硬件。然后,匹配的恢复调用将在重新激活其类I/O队列之前完全重新初始化硬件。
更具电源感知能力的驱动程序可能会准备设备以触发系统唤醒事件。
系统的挂起或恢复是通过几个阶段完成的。不同的阶段用于挂起到空闲、浅层(待机)和深度(“挂起到RAM”)睡眠状态以及休眠状态(“挂起到磁盘”)。每个阶段都涉及在下一个阶段开始之前执行每个设备的回调。并非所有总线或类别都支持所有这些回调,也并非所有驱动程序都使用所有这些回调。各个阶段始终在任务被冻结后运行,并在它们被解冻之前运行。此外,*_noirq阶段在禁用IRQ处理程序的时候运行(除了那些标记有IRQF_NO_SUSPEND标志的处理程序)。
所有阶段都使用PM域、总线、类型、类别或驱动程序回调(即在dev->pm_domain->ops、dev->bus->pm、dev->type->pm、dev->class->pm或dev->driver->pm中定义的方法)。这些回调被PM核心视为互斥的。此外,PM域回调始终优先于所有其他回调,例如,类型回调优先于总线、类别和驱动程序回调。具体来说,以下规则用于确定在给定阶段执行哪个回调:
这使得PM域和设备类型可以在必要时覆盖总线类型或设备类别提供的回调。
PM域、类型、类别和总线回调可能反过来调用存储在dev->driver->pm中的设备或驱动程序特定方法,但它们不一定要这样做。
如果选择执行的子系统回调不存在,PM核心将执行dev->driver->pm集合中的相应方法。
当系统进入冻结、待机或内存睡眠状态时,各个阶段为:准备(prepare)、挂起(suspend)、挂起晚期(suspend_late)、挂起无IRQ(suspend_noirq)。
在这些阶段结束时,驱动程序应该已经停止了所有I/O事务(DMA、IRQ),保存了足够的状态,以便它们可以重新初始化或恢复先前的状态(硬件所需),并将设备置于低功耗状态。在许多平台上,它们将关闭一个或多个时钟源;有时它们还会关闭电源或降低电压。[支持运行时PM的驱动程序可能已经执行了这些步骤的一些或全部。]
如果device_may_wakeup()返回true,则应准备设备以生成硬件唤醒信号,以在系统处于睡眠状态时触发系统唤醒事件。例如,enable_irq_wake()可能会识别连接到开关或其他外部硬件的GPIO信号,而pci_enable_wake()对PCIPME信号执行类似的操作。
如果这些回调中的任何一个返回错误,系统将不会进入所需的低功耗状态。相反,PM核心将通过恢复所有被挂起的设备来撤消其操作。
当从冻结、待机或内存睡眠状态恢复时,各个阶段为:恢复无IRQ(resume_noirq)、早期恢复(resume_early)、恢复(resume)、完成(complete)。
但是,这里的细节可能再次是特定于平台的。例如,某些系统支持多个“运行”状态,并且在恢复结束时生效的模式可能不是挂起之前的模式。这意味着某些时钟或电源供应的可用性发生了变化,这可能会轻易影响驱动程序的工作。
驱动程序需要能够处理自挂起方法被调用以来已被重置的硬件,例如通过完全重新初始化。这可能是最困难的部分,并且是由NDA文档和芯片勘误所保护的部分。如果目标系统睡眠进入了挂起到空闲状态,那么只有在这种情况下才能保证硬件状态自挂起以来没有发生变化。对于可能不是这种情况的其他系统睡眠状态(通常不是ACPI定义的系统睡眠状态,如S3),无法保证这一点。
驱动程序还必须准备好注意到设备在系统关闭电源时已被移除,无论在物理上是否可能。PCMCIA、MMC、USB、Firewire、SCSI,甚至IDE都是常见的Linux平台可能会看到此类移除的总线示例。驱动程序将如何注意到和处理此类移除的细节目前是特定于总线的,并且通常涉及一个单独的线程。
这些回调可能返回错误值,但是PM核心将忽略这些错误,因为除了在系统日志中打印它们之外,它无法对它们做任何事情。
将系统置于休眠状态比将其置于睡眠状态更复杂,因为它涉及创建和保存系统映像。因此,休眠有更多的阶段,具有不同的回调集。这些阶段总是在任务被冻结并释放了足够的内存之后运行。
休眠的一般过程是:静默所有设备("冻结"),在一切稳定时创建系统内存的映像,重新激活所有设备("解冻"),将映像写入永久存储,最后关闭系统("关机")。用于完成此过程的阶段有:准备、冻结、冻结晚期、冻结无IRQ、解冻无IRQ、解冻早期、解冻、完成、准备、关机、关机晚期、关机无IRQ。
此时创建了系统映像。在此期间,所有设备应处于非活动状态,并且在此期间内存的内容应保持不变,以便映像形成系统状态的原子快照。
此时保存了系统映像,然后需要准备设备以进行即将到来的系统关闭。这与将系统置于挂起到空闲、浅度睡眠或深度睡眠状态之前暂停它们的方式非常相似,阶段也相似。
->poweroff、->poweroff_late和->poweroff_noirq回调应该做的事情与->suspend、->suspend_late和->suspend_noirq回调基本相同。值得注意的是,它们不需要存储设备寄存器的值,因为在冻结、冻结晚期或冻结无IRQ阶段应该已经存储了寄存器的值。此外,在许多机器上,固件将关闭整个系统,因此回调不需要将设备置于低功耗状态。
从休眠中恢复与从保留主存储器内容的睡眠状态中恢复相比,更复杂,因为它需要将系统映像加载到内存中,并在将控制权传递回映像内核之前恢复休眠前的内存内容。
尽管原则上,映像可能会由引导加载程序加载到内存中,并恢复休眠前的内存内容,但实际上这是不可能的,因为引导加载程序不够智能,也没有建立的协议来传递必要的信息。因此,引导加载程序将一个新的内核实例(称为"恢复内核")加载到内存中,并以通常的方式将控制权传递给它。然后,恢复内核读取系统映像,恢复休眠前的内存内容,并将控制权传递给映像内核。因此,在从休眠中恢复时涉及两个不同的内核实例。实际上,恢复内核可能与映像内核完全不同:不同的配置甚至不同的版本。这对设备驱动程序及其子系统有重要影响。
为了能够将系统映像加载到内存中,恢复内核需要包含至少一部分设备驱动程序,以使其能够访问包含映像的存储介质,尽管它不需要包含映像内核中存在的所有驱动程序。在加载映像后,由引导内核管理的设备需要准备好将控制权传递回映像内核。这与创建系统映像的初始步骤非常相似,并且以相同的方式完成,使用准备、冻结和冻结无IRQ阶段。但是,这些阶段影响的设备仅限于恢复内核中具有驱动程序的设备;其他设备仍处于引导加载程序留下的任何状态。
如果恢复休眠前的内存内容失败,恢复内核将按照上面描述的"解冻"过程进行,使用解冻无IRQ、解冻早期、解冻和完成阶段,然后继续正常运行。这种情况很少发生。通常情况下,休眠前的内存内容会成功恢复,并将控制权传递给映像内核,然后映像内核负责将系统恢复到工作状态。
为了实现这一点,映像内核必须恢复设备的休眠前功能。这个操作很像从睡眠状态唤醒(保留内存内容),尽管它涉及不同的阶段:恢复无IRQ、恢复早期、恢复、完成。
与resume[_early|_noirq]的主要区别在于,restore[_early|_noirq]必须假设设备已被引导加载程序或恢复内核访问和重新配置。因此,设备的状态可能与从冻结、冻结晚期和冻结无IRQ阶段记住的状态不同。设备甚至可能需要被重置和完全重新初始化。在许多情况下,这种差异并不重要,因此->resume[_early|_noirq]和->restore[_early|_norq]方法指针可以设置为相同的例程。然而,为了防止出现实际上确实重要的情况,使用不同的回调指针。
在讨论的电源管理回调中,有一些操作无法在回调发生得太晚或太早时执行。为了处理这些情况,子系统和设备驱动程序可以注册电源管理通知器,在任务被冻结之前和解冻之后调用它们。一般来说,电源管理通知器适合执行需要用户空间可用的操作,或者至少不会干扰用户空间的操作。
有关详细信息,请参阅挂起/休眠通知器。
设备的低功耗状态并不是标准的。一个设备可能只处理“开”和“关”,而另一个设备可能支持十几种不同版本的“开”(有多少个引擎是活动的?),再加上一个比完全“关”更快返回到“开”的状态。
一些总线定义了有关不同挂起状态的规则。PCI提供了一个例子:在挂起序列完成后,非传统PCI设备可能不会执行DMA或发出IRQ,并且它发出的任何唤醒事件都将通过PME#总线信号发出。此外,有几种PCI标准设备状态,其中一些是可选的。
相比之下,集成式系统芯片处理器通常使用IRQ作为唤醒事件源(因此驱动程序将调用enable_irq_wake()),并且可能能够将DMA完成视为唤醒事件(有时DMA也可以保持活动状态,只有CPU和一些外围设备会休眠)。
这里的一些细节可能是特定于平台的。系统可能有一些设备在某些睡眠状态下可以完全活动,比如使用DMA刷新的LCD显示器,而系统的大部分部分处于轻度休眠状态……并且其帧缓冲区甚至可能会被DSP或其他非LinuxCPU更新,而Linux控制处理器保持空闲。
此外,采取的具体操作可能取决于目标系统状态。一个目标系统状态可能允许给定设备非常操作;另一个可能需要在恢复时进行硬关机并重新初始化。而两个不同的目标系统可能以不同的方式使用相同的设备;前述的LCD在一个产品的“待机”状态下可能是活动的,但使用相同SOC的另一产品可能以不同的方式工作。
有时设备共享参考时钟或其他电源资源。在这些情况下,通常无法单独将设备置于低功耗状态。相反,通过关闭共享电源资源,一组共享电源资源的设备可以同时进入低功耗状态。当然,它们也需要一起进入全功率状态,通过打开共享电源资源。具有这种属性的一组设备通常被称为电源域。电源域也可以嵌套在另一个电源域中。嵌套域被称为父域的子域。
对电源域的支持是通过structdevice的pm_domain字段提供的。该字段是指向structdev_pm_domain类型对象的指针,该类型在include/linux/pm.h中定义,提供了一组与给定设备在所有电源转换期间执行的子系统级和设备驱动程序回调类似的电源管理回调。具体来说,如果设备的pm_domain指针不为NULL,则将执行指向它的对象的->suspend()回调,而不是其子系统的(例如总线类型的)->suspend()回调,对于所有其他回调也是如此。换句话说,如果为给定设备定义了电源管理域回调,则这些回调始终优先于设备子系统(例如总线类型)提供的回调。
设备电源管理域的支持仅适用于需要在许多不同的电源域配置中使用相同的设备驱动程序电源管理回调的平台,并且希望避免将对电源域的支持合并到子系统级回调中,例如通过修改平台总线类型。其他平台不需要实现它或以任何方式考虑它。
设备可能被定义为IRQ安全,这向PM核心指示其运行时PM回调可能在禁用中断时被调用(有关更多信息,请参阅I/O设备的运行时电源管理框架)。如果IRQ安全设备属于PM域,则除非PM域本身被定义为IRQ安全,否则将禁止PM域的运行时PM。但是,只有当所有该域中的设备都是IRQ安全的时,才有意义定义PM域为IRQ安全。此外,如果IRQ安全域有父域,则只有当父域本身也是IRQ安全的时,才允许父域的运行时PM,还有一个额外的限制是IRQ安全父域的所有子域也必须是IRQ安全的。
许多设备能够在系统仍在运行时动态关闭电源。这对于未被使用的设备非常有用,并且可以在运行中的系统上提供显著的节能。这些设备通常支持一系列运行时电源状态,可能使用名称如“关闭”、“休眠”、“空闲”、“活动”等。在某些情况下(如PCI),这些状态可能部分受到设备使用的总线的限制,并且通常包括也用于系统睡眠状态的硬件状态。
可以在一些设备处于运行时低功耗状态时启动系统范围的电源转换。系统睡眠PM回调应该识别这种情况并适当地对其做出反应,但必要的操作是特定于子系统的。
在某些情况下,决定可能是在子系统级别做出的,而在其他情况下,可能留给设备驱动程序决定。在某些情况下,可能希望在系统范围的电源转换期间将挂起的设备保持在该状态,但在其他情况下,可能需要将设备暂时放回全功率状态,例如以便临时禁用其系统唤醒功能。这一切取决于硬件和所涉及的子系统和设备驱动程序的设计。
一些总线类型和PM域具有在其->suspend回调中提前从运行时挂起中恢复所有设备的策略,但如果设备的驱动程序可以处理运行时挂起的设备,则这可能并不真正必要。驱动程序可以在探测时通过在power.driver_flags中设置DPM_FLAG_SMART_SUSPEND标志,并借助dev_pm_set_driver_flags()辅助例程来指示这一点。
设置该标志会导致PM核心和中间层代码(总线类型、PM域等)在设备在系统范围挂起的这些阶段保持在运行时挂起状态时跳过驱动程序提供的->suspend_late和->suspend_noirq回调(类似地,在系统休眠的“freeze”和“poweroff”部分也是如此)。[否则,同一设备的相同驱动程序回调可能会连续执行两次,这在一般情况下是无效的。]如果设备的中间层系统范围PM回调存在,则它们负责跳过这些驱动程序回调;如果不存在,则PM核心负责跳过它们。子系统回调例程可以通过测试从dev_pm_skip_suspend()辅助函数的返回值来确定它们是否需要跳过驱动程序回调。
此外,设置DPM_FLAG_SMART_SUSPEND后,如果设备在先前的“freeze”转换期间保持在运行时挂起状态,那么在休眠期间会跳过驱动程序的->thaw_noirq和->thaw_early回调。同样,如果设备的中间层回调在设备上存在,它们负责执行此操作,否则PM核心会处理。
在挂起类型转换的“suspend”阶段期间,DPM_FLAG_MAY_SKIP_RESUME标志将与PM核心设置的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状态设置为“挂起”。否则,如果设备在先前的系统范围挂起转换期间处于运行时挂起状态,并且其DPM_FLAG_SMART_SUSPEND已设置,则PM核心将设备的运行时PM状态设置为“活动”。[因此,没有设置DPM_FLAG_SMART_SUSPEND的驱动程序不应该期望在系统范围的恢复类型转换期间由PM核心将其设备的运行时PM状态从“挂起”更改为“活动”。]
如果对设备未设置DPM_FLAG_MAY_SKIP_RESUME标志,但设置了DPM_FLAG_SMART_SUSPEND并且驱动程序的“late”和“noirq”挂起回调被跳过,那么其系统范围的“noirq”和“early”恢复回调(如果存在)将像往常一样被调用,并且PM核心在启用其运行时PM之前将设备的运行时PM状态设置为“活动”。在这种情况下,驱动程序必须准备好处理其系统范围的恢复回调与其->runtime_suspend回调(没有中间的->runtime_resume和系统范围的挂起回调)连续执行,并且设备的最终状态必须在这种情况下反映出“活动”运行时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_SUSPEND和DPM_FLAG_MAY_SKIP_RESUME,并且使用相同的挂起/恢复回调函数进行运行时PM和系统范围挂起/恢复。]