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:

Runtime PM API 选择决策流程 需要操作设备硬件? 当前是否在中断/原子上下文? 是(原子上下文) pm_runtime_get_noresume 否(进程上下文) pm_runtime_get_sync 硬件操作完成 释放时是否在原子上下文? 是 pm_runtime_put_noidle 否 pm_runtime_put_sync 提示:原子上下文中使用 pm_runtime_put_noidle 后,需配合 pm_request_idle 或 pm_schedule_suspend 触发实际挂起

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 完成前被意外挂起。