高级话题:多核系统中的唤醒锁管理、唤醒锁与CPU热插拔、唤醒锁与调度器
各位好,今天我们来聊几个进阶话题。说实话,单核系统下的唤醒锁管理相对简单,但到了多核时代,事情就变得有趣多了。我在项目中遇到过不少因为多核唤醒锁管理不当导致的功耗问题,今天就把这些经验分享给大家。
多核系统中的唤醒锁管理
多核系统里,每个CPU核心都可以独立进入休眠。这意味着什么呢?说白了,就是唤醒锁不再是一个全局概念,而是需要跟具体的CPU核心绑定。
我习惯把多核唤醒锁管理分成三个层次来看:
- 全局唤醒锁:阻止所有核心进入深度睡眠
- 局部唤醒锁:只阻止特定核心休眠
- 集群唤醒锁:控制某个CPU集群(比如大核集群)的休眠状态
你想想看,如果一个应用只在大核上运行,它却持有一个全局唤醒锁,那所有小核也被迫保持唤醒,这显然不合理。嗯,这里要注意,Android的PowerManager API其实没有直接暴露这种细粒度控制,但底层的wakeup source机制是支持的。
核心原则:唤醒锁的作用域应该尽量缩小到实际需要的CPU核心上。全局唤醒锁是万不得已的选择。
我记得有一次调试一个视频播放器的功耗问题。它在播放时持有一个PARTIAL_WAKE_LOCK,结果导致所有核心都无法休眠。实际上它只需要一个核心来解码视频流。后来我们改成了只在需要的核心上持有唤醒锁,功耗直接降了30%。
唤醒锁与CPU热插拔
CPU热插拔,说白了就是动态地开启或关闭CPU核心。这在手机上是家常便饭——你刷个微博,系统可能只开两个小核;你玩个游戏,大核集群就全开了。
唤醒锁和热插拔之间有个微妙的互动关系:
- 当一个CPU核心被热拔出时,它上面持有的唤醒锁怎么办?
- 如果一个唤醒锁绑定了某个核心,而这个核心被热拔了,系统该怎么处理?
我给大家画个图,看看这个流程:
从图中可以看到,当CPU被热拔出时,系统会尝试将唤醒锁迁移到其他核心。我曾经遇到过一个问题:某个驱动在CPU2上注册了一个wakeup source,但CPU2被热拔后,这个wakeup source没有被正确迁移,导致系统再也无法进入深度睡眠。排查了整整两天才找到原因。
避坑指南:我曾经在调试一个功耗问题时发现,某些第三方驱动在CPU热插拔回调中没有正确处理唤醒锁的迁移。结果就是——CPU被拔掉了,但唤醒锁还挂在那个已经不存在的核心上。系统会误以为还有任务在运行,从而阻止整个系统休眠。
解决方案其实不复杂:在CPU热插拔的CPU_DEAD通知中,遍历该核心上的所有唤醒锁,将它们迁移到其他在线核心上。内核中的wakeup_source_migrate()函数就是干这个的。
/* 伪代码:CPU热插拔时的唤醒锁迁移 */
static int cpu_hotplug_notifier(struct notifier_block *nb,
unsigned long action, void *hcpu)
{
unsigned int cpu = (unsigned long)hcpu;
switch (action) {
case CPU_DEAD:
case CPU_DEAD_FROZEN:
/* 关键:迁移该CPU上的所有唤醒锁 */
wakeup_source_migrate(cpu);
break;
}
return NOTIFY_OK;
}
唤醒锁与调度器
调度器和唤醒锁之间的关系,可以说是Android电源管理中最微妙的部分。为什么这么说?因为调度器决定哪个任务在哪个核心上运行,而唤醒锁决定哪些核心不能休眠。这两者必须协同工作。
我给大家总结几个关键点:
| 场景 | 调度器行为 | 唤醒锁影响 | 注意事项 |
|---|---|---|---|
| 持有唤醒锁的任务被调度到小核 | 正常调度 | 小核无法休眠 | 可能造成性能瓶颈 |
| 持有唤醒锁的任务被迁移 | 负载均衡触发迁移 | 唤醒锁跟随迁移 | 迁移开销需考虑 |
| 唤醒锁阻止了核心休眠 | 调度器会避开该核心 | 其他核心负载增加 | 可能导致负载不均 |
| 所有核心都被唤醒锁阻止 | 调度器正常调度 | 系统无法进入深度睡眠 | 功耗会显著增加 |
我个人习惯在调试功耗问题时,先检查/proc/wakeup_sources,看看哪些唤醒锁在活跃。然后结合/proc/sched_debug,看看这些唤醒锁对应的任务被调度到了哪些核心上。这两个信息一对照,问题往往就水落石出了。
实用技巧:在Android 12及以上版本中,你可以通过dumpsys power命令查看唤醒锁的详细信息,包括它们绑定的CPU核心。配合dumpsys cpuinfo使用,效果更佳。
还有一个容易被忽略的点:调度器的select_task_rq()函数在选择目标核心时,会检查该核心是否被唤醒锁阻止休眠。如果某个核心因为唤醒锁而无法休眠,调度器会倾向于把任务调度到那个核心上——反正它已经醒着了,不如让它多干点活。这个策略叫"wakeup-aware scheduling",说白了就是物尽其用。
嗯,这里要提醒一下:这个策略虽然能提高效率,但也会导致某些核心过热。我在一个项目中就遇到过这种情况——某个核心因为持有唤醒锁,被调度器塞满了任务,温度直接飙到了85度。后来我们不得不加了一个温度阈值,超过一定温度就不再往这个核心上调度新任务了。
最后说一个我踩过的坑:有一次,我发现系统在播放音乐时功耗异常高。排查了半天,发现是音频驱动在播放结束后没有释放唤醒锁。但有趣的是,这个唤醒锁被调度器感知到了,导致调度器认为这个核心很忙,就把其他任务也调度过来。结果就是——一个本该休眠的核心,因为一个没释放的唤醒锁,变成了全系统的性能核心。修复方法很简单:确保音频驱动在播放结束后正确释放唤醒锁。
好了,多核系统中的唤醒锁管理、热插拔和调度器的关系,说白了就是三个字:协同。唤醒锁告诉电源管理哪些核心不能睡,热插拔告诉唤醒锁核心要下线了,调度器则在这两者之间找到最优的任务分配方案。这三者配合好了,系统才能既省电又流畅。
公众号:蓝海资料掘金营,微信deep3321