4. Runtime PM 回调函数:struct dev_pm_ops 中的 runtime_suspend、runtime_resume、runtime_idle

Runtime PM 的核心机制依赖于设备驱动向 PM 核心注册的三个回调函数。它们定义在 struct dev_pm_ops 结构体中,分别对应设备进入低功耗状态、恢复全速运行以及空闲通知。理解这三个回调的触发时机与实现规范,是编写正确 Runtime PM 驱动的关键。

4.1 回调函数定义与数据结构

在 Linux 内核中,Runtime PM 回调是 struct dev_pm_ops 的一部分。该结构体同时包含了系统睡眠(Suspend-to-RAM、Hibernate)的回调,但 Runtime PM 只使用其中三个特定成员:

struct dev_pm_ops {
    // ... 系统睡眠回调(suspend, resume, freeze, poweroff, restore)
    int (*runtime_suspend)(struct device *dev);
    int (*runtime_resume)(struct device *dev);
    int (*runtime_idle)(struct device *dev);
};
回调名称 函数指针 核心职责 返回值
runtime_suspend int (*)(struct device *) 将设备硬件置于低功耗状态,保存必要的上下文 0 成功;-EAGAIN 或 -EBUSY 暂缓挂起
runtime_resume int (*)(struct device *) 将设备硬件恢复到全速运行状态,恢复上下文 0 成功;负值表示恢复失败
runtime_idle int (*)(struct device *) 通知驱动设备已经空闲,驱动可决定是否请求挂起 0 成功;-EAGAIN 暂不处理
关键区别: runtime_idle 是一个“通知”回调,驱动在此处通常调用 pm_runtime_autosuspend() 或 pm_request_autosuspend() 来触发后续的 runtime_suspend。而 runtime_suspend 和 runtime_resume 是真正的硬件操作回调。

4.2 触发时机与状态机

这三个回调的触发由 Runtime PM 核心的状态机控制。设备状态在 RPM_ACTIVE、RPM_SUSPENDED 和 RPM_SUSPENDING/RPM_RESUMING 之间转换。下图展示了核心的触发流程:

Runtime PM 回调触发状态机 RPM_ACTIVE RPM_SUSPENDED RPM_SUSPENDING RPM_RESUMING usage_count=0 调用 runtime_suspend suspend 成功 usage_count>0 调用 runtime_resume resume 成功 pm_runtime_idle() 调用 runtime_idle 驱动决策: pm_runtime_autosuspend() 触发时机总结: 1. runtime_idle:在 pm_runtime_idle() 被调用时触发,通常由设备使用计数降为 0 引起。 2. runtime_suspend:在 pm_runtime_suspend() 被调用且条件满足时触发。 3. runtime_resume:在 pm_runtime_get() 系列函数被调用时触发。

4.3 回调实现规范与最佳实践

4.3.1 runtime_suspend 实现规范

  • 硬件操作: 关闭设备时钟、关闭电源域、将引脚置于高阻态或下拉状态。
  • 上下文保存: 如果设备在挂起后会丢失寄存器状态,必须在关闭时钟前保存关键寄存器。
  • 原子性: 该回调在 spinlock 或 mutex 保护下执行,但禁止调用可能导致睡眠的函数(如 msleep),除非明确知道当前上下文允许。
  • 返回值: 返回 -EAGAIN 或 -EBUSY 可以暂缓挂起,PM 核心会稍后重试。
static int my_device_runtime_suspend(struct device *dev)
{
    struct my_device *mdev = dev_get_drvdata(dev);

    dev_dbg(dev, "Runtime suspend: saving context and powering off\n");

    // 1. 保存硬件上下文(如果必要)
    mdev->saved_reg = readl(mdev->regs + REG_CTRL);

    // 2. 关闭设备时钟
    clk_disable_unprepare(mdev->clk);

    // 3. 可选:通知电源域控制器
    // regulator_disable(mdev->regulator);

    return 0;
}

4.3.2 runtime_resume 实现规范

  • 硬件操作: 开启时钟、恢复电源、重新初始化硬件。
  • 上下文恢复: 在硬件准备好后,将保存的寄存器值写回。
  • 顺序: 先恢复电源/时钟,再恢复寄存器,最后清除可能挂起的 IRQ。
  • 性能: 该回调在设备访问路径上频繁调用,应尽量轻量,避免不必要的延迟。
static int my_device_runtime_resume(struct device *dev)
{
    struct my_device *mdev = dev_get_drvdata(dev);

    dev_dbg(dev, "Runtime resume: powering on and restoring context\n");

    // 1. 开启设备时钟
    clk_prepare_enable(mdev->clk);

    // 2. 恢复硬件上下文
    writel(mdev->saved_reg, mdev->regs + REG_CTRL);

    // 3. 重新初始化硬件状态机
    writel(INIT_VAL, mdev->regs + REG_STATUS);

    return 0;
}

