“暂停执行已缓存的应用” 是如何工作的

前言

终于有机会来写写关于这个话题的东西了。两三个月前,“暂停执行已缓存的应用” 被当作 安卓版的 “墓碑后台” 大火了一阵。

“暂停执行已缓存的应用” 是如何工作的

imo 这一切只是以讹传讹,是人类愚蠢和安慰剂效应的再次体现。这个功能在 Android 11 后期下放到 Pixel 时,对它的误解就已经开始。

通过这篇文章,我希望你能够了解它是如何工作的。

文章只针对 Pixel 自带系统和 AOSP,若厂商对其有任何魔改则不算数。

文章基于 Android 12L。

原理基础

冻结

“暂停执行已缓存的应用” 在内核层面使用的是 cgroup freezer,具体来说是 cgroup v2 freezer。相较于使用信号 SIGSTOP 与 SIGCONT 实现的挂起与恢复,cgroup freezer 无法被拦截或观察到,具有对用户程序完全透明的特性。

对于 cgroup freezer 在内核中的工作方式,此处不再深入展开。需要知道的只有——假如一个程序被 freezer “冻结”住,它将完全被“暂停”,不再消耗任何 CPU 资源。直观的感受就是这个程序进入了“未响应”状态。嗯,假如它在前台,那它确实会“未响应”——无法对任何交互操作做出反馈,系统甚至还会弹一个 ANR 对话框。

cgroup v1

cgroup 本身就在 Android 系统中有广泛的应用:

4829 4751 0:23 / /dev/blkio rw,nosuid,nodev,noexec,relatime master:15 - cgroup none rw,blkio
4868 4751 0:25 / /dev/cpuctl rw,nosuid,nodev,noexec,relatime master:17 - cgroup none rw,cpu
5536 4751 0:26 / /dev/cpuset rw,nosuid,nodev,noexec,relatime master:18 - cgroup none rw,cpuset,noprefix,release_agent=/sbin/cpuset_release_agent
5537 4751 0:27 / /dev/stune rw,nosuid,nodev,noexec,relatime master:19 - cgroup none rw,schedtune

像是常见的控制 CPU 分配的 cpuset,控制 EAS 调度器的 stune,都会在系统启动时被挂载到 /dev 下,每一个这样的“挂载点”被称为一个“层级”(hierarchy)。以上面为例,上面的挂载信息中共有四个“层级”。每个“层级”都可以关联到一个或多个“控制器”(controller)(在挂载时直接绑定)。每个“层级”下又建立有多个“控制组”(control group),通过将任务加入“控制组”并调整“控制组”的参数,即可实现对“层级”所绑定的“控制器”的资源分配。

比如对于 stune “层级”,它在挂载时绑定了 schedtune “控制器”,可以实现对于 EAS scheduler 的资源控制。
系统在其下建立了多个“控制组”,比如foregroundbackgroundtop-apprt

system/core/rootdir/init.rc

    # Create energy-aware scheduler tuning nodes
    mkdir /dev/stune/foreground
    mkdir /dev/stune/background
    mkdir /dev/stune/top-app
    mkdir /dev/stune/rt

这些“控制组”具有不同的参数(比如调度激进程度),通过将进程加入这些“控制组”即可实现资源分配。也就是说,进程们被按照工作状态分为了几类,每类进程都会被绑定到对应的“控制组”,享受“控制组”限制的资源。
系统与这些 cgroup 的交互通过 libprocessgroup(system/core/libprocessgroup) 模块完成。

好了完美离题,不再继续展开。上面的东西展示了从用户视角看 cgroup v1 是长啥样的,但是 “暂停执行已缓存的应用” 依赖于 cgroup v2 实现。

cgroup v2 vs cgroup v1

Android 系统将 cgroup v2 挂载在了 /sys/fs/cgroup:

6476 6474 0:24 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime master:16 - cgroup2 none rw

cgroup v2 与 v1 最大的区别是:它只有一个“层级”,并不会像 v1 那样东挂载一个西挂载一个,也不需要通过在挂载时指定参数来绑定“控制器”。
通过在根“层级”下新建文件夹来创建“控制组”(这个和 v1 一样),通过写入节点的方式来为子“控制组”指定可用的“控制器”(这个比 v1 方便的多),再将任务加入对应的“控制组”即可实现资源分配。
“控制组”是可以嵌套创建的,按照设计外层“控制组”施加的限制也会作用到内层,但是这要取决于相关“控制器”的实现(一些比较“抽象”的资源分配就没法做到)。
为了避免冲突,任务只能被增加到最内层的“控制组”,如果从一棵树的角度来理解就是“叶节点”。
具体内容参见 权威的官方文档

对于 Android 来说,看起来它还完全没有准备好迁移到 cgroup v2。由于所有可用的“控制器”都被关联到了 cgroup v1,因此完全没法用 cgroup v2 来分配资源,就目前来看,v2 就是专门为“暂停执行已缓存的应用”服务的(嗯,冻结是它本身就有的功能,不需要依赖别的“控制器”)。

