3、Linux电源管理框架:从整体到细节

各位好,我是你们的讲师。今天我们来聊聊Linux电源管理框架。说实话,这个框架刚接触时确实有点绕,但搞懂了之后,你会发现它设计得非常巧妙。

我个人习惯把Linux电源管理分成三个层次来看:

  • 系统级PM:比如Suspend-to-RAM、Suspend-to-Disk
  • 设备级PM:Runtime PM,让设备按需开关
  • 框架层:把上面这些串起来的核心机制

嗯,咱们一个一个来拆解。

3.1 Linux PM子系统概览

Linux PM子系统,说白了就是一套管理功耗的“总指挥部”。它负责协调CPU、内存、外设等所有硬件,在合适的时候让它们休息,在需要的时候唤醒它们。

它的核心组件包括:

  • PM核心层:提供统一的接口,比如suspend、resume的回调
  • Runtime PM框架:设备级别的动态电源管理
  • 系统睡眠状态管理:处理suspend-to-RAM、suspend-to-disk等
  • CPU频率/空闲管理:cpufreq、cpuidle等子系统

我在项目中遇到过一个问题:某个外设在系统suspend后无法正常唤醒。查了半天,发现是PM回调函数没注册对。你想想看,这种问题排查起来真的很头疼。

核心要点:PM子系统不是孤立的,它需要设备驱动、总线驱动、平台代码共同配合。任何一个环节出问题,都可能导致系统无法正常休眠或唤醒。

3.2 Runtime PM:让设备按需工作

Runtime PM,全称是Runtime Power Management。它解决的是“设备在空闲时自动关闭,在需要时自动开启”的问题。

举个例子:你的嵌入式设备上有个USB摄像头。平时不用的时候,它完全可以断电。但你不能手动去关,对吧?Runtime PM就是干这个的——它根据设备的使用情况,自动管理电源状态。

它的工作流程大致是这样的:

  1. 设备驱动调用pm_runtime_get_sync(),告诉PM子系统“我要用这个设备了”
  2. PM子系统检查设备当前状态,如果处于挂起状态,就调用驱动的runtime_resume回调
  3. 设备使用完毕后,驱动调用pm_runtime_put_sync(),告诉PM子系统“我用完了”
  4. PM子系统根据引用计数,决定是否调用驱动的runtime_suspend回调

这里有个关键点:引用计数。每个设备都有一个usage_count,只有这个计数降到0时,PM子系统才会真正挂起设备。

我的经验:我曾经在调试一个音频驱动时,发现设备永远无法进入runtime suspend。后来发现是某个模块在初始化时调用了pm_runtime_get_sync(),但忘记在退出时调用pm_runtime_put_sync()。引用计数一直不为0,设备自然就挂不掉了。

驱动中如何实现Runtime PM?看个简单的例子:

static int my_device_runtime_suspend(struct device *dev)
{
    // 关闭硬件时钟
    // 保存寄存器状态
    // 关闭电源
    dev_dbg(dev, "device runtime suspended\n");
    return 0;
}

static int my_device_runtime_resume(struct device *dev)
{
    // 打开电源
    // 恢复寄存器状态
    // 开启硬件时钟
    dev_dbg(dev, "device runtime resumed\n");
    return 0;
}

static const struct dev_pm_ops my_device_pm_ops = {
    SET_RUNTIME_PM_OPS(my_device_runtime_suspend,
                       my_device_runtime_resume,
                       NULL)
};

嗯,这里要注意:SET_RUNTIME_PM_OPS这个宏,它会把suspend和resume回调注册到PM核心中。如果你的内核配置了CONFIG_PM,这些回调才会生效。

3.3 Suspend-to-RAM:让系统“假寐”

Suspend-to-RAM,也叫STR或者“挂起到内存”。说白了,就是把系统状态保存在内存中,然后关闭大部分硬件,只保留内存供电。这样功耗极低,但唤醒速度很快。

