1. 前言(android13没有使能CONFIG_PM_AUTOSLEEP)
Autosleep也是从Android wakelocks补丁集中演化而来的(Linux电源管理(9)_wakelocks),用于取代Android wakelocks中的自动休眠功能。它基于wakeup source实现,从代码逻辑上讲,autosleep是一个简单的功能,但背后却埋藏着一个值得深思的话题:
计算机的休眠(通常是STR、Standby、Hibernate等suspend操作),应当在什么时候、由谁触发?
蜗蜗在“Linux电源管理(2)_Generic PM之基本概念和软件架构”中有提过,在传统的操作场景下,如PC、笔记本电脑,这个问题很好回答:由用户、在其不想或不再使用时。
但在移动互联时代,用户随时随地都可能使用设备,上面的回答就不再成立,怎么办?这时,Android提出了“Opportunistic suspend(这个词汇太传神了,很难用简洁的中文去翻译,就不翻译了)”的理论,通俗的讲,就是“逮到机会就睡”。而autosleep功能,无论是基于Android wakelocks的autosleep,还是基于wakeup source的autosleep,都是为了实现“Opportunistic suspend”。
相比较“对多样的系统组件单独控制”的电源管理方案(如Linux kernel的Dynamic PM),“Opportunistic suspend”是非常简单的,只要检测到系统没有事情在做(逮到机会),就suspend整个系统。这对系统的开发人员(特别是driver开发者)来说,很容易实现,几乎不需要特别处理。
但困难的是,“系统没有事情在做”的判断依据是什么?能判断准确吗?会不会浪费过多的资源在"susend->resume-supsend…”的无聊动作上?如果只有一个设备在做事情,其它设备岂不是也得陪着耗电?等等…
所以,实现“Opportunistic suspend”机制的autosleep功能,是充满争议的。说实话,也是不优雅的。但它可以解燃眉之急,因而虽然受非议,却在Android设备中广泛使用。
其实Android中很多机制都是这样的(如wakelocks,如binder,等等),可以这样比方:Android是设计中的现实主义,Linux kernel是设计中的理想主义,当理想和现实冲突时,怎么调和?不只是Linux kernel,其它的诸如设计、工作和生活,都会遇到类似的冲突,怎么对待?没有答案,但有一个原则:不要偏执,不要试图追求非黑即白的真理!
我们应该庆幸有Android这样的开源软件,让我们可以对比,可以思考。偏题有点远,言归正传吧,去看看autosleep的实现。
2. 功能总结和实现原理
经过前言的瞎聊,Autosleep的功能很已经很直白了,“系统没有事情在做”的时候,就将系统切换到低功耗状态。
根据使用场景,低功耗状态可以是Freeze、Standby、Suspend to RAM(STR)和Suspend to disk(STD)中的任意一种。而怎么判断系统没有事情在做呢?依赖wakeup events framework。只要系统没有正在处理和新增的wakeup events,就尝试suspend,如果suspend的过程中有events产生,再resume就是了。
由于suspend/resume的操作如此频繁,解决同步问题就越发重要,这也要依赖wakeup events framework及其wakeup count功能。
3. 在电源管理中的位置
autosleep的实现位于kernel/power/autosleep.c中,基于wakeup count和suspend & hibernate功能,并通过PM core的main模块向用户空间提供sysfs文件(/sys/power/autosleep)
注1:我们在“Linux电源管理(8)_Wakeup count功能”中,讨论过wakeup count功能,本文的autosleep,就是使用wakeup count的一个实例。
4. 代码分析
4.1 /sys/power/autosleep
#ifdef CONFIG_PM_AUTOSLEEP
static ssize_t autosleep_show(struct kobject *kobj,
struct kobj_attribute *attr,
char *buf)
{
// 获取当前的自动休眠状态
suspend_state_t state = pm_autosleep_state();
// 如果状态为PM_SUSPEND_ON,表示自动休眠已关闭
if (state == PM_SUSPEND_ON)
return sprintf(buf, "off\n");
#ifdef CONFIG_SUSPEND
if (state < PM_SUSPEND_MAX)
return sprintf(buf, "%s\n", pm_states[state] ?
pm_states[state] : "error");
#endif
#ifdef CONFIG_HIBERNATION
// 如果支持休眠,但不支持挂起,返回"disk"
return sprintf(buf, "disk\n");
#else
// 如果既不支持休眠也不支持挂起,返回"error"
return sprintf(buf, "error");
#endif
}
static ssize_t autosleep_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf, size_t n)
{
// 解析用户输入的状态
suspend_state_t state = decode_state(buf, n);
int error;
// 如果状态为PM_SUSPEND_ON,并且输入不为"off",返回错误
if (state == PM_SUSPEND_ON
&& strcmp(buf, "off") && strcmp(buf, "off\n"))
return -EINVAL;
// 如果状态为PM_SUSPEND_MEM,设置为当前内存休眠状态
if (state == PM_SUSPEND_MEM)
state = mem_sleep_current;
// 设置自动休眠状态
error = pm_autosleep_set_state(state);
return error ? error : n;
}
// 定义并初始化autosleep属性
power_attr(autosleep);
#endif /* CONFIG_PM_AUTOSLEEP */
这段代码是Linux内核中用于处理自动休眠状态的部分。它包含了两个函数:autosleep_show 和 autosleep_store。这两个函数都用于读取和设置自动休眠状态。
autosleep_show 函数用于读取当前的自动休眠状态并将其显示为字符串。根据状态的不同,它可能返回 “off” 表示自动休眠已关闭,或者返回支持的休眠/挂起状态,或者返回 “disk” 表示只支持休眠而不支持挂起,或者返回 “error” 表示既不支持休眠也不支持挂起。
autosleep_store 函数用于根据用户输入设置自动休眠状态。它会解析用户输入的状态,并根据状态的不同进行相应的设置。如果状态为PM_SUSPEND_ON,只有输入为 “off” 时才能关闭自动休眠。如果状态为PM_SUSPEND_MEM,将其设置为当前内存休眠状态。
最后,使用 power_attr(autosleep) 定义并初始化了名为 autosleep 的属性,用于在 sysfs 中显示和修改自动休眠状态。整个功能在编译时会受到是否启用 CONFIG_PM_AUTOSLEEP、CONFIG_SUSPEND 和 CONFIG_HIBERNATION 配置选项的影响。
4.2 pm_autosleep_init
开始之前,先介绍一下autosleep的初始化函数,该函数在kernel PM初始化时(.\kernel\power\main.c:pm_init)被调用,负责初始化autosleep所需的2个全局参数:
1)一个名称为“autosleep”的wakeup source(autosleep_ws),在autosleep执行关键操作时,阻止系统休眠(我们可以从中理解wakeup source的应用场景和使用方法)。
2)一个名称为“autosleep”的有序workqueue,用于触发实际的休眠动作(休眠应由经常或者线程触发)。这里我们要提出2个问题:什么是有序workqueue?为什么要使用有序workqueue?后面分析代码时会有答案。
如下:
/**
* pm_autosleep_init - 初始化自动休眠模块
*
* 这个函数用于在系统初始化时初始化自动休眠模块。它会创建一个唤醒源
* (wakeup source) 用于跟踪自动休眠事件,并分配一个有序工作队列
* (ordered workqueue) 用于处理自动休眠相关的任务。
*
* 返回:0 表示成功初始化,负数表示出现错误
*/
int __init pm_autosleep_init(void)
{
// 注册一个唤醒源 "autosleep",用于跟踪自动休眠事件
autosleep_ws = wakeup_source_register(NULL, "autosleep");
if (!autosleep_ws)
return -ENOMEM;
// 分配一个有序工作队列 "autosleep",用于处理自动休眠相关的任务
autosleep_wq = alloc_ordered_workqueue("autosleep", 0);
if (autosleep_wq)
return 0;
// 如果分配工作队列失败,取消唤醒源的注册并返回错误
wakeup_source_unregister(autosleep_ws);
return -ENOMEM;
}
4.3 pm_autosleep_set_state
pm_autosleep_set_state负责设置autosleep的状态,autosleep状态和“Linux电源管理(5)_Hibernate和Sleep功能介绍”所描述的电源管理状态一致,共有freeze、standby、STR、STD和off五种(具体依赖于系统实际支持的电源管理状态)。具体如下:
/**
* pm_autosleep_set_state - 设置自动休眠状态
* @state: 要设置的休眠状态
*
* 这个函数用于设置自动休眠的状态。它会根据给定的状态来控制是否启用
* 自动休眠功能,以及触发相应的操作。根据不同的状态,会执行不同的操作,
* 包括启用/禁用自动休眠、排队休眠任务等。
*
* 返回:0 表示成功设置状态,负数表示出现错误
*/
int pm_autosleep_set_state(suspend_state_t state)
{
#ifndef CONFIG_HIBERNATION
// 如果不支持休眠状态大于等于 PM_SUSPEND_MAX,返回错误
if (state >= PM_SUSPEND_MAX)
return -EINVAL;
#endif
// 保持唤醒状态以防止系统进入休眠
__pm_stay_awake(autosleep_ws);
// 锁定自动休眠互斥锁,确保设置状态的原子性
mutex_lock(&autosleep_lock);
// 更新自动休眠状态
autosleep_state = state;
// 解除唤醒状态
__pm_relax(autosleep_ws);
// 根据状态执行不同的操作
if (state > PM_SUSPEND_ON) {
// 启用自动休眠唤醒事件
pm_wakep_autosleep_enabled(true);
// 排队休眠任务
queue_up_suspend_work();
} else {
// 禁用自动休眠唤醒事件
pm_wakep_autosleep_enabled(false);
}
// 解锁自动休眠互斥锁
mutex_unlock(&autosleep_lock);
return 0;
}
pm_wakep_autosleep_enabled主要用于更新wakeup source中和autosleep有关的信息,代码和执行逻辑如下:
#ifdef CONFIG_PM_AUTOSLEEP
/**
* pm_wakep_autosleep_enabled - 修改所有唤醒源的 autosleep_enabled 标志
* @set: 是否设置 autosleep_enabled 标志
*
* 这个函数用于修改所有唤醒源的 autosleep_enabled 标志,以控制是否允许自动休眠。
* 根据传入的参数,可以设置或者清除唤醒源的 autosleep_enabled 标志。
*/
void pm_wakep_autosleep_enabled(bool set)
{
struct wakeup_source *ws;
ktime_t now = ktime_get();
int srcuidx;
// 使用 srcu 读锁来遍历唤醒源列表
srcuidx = srcu_read_lock(&wakeup_srcu);
list_for_each_entry_rcu_locked(ws, &wakeup_sources, entry) {
spin_lock_irq(&ws->lock);
// 如果 autosleep_enabled 标志需要修改
if (ws->autosleep_enabled != set) {
ws->autosleep_enabled = set;
// 如果唤醒源处于活跃状态
if (ws->active) {
// 根据设置的状态更新 prevent_sleep 时间
if (set)
ws->start_prevent_time = now;
else
update_prevent_sleep_time(ws, now);
}
}
spin_unlock_irq(&ws->lock);
}
srcu_read_unlock(&wakeup_srcu, srcuidx);
}
#endif /* CONFIG_PM_AUTOSLEEP */
这个函数用于修改所有唤醒源的 autosleep_enabled 标志,以控制是否允许自动休眠。根据传入的参数 set,可以设置或者清除唤醒源的 autosleep_enabled 标志。
queue_up_suspend_work比较简单,就是把suspend_work挂到workqueue,等待被执行。而suspend_work的处理函数为try_to_suspend,如下:
/**
* 声明一个待执行的工作队列(suspend_work)并指定工作函数为 try_to_suspend。
* 这个工作队列用于在自动休眠状态下排队挂起操作。
*/
static DECLARE_WORK(suspend_work, try_to_suspend);
/**
* 将挂起操作工作(suspend_work)排队到工作队列中。
* 这个函数会根据当前的自动休眠状态决定是否将挂起操作工作排队。
*/
void queue_up_suspend_work(void)
{
// 如果自动休眠状态大于 PM_SUSPEND_ON,表示系统可以进行挂起操作
if (autosleep_state > PM_SUSPEND_ON)
// 将挂起操作工作排队到指定的工作队列(autosleep_wq)中
queue_work(autosleep_wq, &suspend_work);
}
4.4 try_to_suspend
try_to_suspend是suspend的实际触发者,代码如下:
/**
* 尝试进入挂起状态的函数。
* @work: 指向工作项的指针
*/
static void try_to_suspend(struct work_struct *work)
{
unsigned int initial_count, final_count;
// 获取当前系统的唤醒计数
if (!pm_get_wakeup_count(&initial_count, true))
goto out;
// 获取 autosleep_lock 互斥锁,用于确保挂起操作的同步
mutex_lock(&autosleep_lock);
// 如果无法保存唤醒计数或者系统不处于运行状态,则解锁并返回
if (!pm_save_wakeup_count(initial_count) ||
system_state != SYSTEM_RUNNING) {
mutex_unlock(&autosleep_lock);
goto out;
}
// 如果自动休眠状态是 PM_SUSPEND_ON,则直接解锁并返回
if (autosleep_state == PM_SUSPEND_ON) {
mutex_unlock(&autosleep_lock);
return;
}
// 如果自动休眠状态大于等于 PM_SUSPEND_MAX,则执行休眠操作
if (autosleep_state >= PM_SUSPEND_MAX)
hibernate();
else
pm_suspend(autosleep_state);
// 解锁 autosleep_lock 互斥锁
mutex_unlock(&autosleep_lock);
// 获取最终的唤醒计数
if (!pm_get_wakeup_count(&final_count, false))
goto out;
/*
* 如果唤醒计数没有发生变化,说明发生了未知原因的唤醒,
* 则等待一段时间以防止系统陷入紧密的挂起与唤醒循环中。
*/
if (final_count == initial_count)
schedule_timeout_uninterruptible(HZ / 2);
out:
// 将挂起操作工作重新排队以便后续执行
queue_up_suspend_work();
}
该接口是wakeup count的一个例子,根据我们在“Linux电源管理(8)_Wakeup count功能”的分析,就是read wakeup count,write wakeup count,suspend,具体为:
a)调用pm_get_wakeup_count(block为true),获取wakeup count,保存在initial_count中。如果有wakeup events正在处理,阻塞等待。
b)将读取的count,写入。如果成功,且当前系统状态为running,根据autosleep状态,调用hibernate或者pm_suspend,suspend系统。
d)如果写count失败,说明读写的过程有events产生,退出,进行下一次尝试。
e)如果suspend的过程中,或者是suspend之后,产生了events,醒来,再读一次wakeup count(此时不再阻塞),保存在final_count中。
f)如果final_count和initial_count相同,发生怪事了,没有产生events,竟然醒了。可能有异常,不能再立即启动autosleep(恐怕陷入sleep->wakeup->sleep->wakeup的快速loop中),等待0.5s,再尝试autosleep。
4.5 pm_autosleep_state
该接口比较简单,获取autosleep_state的值返回即可。
参考文章: http://www.wowotech.net/linux_kenrel/autosleep.html