目前,cgroup v2 工作在以下结构下:
/sys/fs/cgroup/uid_<uid>/pid_<pid>
比如对于进程号为 6383 、用户号为 10081 的进程,它会被加入到以下“控制组”中:
/sys/fs/cgroup/uid_10081/pid_6383

当它需要被冻结的时候,只需要将其所在的“控制组”冻结即可。
echo 1 > /sys/fs/cgroup/uid_10081/pid_6383/cgroup.freeze

而这也正是“暂停执行已缓存的应用”与内核的交互接口。

平台支持

cgroup v2 在 Linux 4.5 就已经被加入,但是 cgroup v2 的 freezer 支持直到 Linux 5.1 才加入。
对于 Android 平台来说,5.4 内核自带完备的 cgroup v2 freezer 支持,而 5.4 以下的内核版本则需要移植。
4.19 内核得到了谷歌的官方移植 —— cgroup v2 freezer 被谷歌反向移植到了 kernel/common 的 android-4.19-stable 分支 上,这可真是“幸运”极了。之所以要带引号,先卖个关子,下面就知道了。

高通平台的开源代码会定期合并谷歌的 kernel/common (前提是它还在积极的维护),于是乎 cgroup v2 freezer 在 CAF Tag LA.UM.9.12.r1-09300-SMxx50.0 进入了高通开源代码。由于这种合并也会造成内核版本的变化,于是对于高通平台,有一种简单粗暴的方法可以判断内核是否支持 cgroup v2 freezer:看内核版本是否大于等于 4.19.152。

系统判断是否支持 cgroup v2 freezer 的方法非常粗暴:

services/core/java/com/android/server/am/CachedAppOptimizer.java

    public static boolean isFreezerSupported() {
        boolean supported = false;
        FileReader fr = null;

        try {
            fr = new FileReader(getFreezerCheckPath());
            char state = (char) fr.read();

            if (state == '1' || state == '0') {
                supported = true;
            } else {
                Slog.e(TAG_AM, "unexpected value in cgroup.freeze");
            }
        } catch (java.io.FileNotFoundException e) {
            Slog.d(TAG_AM, "cgroup.freeze not present");
        } catch (Exception e) {
            Slog.d(TAG_AM, "unable to read cgroup.freeze: " + e.toString());
        }

        if (fr != null) {
            try {
                fr.close();
            } catch (java.io.IOException e) {
                Slog.e(TAG_AM, "Exception closing freezer.killable: " + e.toString());
            }
        }

        return supported;
    }

它直接去尝试读取上面所提及的 cgroup.freeze 节点,如果返回值比较“正常”则直接认为系统支持该功能。
只有在系统支持该功能的时候,开发者选项里才会显示“暂停执行已缓存的应用”设置项。
如果系统支持 cgroup v2 freezer,在 Android 12 以上设备默认启用“暂停执行已缓存的应用”。
因此在 Android 12+ 设备上打开这个选项觉得变省电流畅的,均是幻觉。

工作机制

结构

“暂停执行已缓存的应用” 的核心代码工作在 services/core/java/com/android/server/am/CachedAppOptimizer.java
而 CachedAppOptimizer 则是 OomAdjuster (services/core/java/com/android/server/am/OomAdjuster.java)的一个成员。
OomAdjuster 则是 ActivityManagerService (services/core/java/com/android/server/am/ActivityManagerService.java)的一个成员。
从结构上看,这一切都由 AMS 负责,不过这也很正常,毕竟 AMS 负责着应用的生命周期嘛。

OomAdjuster 负责调整与更新应用的 oom_score_adj,它是一个与 lmk (lowmemorykiller) 有关的参数,会被汇报给内核 /proc/<pid>/oom_score_adj,供内核态的 lmk 使用(比如旧的 lowmemorykiller 或者 simple_lmk),用户态的 lmkd 也会读取此参数来决定内存不足时杀应用的顺序。

系统内置了一张表,描述了不同应用类型所对应的 oom_score_adj 优先级。

