3. Runtime PM API 详解:核心函数的使用与区别
Runtime PM(运行时电源管理)的核心操作围绕“引用计数”展开。设备在运行时,驱动通过增减引用计数来请求硬件进入或退出低功耗状态。Linux 内核提供了一组成对出现的 API,用于管理设备的 runtime 状态。理解这些 API 的语义差异,是正确实现驱动电源管理的关键。
核心原则: Runtime PM 的引用计数(usage_count)决定设备是否可以被挂起。计数为 0 时,系统允许执行挂起回调;计数大于 0 时,设备保持唤醒状态。
3.1 核心同步 API:pm_runtime_get_sync 与 pm_runtime_put_sync
这两个函数是最常用的同步接口,它们会立即执行状态切换操作,并等待操作完成。
3.1.1 pm_runtime_get_sync
功能: 增加设备的引用计数,并立即唤醒设备(如果当前处于挂起状态)。函数会阻塞直到设备进入“活跃”(active)状态。
- 调用时机: 在访问硬件寄存器前、开启时钟前、或任何需要设备保证供电的场景。
- 返回值: 0 表示成功;负数表示错误(如 -EACCES 表示设备不支持 runtime PM,-EBUSY 表示正在处理其他状态变更)。
- 注意事项: 调用后必须配对调用
pm_runtime_put_sync或pm_runtime_put,否则设备将永远无法挂起。
3.1.2 pm_runtime_put_sync
功能: 减少设备的引用计数,并立即尝试挂起设备(如果计数降为 0 且满足挂起条件)。函数会阻塞直到挂起操作完成或确定无法挂起。
- 调用时机: 在硬件访问结束后、时钟关闭后、或数据发送完成时。
- 返回值: 0 表示成功;1 表示设备已经处于挂起状态;负数表示错误。
- 注意事项: 如果设备在挂起过程中被其他上下文再次
get,挂起操作会被取消。
典型使用模式:
/* 在 SPI 传输开始前 */
pm_runtime_get_sync(&spi->dev);
/* 执行硬件操作 */
spi_write_then_read(spi, tx, len, rx, len);
/* 在传输结束后 */
pm_runtime_put_sync(&spi->dev);
3.2 轻量级 API:pm_runtime_get_noresume 与 pm_runtime_put_noidle
这两个函数只操作引用计数,不触发任何状态切换回调。它们适用于不需要立即改变硬件状态的场景,例如在中断上下文或自旋锁保护区域中。
| API | 操作 | 是否触发回调 | 典型场景 |
|---|---|---|---|
pm_runtime_get_noresume |
引用计数 +1 | 否 | 中断处理函数中标记设备活跃 |
pm_runtime_put_noidle |
引用计数 -1 | 否 | 中断处理函数末尾释放引用 |
3.2.1 pm_runtime_get_noresume
功能: 仅增加引用计数,不执行 runtime_resume 回调。如果设备当前处于挂起状态,调用此函数后设备仍然保持挂起,但引用计数增加会阻止后续的挂起尝试。
- 使用限制: 调用者必须确保设备在调用前已经处于活跃状态,或者后续会通过其他方式唤醒设备。
- 返回值: 无返回值(void)。
3.2.2 pm_runtime_put_noidle
功能: 仅减少引用计数,不触发 runtime_suspend 回调。即使计数降为 0,系统也不会立即尝试挂起设备。
- 使用限制: 通常需要配合
pm_request_idle或pm_schedule_suspend来触发实际的挂起操作。 - 返回值: 无返回值(void)。
常见错误: 在中断上下文中使用
pm_runtime_get_sync 会导致内核调度器阻塞,可能引发死锁。此时必须使用 pm_runtime_get_noresume 替代。
3.3 异步 API 与辅助函数
除了同步和轻量级 API,内核还提供了异步版本和辅助函数,用于更灵活地控制电源状态。
| API | 行为描述 | 与同步版本的区别 |
|---|---|---|
pm_runtime_get |
异步唤醒设备,立即返回 | 不等待唤醒完成,适合非关键路径 |
pm_runtime_put |
异步尝试挂起设备,立即返回 | 挂起操作在工作队列中执行 |
pm_request_idle |
请求设备进入空闲状态 | 仅在计数为 0 时触发挂起检查 |
pm_schedule_suspend |
延迟挂起设备 | 可指定延迟时间,避免频繁开关 |
3.4 API 选择决策流程
下图展示了在驱动开发中如何根据上下文选择合适的 Runtime PM API:
3.5 四组核心 API 对比总结
下表从多个维度对比了四组最常用的 Runtime PM API:
| API 名称 | 引用计数操作 | 触发回调 | 阻塞等待 | 原子安全 | 典型返回值 |
|---|---|---|---|---|---|
pm_runtime_get_sync |
+1 | runtime_resume | 是 | 否 | 0 / 负错误码 |
pm_runtime_put_sync |
-1 | runtime_suspend | 是 | 否 | 0 / 1 / 负错误码 |
pm_runtime_get_noresume |
+1 | 无 | 否 | 是 | void |
pm_runtime_put_noidle |
-1 | 无 | 否 | 是 | void |
3.6 实战代码示例:SPI 控制器驱动中的 API 组合
以下代码展示了一个 SPI 控制器驱动中,如何在不同的执行路径中组合使用这些 API:
/* SPI 消息发送函数(进程上下文) */
static int spi_send_message(struct spi_device *spi, const void *buf, size_t len)
{
struct spi_controller *ctlr = spi->controller;
int ret;
/* 同步唤醒控制器 */
ret = pm_runtime_get_sync(ctlr->dev.parent);
if (ret < 0) {
dev_err(&spi->dev, "failed to resume controller: %d\n", ret);
return ret;
}
/* 执行硬件传输 */
ret = ctlr->transfer_one_message(ctlr, spi, buf, len);
/* 同步尝试挂起 */
pm_runtime_put_sync(ctlr->dev.parent);
return ret;
}
/* 中断处理函数(原子上下文) */
static irqreturn_t spi_irq_handler(int irq, void *dev_id)
{
struct spi_controller *ctlr = dev_id;
/* 仅增加引用计数,不触发回调 */
pm_runtime_get_noresume(ctlr->dev.parent);
/* 处理中断事件 */
process_spi_interrupt(ctlr);
/* 仅减少引用计数,不触发挂起 */
pm_runtime_put_noidle(ctlr->dev.parent);
/* 请求系统在空闲时检查是否可挂起 */
pm_request_idle(ctlr->dev.parent);
return IRQ_HANDLED;
}
关键要点:
- 进程上下文使用
get_sync/put_sync保证状态同步。 - 中断上下文使用
get_noresume/put_noidle避免阻塞。 - 中断中释放引用后,调用
pm_request_idle触发挂起检查。 - 每个
get必须配对put,否则引用计数泄漏会导致设备无法挂起。
3.7 常见陷阱与调试建议
- 引用计数泄漏: 使用
cat /sys/devices/.../power/runtime_usage查看当前计数。如果计数持续增长,说明get和put未配对。 - 死锁风险: 在
runtime_suspend或runtime_resume回调中调用pm_runtime_get_sync会导致递归死锁。应使用pm_runtime_get_noresume替代。 - 状态不一致: 调用
pm_runtime_put_sync后立即访问硬件,可能因设备尚未完全挂起而失败。建议在put后检查返回值或使用异步版本。 - 调试工具: 开启内核配置
CONFIG_PM_RUNTIME_DEBUG和CONFIG_PM_DEBUG,通过echo enabled > /sys/devices/.../power/control启用 runtime PM 调试。
最佳实践: 在驱动初始化时调用
pm_runtime_enable(dev),在移除时调用 pm_runtime_disable(dev)。同时设置 dev->power.usage_count = 1 防止设备在 probe 完成前被意外挂起。