31、Linux系统电源管理:从电源域到唤醒源的深度解析

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

以上流程图展示了使设备成为唤醒源的主要步骤。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值