services/core/java/com/android/server/am/ProcessList.java

    // OOM adjustments for processes in various states:

    // Uninitialized value for any major or minor adj fields
    static final int INVALID_ADJ = -10000;

    // Adjustment used in certain places where we don't know it yet.
    // (Generally this is something that is going to be cached, but we
    // don't know the exact value in the cached range to assign yet.)
    static final int UNKNOWN_ADJ = 1001;

    // This is a process only hosting activities that are not visible,
    // so it can be killed without any disruption.
    static final int CACHED_APP_MAX_ADJ = 999;
    static final int CACHED_APP_MIN_ADJ = 900;

    // This is the oom_adj level that we allow to die first. This cannot be equal to
    // CACHED_APP_MAX_ADJ unless processes are actively being assigned an oom_score_adj of
    // CACHED_APP_MAX_ADJ.
    static final int CACHED_APP_LMK_FIRST_ADJ = 950;

    // Number of levels we have available for different service connection group importance
    // levels.
    static final int CACHED_APP_IMPORTANCE_LEVELS = 5;

    // The B list of SERVICE_ADJ -- these are the old and decrepit
    // services that aren't as shiny and interesting as the ones in the A list.
    static final int SERVICE_B_ADJ = 800;

    // This is the process of the previous application that the user was in.
    // This process is kept above other things, because it is very common to
    // switch back to the previous app.  This is important both for recent
    // task switch (toggling between the two top recent apps) as well as normal
    // UI flow such as clicking on a URI in the e-mail app to view in the browser,
    // and then pressing back to return to e-mail.
    static final int PREVIOUS_APP_ADJ = 700;

    // This is a process holding the home application -- we want to try
    // avoiding killing it, even if it would normally be in the background,
    // because the user interacts with it so much.
    static final int HOME_APP_ADJ = 600;

    // This is a process holding an application service -- killing it will not
    // have much of an impact as far as the user is concerned.
    static final int SERVICE_ADJ = 500;

    // This is a process with a heavy-weight application.  It is in the
    // background, but we want to try to avoid killing it.  Value set in
    // system/rootdir/init.rc on startup.
    static final int HEAVY_WEIGHT_APP_ADJ = 400;

    // This is a process currently hosting a backup operation.  Killing it
    // is not entirely fatal but is generally a bad idea.
    static final int BACKUP_APP_ADJ = 300;

    // This is a process bound by the system (or other app) that's more important than services but
    // not so perceptible that it affects the user immediately if killed.
    static final int PERCEPTIBLE_LOW_APP_ADJ = 250;

    // This is a process hosting services that are not perceptible to the user but the
    // client (system) binding to it requested to treat it as if it is perceptible and avoid killing
    // it if possible.
    static final int PERCEPTIBLE_MEDIUM_APP_ADJ = 225;

    // This is a process only hosting components that are perceptible to the
    // user, and we really want to avoid killing them, but they are not
    // immediately visible. An example is background music playback.
    static final int PERCEPTIBLE_APP_ADJ = 200;

    // This is a process only hosting activities that are visible to the
    // user, so we'd prefer they don't disappear.
    static final int VISIBLE_APP_ADJ = 100;
    static final int VISIBLE_APP_LAYER_MAX = PERCEPTIBLE_APP_ADJ - VISIBLE_APP_ADJ - 1;

    // This is a process that was recently TOP and moved to FGS. Continue to treat it almost
    // like a foreground app for a while.
    // @see TOP_TO_FGS_GRACE_PERIOD
    static final int PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ = 50;

    // This is the process running the current foreground app.  We'd really
    // rather not kill it!
    static final int FOREGROUND_APP_ADJ = 0;

    // This is a process that the system or a persistent process has bound to,
    // and indicated it is important.
    static final int PERSISTENT_SERVICE_ADJ = -700;

    // This is a system persistent process, such as telephony.  Definitely
    // don't want to kill it, but doing so is not completely fatal.
    static final int PERSISTENT_PROC_ADJ = -800;

    // The system process runs at the default adjustment.
    static final int SYSTEM_ADJ = -900;

    // Special code for native processes that are not being managed by the system (so
    // don't have an oom adj assigned by the system).
    static final int NATIVE_ADJ = -1000;

啊,有点离题了,不再继续展开这个东西了,你只需要知道,数值越小,代表应用的优先级越高,杀后台的优先级越低,越不容易被杀掉。
“已缓存的应用” 的优先级非常低。从上表可以看到,它们高高位于表的顶端,再往上只有 “未知” 和 “无效” 了。
也就是说,当内存不足时,“已缓存的应用”是会被第一时间回收掉的,甚至连一般的“后台服务”都比它们生命力强的多。

挑战一:优先级

那么什么是“已缓存的应用”呢?我们可以用排除法。一个应用,要想成为“已缓存的应用”,得不在前台,得不能是刚刚切到后台,得不可见(比如不能有悬浮窗),得难以察觉(比如不能在后台放音乐),还得不能有后台服务。什么样的应用能如此卑微?说白了,这个应用本身得足够老实,没什么后台功能,它呆在后台的唯一作用是为了切回去不用重新加载,那么它才有“资格”沦为“已缓存的应用”。

OomAdjuster 是负责更新这个东西的,那么也就是说,它会被 AMS 中的其它组件在进程生命周期的适宜时机被调用,第一时间掌握 OOM 优先级改变的信息。
我们的主角 CachedAppOptimizer 是它的一个成员,为它所操纵,根据新拿到的 OOM 优先级执行相关操作:

services/core/java/com/android/server/am/OomAdjuster.java

    @GuardedBy({"mService", "mProcLock"})
    private void updateAppFreezeStateLSP(ProcessRecord app) {
        if (!mCachedAppOptimizer.useFreezer()) {
            return;
        }

        if (app.mOptRecord.isFreezeExempt()) {
            return;
        }

        final ProcessCachedOptimizerRecord opt = app.mOptRecord;
        // if an app is already frozen and shouldNotFreeze becomes true, immediately unfreeze
        if (opt.isFrozen() && opt.shouldNotFreeze()) {
            mCachedAppOptimizer.unfreezeAppLSP(app);
            return;
        }

        final ProcessStateRecord state = app.mState;
        // Use current adjustment when freezing, set adjustment when unfreezing.
        if (state.getCurAdj() >= ProcessList.CACHED_APP_MIN_ADJ && !opt.isFrozen()
                && !opt.shouldNotFreeze()) {
            mCachedAppOptimizer.freezeAppAsyncLSP(app);
        } else if (state.getSetAdj() < ProcessList.CACHED_APP_MIN_ADJ) {
            mCachedAppOptimizer.unfreezeAppLSP(app);
        }
    }

