4、runtime PM框架:核心数据结构、接口与自动挂起唤醒
好,咱们今天来聊聊 runtime PM 框架。说实话,这是内核动态电源管理里最实用的部分之一。我当年刚接触这个模块时,觉得它就是个「黑盒」——调个接口设备就能休眠,再调一下又能醒来。但后来踩过几次坑才明白,不理解核心数据结构,你根本玩不转这套机制。
4.1 核心数据结构:dev->power 的秘密
每个设备结构体 struct device 里,都藏着一个 struct dev_pm_info power 成员。这就是 runtime PM 的「心脏」。我习惯把它想象成设备的「状态机控制器」。
来看看这个结构体的关键字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| usage_count | atomic_t | 使用计数,>0 时设备不能挂起 |
| runtime_status | enum rpm_status | 当前状态:ACTIVE/SUSPENDED/SUSPENDING/RESUMING |
| runtime_error | int | 记录最后一次 PM 操作的错误码 |
| disable_depth | unsigned int | 禁用深度,>0 时 runtime PM 被禁用 |
| idle_notification | bool | 是否正在发送 idle 通知 |
| request_pending | bool | 是否有挂起的请求 |
| request | enum rpm_request | 挂起的请求类型 |
| work | struct work_struct | 用于异步操作的工作队列 |
| timed_autosuspend | bool | 是否启用定时自动挂起 |
| autosuspend_delay | int | 自动挂起延迟时间(毫秒) |
| last_busy | ktime_t | 最后一次 busy 时间戳 |
嗯,这里要注意 usage_count。它是个原子变量,每次调用 pm_runtime_get() 就加1,pm_runtime_put() 就减1。只有减到0时,系统才会考虑让设备休眠。我在项目中遇到过一个问题:某个驱动在 probe 时忘了调用 pm_runtime_put(),结果 usage_count 一直为1,设备永远挂不起来。排查了半天才发现是计数没配对。
核心原则:usage_count 就像一把锁。你拿一次就得还一次,否则设备永远醒着。
4.2 pm_runtime_get/put 接口:成对使用是铁律
这两个接口是 runtime PM 的「门面」。说白了,驱动开发者大部分时间就是在调它们。
先看 pm_runtime_get() 系列:
// 同步获取,会阻塞直到设备 resume 完成
int pm_runtime_get_sync(struct device *dev);
// 异步获取,立即返回
int pm_runtime_get(struct device *dev);
// 带返回值检查的获取
int pm_runtime_resume_and_get(struct device *dev);
再看 pm_runtime_put() 系列:
// 同步释放,会尝试立即挂起
int pm_runtime_put_sync(struct device *dev);
// 异步释放,延迟挂起
int pm_runtime_put(struct device *dev);
// 带自动挂起延迟的释放
int pm_runtime_put_autosuspend(struct device *dev);
我个人习惯用 pm_runtime_resume_and_get()。为什么?因为它会帮你检查返回值。如果设备 resume 失败,它直接返回错误码,省得你再去判断。我曾经在某个项目中用了 pm_runtime_get_sync(),结果设备 resume 失败返回了0,我以为成功了,接着就去操作硬件寄存器...嗯,后果你懂的,系统直接挂了。
我的建议:新代码一律用 pm_runtime_resume_and_get()。这是内核社区推荐的做法,也是我踩坑后的血泪教训。
来看一个典型的用法模式:
static int my_device_read(struct my_dev *dev, void *buf, size_t len)
{
int ret;
// 1. 唤醒设备
ret = pm_runtime_resume_and_get(dev->device);
if (ret < 0)
return ret;
// 2. 执行硬件操作
ret = hardware_read(dev, buf, len);
// 3. 释放设备,允许挂起
pm_runtime_mark_last_busy(dev->device);
pm_runtime_put_autosuspend(dev->device);
return ret;
}
你想想看,这个模式其实很清晰:拿资源 -> 干活 -> 还资源。跟锁的使用一模一样。
4.3 自动挂起与唤醒:让内核帮你「偷懒」
手动调 get/put 当然可以,但很多时候我们希望设备在空闲一段时间后自动休眠。这就是自动挂起(autosuspend)的用武之地。
启用自动挂起需要三步:
- 设置延迟时间:调用
pm_runtime_set_autosuspend_delay(dev, delay_ms) - 启用自动挂起:调用
pm_runtime_use_autosuspend(dev) - 标记最后 busy 时间:每次使用完设备后调用
pm_runtime_mark_last_busy(dev)
内核会在设备空闲(usage_count=0)后,等待 autosuspend_delay 毫秒,如果期间没有新的 get 请求,就自动触发挂起。
我记得有个项目是做 USB 网卡的驱动。网卡在传输数据时频繁调 get/put,如果每次空闲都立即挂起,再醒来,开销反而更大。后来我设置了 100ms 的自动挂起延迟,效果立竿见影——吞吐量上去了,功耗也没增加多少。
避坑指南:我曾经把 autosuspend_delay 设成 0,以为这样能最快省电。结果设备在 idle 和 active 之间疯狂切换,功耗反而比一直 active 还高。自动挂起不是越快越好,要根据实际业务场景调参。
自动唤醒又是怎么回事?其实很简单:当设备挂起后,如果有硬件事件(比如中断)需要处理,驱动需要在中断处理函数中调用 pm_runtime_get() 来唤醒设备。内核提供了 pm_request_resume() 用于异步唤醒请求。
来看一个完整的自动挂起/唤醒示例:
static int my_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
// 启用 runtime PM
pm_runtime_enable(dev);
// 设置自动挂起延迟 200ms
pm_runtime_set_autosuspend_delay(dev, 200);
pm_runtime_use_autosuspend(dev);
// 初始状态为 active
pm_runtime_get_sync(dev);
// ... 其他初始化 ...
// 释放初始引用,允许自动挂起
pm_runtime_mark_last_busy(dev);
pm_runtime_put_autosuspend(dev);
return 0;
}
static irqreturn_t my_irq_handler(int irq, void *data)
{
struct my_dev *dev = data;
// 唤醒设备处理中断
pm_runtime_get_sync(dev->device);
// ... 处理中断 ...
pm_runtime_mark_last_busy(dev->device);
pm_runtime_put_autosuspend(dev->device);
return IRQ_HANDLED;
}
这里有个细节:probe 时先 get_sync 再 put_autosuspend,目的是确保设备在初始化完成后处于 active 状态,然后让自动挂起机制接管后续的休眠决策。
4.4 状态机与回调函数
runtime PM 内部维护了一个状态机。设备在 ACTIVE、SUSPENDED、SUSPENDING、RESUMING 四个状态间切换。每次状态转换都会触发对应的回调:
| 回调函数 | 触发时机 | 注意事项 |
|---|---|---|
| runtime_suspend | 设备从 ACTIVE -> SUSPENDED | 不能睡眠,要快速完成 |
| runtime_resume | 设备从 SUSPENDED -> ACTIVE | 恢复硬件状态到挂起前 |
| runtime_idle | 设备空闲,即将决定是否挂起 | 可以返回 -EBUSY 阻止挂起 |
这些回调定义在 struct dev_pm_ops 中。我习惯在 runtime_suspend 里保存硬件上下文,在 runtime_resume 里恢复。注意,这两个回调是在进程上下文中执行的,但不可以调用可能导致睡眠的函数——因为内核持有 spinlock。
关键点:runtime_suspend 和 runtime_resume 必须成对实现。如果你只实现了 suspend 没有 resume,那设备挂起后就再也醒不来了。别笑,我真见过这样的代码。
4.5 调试与常见问题
最后分享几个调试技巧:
- 查看 runtime PM 状态:
/sys/devices/.../power/runtime_status文件显示当前状态 - 手动控制:
/sys/devices/.../power/control可以设为 on/auto 来启用或禁用 runtime PM - 统计信息:
/sys/devices/.../power/runtime_usage显示 usage_count,runtime_active_time显示 active 累计时间
常见问题就两个:
- 设备挂不起来:检查 usage_count 是否归零,disable_depth 是否为0
- 设备频繁挂起唤醒:调整 autosuspend_delay,或者检查是否有中断在误触发
好了,runtime PM 的核心内容就这些。说白了就是三件事:理解数据结构、配对使用 get/put、合理配置自动挂起。下一章咱们会深入 system PM 的休眠流程,到时候你会发现,runtime PM 和 system PM 其实是同一枚硬币的两面。