Linux系统电源管理:从电源域到唤醒源的深度解析
1. 电源域的概念
电源域在不同视角下有不同的定义:
-
技术层面
:电源域是共享电源资源(如时钟或电源平面)的一组设备。
-
内核视角
:电源域是一组设备,其电源管理在子系统级别使用相同的回调集和通用的PM数据。
-
硬件视角
:电源域是用于管理电源电压相关设备的硬件概念,例如视频核心IP与显示IP共享一条电源轨。
由于SoC设计日益复杂,需要一种抽象方法来使驱动尽可能通用,于是出现了通用电源域(Generic Power Domain,genpd)。它是Linux内核的一种抽象,将每个设备的运行时电源管理扩展到共享电源轨的一组设备。此外,电源域在设备树中定义,描述了设备与电源控制器之间的关系,这使得电源域可以动态重新设计,驱动程序无需重启整个系统或重建新内核即可适应变化。
如果设备存在电源域对象,其PM回调将优先于总线类型(或设备类或类型)回调。相关的通用文档可在
Documentation/devicetree/bindings/power/power_domain.txt
内核源文件中找到,特定SoC的文档也可在同一目录中找到。
2. 系统挂起和恢复序列
struct dev_pm_ops
数据结构的引入有助于理解PM核心在挂起或恢复阶段执行的步骤和操作,其完整的系统PM链如下:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A([prepare]):::startend --> B(Suspend):::process
B --> C(suspend_late):::process
C --> D(suspend_noirq):::process
D --> E(Wakeup):::process
E --> F(resume_noirq):::process
F --> G(resume_early):::process
G --> H(resume):::process
H --> I([complete]):::startend
这个流程对应于
include/linux/suspend.h
中定义的
enum suspend_stat_step
枚举。在Linux内核代码中,
enter_state()
函数由系统电源管理核心调用,用于使系统进入睡眠状态。下面详细介绍系统挂起和恢复过程中实际发生的操作。
2.1 挂起阶段
enter_state()
在挂起时会经历以下步骤:
1.
同步文件系统
:如果未设置
CONFIG_SUSPEND_SKIP_SYNC
内核配置选项,则首先在文件系统上调用
sync()
(参见
ksys_sync()
)。
2.
调用挂起通知器
:在用户空间仍然存在时调用挂起通知器,可参考
register_pm_notifier()
进行注册。
3.
冻结任务
:调用
suspend_freeze_processes()
冻结用户空间和内核线程。如果内核配置中未设置
CONFIG_SUSPEND_FREEZER
,则跳过此步骤。
4.
挂起设备
:调用驱动程序注册的每个
.suspend()
回调,这是挂起的第一阶段(参见
suspend_devices_and_enter()
)。
5.
禁用设备中断
:调用
suspend_device_irqs()
禁用设备中断,防止设备驱动程序接收中断。
6.
第二阶段挂起设备
:调用
.suspend_noirq
回调,此步骤称为无中断(noirq)阶段。
7.
禁用非引导CPU
:使用CPU热插拔禁用非引导CPU,并通知CPU调度器在这些CPU离线前不要调度任何任务(参见
disable_nonboot_cpus()
)。
8.
关闭中断
:关闭系统的中断。
9.
执行系统核心回调
:调用
syscore_suspend()
执行系统核心回调。
10.
使系统进入睡眠状态
:将系统置于睡眠状态。
这些操作的行为可能会根据系统要进入的睡眠状态略有不同。
2.2 恢复阶段
当系统挂起后,一旦发生唤醒事件,系统需要恢复。PM核心执行以下步骤来唤醒系统:
1.
唤醒信号
:接收到唤醒信号。
2.
运行CPU唤醒代码
:执行CPU的唤醒代码。
3.
执行系统核心回调
:执行系统核心回调。
4.
打开中断
:打开系统的中断。
5.
启用非引导CPU
:使用CPU热插拔启用非引导CPU。
6.
设备恢复的第一阶段
:调用
.resume_noirq()
回调。
7.
启用设备中断
:启用设备的中断。
8.
设备恢复的第二阶段
:调用
.resume()
回调。
9.
解冻任务
:解冻之前冻结的任务。
10.
调用通知器
:当用户空间恢复后调用通知器。
从驱动程序的角度来看,这些步骤是透明的。驱动程序只需根据希望参与的步骤,在
struct dev_pm_ops
中填充适当的回调即可。
3. 实现系统睡眠能力
系统睡眠和运行时PM虽然相关,但却是不同的概念。在某些情况下,以不同方式操作可能会使系统达到相同的物理状态,因此通常不建议用一个替代另一个。
设备驱动程序通过根据需要参与的睡眠状态,在
struct dev_pm_ops
数据结构中填充一些回调来参与系统睡眠。常见的回调及其定义如下:
| 回调 | 描述 |
| ---- | ---- |
|
.suspend
| 在系统进入保留主内存内容的睡眠状态之前执行。 |
|
.resume
| 在系统从保留主内存内容的睡眠状态唤醒后调用,设备在此回调运行时的状态取决于设备所属的平台和子系统。 |
|
.freeze
| 特定于休眠,在创建休眠映像之前执行。类似于
.suspend
,但不应使设备能够发出唤醒事件或更改其电源状态。大多数实现此回调的设备驱动程序只需将设备设置保存在内存中,以便在后续从休眠恢复时使用。 |
|
.thaw
| 特定于休眠,在创建休眠映像后或映像创建失败时执行。也在尝试从此类映像恢复主内存内容失败后执行。必须撤销先前
.freeze
所做的更改,以使设备的操作方式与调用
.freeze
之前相同。 |
|
.poweroff
| 特定于休眠,在保存休眠映像后执行。类似于
.suspend
,但无需将设备设置保存在内存中。 |
|
.restore
| 特定于休眠,在从休眠映像恢复主内存内容后执行。类似于
.resume
。 |
为了提高代码可读性或便于回调填充,PM核心提供了一些宏:
3.1 SET_SYSTEM_SLEEP_PM_OPS宏
#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,
该宏接受挂起和恢复函数,并填充与系统相关的PM回调。
3.2 SET_NOIRQ_SYSTEM_SLEEP_PM_OPS宏
#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,
如果驱动程序仅需要参与系统挂起的无中断阶段,可以使用此宏自动填充
struct dev_pm_ops
数据结构中与
_noirq()
相关的回调。
3.3 SET_LATE_SYSTEM_SLEEP_PM_OPS宏
#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,
该宏将
suspend_late
、
freeze_late
和
poweroff_late
指向同一个函数,反之,
resume_early
、
thaw_early
和
restore_early
也指向同一个函数。
所有这些宏都受
#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
是系统进入挂起状态或从睡眠状态恢复时调用的回调。
4. 成为系统唤醒源
PM核心允许系统在挂起后被唤醒,能够唤醒系统的设备在PM术语中被称为唤醒源。唤醒源正常工作需要所谓的唤醒事件,大多数情况下,唤醒事件与IRQ线相关联。也就是说,唤醒源会生成唤醒事件,当唤醒源生成唤醒事件时,通过唤醒事件框架提供的接口将唤醒源设置为激活状态,事件处理结束后,将其设置为停用状态。激活和停用之间的间隔表示事件正在处理中。下面介绍如何在驱动代码中使设备成为系统唤醒源。
4.1 唤醒源数据结构
内核通过
struct wakeup_source
抽象唤醒源,该结构也用于收集与唤醒源相关的统计信息,其定义如下:
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;
};
该结构各字段含义如下:
| 字段 | 描述 |
| ---- | ---- |
|
name
| 唤醒源的名称 |
|
entry
| 用于在链表中跟踪所有唤醒源 |
|
lock
| 自旋锁 |
|
wakeirq
| 唤醒IRQ相关结构 |
|
timer
| 定时器 |
|
timer_expires
| 定时器到期时间 |
|
total_time
| 唤醒源处于激活状态的总时间 |
|
max_time
| 唤醒源连续处于激活状态的最长时间 |
|
last_time
| 唤醒源最后一次激活的开始时间 |
|
start_prevent_time
| 唤醒源开始阻止系统自动睡眠的时间点 |
|
prevent_sleep_time
| 唤醒源阻止系统自动睡眠的总时间 |
|
event_count
| 唤醒源报告的事件数量 |
|
active_count
| 唤醒源被激活的次数 |
|
relax_count
| 唤醒源被停用的次数 |
|
expire_count
| 唤醒源定时器到期的次数 |
|
wakeup_count
| 唤醒源终止挂起过程的次数 |
|
active
| 唤醒源的激活状态 |
|
autosleep_enabled
| 记录系统自动睡眠状态是否启用 |
4.2 使设备成为唤醒源的步骤
4.2.1 初始化唤醒能力
为了使设备成为唤醒源,其驱动程序必须调用
device_init_wakeup()
函数。该函数会设置设备的
power.can_wakeup
标志,使
device_can_wakeup()
辅助函数能够返回当前设备作为唤醒源的能力,并将其与唤醒相关的属性添加到sysfs中。此外,它会创建一个唤醒源对象,注册该对象并将其附加到设备(
dev->power.wakeup
)。但
device_init_wakeup()
仅使设备成为具有唤醒能力的设备,不会为其分配唤醒事件。
重要注意事项
:只有具有唤醒能力的设备才会在sysfs中有一个
power
目录,以提供所有唤醒信息。
4.2.2 分配唤醒事件
为了分配唤醒事件,驱动程序必须调用
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
)。请注意,对于不能生成系统唤醒事件的设备,该文件不存在。
4.3 驱动示例
以下是
i.MX 6 SNV S powerkey
驱动的部分代码示例:
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;
}
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,
};
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;
}
在上述代码中:
-
imx_snvs_pwrkey_probe
函数首先使用
device_init_wakeup()
函数启用设备的唤醒能力。
-
imx_snvs_pwrkey_suspend
函数在PM挂起回调中,使用
device_may_wakeup()
辅助函数检查设备是否允许发出唤醒信号,然后调用
enable_irq_wake()
启用唤醒事件。
-
imx_snvs_pwrkey_resume
函数在PM恢复回调中,同样使用
device_may_wakeup()
进行检查,然后调用
disable_irq_wake()
禁用唤醒事件的IRQ线。
-
SIMPLE_DEV_PM_OPS
宏表示将使用相同的挂起回调(
imx_snvs_pwrkey_suspend
)和恢复回调(
imx_snvs_pwrkey_resume
)来处理挂起到RAM或休眠睡眠状态。
-
imx_snvs_pwrkey_interrupt
函数中的
pm_wakeup_event()
函数用于报告唤醒事件,并可能会中止当前的系统状态转换。其原型如下:
void pm_wakeup_event(struct device *dev, unsigned int msec)
第一个参数是唤醒源所属的设备,第二个参数
msec
是在PM唤醒核心自动将唤醒源切换到非激活状态之前等待的毫秒数。如果
msec
等于0,则在报告事件后立即禁用唤醒源;如果
msec
不为0,则在未来
msec
毫秒后安排唤醒源停用。
综上所述,通过理解电源域、系统挂起和恢复序列、实现系统睡眠能力以及使设备成为唤醒源等方面的知识,开发者可以更好地管理Linux系统的电源,提高系统的能效和响应性。
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
A([设备驱动]):::startend --> B(调用device_init_wakeup):::process
B --> C(设置power.can_wakeup标志):::process
C --> D(添加唤醒相关属性到sysfs):::process
D --> E(创建并注册唤醒源对象):::process
E --> F(调用enable_irq_wake):::process
F --> G(开启平台逻辑处理IRQ):::process
G --> H(指示suspend_device_irqs特殊处理IRQ):::process
H --> I([设备成为唤醒源]):::startend
以上流程图展示了使设备成为唤醒源的主要步骤。
超级会员免费看
1330

被折叠的 条评论
为什么被折叠?