freezeAppAsyncLSP() 是准备冻结应用的方法,方法名中的 LSP 大概指的是 Lock mService mProcLock,代表了调用这个方法之前必须取得这俩锁。
可以看到,冻结的必要不充分条件是:进程的 OOM 优先级数值大于 CACHED_APP_MIN_ADJ,也就是说,只有进程是“已缓存的应用”或者比它们更不重要才有机会被冻结。而从上面的介绍中我们已经知道,被标记为“已缓存的应用”的进程基本已经是属于最不重要的那种了。但是,调用了这个“准备冻结”方法,应用的进程马上就被冻结了吗?别急,还有层层挑战在后面等着。

挑战二:消抖

services/core/java/com/android/server/am/CachedAppOptimizer.java

    void freezeAppAsyncLSP(ProcessRecord app) {
        final ProcessCachedOptimizerRecord opt = app.mOptRecord;
        if (opt.isPendingFreeze()) {
            // Skip redundant DO_FREEZE message
            return;
        }

        mFreezeHandler.sendMessageDelayed(
                mFreezeHandler.obtainMessage(
                    SET_FROZEN_PROCESS_MSG, DO_FREEZE, 0, app),
                mFreezerDebounceTimeout);
        opt.setPendingFreeze(true);
        if (DEBUG_FREEZER) {
            Slog.d(TAG_AM, "Async freezing " + app.getPid() + " " + app.processName);
        }
    }

freezeAppAsyncLSP() 所做的事情是准备(看方法名中的 Async 可知它并不会立刻开始冻结),它将进程设置为了“等待冻结”的状态,并利用 Handler 的 sendMessageDelayed() 设定了一个延时消息事件发送:这条消息(SET_FROZEN_PROCESS_MSG)将会在 mFreezerDebounceTimeout 后才被 Handler 本体收到:

services/core/java/com/android/server/am/CachedAppOptimizer.java

    private final class FreezeHandler extends Handler {
        ......

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case SET_FROZEN_PROCESS_MSG:
                    synchronized (mAm) {
                        freezeProcess((ProcessRecord) msg.obj);
                    }
                    break;
                ......
            }
        }
        ......

而在 Handler 本体收到这一消息后,真正的冻结才会开始。

嗯,所以在准备冻结后,至少要等待 mFreezerDebounceTimeout 的时长,Handler 才会收到消息,冻结才会开始,对吗?
不完全正确,因为它可能就不会开始:

services/core/java/com/android/server/am/CachedAppOptimizer.java

    void unfreezeAppLSP(ProcessRecord app) {
        final int pid = app.getPid();
        final ProcessCachedOptimizerRecord opt = app.mOptRecord;
        if (opt.isPendingFreeze()) {
            // Remove pending DO_FREEZE message
            mFreezeHandler.removeMessages(SET_FROZEN_PROCESS_MSG, app);
            opt.setPendingFreeze(false);
            if (DEBUG_FREEZER) {
                Slog.d(TAG_AM, "Cancel freezing " + pid + " " + app.processName);
            }
        }
        ......
    }

假如在等待的过程中,unfreezeAppLSP() 被调用,也就是说,应用被请求解除冻结,那么,这个“设置冻结”的“延时消息事件”将会被直接被移除,这条消息也将永远无法被 Handler 收到,冻结也永远不会开始。

services/core/java/com/android/server/am/CachedAppOptimizer.java

    void unfreezeTemporarily(ProcessRecord app) {
        if (mUseFreezer) {
            synchronized (mProcLock) {
                if (app.mOptRecord.isFrozen() || app.mOptRecord.isPendingFreeze()) {
                    unfreezeAppLSP(app);
                    freezeAppAsyncLSP(app);
                }
            }
        }
    }

又比如说这个方法?unfreezeTemporarily() 方法进行了一个更高级的封装,它先调用 unfreezeAppLSP() 移除上一条延时消息,然后又调用 freezeAppAsyncLSP() 再准备发送一条延时消息,这是干了啥呢?嗯,重置计时器。说白了,刚刚等待过的时间不算,现在得重新等待 mFreezerDebounceTimeout 的时长,冻结才会真正开始。

也就是说,在这个等待冻结的缓冲时间内,倒计时可能会被重置,也有可能会被直接取消。

什么时候会取消或重置呢?

比如这个进程不再是“已缓存的应用”了,等待中的冻结会被直接取消:

services/core/java/com/android/server/am/OomAdjuster.java

    } else if (state.getSetAdj() < ProcessList.CACHED_APP_MIN_ADJ) {
        mCachedAppOptimizer.unfreezeAppLSP(app);
    }

又比如说有一个广播即将发往目标进程,等待中的冻结倒计时会被重置:

services/core/java/com/android/server/am/BroadcastQueue.java

    } else if (filter.receiverList.app != null) {
        mService.mOomAdjuster.mCachedAppOptimizer.unfreezeTemporarily(filter.receiverList.app);
    }

