Android 7系统休眠唤醒(七)内核层—wakelock与autosleep机制

系列目录 :第一篇:电源管理架构全景图 | 第二篇:开机全链路---BootROM到Launcher | 第三篇:关机/重启全链路---ShutdownThread到kernel_power_off | 第四篇:休眠唤醒与开关机---核心差异深度对比 | 第五篇:休眠全链路---PMS到Kernel Suspend | 第六篇:唤醒全链路---Kernel Resume到屏幕点亮 | 第七篇:内核层---wakelock与autosleep机制 | 第八篇:内核层---Alarm定时唤醒与硬件唤醒源 | 第九篇:Native层---libsuspend与Power HAL | 第十篇:实战调试与问题排查


一、为什么要深入内核 wakelock

第五篇和第六篇追踪休眠/唤醒全链路时,两次提到了 suspend_blocker 和内核 wakelock------它们是连接用户空间与内核休眠机制的桥梁。但你可能会疑惑:

  • 标准 Linux 用 wakeup_count 解决休眠竞态,Android 为什么要另起炉灶搞一套 wakelock?
  • 内核 wakelock 到底是怎么阻止休眠的?只要有一个 wakelock 没释放,系统就真的不能睡?
  • autosleep 线程是怎么做到"自动休眠"的?它和用户空间轮询有什么本质区别?

带着这些问题,本篇深入内核层,逐一拆解。


二、设计背景:为什么需要内核 wakelock

2.1 Linux 标准 suspend 的缺陷

在标准 Linux 中,用户空间通过写入 "mem" 到 /sys/power/state 触发休眠。但这带来了一个竞态问题:

复制代码
用户空间:准备休眠 → write("mem", /sys/power/state)
     ↑                            ↓
     │                    内核进入 suspend...
     │                            ↓
     正在此时,一个唤醒事件发生了!
     │
    事件被忽略,系统继续休眠 → 用户按电源键也没反应 → 死休眠

Linux 社区引入了 wakeup_count 机制解决此问题(见第五篇),但 Android 选择了一条不同的路线------wakelock。

2.2 Android wakelock 的设计哲学

核心思想:只要系统中有任何组件持有 wakelock,内核就不允许休眠。

这意味着休眠的决策权从"用户空间主动请求"变成了"内核被动等待 wakelock 全部释放"。这避免了竞态问题------因为 wakelock 的获取和释放是原子操作。

关键设计:wakeup_count 是"用户空间问内核:现在能睡吗?",wakelock 是"内核自己知道:还有锁没放,不能睡"。后者天然无竞态。


三、wakelock 的数据结构

3.1 struct wakeup_source

源码路径 :include/linux/wakeup_source.h

c 复制代码
struct wakeup_source {
    const char          *name;           // wakelock 名称
    struct list_head    entry;           // 链表节点,链入全局列表
    spinlock_t          lock;            // 保护当前结构的自旋锁
    ktime_t             last_time;       // 最后一次活跃事件的时间
    unsigned long       active_count;    // activate 调用次数
    unsigned long       event_count;     // 事件触发次数
    unsigned long       wakeup_count;    // 唤醒事件次数
    unsigned long long  total_time;      // 累计活跃时间
    unsigned long long  max_time;        // 单次最长活跃时间
    unsigned long       expire_count;    // 超时到期次数
    struct timer_list   timer;           // 超时定时器
    bool                active;          // 当前是否活跃
};

关键设计 :三个时间字段(last_time、total_time、max_time)是功耗分析的宝贵数据------通过 debugfs 可以查看每个唤醒源的累积活跃时间,定位功耗热点。

核心字段:

  • active:是否处于活跃状态。为 true 时阻止系统休眠
  • timer:超时定时器。pm_wakeup_event(ws, timeout) 在 timeout 毫秒后自动释放 wakelock
  • total_time:累计活跃时间,用于功耗分析

3.2 全局唤醒源列表

源码路径 :kernel/power/wakelock.c

c 复制代码
// 全局链表,所有注册的 wakeup_source 都链接在此
static LIST_HEAD(wakeup_sources);

关键设计 :内核通过遍历这个链表,检查是否有 active == true 的 wakeup_source,来决定是否允许休眠。整个判断逻辑只有一个链表遍历,非常轻量。


四、wakelock 的 API

4.1 创建和销毁

