共计 9805 个字符,预计需要花费 25 分钟才能阅读完成。
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.java
frameworks/base/services/core/java/com/android/server/am/CachedAppOptimizer.java
frameworks/base/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp
frameworks/base/core/java/android/os/Process.java
frameworks/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 查问过程是否被冰冻?
目前没有。