为了顺应商业潮流,满足消费者的需求,移动设备正变得越来越复杂,功能越来越多。 虽然这类设备的少数部分运行专有或裸机软件,但它们中的大多数都运行基于 Linux 的操作系统(仅举几例,嵌入式 Linux 发行版、Android 等),而且所有这些设备都是由电池供电的。 除了完整的功能和性能之外,消费者还需要尽可能长的自主性和持久的电池。 不用说,完全性能和自主性(省电)是两个完全不相容的概念,在使用设备时必须始终找到折衷方案。 电源管理提供了这一折衷方案,使我们能够尽可能降低功耗和设备性能,而不会忽略设备在进入低功耗状态后唤醒(或完全运行)所需的时间。
Linux 内核提供了几种电源管理功能,从允许您在短暂的空闲期(或执行功耗需求较低的任务)时节省电量,到在不活跃使用时将整个系统置于睡眠状态。
此外,当设备添加到系统中时,由于 Linux 内核提供的通用电源管理 API,它们可以参与此电源管理工作,以便使设备驱动开发人员能够从设备中实现的电源管理机制中获益,无论设备是什么。 这允许调整每个设备或整个系统的电源参数,以便不仅延长设备的自主性,而且延长电池的寿命。
在本章中,我们将介绍 Linux 内核电源管理子系统,利用它的 API 并从用户空间管理它的选项。 因此,我们将介绍以下主题:
- 基于 Linux 系统的电源管理概念
- 向设备驱动添加电源管理功能
- 是系统唤醒的来源
为了更好地理解本章,您需要以下内容:
- 基本的电气知识
- 基本的 C 编程技能
- 具备良好的计算机体系结构知识
- Linux Kernel 4.19 源代码位于https://github.com/torvalds/linux
电源管理(PM)要求在任何时候消耗尽可能少的电力。 操作系统必须处理两种类型的电源管理:设备电源管理和系统电源管理。
- 设备电源管理:这是特定于设备的。 它允许在系统运行时将设备置于低功率状态。 除其他事项外,这可能允许关闭当前未使用的设备的一部分以节省电量,例如在您不打字时的键盘背光。 单独的设备电源管理可以在设备上显式调用,而与电源管理活动无关,或者可以在设备空闲了设定的时间量之后自动发生。 设备电源管理是所谓的运行时电源管理的别名。
- 系统电源管理,也称为休眠状态:这使平台能够进入系统范围的低功耗状态。 换句话说,进入休眠状态是将整个系统置于低功率状态的过程。 系统可能会进入几种低功耗状态(或休眠状态),具体取决于平台、其功能和目标唤醒延迟。 例如,当笔记本电脑盖上盖子、关闭手机屏幕或达到某些关键状态(如电池电量)时,就会发生这种情况。 这些状态中的许多在不同平台上都是相似的(例如冻结,这纯粹是软件,因此与设备或系统无关),稍后将详细讨论。 一般概念是在系统断电(或进入休眠状态,这与关机不同)之前保存正在运行的系统的状态,并在系统恢复供电后恢复。 这可防止系统执行整个关机和启动顺序。
尽管系统 PM 和运行时 PM 处理不同的空闲管理场景,但是部署两者对于防止浪费平台的电力是很重要的。 你应该认为它们是互补的,正如我们将在即将到来的节离子中看到的那样。
这是 Linux PM 的一部分,管理单个设备的电源,而不会使整个系统进入低功耗状态。 在此模式下,操作在系统运行时生效,因此其名称为 Runtime Power Management。 为了适应设备功耗,在系统仍在运行的情况下,动态更改其属性,他将其另一个名称称为动态电源管理**。**
除了驱动开发人员可以在设备驱动中实现的每个设备的电源管理功能外,Linux 内核还提供了用户空间接口来添加/删除/修改电源策略。 下面列出了其中最知名的几个:
- CPU 空闲:当 CPU 没有要执行的任务时,此有助于管理 CPU 功耗。
- CPUFreq:此允许根据系统负载更改 CPU 电源属性(即相关的电压和频率)。
- 散热:此允许根据在系统的预定义区域(大多数时间靠近 CPU 的区域)检测到的温度来调整电源属性。
您可能已经注意到,前面的策略处理 CPU。 这是因为 CPU 是移动设备(或嵌入式系统)功耗的主要来源之一。 虽然在接下来的部分中只介绍了三个接口,但是还有其他接口 too,例如 QoS 和 DevFreq。 读者可以自由探索这些内容来满足他们的好奇心。
每当系统中的逻辑 CPU 没有要执行的任务时,可能需要将其置于特定状态以节省电能。 在这种情况下,大多数操作系统简单地调度所谓的空闲线程。 在执行此线程时,CPU 被称为空闲或处于空闲状态。 CPU Idle是一个管理空闲线程的框架。 有几个级别(或模式或状态)的空闲。 它依赖于嵌入在 CPU 中的内置节能硬件。 CPU 空闲模式有时称为 C 模式,甚至称为 C 状态,这是高级配置和电源接口(ACPI)术语。 这些状态通常从C0
开始,这是正常的 CPU 操作模式;换句话说,CPU 是 100%打开的。 随着 C 值的增加,CPU 休眠模式变得更深;换句话说,关闭的电路和信号越多,CPU 返回C0
模式(即唤醒)所需的时间就越长。 C1
是第一个 C 状态,C2
是第二个状态,依此类推。 当逻辑处理器空闲时(除C0
之外的任何 C 状态),其频率通常为0
。
下一个事件(时间)决定 CPU 可以休眠多长时间。 每种空闲状态由三个特征描述:
- 退出延迟(µS):这是退出此状态的延迟。
- 功耗(以兆瓦为单位):这并不总是可靠的。
- 目标驻留时间(µS):这是开始使用此状态的空闲持续时间。
CPU 空闲驱动是特定于平台的,Linux 内核希望 CPU 驱动最多支持 10 种状态(参见内核源代码中的CPUIDLE_STATE_MAX
)。 然而,实际状态数取决于底层 CPU 硬件(嵌入内置省电逻辑),而且大多数 ARM 平台只提供一到两个空闲状态。 进入该州的选择是基于州长管理的政策。
在这个上下文中,调控器是一个简单的模块,它实现了一种算法,可以根据某些属性做出最佳的 C 状态选择。 换句话说,调控器是决定系统目标 C 状态的人。 尽管系统上可以存在多个调控器,但任何时候都只有一个调控器控制给定的 CPU。 它的设计方式是,如果调度器运行队列为空(这意味着 CPU 没有其他事情可做),并且它需要空闲 CPU,它将向 CPU 空闲框架请求 CPU 空闲。 然后,框架将依赖于当前选择的调控器来选择适当的C 状态。 有两个 CPU 空闲调控器:ladder
(对于基于周期计时器计时的系统)和menu
(对于无计时的系统)。 虽然ladder
调控器总是可用,但如果选择了CONFIG_CPU_IDLE
,则menu
调控器还需要设置CONFIG_NO_HZ_IDLE
(或旧内核上的CONFIG_NO_HZ
)。 在配置内核时选择调控器。 粗略地说,使用它们中的哪一个取决于内核的配置,特别是取决于调度程序的滴答是否可以被空闲循环停止,因此CONFIG_NO_HZ_IDLE
。 关于这一点,您可以参考Documentation/timers/NO_HZ.txt
进行进一步的阅读。
调速器可以决定是继续当前状态还是转换到不同状态,在这种情况下,它将指示当前驾驶员转换到所选状态。 可以通过读取/sys/devices/system/cpu/cpuidle/current_driver
文件的内容和/sys/devices/system/cpu/cpuidle/current_governor_ro
中的当前调速器来识别当前空闲驱动:
$ cat /sys/devices/system/cpu/cpuidle/current_governor_ro menu
在给定系统上,/sys/devices/system/cpu/cpuX/cpuidle/
中的每个目录对应一个 C 状态,每个 C 状态目录属性文件的内容描述该 C 状态:
$ ls /sys/devices/system/cpu/cpu0/cpuidle/
state0 state1 state2 state3 state4 state5 state6 state7 state8
$ ls /sys/devices/system/cpu/cpu0/cpuidle/state0/
above below desc disable latency name power residency time usage
在 ARM 平台上,可以在设备树中描述空闲状态。 您可以参考内核源代码中的Documentation/devicetree/bindings/arm/idle-states.txt
文件,了解有关这方面的更多信息。
重要音符
与其他电源管理框架不同,CPU Idle 无需用户干预即可工作。
有一个与此框架略有相似的框架,即CPU Hotplug
,它允许在运行时动态启用和禁用 CPU,而无需重启系统。 例如,要将 2 号 CPU 热插拔出系统,可以使用以下命令:
# echo 0 > /sys/devices/system/cpu/cpu2/online
我们可以通过读取/proc/cpuinfo
来确保 CPU#2 实际上被禁用:
# grep processor /proc/cpuinfo
processor : 0
processor : 1
processor : 3
processor : 4
processor : 5
processor : 6
processor : 7
前面的说明确认 CPU2 现在处于离线状态。 为了将 CPU 热插拔回系统,我们可以执行以下命令:
# echo 1 > /sys/devices/system/cpu/cpu2/online
CPU 热插拔在幕后的作用将取决于您特定的硬件和驱动。 这可能只会导致 SOMe 系统上的 CPU 进入空闲状态,而其他系统可能会物理地从指定的内核中移除电源。
该框架允许基于约束和要求、用户偏好或其他因素对 CPU 进行动态电压选择和频率缩放。 因为该框架处理频率,所以它无条件地涉及时钟框架。 该框架使用概念操作性能点(opps),它由用{Frequency,voltage}
元组表示系统的性能状态组成。
OPP 可以在设备树中描述,内核源代码中的绑定文档可以作为了解更多信息的一个很好的起点:Documentation/devicetree/bindings/opp/opp.txt
。
重要音符
您偶尔会遇到术语P 状态。 这也是一个 ACPI 术语(与 C 状态一样),用于指定 CPU 内置硬件操作。 有些英特尔 CPU 就是这种情况,操作系统使用策略对象来处理这些问题。 您可以在基于英特尔的机器上检查ls /sys/devices/system/cpu/cpufreq/
的结果。 因此,与 P 状态相反,C 状态是空闲节电状态,P 状态是执行节电状态。
CPUfreq 还使用调控器(实现缩放算法)的概念,该框架中的调控器如下:
ondemand
:该调控器对 CPU 的负载进行采样,并积极放大以提供适当数量的处理能力,但在必要时会将频率重置为最大值。conservative
:这类似于ondemand
,但使用了一种不太激进的增加 OPP 的方法。 例如,即使系统突然需要高性能,它也永远不会从最低的 OPP 跳到最高的 OPP。 它将循序渐进地做到这一点。performance
:此调速器始终选择频率尽可能高的 OPP。 这位州长把绩效放在首位。powersave
:与性能不同,此调控器始终选择频率尽可能低的 OPP。 这位州长把节电放在首位。userspace
:此调速器允许用户使用在/sys/devices/system/cpu/cpuX/cpufreq/scaling_available_frequencies
中找到的任何值,通过将其回显到/sys/devices/system/cpu/cpuX/cpufreq/scaling_setspeed
来设置所需的 OPP。schedutil
:此调控器是调度器的一部分,因此它可以在内部访问调度器数据结构,使其能够获取更可靠、更准确的系统负载统计信息,以便更好地选择适当的 OPP。
userspace
调速器是唯一允许用户选择 OPP 的调速器。 对于其他调速器,OPP 更改会根据其算法的系统负载自动发生。 也就是说,从userspace
开始,下面列出了可用的调控器:
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors
performance powersave
要查看当前调控器,请执行以下命令:
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
powersave
要设置调控器,可以使用以下命令:
$ echo userspace > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
要查看当前的 OPP(频率,单位为 kHz),请执行以下命令:
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq 800031
要查看支持的 opps(频率,单位为 kHz),请执行以下命令:
$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies
275000 500000 600000 800031
要更改 OPP,可以使用以下命令:
$ echo 275000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed
重要音符
还有devfreq
框架,它是用于非 CPU 设备的通用动态电压和频率缩放(DVFS)框架,具有诸如Ondemand
、performance
、powersave
和passive
等调控器。
请注意,前面的命令仅在选择ondemand
调控器时才起作用,因为它是唯一允许更改 OPP 的命令。 然而,在前面的所有命令中,cpu0
仅用于说教目的。 可以将其视为cpuX,其中X是系统看到的 CPU 的索引。
此框架专门用于监控系统温度。 它根据温度阈值有专门的配置文件。 热传感器感应热点并报告。 该框架与冷却设备配合使用,有助于降低功耗以控制/限制过热。
散热框架使用以下概念:
- 热区:您可以将热区视为需要监控其温度的硬件。
- 热传感器:这些组件用于测量温度。 热传感器在热区提供温度传感功能。
- 冷却设备:这些设备在功耗方面提供控制。 通常有两种冷却方法:被动冷却,包括调节设备性能,在这种情况下使用 DVFS;主动冷却,包括激活特殊的冷却设备,如风扇(GPIO 风扇、PWM 风扇)。
- 跳闸点:这些跳闸点描述建议采取冷却行动的关键温度(实际阈值)。 这些点集是根据硬件限制选择的。
- 调控器:这些包括根据某些标准选择最佳冷却的算法。
- 冷却图:这些图用于描述跳闸点和冷却设备之间的链接。
热框架可以分为四个部分,它们是thermal zone
、thermal governor
、thermal cooling
和thermal core
,thermal core
是前三个部分之间的粘合剂。 它可以在用户空间中从/sys/class/thermal/
目录中进行管理:
$ ls /sys/class/thermal/
cooling_device0 cooling_device4 cooling_device8 thermal_zone3 thermal_zone7
cooling_device1 cooling_device5 thermal_zone0 thermal_zone4
cooling_device2 cooling_device6 thermal_zone1 thermal_zone5
cooling_device3 cooling_device7 thermal_zone2 thermal_zone6
在前面的说明中,每个thermal_zoneX
文件代表一个热区驱动或一个热驱动。 热区驱动器是与热区相关联的热传感器的驱动器。 此驱动会显示需要冷却的跳闸点,但也会提供与传感器相关的冷却设备列表。 热工流程的设计是通过热区驱动器获得温度,然后通过热调速器进行决策,最后通过热冷却的方式进行温度控制。 有关这方面的更多信息,请参阅内核源代码Documentation/thermal/sysfs- api.txt
中的热 sysfs 文档。 此外,可以在设备树中执行热区描述、跳闸点定义和冷却设备绑定,源代码中的相关文档为Documentation/devicetree/bindings/thermal/thermal.txt
。
系统电源管理针对整个系统。 其目的是将其置于低功率状态。 在这种低功耗状态下,系统消耗的电量很少,但对用户的响应延迟却相对较低。 电源和响应延迟的确切数量取决于系统所处的睡眠状态的深度。 这也称为静态电源管理,因为它在系统长时间处于非活动状态时被激活。
系统可以进入的状态取决于底层平台,并且在不同的体系结构,甚至同一体系结构的世代或家族之间也会有所不同。 然而,在大多数平台上有四种常见的睡眠状态。 它们是挂起到空闲(也称为冻结)、开机待机(待机)、挂起到 RAM(内存)和挂起到磁盘(休眠)。 这些状态有时也由它们的 ACPI 状态引用:S0
、S1
、S3
和S4
:
# cat /sys/power/state
freeze mem disk standby
CONFIG_SUSPEND
是必须设置的内核配置选项,系统才能支持系统的电源管理休眠状态。 也就是说,除了冻结之外,每个休眠状态都是特定于平台的。 因此,要使平台支持其余三个状态中的任何一个,它必须向核心系统挂起子系统显式注册每个状态。 但是,对休眠的支持取决于其他内核配置选项,我们稍后将看到这一点。
重要音符
因为只有用户知道系统何时不会被使用(甚至不会使用用户代码,比如 GUI),所以系统电源管理操作总是从用户空间启动的。 内核对此一无所知。 这就是本节中的大部分内容使用sysfs
和命令行处理 w 的原因。
这是最基本、最轻便的。 这种状态纯粹是软件驱动的,涉及到尽可能将 CPU 保持在其最深的空闲状态。 为此,冻结用户空间(冻结所有用户空间任务),并将所有 I/O 设备置于低功率状态(可能低于运行时的可用功率),以便处理器可以在其空闲状态中花费更多时间。 以下是使系统空闲的命令:
$ echo freeze > /sys/power/state
前面的命令将系统置于空闲状态。 因为它是纯软件,所以始终支持此状态(假设设置了CONFIG_SUSPEND
内核配置选项)。 此状态可用于没有开机挂起或挂起到内存支持的平台。 但是,正如我们稍后将看到的,除了挂起到 RAM 之外,还可以使用它来提供更短的恢复延迟。
重要音符
挂起到空闲等于冻结进程+挂起个设备+空闲处理器
除了冻结用户空间并将所有 I/O 设备置于低功率状态外,此状态执行的另一个操作是关闭所有非引导 CPU 的电源。 以下是使系统进入待机状态的命令(假设平台支持):
$ echo standby > /sys/power/state
由于此状态比冻结状态走得更远,因此相对于挂起到空闲,它还允许节省更多能量,但恢复等待时间通常比 FREEZe 状态大,尽管它相当低。
除了将系统中的所有内容置于低功耗状态之外,此状态还会进一步关闭所有 CPU,并将内存置于自刷新状态,以使其内容不会丢失,尽管可能会根据平台的能力进行其他操作。 响应延迟高于待机,但仍相当低。 在此状态下,系统和设备状态被保存并保存在内存中。 这就是为什么只有 RAM 完全运行的原因,因此有了状态名称:
# echo mem > /sys/power/state
前面的命令应该将系统置于挂起到 RAM 状态。 但是,写入mem
字符串时执行的实际操作由/sys/power/mem_sleep
文件控制。 该文件包含一个字符串列表,其中每个字符串表示在将mem
写入/sys/power/state
之后系统可以进入的模式。 虽然并非所有模式都始终可用(取决于平台),但可能的模式包括:
s2idle
:这相当于挂起到空闲。 因此,它始终可用。shallow
:这相当于开机挂起或待机。 它的可用性取决于平台对待机模式的支持。deep
:这是实际的挂起到 RAM 状态,其可用性取决于平台。
查询内容的示例如下所示:
$ cat /sys/power/mem_sleep
[s2idle] deep
所选模式用方括号[ ]
括起来。 如果平台不支持某一模式,则与其对应的字符串仍不会出现在/sys/power/mem_sleep
中。 将/sys/power/mem_sleep
中存在的其他字符串之一写入该字符串会导致随后使用的挂起模式更改为该字符串所表示的模式。
当系统启动时,默认的挂起模式(换句话说,不向/sys/power/mem_sleep
写入任何内容的模式)是deep
(如果支持挂起到 RAM)或s2idle
,但它可以被内核命令行中的mem_sleep_default
参数的值覆盖。
测试的一种方法是使用系统上可用的 RTC,假设它支持wakeup alarm
功能。 您可以使用ls /sys/class/rtc/
确定系统上的可用 RTC。 每个 RTC 都有一个目录(换句话说,rtc0
和rtc1
)。 对于支持alarm
功能的rtc
,在该rtc
目录中将有一个wakealarm
文件,该文件可用于配置报警,然后将系统挂起到 RAM:
/* No value returned means no alarms are set */
$ cat /sys/class/rtc/rtc0/wakealarm
/* Set the wakeup alarm for 20s */
# echo +20 > /sys/class/rtc/rtc0/wakealarm
/* Now Suspend system to RAM */ # echo mem > /sys/power/state
在唤醒之前,您应该不会在控制台上看到进一步的活动。
由于尽可能多地关闭系统电源(包括内存),此状态可提供最大的节能效果。 内存内容(快照)写入永久介质,通常是磁盘。 在此之后,内存和整个系统都会断电。 恢复时,快照被读回内存,系统从该休眠映像引导。 但是,此状态也是恢复时间最长的状态,但仍比执行完整(重新)引导序列更快:
$ echo disk > /sys/power/state
将内存状态写入磁盘后,可以执行几个操作。 要执行的操作由/sys/power/disk
文件及其内容控制。 该文件包含一个字符串列表,其中每个字符串表示在将系统状态保存到永久存储介质后(在实际保存休眠映像之后)可以执行的操作。 可能的操作包括以下几项:
platform
:特定于定制和平台,可能需要固件(BIOS)干预。shutdown
:关闭系统电源。reboot
:重新启动系统(主要用于诊断)。suspend
:使系统进入通过前面描述的mem_sleep
文件选择的挂起休眠状态。 如果系统从该状态成功唤醒,则休眠映像将被简单地丢弃,所有操作将继续。 否则,该映像将用于恢复系统以前的状态。test_resume
:这是为了进行系统恢复诊断。 加载映像,就好像系统刚刚从休眠中唤醒,并且当前运行的内核实例是还原内核,然后执行完全系统恢复。
但是,给定平台上支持的操作取决于/sys/power/disk
文件的内容:
$ cat /sys/power/disk
[platform] shutdown reboot suspend test_resume
选定的操作用方括号括起来,[ ]
。 将其中一个列出的字符串写入此文件会导致选择它所代表的选项。 休眠是一项非常复杂的操作,它有自己的配置选项CONFIG_HIBERNATION
。 必须设置此选项才能启用休眠功能。 也就是说,只有当对给定 CPU 体系结构的支持包括用于系统恢复的低级代码时,才能设置此选项(请参阅ARCH_HIBERNATION_POSSIBLE
内核配置选项)。
要使挂起到磁盘工作,并根据休眠映像存储位置的不同,可能需要在磁盘上设置专用分区。 此分区也称为交换分区。 此分区用于将内存内容写入可释放的交换空间。 为了检查休眠是否按预期工作,通常尝试在reboot
模式下休眠,如下所示:
$ echo reboot > /sys/power/disk
# echo disk > /sys/power/state
第一个命令通知电源管理核心在创建休眠映像时应该执行什么操作。 在本例中,它是重新启动。 重新启动后,系统将从休眠映像恢复,您应该返回到开始转换的命令提示符。 这项测试的成功可能表明休眠最有可能正常工作。 那就是说,为了加强测试,应该做几次。
现在我们已经从运行的系统中完成了休眠状态管理,我们可以看看如何在驱动代码中实现它的支持。
本身的设备驱动可以实现独特的电源管理功能,这称为运行时电源管理。 并非所有设备都支持运行时电源管理。 但是,那些这样做的人必须导出一些回调,以根据用户或系统的策略决策控制其电源状态。 正如我们在前面看到的,这是特定于设备的。 在本节中,我们将学习如何通过电源管理支持来扩展设备驱动功能。
虽然设备驱动提供运行时电源管理回调,但它们也通过提供另一组回调来促进和参与系统休眠状态,其中每组回调都参与特定的系统休眠状态。 每当系统需要进入给定的集合或从给定的集合恢复时,内核都会遍历为该状态提供回调的每个驱动,然后以精确的顺序调用它们。 简单地说,设备电源管理包括对设备所处状态的描述,以及用于控制这些状态的机制。 内核提供了对电源管理感兴趣的每个设备驱动/类/总线必须填充的struct dev_pm_ops
,从而促进了这一点。 这允许内核与系统中的每个设备通信,而不考虑设备所在的总线或它所属的类。 让我们后退一步,记住 astruct device
是什么样子的:
struct device {
[...]
struct device *parent;
struct bus_type *bus;
struct device_driver *driver;
struct dev_pm_info power;
struct dev_pm_domain *pm_domain;
}
在前面的struct device
数据结构中,我们可以看到设备既可以是子设备(其.parent
字段指向另一个设备),也可以是设备父设备(当另一个设备的.parent
字段指向它时),可以位于给定的总线后面,也可以属于给定的类,或者可以间接地属于给定的子系统。 此外,我们可以看到,设备可以是给定电源域的一部分。 .power
字段为struct dev_pm_info
类型。 主要保存 PM 相关的状态,如当前电源状态、是否能唤醒、是否已准备好、是否已挂起等。 由于涉及的内容太多,我们在使用时会详细讲解。
为了让设备在子系统级别或设备驱动级别参与电源管理,其驱动需要通过定义和填充include/linux/pm.h
中定义的struct 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);
};
在前面的数据结构中,为了可读性,删除了*_early()
和*_late()
回调。 我建议您看一下完整的定义。 也就是说,鉴于回调的数量巨大,我们将在适当的时候在本章需要使用它们的部分描述它们。
重要音符
受 PCI 设备和 ACPI 规范的启发,设备电源状态有时称为D状态。 这些状态的范围从状态D0
到D3
,包括状态D0
和D3
。 虽然不是所有的设备类型都以这种方式定义电源状态 E,但是这种 REPR 指示可以映射到所有已知的设备类型。
运行时电源管理是针对每个设备的电源管理功能,允许特定设备在系统运行时控制其状态,而与全局系统无关。 驱动要实现运行时电源管理,应该只提供struct dev_pm_ops
中整个回调列表的一个子集,如下所示:
struct dev_pm_ops {
[...]
int (*runtime_suspend)(struct device *dev);
int (*runtime_resume)(struct device *dev);
int (*runtime_idle)(struct device *dev);
};
内核还提供了SET_RUNTIME_PM_OPS()
,它接受要填充到结构中的三个回调。 此宏的定义如下:
#define SET_RUNTIME_PM_OPS(suspend_fn, resume_fn, idle_fn) \
.runtime_suspend = suspend_fn, \
.runtime_resume = resume_fn, \
.runtime_idle = idle_fn,
前面的回调是运行时电源管理中唯一涉及的回调,下面是它们必须执行的操作的说明:
.runtime_suspend()
如有必要,必须记录设备的当前状态,并将设备置于静止状态。 此方法由 PM 在设备不使用时调用。 在其简单形式中,此方法必须将设备置于无法与 CPU 和 RAM 通信的状态。- 当设备必须处于完全功能状态时调用
.runtime_resume()
。 如果系统需要访问此设备,则可能会出现这种情况。 此方法必须恢复电源并重新加载任何所需的设备状态。 - 当设备不再使用时,根据设备使用计数器(实际上是当它达到
0
时)以及活动子设备的数量,调用.runtime_idle()
。 但是,此回调执行的操作是特定于驱动的。 在大多数情况下,如果满足某些条件,驱动会在设备上调用runtime_suspend()
,或者调用pm_schedule_suspend()
(为了设置计时器以在将来提交挂起请求而给出延迟),或者pm_runtime_autosuspend()
(根据已经使用pm_runtime_set_autosuspend_delay()
设置的延迟来安排将来的挂起请求)。 如果.runtime_idle
回调不存在或返回0
,PM 核心将立即调用.runtime_suspend()
回调。 对于什么都不做的 PM 核心,.runtime_idle()
必须返回一个非零值。 在这种情况下,驱动返回-EBUSY
或1
是很常见的。
回调实现后,可以在struct dev_pm_ops
中回馈,如下例所示:
static const struct dev_pm_ops bh1780_dev_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(pm_runtime_force_suspend,
pm_runtime_force_resume)
SET_RUNTIME_PM_OPS(bh1780_runtime_suspend,
bh1780_runtime_resume, NULL)
};
[...]
static struct i2c_driver bh1780_driver = {
.probe = bh1780_probe,
.remove = bh1780_remove,
.id_table = bh1780_id,
.driver = {
.name = “bh1780”,
.pm = &bh1780_dev_pm_ops,
.of_match_table = of_match_ptr(of_bh1780_match),
},
};
module_i2c_driver(bh1780_driver);
以上是 IIO 环境光传感器驱动drivers/iio/light/bh1780.c
的摘录。 在这段摘录中,我们可以看到如何使用方便的宏来填充struct dev_pm_ops
。 这里使用SET_SYSTEM_SLEEP_PM_OPS
来填充系统休眠相关的宏,我们将在下一节中看到。 pm_runtime_force_suspend
和pm_runtime_force_resume
分别是 PM 核心公开以强制设备挂起和恢复的特殊帮助器。
事实上,PM 核心使用两个计数器跟踪每个设备的活动。 第一个计数器是power.usage_count
,它对设备的活动引用进行计数。 这些引用可以是外部引用,如打开的文件句柄,也可以是使用此引用的其他设备,也可以是用于使设备在操作期间保持活动状态的内部引用。 另一个计数器是power.child_count
,它计算活动的子代的数量。
这些计数器从 PM 的角度定义给定设备的活动/空闲条件。 设备的活动/空闲状态是 PM 核心确定设备是否可访问的唯一可靠手段。 空闲状态是指设备使用计数递减到0
,并且每当设备使用计数递增时都会出现活动状态(也称为恢复条件)。
在空闲情况下,PM 内核发送/执行空闲通知(即,将设备的power.idle_notification
字段设置为true
,调用总线类型/类别/设备->runtime_idle()
回调,并再次将.idle_notification
字段设置回false
),以检查设备是否可以挂起。 如果不存在->runtime_idle()
回调或返回0
,PM 内核会立即调用->runtime_suspend()
回调来挂起设备,之后将设备的power.runtime_status
字段设置为RPM_SUSPENDED
,这意味着设备挂起。 在恢复条件(设备使用计数递增)时,PM 核心将同步或异步地执行此设备的恢复(仅在特定条件下)。 请看一下rpm_resume()
函数及其在drivers/base/power/runtime.c
中的描述。
最初,对所有设备禁用运行时 PM。 这意味着,在为设备调用pm_runtime_enable()
之前,在设备上调用大多数与 PM 相关的帮助器都将失败,这将启用此设备的运行时 PM。 尽管所有设备的初始运行时 PM 状态都是挂起的,但它不需要反映设备的实际物理状态。 因此,如果设备最初是活动的(换句话说,它能够处理 I/O),则必须在pm_runtime_set_active()
的帮助下将其运行时 PM 状态更改为活动(这会将power.runtime_status
设置为RPM_ACTIVE
),并且如果可能,在为该设备调用pm_runtime_enable()
之前,必须使用pm_runtime_get_noresume()
增加其使用计数。 一旦设备完全初始化,您就可以对其调用pm_runtime_put()
。
这里调用pm_runtime_get_noresume()
的原因是,如果有对pm_runtime_put()
的调用,则设备使用计数将返回零,这对应于空闲条件,然后执行空闲通知。 此时,您将能够检查是否已满足必要条件并挂起设备。 然而,如果初始设备状态是禁用,则不需要这样做。
还有pm_runtime_get()
、pm_runtime_get_sync()
、pm_runtime_put_noidle()
和pm_runtime_put_sync()
帮助器。 pm_runtime_get_sync()
、pm_runtime_get()
和pm_runtime_get_noresume()
之间的区别在于,如果在设备使用计数已递增之后,活动/恢复条件匹配,则前者将同步(立即)执行设备恢复,而第二助手将异步执行(提交请求)。 第三个也是最后一个将在设备使用计数减少后立即返回(甚至不检查恢复条件)。 同样的机制适用于pm_runtime_put_sync()
、pm_runtime_put()
和pm_runtime_put_noidle()
。
给定设备的活动子项的数量会影响此设备的使用计数。 通常情况下,需要父母来访问孩子,因此在孩子活动时关闭父母的电源会适得其反。 但是,有时在确定设备是否空闲时,可能需要忽略该设备的活动子设备。 I2C 总线就是一个很好的例子,在该总线上的设备(子级)处于活动状态时,总线可以报告为空闲。 对于这种情况,可以调用pm_suspend_ignore_children()
以允许设备报告为空闲,即使它有活动的子项也是如此。
在上一节中,我们介绍了 PM 核心可以执行同步或异步 PM 操作的事实。 虽然同步操作很简单(方法调用是序列化的),但我们需要注意在 PM 上下文中异步调用时执行哪些步骤。
您应该记住,在异步模式下,会提交操作请求,或者立即调用此操作的处理程序。 它的工作方式如下:
- PM 核心将设备的
power.request
字段(类型为enum rpm_request
)设置为要提交的请求类型(换言之,RPM_REQ_IDLE
用于空闲通知请求,RPM_REQ_SUSPEND
用于暂停请求,或者RPM_REQ_AUTOSUSPEND
用于自动暂停请求),其对应于要执行的动作。 - PM 核心将设备的
power.request_pending
字段设置为true
。 - PM 核心在全局 PM 相关工作队列中排队(计划稍后执行)设备的 RPM 相关工作(
power.work
,其工作函数是pm_runtime_work()
;请参见pm_runtime_init()
,其中初始化了它)。 - 当此工作有机会运行时,工作函数(即
pm_runtime_work()
)将首先检查设备(if (dev->power.request_pending)
)上是否仍有请求挂起,并对设备的power.request_pending
字段执行switch ... case
,以便调用底层请求处理程序。
请注意,工作队列管理其自己的线程,这些线程可以运行计划的工作。 因为在异步模式下,处理程序是在工作队列中调度的,所以在原子上下文中调用异步 PM 相关帮助器是完全安全的。 例如,如果在 IRQ 处理程序中调用,它将等同于推迟 PM 请求处理。
Autosuspend 是驱动使用的一种机制,这些驱动不希望设备在运行时一空闲就挂起,而是希望设备首先在特定的最小时间段内保持非活动状态。
在 RPM 上下文中,术语autosuspend并不意味着设备自动挂起。 取而代之的是基于计时器,该计时器在到期时将暂停请求排队。 该定时器实际上是设备的power.suspend_timer
字段(请参见设置该定时器的pm_runtime_init()
)。 调用pm_runtime_put_autosuspend()
将启动计时器,而调用pm_runtime_set_autosuspend_delay()
将设置由设备的power.autosuspend_delay
字段表示的超时(尽管可以通过/sys/devices/.../power/autosuspend_delay_ms
属性中的sysfs
设置)。
此计时器也可由pm_schedule_suspend()
帮助器使用,但参数延迟(在本例中将优先于power.autosuspend_delay
字段中设置的参数),之后将提交挂起请求。 您可以将此计时器视为可用于在计数器达到零和设备被视为空闲之间增加延迟。 这对于与打开或关闭相关的高成本设备非常有用。
为了使用autosuspend
,子系统或驱动必须调用pm_runtime_use_autosuspend()
(最好在注册设备之前)。 该帮助器将设备的power.use_autosuspend
字段设置为true
。 在请求启用了自动暂停的设备后,您应该在此设备上调用pm_runtime_mark_last_busy()
,这允许它将power.last_busy
字段设置为当前时间(在jiffies
中),因为此字段用于计算自动暂停的非活动时段(例如,new_expire_time = last_busy + msecs_to_jiffies(autosuspend_delay)
)。
考虑到所有引入的运行时 PM 概念,现在让我们把放在一起,看看在真正的驱动中是如何完成任务的。
如果没有真正的案例研究,前面关于运行时 PM 核心的理论研究就不那么有意义了。 现在是时候看看之前的概念是如何应用的了。 对于此案例研究,我们将选择bh1780
Linux 驱动,这是一个数字 16 位 I2C环境光传感器。 该设备的驱动位于 Linux 内核源代码中的drivers/iio/light/bh1780.c
。
首先,让我们看一下probe
方法的摘录:
static int bh1780_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
[...]
/* Power up the device */ [...]
pm_runtime_get_noresume(&client->dev);
pm_runtime_set_active(&client->dev);
pm_runtime_enable(&client->dev);
ret = bh1780_read(bh1780, BH1780_REG_PARTID);
dev_info(&client->dev, “Ambient Light Sensor, Rev : %lu\n”,
(ret & BH1780_REVMASK));
/*
* As the device takes 250 ms to even come up with a fresh
* measurement after power-on, do not shut it down * unnecessarily.
* Set autosuspend to five seconds.
*/
pm_runtime_set_autosuspend_delay(&client->dev, 5000);
pm_runtime_use_autosuspend(&client->dev);
pm_runtime_put(&client->dev);
[...]
ret = iio_device_register(indio_dev);
if (ret)
goto out_disable_pm; return 0;
out_disable_pm:
pm_runtime_put_noidle(&client->dev);
pm_runtime_disable(&client->dev); return ret;
}
在前面的片段中,出于可读性的考虑,只留下了与电源管理相关的调用。 首先,pm_runtime_get_noresume()
将增加设备使用计数,而不携带设备的空闲通知(_noidle
后缀)。 您可以使用pm_runtime_get_noresume()
接口关闭运行时挂起功能或在设备挂起时使使用量计数为正,以避免由于运行时挂起而导致无法正常唤醒的问题。 然后,驱动中的下一行是pm_runtime_set_active()
。 此辅助对象将设备标记为活动(power.runtime_status = RPM_ACTIVE
),并清除设备的power.runtime_error
字段。 此外,修改设备父设备的未挂起(活动)子项的计数器以反映新状态(它实际上是递增的)。 在设备上调用pm_runtime_set_active()
将防止此设备的父设备在运行时挂起(假设父设备的运行时 PM 已启用),除非设置了父设备的power.ignore_children
标志。 因此,一旦为设备调用了pm_runtime_set_active()
,也应该在合理的情况下尽快调用pm_runtime_enable()
。 调用此函数不是强制性的;它必须与 PM 核心和设备状态保持一致,假设初始状态为RPM_SUSPENDED
。
重要音符
与pm_runtime_set_active()
相反的是pm_runtime_set_suspended()
,它将设备状态更改为RPM_SUSPENDED
,并递减活动子级的父级计数器。 提交针对父对象的空闲通知请求。
pm_runtime_enable()
是强制的运行时 PM 帮助器,它启用设备的运行时 PM,也就是说,在设备的power.disable_depth
值大于0
的情况下递减设备的power.disable_depth
值。 作为信息,设备的power.disable_depth
值在每次运行时 PM 帮助器调用时都会被检查,它的值必须是0
才能使帮助器继续进行。 它的初始值是1
,该值在调用pm_runtime_enable()
时递减。 在错误路径上,调用pm_runtime_put_noidle()
以使 PM 运行时计数器平衡,并且pm_runtime_disable()
完全禁用设备上的运行时 PM。
正如您可能已经猜到的,该驱动还处理 IIO 框架,这意味着它公开 sysfs 中的条目,这些条目对应于它的物理转换通道。 读取与通道对应的 sysfs 文件将报告该通道产生的转换的数字值。 然而,对于bh1780
,其驱动器中的通道读取入口点是bh1780_read_raw()
。 此方法的摘录可在此处看到:
static int bh1780_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask)
{
struct bh1780_data *bh1780 = iio_priv(indio_dev);
int value;
switch (mask) {
case IIO_CHAN_INFO_RAW:
switch (chan->type) {
case IIO_LIGHT:
pm_runtime_get_sync(&bh1780->client->dev);
value = bh1780_read_word(bh1780, BH1780_REG_DLOW);
if (value < 0)
return value;
pm_runtime_mark_last_busy(&bh1780->client->dev);
pm_runtime_put_autosuspend(&bh1780->client->dev);
*val = value;
return IIO_VAL_INT;
default:
return -EINVAL;
case IIO_CHAN_INFO_INT_TIME:
*val = 0;
*val2 = BH1780_INTERVAL * 1000;
return IIO_VAL_INT_PLUS_MICRO;
default:
return -EINVAL;
}
}
在这里,同样,只有运行时 PM 相关的函数调用值得我们关注。 在通道读取的情况下,调用前面的函数。 设备驱动必须指示设备对通道进行采样,以执行转换,转换结果将由设备驱动读取并报告给读取器。 问题是,设备可能处于挂起状态。 因此,因为驱动需要立即访问设备,所以驱动对其调用pm_runtime_get_sync()
。 如果您还记得,此方法会递增设备使用计数,并对设备执行同步(_sync
后缀)恢复。 设备恢复后,驱动可以与设备对话并读取转换值。 因为驱动支持 autosuspend,所以调用pm_runtime_mark_last_busy()
是为了标记设备上次处于活动状态的时间。 这将更新用于自动暂停的计时器的超时值。 最后,驱动调用pm_runtime_put_autosuspend()
,它将在自动暂停定时器到期后执行设备的运行时挂起,除非通过在某个地方调用pm_runtime_mark_last_busy()
或在到期前再次进入读取功能(例如,在 sysfs 中读取通道)重新启动该定时器。
总而言之,在访问硬件之前,驱动可以使用pm_runtime_get_sync()
恢复设备,当它使用完硬件时,驱动可以使用pm_runtime_put_sync()
、pm_runtime_put()
或pm_runtime_put_autosuspend()
通知设备空闲(假设启用了 autosuspend,在这种情况下,必须事先调用pm_runtime_mark_last_busy()
以更新 autosuspend 计时器的超时)。
最后,让我们将重点放在卸载模块时调用的方法上。 以下摘录只对与 PM 相关的呼叫感兴趣:
static int bh1780_remove(struct i2c_client *client)
{
int ret;
struct iio_dev *indio_dev = i2c_get_clientdata(client);
struct bh1780_data *bh1780 = iio_priv(indio_dev);
iio_device_unregister(indio_dev);
pm_runtime_get_sync(&client->dev);
pm_runtime_put_noidle(&client->dev);
pm_runtime_disable(&client->dev);
ret = bh1780_write(bh1780, BH1780_REG_CONTROL, BH1780_POFF);
if (ret < 0) {
dev_err(&client->dev, “failed to power off\n”);
return ret;
}
return 0;
}
这里调用的第一个运行时 PM 方法是pm_runtime_get_sync()
。 这个调用让我们猜测设备将被使用,也就是说,驱动需要访问硬件。 因此,该帮助器立即恢复设备(它实际上递增设备使用计数器并执行设备的同步恢复)。 在此之后,调用pm_runtime_put_noidle()
以便在不携带空闲通知的情况下递减设备使用计数。 接下来,调用pm_runtime_disable()
以禁用设备上的运行时 PM。 这将为设备增加power.disable_depth
,如果之前为零,则取消该设备的所有挂起的运行时 PM 请求,并等待正在进行的所有操作完成,因此对于 PM 核心,该设备将不再存在(请记住,power.disable_depth
将与 PM 核心的预期不符,这意味着在此设备上调用的任何进一步的运行时 PM 助手都将失败)。 最后,由于 i2c 命令关闭了设备的电源,之后其硬件状态将反映其运行时 PM 状态。
以下是适用于运行时 PM 回调和执行的一般规则:
->runtime_idle()
和->runtime_suspend()
只能对活动设备(状态为活动的设备)执行。->runtime_idle()
和->runtime_suspend()
只能针对使用计数器等于零、活动子计数器等于零或设置了power.ignore_children
标志的设备执行。->runtime_resume()
只能对挂起的设备(状态为挂起的设备)执行。
另外,PM 内核提供的 helper 函数遵循以下规则:
- 如果
->runtime_suspend()
即将执行,或者有一个待执行的请求要执行,则不会为同一设备执行->runtime_idle()
。 - 执行或调度执行
->runtime_suspend()
的请求将取消对同一设备执行->runtime_idle()
的任何待定请求。 - 如果
->runtime_resume()
即将执行,或者有一个待执行的请求要执行,则不会为同一设备执行其他回调。 - 执行
->runtime_resume()
的请求将取消任何挂起或计划的请求,以执行同一设备的其他回调(计划的自动暂停除外)。
前面的规则很好地指出了调用这些回调可能失败的原因。 从这些方面,我们还可以观察到服务于恢复或恢复请求的性能优于任何其他回调或请求。
从技术上讲,POWER 域是一组共享电源资源(例如,时钟或电源平面)的设备。 从内核的角度来看,电源域是一组设备,它们的电源管理在子系统级别使用相同的回调集和公共 PM 数据。 从硬件角度来看,电源域是用于管理其电源电压相关的设备的硬件概念;例如,视频核心 IP 与显示 IP 共享电源线。
由于 SoC 设计更加复杂,需要找到一种抽象方法,以便尽可能保持驱动的通用性;然后,genpd
问世了。 这代表通用电源域。 它是一个 Linux 内核抽象,将每个设备的运行时电源管理扩展到一组共享电源轨的设备。 此外,电源域被定义为设备树的一部分,其中描述了设备和电源控制器之间的关系。 这样就可以动态地重新设计电源域,无需重新启动整个系统或重新构建新内核,驱动就可以进行调整。
它的设计目的是,如果设备存在电源域对象,则其 PM 回调优先于总线类型(或设备类或类型)回调。 在内核源代码的Documentation/devicetree/bindings/power/power_domain.txt
中可以找到有关这方面的通用文档,与 SoC 相关的文档也可以在同一目录中找到。
struct dev_pm_ops
数据结构的引入在某种程度上促进了对 PM 核心在暂停或恢复阶段执行的步骤和动作的的理解,这些步骤和动作可以概括如下:
“prepare —> Suspend —> suspend_late —> suspend_noirq”
|---------- Wakeup ----------|
“resume_noirq —> resume_early —> resume -> complete”
前面是include/linux/suspend.h
中定义的enum suspend_stat_step
中列举的完整系统 PM 链。 此流应该会让您想起struct dev_pm_ops
数据结构。
在 Linux 内核代码中,enter_state()
是由系统电源管理核心调用以进入系统休眠状态的函数。 现在让我们花点时间来了解一下在系统挂起和恢复期间到底发生了什么。
以下是enter_state()
挂起时所经历的步骤:
- 如果没有设置
CONFIG_SUSPEND_SKIP_SYNC
内核配置选项,它首先在文件系统上调用sync()
(参见ksys_sync()
)。 - 它调用挂起通知程序(当用户空间仍然存在时)。 请参考
register_pm_notifier()
,这是他们注册时使用的帮助器。 - 它冻结任务(参见
suspend_freeze_processes()
),从而冻结用户空间和内核线程。 如果未在内核配置中设置CONFIG_SUSPEND_FREEZER
,则跳过此步骤。 - 通过调用驱动注册的每个
.suspend()
回调来挂起设备。 这是暂停的第一阶段(见suspend_devices_and_enter()
)。 - 它禁用设备中断(见
suspend_device_irqs()
)。 这可以防止设备驱动接收中断。 - 然后,发生挂起设备的第二阶段(调用
.suspend_noirq
回调)。 此步骤称为noirq阶段。 - 它禁用个非引导 CPU(使用 CPU 热插拔)。 CPU 调度器被告知在这些 CPU 离线之前不要调度它们上的任何东西(参见
disable_nonboot_cpus()
)。 - 它会关闭中断。
- 它执行系统核心回调(参见
syscore_suspend()
)。 - 它会让系统进入睡眠状态。
这是对系统进入睡眠之前执行的操作的粗略描述。 根据系统将要进入的睡眠状态,某些操作的行为可能会略有不同。
一旦系统挂起(无论有多深),一旦发生唤醒事件,系统需要恢复。 以下是 PM 核心为唤醒系统而执行的步骤和操作:
- (唤醒信号。)
- 运行 CPU 的唤醒代码。
- 执行系统核心回调。
- 打开中断。
- 启用非引导 CPU(使用 CPU 热插拔)。
- 恢复设备的第一阶段(
.resume_noirq()
回调)。 - 启用设备中断。
- 挂起设备的第二阶段(
.resume()
回调)。 - 解冻任务。
- 调用通知程序(当用户空间恢复时)。
我将让您在 PM 代码中发现恢复过程的每个步骤都调用了哪些函数。 然而,在驱动内部,这些步骤都是透明的。 驱动 n 需要做的唯一一件事就是根据它希望参与的 s 测试用适当的回调填充struct dev_pm_ops
,我们将在下一节中看到这一点。
系统休眠和运行时 PM 是不同的东西,尽管它们彼此相关。 有些情况下,通过不同的方式,它们会将系统带到相同的物理状态。 因此,用一个替换另一个通常不是一个好主意。
我们已经看到了设备驱动如何根据它们需要参与的休眠状态在struct dev_pm_ops
数据结构中填充一些回调来参与系统休眠。 通常提供的回调(与休眠状态无关)是.suspend
、.resume
、.freeze
、.thaw
、.poweroff
和.restore
。 它们是非常通用的回调,定义如下:
.suspend
:这是在系统进入保存主存储器内容的休眠状态之前执行的。.resume
:在将系统从保存了主存储器内容的休眠状态唤醒之后调用此回调,并且运行此回调时设备的状态取决于设备所属的平台和子系统。.freeze
:特定于休眠,此回调在创建休眠映像之前执行。 它类似于.suspend
,但它不应该使设备能够发出唤醒事件信号或改变其电源状态。 实现此回调的大多数设备驱动只需将设备设置保存在内存中,以便在休眠后的.resume
期间将其重新使用。.thaw
:此回调是特定于休眠的,在创建休眠映像之后或创建映像失败时执行。 它也是在尝试从这样的映像恢复主存储器的内容失败之后执行的。 它必须撤消前面的.freeze
所做的更改,才能使设备以与调用.freeze
之前相同的方式运行。.poweroff
:也是特定于休眠的,这是在保存休眠图像之后执行的。 它类似于.suspend
,但它不需要将设备的设置保存在内存中。.restore
:这是最后一个特定于休眠的回调,在从休眠映像恢复主内存内容后执行。 它类似于.resume
。
前面的大多数回调都非常相似,或者执行的操作大致相似。 虽然.resume
、.thaw
和.restore
三人组可能执行类似的任务,但其他三人组-->suspend
、->freeze
和->poweroff
也是如此。 因此,为了提高代码可读性或促进回调填充,PM 核心提供了SET_SYSTEM_SLEEP_PM_OPS
宏,该宏采用suspend
和resume
函数并填充与系统相关的 PM 回调,如下所示:
#define SET_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn) \
.suspend = suspend_fn, \
.resume = resume_fn, \
.freeze = suspend_fn, \
.thaw = resume_fn, \
.poweroff = suspend_fn, \
.restore = resume_fn,
与_noirq()
相关的回调也是如此。 如果驱动只需要参与系统挂起的noirq
阶段,则可以使用SET_NOIRQ_SYSTEM_SLEEP_PM_OPS
宏自动填充struct dev_pm_ops
数据结构中与_noirq()
相关的回调。 以下是宏的定义:
#define SET_NOIRQ_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn) \
.suspend_noirq = suspend_fn, \
.resume_noirq = resume_fn, \
.freeze_noirq = suspend_fn, \
.thaw_noirq = resume_fn, \
.poweroff_noirq = suspend_fn, \
.restore_noirq = resume_fn,
前面的宏只有两个参数,这两个参数表示suspend
和resume
回调,但这次是noirq
阶段。 您应该记住,这样的回调是在系统上禁用 IRQ 的情况下调用的。
最后是SET_LATE_SYSTEM_SLEEP_PM_OPS
宏,它将把 -> suspend_late
、-> freeze_late
和-> poweroff_late
指向相同的函数,而对于->resume_early
、->thaw_early
和->restore_early
,会指向,反之亦然:
#define SET_LATE_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn) \
.suspend_late = suspend_fn, \
.resume_early = resume_fn, \
.freeze_late = suspend_fn, \
.thaw_early = resume_fn, \
.poweroff_late = suspend_fn, \
.restore_early = resume_fn,
除了减少编码工作外,前面的所有宏都用#ifdef CONFIG_PM_SLEEP
内核配置选项进行了调整,以便在不需要 PM 的情况下不会构建它们。 最后,如果您想对 RAM 和休眠使用相同的暂停和恢复回调,可以使用以下命令:
#define SIMPLE_DEV_PM_OPS(name, suspend_fn, resume_fn) \
const struct dev_pm_ops name = { \
SET_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn) \
}
在前面的代码段中,name
表示设备 PM 操作结构将被实例化的名称。 suspend_fn
和resume_fn
是系统进入挂起状态或从休眠状态恢复时要调用的回调。
既然我们已经能够在驱动代码中实现系统休眠功能,让我们看看如何操作系统唤醒源,从而允许退出休眠状态。
PM 内核允许在系统挂起后唤醒系统。 能够唤醒系统的设备在 PM 语言中称为唤醒源。 为了唤醒源正常运行,它需要一个所谓的唤醒事件,该事件在大部分时间内被同化为一条 IRQ 线路。 换句话说,唤醒源生成唤醒事件。 当唤醒源生成唤醒事件时,唤醒源通过唤醒事件框架提供的接口设置为激活状态。 当事件处理结束时,将其设置为停用状态。 激活和停用之间的间隔表示正在处理事件。 在本节中,我们将了解如何使您的设备成为驱动代码中的系统唤醒源。
唤醒源工作,因此当系统中有任何唤醒事件正在处理时,不允许挂起。 如果挂起正在进行,则会终止。 内核通过struct wakeup_source
对唤醒源进行抽象,struct wakeup_source
也用于收集与唤醒源相关的统计信息。 以下是include/linux/pm_wakeup.h
中对此数据结构的定义:
struct wakeup_source {
const char *name;
struct list_head entry;
spinlock_t lock;
struct wake_irq *wakeirq;
struct timer_list timer;
unsigned long timer_expires;
ktime_t total_time;
ktime_t max_time;
ktime_t last_time;
ktime_t start_prevent_time;
ktime_t prevent_sleep_time;
unsigned long event_count;
unsigned long active_count;
unsigned long relax_count;
unsigned long expire_count;
unsigned long wakeup_count;
bool active:1;
bool autosleep_enabled:1;
};
就代码而言,这个结构对您毫无用处,但是研究它将帮助您理解唤醒源sysfs
属性的含义:
entry
用于跟踪链表中的所有唤醒源。timer
与timer_expires
齐头并进。 当唤醒源产生唤醒事件并且该事件正在被处理时,唤醒源被称为活动,并且这防止了系统挂起。 在处理唤醒事件之后(系统不再需要为此处于活动状态),它将恢复为非活动状态。 激活和停用操作都可以由驱动执行,或者驱动可以通过指定激活期间的超时来决定不同的操作。 PM 唤醒核心将使用该超时来配置计时器,该计时器将在事件期满后自动将其设置为非活动状态。timer
和timer_expires
用于此目的。total_time
是此唤醒源处于活动状态的总时间。 它汇总唤醒源处于活动状态的总时间。 它是与唤醒源对应的设备的忙碌级别和功耗级别的良好指示器。max_time
是唤醒源保持(或连续)处于活动状态的最长时间。 时间越长,越不正常。last_time
指示此唤醒源上一次处于活动状态的开始时间。start_prevent_time
是唤醒源开始阻止系统自动休眠的时间点。prevent_sleep_time
是此唤醒源阻止系统自动休眠的总时间。event_count
表示唤醒源报告的事件数。 换句话说,它指示发出信号的唤醒事件的数量。active_count
表示唤醒源被激活的次数。 在某些情况下,该值可能不相关或不连贯。 例如,当发生唤醒事件时,唤醒源需要切换到活动状态。 但是,情况并不总是如此,因为事件可能在唤醒源已经激活时发生。 因此,active_count
可能小于event_count
,在这种情况下,这意味着很可能在前一个唤醒事件被处理到结束之前生成了另一个唤醒事件。 这在一定程度上反映了以醒醒源为代表的设备的业务。relax_count
表示唤醒源被停用的次数。expire_count
表示唤醒源超时过期的次数。wakeup_count
是唤醒源终止挂起进程的次数。 如果唤醒源在挂起过程中生成唤醒事件,则挂起过程将中止。 此变量记录唤醒源终止挂起进程的次数。 这可能是一个很好的指标,可以用来检查您是否已确定系统始终无法挂起。active
表示唤醒源的激活状态。autosleep_enabled
,对我来说,记录系统自动睡眠状态的状态,无论它是否启用。
要使设备成为唤醒源,其驱动必须调用device_init_wakeup()
。 此函数设置设备的power.can_wakeup
标志(以便device_can_wakeup()
帮助器返回当前设备作为唤醒源的能力),并将其与唤醒相关的属性添加到 sysfs。 此外,它还创建唤醒源对象,注册它,并将其附加到设备(dev->power.wakeup
)。 但是,device_init_wakeup()
只会将设备转换为支持唤醒的设备,而不会为其分配唤醒事件。
重要音符
请注意,只有具有唤醒功能的设备才会在 sysfs 中有一个电源目录来提供所有唤醒信息。
为了分配唤醒事件,驱动必须调用enable_irq_wake()
,给出将用作唤醒事件的 IRQ 行作为参数。 enable_irq_wake()
做的事情可能是特定于平台的(除了其他事情外,它还调用由底层 irqChip 驱动公开的irq_chip.irq_set_wake
回调)。 除了打开将给定 IRQ 作为系统唤醒中断线路处理的平台逻辑外,它还指示suspend_device_irqs()
(在系统挂起路径上调用:参见暂停阶段部分,步骤 5)以不同方式对待给定 IRQ。 因此,IRQ 将在下一个中断时保持启用状态,之后它将被禁用、标记为挂起和挂起,以便在随后的系统恢复期间由resume_device_irqs()
重新启用。 这使得驱动的->suspend
方法成为调用enable_irq_wake()
的正确位置,因此唤醒事件总是在正确的时刻重新武装。 另一方面,驱动的->resume
回调是调用disable_irq_wake()
的正确位置,这将关闭 IRQ 的系统唤醒功能的平台配置。
虽然设备作为唤醒源的能力取决于硬件,但具有唤醒功能的设备是否应该发出唤醒事件是一个策略决策,并由用户空间通过sysfs
属性/sys/devices/.../power/wakeup
进行管理。 此文件允许用户空间检查或决定是否启用设备(通过其唤醒事件)将系统从睡眠状态唤醒。 此文件可以读取和写入。 读取时,可以返回enabled
或disabled
。 如果返回enabled
,则表示设备能够发出事件;如果返回disabled
,则表示设备无法执行此操作。 向其写入enabled
或disabled
字符串将分别指示设备是否应该发出系统唤醒信号(内核device_may_wakeup()
帮助器将分别返回true
或false
)。 请注意,对于不能生成系统唤醒事件的设备,此文件不存在。
让我们在示例中看看驱动如何利用设备的唤醒功能。 以下是drivers/input/keyboard/snvs_pwrkey.c
中的i.MX6 SNVSPowerKey 驱动的摘录:
static int imx_snvs_pwrkey_probe(struct platform_device *pdev)
{
[...]
error = devm_request_irq(&pdev->dev, pdata->irq,
imx_snvs_pwrkey_interrupt, 0, pdev->name, pdev);
pdata->wakeup = of_property_read_bool(np, “wakeup-source”);
[...]
device_init_wakeup(&pdev->dev, pdata->wakeup);
return 0;
}
static int
maybe_unused imx_snvs_pwrkey_suspend(struct device *dev)
{
[...]
if (device_may_wakeup(&pdev->dev))
enable_irq_wake(pdata->irq);
return 0;
}
static int maybe_unused imx_snvs_pwrkey_resume(struct device *dev)
{
[...]
if (device_may_wakeup(&pdev->dev))
disable_irq_wake(pdata->irq);
return 0;
}
在前面的代码摘录中,从上到下,我们使用了驱动探测方法,它首先使用device_init_wakeup()
函数启用设备唤醒功能。 然后,在 PM 恢复回调中,它在通过调用enable_irq_wake()
来启用唤醒事件之前,使用关联的 IRQ 号作为参数,检查是否允许设备发出唤醒信号,这要归功于device_may_wakeup()
帮助器。 将device_may_wakeup()
用于条件唤醒事件启用/禁用的原因是,用户空间可能已经更改了此设备的唤醒策略(由于 /sys/devices/.../power/wakeup``sysfs
文件),在这种情况下,此帮助程序将返回当前启用/禁用状态。 该助手实现了与用户空间决策的一致性。 Resume 方法也是如此,它在禁用唤醒事件的 IRQ 行之前执行相同的检查。
接下来,在驱动代码的底部,我们可以看到以下内容:
static SIMPLE_DEV_PM_OPS(imx_snvs_pwrkey_pm_ops,
imx_snvs_pwrkey_suspend,
imx_snvs_pwrkey_resume);
static struct platform_driver imx_snvs_pwrkey_driver = {
.driver = {
.name = “snvs_pwrkey”,
.pm = &imx_snvs_pwrkey_pm_ops,
.of_match_table = imx_snvs_pwrkey_ids,
},
.probe = imx_snvs_pwrkey_probe,
};
上面显示了著名的SIMPLE_DEV_PM_OPS
宏的用法,这意味着相同的挂起回调(即imx_snvs_pwrkey_suspend
)将用于挂起到 RAM 或休眠睡眠状态,而相同的恢复回调(实际上是imx_snvs_pwrkey_resume
)将用于从这些状态恢复。 正如我们在宏中看到的,设备 PM 结构被命名为imx_snvs_pwrkey_pm_ops
,并在稍后提供给驱动。 填充 PM 操作就这么简单。
在结束这一节之前,让我们先来关注一下此设备驱动中的 IRQ 处理程序:
static irqreturn_t imx_snvs_pwrkey_interrupt(int irq,
void *dev_id)
{
struct platform_device *pdev = dev_id;
struct pwrkey_drv_data *pdata = platform_get_drvdata(pdev);
pm_wakeup_event(pdata->input->dev.parent, 0);
[...]
return IRQ_HANDLED;
}
这里的关键函数是pm_wakeup_event()
。 粗略地说,它报告了一个唤醒事件。 此外,这将暂停当前系统状态转换。 例如,在挂起路径上,它将中止挂起操作并阻止系统进入休眠状态。 以下是该函数的原型:
void pm_wakeup_event(struct device *dev, unsigned int msec)
第一个参数是唤醒源所属的设备,第二个参数msec
是唤醒源被 PM 唤醒核心自动切换到非活动状态之前等待的毫秒数。 如果msec
等于 0,则在报告事件后立即禁用唤醒源。 如果msec
不同于 0,则将唤醒源停用安排在未来msec
毫秒之后。
这是唤醒源的timer
和timer_expires
字段使用的地方。 粗略地说,唤醒事件上报由以下步骤组成:
-
它递增唤醒源的
event_count
计数器,并递增唤醒源的wakeup_count
,这是唤醒源可能中止挂起操作的次数。 -
If the wakeup source is not yet active (the following are the steps performed on the activation path):
-它将唤醒源标记为活动,并递增唤醒源的
active_count
元素。-它将唤醒源的
last_time
字段更新为当前时间。-如果另一个字段
autosleep_enabled
为true
,则更新唤醒源的start_prevent_time
字段。
然后,去激活唤醒源包括以下步骤:
- 它将唤醒源的
active
字段设置为false
。 - 它通过将处于活动状态的时间与其旧值相加来更新唤醒源的
total_time
字段。 - 如果唤醒源的
max_time
字段的持续时间大于旧的max_time
字段的值,则它使用处于活动状态的持续时间来更新该字段。 - 它用当前时间更新唤醒源的
last_time
字段,删除唤醒源的计时器,并清除timer_expires
。 - 如果另一个字段
prevent_sleep_time
为true
,则更新唤醒源的prevent_sleep_time
字段。
如果为msec == 0
,则可以立即停用;如果不为零,则可以计划在将来停用msec
毫秒。 所有这些都应该让您想起我们前面介绍的struct wakeup_source
,它的大部分元素都是通过这个函数调用更新的。 IRQ 处理程序是调用它的好地方,因为中断 t 操作还标记唤醒事件。 您还应该注意到,可以从 sysfs 接口检查任何唤醒的源的每个属性,我们将在下一节中看到。
这里还需要提到一些其他事情,至少出于调试的目的。 通过打印/sys/kernel/debug/wakeup_sources
的内容可以列出系统中的整个唤醒源列表(假设系统上安装了debugfs
):
# cat /sys/kernel/debug/wakeup_sources
该文件还报告了个唤醒源的统计信息,由于设备的与电源相关的 sysfs 属性,可以单独收集这些信息。 其中一些 sysfs 文件属性如下所示:
#ls /sys/devices/.../power/wake*
wakeup wakeup_active_count wakeup_last_time_ms autosuspend_delay_ms wakeup_abort_count wakeup_count wakeup_max_time_ms wakeup_active wakeup_expire_count wakeup_total_time_ms
我使用wake*
模式是为了过滤出与运行时 PM 相关的属性,这些属性也在同一目录中。 与其描述每个属性是什么,不如指出前面的属性映射到struct wakeup_source
结构中的哪些字段中会更有价值:
wakeup
是 RW 属性,前面已经描述过。 它的内容决定了device_may_wakeup()
帮助器的返回值。 只有此属性是既可读又可写的。 这里的其他文件都是只读的。wakeup_abort_count
和wakeup_count
是指向相同字段(即wakeup->wakeup_count
)的只读属性。- 将
wakeup_expire_count
属性映射到wakeup->expire_count
字段。 wakeup_active
是只读的,映射到wakeup->active
元素。wakeup_total_time_ms
是返回wakeup->total_time
值的只读属性,其单位是ms
。wakeup_max_time_ms
返回ms
中的power.wakeup->max_time
值。- 只读属性
wakeup_last_time_ms
对应于wakeup->last_time
值;单位为ms
。 wakeup_prevent_sleep_time_ms
也是只读的,并映射到以ms
为单位的唤醒->prevent_sleep_time
值。
并不是所有的设备都能唤醒,但那些能够唤醒的设备可以大致遵循这个指导原则。
现在我们已经完成并熟悉了 sysfs 中的唤醒源管理,我们可以引入特殊的IRQF_NO_SUSPEND
flag,它有助于防止 IRQ 在系统挂起路径中被禁用。
即使在整个系统挂起-恢复周期期间,也有个中断需要能够触发,包括挂起和恢复设备的noirq
个阶段,以及非引导 CPU 脱机和重新联机期间。 例如,计时器中断就是这种情况。 必须在此类中断上设置此标志。 虽然该标志有助于在挂起阶段保持启用中断,但它不能保证 IRQ 会将系统从挂起状态唤醒-对于这种情况,有必要使用enable_irq_wake()
,它同样是特定于平台的。 因此,您不应该混淆或混合使用IRQF_NO_SUSPEND
标志和enable_irq_wake()
。
如果带有此标志的 IRQ 由多个用户共享,则每个用户都会受到影响,而不仅仅是设置了该标志的用户。 换句话说,即使在suspend_device_irqs()
之后,向中断注册的每个处理程序也将照常被调用。 这可能不是你需要的。 因此,您应该避免混合使用IRQF_NO_SUSPEND
和IRQF_SHARED
标志。
在本章中,我们学习了如何管理系统的功耗,既可以从驱动中的代码中进行管理,也可以通过命令行从用户空间进行管理),或者在运行时通过对单个设备进行操作,或者通过处理睡眠状态来对整个系统进行操作。 我们还了解了其他框架如何帮助降低系统的功耗(如 CPUFreq、热量和 CPUIdle)。
在下一章中,我们将转到 PCI 设备驱动,它处理位于这条著名的总线上的设备,不需要介绍。