源码路径 :kernel/power/wakelock.c

c 复制代码
// 创建唤醒源
struct wakeup_source *wakeup_source_create(const char *name);

// 注册到全局列表
void wakeup_source_add(struct wakeup_source *ws);

// 注销并销毁
void wakeup_source_remove(struct wakeup_source *ws);
void wakeup_source_destroy(struct wakeup_source *ws);

关键设计 :创建和注册分离------先 create 再 add,允许驱动在注册前初始化自定义字段。销毁也是先 remove 再 destroy,确保注销后不会有新的访问。

4.2 激活和释放

这三个函数是 wakelock 的核心操作------激活阻止休眠、释放允许休眠、带超时激活:

c 复制代码
// 激活 wakelock(阻止休眠)
void __pm_stay_awake(struct wakeup_source *ws);

// 释放 wakelock(允许休眠)
void __pm_relax(struct wakeup_source *ws);

// 带超时的激活:timeout 毫秒后自动释放
void __pm_wakeup_event(struct wakeup_source *ws, unsigned long msec);

关键设计 :__pm_wakeup_event 是中断处理中最常用的接口------硬件中断触发后持锁,timeout 毫秒后自动释放,无需手动管理生命周期。

4.3 查询函数

源码路径 :kernel/power/wakelock.c

c 复制代码
// 检查是否有活跃的唤醒源阻止休眠
bool pm_wakeup_pending(void);

// 检查指定唤醒源是否活跃
bool pm_wakeup_active(struct wakeup_source *ws);

关键设计 :pm_wakeup_pending() 是 autosleep 线程和 suspend 流程的核心判断依据------遍历全局链表,只要有一个 active == true 就返回 true。

4.4 wake_lock/wake_unlock 兼容接口

源码路径 :kernel/power/wakelock.c

为了兼容旧的 wakelock 驱动,内核提供了基于字符串的简化接口:

c 复制代码
// 通过字符串名称获取/释放锁
void wake_lock_init(struct wake_lock *lock, int type, const char *name);
void wake_lock(struct wake_lock *lock);
void wake_unlock(struct wake_lock *lock);
void wake_lock_destroy(struct wake_lock *lock);

关键设计 :这是给旧驱动用的"字符串接口"------通过名称查找 wakeup_source,性能不如直接操作指针。新驱动应该使用 wakeup_source_* 系列 API。


五、autosleep 工作线程

5.1 设计动机

wakeup_count 模式下,用户空间每次想休眠都需要:

  1. 确认有 wakelock 未释放 → 不写 state
  2. wakelock 全部释放 → 写 /sys/power/state 触发休眠
  3. 唤醒后 → 循环

这种"用户空间轮询"的方式繁琐且效率低。autosleep 将休眠决策交给内核自己处理。

5.2 autosleep 线程

源码路径 :kernel/power/autosleep.c

c 复制代码
// autosleep 线程的主循环
static int autosleep_thread(void *unused) {
    while (!kthread_should_stop()) {
        // 1. 等待:autosleep 启用 + wakelock 全部释放
        //    或收到停止信号
        wait_event_freezable(autosleep_wq,
            autosleep_state == AUTOSLEEP_ACTIVE &&
            !pm_wakeup_pending());

        // 2. 休眠
        if (autosleep_state == AUTOSLEEP_ACTIVE) {
            pm_suspend(requested_suspend_state);
        }
    }
    return 0;
}

关键设计 :wait_event_freezable() 是可冻结的等待------系统休眠前会冻结此线程,唤醒后恢复执行。线程只在两个条件同时满足时触发休眠:autosleep 已启用 且 没有活跃 wakelock。

关键机制:

  1. wait_event_freezable() --- 这是一个可冻结的等待。在休眠过程中,autosleep 线程本身被冻结,唤醒后继续执行
  2. pm_wakeup_pending() --- 检查是否有活跃 wakelock。如果有,线程继续等待
  3. pm_suspend() --- 当 autosleep 启用且没有活跃 wakelock 时,调用标准休眠函数

5.3 /sys/power/autosleep --- 控制接口

源码路径 :kernel/power/autosleep.c

用户空间通过 sysfs 接口控制 autosleep 线程的启用和禁用:

