高级话题:唤醒锁与内核同步机制、唤醒锁与RCU、唤醒锁与内存管理
各位,今天我们聊点硬核的。唤醒锁(Wake Lock)这东西,很多人以为它就是个“别让系统睡”的开关。其实不然。当你深入内核,你会发现唤醒锁和同步机制、RCU、内存管理之间,有着千丝万缕的联系。搞不清楚这些,你的系统可能莫名其妙地卡死、内存泄漏,甚至睡死过去。
我个人习惯把这三块内容放在一起讲,因为它们本质上都是在解决同一个问题:在电源管理的“睡眠”与“唤醒”边界上,如何保证数据一致性和系统稳定性。
唤醒锁与内核同步机制
先说说同步。你想想看,一个唤醒锁被持有的时候,系统不能进入深度睡眠。但内核里可能有几十个线程在同时跑,它们都在申请、释放唤醒锁。如果没有同步保护,会发生什么?
嗯,我遇到过。有一次调试一个平板设备的待机功耗,发现唤醒锁计数莫名其妙地变成了负数。一查,原来是两个线程同时释放了同一个唤醒锁,没有加锁保护。内核直接崩了。
核心要点:唤醒锁的申请和释放操作,必须在内核的原子上下文中进行。说白了,就是要用自旋锁(spinlock)或者原子操作来保护。
内核里是怎么做的?我们来看一段伪代码:
// 唤醒锁结构体
struct wakeup_source {
const char *name;
struct list_head entry;
atomic_t active_count; // 原子变量,防止并发
spinlock_t lock; // 自旋锁保护
...
};
// 申请唤醒锁
void __pm_stay_awake(struct wakeup_source *ws) {
unsigned long flags;
spin_lock_irqsave(&ws->lock, flags);
if (atomic_inc_return(&ws->active_count) == 1) {
// 第一次激活,通知电源管理子系统
wakeup_source_activate(ws);
}
spin_unlock_irqrestore(&ws->lock, flags);
}
注意这里用了 spin_lock_irqsave,为什么?因为唤醒锁的申请可能发生在中断上下文。如果你用普通的互斥锁(mutex),在中断里调用会导致死锁。我曾经在这个坑里爬了整整两天。
避坑指南:我曾经在驱动里用了一个 mutex 来保护唤醒锁的计数,结果在中断处理函数里调用了 pm_stay_awake(),系统直接死锁。记住:中断上下文只能用 spinlock,不能用 mutex。
唤醒锁与RCU
RCU(Read-Copy Update)是内核里一种非常优雅的同步机制。它允许读操作几乎不加锁,写操作通过“延迟回收”来保证一致性。那唤醒锁和 RCU 有什么关系?
关系大了。你想想看,唤醒锁的链表(所有已注册的唤醒源)会被频繁遍历——电源管理子系统要检查当前有哪些唤醒锁被持有。如果每次遍历都加锁,性能会很难看。
所以,内核里唤醒锁的注册和注销,用的是 RCU 机制。读端(遍历链表)不需要加锁,写端(添加/删除唤醒锁)通过 RCU 的 grace period 来安全释放内存。
我的经验:在调试一个多核设备的唤醒延迟时,我发现遍历唤醒锁链表占了大量 CPU 时间。改用 RCU 后,读操作几乎零开销,唤醒延迟从 5ms 降到了 0.5ms。
来看一个简化版的实现思路:
// 唤醒锁链表,用 RCU 保护
static LIST_HEAD(wakeup_sources);
static DEFINE_SPINLOCK(wakeup_sources_lock); // 写锁
// 注册唤醒锁(写操作)
void wakeup_source_register(struct wakeup_source *ws) {
spin_lock(&wakeup_sources_lock);
list_add_rcu(&ws->entry, &wakeup_sources);
spin_unlock(&wakeup_sources_lock);
synchronize_rcu(); // 等待所有读端完成
}
// 遍历唤醒锁(读操作)
void wakeup_sources_read(void) {
struct wakeup_source *ws;
rcu_read_lock();
list_for_each_entry_rcu(ws, &wakeup_sources, entry) {
// 安全读取 ws 的内容
if (ws->active_count > 0) {
// 处理活跃的唤醒锁
}
}
rcu_read_unlock();
}
这里的关键是 list_add_rcu 和 list_for_each_entry_rcu。它们保证了读操作在遍历过程中,即使有写操作在添加或删除节点,也不会读到野指针。
注意:RCU 的写操作代价较高(需要等待 grace period),所以它适合“读多写少”的场景。唤醒锁的注册和注销通常只在驱动加载/卸载时发生,而遍历是高频操作,正好匹配。
唤醒锁与内存管理
最后聊聊内存管理。你可能觉得唤醒锁和内存管理八竿子打不着。错了。唤醒锁直接影响系统的内存回收策略。
为什么?因为当系统持有唤醒锁时,它不能进入深度睡眠。而深度睡眠的一个重要步骤就是压缩或回收内存(比如把匿名页换出到 zram)。如果唤醒锁阻止了睡眠,内存回收的时机就会被推迟。
我在项目中遇到过一个问题:某个多媒体应用持有一个唤醒锁,导致系统长时间无法进入 suspend。结果内存压力越来越大,kswapd 疯狂运行,CPU 占用飙升,电池耗电反而比不休眠还快。
避坑指南:我曾经见过一个驱动,在申请唤醒锁的同时分配了大量内存。结果唤醒锁阻止了系统睡眠,内存无法回收,最终 OOM(Out Of Memory)杀掉了关键进程。记住:持有唤醒锁时,要尽量减少内存分配。
内核里有一个机制叫 wakeup_source_trash,专门用来处理这种情况。当系统检测到某个唤醒锁持有者占用了过多内存,会强制触发内存回收,甚至主动释放该唤醒锁。
来看一个内存管理相关的唤醒锁策略表:
| 场景 | 唤醒锁行为 | 内存管理影响 | 我的建议 |
|---|---|---|---|
| 短时操作(如按键响应) | 持有唤醒锁,完成后立即释放 | 几乎无影响 | 正常使用,注意释放时机 |
| 长时操作(如视频播放) | 持有唤醒锁,但允许部分内存回收 | 可能增加 kswapd 负载 | 使用 wake_lock_timeout 限制时长 |
| 后台服务(如音乐播放) | 持有部分唤醒锁,配合 wakelock 超时 | 内存回收受限,需手动触发 | 定期调用 shrink_all_memory |
| 驱动错误(唤醒锁未释放) | 唤醒锁泄漏 | 内存无法回收,最终 OOM | 使用 wakeup_sources_stats 监控 |
另外,内核里还有一个 struct wakeup_source 的 memory_footprint 字段,用来记录该唤醒锁关联的内存使用量。当系统内存紧张时,电源管理子系统会优先释放那些“内存占用高但唤醒锁优先级低”的源。
一个小技巧:在调试唤醒锁导致的内存问题时,可以用 /sys/kernel/debug/wakeup_sources 查看每个唤醒锁的 active_count 和 memory_footprint。我曾经靠这个定位到一个驱动在持有唤醒锁时分配了 200MB 的缓存。
最后,我想说一个观点:唤醒锁不是孤立存在的。它和内核的同步机制、RCU、内存管理紧密耦合。你写驱动时,不能只想着“我要阻止系统睡眠”,还要考虑“我阻止睡眠后,系统还能不能正常工作”。
嗯,今天就聊到这里。这些内容有点深,但理解了它们,你就能真正驾驭 Android 的电源管理。
公众号:蓝海资料掘金营,微信deep3321