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 之间转换。下图展示了核心的触发流程:
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 递归死锁。
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 状态。