I/O 设备运行时电源管理框架¶
2009-2011 Rafael J. Wysocki <rjw@sisk.pl>, Novell Inc.
2010 Alan Stern <stern@rowland.harvard.edu>
2014 Intel Corp., Rafael J. Wysocki <rafael.j.wysocki@intel.com>
1. 简介¶
I/O 设备运行时电源管理(运行时 PM)的支持由电源管理核心(PM 核心)级别通过以下方式提供:
电源管理工作队列 pm_wq,总线类型和设备驱动程序可以将它们的 PM 相关工作项放入其中。强烈建议使用 pm_wq 来排队所有与运行时 PM 相关的工作项,因为这允许它们与系统范围的电源转换(挂起到 RAM、休眠和从系统睡眠状态恢复)同步。pm_wq 在 include/linux/pm_runtime.h 中声明,并在 kernel/power/main.c 中定义。
‘
struct device
’(类型为 ‘struct dev_pm_info’,定义在 include/linux/pm.h 中)的 ‘power’ 成员中的一些运行时 PM 字段,可用于同步运行时 PM 操作。‘
struct dev_pm_ops
’(定义在 include/linux/pm.h 中)中的三个设备运行时 PM 回调。drivers/base/power/runtime.c 中定义的一组辅助函数,可用于执行运行时 PM 操作,PM 核心将负责它们之间的同步。鼓励总线类型和设备驱动程序使用这些函数。
下面描述了 ‘struct dev_pm_ops
’ 中的运行时 PM 回调、‘struct dev_pm_info’ 的设备运行时 PM 字段以及为运行时 PM 提供的核心辅助函数。
2. 设备运行时 PM 回调¶
‘struct dev_pm_ops
’ 中定义了三个设备运行时 PM 回调:
struct dev_pm_ops {
...
int (*runtime_suspend)(struct device *dev);
int (*runtime_resume)(struct device *dev);
int (*runtime_idle)(struct device *dev);
...
};
PM 核心为设备的子系统执行 ->runtime_suspend()、->runtime_resume() 和 ->runtime_idle() 回调,子系统可能是以下之一:
设备的 PM 域,如果设备的 PM 域对象 dev->pm_domain 存在。
设备的设备类型,如果 dev->type 和 dev->type->pm 都存在。
设备的设备类,如果 dev->class 和 dev->class->pm 都存在。
设备的总线类型,如果 dev->bus 和 dev->bus->pm 都存在。
如果通过应用上述规则选择的子系统未提供相关的回调,PM 核心将直接调用存储在 dev->driver->pm 中的相应驱动程序回调(如果存在)。
PM 核心始终按照上述顺序检查要使用的回调,因此回调的优先级顺序从高到低为:PM 域、设备类型、类和总线类型。此外,高优先级的回调始终优先于低优先级的回调。在下文中,PM 域、总线类型、设备类型和类回调被称为子系统级回调。
默认情况下,回调始终在启用中断的进程上下文中调用。但是,pm_runtime_irq_safe() 辅助函数可用于告知 PM 核心,在禁用中断的原子上下文中运行给定设备的 ->runtime_suspend()、->runtime_resume() 和 ->runtime_idle() 回调是安全的。这意味着所讨论的回调例程不得阻塞或睡眠,但也意味着可以在中断处理程序中或通常在原子上下文中为该设备使用第 4 节末尾列出的同步辅助函数。
子系统级的挂起回调(如果存在)_完全_ _负责_ 处理设备的挂起,这可能包括(但不需要)执行设备驱动程序自己的 ->runtime_suspend() 回调(从 PM 核心的角度来看,只要子系统级的挂起回调知道如何处理设备,就不需要在设备驱动程序中实现 ->runtime_suspend() 回调)。
一旦子系统级的挂起回调(或直接调用的驱动程序挂起回调)为给定设备成功完成,PM 核心将认为该设备已挂起,这并不一定意味着该设备已进入低功耗状态。但应该意味着,在为该设备执行适当的恢复回调之前,该设备将不会处理数据,也不会与 CPU 和 RAM 通信。成功执行挂起回调后,设备的运行时 PM 状态为“已挂起”。
如果挂起回调返回 -EBUSY 或 -EAGAIN,则设备的运行时 PM 状态保持为“活动”,这意味着该设备之后_必须_ 完全可操作。
如果挂起回调返回的错误代码不是 -EBUSY 和 -EAGAIN,则 PM 核心会将其视为致命错误,并将拒绝为该设备运行第 4 节中描述的辅助函数,直到其状态直接设置为“活动”或“已挂起”(PM 核心为此目的提供了特殊的辅助函数)。
特别是,如果驱动程序需要远程唤醒功能(即允许设备请求更改其电源状态的硬件机制,例如 PCI PME)才能正常工作,并且 device_can_wakeup() 为该设备返回 ‘false’,则 ->runtime_suspend() 应返回 -EBUSY。另一方面,如果 device_can_wakeup() 为该设备返回 ‘true’,并且设备在执行挂起回调期间进入低功耗状态,则期望为该设备启用远程唤醒。通常,应为运行时进入低功耗状态的所有输入设备启用远程唤醒。
子系统级的恢复回调(如果存在)__完全负责__处理设备的恢复,这可能包括(但不需要)执行设备驱动程序自己的 ->runtime_resume() 回调(从 PM 核心的角度来看,只要子系统级的恢复回调知道如何处理设备,就不需要在设备驱动程序中实现 ->runtime_resume() 回调)。
一旦子系统级的恢复回调(或直接调用的驱动程序恢复回调)成功完成,PM 核心将认为该设备完全可操作,这意味着该设备_必须_ 能够根据需要完成 I/O 操作。该设备的运行时 PM 状态为“活动”。
如果恢复回调返回错误代码,则 PM 核心会将其视为致命错误,并将拒绝为该设备运行第 4 节中描述的辅助函数,直到其状态直接设置为“活动”或“已挂起”(通过 PM 核心为此目的提供的特殊辅助函数)。
当设备似乎处于空闲状态时,PM 核心会执行空闲回调(子系统级的空闲回调,如果存在,或驱动程序级的空闲回调),这由两个计数器向 PM 核心指示,即设备的用途计数器和设备的“活动”子项计数器。
如果使用 PM 核心提供的辅助函数减少了这些计数器中的任何一个,并且结果等于零,则检查另一个计数器。如果该计数器也等于零,则 PM 核心将执行空闲回调,并将设备作为其参数。
空闲回调执行的操作完全取决于所讨论的子系统(或驱动程序),但预期和推荐的操作是检查设备是否可以挂起(即,是否满足挂起设备所需的所有条件),并在这种情况下为该设备排队挂起请求。如果没有空闲回调,或者如果回调返回 0,则 PM 核心将尝试对设备执行运行时挂起,同时也会遵守配置为自动挂起的设备。本质上,这意味着调用 __pm_runtime_autosuspend()(请注意,驱动程序需要更新设备上次繁忙标记 pm_runtime_mark_last_busy(),以控制这种情况下的延迟)。为了防止这种情况(例如,如果回调例程已启动延迟挂起),例程必须返回一个非零值。PM 核心会忽略负的错误返回码。
第 4 节中描述的 PM 核心提供的辅助函数保证满足以下关于一个设备的运行时 PM 回调的约束:
回调是互斥的(例如,禁止并行执行 ->runtime_suspend() 和 ->runtime_resume() 或同一设备的另一个 ->runtime_suspend() 实例),除非 ->runtime_suspend() 或 ->runtime_resume() 可以与 ->runtime_idle() 并行执行(尽管在为同一设备执行任何其他回调时,不会启动 ->runtime_idle())。
仅可对“活动”设备执行 ->runtime_idle() 和 ->runtime_suspend()(即,PM 核心仅对运行时 PM 状态为“活动”的设备执行 ->runtime_idle() 或 ->runtime_suspend())。
仅可对用途计数器等于零_且_其“活动”子项计数器等于零或设置了 ‘power.ignore_children’ 标志的设备执行 ->runtime_idle() 和 ->runtime_suspend()。
仅可对“已挂起”设备执行 ->runtime_resume()(即,PM 核心仅对运行时 PM 状态为“已挂起”的设备执行 ->runtime_resume())。
此外,PM 核心提供的辅助函数遵守以下规则:
如果即将执行 ->runtime_suspend() 或有待执行的请求,则不会为同一设备执行 ->runtime_idle()。
执行或调度执行 ->runtime_suspend() 的请求将取消同一设备的任何待执行 ->runtime_idle() 的请求。
如果 ->runtime_resume() 即将被执行,或者有待执行它的请求,则同一设备的其他回调将不会被执行。
执行 ->runtime_resume() 的请求将取消同一设备的其他回调的任何待处理或已调度的请求,除了已调度的自动挂起。
3. 运行时 PM 设备字段¶
以下设备运行时 PM 字段存在于 ‘struct dev_pm_info’ 中,定义于 include/linux/pm.h
- struct timer_list suspend_timer;
用于调度(延迟)挂起和自动挂起请求的定时器
- unsigned long timer_expires;
定时器到期时间,以 jiffies 为单位(如果此值不为零,则定时器正在运行并将在该时间到期,否则定时器未运行)
- struct work_struct work;
用于将请求排队的 work 结构(即 pm_wq 中的工作项)
- wait_queue_head_t wait_queue;
如果任何辅助函数需要等待另一个函数完成时使用的等待队列
- spinlock_t lock;
用于同步的锁
- atomic_t usage_count;
设备的使用计数器
- atomic_t child_count;
设备的“活动”子节点的计数
- unsigned int ignore_children;
如果设置,则忽略 child_count 的值(但仍然更新)
- unsigned int disable_depth;
用于禁用辅助函数(如果此值等于零,它们会正常工作);它的初始值为 1(即,运行时 PM 最初对所有设备禁用)
- int runtime_error;
如果设置,则表示存在致命错误(如第 2 节所述,其中一个回调返回了错误代码),因此辅助函数将不会工作,直到清除此标志;这是失败的回调返回的错误代码
- unsigned int idle_notification;
如果设置,则表示 ->runtime_idle() 正在执行
- unsigned int request_pending;
如果设置,则表示存在待处理的请求(即,一个工作项排队到 pm_wq 中)
- enum rpm_request request;
待处理的请求类型(如果设置了 request_pending 则有效)
- unsigned int deferred_resume;
如果当该设备的 ->runtime_suspend() 正在执行时,->runtime_resume() 即将被运行,并且等待挂起完成是不切实际的,则设置此标志;表示 “一旦你挂起就立即开始恢复”
- enum rpm_status runtime_status;
设备的运行时 PM 状态;此字段的初始值为 RPM_SUSPENDED,这意味着每个设备最初都被 PM 核心视为“已挂起”,无论其真实的硬件状态如何
- enum rpm_status last_status;
在禁用设备的运行时 PM 之前捕获的设备的最后一次运行时 PM 状态(初始无效,并且当 disable_depth 为 0 时也无效)
- unsigned int runtime_auto;
如果设置,则表示用户空间已允许设备驱动程序在运行时通过 /sys/devices/.../power/control 接口 来进行设备的电源管理;它只能通过 pm_runtime_allow() 和 pm_runtime_forbid() 辅助函数进行修改
- unsigned int no_callbacks;
表示设备不使用运行时 PM 回调(参见第 8 节);它只能通过 pm_runtime_no_callbacks() 辅助函数进行修改
- unsigned int irq_safe;
表示在持有自旋锁并禁用中断的情况下将调用 ->runtime_suspend() 和 ->runtime_resume() 回调
- unsigned int use_autosuspend;
表示设备的驱动程序支持延迟自动挂起(参见第 9 节);它只能通过 pm_runtime{_dont}_use_autosuspend() 辅助函数进行修改
- unsigned int timer_autosuspends;
表示当定时器到期时,PM 核心应尝试执行自动挂起而不是正常的挂起
- int autosuspend_delay;
用于自动挂起的延迟时间(以毫秒为单位)
- unsigned long last_busy;
上次为此设备调用 pm_runtime_mark_last_busy() 辅助函数的时间(以 jiffies 为单位);用于计算自动挂起的不活动期
以上所有字段都是 ‘struct device
’ 的 ‘power’ 成员的成员。
4. 运行时 PM 设备辅助函数¶
以下运行时 PM 辅助函数在 drivers/base/power/runtime.c 和 include/linux/pm_runtime.h 中定义
- void pm_runtime_init(struct device *dev);
初始化 ‘struct dev_pm_info’ 中的设备运行时 PM 字段
- void pm_runtime_remove(struct device *dev);
确保在从设备层次结构中删除设备后,设备的运行时 PM 将被禁用
- int pm_runtime_idle(struct device *dev);
执行设备的子系统级空闲回调;失败时返回错误代码,其中 -EINPROGRESS 表示 ->runtime_idle() 已经在执行;如果没有回调或回调返回 0,则运行 pm_runtime_autosuspend(dev) 并返回其结果
- int pm_runtime_suspend(struct device *dev);
执行设备的子系统级挂起回调;成功时返回 0,如果设备的运行时 PM 状态已经为“已挂起”,则返回 1,失败时返回错误代码,其中 -EAGAIN 或 -EBUSY 表示将来可以安全地尝试再次挂起设备,而 -EACCES 表示 ‘power.disable_depth’ 不等于 0
- int pm_runtime_autosuspend(struct device *dev);
与 pm_runtime_suspend() 相同,但会考虑自动挂起延迟;如果 pm_runtime_autosuspend_expiration() 表示延迟尚未到期,则会在适当的时间安排自动挂起并返回 0
- int pm_runtime_resume(struct device *dev);
执行设备的子系统级恢复回调;成功时返回 0,如果设备的运行时 PM 状态已经为“活动”(如果 ‘power.disable_depth’ 不为零,但当它从 0 变为 1 时,状态为“活动”,也会返回 1),或者失败时返回错误代码,其中 -EAGAIN 表示将来可能可以安全地尝试再次恢复设备,但应额外检查 ‘power.runtime_error’,而 -EACCES 表示由于 ‘power.disable_depth’ 不等于 0 而无法运行回调
- int pm_runtime_resume_and_get(struct device *dev);
运行 pm_runtime_resume(dev),如果成功,则增加设备的使用计数器;成功时返回 0(无论设备的运行时 PM 状态是否已经为“活动”),或者失败时返回 pm_runtime_resume() 的错误代码。
- int pm_request_idle(struct device *dev);
提交一个请求以执行设备的子系统级空闲回调(该请求由 pm_wq 中的一个工作项表示);成功时返回 0,或者如果请求未排队,则返回错误代码
- int pm_request_autosuspend(struct device *dev);
安排在自动挂起延迟到期时执行设备的子系统级挂起回调;如果延迟已经到期,则立即将工作项排队
- int pm_schedule_suspend(struct device *dev, unsigned int delay);
安排在未来执行设备的子系统级挂起回调,其中 ‘delay’ 是在将挂起工作项排入 pm_wq 之前要等待的时间,以毫秒为单位(如果 ‘delay’ 为零,则立即将工作项排队);成功时返回 0,如果设备的 PM 运行时状态已经为“已挂起”,则返回 1,或者如果请求未被安排(或者如果 ‘delay’ 为 0,则未排队),则返回错误代码;如果 ->runtime_suspend() 的执行已经被安排但尚未到期,则 ‘delay’ 的新值将用作等待时间
- int pm_request_resume(struct device *dev);
提交一个请求以执行设备的子系统级恢复回调(该请求由 pm_wq 中的一个工作项表示);成功时返回 0,如果设备的运行时 PM 状态已经为“活动”,则返回 1,或者如果请求未排队,则返回错误代码
- void pm_runtime_get_noresume(struct device *dev);
增加设备的使用计数器
- int pm_runtime_get(struct device *dev);
增加设备的使用计数器,运行 pm_request_resume(dev) 并返回其结果
- int pm_runtime_get_sync(struct device *dev);
增加设备的使用计数器,运行 pm_runtime_resume(dev) 并返回其结果;请注意,它不会在发生错误时删除设备的使用计数器,因此请考虑使用 pm_runtime_resume_and_get() 代替它,尤其是当调用者检查其返回值时,因为这很可能会产生更简洁的代码。
- int pm_runtime_get_if_in_use(struct device *dev);
如果 ‘power.disable_depth’ 不为零,则返回 -EINVAL;否则,如果运行时 PM 状态为 RPM_ACTIVE 且运行时 PM 使用计数器不为零,则增加计数器并返回 1;否则返回 0,不更改计数器
- int pm_runtime_get_if_active(struct device *dev);
如果 ‘power.disable_depth’ 不为零,则返回 -EINVAL;否则,如果运行时 PM 状态为 RPM_ACTIVE,则增加计数器并返回 1;否则返回 0,不更改计数器
- void pm_runtime_put_noidle(struct device *dev);
减少设备的使用计数器
- int pm_runtime_put(struct device *dev);
减少设备的使用计数器;如果结果为 0,则运行 pm_request_idle(dev) 并返回其结果
- int pm_runtime_put_autosuspend(struct device *dev);
现在与 __pm_runtime_put_autosuspend() 的作用相同,但在未来,它也会调用 pm_runtime_mark_last_busy(),请勿使用!
- int __pm_runtime_put_autosuspend(struct device *dev);
减少设备的使用计数器;如果结果为 0,则运行 pm_request_autosuspend(dev) 并返回其结果
- int pm_runtime_put_sync(struct device *dev);
减少设备的使用计数器;如果结果为 0,则运行 pm_runtime_idle(dev) 并返回其结果
- int pm_runtime_put_sync_suspend(struct device *dev);
减少设备的使用计数器;如果结果为 0,则运行 pm_runtime_suspend(dev) 并返回其结果
- int pm_runtime_put_sync_autosuspend(struct device *dev);
减少设备的使用计数器;如果结果为 0,则运行 pm_runtime_autosuspend(dev) 并返回其结果
- void pm_runtime_enable(struct device *dev);
减少设备的 ‘power.disable_depth’ 字段;如果该字段等于零,则运行时 PM 辅助函数可以为设备执行第 2 节中描述的子系统级回调
- int pm_runtime_disable(struct device *dev);
增加设备的 ‘power.disable_depth’ 字段(如果该字段的值之前为零,则会阻止为设备运行子系统级运行时 PM 回调),确保设备上的所有待处理运行时 PM 操作要么完成要么取消;如果有一个待处理的恢复请求,并且有必要为设备执行子系统级恢复回调以满足该请求,则返回 1,否则返回 0
- int pm_runtime_barrier(struct device *dev);
检查设备是否有一个待处理的恢复请求,并在这种情况下(同步)恢复它,取消任何其他关于它的待处理运行时 PM 请求,并等待所有正在进行的运行时 PM 操作完成;如果有一个待处理的恢复请求,并且有必要为设备执行子系统级恢复回调以满足该请求,则返回 1,否则返回 0
- void pm_suspend_ignore_children(struct device *dev, bool enable);
设置/取消设置设备的 power.ignore_children 标志
- int pm_runtime_set_active(struct device *dev);
清除设备的 ‘power.runtime_error’ 标志,将设备的运行时 PM 状态设置为“活动”,并根据需要更新其父节点的“活动”子节点计数器(只有当 ‘power.runtime_error’ 设置或 ‘power.disable_depth’ 大于零时,才可以使用此函数);如果设备有一个非活动的父节点,并且该父节点的 ‘power.ignore_children’ 标志未设置,则该函数将失败并返回错误代码
- void pm_runtime_set_suspended(struct device *dev);
清除设备的 ‘power.runtime_error’ 标志,将设备的运行时 PM 状态设置为“已挂起”,并根据需要更新其父节点的“活动”子节点计数器(只有当 ‘power.runtime_error’ 设置或 ‘power.disable_depth’ 大于零时,才可以使用此函数)
- bool pm_runtime_active(struct device *dev);
如果设备的运行时 PM 状态为“active”或其“power.disable_depth”字段不等于零,则返回 true,否则返回 false。
- bool pm_runtime_suspended(struct device *dev);
如果设备的运行时 PM 状态为“suspended”且其“power.disable_depth”字段等于零,则返回 true,否则返回 false。
- bool pm_runtime_status_suspended(struct device *dev);
如果设备的运行时 PM 状态为“suspended”,则返回 true。
- void pm_runtime_allow(struct device *dev);
设置设备的 power.runtime_auto 标志,并减少其使用计数器(由 /sys/devices/.../power/control 接口使用,以有效地允许设备在运行时进行电源管理)。
- void pm_runtime_forbid(struct device *dev);
取消设置设备的 power.runtime_auto 标志,并增加其使用计数器(由 /sys/devices/.../power/control 接口使用,以有效地阻止设备在运行时进行电源管理)。
- void pm_runtime_no_callbacks(struct device *dev);
设置设备的 power.no_callbacks 标志,并从 /sys/devices/.../power 中删除运行时 PM 属性(或阻止在注册设备时添加它们)。
- void pm_runtime_irq_safe(struct device *dev);
设置设备的 power.irq_safe 标志,使运行时 PM 回调在关闭中断的情况下被调用。
- bool pm_runtime_is_irq_safe(struct device *dev);
如果为设备设置了 power.irq_safe 标志,使运行时 PM 回调在关闭中断的情况下被调用,则返回 true。
- void pm_runtime_mark_last_busy(struct device *dev);
将 power.last_busy 字段设置为当前时间。
- void pm_runtime_use_autosuspend(struct device *dev);
设置 power.use_autosuspend 标志,启用自动挂起延迟;如果该标志之前被清除且 power.autosuspend_delay 为负数,则调用 pm_runtime_get_sync。
- void pm_runtime_dont_use_autosuspend(struct device *dev);
清除 power.use_autosuspend 标志,禁用自动挂起延迟;如果该标志之前被设置且 power.autosuspend_delay 为负数,则减少设备的使用计数器;调用 pm_runtime_idle。
- void pm_runtime_set_autosuspend_delay(struct device *dev, int delay);
将 power.autosuspend_delay 值设置为 “delay”(以毫秒为单位);如果 “delay” 为负数,则会阻止运行时挂起;如果设置了 power.use_autosuspend,则可能会调用 pm_runtime_get_sync,或者设备的使用计数器可能会被递减并调用 pm_runtime_idle,具体取决于 power.autosuspend_delay 是否从负值更改为或更改自负值;如果 power.use_autosuspend 被清除,则调用 pm_runtime_idle。
- unsigned long pm_runtime_autosuspend_expiration(struct device *dev);
根据 power.last_busy 和 power.autosuspend_delay 计算当前自动挂起延迟周期何时到期;如果延迟时间为 1000 毫秒或更大,则到期时间向上舍入到最接近的秒;如果延迟周期已过期或未设置 power.use_autosuspend,则返回 0,否则返回以 jiffies 为单位的到期时间。
从中断上下文中执行以下辅助函数是安全的:
pm_request_idle()
pm_request_autosuspend()
pm_schedule_suspend()
pm_request_resume()
pm_runtime_get_noresume()
pm_runtime_get()
pm_runtime_put_noidle()
pm_runtime_put()
pm_runtime_put_autosuspend()
__pm_runtime_put_autosuspend()
pm_runtime_enable()
pm_suspend_ignore_children()
pm_runtime_set_active()
pm_runtime_set_suspended()
pm_runtime_suspended()
pm_runtime_mark_last_busy()
pm_runtime_autosuspend_expiration()
如果已为设备调用了 pm_runtime_irq_safe(),则以下辅助函数也可以在中断上下文中使用:
pm_runtime_idle()
pm_runtime_suspend()
pm_runtime_autosuspend()
pm_runtime_resume()
pm_runtime_get_sync()
pm_runtime_put_sync()
pm_runtime_put_sync_suspend()
pm_runtime_put_sync_autosuspend()
5. 运行时 PM 初始化、设备探测和移除¶
最初,所有设备的运行时 PM 都被禁用,这意味着在对设备调用 pm_runtime_enable() 之前,第 4 节中描述的大部分运行时 PM 辅助函数将返回 -EAGAIN。
除此之外,所有设备的初始运行时 PM 状态为“suspended”,但它不必反映设备的实际物理状态。因此,如果设备最初处于活动状态(即它能够处理 I/O),则必须在对设备调用 pm_runtime_enable() 之前,借助 pm_runtime_set_active() 将其运行时 PM 状态更改为“active”。
但是,如果设备有父设备,并且父设备的运行时 PM 已启用,则除非设置了父设备的 “power.ignore_children” 标志,否则对设备调用 pm_runtime_set_active() 将会影响父设备。也就是说,在这种情况下,只要子设备的状态为 “active”,即使子设备的运行时 PM 仍然被禁用(即尚未对子设备调用 pm_runtime_enable() 或已对其调用 pm_runtime_disable()),父设备也无法在运行时使用 PM 核心的辅助函数挂起。因此,一旦对设备调用了 pm_runtime_set_active(),就应尽快对它调用 pm_runtime_enable(),或者借助 pm_runtime_set_suspended() 将其运行时 PM 状态更改回 “suspended”。
如果设备的默认初始运行时 PM 状态(即“suspended”)反映了设备的实际状态,则其总线类型或其驱动程序的 ->probe() 回调可能需要使用第 4 节中描述的 PM 核心的辅助函数之一将其唤醒。在这种情况下,应使用 pm_runtime_resume()。当然,为此,必须通过调用 pm_runtime_enable() 较早地启用设备的运行时 PM。
请注意,如果设备在探测期间可能会执行 pm_runtime 调用(例如,如果它已注册到可能会回调的子系统),则配对 pm_runtime_get_sync() 和 pm_runtime_put() 调用将适合确保设备在探测期间不会被放回睡眠状态。对于诸如网络设备层之类的系统,可能会发生这种情况。
可能需要在 ->probe() 完成后挂起设备。因此,驱动程序核心使用异步 pm_request_idle() 来提交请求,以便在该时间执行设备的子系统级别空闲回调。使用运行时自动挂起功能的驱动程序可能希望在从 ->probe() 返回之前更新上次繁忙标记。
此外,驱动程序核心会阻止运行时 PM 回调与 __device_release_driver() 中的总线通知程序回调发生竞争,这是必要的,因为某些子系统使用通知程序来执行影响运行时 PM 功能的操作。它通过在 driver_sysfs_remove() 和 BUS_NOTIFY_UNBIND_DRIVER 通知之前调用 pm_runtime_get_sync() 来实现这一点。如果设备处于挂起状态,这将恢复设备,并防止在执行这些例程时再次挂起设备。
为了允许总线类型和驱动程序通过从其 ->remove() 例程中调用 pm_runtime_suspend() 来将设备置于挂起状态,驱动程序核心会在运行 __device_release_driver() 中的 BUS_NOTIFY_UNBIND_DRIVER 通知后执行 pm_runtime_put_sync()。这要求总线类型和驱动程序使其 ->remove() 回调避免与运行时 PM 直接竞争,但也允许在删除其驱动程序期间更灵活地处理设备。
->remove() 回调中的驱动程序应撤消在 ->probe() 中完成的运行时 PM 更改。通常,这意味着调用 pm_runtime_disable()、pm_runtime_dont_use_autosuspend() 等。
用户空间可以通过将其 /sys/devices/.../power/control 属性的值更改为 “on” 来有效地禁止设备的驱动程序在运行时对其进行电源管理,这将导致调用 pm_runtime_forbid()。原则上,此机制也可以由驱动程序使用,以有效地关闭设备的运行时电源管理,直到用户空间将其打开为止。也就是说,在初始化期间,驱动程序可以确保设备的运行时 PM 状态为 “active” 并调用 pm_runtime_forbid()。但是,应该注意的是,如果用户空间已经有意将 /sys/devices/.../power/control 的值更改为 “auto” 以允许驱动程序在运行时对设备进行电源管理,则驱动程序可能会通过使用 pm_runtime_forbid() 来混淆它。
6. 运行时 PM 和系统睡眠¶
运行时 PM 和系统睡眠(即系统挂起和休眠,也称为挂起到 RAM 和挂起到磁盘)以几种方式相互作用。如果设备在系统睡眠开始时处于活动状态,则一切都很简单。但是,如果设备已经挂起,该怎么办?
设备可能对运行时 PM 和系统睡眠具有不同的唤醒设置。例如,可能为运行时挂起启用了远程唤醒,但禁止用于系统睡眠(device_may_wakeup(dev) 返回 ‘false’)。发生这种情况时,子系统级别的系统挂起回调负责更改设备的唤醒设置(它可能会将其留给设备驱动程序的系统挂起例程)。可能需要恢复设备并再次挂起设备才能执行此操作。如果驱动程序对运行时挂起和系统睡眠使用不同的功率级别或其他设置,情况也是如此。
在系统恢复期间,最简单的方法是将所有设备恢复到全功率,即使它们在系统挂起开始之前已挂起也是如此。这样做有几个原因,包括:
设备可能需要切换功率级别、唤醒设置等。
固件可能会丢失远程唤醒事件。
设备的子设备可能需要设备处于全功率状态才能恢复自身。
驱动程序对设备状态的想法可能与设备的物理状态不一致。这可能会在从休眠状态恢复期间发生。
设备可能需要重置。
即使设备已挂起,如果其使用计数器 > 0,则很可能在不久的将来也需要运行时恢复。
如果设备在系统挂起开始之前已挂起,并且在恢复期间恢复到全功率,则必须更新其运行时 PM 状态以反映实际的系统睡眠后状态。执行此操作的方法是:
pm_runtime_disable(dev);
pm_runtime_set_active(dev);
pm_runtime_enable(dev);
PM 核心总是在调用 ->suspend() 回调之前递增运行时使用计数器,并在调用 ->resume() 回调之后递减它。因此,像这样暂时禁用运行时 PM 不会导致任何运行时挂起尝试永久丢失。如果使用计数在 ->resume() 回调返回后变为零,则将像往常一样调用 ->runtime_idle() 回调。
但是,在某些系统上,系统睡眠不是通过全局固件或硬件操作进入的。相反,所有硬件组件都由内核以协调的方式直接置于低功耗状态。然后,系统睡眠状态有效地遵循硬件组件最终所处的状态,并且系统通过硬件中断或完全由内核控制的类似机制从该状态唤醒。因此,内核永远不会放弃控制权,并且内核确切地知道恢复期间所有设备的状态。如果情况如此,并且没有发生上述任何情况(特别是如果系统没有从休眠状态唤醒),则将系统挂起开始之前已挂起的设备保留在挂起状态可能会更有效。
为此,PM 核心提供了一种机制,允许设备层级之间进行某种程度的协调。具体来说,如果一个系统的挂起 .prepare() 回调为某个设备返回一个正数,则向 PM 核心表明该设备似乎已运行时挂起,并且其状态良好,因此只要其所有后代也保持运行时挂起状态,则可以将其保留在运行时挂起状态。如果发生这种情况,PM 核心将不会对所有这些设备执行任何系统挂起和恢复回调,除了 .complete() 回调之外,该回调完全负责以适当的方式处理设备。这仅适用于与休眠无关的系统挂起转换(有关详细信息,请参阅设备电源管理基础)。
PM 核心会尽力减少运行时 PM 和系统挂起/恢复(以及休眠)回调之间的竞争条件,方法是执行以下操作:
在系统挂起期间,在为每个设备执行子系统级 .prepare() 回调之前,会调用 pm_runtime_get_noresume(),并在为每个设备执行子系统级 .suspend() 回调之前,会调用 pm_runtime_barrier()。除此之外,PM 核心还在为每个设备执行子系统级 .suspend_late() 回调之前,调用 __pm_runtime_disable(),并将“false”作为第二个参数。
在系统恢复期间,在为每个设备执行子系统级 .resume_early() 回调之后,会调用 pm_runtime_enable(),并在为每个设备执行子系统级 .complete() 回调之后,会调用 pm_runtime_put()。
7. 通用子系统回调¶
子系统可能希望通过使用 PM 核心提供的一组通用电源管理回调来节省代码空间,这些回调在 driver/base/power/generic_ops.c 中定义。
- int pm_generic_runtime_suspend(struct device *dev);
调用此设备的驱动程序提供的 ->runtime_suspend() 回调并返回其结果,如果未定义则返回 0
- int pm_generic_runtime_resume(struct device *dev);
调用此设备的驱动程序提供的 ->runtime_resume() 回调并返回其结果,如果未定义则返回 0
- int pm_generic_suspend(struct device *dev);
如果设备在运行时未被挂起,则调用其驱动程序提供的 ->suspend() 回调并返回其结果,如果未定义则返回 0
- int pm_generic_suspend_noirq(struct device *dev);
如果 pm_runtime_suspended(dev) 返回 “false”,则调用设备的驱动程序提供的 ->suspend_noirq() 回调并返回其结果,如果未定义则返回 0
- int pm_generic_resume(struct device *dev);
调用此设备的驱动程序提供的 ->resume() 回调,如果成功,则将设备的运行时 PM 状态更改为 “active”
- int pm_generic_resume_noirq(struct device *dev);
调用此设备的驱动程序提供的 ->resume_noirq() 回调
- int pm_generic_freeze(struct device *dev);
如果设备在运行时未被挂起,则调用其驱动程序提供的 ->freeze() 回调并返回其结果,如果未定义则返回 0
- int pm_generic_freeze_noirq(struct device *dev);
如果 pm_runtime_suspended(dev) 返回 “false”,则调用设备的驱动程序提供的 ->freeze_noirq() 回调并返回其结果,如果未定义则返回 0
- int pm_generic_thaw(struct device *dev);
如果设备在运行时未被挂起,则调用其驱动程序提供的 ->thaw() 回调并返回其结果,如果未定义则返回 0
- int pm_generic_thaw_noirq(struct device *dev);
如果 pm_runtime_suspended(dev) 返回 “false”,则调用设备的驱动程序提供的 ->thaw_noirq() 回调并返回其结果,如果未定义则返回 0
- int pm_generic_poweroff(struct device *dev);
如果设备在运行时未被挂起,则调用其驱动程序提供的 ->poweroff() 回调并返回其结果,如果未定义则返回 0
- int pm_generic_poweroff_noirq(struct device *dev);
如果 pm_runtime_suspended(dev) 返回 “false”,则运行设备的驱动程序提供的 ->poweroff_noirq() 回调并返回其结果,如果未定义则返回 0
- int pm_generic_restore(struct device *dev);
调用此设备的驱动程序提供的 ->restore() 回调,如果成功,则将设备的运行时 PM 状态更改为 “active”
- int pm_generic_restore_noirq(struct device *dev);
调用此设备的驱动程序提供的 ->restore_noirq() 回调
如果子系统没有为其子系统级的 dev_pm_ops 结构中的 ->runtime_idle()、->runtime_suspend()、->runtime_resume()、->suspend()、->suspend_noirq()、->resume()、->resume_noirq()、->freeze()、->freeze_noirq()、->thaw()、->thaw_noirq()、->poweroff()、->poweroff_noirq()、->restore()、->restore_noirq() 提供自己的回调,则这些函数是 PM 核心使用的默认值。
希望将同一函数用作系统挂起、冻结、关机和运行时挂起回调,以及类似地用于系统恢复、解冻、还原和运行时恢复的设备驱动程序,可以在 include/linux/pm_runtime.h 中定义的 DEFINE_RUNTIME_DEV_PM_OPS() 的帮助下实现类似的行为(可能会将其最后一个参数设置为 NULL)。
8. “无回调” 设备¶
某些“设备”只是其父设备的逻辑子设备,无法单独进行电源管理。(原型示例是 USB 接口。整个 USB 设备可以进入低功耗模式或发送唤醒请求,但单个接口都无法实现。)这些设备的驱动程序不需要运行时 PM 回调;如果回调存在,->runtime_suspend() 和 ->runtime_resume() 将始终返回 0 而不执行其他任何操作,并且 ->runtime_idle() 将始终调用 pm_runtime_suspend()。
子系统可以通过调用 pm_runtime_no_callbacks() 来告知 PM 核心这些设备。这应该在设备结构初始化之后和注册之前完成(尽管在设备注册之后也可以)。该例程将设置设备的 power.no_callbacks 标志,并防止创建非调试运行时 PM sysfs 属性。
当设置 power.no_callbacks 时,PM 核心不会调用 ->runtime_idle()、->runtime_suspend() 或 ->runtime_resume() 回调。相反,它会假设挂起和恢复始终成功,并且空闲设备应该被挂起。
因此,PM 核心永远不会直接将运行时电源更改通知给设备的子系统或驱动程序。相反,设备父设备的驱动程序必须负责在父设备的电源状态更改时通知设备的驱动程序。
请注意,在某些情况下,子系统/驱动程序可能不希望为其设备调用 pm_runtime_no_callbacks()。这可能是因为需要实现运行时 PM 回调的子集,平台相关的 PM 域可能会附加到设备,或者设备是通过供应商设备链接进行电源管理的。出于这些原因,以及为了避免子系统/驱动程序中的样板代码,PM 核心允许不分配运行时 PM 回调。更准确地说,如果回调指针为 NULL,则 PM 核心将表现得好像存在回调并且它返回 0。
9. 自动挂起,或自动延迟挂起¶
更改设备的电源状态不是免费的;它既需要时间和能量。只有在有理由认为设备将在此状态下保持相当长一段时间时,才应将设备置于低功耗状态。一个常见的启发式方法说,一段时间未使用过的设备很可能仍会保持未使用状态;根据此建议,驱动程序不应允许设备在运行时挂起,除非它们已处于非活动状态一段时间。即使启发式方法最终不是最优的,它仍然可以防止设备在低功耗和全功率状态之间过于快速地“弹跳”。
术语“自动挂起”是历史遗留物。它并不意味着设备会自动挂起(子系统或驱动程序仍然必须调用适当的 PM 例程);相反,它意味着运行时挂起将自动延迟,直到所需的非活动时间段过去。
非活动状态是根据 power.last_busy 字段确定的。驱动程序应在执行 I/O 后调用 pm_runtime_mark_last_busy() 来更新此字段,通常是在调用 __pm_runtime_put_autosuspend() 之前。所需的非活动时间段的长度是一个策略问题。子系统可以通过调用 pm_runtime_set_autosuspend_delay() 来最初设置此长度,但是在设备注册之后,该长度应由用户空间控制,使用 /sys/devices/.../power/autosuspend_delay_ms 属性。
为了使用自动挂起,子系统或驱动程序必须调用 pm_runtime_use_autosuspend()(最好在注册设备之前),此后他们应使用各种 *_autosuspend() 辅助函数,而不是非自动挂起对应的函数
Instead of: pm_runtime_suspend use: pm_runtime_autosuspend;
Instead of: pm_schedule_suspend use: pm_request_autosuspend;
Instead of: pm_runtime_put use: __pm_runtime_put_autosuspend;
Instead of: pm_runtime_put_sync use: pm_runtime_put_sync_autosuspend.
驱动程序也可以继续使用非自动挂起辅助函数;它们将正常运行,这意味着有时会考虑自动挂起延迟(请参阅 pm_runtime_idle)。
在某些情况下,即使使用计数器为零并且自动挂起延迟时间已过期,驱动程序或子系统也可能希望阻止设备立即自动挂起。如果 ->runtime_suspend() 回调返回 -EAGAIN 或 -EBUSY,并且如果下一个自动挂起延迟过期时间在将来(如果回调调用 pm_runtime_mark_last_busy(),则通常会如此),则 PM 核心将自动重新安排自动挂起。->runtime_suspend() 回调本身无法执行此重新安排,因为在设备挂起时(即,在回调运行时)不接受任何类型的挂起请求。
该实现非常适合在中断上下文中进行异步使用。但是,这种使用不可避免地会涉及竞争,因为 PM 核心无法将 ->runtime_suspend() 回调与 I/O 请求的到达同步。此同步必须由驱动程序使用其私有锁处理。这是一个示意性的伪代码示例
foo_read_or_write(struct foo_priv *foo, void *data)
{
lock(&foo->private_lock);
add_request_to_io_queue(foo, data);
if (foo->num_pending_requests++ == 0)
pm_runtime_get(&foo->dev);
if (!foo->is_suspended)
foo_process_next_request(foo);
unlock(&foo->private_lock);
}
foo_io_completion(struct foo_priv *foo, void *req)
{
lock(&foo->private_lock);
if (--foo->num_pending_requests == 0) {
pm_runtime_mark_last_busy(&foo->dev);
__pm_runtime_put_autosuspend(&foo->dev);
} else {
foo_process_next_request(foo);
}
unlock(&foo->private_lock);
/* Send req result back to the user ... */
}
int foo_runtime_suspend(struct device *dev)
{
struct foo_priv foo = container_of(dev, ...);
int ret = 0;
lock(&foo->private_lock);
if (foo->num_pending_requests > 0) {
ret = -EBUSY;
} else {
/* ... suspend the device ... */
foo->is_suspended = 1;
}
unlock(&foo->private_lock);
return ret;
}
int foo_runtime_resume(struct device *dev)
{
struct foo_priv foo = container_of(dev, ...);
lock(&foo->private_lock);
/* ... resume the device ... */
foo->is_suspended = 0;
pm_runtime_mark_last_busy(&foo->dev);
if (foo->num_pending_requests > 0)
foo_process_next_request(foo);
unlock(&foo->private_lock);
return 0;
}
重点是,在 foo_io_completion() 请求自动挂起之后,foo_runtime_suspend() 回调可能会与 foo_read_or_write() 竞争。因此,foo_runtime_suspend() 必须(在持有私有锁的同时)检查是否有任何待处理的 I/O 请求,然后才允许继续挂起。
此外,用户空间可以随时更改 power.autosuspend_delay 字段。如果驱动程序关心这一点,它可以在持有其私有锁的同时,从 ->runtime_suspend() 回调中调用 pm_runtime_autosuspend_expiration()。如果该函数返回非零值,则表示延迟尚未过期,回调应返回 -EAGAIN。