I/O 设备的运行时电源管理框架

  1. 2009-2011 Rafael J. Wysocki <rjw@sisk.pl>, Novell Inc.

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

  1. 2014 Intel Corp., Rafael J. Wysocki <rafael.j.wysocki@intel.com>

1. 简介

I/O 设备的运行时电源管理 (runtime PM) 支持由电源管理核心 (PM 核心) 层提供,具体方式包括:

  • 电源管理工作队列 pm_wq,总线类型和设备驱动程序可以将与 PM 相关的工作项放入其中。强烈建议使用 pm_wq 来排队所有与运行时 PM 相关的工作项,因为这允许它们与系统范围的电源转换(内存挂起、休眠和从系统睡眠状态恢复)同步。pm_wqinclude/linux/pm_runtime.h 中声明,在 kernel/power/main.c 中定义。

  • struct devicepower 成员(类型为 struct dev_pm_info,定义在 include/linux/pm.h 中)中的多个运行时 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);
      ...
};

->runtime_suspend()->runtime_resume()->runtime_idle() 回调由 PM 核心为设备的子系统执行,该子系统可以是以下任一:

  1. 设备的 PM 域,如果设备的 PM 域对象 dev->pm_domain 存在。

  2. 设备的设备类型,如果 dev->typedev->type->pm 都存在。

  3. 设备的设备类别,如果 dev->classdev->class->pm 都存在。

  4. 设备的总线类型,如果 dev->busdev->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(s) 和 RAM 通信。设备在成功执行挂起回调后的运行时 PM 状态为“suspended”。

  • 如果挂起回调返回 -EBUSY-EAGAIN,则设备的运行时 PM 状态仍保持“active”,这意味着设备在此之后必须完全运行。

  • 如果挂起回调返回的错误代码与 -EBUSY-EAGAIN 不同,PM 核心将此视为致命错误,并将拒绝为该设备运行第 4 节中描述的辅助函数,直到其状态直接设置为“active”或“suspended”(PM 核心为此目的提供了特殊的辅助函数)。

特别地,如果驱动程序需要远程唤醒功能(即允许设备请求更改其电源状态的硬件机制,例如 PCI PME)才能正常运行,并且 device_can_wakeup() 为该设备返回“false”,则 ->runtime_suspend() 应该返回 -EBUSY。另一方面,如果 device_can_wakeup() 为该设备返回“true”,并且设备在执行挂起回调期间进入低功耗状态,则预计将为该设备启用远程唤醒。通常,所有在运行时进入低功耗状态的输入设备都应启用远程唤醒。

子系统级恢复回调(如果存在)完全负责 适当地处理设备的恢复,这可以(但不必)包括执行设备驱动程序自己的 ->runtime_resume() 回调(从 PM 核心的角度来看,只要子系统级恢复回调知道如何处理设备,就没必要在设备驱动程序中实现 ->runtime_resume() 回调)。

  • 一旦子系统级恢复回调(如果直接调用,则是驱动程序恢复回调)成功完成,PM 核心就将该设备视为完全运行,这意味着设备必须能够根据需要完成 I/O 操作。设备的运行时 PM 状态随后变为“active”。

  • 如果恢复回调返回错误代码,PM 核心将此视为致命错误,并将拒绝为该设备运行第 4 节中描述的辅助函数,直到其状态直接设置为“active”或“suspended”(通过 PM 核心为此目的提供的特殊辅助函数)。

空闲回调(如果存在,则为子系统级回调,否则为驱动程序回调)由 PM 核心在设备看似空闲时执行,这通过两个计数器指示给 PM 核心:设备的使用计数器和设备的“active”子设备计数器。

  • 如果使用 PM 核心提供的辅助函数减少了这些计数器中的任何一个,并且结果为零,则检查另一个计数器。如果该计数器也为零,PM 核心将以设备作为参数执行空闲回调。

空闲回调执行的操作完全取决于相关的子系统(或驱动程序),但预期和推荐的操作是检查设备是否可以挂起(即是否满足挂起设备所需的所有条件),并在这种情况下为设备排队挂起请求。如果没有空闲回调,或者回调返回 0,那么 PM 核心将尝试对设备执行运行时挂起,同时也会尊重配置为自动挂起的设备。本质上,这意味着调用 pm_runtime_autosuspend()(请注意,在这种情况下,驱动程序需要更新设备最后忙碌标记 pm_runtime_mark_last_busy() 来控制延迟)。为了防止这种情况(例如,如果回调例程已经启动了延迟挂起),该例程必须返回非零值。负错误返回代码会被 PM 核心忽略。

第 4 节中描述的 PM 核心提供的辅助函数保证了针对单个设备的运行时 PM 回调满足以下约束:

  1. 回调是互斥的(例如,禁止为同一设备并行执行 ->runtime_suspend()->runtime_resume() 或另一个 ->runtime_suspend() 实例),但 ->runtime_suspend()->runtime_resume() 可以与 ->runtime_idle() 并行执行(尽管在为同一设备执行其他任何回调时,->runtime_idle() 不会启动)。

  2. ->runtime_idle()->runtime_suspend() 只能为“active”设备执行(即,PM 核心只为运行时 PM 状态为“active”的设备执行 ->runtime_idle()->runtime_suspend())。

  3. ->runtime_idle()->runtime_suspend() 只能为满足以下条件的设备执行:其使用计数器为零 并且 其“active”子设备计数器为零,或者其 power.ignore_children 标志已设置。

  4. ->runtime_resume() 只能为“suspended”设备执行(即,PM 核心只为运行时 PM 状态为“suspended”的设备执行 ->runtime_resume())。

