高级话题:唤醒锁与内核同步机制、唤醒锁与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_rculist_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_sourcememory_footprint 字段,用来记录该唤醒锁关联的内存使用量。当系统内存紧张时,电源管理子系统会优先释放那些“内存占用高但唤醒锁优先级低”的源。

一个小技巧:在调试唤醒锁导致的内存问题时,可以用 /sys/kernel/debug/wakeup_sources 查看每个唤醒锁的 active_countmemory_footprint。我曾经靠这个定位到一个驱动在持有唤醒锁时分配了 200MB 的缓存。

最后,我想说一个观点:唤醒锁不是孤立存在的。它和内核的同步机制、RCU、内存管理紧密耦合。你写驱动时,不能只想着“我要阻止系统睡眠”,还要考虑“我阻止睡眠后,系统还能不能正常工作”。

嗯,今天就聊到这里。这些内容有点深,但理解了它们,你就能真正驾驭 Android 的电源管理。


公众号:蓝海资料掘金营,微信deep3321