对了,unfreezeAppLSP() 的本职工作是“解冻已被冻结的应用”,取消等待中的冻结只能算是其准备工作中的一小部分。
因此,对于上面两种情况:不再是已缓存的应用会被解冻;如果一个广播被发往已冻结的应用,那么目标应用会被解冻,并且由于冻结倒计时重新开始,它会至少经过 mFreezerDebounceTimeout 的时长才会再次被尝试冻结

为什么要等待?为什么不直接冻住呢?为的是“消抖”。

这种“消抖”有什么作用呢?想象一下:假如 freezeAppAsyncLSP() 后一毫秒来了个 unfreezeAppLSP(),如果没有这个“延时”过程,那么应用将会被冻结又马上重新解冻,这不是纯纯的资源浪费吗?假如一个应用经常在后台收到广播,如果不“消抖”,那频繁的冻结和解冻不也显得很没必要吗?

“消抖”为真正的冻结提供了一段缓冲时间,只有在这段时间内保持平静,冻结才会真正开始,以此确保冻结的是那些真正需要冻结的应用。

那么等待冻结的缓冲时间 mFreezerDebounceTimeout 默认是多少呢?十秒?半分钟?不,是十分钟:

services/core/java/com/android/server/am/CachedAppOptimizer.java

    @VisibleForTesting static final long DEFAULT_FREEZER_DEBOUNCE_TIMEOUT = 600_000L;

也就是说,应用至少要进缓存十分钟,而且在这十分钟内安静老实还无所事事才有机会被冻结,稍有风吹草动不仅会被解冻,倒计时还会重置,冻结仿佛成了一种珍贵高雅的享受。

由此我们也可以看出“暂停执行已缓存的应用”的设计思路:它根本就不是用来压制后台应用的,后台应用随便跳一跳就能使它失效;它的作用对象就是那些真正“小而美”的应用——将安静的它们冻住来稍微节省那么一点点的电量。

不过这个“十分钟”是默认值,还是有机会对其进行调整的,虽然我觉得调整它并不能使更多的应用被冻住。
你可以通过 DeviceConfig,对 activity_manager_native_boot 命名空间中的 freeze_debounce_timeout 属性进行调整,从而改变默认的“消抖时间”。
对 DeviceConfig 的介绍,详见此处

挑战三:冻结

别以为经过了那难熬的十分钟,开始冻结了,一切就万事大吉了。

接下来把目光聚焦在消抖延时结束后,Handler 收到消息后,真正会调用的冻结函数,来看看距离真正的冻结还要卖过几道坎:

services/core/java/com/android/server/am/CachedAppOptimizer.java

        private void freezeProcess(final ProcessRecord proc) {
            int pid = proc.getPid(); // Unlocked intentionally
            final String name = proc.processName;
            final long unfrozenDuration;
            final boolean frozen;
            final ProcessCachedOptimizerRecord opt = proc.mOptRecord;

            opt.setPendingFreeze(false);

            try {
                // pre-check for locks to avoid unnecessary freeze/unfreeze operations
                if (mProcLocksReader.hasFileLocks(pid)) {
                    if (DEBUG_FREEZER) {
                        Slog.d(TAG_AM, name + " (" + pid + ") holds file locks, not freezing");
                    }
                    return;
                }
            } catch (Exception e) {
                Slog.e(TAG_AM, "Not freezing. Unable to check file locks for " + name + "(" + pid
                        + "): " + e);
                return;
            }

            synchronized (mProcLock) {
                pid = proc.getPid();
                if (proc.mState.getCurAdj() < ProcessList.CACHED_APP_MIN_ADJ
                        || opt.shouldNotFreeze()) {
                    if (DEBUG_FREEZER) {
                        Slog.d(TAG_AM, "Skipping freeze for process " + pid
                                + " " + name + " curAdj = " + proc.mState.getCurAdj()
                                + ", shouldNotFreeze = " + opt.shouldNotFreeze());
                    }
                    return;
                }

                if (mFreezerOverride) {
                    opt.setFreezerOverride(true);
                    Slog.d(TAG_AM, "Skipping freeze for process " + pid
                            + " " + name + " curAdj = " + proc.mState.getCurAdj()
                            + "(override)");
                    return;
                }

                if (pid == 0 || opt.isFrozen()) {
                    // Already frozen or not a real process, either one being
                    // launched or one being killed
                    return;
                }

                Slog.d(TAG_AM, "freezing " + pid + " " + name);

                // Freeze binder interface before the process, to flush any
                // transactions that might be pending.
                try {
                    if (freezeBinder(pid, true) != 0) {
                        rescheduleFreeze(proc, "outstanding txns");
                        return;
                    }
                } catch (RuntimeException e) {
                    Slog.e(TAG_AM, "Unable to freeze binder for " + pid + " " + name);
                    mFreezeHandler.post(() -> {
                        synchronized (mAm) {
                            proc.killLocked("Unable to freeze binder interface",
                                    ApplicationExitInfo.REASON_OTHER,
                                    ApplicationExitInfo.SUBREASON_FREEZER_BINDER_IOCTL, true);
                        }
                    });
                }

                long unfreezeTime = opt.getFreezeUnfreezeTime();

                try {
                    Process.setProcessFrozen(pid, proc.uid, true);

                    opt.setFreezeUnfreezeTime(SystemClock.uptimeMillis());
                    opt.setFrozen(true);
                } catch (Exception e) {
                    Slog.w(TAG_AM, "Unable to freeze " + pid + " " + name);
                }

                unfrozenDuration = opt.getFreezeUnfreezeTime() - unfreezeTime;
                frozen = opt.isFrozen();
            }

            if (!frozen) {
                return;
            }

            Slog.d(TAG_AM, "froze " + pid + " " + name);

            EventLog.writeEvent(EventLogTags.AM_FREEZE, pid, name);

            // See above for why we're not taking mPhenotypeFlagLock here
            if (mRandom.nextFloat() < mFreezerStatsdSampleRate) {
                FrameworkStatsLog.write(FrameworkStatsLog.APP_FREEZE_CHANGED,
                        FrameworkStatsLog.APP_FREEZE_CHANGED__ACTION__FREEZE_APP,
                        pid,
                        name,
                        unfrozenDuration);
            }

            try {
                // post-check to prevent races
                int freezeInfo = getBinderFreezeInfo(pid);

                if ((freezeInfo & TXNS_PENDING_WHILE_FROZEN) != 0) {
                    synchronized (mProcLock) {
                        rescheduleFreeze(proc, "new pending txns");
                    }
                    return;
                }
            } catch (RuntimeException e) {
                Slog.e(TAG_AM, "Unable to freeze binder for " + pid + " " + name);
                mFreezeHandler.post(() -> {
                    synchronized (mAm) {
                        proc.killLocked("Unable to freeze binder interface",
                                ApplicationExitInfo.REASON_OTHER,
                                ApplicationExitInfo.SUBREASON_FREEZER_BINDER_IOCTL, true);
                    }
                });
            }

            try {
                // post-check to prevent races
                if (mProcLocksReader.hasFileLocks(pid)) {
                    if (DEBUG_FREEZER) {
                        Slog.d(TAG_AM, name + " (" + pid + ") holds file locks, reverting freeze");
                    }
                    unfreezeAppLSP(proc);
                }
            } catch (Exception e) {
                Slog.e(TAG_AM, "Unable to check file locks for " + name + "(" + pid + "): " + e);
                unfreezeAppLSP(proc);
            }
        }