它的流程是这样的:

  1. 用户触发suspend(比如按电源键)
  2. 内核冻结所有用户空间进程
  3. 逐个挂起所有设备(调用驱动的suspend回调)
  4. 关闭CPU、关闭外设时钟
  5. 进入睡眠状态,等待唤醒事件
  6. 唤醒后,反向执行:恢复设备、解冻进程

我在项目中遇到过一个问题:某个设备在suspend时,它的suspend回调里调用了msleep()。结果呢?系统卡死了。为什么?因为suspend过程中,调度器已经停止了,msleep()根本不会返回。

避坑指南:在suspend/resume回调中,不要使用任何可能引起睡眠的函数,比如msleep()wait_event()mutex_lock()等。我曾经因为这个bug,排查了整整两天。

驱动中如何实现suspend/resume?看代码:

static int my_device_suspend(struct device *dev)
{
    // 保存硬件状态
    // 关闭设备
    dev_dbg(dev, "device suspended\n");
    return 0;
}

static int my_device_resume(struct device *dev)
{
    // 恢复硬件状态
    // 重新初始化设备
    dev_dbg(dev, "device resumed\n");
    return 0;
}

static const struct dev_pm_ops my_device_pm_ops = {
    .suspend = my_device_suspend,
    .resume = my_device_resume,
};

这里有个细节:suspendresume回调的执行顺序,是由设备在设备树中的顺序决定的。父设备先suspend,后resume;子设备后suspend,先resume。这个顺序很重要,因为子设备可能依赖父设备的时钟或电源。

3.4 Suspend-to-Disk:让系统“冬眠”

Suspend-to-Disk,也叫STD或者“休眠”。它比STR更进一步:把系统状态保存到磁盘上,然后完全断电。下次开机时,从磁盘恢复状态。

它的流程是:

  1. 用户触发休眠
  2. 内核创建内存镜像,写入交换分区或休眠文件
  3. 完全断电
  4. 下次开机时,引导加载器检测到休眠镜像,加载并恢复

你想想看,STD和STR最大的区别是什么?STR需要保持内存供电,而STD完全不需要。所以STD的功耗更低,但唤醒时间更长(因为要从磁盘读取镜像)。

驱动开发中,STD和STR的接口是相同的——都是suspendresume回调。但有个区别:在STD中,设备在suspend后会被完全断电,所以resume时需要重新初始化硬件,而不是简单地恢复寄存器。

关键点:如果你的设备在STR和STD下的行为不同,可以通过检查pm_message_t中的事件类型来区分。比如:

static int my_device_suspend(struct device *dev, pm_message_t state)
{
    if (state.event == PM_EVENT_SUSPEND) {
        // STR模式
    } else if (state.event == PM_EVENT_HIBERNATE) {
        // STD模式
    }
    return 0;
}

我记得有一次,一个同事在调试STD时,发现系统恢复后某些设备无法正常工作。排查后发现,这些设备的resume回调里没有做完整的硬件初始化,只是恢复了寄存器。但在STD模式下,设备已经完全断电,寄存器内容早就丢了。

我的建议:在实现resume回调时,最好统一做完整的硬件初始化,而不是依赖寄存器恢复。这样无论是STR还是STD,都能正常工作。

3.5 总结一下

好了,我们来快速回顾一下今天的内容:

  • PM子系统是Linux电源管理的总框架,协调所有硬件
  • Runtime PM让设备按需开关,通过引用计数管理
  • Suspend-to-RAM保存状态到内存,快速唤醒
  • Suspend-to-Disk保存状态到磁盘,完全断电

嗯,这些概念听起来可能有点抽象,但你在实际开发中一定会遇到。尤其是Runtime PM,几乎每个驱动都需要实现。我建议你从简单的设备开始,逐步理解整个流程。

下一章,我们会深入讲解PM回调函数的实现细节,包括如何正确处理时钟、电源、中断等资源。到时候见!