此外,PM 核心提供的辅助函数遵循以下规则:

  • 如果 ->runtime_suspend() 即将执行或有待处理的执行请求,则 ->runtime_idle() 不会为同一设备执行。

  • 执行或调度 ->runtime_suspend() 的请求将取消为同一设备执行 ->runtime_idle() 的任何待处理请求。

  • 如果 ->runtime_resume() 即将执行或有待处理的执行请求,则其他回调不会为同一设备执行。

  • 执行 ->runtime_resume() 的请求将取消为同一设备执行其他回调的任何待处理或已调度请求,但已调度的自动挂起除外。

3. 运行时 PM 设备字段

struct dev_pm_info(定义在 include/linux/pm.h 中)中存在以下设备运行时 PM 字段:

struct timer_list suspend_timer;
  • 用于调度(延迟)挂起和自动挂起请求的定时器

unsigned long timer_expires;
  • 定时器过期时间,以 jiffies 为单位(如果此值不为零,则定时器正在运行并将在该时间过期,否则定时器未运行)

struct work_struct work;
  • 用于排队请求的工作结构(即 pm_wq 中的工作项)

wait_queue_head_t wait_queue;
  • 当任何辅助函数需要等待另一个函数完成时使用的等待队列

spinlock_t lock;
  • 用于同步的锁

atomic_t usage_count;
  • 设备的使用计数器

atomic_t child_count;
  • 设备的“active”子设备计数

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_resume() 即将运行,而 ->runtime_suspend() 正在为该设备执行,并且等待挂起完成不切实际时设置;意味着“一旦挂起完成就立即开始恢复”

enum rpm_status runtime_status;
  • 设备的运行时 PM 状态;此字段的初始值为 RPM_SUSPENDED,这意味着 PM 核心最初将每个设备视为“suspended”,无论其真实的硬件状态如何

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 devicepower 成员的组成部分。

4. 运行时 PM 设备辅助函数

以下运行时 PM 辅助函数定义在 drivers/base/power/runtime.cinclude/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 状态已为“suspended”则返回 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 状态已为“active”(如果 power.disable_depth 非零,但状态在从 0 变为 1 时为“active”,也返回 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 状态是否已为“active”),失败时返回 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 运行时状态已为“suspended”则返回 1,如果请求尚未调度(如果 delay 为 0,则未入队)则返回错误代码;如果 ->runtime_suspend() 的执行已调度且尚未过期,则 delay 的新值将用作等待时间

int pm_request_resume(struct device *dev);
  • 提交执行设备子系统级恢复回调的请求(该请求由 pm_wq 中的工作项表示);成功时返回 0,如果设备运行时 PM 状态已为“active”则返回 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 状态设置为“active”,并相应地更新其父设备的“active”子设备计数器(仅当 power.runtime_error 已设置或 power.disable_depth 大于零时,使用此函数才有效);如果设备有一个未处于活跃状态且 power.ignore_children 标志未设置的父设备,则它将失败并返回错误代码

void pm_runtime_set_suspended(struct device *dev);
  • 清除设备的 power.runtime_error 标志,将设备的运行时 PM 状态设置为“suspended”,并相应地更新其父设备的“active”子设备计数器(仅当 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 已设置,则根据 power.autosuspend_delay 是否更改为负值或从负值更改,可能会调用 pm_runtime_get_sync 或减少设备的使用计数器并调用 pm_runtime_idle;如果 power.use_autosuspend 已清除,则调用 pm_runtime_idle

unsigned long pm_runtime_autosuspend_expiration(struct device *dev);
  • 根据 power.last_busypower.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 都被禁用,这意味着第 4 节中描述的大多数运行时 PM 辅助函数将返回 -EAGAIN,直到为设备调用 pm_runtime_enable()

此外,所有设备的初始运行时 PM 状态都是“suspended”,但这不一定反映设备的实际物理状态。因此,如果设备最初是活跃的(即能够处理 I/O),则在为设备调用 pm_runtime_enable() 之前,必须借助 pm_runtime_set_active() 将其运行时 PM 状态更改为“active”。

然而,如果设备有父设备且父设备的运行时 PM 已启用,则为该设备调用 pm_runtime_set_active() 将影响父设备,除非父设备的 power.ignore_children 标志已设置。也就是说,在这种情况下,只要子设备的状态为“active”,父设备就无法在运行时使用 PM 核心的辅助函数进行挂起,即使子设备的运行时 PM 仍然禁用(即尚未为子设备调用 pm_runtime_enable() 或已为其调用 pm_runtime_disable())。因此,一旦为设备调用了 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 和系统睡眠(即系统挂起和休眠,也称为内存挂起和磁盘挂起)以多种方式相互作用。如果设备在系统睡眠开始时处于活跃状态,一切都很简单。但如果设备已经挂起,应该发生什么?

设备可能对运行时 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 核心将不会为所有这些设备执行任何系统挂起和恢复回调,除了 .complete() 回调,该回调随后将完全负责适当地处理设备。这仅适用于与休眠无关的系统挂起转换(更多信息请参见设备电源管理基础)。

PM 核心通过执行以下操作,尽力减少运行时 PM 与系统挂起/恢复(和休眠)回调之间的竞争条件:

  • 在系统挂起期间,在为每个设备执行子系统级 .prepare() 回调之前,会为其调用 pm_runtime_get_noresume(),并在为每个设备执行子系统级 .suspend() 回调之前,会为其调用 pm_runtime_barrier()。此外,PM 核心会在为每个设备执行子系统级 .suspend_late() 回调之前,以“false”作为第二个参数调用 __pm_runtime_disable()

  • 在系统恢复期间,分别在为每个设备执行子系统级 .resume_early() 回调之后和执行子系统级 .complete() 回调之后,会为其调用 pm_runtime_enable()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