首先,被冻结的目标进程不能持有文件锁

services/core/java/com/android/server/am/CachedAppOptimizer.java

    try {
        // pre-check for locks to avoid unnecessary freeze/unfreeze operations
        if (mProcLocksReader.hasFileLocks(pid)) {
            if (DEBUG_FREEZER) {
                Slog.d(TAG_AM, name + " (" + pid + ") holds file locks, not freezing");
            }
            return;
        }
    } catch (Exception e) {
        Slog.e(TAG_AM, "Not freezing. Unable to check file locks for " + name + "(" + pid
                + "): " + e);
        return;
    }

在 Android 中,文件锁只能是需要应用主动查询,并没有强制约束作用的 Advisory Locking,它一般使用 java 层的 FileChannel 创建,方便多进程访问同一文件时的安排与配合。
文件锁可以通过 /proc/locks 进行查看,其中 fd 左边那列记录着持有锁的进程号:

OnePlus8T:/ # cat /proc/locks
1: POSIX  ADVISORY  READ  2048 fd:1c:2173 128 128
2: POSIX  ADVISORY  READ  2048 fd:1c:2168 1073741826 1073742335
3: POSIX  ADVISORY  READ  3512 fd:1c:2854 128 128
4: POSIX  ADVISORY  READ  4573 fd:1c:10117 128 128
5: POSIX  ADVISORY  READ  4573 fd:1c:10114 1073741826 1073742335
6: POSIX  ADVISORY  READ  4194 fd:1c:8732 128 128
7: POSIX  ADVISORY  READ  4194 fd:1c:8727 1073741826 1073742335
8: POSIX  ADVISORY  READ  4194 fd:1c:8647 1073741826 1073742335
9: POSIX  ADVISORY  READ  3512 fd:1c:2979 128 128
10: POSIX  ADVISORY  READ  3512 fd:1c:2970 1073741826 1073742335
11: POSIX  ADVISORY  WRITE 1560 fd:1c:396 0 EOF
12: POSIX  ADVISORY  READ  4573 fd:1c:10205 128 128
13: POSIX  ADVISORY  READ  4573 fd:1c:10201 1073741826 1073742335
14: POSIX  ADVISORY  READ  4194 fd:1c:8650 128 128
15: POSIX  ADVISORY  READ  3512 fd:1c:2851 1073741826 1073742335

至于为什么要跳过冻结持有文件锁的进程,大概是怕冻结后造成死锁吧。虽然 Advisory Locking 没有强制效力,但由于文件锁的使用者的密切配合,死锁并不是不能产生的。
这其实也教会了毒瘤应用们一招:随便找个文件上个锁就能轻松规避冻结。不过对于后台一堆服务的毒瘤应用,单是成为“已缓存的应用”就难以满足,压根就不需要走到这一步。