c 复制代码
static ssize_t autosleep_store(struct kobject *kobj, ...) {
    if (strncmp(buf, "mem", 3) == 0) {
        // 启用 autosleep,目标休眠状态为 mem
        autosleep_state = AUTOSLEEP_ACTIVE;
        requested_suspend_state = PM_SUSPEND_MEM;
        wake_up(&autosleep_wq);
    } else if (strncmp(buf, "off", 3) == 0) {
        // 禁用 autosleep
        autosleep_state = AUTOSLEEP_INACTIVE;
    }
}

关键设计 :写入 "mem" 后,autosleep 线程被唤醒并开始自动循环------此后用户空间无需任何操作,内核会在 wakelock 全部释放时自动休眠,唤醒后自动等待下一次休眠时机。

典型用法:

bash 复制代码
# 启用 autosleep:内核自动管理休眠
echo mem > /sys/power/autosleep

# 禁用 autosleep
echo off > /sys/power/autosleep

六、sysfs 接口全览

6.1 /sys/power/state

源码路径 :kernel/power/main.c

最基础的休眠接口。写入 "mem" 立即触发一次 pm_suspend(PM_SUSPEND_MEM)。

bash 复制代码
echo mem > /sys/power/state

关键设计:这是"一次性触发"------每次写入都执行一次休眠,不像 autosleep 那样持续自动管理。

6.2 /sys/power/wake_lock / wake_unlock

源码路径 :kernel/power/wakelock.c

用户空间可以直接操作内核 wakelock:

bash 复制代码
echo mylock > /sys/power/wake_lock    # 获取
echo mylock > /sys/power/wake_unlock  # 释放

关键设计 :libsuspend 就是通过这两个节点管理 main wakelock 的------用户空间所有 wakelock 汇总为一个 main 锁,写入 wake_lock 持有,写入 wake_unlock 释放。

6.3 /sys/power/wakeup_count

源码路径 :kernel/power/main.c

wakeup_count 机制的核心接口:

c 复制代码
static ssize_t wakeup_count_store(struct kobject *kobj, ...) {
    // 读取传入的 count 值
    // 与当前全局 events_check_enabled 比较
    // 如果不等 → 说明有新唤醒事件发生 → 返回错误
    // 如果相等 → 记录 split_count → 允许此次休眠
}

关键设计:这是一个"乐观锁"机制------先读后写,如果写成功说明期间没有新唤醒事件,允许休眠;写失败说明有新事件,应放弃此次休眠。

用法:

bash 复制代码
# 1. 读取当前值
count=$(cat /sys/power/wakeup_count)

# 2. 写回确认
echo $count > /sys/power/wakeup_count
# 如果写成功 → 说明没有新唤醒事件
# 如果写失败 → 说明有新事件,不应休眠

# 3. 触发休眠
echo mem > /sys/power/state

6.4 /sys/kernel/debug/wakeup_sources

源码路径 :kernel/power/wakelock.c

调试接口,列出所有 wakeup_source 的统计数据:

bash 复制代码
cat /sys/kernel/debug/wakeup_sources

输出格式:

复制代码
name                active_count    event_count     wakeup_count    ...
PowerManagerService.WakeLocks    123    456    78    ...
wlan_wake           45    89    12    ...

关键设计 :这是功耗调试的第一站------total_time 最高的 wakelock 就是阻止系统休眠最久的元凶,active_count 最高的则是唤醒最频繁的。


七、wakelock 统计机制

7.1 时间统计

源码路径 :kernel/power/wakelock.c

c 复制代码
void __pm_stay_awake(struct wakeup_source *ws) {
    // 记录开始时间
    ws->last_time = ktime_get();
    ws->active = true;
    ws->active_count++;
}

void __pm_relax(struct wakeup_source *ws) {
    // 计算本次活跃时长
    ktime_t duration = ktime_sub(ktime_get(), ws->last_time);
    ws->total_time = ktime_add(ws->total_time, duration);

    // 更新最大单次活跃时长
    if (duration > ws->max_time) {
        ws->max_time = duration;
    }

    ws->active = false;
}

关键设计 :total_time 和 max_time 是功耗分析的宝贵数据------通过 /sys/kernel/debug/wakeup_sources 可以查看每个唤醒源的累积活跃时间,定位功耗热点。

7.2 超时机制

源码路径 :kernel/power/wakelock.c

pm_wakeup_event(ws, timeout) 设置一个定时器,timeout 毫秒后自动调用 __pm_relax():

