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

发布时间 2023-12-12 16:08:02作者: 摩斯电码

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设备的运行时电源管理(runtime PM)支持是通过电源管理核心(PM核心)层提供的,具体包括:

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

  • “struct device”中的“power”成员中的一些运行时PM字段(类型为“struct dev_pm_info”,在include/linux/pm.h中定义),可用于将运行时PM操作与彼此同步。

  • “struct dev_pm_ops”中的三个设备运行时PM回调(在include/linux/pm.h中定义)。

  • 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状态应为'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核心会忽略负错误返回代码。

PM核心提供的辅助函数在第4节中描述,保证了对于一个设备的运行时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:用于排队请求的工作结构(即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. 设备运行时电源管理辅助函数

以下运行时电源管理辅助函数定义在drivers/base/power/runtime.c和include/linux/pm_runtime.h中:

  • void pm_runtime_init(struct device *dev);
    初始化“struct dev_pm_info”中的设备运行时电源管理字段。

  • void pm_runtime_remove(struct device *dev);
    确保在从设备层次结构中移除设备后,设备的运行时电源管理将被禁用。

  • int pm_runtime_idle(struct device *dev);
    执行设备的子系统级空闲回调;在失败时返回错误代码,其中-EINPROGRESS表示->runtime_idle()已经在执行;如果没有回调或回调返回0,则运行pm_runtime_autosuspend(dev)并返回其结果。

  • int pm_runtime_suspend(struct device *dev);
    执行设备的子系统级挂起回调;成功时返回0,如果设备的运行时电源管理状态已经是'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,如果设备的运行时电源管理状态已经是'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),如果成功,则增加设备的使用计数;返回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,如果设备的运行时电源管理状态已经是'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, bool ign_usage_count);
    如果'power.disable_depth'不为零,则返回-EINVAL;否则,如果运行时PM状态为RPM_ACTIVE,且ign_usage_count为true或设备的usage_count不为零,则增加计数并返回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);
    减少设备的使用计数;如果结果为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'字段;如果该字段等于零,则运行时电源管理辅助函数可以执行第2节中描述的设备的子系统级回调。

  • int pm_runtime_disable(struct device *dev);
    增加设备的'power.disable_depth'字段(如果该字段的值先前为零,则阻止运行时电源管理的子系统级回调),确保设备上的所有挂起的运行时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,则可能调用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_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. 运行时电源管理初始化、设备探测和移除

首先,对于所有设备来说,初始时运行时电源管理(runtime PM)都是被禁用的,这意味着在为设备调用 Section 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_runtime_enable() 或已为其调用 pm_runtime_disable()),父级就无法使用 PM 核心的辅助函数在运行时挂起。因此,一旦为设备调用了 pm_runtime_set_active(),就应尽快调用 pm_runtime_enable(),或者使用 pm_runtime_set_suspended() 将其运行时 PM 状态更改回'suspended'。

如果设备的默认初始运行时 PM 状态(即'suspended')反映了设备的实际状态,那么它的总线类型或其驱动程序的 ->probe() 回调可能需要使用 Section 4 中描述的 PM 核心的辅助函数之一来唤醒它。在这种情况下,应使用 pm_runtime_resume()。当然,为了实现这一目的,必须先通过调用 pm_runtime_enable() 启用设备的运行时 PM。

需要注意的是,如果设备可能在探测过程中执行 pm_runtime 调用(例如,如果它已向可能回调的子系统注册),则配对使用 pm_runtime_get_sync() 调用和 pm_runtime_put() 调用将是适当的,以确保设备在探测过程中不会被重新置于休眠状态。这种情况可能发生在诸如网络设备层之类的系统中。

一旦 ->probe() 完成,可能希望在设备上挂起。因此,驱动程序核心使用异步的 pm_request_idle() 在那时提交请求,以执行设备的子系统级空闲回调。使用运行时自动挂起功能的驱动程序可能希望在从 ->probe() 返回之前更新最后的繁忙标记。

此外,驱动程序核心通过在 driver_sysfs_remove() 和 BUS_NOTIFY_UNBIND_DRIVER 通知之前调用 pm_runtime_get_sync() 来防止运行时 PM 回调与总线通知回调竞争。这是必要的,因为一些子系统使用该通知器来执行影响运行时 PM 功能的操作。这样做可以在执行这些例程时恢复设备,如果它处于挂起状态,则防止其再次被挂起。

为了允许总线类型和驱动程序通过在其 ->remove() 例程中调用 pm_runtime_suspend() 将设备置于挂起状态,驱动程序核心在运行 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()。原则上,驱动程序也可以通过这种方式有效地关闭设备的运行时电源管理,直到用户空间将其打开。然而,需要注意的是,如果用户空间已经有意将 /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()。此外,在执行其子系统级别的 .suspend_late() 回调之前,对于每个设备,PM 核心会调用带有'false'作为第二个参数的 __pm_runtime_disable()。
  • 在系统恢复期间,对于每个设备,在执行其子系统级别的 .resume_early() 回调之后,会调用 pm_runtime_enable() 和 pm_runtime_put(),在执行其子系统级别的 .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.h中定义的UNIVERSAL_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. 自动挂起,或者自动延迟挂起

改变设备的电源状态并不是免费的;它需要时间和能量。只有在有理由认为设备将长时间保持在低功耗状态时,才应该将设备置于低功耗状态。一个常见的启发式规则是,长时间未使用的设备可能会继续保持未使用状态;根据这一建议,驱动程序不应该允许设备在运行时挂起,直到它们处于非活动状态一段最短时间。即使启发式规则最终变得不太理想,它仍将防止设备在低功耗状态和全功耗状态之间过快地"跳动"。

术语"autosuspend"是一个历史遗留物。它并不意味着设备会自动挂起(子系统或驱动程序仍然必须调用适当的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属性。

为了使用autosuspend,子系统或驱动程序必须调用pm_runtime_use_autosuspend()(最好是在注册设备之前),此后它们应该使用各种*_autosuspend()辅助函数,而不是非autosuspend的对应函数:

而不是:pm_runtime_suspend    使用:pm_runtime_autosuspend;
而不是:pm_schedule_suspend   使用:pm_request_autosuspend;
而不是:pm_runtime_put        使用:pm_runtime_put_autosuspend;
而不是:pm_runtime_put_sync   使用:pm_runtime_put_sync_autosuspend。

驱动程序也可以继续使用非autosuspend的辅助函数;它们将正常工作,这意味着有时会考虑到autosuspend延迟(参见pm_runtime_idle)。

在某些情况下,驱动程序或子系统可能希望阻止设备立即自动挂起,即使使用计数器为零并且autosuspend延迟时间已经过期。如果->runtime_suspend()回调返回-EAGAIN或-EBUSY,并且下一个autosuspend延迟到期时间在未来(通常情况下,如果回调调用了pm_runtime_mark_last_busy(),它将是这样),PM核心将自动重新安排autosuspend。->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。