设备电源管理
Copyright (c) 2010 Rafael J. Wysocki<rjw@sisk.pl>, Novell Inc.
Copyright (c) 2010 Alan Sternstern@rowland.harvard.edu
*************************************************************
本文由DroidPhone翻译于2011.8.5
*************************************************************
Linux的源代码里,大部分都属于设备驱动程序的代码,因此,大多数电源管理(PM)的代码也是存在于驱动程序当中。很多驱动程序可能只做了少量的工作,另外一些,例如使用电池供电的硬件平台(移动电话等)则会在电源管理上做了大量的工作。
这份文档对驱动程序如何与系统的电源管理部分交互做了一个大概的描述,尤其是关联到驱动程序核心中的模型和接口的共享,建议从事驱动程序相关领域的人通过本文档可以了解相关的背景知识。
设备电源管理的两种模型
===================================
驱动程序可以使用其中一种模型来使设备进入低功耗状态:
1. 系统睡眠模型:
驱动程序作为一部分,跟随系统级别的低功耗状态,就像"suspend"(也叫做"suspend-to-RAM"),或者对于有硬盘的系统,可以进入"hibernation"(也叫做"suspend-to-disk")。
这种情况下,驱动程序,总线,设备类驱动一起,通过各种特定于设备的suspend和resume方法,清晰地关闭硬件设备和各个软件子系统,然后在数据不被丢失的情况下重新激活硬件设备。
有些驱动程序可以管理硬件的唤醒事件,这些事件可以让系统离开低功耗状态。这一特性可以通过相应的/sys/devices/.../power/wakeup文件来开启和关闭(对于Ethernet驱动程序,ethtool通过ioctl接口达到同样的目的);使能该功能可能会导致额外的功耗,但他让整个系统有更多的机会进入低功耗状态。
2. Runtime 电源管理模型:
这种模型允许设备在系统运行阶段进入低功耗状态,原则上,他可以独立于其他的电源管理活动。不过,通常设备之间不能单独进行控制(例如,父设备不能进入suspend,除非他的所有子设备已经进入suspend状态)。此外,依据不同的总线类型,可能必须做出一些特别的操作来达到目的。如果设备在系统运行阶段进入了低功耗状态,在系统级别的电源状态迁移时(suspend或hibernation)就必须做出特别的处理。
正因为这个原因,不仅仅设备驱动程序本身,相应的子系统(bus type,device type,device class)驱动程序和电源管理核心也会被卷入到rumtime电源管理的工作中来。比如当系统睡眠时,以上的各模块必须互相合作来实现各种多样的suspend和resume方法,以便让硬件进入低功耗状态,唤醒后继续提供服务而不丢失数据。
对于低功耗状态的定义,我们没有太多可以说的,因为他们通常特定于系统,甚至特定于某个设备。如果在系统运行状态,足够多的设备进入了低功耗状态,这时的效果其实和进入了系统级别的低功耗状态非常相像。这样一些驱动程序可以利用rumtime电源管理让系统进入一种类似深度省电的状态。
大多数进入suspend状态的设备会停止所有的I/O操作:不会有DMA或者IRQ请求(需要唤醒系统的除外),不会有数据的读写,不再接受上层驱动的请求。这对于不同的总线和平台会有不同的要求。
关于硬件唤醒事件的一些例子:由RTC发起的闹钟,网络数据包的到达,键盘或者鼠标的活动,媒体的插入或移除(PCMCIA,MMC/SD,USB,等等)。
进入系统睡眠状态的接口
===================================================
内核为各个子系统(bus type,device type, device class)和驱动程序提供了相应的编程接口,以便它们参与它们所关心的设备的电源管理。这些接口覆盖了系统级别的睡眠和runtime级别的管理。
设备电源管理操作
===================================================
子系统和驱动程序的设备电源管理操作,都定义在dev_pm_ops结构中:
struct dev_pm_ops {
int(*prepare)(struct device *dev);
void(*complete)(struct device *dev);
int(*suspend)(struct device *dev);
int(*resume)(struct device *dev);
int(*freeze)(struct device *dev);
int(*thaw)(struct device *dev);
int(*poweroff)(struct device *dev);
int(*restore)(struct device *dev);
int(*suspend_noirq)(struct device *dev);
int(*resume_noirq)(struct device *dev);
int(*freeze_noirq)(struct device *dev);
int(*thaw_noirq)(struct device *dev);
int(*poweroff_noirq)(struct device *dev);
int(*restore_noirq)(struct device *dev);
int(*runtime_suspend)(struct device *dev);
int(*runtime_resume)(struct device *dev);
int(*runtime_idle)(struct device *dev);
};
这个结构在include/linux/pm.h中定义,它们的作用将会在接下来进行描述。现在,我们只要记住,最后三个方法是专门用于rumtime pm的,其他的则用于系统级别的电源状态迁移。
某些子系统中,依然存在所谓“过时的”或“传统的”电源管理操作接口,这种方式不会使用到dev_pm_ops结构,而且只适用于系统级别的电源管理方法,这边文章里将不会对它进行说明,如果要了解的话请直接查看内核的源代码。
子系统级别(Subsystem-Level)方法
------------------------------------------------
设备进入suspend和resume的关键方法在bus_type结构、device_type结构和class结构的pm成员中,他是一个dev_pm_ops结构的指针。多数情况下,这些都是那些具体总线的体系结构(例如PCI或USB或某个设备类别和设备类)的维护者们来关注的部分。
总线驱动会适当地实现这些方法以供硬件和驱动程序使用它们;因为PCI和USB有不同的工作方式。只有少数人会编写subsystem-level的驱动程序;大多数的设备驱动程序是建立在各种特定总线架构的代码之上。
有关这些调用,稍后会进行更详尽的描述;它们将会顺着父子形式的设备模型树,一个设备一个设备地被调用。
/sys/devices/.../power/wakeup files
-------------------------------------------------
设备模型中的所有设备都有两个标志来控制唤醒事件(可使得设备或系统退出低功耗状态)。设两个标志位由总线或者设备驱动用device_set_wakeup_capable()和device_set_wakeup_enable()来初始化,它们在include/linux/pm_wakeup.h中定义。
"can_wakeup"标志表示设备(或驱动)物理上支持唤醒事件,device_set_wakeup_capable()函数会影响该标志。"should_wakeup"标志控制设备是否应该尝试启用他的唤醒机制。device_set_wakeup_enable()会影响该标志。大部分的驱动程序不会主动修改它们的值。大多数设备的should_wakeup的初始值都被设为false,也有例外,比如电源键、键盘和由ethtool设置了wake-on-LAN功能的网卡。
设备是否有能力发出唤醒事件是一个硬件的问题,内核只是负责持续地跟踪这些事件的发生。另外一方面,一个有唤醒能力的设备是否应该发起唤醒事件则是一个策略问题,它是由用户空间通过sysfs的属性文件(power/wakeup)进行管理的。用户空间可以写入"enabled",或"disabled"来设置或清除shoule_wakeup标志,相应地,读取该文件时,如果can_wakeup标志是true则返回对应的字符串,如果can_wakeup是false,则返回一个空字符串,以此来表明设备不支持唤醒事件。(需要注意的是,尽管返回空字符串,该文件的写入依然会影响should_wakeup标志)
只有当这两个标志都为true时,device_may_wakeup()函数才会返回true。当系统迁移到睡眠状态时,驱动程序应该在让设备进入低功耗状态前通过这一函数检查,确定是否启用唤醒机制。不过,在rumtime电源管理模式下,不管设备和驱动程序是否都支持,也不管should_wakeup标志是否设置,唤醒事件都会被使能。
/sys/devices/.../power/control files
------------------------------------------------
设备模型中的每个设备都有一个标志位来控制它是否属于runtime电源管理模式。这个叫runtime_auto的标志由bus type(或其他子系统)用pm_rumtime_allow()或者是pm_rumtime_forbid()来初始化。默认值是允许rumtimepm的。
用户空间可以通过向设备的sysfs文件power/control写入"on"或者"auto"来修改该标志位。写入"auto"相当于调用了pm_rumtime_allow(),允许设备由驱动程序进行rumtimepm。写入"on"相当于调用pm_rumtime_forbid(),标志位被清除,设备将会从低功耗状态返回全功率状态,并且阻止设备进行runtime电源管理。用户空间也可以读取该文件来检查runtime_auto的当前值。
设备的runtime_auto标志不会影响系统级别电源状态的迁移。特别注意的是,尽管runtime_auto标志被清除,当系统级别的电源状态迁移到睡眠状态时,设备也会被带入低功耗状态。
关于runtime电源管理架构的更多信息,请参看Documentation/power/runtime_pm.txt。
调用驱动程序进入或退出系统睡眠状态
==================================
当系统进入睡眠状态,系统会要求设备驱动程序让设备进入兼容于目标系统的一种状态来挂起(suspend)设备。这通常是某种"off"状态。具体情况都是特定于各系统的。另外,可唤醒的设备一般会保持部分功能以便适当的时候可以唤醒系统。
当系统退出低功耗状态时,设备驱动程序被要求恢复(resume)设备让他进入全电源状态。suspend和resume动作总是一起发生的,两者都可分为多个不同的阶段。
对于相对简单的驱动程序,suspend可能在suspend_noirq阶段使用上层的类代码来停止设备并尽可能让它们进入"off"状态。唤醒时,相对应的resume调用会重新初始化硬件,然后重新激活他们的I/O活动。
对电源有特别需求的驱动程序可能会让设备做出必要的准备,以便之后可以产生唤醒事件。
保证回调的顺序
-------------------------------------
当设备进入suspend或resume时,因为设备之间具备一定的桥接关系,为了确保能够正确地访问它们,suspend时会在设备数中按照自底向上的顺序进行,而resume时则是按照自顶向下的顺序进行。
设备在设备数中的顺序决定于设备注册的顺序:子设备永远不能先于父设备进行注册、探测或resume;也不能在父设备之后进行移除或挂起。
具体的策略就是设备数应该和硬件的总线拓扑结构相吻合。特别是,这就意味着当父设备正在进行挂起动作(例如,已经被pm的核心选为下一个将要被挂起的设备)、或者已经挂起的情况下,注册子设备就会失败。设备驱动程序必须正确地处理这种情况。
系统电源管理中的各个阶段
------------------------------------------------
suspend和resume是分阶段完成的。Standby、Sleep(suspend-to-RAM)和hibernation(suspend-to-disk)会使用到不同的阶段。在进入下一个阶段之前,都需要为每个设备调用属于本阶段的回调函数。不是所有的总线和设备类都会支持所有这些回调,也不是所有的驱动程序都要使用这些回调。有些阶段需要冻结进程后,解冻进程前执行。此外,*_noirq阶段需要在IRQ被关闭的情况下执行(除非他们被IRQ_WAKEUP标记)。
多数阶段使用bus、type和class的回调(也就是定义在dev->bus->pm,dev->type->pm和dev->class->pm中)。不过prepare和complete阶段是个例外,他们仅仅使用了bus的回调。当一个阶段中有多个回调要执行时,按照以下顺序调用,suspend时:<class,type,bus>,resume时:<bus,type,class>。比如,在suspend时将会执行以下调用顺序:
dev->class->pm.suspend(dev);
dev->type->pm.suspend(dev);
dev->bus->pm.suspend(dev);
相反,在resume阶段,移至下一个设备之前,pm核心在当前设备按以下回调进行:
dev->bus->pm.resume(dev);
dev->type->pm.resume(dev);
dev->class->pm.resume(dev);
这些回调可以反过来通过dev->driver->pm来调用设备或驱动特定的方法,但这不是必需的。
系统挂起(suspend)
------------------------------------
当系统进入standby或sleep状态时,需要经历以下阶段:
prepare,suspend,suspend_noirq。
1. prepare阶段主要是通过阻止新设备注册来防止竟态的发生;如果此时要注册子设备,PM的核心将会不知道一个设备的所有子设备已经被suspend。(相反,设备可以在任何时刻被注销。)不像suspend其他的阶段,prepare阶段设备树会自顶向下进行扫描。
prepare阶段只使用了bus的回调。回调返回后,该设备的下面将不可以注册新的子设备。回调方法也会让设备或驱动为将要到来的系统电源状态迁移做出准备,但它不应该让设备进入低功耗状态。
2. suspend阶段由suspend回调实现,它停止设备的一切I/O操作。它同时也可以保存设备的寄存器,依据设备所属的总线类型,让设备进入合适的低功耗状态,同时可以使能唤醒事件。
3. suspend_noirq阶段发生在IRQ被禁止之后,这意味着该回调运行期间,驱动程序的中断处理代码不会被调用。回调方法可以保存上一阶段没有保存的寄存器并最终让设备进入相应的低功耗状态。
大多数子系统(subsystem)和驱动程序不需要实现这一回调。不过,某些允许设备共享中断向量的总线类型,例如PCI,通常需要这一回调;否则,当本设备已经进入低功耗时另一个与他共享中断的设备感知中断的发生,驱动程序将会发生错误。
这些阶段结束后,驱动程序必须停止所有的I/O事务(DMA,IRQs),保存足够的状态信息以便它们能被重新初始化或回复之前的状态(按应将的需要而定),然后让设备进入低功耗状态。很多平台上,它们会关闭某些时钟;有时还会关闭电源或者是降低电压。(支持rumtime pm的驱动可能已经提前完成部分或所有的步骤。)
如果device_may_wakeup(dev)返回true,设备准备好产生硬件唤醒信号以便触发一个系统唤醒事件来唤醒已经进入睡眠状态的系统。例如,enable_irq_wakeup()可以让一个连接到某个开关或外部硬件的GPIO被捕捉,pci_enable_wake()则响应类似PCI PME等信号。
只要这些回调中的一个返回错误,系统不会进入所述的低功耗状态,而是由pm的核心对已经suspend的设备发起resume动作进行回退。
退出系统挂起(resume)
----------------------------------------
当系统退出standby或sleep状态时,需要经历以下阶段:
resume_noirq,resume,complete。
1. resume_noirq回调方法应该执行所有在中断处理程序被调用前的必须动作。这通常意味着撤销suspend_noirq阶段所做的动作。如果总线类型允许共享中断向量,例如PCI,该回调方法应该使设备和驱动能够识别自身是否是中断源,如果是,还要能正确地处理。
例如,对于PCI总线,bus->pm.resume_noirq()让设备进入全电源状态(PCI中称作D0),并回复设备的标准配置寄存器。然后,调用设备驱动程序的 ->pm.resume_noirq()方法来执行特定于设备的动作。
2. resume回调方法让设备回到他的工作状态,以便它能执行正常的I/O。这通常等同于执行suspend阶段的撤销工作。
3. complete阶段仅仅使用bus的回调。该方法应该撤销prepare阶段所做出的动作。不过请注意,新设备可能在resume回调返回后立刻被注册,而不必等到complete阶段完成。
这些阶段结束后,驱动应该和suspend之前一样:I/O能通过DMA或IRQs执行,相应的时钟被打开。尽管在系统睡眠之前,设备因为runtime pm已经处于低功耗状态之下,在这之后设备还是应该回到全电源状态。有很多原因说明为什么要这样做,详细的讨论请参考:Documentation/power/runtime_pm.txt。
不过,到这以后,具体还是会特定于平台的。例如,一些系统支持多种"run"状态,resume后的模式可能不同于suspend之前。可能是某些时钟或电源的改变,这些都会很容易影响到驱动程序如何工作。
驱动程序需要能够处理在suspend回调被调用后硬件被复位的情况,例如需要彻底地重新初始化。这可能是最困难的部分,实现细节可能会受到NDA等文档和chip errata的保护。最简单的情况是硬件的状态自suspend被执行后没有改变过,单这是不能保证的(实际上,这通常都不成立)。
不管物理上是否可能,驱动程序也要准备被知会系统power-down期间设备被移除。在Linux中,PCMCIA,MMC,USB,Firewire,SCSI甚至IDE都是可移除的例子。具体的关于驱动程序如何被知会,和处理这种移除事件的工作是特定于总线的,而且通常有单独的线程来处理。
进入Hibernation
--------------------------------------
省略......................................
退出Hibernation
--------------------------------------
省略......................................
系统设备
----------------------------------------
系统设备(sysdevs)遵循稍微不同的API,它们可以在以下文件中找到:
include/linux/sysdev.h
drivers/base/sys.c
系统设备要在中断关闭的情况下进行suspend,并且要在其他设备被挂起之后执行,唤醒时,它们会先于其他设备被resume,当然也是在关中断的情况下。这些动作都特别的"sysdev_driver"阶段发生,该阶段仅会对系统设备起作用。
因此,在suspend_noirq(freeze_noirq,poweroff_noirq)阶段之后,当非启动(non-boot)的CPUs都被关闭而且剩下的CPU的IRQs也被关闭的情况下,这时候就会启动sysdev_driver.suspend阶段,接下来系统进入睡眠状态(对于hibernation是系统映像被创建)。resume期间的顺序就是:sysdev_driver.resume阶段执行,开启启动用CPU的IRQ,打开其他非启动CPU,然后开始resume_noirq阶段。
实际进入和退出系统级别低功耗状态的代码有时候会调用一些只有boot firmware(bios?bootloader?)才知道的硬件操作,然后保留CPU运行某一软件(从RAM或者FLASH中)来监控系统和管理唤醒序列。
设备低功耗(suspend)状态
------------------------------------------
设备的低功耗状态并没有标准可言。某个设备可以只处理"on"和"off",但另一个设备可能支持一打不同版本的"on"(多少个引擎被激活?),加上一个可以比彻底"off"更快地回到"on"的状态。
一些总线对不同的suspend状态定义了一些规则。PCI可以给出一个例子来:suspend的序列完成后,一个非传统(non-legacy)德尔PCI设备不可以执行DMA或发出IRQs,而且唤醒事件要通过PME#总线信号发出。还定义了几个PCI标准的设备状态,其中一些状态可以只是作为选项。
相反,集成度较高的SOC处理器经常使用IRQs作为唤醒源(因此驱动要调用enable_irq_wake()),而且可以用DMA的完成中断作为唤醒事件(有时DMA能保持激活,只是CPU和一些外设进入睡眠)。
这里有些细节可以是特定于平台的。在某些睡眠状态下,系统可以有部分设备保持激活,例如系统轻度睡眠时,LCD显示器会使用DMA继续进行刷新,frame buffer甚至可能有DSP或者另外的非Linux的CPU来刷新,而运行Linux的CPU却可以处于idle状态。
再有,依赖于不同的目标系统的状态,一些特殊的事情可能发生。一些目标系统状态可以允许设备有很多的操作活动,另一些目标系统状态也许会要求硬关机然后再resume时重新初始化。而且,两个不同的目标系统可以按不同的方法使用相同的设备;就像上面提到的LCD那样,他可以在一个产品的"standby"下保持在激活状态,但另一个使用同样SOC的不同产品可能就会有不一样的工作方式。
电源管理通知消息
------------------------------
有些操作在上面讨论的电源管理回调方法中是不能被开展的,因为回调发生时已经太晚或者太早。为了处理这些情况,子系统和驱动程序可以注册电源管理通知,以便在进程被冻结之前或者是解冻之后调用某个操作。一般来说,PM通知机制适合于执行用户空间可以利用的活动,或者至少不至于干扰到用户空间的活动。
详细说明可以参考文档Documentation/power/notifiers.txt。
runtime电源管理
======================
许多设备能够在系统运行时动态地关闭,这个特性对那些已经没被使用的设备特别有用,而且能让运行中的系统有更高效地节约能源。这些设备通常支持一定范围的runtime电源状态,例如"off","sleep","idle","active"等等,这些状态有时会被设备所使用的总线所约束,而且通常会包含系统级别睡眠所用到的硬件状态。
系统级别电源状态迁移可以在某些设备因为rumtimepm而进入低功耗状态的情况下开始。系统睡眠的PM回调应该要能识别这种情况并用适当的方法重新激活他们,不过这些动作都是特定于各个子系统的。
有时候这会由子系统这一级别来决定,有时候也会让设备驱动程序自己决定,在系统级别的电源状态迁移时可以让一个已经suspend的设备保留注意一状态,另一些情况则可能会临时让设备回到全电源状态,例如为了禁止它唤醒系统的能力。这些都依赖于具体的硬件和子系统的设计,是驱动程序要关注的问题。
当系统从睡眠状态唤醒的过程中,最好是让设备回到全电源状态,解释请参考文档Documentation/power/runtime_pm.txt。该文档有更详细的关于这些问题的讨论,也解释了runtime电源管理的通用架构