Google从Android11系统开始支持应用冻结功能,可以将后台长时间未运行的任务暂缓执行,通过将对应的进程迁移到对应的cgroup分组来冻结对应的后台缓存应用,这样可以减少如CPU、内存等资源占用,减少业务在后台的不当行为,尽可能减少功耗。本文将对Android的进程冻结的实现原理、冻结策略进行详细的介绍与阐述,争取把相关的策略与机制都讲述清楚,主要分为以下几个部分 :
Android进程冻结的大致框架:主要介绍进程冻结的总体框架与思路Android进程冻结的实现原理:介绍Android如何实现进程冻结Android进程冻结的冻结策略:进程冻结的具体策略
Android进程冻结整体框架
Android中每个应用都有一个oom_adj(out of memory ajustment)值,用来标记应用的优先级状态;在应用创建、前后台切换、广播接收、服务绑定以及进程崩溃等事件(具体可以参考如下调整的原因)时,会触发oom_adj的变化,oom_adj的变化会导致Android系统执行某些特定的策略,比如调整进程所在的cgroup分组,回收应用或者系统内存,或者执行进程冻结,以减少CPU、内存的占用。
1 |
|
Android系统进程的冻结主要通过内核中cgroup冻结(freezer)子系统来实现的,对应是下述框图中的右侧区域;如果冻结的进程提供了binder接口,首先需要通过binder接口设置当前服务进程处于冻结状态,这样客户端调用相关的接口时,主动返回错误,而不至于阻塞客户端进程。
ActivityManagerService(AMS)系统的核心服务,主要负责应用的创建与状态管理,AMS会通过OomAjduster的接口来调整进程的优先级状态OomAjduster主要用来计算、调整进程的状态与优先级,为内存回收、进程冻结提供参考依据CachedAppOptimizer提供内存回收与进程冻结的能力,对长时间处于后台的应用进行相应的优化处理Process用于管理应用进程,提供如进程创建,进程优先级调整,进程分组等接口
进程冻结实际会分为两个具体的步骤:
- 首先通过
freezeBinder发送命令给binder驱动尝试冻结服务端的进程,binder驱动会冻结对应pid的服务,后续请求都会直接返回一个错误 binder服务冻结后,需要通过cgroup冻结子系统执行冻结;进程冻结完成后,进程状态变为S,执行的路径会阻塞在do_freezer_trap
Android进程冻结实现原理
进程冻结分组挂载
Android冻结的核心原理是基于cgroup中的冻结子系统来完成任务的冻结与解冻;cgroup是最开始是Google工程师引入,是内核用于控制资源比如CPU,内存,IO等的一种非常有效的手段。在Android初始化过程中,会通过解析系统中的cgroups.json文件,将常用的分组挂载到系统中:
- 进程冻结分组
freezer会挂载到/sys/fs/cgroup节点 cpu关联的分组有两个,一个是/dev/cpuctl,主要用于控制CPU的调度,一个是/dev/cpuset,主要用于控制CPU的亲和性、大小核绑定memory对应的分组是/dev/memcg,主要用于控制内存的分配io对应的分组是/dev/blkio,主要用于控制IO的调度
1 |
|
Android系统中,cgroups.json文件位于/system/etc/cgroups.json,文件内容如下:
1 |
|
cgroup挂载完成后,通过adb的指令mount可以查看挂载的cgroup信息:
1 |
|
后续在应用启动创建进程的过程中,AMS会调用ProcessList.startProcess通过Process.createProcessGroup的接口来创建对应用户UID的冻结cgroup分组:
1 |
|
Process.createProcessGroup实际是一个native方法,android_os_Process_createProcessGroup方法最终调用processgroup.cpp中的createProcessGroupInternal函数,这个函数最终做两件事情:
- 根据进程的
uid与pid在/sys/fs/cgroup/目录下创建对应的cgroup分组 - 将进程的
pid写入到cgroup分组的procs文件中
1 |
|
等系统正常启动完成后,我们可以到/sys/fs/cgroup/目录下查看对应的cgroup分组状态:
1 |
|
进程冻结实现原理
在文章开始我们提到Android进程冻结的核心原理是基于cgroup中的冻结子系统来完成任务的冻结与解冻;具体来说,Android进程冻结分为两个步骤:
- 首先通过
IPCThreadState.freeze发送命令给binder驱动尝试冻结服务端的进程,binder驱动会冻结对应pid的服务,后续请求都会直接返回一个错误
1 |
|
binder驱动接收到冻结指令BINDER_FREEZE后,会将对应的binder服务进程设置为frozen状态,后续请求都会直接返回一个BR_FROZEN_REPLY错误码,表示binder服务已经被冻结;如果设置了timeout_ms,则需要等待binder服务完成所有客户端的请求后再返回。
1 |
|
binder服务冻结后,需要通过android_os_Process_setProcessFrozen接口通过cgroup冻结子系统执行冻结;进程冻结完成后,进程状态变为S,执行的路径会阻塞在do_freezer_trap
1 |
|
Android进程cgroup相关的配置文件有两个:一个是controller相关的cgroups.json,另一个是profiles相关的task_profiles.json。在task_profiles.json中,Frozen与Unfrozen两个profiles分别对应FreezerState的1与0,而FreezerState对应的是控制器freezer的cgroup.freeze文件。
有关
cgroup的详细介绍可以参考如何利用cgroups优化Android系统性能
1 |
|
SetProcessProfiles调用TaskProfiles.SetProcessProfiles函数来完成进程的冻结:SetProcessProfiles函数首先遍历系统中存在的所有profiles,找到对应名字为Frozen的profile,然后调用TaskProfile.ExecuteForProcess来完成进程的冻结。
1 |
|
ExecuteForTask首先需要通过对应的ProfileAttribute获取到对应的cgroup路径,然后通过WriteStringToFile将FreezerState的值写入到对应的cgroup.freeze文件中:
1 |
|
GetPathForTask函数通过controller()->GetTaskGroup获取到对应的cgroup路径,然后通过StringPrintf将cgroup.freeze文件的路径拼接起来,最终对应的路径为/sys/fs/cgroup/<uid>/<pid>/cgroup.freeze: 在该路径下写入1表示进程被冻结,写入0表示进程被解冻。
1 |
|
GetTaskGroup首先根据进程pid找到对应的cgroup所属的分组信息:冻结分组比较特殊,以0::开头,其余分组的则通过1:的形式开头。
1 |
|
写入cgroup.freeze文件后,对应调用到内核函数cgroup_freeze_write,实际通过cgroup_freeze将该分组下面的搜友子分组对应的所有任务都设置为FROZEN状态:
1 |
|
对于单个任务的冻结,都是通过函数cgroup_freeze_task来完成,该函数通过设置task->jobctl的JOBCTL_TRAP_FREEZE位来完成任务的冻结,通过清除task->jobctl的JOBCTL_TRAP_FREEZE位来完成任务的解冻。可以看到,内核实现任务的冻结并没有直接通过向对应的任务发送信号,而是首先设置一个JOBCTL_TRAP_FREEZE位;并通过set_tsk_thread_flag来标记当前任务有需要处理的信号,然后通过signal_wake_up函数唤醒对应的任务。任务唤醒后会返回到用户空间,然后在返回的路径上处理任务阻塞的信号,最终调用到get_signal函数来完成进程的冻结。
详细的内核冻结流程可以参考深入探究 Linux 内核中的 cgroup freezer 子系统
1 |
|
get_signal函数会检查当前进程是否需要处理信号,并检查JOBCTL_TRAP_FREEZE标志位,如果任务设置了该标志位,则调用do_freezer_trap函数来完成进程的冻结,这个函数也是冻结的任务最后执行的函数,在进程冻结后,我们可以通过查看进程的堆栈来确认这一点。
1 |
|
do_freezer_trap实际就做了这么三件事情:
- 将当前任务的状态设置为
TASK_INTERRUPTIBLE,并清除TIF_SIGPENDING标志位 - 调用
cgroup_enter_frozen设置当前任务为FROZEN状态,并更新对应分组的状态 - 调用
freezable_schedule启动调度,冻结的任务会移除调度队列,任务处于睡眠状态,切换其他任务执行
1 |
|
进程完全冻结后,我们通过ps -A命令查看进程状态,可以看到进程的状态为S,任务的等待通道(wait channel)为do_freezer_trap;查看进程的堆栈,可以看到进程确实是通过信号处理函数进入了冻结状态。
1 |
|
Android进程冻结策略
Android系统会在进程启动、服务绑定、应用前后台切换、发送/接收广播等场景会主动更新系统所有应用的adj值,adj值越小,表示进程优先级越高,对应的存活时间越久,越不容易被系统杀死。一个应用处于后台,如果长时间没有活动,系统会调整adj值,在系统资源紧张(比如内存不足时),会主动清理(冻结或者杀死)这些adj值较大(CACHED_APP_MIN_ADJ(900)<=adj<=CACHED_APP_MAX_ADJ(999))的进程。
应用调整adj值的核心逻辑都在OomAdjuster类中实现;更新完所有应用的adj值后,如果发现该进程的adj值大于CACHED_APP_MIN_ADJ,则会尝试调用CachedAppOptimizer.freezeAppAsyncLSP冻结该进程。其调用的链路大致如下:
1 |
|
updateAppFreezeStateLSP函数首先会判断系统是否开启了进程冻结功能,该功能默认是开启的,具体的值可以通过设置两个配置项来开关(全局数据库的配置优先级更高):
- 全局数据库
Settings.Global.CACHED_APPS_FREEZER_ENABLED:存放在系统数据库中的开关项,比如adb shell settings put global cached_apps_freezer 1 - 设备配置
DeviceConfig中的use_freezer项来设置,比如adb shell device_config put activity_manager_native_boot use_freezer true
如果未两个配置项都未开启,则说明系统不支持进程冻结,直接返回;否则如果进程的adj值大于等于CACHED_APP_MIN_ADJ且未被冻结过,则调用freezeAppAsyncLSP函数来冻结进程。
1 |
|
freezeAppAsyncLSP并不会立即执行进程的冻结,而是通过mFreezeHandler发送一个延迟10分钟的SET_FROZEN_PROCESS_MSG消息,如果在此期间,系统的adj没有变小,则执行进程的冻结。
1 |
|
总结
进程冻结的核心目标是在Android内存紧张时,主动冻结长时间不活动的后台应用,释放内存资源,从而节省功耗,提升系统性能。但目前来说,Android进程冻结的实现并不完善,还存在一些可以改善的地方,比如:
- 进程冻结只考虑到了内存资源情况,没有考虑到如CPU、IO等其他系统资源的占用情况
- 进程冻结目前只支持
Java层的应用,对于Native的进程并不支持冻结
参考文献
- https://gityuan.com/2018/05/19/android-process-adj/
- https://android.googlesource.com/platform/frameworks/base/+/master/services/core/java/com/android/server/am/OomAdjuster.md
- https://sniffer.site/2024/04/15/%E5%A6%82%E4%BD%95%E5%88%A9%E7%94%A8cgroup%E4%BC%98%E5%8C%96android%E7%B3%BB%E7%BB%9F%E6%80%A7%E8%83%BD
- https://kernel.meizu.com/2024/07/12/sub-system-cgroup-freezer-in-Linux-kernel/
