1 概述

Android从诞生之初就有一个难题: 怎么最大限度的优化过程对无限的零碎物理资源的应用,比方CPU、电量、内存等,同时保障良好的用户体验。

很多过程在进行和用户交互之后,会长期停留在后盾,此时它们对于用户体验没有任何奉献。

Android之所以没有立即杀掉这些过程,是出于用户复原应用这些过程时,启动速度的思考。然而这些过程在后盾却能够继续占据应用CPU,有些会在后盾继续耗费内存。

怎么在不杀掉这些过程的根底之上,最大限度的限度这些过程?

Freezer 挺身而出,通过“冰冻”的形式解决问题。

1.1 性能开启

形式一 开发者选项 "Suspend execution for cached apps"。 可选项一共有三个, Device default, Enabled 和Disabled。须要留神的是,Android R下面的default 等于性能敞开; Android S 下面是默认开启。

形式二 adb 命令: adb shell settings put global cached\_apps\_freezer <enabled|disabled|default>

通过以上任意形式开启敞开该性能,都须要重启失效。

1.2 版本要求

Kernel 5.4, kernel 5.10或者更高版本。Pixel 4.14 和4.19的kernel 也是反对的。

Android 版本最低要求是R,然而经测试R下面性能还有些问题,倡议在S上正式应用。

1.3 原理

Android依照优先级将个别的APP从高到低分为: _前台过程 --> 可感知过程--> 服务过程 --> Cached过程_。