4.3.3 runtime_idle 实现规范

  • 决策点: 驱动在此回调中判断设备是否真的可以进入低功耗状态。
  • 典型操作: 调用 pm_runtime_autosuspend() 或 pm_request_autosuspend() 来启动延迟挂起。
  • 避免直接挂起: 通常不直接在 runtime_idle 中调用 pm_runtime_suspend(),而是使用自动挂起机制,以避免频繁的挂起/恢复震荡。
  • 返回值: 返回 -EAGAIN 表示驱动暂不处理空闲通知,PM 核心会继续尝试。
static int my_device_runtime_idle(struct device *dev)
{
    struct my_device *mdev = dev_get_drvdata(dev);

    dev_dbg(dev, "Runtime idle: device is idle, scheduling autosuspend\n");

    // 使用自动挂起,延迟 100ms 后执行挂起
    pm_runtime_autosuspend(dev);

    // 返回 0 表示已处理空闲通知
    return 0;
}
常见陷阱: 在 runtime_suspend 中调用 pm_runtime_get_sync() 会导致递归死锁。同样,在 runtime_resume 中调用 pm_runtime_put_sync() 也可能引发状态机混乱。回调函数内部应避免调用任何可能改变当前设备 PM 状态的函数。

4.4 回调函数对比速查表

对比维度 runtime_suspend runtime_resume runtime_idle
触发原因 设备使用计数降为 0,且空闲超时到期 设备使用计数从 0 变为正数 设备使用计数降为 0 时立即触发
硬件操作 关闭时钟/电源,保存上下文 开启时钟/电源,恢复上下文 无硬件操作,仅软件决策
可睡眠 是(在进程上下文) 是(在进程上下文) 是
典型延迟要求 可接受毫秒级延迟 要求低延迟(微秒级最佳) 无严格要求
错误处理 返回 -EAGAIN 重试 返回负值,设备状态不变 返回 -EAGAIN 暂不处理
调用频率 低(仅在空闲时) 高(每次设备访问) 中等(每次使用计数归零)

4.5 回调注册与驱动绑定

驱动通过 struct platform_driver 或 struct i2c_driver 等总线驱动结构体中的 driver.pm 字段注册回调:

static const struct dev_pm_ops my_device_pm_ops = {
    .runtime_suspend = my_device_runtime_suspend,
    .runtime_resume  = my_device_runtime_resume,
    .runtime_idle    = my_device_runtime_idle,
};

static struct platform_driver my_device_driver = {
    .driver = {
        .name = "my_device",
        .pm   = &my_device_pm_ops,
    },
    .probe  = my_device_probe,
    .remove = my_device_remove,
};

module_platform_driver(my_device_driver);
内核版本注意: 自 Linux 2.6.33 引入 Runtime PM 框架以来,struct dev_pm_ops 的接口保持稳定。如果驱动不需要 runtime_idle 回调,可以将其设置为 NULL,PM 核心会使用默认行为(立即尝试挂起)。

4.6 回调执行上下文与锁

Runtime PM 回调的执行上下文取决于调用路径:

  • 进程上下文: 当通过 pm_runtime_get_sync()、pm_runtime_put_sync() 等同步接口调用时,回调在调用者进程上下文中执行,可以睡眠。
  • 中断上下文: 当通过 pm_runtime_get()、pm_runtime_put() 等异步接口调用时,回调在 PM 工作队列(pm_wq)中执行,同样可以睡眠。
  • 原子上下文: 在 spinlock 保护的临界区内,禁止调用任何可能触发 Runtime PM 回调的函数。

PM 核心在调用回调前会持有设备的 power.lock(spinlock_t 类型),因此回调内部不能再次尝试获取该锁,否则会导致 spinlock 递归死锁。

回调执行上下文与锁层级 调用者(进程/中断) PM 核心 power.lock 驱动回调 ⚠ 回调执行时 power.lock 已被持有,回调内禁止调用 pm_runtime_get/put 等操作 ⚠ 回调内可以睡眠,但必须注意不能长时间阻塞工作队列

4.7 调试与验证

在开发阶段,可以通过以下方式验证回调是否正确触发:

  • 动态调试: 在回调中加入 dev_dbg() 或 trace_printk(),通过 echo 'file my_driver.c +p' > /sys/kernel/debug/dynamic_debug/control 开启。
  • PM 统计: 查看 /sys/devices/.../power/ 下的 runtime_status、runtime_usage、runtime_active_time 等文件。
  • ftrace: 使用 trace-cmd record -e power:cpu_frequency -e rpm:* 捕获 Runtime PM 事件。
调试技巧: 如果 runtime_suspend 从未被调用,检查设备是否启用了 Runtime PM(pm_runtime_enable()),以及使用计数是否真的降到了 0。使用 cat /sys/kernel/debug/pm_runtime/devices 可以查看所有设备的 Runtime PM 状态。