前言:
电源管理是嵌入式设备中非常重要的一环,特别是android手机上电源管理直接关系到手机的续航时间,每人都希望自己的手机能够充电一次运行终生,不过能量守恒定律决定了它只能运行有限的时间,所以只能延长续航时间。手机电源管理中有一个重要的过程,当按下电源键的时候,整个手机都进入一个挂起状态,专业点叫supend to ram简称s2ram,所有的进程都不在运行,所有的设备都不再响应,ram进入了自刷新状态,cpu的时钟和供电也停掉了,整个手机都进入了假死状态,这个时候CPU自己是无法觉醒的,需要额外的PMIC来响应事件(例如按下电源键或者定时器到期)。期间需要挂起所有的进程,也即是所有的进程都不在运行,这个挂起所有进程的过程叫做freezer。为什么需要这样?他是如何做的?还有什么问题吗?
1.为什么需要进程冻结
s2ram最终的目的是挂起物理设备,切断他们的时钟和供电,而这些物理设备是为了用户任务而服务的,suspend的过程是:冻结进程、挂起普通设备、挂起系统级设备(定时器、regulator、中断控制器、pci控制器等),CPU通知PMIC将自己挂起。挂起设备就是依次挂起设备,线性串行来做这件事,假如有两个设备A和B,A已经挂起,正在挂起设备B,有任务又使用A,所以可能会挂起失败,还可能正在做关键性任务但是系统却强制掉电了。系统的电源管理逻辑完全是通过软件层面管理的,硬件无法知道自己是否可以挂起(runtime pm driver可以在自己无事可做的时候挂起设备)的。所以我们需要将所有的进程都冻结起来防止这种情况
2.如何冻结和解冻
每个task_struct有三个标记用来做冻结管理:PF_NOFREEZE, PF_FROZEN,PF_FREEZER_SKIP ,如果task清除了PF_NOFREEZE标记(所有的task可以清除这个标记,有些内核线程也可以清除这个标记)那它就被当做freezable的并且在系统进入suspend状态的时候被冻结。
系统进入suspend状态的第一步就是调用freezer_processes()(kernel/power/process.c),系统全局变量system_freezing_cnt被设置来表明系统是否正在进行冻结过程。接下来执行try_to_freeze_tasks()来给所有的用户进程发送一个假的信号并且唤醒所有的内核线程。所有的freezable进程在处理信号的时候都会看一下是否应该进入冻结try_to_freeze(),最后通过__refrigerator()(kernel/freezer.c),他会设置task的PF_FROZEN标记并且改变他的状态为TASK_UNINTERRUPTIBLE,让它循环一直到PF_FROZEN标记被清除.现在task就处于frozen状态,这一系列的过程称为freezer(kenrle/power/process.c, kernel/freezer.c, include/linux/freezer.h),用户进程都是在内核线程之前被冻结。
__refrigerator()一定不能直接调用,task必须通过try_to_freeze()函数来检查task是否应该可以冻结然后通过__refrigerator()来真正进入冻结状态。
用户空间的进程会在信号处理的代码中自动调用try_to_freeze(),但是内核线程必须显式调用try_to_freeze或者使用wait_event_freezable()/wait_event_freezable_timeout()宏,它会检查是否可以进入可中断睡眠并且检查任务是否可以进入冻结状态并且尝试调用try_to_freeze(),一个可以冻结的内核线程可能写成下面这种形式:
set_freezable();
do {
hub_events();
wait_event_freezable(khubd_wait,
!list_empty(&hub_event_list) ||
kthread_should_stop());
} while (!kthread_should_stop() || !list_empty(&hub_event_list));
如果一个可冻结内核线程try_to_freeze()返回失败,那么整个的冻结过程就整体失败了,suspend过程就需要取消,对所有进程进行解冻。
当系统整体退出suspend过程并且设备都被重新初始化,thaw_processess()就会被调用来清除所有task的PF_FROZEN标记,然后就会task就会离开__refrigerator()然后继续运行。
3.冻结带来的问题
冻结之后的suspend会延长手机的续航,不过使用它也会带来一些问题,大部分有了解决方案,但是仍需要我们注意并且加以特殊处理。
1.如果内核线程之间存在依赖关系,冻结过程可能会引起一些问题。
内核线程A正处于TASK_UNINTERRUPTIBLE等待条件变量,但是这个东西需要内核线程B,它是一个可冻结的内核线程,但是此时它已经被冻结了,所以A会一直被堵塞直到B被解冻,这个可能不是我们期望的。所以内核线程默认是不可冻结的。
2.系统的平均负载可能会被弄乱了,冻结期间所有的进程都被冻结了,处于TASK_UNINTERRUPTIBLE状态,这段的平均负载就是0,但是可能是有task正在运行的但是被强制冻结了,这样计算的话可能就不准确了,所以需要将冻结的这段时间不计入平均负载。
3.如果使用FUSE文件下系统或者是其他在用户空间中做设备驱动工作的,类似于dpdk这种,因为冻结过程会先冻结用户进程再冻结内核线程,它没有区分上面这类进程。通常挂起这种设备的时候可能是这样的:挂起设备的时候发送请求给用户进程,但是用户进程已经被冻结了不再读取请求了,完成不了冻结过程。不过我们可以通过work around的方法来避免这个问题,在suspend过程中发出通知,内核模块中接收到通知再通知用户进程,主动让它开始挂起操作,不过这样一来不能做到用户进程对suspend过程透明,并且它还得处理这种场景操作。
在唤醒的会后如果驱动.resume过程中调用request_firmware(),通常会有用户空间进程来响应这个请求,但是现在还处于冻结状态,所以它最终只会超时失败。
如果固件驱动位于文件系统上,但是这个文件系统需要通过另外的驱动来访问获取到,但是这个驱动还没有resume,这种情况也会导致问题。所以驱动如果需要其他的firmware,那么在suspend之前需要把他们load到内存中,但是如果firmware比较大,一直占据内存就不太划算了,所以可以在suspend通知时将自己所需的firmware加载进来。