Freezer通过冻住cached过程(如果对cached这个概念不分明,能够参考developer.android.google.cn/guide/compo… 来迫使这些过程让出CPU,以达到优化系统资源应用的目标。

1.4 框架

Framework 下层次要由两个类管制, OomAdjuster 负责计算APP的oom\_score\_adj,一旦某个APP的adj大于等于CACHED\_APP\_MIN\_ADJ,就会将冰冻该过程的工作委托给CachedAppOptimizer去解决,后者跑在独立的线程。

Android Q 开始,谷歌引入了cgroup形象层,搭配应用工作配置文件,来屏蔽底层cgroup调用细节,向上提供API。cgroup形象层编译成库libprocessgroup。形象层通过往cgroup的文件节点写入相应的值,来触发kernel的回调。

最终kernel cgroup机制的freezer管制子系统真正实现了冰冻过程的性能。

一个过程从前台运行到被冰冻的旅程

[图片上传失败...(image-187482-1648215439544)]

2 Framework 下层

2.1 代码门路及流程

frameworks/base/services/core/java/com/android/server/am/OomAdjuster.javaframeworks/base/services/core/java/com/android/server/am/CachedAppOptimizer.javaframeworks/base/services/core/jni/com_android_server_am_CachedAppOptimizer.cppframeworks/base/core/java/android/os/Process.javaframeworks/base/core/jni/android_util_Process.cpp

2.2 OomAdjuster更新过程adj的场景

 static final String OOM_ADJ_REASON_NONE = OOM_ADJ_REASON_METHOD + "_meh"; static final String OOM_ADJ_REASON_ACTIVITY = OOM_ADJ_REASON_METHOD + "_activityChange"; static final String OOM_ADJ_REASON_FINISH_RECEIVER = OOM_ADJ_REASON_METHOD + "_finishReceiver"; static final String OOM_ADJ_REASON_START_RECEIVER = OOM_ADJ_REASON_METHOD + "_startReceiver"; static final String OOM_ADJ_REASON_BIND_SERVICE = OOM_ADJ_REASON_METHOD + "_bindService"; static final String OOM_ADJ_REASON_UNBIND_SERVICE = OOM_ADJ_REASON_METHOD + "_unbindService"; static final String OOM_ADJ_REASON_START_SERVICE = OOM_ADJ_REASON_METHOD + "_startService"; static final String OOM_ADJ_REASON_GET_PROVIDER = OOM_ADJ_REASON_METHOD + "_getProvider"; static final String OOM_ADJ_REASON_REMOVE_PROVIDER = OOM_ADJ_REASON_METHOD + "_removeProvider"; static final String OOM_ADJ_REASON_UI_VISIBILITY = OOM_ADJ_REASON_METHOD + "_uiVisibility"; static final String OOM_ADJ_REASON_ALLOWLIST = OOM_ADJ_REASON_METHOD + "_allowlistChange"; static final String OOM_ADJ_REASON_PROCESS_BEGIN = OOM_ADJ_REASON_METHOD + "_processBegin"; static final String OOM_ADJ_REASON_PROCESS_END = OOM_ADJ_REASON_METHOD + "_processEnd";

通过更新起因能够看到,过程的状态变动,或者组件的状态变动会触发从新计算adj。

举例, 当一个APP解决播送时,就会触发从新计算这个APP的adj,此时的更新起因是OOM\_ADJ\_REASON\_START\_RECEIVER:

      private final void processCurBroadcastLocked(BroadcastRecord r,              ProcessRecord app) throws RemoteException {          ......          r.receiver = thread.asBinder();          r.curApp = app;          final ProcessReceiverRecord prr = app.mReceivers;          prr.addCurReceiver(r);          app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER);          mService.updateLruProcessLocked(app, false, null);          // Make sure the oom adj score is updated before delivering the broadcast.          // Force an update, even if there are other pending requests, overall it still saves time,          // because time(updateOomAdj(N apps)) <= N * time(updateOomAdj(1 app)).          mService.enqueueOomAdjTargetLocked(app);          mService.updateOomAdjPendingTargetsLocked(OomAdjuster.OOM_ADJ_REASON_START_RECEIVER);          ......      }

OomAdjuster在computeOomAdjLSP中会将该app的adj晋升至FOREGROUND\_APP\_ADJ:

     private boolean computeOomAdjLSP(ProcessRecord app, int cachedAdj,              ProcessRecord topApp, boolean doingAll, long now, boolean cycleReEval,              boolean computeClients) {              ......          } else if (state.getCachedIsReceivingBroadcast(mTmpBroadcastQueue)) {              // An app that is currently receiving a broadcast also              // counts as being in the foreground for OOM killer purposes.              // It's placed in a sched group based on the nature of the              // broadcast as reflected by which queue it's active in.              adj = ProcessList.FOREGROUND_APP_ADJ;              schedGroup = (mTmpBroadcastQueue.contains(mService.mFgBroadcastQueue))                      ? ProcessList.SCHED_GROUP_DEFAULT : ProcessList.SCHED_GROUP_BACKGROUND;              state.setAdjType("broadcast");              procState = ActivityManager.PROCESS_STATE_RECEIVER;              if (DEBUG_OOM_ADJ_REASON || logUid == appUid) {                  reportOomAdjMessageLocked(TAG_OOM_ADJ, "Making broadcast: " + app);              }          }           ......

2.3 Debounce time

当某个APP过程的adj达到CACHED\_APP\_MIN\_ADJ时,CachedAppOptimizer就会调用freezeAppAsyncLSP,在debounce time之后,正式冰冻该过程, 这是为了避免过程在进入cached状态时还有工作没有实现从而间接影响用户体验。

Debounce time 在Android S 上默认是十分钟,这个值是能够在线批改的, 比方改为1秒:

adb shell device_config put activity_manager_native_boot freeze_debounce_timeout 1000

批改后的值重启会生效,变回默认值。

与异步冰冻流程相同,冻结的过程是同步进行的,一个App过程一旦被冻结胜利,它就立即恢复正常运行。

2.4 freezeProcess

第一步,查看文件锁"/proc/locks"的状态。这是为了避免冰冻过程持有文件锁引起死锁。思考到一些非凡场景下,过程在被冰冻的过程中拿住了文件锁,冰冻胜利后还会再查看一次,发现持有锁就立即冻结。

第二步,freeze binder。这一步禁掉该过程对同步binder申请的接管和解决,以及对异步binder申请的解决。该过程须要在100ms内胜利,如果过程须要解决的申请过多导致无奈实现,则再多给一轮debounce time。

第三步,setProcessFrozen,调用形象层提供的API冰冻过程。

第四步,再次查看该过程有没有须要解决的binder申请,有则冻结过程,再多给一轮debounce time。这个查看是为一些非凡场景下,过程在被冰冻的过程中产生了binder申请。

2.5 setProcessFrozen

  void android_os_Process_setProcessFrozen(          JNIEnv *env, jobject clazz, jint pid, jint uid, jboolean freeze)  {      bool success = true;        if (freeze) {          success = SetProcessProfiles(uid, pid, {"Frozen"});      } else {          success = SetProcessProfiles(uid, pid, {"Unfrozen"});      }        if (!success) {          signalExceptionForGroupError(env, EINVAL, pid);      

调用cgroup形象层的SetProcessProfiles, 传入参数“Frozen”。

3 cgroup 两头形象层

cgroup两头形象层libprocessgroup,次要提供两个性能,其一在启动阶段,依据cgroups.json 来装载具体的cgroup; 其二依据task\_profiles.json来定义对cgroup具体的操作以及参数。 次要代码:

/system/core/libprocessgroup/

3.1 cgroups.json

示例文件:

{  "Cgroups": [    {      "Controller": "cpu",      "Path": "/dev/cpuctl",      "Mode": "0755",      "UID": "system",      "GID": "system"    },    {      "Controller": "memory",      "Path": "/dev/memcg",      "Mode": "0700",      "Optional": true    }  ], "Cgroups2": {   "Path": "/sys/fs/cgroup",   "Mode": "0755",   "UID": "system",   "GID": "system",   "Controllers": [     {       "Controller": "freezer",       "Path": ".",       "Mode": "0755",       "UID": "system",       "GID": "system"     }   ] }} 

该文件分为两局部,"Cgroups" 形容cgroup v1 的管制子系统 和"Cgroups2" 形容cgroup v2的管制子系统。如果不理解cgroup 两个版本,能够参看以下网址:

www.kernel.org/doc/html/la… www.kernel.org/doc/html/la…

目前v1和v2是共存的, freezer所应用的是v2,对应根目录是/sys/fs/cgroup, UID和GID都是system。

启动阶段,Init在SetupCgroupAction中通过调用CgroupSetup,解析cgroups.json文件,实现对cgroup的装载。

须要留神的是,cgroups.json文件可能不止一个:

/system/core/libprocessgroup/profiles/cgroups.json              //默认文件,都有/system/core/libprocessgroup/profiles/cgroups_<API level>.json  //API级别的文件,可能有/vendor/xxx/cgroups.json                                        //vendor自定义的文件

这三个文件加载的程序是: 默认--> API级别 --> vendor,于是就存在一个笼罩的流程。只有前面一个文件中定义的"Controller"与后面的字符串雷同,就会笼罩前者的定义。

造成的层级构造是这样的:

Android freezer 在/sys/fs/cgroup下建设了以uid/pid命名的二级子目录,对uid节点的管制会同步利用于其上面所有的pid节点。 uid在利用装置的时候确定, pid在利用应用的时候产生。如果manifest中应用了android:process,则在以后uid目录下建设一个新的pid结尾的目录; 如果过程被动调用了fork产生子过程,则不会产生新的pid目录,而是放在父过程目录下,这就意味着父过程被冰冻的时候同目录的子过程也会被冰冻。

3.2 task\_profiles.json

示例 task\_profiles.json

 {  "Attributes": [     {       "Name": "UClampLatencySensitive",       "Controller": "cpu",       "File": "cpu.uclamp.latency_sensitive"     },     {       "Name": "FreezerState",       "Controller": "freezer",       "File": "cgroup.freeze"     }   ],    "Profiles": [     {       "Name": "Frozen",       "Actions": [         {           "Name": "SetAttribute",           "Params":           {             "Name": "FreezerState",             "Value": "1"           }         }       ]     },     {       "Name": "Unfrozen",       "Actions": [         {           "Name": "SetAttribute",           "Params":           {             "Name": "FreezerState",             "Value": "0"           }         }       ]     },    {       "Name": "CpuPolicySpread",       "Actions": [         {           "Name": "SetAttribute",           "Params":           {             "Name": "UClampLatencySensitive",             "Value": "1"           }         }       ]     }   ],    "AggregateProfiles": [     {       "Name": "SCHED_SP_BACKGROUND",       "Profiles": [ "HighEnergySaving", "LowIoPriority", "TimerSlackHigh" ]     }   ] }

task\_profiles.json次要由三局部组成, Attributes, Profiles 和AggregateProfiles。

Attributes定义属性, 蕴含名称,管制子系统 以及接口文件。比方下面的示例文件中,定义了名称“FreezerState”的属性, 应用到的管制子系统是freezer, 接口文件是"cgroup.freeze"。

Profiles 定义操作。 一种是SetAttribute,援用后面定义的Attributes项,增加参数(Value);一种是JoinCgroup, 蕴含管制子系统及其门路。

比方下面的示例文件中,名称“Frozen”的项,应用SetAttribute的action,援用“FreezerState” 这个attribute,参数为1,也就是往cgroup.freeze写入1,这正是2.5大节SetProcessProfiles的具体实现。

AggregateProfiles 是Profiles项的组合应用。

task\_profiles.json也可能不止一个:

/system/core/libprocessgroup/profiles/task_profiles.json/system/core/libprocessgroup/profiles/task_profiles_<API>.json/vendor/xxx/task_profiles.json

加载、笼罩的程序和cgroups.json相似,依照"Name"来匹配,只有两个文件中定义了同名项,后者就会笼罩前者的定义。

须要留神的是,厂商须要定制本人的cgroup 或者task profile时,应该去批改vendor上面对应的文件, 而不要去改默认的或者API对应的文件:

/vendor/xxx/cgroups.json /vendor/xxx/task_profiles.json

4 Kernel

从cgroup的形象层到 kernel 通过文件写入:

/sys/fs/cgroup/<UID_xxx>/<PID_xxx>/cgroup.freeze

对该文件的写入触发写回调cgroup\_freeze\_write, 通过以后的kernfs找到对应的cgroup, 将这个cgroup下的所有过程以及子过程都freeze。具体代码流程留待下一篇再述。

    {        .name = "cgroup.freeze",        .flags = CFTYPE_NOT_ON_ROOT,        .seq_show = cgroup_freeze_show,        .write = cgroup_freeze_write,    },

5 察看冰冻过程

5.1 应用ps命令查看被冻住的过程

u0_a108 17788  1045 1347272 82528 do_freezer_trap  0 S com.android.testapp

能够看到该过程被冻住之后,处于S 睡眠态, WCHAN 是do\_freezer\_trap。

5.2 致命信号

冻住的过程能够被致命信号杀掉。这里的致命信号是指默认的信号处理函数是"terminate"或者"dump core"的信号,不能够被疏忽,也不能够从新注册处理函数。

举个例子,信号SIGSEGV是杀不掉被冻住的过程的,因为它的信号处理函数曾经被从新注册为debuggerd\_signal\_handler了。

5.3 binder 状态

向冻住的过程发送同步binder申请会立即收到错误码BR\_FROZEN\_REPLY。

过程在被冻结之前零碎会先查看该过程有无在冰冻期间收到同步binder申请(SYNC\_RECEIVED\_WHILE\_FROZEN), 有则会杀掉该过程。

     void unfreezeAppLSP(ProcessRecord app) {         final int pid = app.getPid();         final ProcessCachedOptimizerRecord opt = app.mOptRecord;         ......          try {             int freezeInfo = getBinderFreezeInfo(pid);              if ((freezeInfo & SYNC_RECEIVED_WHILE_FROZEN) != 0) {                 Slog.d(TAG_AM, "pid " + pid + " " + app.processName + " "                         + " received sync transactions while frozen, killing");                 app.killLocked("Sync transaction while in frozen state",                         ApplicationExitInfo.REASON_OTHER,                         ApplicationExitInfo.SUBREASON_FREEZER_BINDER_TRANSACTION, true);                 processKilled = true;             }              if ((freezeInfo & ASYNC_RECEIVED_WHILE_FROZEN) != 0) {                 Slog.d(TAG_AM, "pid " + pid + " " + app.processName + " "                         + " received async transactions while frozen");             }         } catch (Exception e) {             Slog.d(TAG_AM, "Unable to query binder frozen info for pid " + pid + " "                     + app.processName + ". Killing it. Exception: " + e);             app.killLocked("Unable to query binder frozen stats",                     ApplicationExitInfo.REASON_OTHER,                     ApplicationExitInfo.SUBREASON_FREEZER_BINDER_IOCTL, true);             processKilled = true;         }       ......      }

冻住的过程能够接管异步binder申请,然而不会解决,只是放入binder buffer, 过多的申请会导致buffer耗尽。

6 其余

问题 1. freezer有无豁免机制?

持有INSTALL\_PACKAGES的APP不会被冰冻。一般APP不应该思考逃脱冰冻,而是应该严格遵守开发标准。

问题 2. 有无API提供给APP查问过程是否被冰冻?

目前没有。