接下来,将会尝试对目标进程的 binder 接口尝试冻结:

services/core/java/com/android/server/am/CachedAppOptimizer.java

    try {
        if (freezeBinder(pid, true) != 0) {
            rescheduleFreeze(proc, "outstanding txns");
            return;
        }
    } catch (RuntimeException e) {
        Slog.e(TAG_AM, "Unable to freeze binder for " + pid + " " + name);
        mFreezeHandler.post(() -> {
            synchronized (mAm) {
                proc.killLocked("Unable to freeze binder interface",
                        ApplicationExitInfo.REASON_OTHER,
                        ApplicationExitInfo.SUBREASON_FREEZER_BINDER_IOCTL, true);
            }
        });
    }

调用 freezeBinder() 时会进入 native 层:

services/core/jni/com_android_server_am_CachedAppOptimizer.cpp

static jint com_android_server_am_CachedAppOptimizer_freezeBinder(
        JNIEnv *env, jobject clazz, jint pid, jboolean freeze) {

    jint retVal = IPCThreadState::freeze(pid, freeze, 100 /* timeout [ms] */);
    if (retVal != 0 && retVal != -EAGAIN) {
        jniThrowException(env, "java/lang/RuntimeException", "Unable to freeze/unfreeze binder");
    }

    return retVal;
}

并最终通过向 binder 设备发起 ioctl() 的方式进入内核态:

frameworks/native/libs/binder/IPCThreadState.cpp

status_t IPCThreadState::freeze(pid_t pid, bool enable, uint32_t timeout_ms) {
    struct binder_freeze_info info;
    int ret = 0;

    info.pid = pid;
    info.enable = enable;
    info.timeout_ms = timeout_ms;


#if defined(__ANDROID__)
    if (ioctl(self()->mProcess->mDriverFD, BINDER_FREEZE, &info) < 0)
        ret = -errno;
#endif

    //
    // ret==-EAGAIN indicates that transactions have not drained.
    // Call again to poll for completion.
    //
    return ret;
}

在内核中,驱动会尝试等待目标进程把未完成的 binder transaction 处理完,再将其设置为 “不再接收 binder transaction” 的状态:

/drivers/android/binder.c

static int binder_ioctl_freeze(struct binder_freeze_info *info,
                   struct binder_proc *target_proc)
{
    int ret = 0;

    if (!info->enable) {
        binder_inner_proc_lock(target_proc);
        target_proc->sync_recv = false;
        target_proc->async_recv = false;
        target_proc->is_frozen = false;
        binder_inner_proc_unlock(target_proc);
        return 0;
    }

    /*
     * Freezing the target. Prevent new transactions by
     * setting frozen state. If timeout specified, wait
     * for transactions to drain.
     */
    binder_inner_proc_lock(target_proc);
    target_proc->sync_recv = false;
    target_proc->async_recv = false;
    target_proc->is_frozen = true;
    binder_inner_proc_unlock(target_proc);

    if (info->timeout_ms > 0)
        ret = wait_event_interruptible_timeout(
            target_proc->freeze_wait,
            (!target_proc->outstanding_txns),
            msecs_to_jiffies(info->timeout_ms));

    /* Check pending transactions that wait for reply */
    if (ret >= 0) {
        binder_inner_proc_lock(target_proc);
        if (binder_txns_pending_ilocked(target_proc))
            ret = -EAGAIN;
        binder_inner_proc_unlock(target_proc);
    }

    if (ret < 0) {
        binder_inner_proc_lock(target_proc);
        target_proc->is_frozen = false;
        binder_inner_proc_unlock(target_proc);
    }

    return ret;
}

这个过程中发生的任何错误都将会逐级返回,最终回到 CachedAppOptimizer 供处理。
看起来错误被分为两类,一类是内核的 binder 驱动中,“等待目标进程把未完成的 binder transaction 处理完” 超时,返回 -EAGAIN ,此时上层会重新安排下一次冻结。
另一类是“其它错误”,此时上层将会直接杀死目标进程

在冻结 binder 接口后,真正的冻结进程本体才会开始(啊,终于终于开始了),它由 Process.setProcessFrozen() 进行,这个方法会进入 native 层,最终由 libprocessgroup 模块写入 cgroup,完成冻结。

在冻结完成后,还会有一些收尾工作,这里就不再展开了,主要包括:再检测一次是不是真的没有文件锁,如果文件锁突然出现了那就把目标解冻;再检测一次 binder 接口的冻结情况,如果出了问题那就再杀死进程。

于是,冻结的流程结束了,但有意思的事情还没有结束。

翻大车

对上面的冻结 binder 接口,什么时候会发生“其它错误”呢?在内核根本就不支持目标 ioctl 操作码 BINDER_FREEZE 的时候。
诶,内核难道不是只需要支持 cgroup v2 freezer 就够了吗?看到这里,很显然不是的。
假如内核不支持 cgroup v2 freezer,那么“暂停执行已缓存的应用”不会工作,也没有副作用。但是假如内核支持 cgroup v2 freezer 但不支持 binder freeze,那么“暂停执行已缓存的应用”将会变成隐形的后台杀手,凡是符合冻结要求的应用不但不会被冻结,还会被直接杀死。
(回去看看上面,这个功能的兼容性检查只检查了 cgroup v2 freezer ,并没有检测 binder freeze 的支持情况)

