3. 驱动框架与功耗管理:Linux PM框架简介
功耗管理,说白了就是让设备在不用的时候睡觉,用的时候立刻醒。Linux内核的PM框架就是干这个的。我刚开始接触这块时,觉得不就是个suspend/resume嘛,后来踩了坑才发现——没那么简单。
3.1 Linux PM框架的两大体系
Linux内核把功耗管理分成了两套机制:System PM 和 Runtime PM。它们解决的问题不一样,但底层回调函数是共通的。
| 特性 | System PM | Runtime PM |
|---|---|---|
| 触发方式 | 用户空间主动触发(echo mem > /sys/power/state) | 驱动内部自动触发(设备空闲时) |
| 影响范围 | 整个系统(所有设备) | 单个设备 |
| 典型场景 | 合上笔记本盖子、按电源键休眠 | 网卡没有数据收发时自动降速 |
| 回调函数 | suspend() / resume() | runtime_suspend() / runtime_resume() |
核心观点:System PM是全局的,Runtime PM是局部的。但最终落到设备驱动上,都是操作硬件寄存器来切电源或时钟。
3.2 设备驱动中的功耗回调函数
每个设备驱动都可以注册一组功耗回调。Linux内核通过 struct dev_pm_ops 来管理它们。我习惯把这个结构体叫做“功耗回调的入口”。
// 定义在 include/linux/pm.h
struct dev_pm_ops {
// System PM 回调
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);
// Runtime PM 回调
int (*runtime_suspend)(struct device *dev);
int (*runtime_resume)(struct device *dev);
int (*runtime_idle)(struct device *dev);
};
你想想看,一个驱动要同时支持System PM和Runtime PM,就得把这两套回调都实现。但别怕,很多逻辑是共用的。我在项目中经常这么干:
static int mydev_suspend(struct device *dev)
{
// 保存硬件状态
// 关闭时钟
// 关闭电源
return 0;
}
static int mydev_resume(struct device *dev)
{
// 打开电源
// 打开时钟
// 恢复硬件状态
return 0;
}
// Runtime PM 直接复用 System PM 的逻辑
static int mydev_runtime_suspend(struct device *dev)
{
return mydev_suspend(dev);
}
static int mydev_runtime_resume(struct device *dev)
{
return mydev_resume(dev);
}
我的习惯:Runtime PM的回调尽量复用System PM的底层函数。但要注意——Runtime PM的suspend/resume调用频率远高于System PM,所以里面不能有太重的操作,比如msleep()这种。
3.3 Runtime PM的核心流程
Runtime PM的机制其实不复杂。驱动通过几个API来告诉内核:我现在忙不忙。
pm_runtime_get_sync()—— 我要干活了,别让我睡pm_runtime_put_sync()—— 我干完了,可以睡了pm_runtime_allow()—— 开启Runtime PMpm_runtime_forbid()—— 关闭Runtime PM
内核内部有个引用计数。get一次就加1,put一次就减1。当计数降到0时,内核会调用 runtime_suspend()。嗯,这里要注意——计数是原子的,别搞成负数。
我曾经踩过的坑:在中断上下文里调用了 pm_runtime_get_sync(),结果内核直接panic了。因为 pm_runtime_get_sync() 内部可能会休眠。记住:中断上下文只能用 pm_runtime_get() 配合 pm_request_resume()。
3.4 System PM的休眠流程
当用户执行 echo mem > /sys/power/state 时,内核会按设备树的顺序,逐个调用驱动的suspend回调。恢复时顺序相反。
我画了一张图,帮你理清这个流程:
从图上你能看到,System PM的流程是线性的。内核会先冻结用户空间进程,然后按设备依赖关系依次suspend。恢复时反过来。我遇到过一个问题:某个设备的resume依赖另一个设备的时钟,但内核没按我期望的顺序来。后来发现,设备树里的 power-domains 属性可以控制依赖关系。
3.5 驱动中如何注册PM回调
实际写驱动时,我们通过 SET_SYSTEM_SLEEP_PM_OPS 和 SET_RUNTIME_PM_OPS 这两个宏来注册回调。这样代码更干净。
static const struct dev_pm_ops mydev_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(mydev_suspend, mydev_resume)
SET_RUNTIME_PM_OPS(mydev_runtime_suspend, mydev_runtime_resume, NULL)
};
struct platform_driver mydev_driver = {
.driver = {
.name = "my_device",
.pm = &mydev_pm_ops,
},
.probe = mydev_probe,
.remove = mydev_remove,
};
小技巧:如果驱动不支持System PM,可以把 SET_SYSTEM_SLEEP_PM_OPS 留空。但Runtime PM建议一定要实现,哪怕只是关个时钟。我在一个IoT项目里,光靠Runtime PM就把整机功耗降了30%。
3.6 避坑指南
做功耗管理驱动,有几个坑我反复踩过,列出来给你参考:
- 回调函数里不能调
msleep()吗? 可以,但Runtime PM的suspend里尽量别用。因为Runtime PM调用频繁,sleep会拖慢系统响应。 - 设备状态保存要完整。 我曾经只保存了控制寄存器,忘了保存FIFO状态,恢复后数据全乱了。
- 时钟和电源的开关顺序。 先关时钟再关电源,恢复时先开电源再开时钟。这个顺序错了,硬件可能锁死。
- 调试时多用
pm_runtime_get_noresume()。 这个API只增加引用计数,不会真正唤醒设备。我在调试阶段用它来模拟设备忙状态。
我曾经犯过的错:在probe函数里调用了 pm_runtime_enable(),但忘了调 pm_runtime_get_sync()。结果probe还没跑完,内核就以为设备空闲了,直接调了runtime_suspend。硬件还没初始化完就被关了,系统直接挂掉。正确的做法是:probe里先 pm_runtime_enable(),然后 pm_runtime_get_sync(),等初始化完成后再 pm_runtime_put_sync()。