1. 前言
相信工作稍微久一点的linux驱动工程师都深有体会:
在旧时光里,实现某一个设备的电源管理功能,是非常简单的一件事情。大多数设备都被抽象为platform设备,driver只需要提供suspend/resume/shutdown等回调函数,并注册到kernel即可。kernel会在系统电源状态切换的过程中,调用driver提供的回调函数,切换设备的电源状态。
但是在新时代中,设备电源管理有关的操作,被统一封装在struct dev_pm_ops结构中了。该结构包含20多个回调函数,再加上复杂的电源管理机制(常规的suspend/resume、runtime PM等等),使设备驱动的电源管理工作不再那么单纯,工程师(如蜗蜗自己)的思路也不再特别清晰。
因此本文希望能以单一设备的电源管理为出发点,结合kernel的电源管理机制,介绍怎样在设备驱动中添加电源管理功能,并分析设备电源状态切换和系统电源状态切换的关系。
另外,我们在电源管理系列文章中,介绍了很多的电源管理机制,如generic PM、wakeup event framework、wakelock、autosleep、runtime PM、PM domain、等等,本文也算是对它们的梳理和总结。
2. 功能描述
设备的电源状态切换,和系统电源状态切换基本保持一致(runtime PM除外),切换的场景如下:
1)系统reboot的过程,包括halt、power off、restart等(可参考“Linux电源管理(3)_Generic PM之Reboot过程”),要求设备进入shutdown状态,以避免意外产生。
2)系统suspend/resume的过程(可参考“Linux电源管理(6)_Generic PM之Suspend功能”),要求设备也同步suspend/resume。
3)系统hibernate及恢复的过程,要求设备在suspend/resume的基础上,增加poweroff的动作。
4)runtime PM过程(可参考“Linux电源管理(11)_Runtime PM之功能描述”),要求设备在引用计数为0时suspend甚至power off,并在引用计数大于0时power on以及resume。
旧有的电源管理框架中,通过bus、class、device_driver等结构体中的shutdown、suspend、resume三个回调函数,就可以实现上面除runtime PM之外的所有功能。但是在新框架中,特别是引入struct dev_pm_ops结构之后,其中的suspend/resume就不再推荐使用了。
不过,对有些设备来说,例如platform device,如果电源管理需求不是很复杂,driver工程师仍然可以使用旧的方法实现,kernel会自动帮忙转换为新的方式。但是,如果有更多需求,就不得不面对struct dev_pm_ops了。下面将会详细说明。
3. 数据结构回顾
正式开始之前,我们先回顾一下设备电源管理有关的数据结构。它们大多都在之前的文章中介绍过了,本文放在一起,权当一个总结。
3.1 .shutdown回调函数以及使用方法
由于reboot过程是相对独立和稳定的,且该过程依赖于设备的.shutdown回调函数,这里把它独立出来,单独描述,后面就不再涉及了。
.shutdown回调函数存在于两个数据结构中:struct device_driver和struct bus_type,在系统reboot的过程中被调用,负责关闭设备。设备驱动可以根据需要,实现其中的一个。我们以一个普通的platform设备为例,介绍这个过程。
1)定义一个platform_driver,并实现其.shutdown回调,然后调用platform_driver_register将它注册到kernel中
1: static void foo_shutdown(struct platform_device *pdev) 2: { 3: ... 4: } 5: static platform_driver foo_pdrv = 6: { 7: .shutdown = foo_shutdown, 8: ... 9: };
2)platform_driver_register时,会把struct device_driver变量的shutdown函数,替换为platform设备特有的shutdown函数(platform_drv_shutdown),并调用driver_register将device_driver注册到kernel
1: int __platform_driver_register(struct platform_driver *drv, 2: struct module *owner) 3: { 4: … 5: 6: if (drv->shutdown) 7: drv->driver.shutdown = platform_drv_shutdown; 8: 9: return driver_register(&drv->driver); 10: }
3)系统reboot的过程中,会调用每个设备的shutdown函数。对这里的foo_pdrv而言,会先调用platform_drv_shutdown,它继续调用foo_shutdown。
3.2 legacy的不再使用的.suspend/.resume
旧的suspend/resume操作,主要依赖struct device_driver、struct class、struct bus_type等结构中的suspend和resume回调函数,其使用方式和上面的.shutdown几乎完全一样。以platform设备为例,只需多定义两个函数即可,如下:
1: static int foo_suspend(struct platform_device *pdev, pm_message_t state) 2: { 3: ... 4: } 5: 6: static int foo_resume(struct platform_device *pdev) 7: { 8: ... 9: } 10: 11: static void foo_shutdown(struct platform_device *pdev) 12: { 13: ... 14: } 15: 16: static platform_driver foo_pdrv = { 17: .suspend = foo_suspend, 18: .resume = foo_resume, 19: .shutdown = foo_shutdown, 20: ... 21: };
在较新的kernel中,已经不再建议使用这些回调函数了,但对platform设备来说,如果场景比较简单,可以照旧使用上面的实现方法,platform.c会自动帮忙转换为struct dev_pm_ops回调,具体请参考后面描述。
3.3 struct dev_pm_ops结构
struct dev_pm_ops是设备电源管理的核心数据结构,用于封装和设备电源管理有关的所有操作。
/** * struct dev_pm_ops - 设备电源管理回调函数结构体。 * * @prepare: 这个回调函数的主要作用是在返回后阻止设备的新子设备注册(一旦 @prepare() 成功返回后, * 驱动程序的子系统和通常是内核的其余部分也应该防止再次调用探测方法)。如果 @prepare() * 发现它无法处理的情况(例如,正在进行子设备的注册),它可以返回 -EAGAIN,以便 PM * 核心可以再次执行它(例如,在新子设备已注册后)以从竞态条件中恢复。该方法对所有种类的挂起 * 过渡都会执行,并在之后执行挂起回调之一:@suspend()、@freeze() 或 @poweroff()。 * 如果过渡是挂起到内存或待机(即与休眠无关),则 @prepare() 的返回值可以用于指示 PM * 核心在整个过渡期间和在随后的恢复期间保持设备在运行时挂起状态,如果适用的话。也就是说, * 如果 @prepare() 返回一个正数,PM 核心将理解为声明设备似乎已经运行时挂起,并且如果所有 * 后代设备也保持在运行时挂起状态,那么可以在整个过渡期间和随后的恢复期间保持在该状态。 * 如果发生这种情况,@complete() 将直接在 @prepare() 之后执行,它必须确保在系统恢复后设备 * 的正常运行。PM 核心在开始调用任何设备的挂起回调之前执行子系统级 @prepare(),因此通常可以 * 假定在执行 @prepare() 期间设备是可用的或者对运行时恢复请求做出响应的。但是,设备驱动 * 程序不能假设此时用户空间的可用性,并且在 @prepare() 中请求固件是无效的(这太晚了)。在 * @prepare() 中从 GFP_KERNEL 模式中分配大量内存也是无效的(为了解决这些限制,驱动程序可以 * 注册挂起和休眠通知以在任务冻结之前执行)。 * * @complete: 撤销 @prepare() 所做的更改。此方法对所有种类的恢复过渡都会执行,其后跟着一个恢复回调: * @resume()、@thaw() 或 @restore()。如果在驱动程序的挂起回调之前发生状态过渡失败(例如, * 如果挂起回调对 PM 核心早先尝试挂起的其他设备失败),也会调用 @complete()。PM 核心在执行 * 所有设备的适当恢复回调之后执行子系统级 @complete()。如果在挂起过渡的开始时 @prepare() * 返回一个正数,并且设备在运行时挂起(没有执行任何挂起和恢复回调),那么在恢复时只会执行 * @complete() 回调。在这种情况下,@complete() 必须准备执行任何必要的操作,以确保系统恢复后 * 设备的正常运行。为此,@complete() 可以检查设备的 power.direct_complete 标志,以了解先前 * 的挂起和恢复回调是否已对其执行。 * * @suspend: 在将系统置于保留主存储器内容的睡眠状态之前执行。执行的确切操作取决于设备的子系统 * (PM 域、设备类型、类别或总线类型),但通常在子系统级别的 @suspend() 返回后, * 设备必须保持安静,以便在不执行任何 I/O 或 DMA 的情况下。子系统级 @suspend() 在调用 * 所有设备的子系统级 @prepare() 之后执行。 * * @suspend_late: 继续由 @suspend() 启动的操作。对于许多设备,@suspend_late() 可能指向与运行时挂起 * 回调相同的回调例程。 * * @resume: 在从保留主存储器内容的睡眠状态唤醒系统后执行。执行的确切操作取决于设备的子系统,但通常 * 驱动程序应该重新开始工作,响应硬件事件和软件请求(设备本身可能会保持在低功耗状态,等待 * 运行时恢复发生)。设备在其驱动程序的 @resume() 回调运行时的状态取决于平台和设备所属的子系统。 * 在大多数平台上,在 @resume() 期间对资源(如时钟)的可用性没有限制。在调用所有设备的子系统级 * @resume_noirq() 之后,将为所有设备执行子系统级 @resume()。 * * @resume_early: 准备执行 @resume()。对于许多设备,@resume_early() 可能指向与运行时恢复回调相同的回调例程。 * * @freeze: 在创建休眠图像之前执行的休眠特定回调。类似于 @suspend(),但它不应启用设备以发出唤醒事件或更改 * 其电源状态。大多数子系统(PCI 总线类型是一个显著的例外)期望驱动程序级别的 @freeze() 将设备设置保存 * 在内存中以供在随后的休眠恢复期间使用。 * 在调用所有设备的子系统级 @prepare() 之后,将执行子系统级 @freeze()。 * * @freeze_late: 继续由 @freeze() 启动的操作。类似于 @suspend_late(),但它不应启用设备以发出唤醒事件或更改 * 其电源状态。 * * @thaw: 在创建休眠图像之后执行的休眠特定回调,或者如果创建图像失败,则执行。还在尝试从此类图像中恢复主存储 * 内容失败后执行。撤销前面 @freeze() 所做的更改,以便设备可以以与 @freeze() 调用之前相同的方式操作。 * 在调用所有设备的子系统级 @thaw_noirq() 之后,也可能在过渡错误的情况下直接在 @freeze() 之后执行。 * * @thaw_early: 准备执行 @thaw()。撤销前面 @freeze_late() 所做的更改。 * * @poweroff: 在保存休眠图像后执行的休眠特定回调。类似于 @suspend(),但不需要将设备的设置保存在内存中。 * 在调用所有设备的子系统级 @prepare() 之后,将执行子系统级 @poweroff()。 * * @poweroff_late: 继续由 @poweroff() 启动的操作。类似于 @suspend_late(),但不需要将设备的设置保存在内存中。 * * @restore: 在从休眠图像中恢复主存储器内容后执行的休眠特定回调,类似于 @resume()。 * * @restore_early: 准备执行 @restore(),类似于 @resume_early()。 * * @suspend_noirq: 完成由 @suspend() 启动的操作。执行可能与驱动程序的中断处理程序竞争的任何对于挂起设备所需的 * 额外操作,而在执行 @suspend_noirq() 时保证其中断处理程序不会运行。通常预期设备将在子系统级 * @suspend_noirq() 成功返回后处于低功耗状态(适用于目标系统睡眠状态)。如果设备可以生成 * 系统唤醒信号并且被启用以唤醒系统,则应在此时配置它。但是,根据平台和设备的子系统,也可能允许 * @suspend() 或 @suspend_late() 将设备放入低功耗状态并配置为生成唤醒信号,在这种情况下通常不需要定义 * @suspend_noirq()。 * * @resume_noirq: 通过执行在 @resume() 期间可能与其驱动程序的中断处理程序竞争的操作,准备执行 @resume()。 * * @freeze_noirq: 完成由 @freeze() 启动的操作。执行可能与驱动程序的中断处理程序竞争的任何对于冻结设备所需的 * 额外操作,而在执行 @freeze_noirq() 时保证其中断处理程序不会运行。设备的电源状态不应该由 * @freeze()、@freeze_late() 或 @freeze_noirq() 更改,并且不应该通过这些回调配置为通过任何 * 回调信号系统唤醒。 * * @thaw_noirq: 通过执行在 @thaw() 期间可能与其驱动程序的中断处理程序竞争的操作,准备执行 @thaw()。 * * @poweroff_noirq: 完成由 @poweroff() 启动的操作。类似于 @suspend_noirq(),但不需要将设备的设置保存在内存中。 * * @restore_noirq: 通过执行在 @restore() 期间可能与其驱动程序的中断处理程序竞争的操作,准备执行 @restore(), * 类似于 @resume_noirq()。 * * @runtime_suspend: 为设备准备在由于电源管理而无法与 CPU(s) 和 RAM 通信的条件。这不一定意味着设备应该被置于 * 低功耗状态。例如,如果设备位于即将关闭的链路后面,则设备可能仍然处于完全电源状态。如果设备 * 确实进入低功耗状态并且能够生成运行时唤醒事件,则应为其启用远程唤醒(即硬件机制允许设备通过 * 中断请求请求其电源状态更改)。 * * @runtime_resume: 在硬件生成的唤醒事件或软件请求的响应中,将设备置于完全活动状态。如果需要,将设备置于完全电源状态 * 并恢复其寄存器,以使其完全运行。 * * @runtime_idle: 设备似乎处于非活动状态,如果所有必要的条件都得到满足,它可能被置于低功耗状态。检查这些条件, * 如果适合,返回 0,表示允许 PM 核心为设备排队挂起请求。 * * /* * 结构体 dev_pm_ops 定义了设备的电源管理操作,包括准备、完成、挂起、恢复等多个阶段的回调函数。 * 这些操作涉及到设备的电源状态转换,可能影响到I/O队列、中断、唤醒、DMA等硬件状态。 * 部分操作是对外可见的,而另一部分是内部的低功耗模式转换,对外部不可见(例如关闭未在使用中的时钟)。 */ 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_late)(struct device *dev); // 唤醒前期执行的回调函数,返回错误代码。 int (*resume_early)(struct device *dev); // 冻结后期执行的回调函数,返回错误代码。 int (*freeze_late)(struct device *dev); // 解冻前期执行的回调函数,返回错误代码。 int (*thaw_early)(struct device *dev); // 关闭电源后期执行的回调函数,返回错误代码。 int (*poweroff_late)(struct device *dev); // 恢复后期执行的回调函数,返回错误代码。 int (*restore_early)(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); }; /* * 以上回调函数中,除了 @complete() 以外的所有函数都返回错误代码。 * 在 @resume() 等函数中返回的错误代码不会导致 PM 核心在恢复期间中止。 * 这些错误代码仅用于调试目的,会记录在系统日志中。 * 建议驱动程序仅在发生不可恢复的失败时(例如设备拒绝恢复且无法使用)才从恢复方法中返回错误代码, * 以便将来可以修改 PM 核心以避免尝试处理无法恢复的设备及其子设备。 * * 在执行以上回调函数期间,允许取消注册设备,但回调函数不能尝试注销其调用的设备, * 尽管它可以注销该设备的子设备(例如,在系统休眠时检测到子设备被拔掉)。 * * 此外,还有与设备运行时电源管理相关的回调函数,这些回调函数由 PM 核心执行,针对子系统(PM 域、设备类型、类别和总线类型), * 期望子系统级回调调用驱动程序的回调函数。设备驱动程序的回调函数的确切操作通常取决于设备所属的平台和子系统。 * 有关设备运行时电源管理中 @runtime_suspend()、@runtime_resume() 和 @runtime_idle() 回调函数的更多信息, * 请参阅 Documentation/power/runtime_pm.rst。 */
该结构基本上是个大杀器了,该有的东西都有,主要分为几类: 传统suspend的常规路径,prepare/complete、suspend/resume、freeze/thaw、poweroff、restore; 传统suspend的特殊路径,early/late、noirq; runtime PM,suspend/resume/idle。
各类driver需要做的事情很单纯,实现这些回调函数,并保存在合适的位置,我们接着往下看。
3.4 struct dev_pm_ops的位置
1: struct device { 2: ... 3: struct dev_pm_domain *pm_domain; 4: const struct device_type *type; 5: struct class *class; 6: struct bus_type *bus; 7: struct device_driver *driver; 8: ... 9: }; 10: 11: 12: 13: struct dev_pm_domain { 14: struct dev_pm_ops ops; 15: ... 16: }; 17: 18: struct device_type { 19: ... 20: const struct dev_pm_ops *pm; 21: }; 22: 23: struct class { 24: ... 25: const struct dev_pm_ops *pm; 26: ... 27: }; 28: 29: struct bus_type { 30: ... 31: const struct dev_pm_ops *pm; 32: ... 33: }; 34: 35: struct device_driver { 36: ... 37: const struct dev_pm_ops *pm; 38: ... 39: };
可谓是狡兔多窟,struct dev_pm_ops存在于struct device、struct device_type、struct class、struct bus_type、struct device_driver等所有和设备模型有关的实体中。 由之前的文章可知,kernel在电源管理的过程中,会按照如下优先级调用dev_pm_ops中的回调函数,以命令设备实现相应的状态切换: dev->pm_domain->ops、dev->type->pm、dev->class->pm、dev->bus->pm、dev->driver->pm。 因此,设备driver需要做的事情也很单纯,实现这些回调函数,并保存在合适的位置。但这么多位置,到底怎么实现呢?我们接着分析。
4. struct dev_pm_ops的实现
由之前的描述可知,系统在电源状态切换时,会按照一定的优先顺序,调用设备的pm ops。所谓的优先顺序,是指:只要存在优先级高的ops(如dev->pm_domain->ops),则调用该ops,否则继续查找下一个优先级。因此,设备驱动可以根据该设备的实际情况,在指定层次上,实现dev pm ops,以达到电源管理的目的。
dev pm ops可以存在于pm domain、device type、class、bus、device driver任何一个地方,本章以pm domain、bus和device driver三个典型场景为例,介绍设备电源管理的实现思路。
注1:为了方便,我会以struct dev_pm_ops中的.suspend函数为例,其它类似。
4.1 pm domain
当一个设备属于某个pm domain时(具体可参考“Linux PM domain framework(1)_概述和使用流程”),系统suspend的过程中,会直接调用pm_domain->ops.suspend。而由pm_genpd_init可知,pm_domain->ops.suspend由pm_genpd_suspend实现:
genpd->domain.ops.suspend = pm_genpd_suspend;
该接口的实现为:
1: static int pm_genpd_suspend(struct device *dev) 2: { 3: struct generic_pm_domain *genpd; 4: 5: dev_dbg(dev, "%s() ", __func__); 6: 7: genpd = dev_to_genpd(dev); 8: if (IS_ERR(genpd)) 9: return -EINVAL; 10: 11: return genpd->suspend_power_off ? 0 : pm_generic_suspend(dev); 12: }
最终会调用pm_generic_suspend,由“Linux电源管理(4)_Power Management Interface”的描述可知,该接口最终会调用该设备驱动的suspend接口(如果有的话),即:dev->driver->pm->suspend。
看来是空欢喜一场,本以为pm domain帮忙做了,设备驱动就可以偷一点懒,谁知道绕来绕去,又把球踢给了设备驱动!让我们思考一下其中的原因:
1)suspend时,设备的动作到底是什么,只有设备驱动最清楚,所以,把事情交给driver做,是合理的。
2)那么,为什么要经过pm domain这一层呢?直接调用driver的suspend不就可以了吗?因为需要在suspend前,由pm domain做一些处理,例如判断该设备是否已经掉电(如果掉电了,就不能再suspend了,否则可能有非预期的结果),等等。
4.2 dev->bus->pm
来看另一个例子,如果该设备所在的bus提供了dev_pm_ops呢?开始之前,我们再强调一下这个事实:suspend时,设备的动作到底是什么,只有设备驱动最清楚,所以,把事情交给driver做,是合理的。所以相信大家猜到了,就算bus有suspend回调,最终还是要绕到设备驱动的suspend接口上。
我们以platform bus为例,原因是这个bus很简单,而且我们平时需要面对的大多数设备都是platform设备。
在drivers/base/platform.c中,platform bus是这样定义的:
1: struct bus_type platform_bus_type = { 2: .name = "platform", 3: .dev_groups = platform_dev_groups, 4: .match = platform_match, 5: .uevent = platform_uevent, 6: .pm = &platform_dev_pm_ops, 7: };
接着看一下platform_dev_pm_ops:
1: static const struct dev_pm_ops platform_dev_pm_ops = { 2: .runtime_suspend = pm_generic_runtime_suspend, 3: .runtime_resume = pm_generic_runtime_resume, 4: USE_PLATFORM_PM_SLEEP_OPS 5: };
哦,有runtime PM相关的两个回调,有一个宏定义:USE_PLATFORM_PM_SLEEP_OPS,该宏定义指定了dev_pm_ops的suspend回调为platform_pm_suspend(其它的类似)。该接口的实现如下:
1: int platform_pm_suspend(struct device *dev) 2: { 3: struct device_driver *drv = dev->driver; 4: int ret = 0; 5: 6: if (!drv) 7: return 0; 8: 9: if (drv->pm) { 10: if (drv->pm->suspend) 11: ret = drv->pm->suspend(dev); 12: } else { 13: ret = platform_legacy_suspend(dev, PMSG_SUSPEND); 14: } 15: 16: return ret; 17: }
原来如此,如果该设备的驱动提供了dev_pm_ops指针,调用相应的suspend接口。否则,调用legacy的接口(即pdrv->suspend)。再对比3.1,3.2小节的描述,是不是豁然开朗了? 另外,由于platform bus是一个虚拟的bus,不需要其它的动作。对于一些物理bus,可以在bus的suspend接口中,实现bus有关的suspend操作。这就是设备模型的魅力所在。
4.3 dev->driver->pm
无论怎样,如果一个设备需要在suspend时有一些动作,就一定要在设备驱动中实现suspend,那样怎么实现呢?定义一个struct dev_pm_ops变量,并实现设备所需的回调函数,在driver注册之前,保存在driver->pm指针中即可。
那有什么变化?大多数的设备是platform设备,我们也可以用旧的方式(3.1,3.2小节),实现platform driver的suspend/resume。但是,在新时代,不建议这样做了,注意platform_legacy_suspend中的legacy字样哦,遗产、遗留下来的,只是为了兼容。如果我们新写driver,就用新的方式好了。
5. 设备电源状态的切换过程
本来还想梳理一下系统电源切换的过程中,driver是怎么处理的。但经过上面的分析,传统的suspend/resume已经很明确了,无非是按照pm_domain—>device driver或者class—>device driver或者bus—>device driver的顺序,调用相应的回调函数。而runtime PM,还是放到runtime PM的分析文章里比较好。所以本文就结束好了。