Suspend流程介绍
随着智能手机的普及,大家对手机续航也越来越重视,而手机处于休眠状态又是手机最省电的一种模式,因此本文简单介绍下android下suspend的流程。
一、用户空间发起suspend流程
我们知道内核提供了文件节点 /sys/power/state 给用户空间,用来设置当前系统要处于的状态(s2idle/s2ram/std),本文主要介绍s2ram,我们先查看与这个文件相关的进程:
发现是android.system.suspend@1.0-service这个进程在打开这个文件,查看这个进程相关的代码,可以看到autosuspend的流程:
图 1 autosuspend流程
-
Sleep 100ms(初始值)。
-
读取/sys/power/wakeup_count的值,读取不到返回第一步;读到的值这里暂以count_r来代替。
-
把count_r写入/sys/power/wakeup_count文件节点,写入成功,则写“mem”到/sys/power/state节点;写入失败,增加sleep时间(maxsleeptime:2min),转到1。
从上面的流程能看到wakeup_count很重要,读取不到wakeup_count及写入wakeup_count失败,都会导致本次的suspend流程失败。
1、wakeup_count的由来
想想如果没有wakeup_count,这时系统马上就要进入suspend,但一个event已经通知到了用户空间,还没来得及执行后续的动作,那这个event只能等待下个wakeup event唤醒系统才有机会执行了。因此,wakeup_count存在的一个根本原因是保证wakeup event执行的原子性(这里与指令操作的原子性有区别)。
wakeup_count是怎样影响suspend流程的呢?
先看下kernel里面wakeup_count的存在形式:
回到autosuspend的流程,当在读取wakeup_count的时候,如果有正处于active状态的wakeup event时, autosuspend进程会在此阻塞(能走到suspend流程说明系统也确实没其它什么事了),直到系统没有了处于active状态的wakeup event或者该进程被中断、信号唤醒才会返回,当返回时如果还有处于active状态的wakeup event,这也标识着读失败,继续循环读取;读取成功后,会把前16bit的值传给autosuspend进程。
读到wakeup_count后,会尝试再往wakeup_count里面写入读到的值,如果这时检测到有处于active状态的wakeup event或者本次读取到的Registered wakeup events与写入的值不相等(表示这期间有wakeup 的events触发过,是有风险的),这时会写入失败,suspend流程被中止。
2、pm_wakeup_pending的作用
检测suspend流程是否能触发,并继续走下去,能看到是内核与用户空间通过wakeup_count这个媒介一起配合完成的(参考pm_save_wakeup_count的实现),而在写入wakeup_count完成后,内核继续在suspend 执行路径里面做这种异常的检测,基本是在suspend路径中比较耗时的动作前后插桩检测,见pm_wakeup_pending的实现,如果我们想要知道在suspend过程中是因为哪些wakeup events的出现导致了流程被中止,在这里可以做下标识。
二、Suspend内核流程跟踪
风险暂时排除后,开始尝试进入suspend流程,通过写入“mem”到state节点(本文只讨论mem的这种情况)。由于suspend内核流程比较复杂,这里只描述比较重要的环节。
如果系统走的是psci的统一接口,suspend_ops只有valid和enter被赋值,其它的成员先暂不介绍。
图 2 suspend_ops示例
1、首先是同步文件系统,见SYSCALL_DEFINE0(sync)的实现,这一步保证suspend前所有写入的数据被同步到块设备,不会丢失,主要是唤醒块设备的pdflush相关进程,然后写脏inode,写块设备,提交缓存数据到journal(ext4),保证在sync这一刻之前的数据不会丢失。
2、准备并冻结进程(suspend_prepare),由以下几步组成:
1) pm_prepare_console给suspend分配一个虚拟终端来输出信息;
2) 发送一个系统要进入suspend的notify。
3) _usermodehelper_disable禁止创建新的usermode helper,内核不再接收这种helper进程的创建。
4) 冻结用户进程及内核进程,及内核相工作队列,唤醒所有的应用进程(fake_signal_wake_up)及内核进程,应用进程的freeze通过信号处理相关函数来调用到try_to_freeze进行冻结,而可冻结的内核进程需要有能力处理冻结流程并显式调用try_to_freeze进行冻结,冻结的最终实现在__refrigerator,每一个被冻结的进程都会调到这来,就像放入冰箱一样,主要也是设置state为TASK_UNINTERRUPTIBLE,然后判断如果要freeze,就调用schedule()把自己切出运行队列。
3、suspend_test这个功能,我们可以用来调试系统在未进入真正的suspend前的某些固定阶段的状态,使能后,借助rtc来做timer发起suspend test主流程,这些固定阶段嵌在suspend流程里,包括以下几个阶段:TEST_FREEZER->TEST_DEVICES-> TEST_PLATFORM->TEST_CPUS->TEST_CORE,由浅入深,当suspend走到你要调试的阶段后,会在这个阶段上睡眠一段时间,供我们来调试系统。
4、休眠设备并进入suspend(suspend_devices_and_enter),由以下步骤组成:
1) 调用platform_suspend_begin,这个区分平台,看下平台是否有在suspend开始时执行操作的需求。略过。
2) suspend console,ptintk将不能打印。
3) suspend所有非系统设备,即调用设备注册的suspend回调(dpm_suspend_start)。device节点的流转过程就是device在整个休眠唤醒过程中的流程,对文字敏感的朋友可以直接看下图。对单个设备来讲,先从dpm_list链表中取出device并执行该 device的prepare回调,成功后会把该device节点移动到dpm_prepared_list链表,后面在做 dpm_suspend时,会从这个链表里面取device,执行suspend的回调,成功后会把该device移动到dpm_suspended_list链表,dpm_suspend_late中会从dpm_suspended_list链表中取出device执行suspend_late回调,然后把device节点移动到dpm_late_early_list链表,dpm_noirq_suspend_devices里会从dpm_late_early_list链表中取出device执行suspend_noirq回调,并把device节点移动到dpm_noirq_list链表,在后续resume过程中dpm_resume_noirq中又会从dpm_noirq_list链表中取出device执行resume_noirq回调,并把device节点移动到dpm_late_early_list链表,dpm_resume_early时会从dpm_late_early_list链表中取出device并执行resume_early回调,然后把device节点链入dpm_suspended_list链表,在dpm_resume时会从dpm_suspended_list链表中拿到device执行resume回调,并把device节点再移动到dpm_prepared_list链表,最后执行dpm_complete时,会从dpm_prepared_list链表中拿到device执行complete回调,并把device重新链入dpm_list链表。完成device节点的流转。设备的休眠和唤醒是系统休眠唤醒的重要组成部分,流转流程如下图:
图 3 设备节点在休眠唤醒流程中流转过程
4) 把系统进入到要求的sleep状态,然后停止运行(suspend_enter)。
三、suspend_enter
单独介绍下suspend_enter,略过一些无感的流程:
1、某些平台需要在这个阶段做一些准备动作,通过调用suspend_ops->prepare来实现平台相关的操作。
2、执行上面已经介绍过的设备的suspend_late回调。
3、dpm_suspend_noirq做的工作是先禁止cpu进入idle,毕竟要进suspend了就不需要再进行idle的选择了,然后开始禁止设备的中断,禁止中断前,会先遍历所有的wakeup source,把对应的中断状态置位IRQD_WAKEUP_STATE flag,这样再禁止中断的过程中轮询到这个中断时,会再加上IRQD_WAKEUP_ARMED flag,没有包含IRQD_WAKEUP_STATE状态的中断会被禁掉。IRQD_WAKEUP_ARMED主要是用来对进入到suspend的wakeup source对应的irq做标识,在irq_may_run里面做快速响应,想想如果在irq_may_run里面返回false代表了什么呢?
4、disable_nonboot_cpus 负责关闭noboot的那些cpu,随后用arch_suspend_disable_irqs关闭bootcpu的中断,即cpu不再响应中断。
5、进入suspend前的最后一步,syscore_suspend主要是执行那些系统及平台在suspend前必需的流程,比如保存一些clk的状态,在resume的时候能恢复clk到suspend前的状态、系统为了对suspend时间的统计及系统时间的更新、也可以用作调试手段,来收集suspend前后的相关信息。
6、最后一步,进入suspend,每种类型的平台实现不同。一般是把主时钟停掉,会启用一个频率更低的时钟来减少功耗,cpu停止运行。
参考文献:
kernel-4.14/Documentation/power/freezing-of-tasks.txt
http://www.wowotech.net/linux_kenrel/suspend_and_resume.html