c 复制代码
void __pm_wakeup_event(struct wakeup_source *ws, unsigned int msec) {
    __pm_stay_awake(ws);
    mod_timer(&ws->timer, jiffies + msecs_to_jiffies(msec));  ← 到期后自动调用 __pm_relax
}

关键设计:超时机制常见于硬件中断场景------按键消抖、WiFi 数据收发等短暂处理,处理完成后自动释放 wakelock,无需手动管理。


八、wakelock 的内核使用场景

8.1 驱动中使用示例

c 复制代码
#include <linux/wakelock.h>

// 方式一:直接使用 wake_lock
struct wake_lock my_lock;
wake_lock_init(&my_lock, WAKE_LOCK_SUSPEND, "my_driver_lock");

// 处理关键事件时持有锁
wake_lock(&my_lock);
// ... 处理逻辑 ...
wake_unlock(&my_lock);

wake_lock_destroy(&my_lock);

关键设计 :wake_lock_init 和 wake_lock_destroy 必须配对使用------init 注册到全局列表,destroy 注销并释放内存。遗漏 destroy 会导致内存泄漏和悬挂链表节点。

8.2 常见的内核 wakelock 来源

下表列出了系统中常见的 wakelock 来源:

wakelock 名称 持有者 用途
main PMS(通过 libsuspend) 汇总所有用户空间 wakelock
PowerManagerService.WakeLocks PMS partial wake_lock 汇总
PowerManagerService.Display PMS 屏幕供电期间持有
wlan_wake WiFi 驱动 数据收发期间持有
alarm Alarm 驱动 有 pending alarm 时持有
PowerManagerService.Broadcasts PMS 广播分发期间持有
event0-xxx input 子系统 按键事件处理期间持有

九、autosleep vs wakeup_count 对比

下表对比两种休眠决策机制的核心差异:

维度 wakeup_count autosleep
休眠决策 用户空间决定何时休眠 内核自动决定
实现复杂度 简单,仅需 sysfs 需要内核线程
竞态防护 wakeup_count 写入检查 wakelock 天然无竞态
CPU 唤醒次数 用户空间可能反复尝试 仅在有 wakelock 释放时尝试
适用场景 简单系统 / 调试 生产环境(Android 默认)

关键设计:Android 优先使用 autosleep(当内核支持时),回退到 wakeup_count 模式------autosleep 更高效,但 wakeup_count 是标准 Linux 方案,兼容性更好。


十、小结

内核 wakelock 是连接用户空间与内核休眠机制的核心桥梁:

  1. 数据结构 :wakeup_source 记录名称、活跃状态、时间统计
  2. 设计哲学:只要有活跃 wakelock,内核绝不休眠------避免了竞态问题
  3. autosleep:内核线程自动管理休眠,用户空间仅需启用/禁用
  4. sysfs 接口 :wake_lock/wake_unlock/autosleep/wakeup_sources
  5. 统计机制:每个 wakelock 的累计活跃时间是功耗分析的宝贵数据

下一篇将分析另一个关键的唤醒机制:Alarm 定时唤醒,看 RTC 如何让系统在指定时间"准时醒来"。

相关推荐
唐诺2 分钟前
5、Android JNI 动态注册中的“签名“详解
android·jni
zhaoshuzhaoshu3 小时前
Android 系统启动与调试中的日志抓取方式全解析
android
雨声不在4 小时前
android 动画掉帧
android
在多学一点5 小时前
MYSQL第二次作业
android·数据库·mysql
骑着蜗牛撵大象3275 小时前
MySQL 8.0 迁移后 SQL 变慢:执行计划里 3 类被忽略的成本差异
android·数据库·sql·mysql·优化器·执行计划
我叫张土豆6 小时前
Android 14+ 模拟器抓包实录:把 Burp CA 装成系统 CA,让 targetSdk≥34 的 App 乖乖解密
android
booboosui10 小时前
Android 悬浮窗藏不住自己:一个标志位与 140 毫秒
android
传奇开心果10 小时前
【Flutter踩坑排雷】第5课:扩展方法冲突坑|同名扩展引发编译歧义,故障规避手段
android·flutter
.痞老板.12 小时前
mysql初阶--数据类型
android·数据库·mysql
StarsGreen12 小时前
Android 启动流程详解
android·linux·单片机·嵌入式硬件