4、设备电源管理:Runtime PM框架、系统睡眠路径中的设备回调、设备电源域(Power Domain)、QoS框架

好,咱们进入设备电源管理这一章。说实话,这部分内容在Linux内核里属于「看着简单,用起来全是坑」的类型。我早年调试一个嵌入式设备,就因为Runtime PM的引用计数没处理好,导致设备在系统休眠时死活不关电,电池一晚上掉光。从那以后,我对这套机制就格外上心。

设备电源管理,说白了就是解决两个问题:设备不用的时候怎么省电,以及系统要睡觉的时候设备怎么配合。咱们一个一个拆开讲。

4.1 Runtime PM框架:让设备自己管自己

Runtime PM,全称Runtime Power Management,运行时电源管理。它的核心思想很简单——设备在用的时候供电,不用的时候自动断电。你想想看,一个USB鼠标,你不动它的时候,它其实没必要一直全速运行。Runtime PM就是干这个的。

这个框架在内核里由一套核心API支撑。我列几个最常用的:

  • pm_runtime_get_sync() — 唤醒设备,增加引用计数
  • pm_runtime_put_sync() — 允许设备休眠,减少引用计数
  • pm_runtime_get_if_in_use() — 如果设备正在用,就获取引用
  • pm_runtime_allow() / pm_runtime_forbid() — 控制是否允许运行时挂起

这里有个关键点:引用计数。内核通过一个计数器来判断设备是否可以被挂起。计数为0时,系统就会调用你注册的runtime_suspend回调。计数大于0时,设备保持唤醒状态。

核心流程:

  1. 驱动初始化时调用pm_runtime_enable()
  2. 每次访问硬件前调用pm_runtime_get_sync()
  3. 访问完成后调用pm_runtime_put_sync()
  4. 内核自动在合适时机调用runtime_suspend

我个人的习惯是,在驱动的probe()函数里就调用一次pm_runtime_get_sync(),然后在remove()里调用pm_runtime_put_sync()。这样能保证设备在驱动加载期间不会意外休眠。

小技巧:调试Runtime PM时,可以查看/sys/devices/.../power/control文件。写on强制唤醒,写auto允许自动休眠。这个我在现场排查问题时经常用。

4.2 系统睡眠路径中的设备回调

系统要睡觉了,设备得配合。Linux支持多种睡眠状态:suspend-to-idle、standby、mem(挂起到内存)、disk(挂起到磁盘)。不管哪种状态,设备驱动的回调函数都是关键。

设备在系统睡眠路径中的回调,按照调用顺序是这样的:

阶段回调函数说明
准备.suspend()保存硬件状态,停止DMA,关中断
延迟.suspend_late()在IRQ被禁用后执行,不能睡眠
完成.suspend_noirq()最后一步,此时中断完全关闭

唤醒路径反过来:.resume_noirq().resume_early().resume()

嗯,这里要注意一个坑:不要在.suspend()里做耗时操作。我曾经见过一个同事在suspend回调里等待硬件复位,结果整个系统睡眠时间被拖长到好几秒。正确的做法是,把耗时操作放到.suspend_late()之前完成,或者干脆用异步方式处理。

避坑指南:我曾经在调试一个网卡驱动时,发现系统休眠后无法唤醒。查了两天才发现,是.suspend_noirq()里调用了msleep()。记住,noirq阶段不能睡眠!用mdelay()或者忙等待代替。

4.3 设备电源域(Power Domain)

电源域这个概念,说白了就是一组共享同一路电源的设备。比如一个SoC里,GPU、ISP、显示控制器可能都在同一个电源域里。要关掉其中一个,得先看看其他设备是不是还在用。

内核里用struct dev_pm_domain来表示电源域。它本质上是一个抽象层,把设备的电源管理操作委托给域控制器。常见的实现有:

  • Generic PM Domain — 通用电源域框架,支持多设备共享
  • CPU PM Domain — 专门管理CPU核的电源域
  • Clock Domain — 基于时钟的电源域,常用于外设

我举个例子。假设你有一个摄像头模块,它属于cam_pd这个电源域。当摄像头驱动调用pm_runtime_put()时,内核不会直接关掉摄像头,而是先检查cam_pd里还有没有其他设备在用。如果都没有了,才会真正切断电源。

电源域的好处:

  • 避免频繁开关电源,减少功耗开销
  • 统一管理依赖关系,比如某个域必须先关A才能关B
  • 支持嵌套域,一个域可以包含另一个域

我个人建议,在设计设备树时就把电源域关系理清楚。别等到驱动写完了才发现,两个设备共享一个电源域但互相不知道对方的存在。那调试起来就痛苦了。

4.4 QoS框架:给性能留个底线

QoS,Quality of Service,服务质量。在电源管理里,QoS框架的作用是防止系统为了省电而牺牲关键性能

举个例子。你在播放视频,解码器需要CPU频率至少1GHz才能流畅播放。如果电源管理策略觉得CPU空闲,把频率降到500MHz,那视频就会卡顿。QoS框架就是用来阻止这种事情的。

内核里QoS的几种类型:

类型含义典型场景
CPU频率QoS限制CPU最低/最高频率视频播放、游戏
DMA延迟QoS限制DMA响应延迟音频流、实时控制
网络QoS限制网络吞吐量视频会议、在线游戏
设备延迟QoS限制设备唤醒延迟触摸屏、传感器

使用QoS的API很简单:

#include <linux/pm_qos.h>

// 请求CPU频率不低于1.2GHz
struct freq_qos_request req;
freq_qos_add_request(&req, FREQ_QOS_MIN, 1200000);

// 使用完后移除请求
freq_qos_remove_request(&req);

你想想看,这个机制其实就是一个「投票系统」。每个驱动或子系统都可以投一票,要求某个性能指标不低于某个值。内核会取所有请求中的最大值(或最小值,取决于类型)来执行。

调试技巧:查看/sys/devices/system/cpu/cpu0/cpufreq/下的文件,可以看到当前的QoS约束。我经常用cat scaling_min_freq来确认是不是有驱动偷偷拉了频率下限。

好了,设备电源管理这部分就讲到这里。总结一下:Runtime PM让设备自己管自己,系统睡眠路径靠回调函数协调,电源域解决设备间的电源依赖,QoS框架保证性能不翻车。这四个东西配合好了,你的设备才能既省电又稳定。

下一章咱们聊聊更高级的话题——CPU频率和电压的协同管理,也就是CPUFreq和CPUIdle的配合。到时候我会分享一个我在手机项目上遇到的「发热降频导致卡顿」的案例,挺有意思的。