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)的用武之地。

启用自动挂起需要三步:

  1. 设置延迟时间:调用 pm_runtime_set_autosuspend_delay(dev, delay_ms)
  2. 启用自动挂起:调用 pm_runtime_use_autosuspend(dev)
  3. 标记最后 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 累计时间

常见问题就两个:

  1. 设备挂不起来:检查 usage_count 是否归零,disable_depth 是否为0
  2. 设备频繁挂起唤醒:调整 autosuspend_delay,或者检查是否有中断在误触发

好了,runtime PM 的核心内容就这些。说白了就是三件事:理解数据结构、配对使用 get/put、合理配置自动挂起。下一章咱们会深入 system PM 的休眠流程,到时候你会发现,runtime PM 和 system PM 其实是同一枚硬币的两面。