Linux电源管理(10)_autosleep

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值