诶,别忘了我们上面说 cgroup v2 freezer 被反向移植到了 kernel/common 的 4.19 内核,并被合并到了高通平台的 4.19 内核上,那么 binder freeze 呢?很遗憾,binder freeze 并没有被移植。也就是说,上面说的那些十分“幸运”的,得到了移植的 4.19 内核设备,不仅没法成功冻结应用,还会加大杀后台的力度。笑拉了。

问题归根结底出在谷歌没有没有把这个功能需要的修改完整移植到 kernel/common 上,高通一合并,直接喜提受害者身份,不知道这一波会祸害多少设备。

对于这些超惨的 4.19 内核设备,你可以从 Pixel 5 的内核 处获得加入 binder freeze 支持的相关提交(嗯,没错,是私货,kernel/common 不给,自家 Pixel 倒是用上了)。

简单看了下,相关的提交应该有以下8个:

BACKPORT: FROMGIT: binder: fix freeze race
UPSTREAM: binder: add flag to clear buffer on txn complete
binder: don't unlock procs while scanning contexts
binder: don't log on EINTR
binder: freeze multiple contexts
binder: use EINTR for interrupted wait for work
binder: introduce the BINDER_GET_FROZEN_INFO ioctl
binder: implement BINDER_FREEZE ioctl

其实谷歌是尝试把这些补丁下放给 kernel/common 了,但是由于 android-4.19-stable 的新特性合并窗口已经关闭,然后又自己否决了。。。

哈?你不信谷歌会犯下这么愚蠢的错误?

那我们来看看日志,它甚至更加愚蠢:

D ActivityManager: freezing 6186 org.chromium.chrome
E ActivityManager: Unable to freeze binder for 6186 org.chromium.chrome
I binder  : 1245:1848 ioctl 400c620e 761c1be5e8 returned -22
I binder  : 1245:1848 ioctl c00c620f 761c1be5c8 returned -22
D ActivityManager: froze 6186 org.chromium.chrome

再来结合一下上面的代码:

    Slog.d(TAG_AM, "freezing " + pid + " " + name);

    // Freeze binder interface before the process, to flush any
    // transactions that might be pending.
    try {
        if (freezeBinder(pid, true) != 0) {
            rescheduleFreeze(proc, "outstanding txns");
            return;
        }
    } catch (RuntimeException e) {
        Slog.e(TAG_AM, "Unable to freeze binder for " + pid + " " + name);
        mFreezeHandler.post(() -> {
            synchronized (mAm) {
                proc.killLocked("Unable to freeze binder interface",
                        ApplicationExitInfo.REASON_OTHER,
                        ApplicationExitInfo.SUBREASON_FREEZER_BINDER_IOCTL, true);
            }
        });
    }

    long unfreezeTime = opt.getFreezeUnfreezeTime();

    try {
        Process.setProcessFrozen(pid, proc.uid, true);

        opt.setFreezeUnfreezeTime(SystemClock.uptimeMillis());
        opt.setFrozen(true);
    } catch (Exception e) {
        Slog.w(TAG_AM, "Unable to freeze " + pid + " " + name);
    }

    unfrozenDuration = opt.getFreezeUnfreezeTime() - unfreezeTime;
    frozen = opt.isFrozen();
}

if (!frozen) {
    return;
}

Slog.d(TAG_AM, "froze " + pid + " " + name);

你就会发现,诶,binder 确实发生错误了,进程确实是被 kill 了,但程序没有 return (它是异步的也没法 return),它接着往下跑,借着进程从被杀到死之间的一点时间,一路向下甚至打印出了 froze xxx。要知道,这条日志在正常情况下可代表着“冻结完成”,于是乎它就凭空给调试增加了许多难度,给人带来它正在正常工作的幻觉。

可笑可笑真是可笑。

总结

看到这里,相信你已经明白了“暂停执行已缓存的应用”和“墓碑后台”之间的关系了吧——可以说没有半点关系。这玩意儿,工作起来一半得看应用的意愿,冻住应用要经过一个又一个的挑战,最后能冻住的“毒瘤应用”,不能说非常少,只能说根本没有。它的设计初衷一直就是“在不破坏应用功能的前提下稍微省一点电”,而“成为毒瘤”正是“毒瘤应用”的功能,系统总不能为了它们,破坏自己的功能设计,自断双臂吧?

后记

慢慢写完这篇文章的时候 Android 13 已经发布了,看了看提交历史,谷歌 终于终于注意到了 上面 “翻大车” 的问题。但遗憾的是谷歌给出的修复是为那些巨惨的合并了 kernel/common 的 4.19 内核设备直接砍掉这个功能。相信一定有人会疑惑为什么升级到安卓 13 之后这个功能为啥在开发者选项里找不到了呢?究竟是该感到高兴还是悲伤呢?

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » “暂停执行已缓存的应用” 是如何工作的