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
的详细介绍可以参考深入理解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/