MIUI13冻结机制分析


对MIUI13冻结机制-MILLET的分析

1.起因

上一段时间,我对天天在后台耗电的QQ动起了心思。首先我是想着通过QQ的开源框架来登录QQ并通过钉钉钉webhook机器人来及时推送消息。但用了一段时间后,觉得太过于麻烦,又看到有人弄了支持mipush的webhook消息推送,几乎做到了无后台推送,我也想着转到mipsuh,但mipush个人接入是没有办法的,放弃了。最后,我看到QQ接入了华为的推送,遂伪装机型来使用推送。实际上挺及时的,但一来消息就拉起QQ那魔鬼般的耗电进程,我就搞了个xposed模块,只接收消息,不拉起QQ。用久了发现啊,我只要一熄屏,过一会消息就不会立马发送过来,甚至不发送,直到我打开手机屏,那消息就立马弹出来,我以为是QQ在搞魔法,但分析QQ的代码确没有任何结果。直到最近几天看到了火起来的“墓碑”模块,通过cgroup freezer v1或v2来冻结进程,我想着可不可能是哪个进程被冻结了?

2.cgroup以及MIllet

2.1 概述

Millet实质上是通过cgroup对进程进行冻结,其中,framework提供服务给电池和性能,电池和性能处理各种事件,并发出冻结请求来冻结进程以达到省电的效果。下面是分析。

2.2 MIUI中cgroup的挂载情况。

apollo的MIUI13挂载的cgroup,关于cgroup v1的资料可以参考

简单来说就是通过创建文件夹定义一些逻辑组,通过配置文件设置对应的控制器,通过write写入进程号到特殊文件来把一个进程分到一个组,把进程分到这个组后就会收到组所定义的限制。

# 看下挂载了哪些cgroup
mount | grep cgroup

none on /dev/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
none on /dev/cg2_bpf type cgroup2 (rw,nosuid,nodev,noexec,relatime)
none on /dev/cpuctl type cgroup (rw,nosuid,nodev,noexec,relatime,cpu)
none on /dev/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset,noprefix,release_agent=/sbin/cpuset_release_agent)
none on /dev/stune type cgroup (rw,nosuid,nodev,noexec,relatime,schedtune)
none on /sys/fs/cgroup type tmpfs (rw,seclabel,relatime,mode=750,gid=1000)
none on /sys/fs/cgroup/freezer type cgroup (rw,relatime,freezer)

看一下目录结构

# 冻结的关键freezer,看看有哪些group
cd /sys/fs/cgroup/freezer
find .

.
./cgroup.procs
./cgroup.sane_behavior
./perf
./perf/cgroup.procs
./perf/thawed
./perf/thawed/cgroup.procs
./perf/thawed/freezer.self_freezing
./perf/thawed/tasks
./perf/thawed/freezer.parent_freezing
./perf/thawed/freezer.state
./perf/thawed/notify_on_release
./perf/thawed/cgroup.clone_children
./perf/freezer.self_freezing
./perf/tasks
./perf/freezer.parent_freezing
./perf/freezer.state
./perf/frozen
./perf/frozen/cgroup.procs
./perf/frozen/freezer.self_freezing
./perf/frozen/tasks
./perf/frozen/freezer.parent_freezing
./perf/frozen/freezer.state
./perf/frozen/notify_on_release
./perf/frozen/cgroup.clone_children
./perf/notify_on_release
./perf/cgroup.clone_children
./tasks
./notify_on_release
./release_agent
./cgroup.clone_children

cat ./perf/frozen/freezer.stat
FROZEN
cat ./perf/thawed/freezer.state
THAWED

大体来看,perf下面定义了俩个组,一个是frozen一个是thawed,对应冻结和解冻。只要把进程划分到对应组就能实现冻结和解冻。这也是所谓墓碑机制的核心。

2.3 Millet的发现

实际上让我发现millet的是这一串警告。

起初我以为是MIUI的bug,这警告意味着cgroup的冻结会完全不可用。其实只是因为cgroup挂载时间太晚了。

我搜了下system目录,这个init.milletmonitor.rc很明显就是挂载cgroup的rc

看看rc的内容

apollo:/system/etc # cat ./init/init.milletmonitor.rc
on property:sys.boot_completed=1
    #cgroup v1 freezer sys/fs/cgroup entries
    mount tmpfs none /sys/fs/cgroup mode=0750,uid=0,gid=1000
    mkdir /sys/fs/cgroup/freezer 0750 root system
    mount cgroup none /sys/fs/cgroup/freezer freezer
    # Xiaomi: Create cgroup in freezer
    mkdir /sys/fs/cgroup/freezer/perf 0750 root system
    mkdir /sys/fs/cgroup/freezer/perf/frozen 0750 root system
    write /sys/fs/cgroup/freezer/perf/frozen/freezer.state FROZEN
    chown root system /sys/fs/cgroup/freezer/perf/frozen/cgroup.procs
    chmod 0660 /sys/fs/cgroup/freezer/perf/frozen/cgroup.procs
    chown root system /sys/fs/cgroup/freezer/perf/frozen/tasks
    chmod 0660 /sys/fs/cgroup/freezer/perf/frozen/tasks
    mkdir /sys/fs/cgroup/freezer/perf/thawed 0750 root system
    chown root system /sys/fs/cgroup/freezer/perf/thawed/cgroup.procs
    chmod 0660 /sys/fs/cgroup/freezer/perf/thawed/cgroup.procs
    chown root system /sys/fs/cgroup/freezer/perf/thawed/tasks
    chmod 0660 /sys/fs/cgroup/freezer/perf/thawed/tasks

    start millet_sig
    start millet_binder
    start millet_pkg
    mkdir /sys/fs/cgroup/frozen/
    chown system system /sys/fs/cgroup/frozen/cgroup.procs
    chown system system /sys/fs/cgroup/frozen/cgroup.freeze
    write /sys/fs/cgroup/frozen/cgroup.freeze 1
    mkdir /sys/fs/cgroup/unfrozen/
    chown system system /sys/fs/cgroup/unfrozen/cgroup.procs
    chown system system /sys/fs/cgroup/unfrozen/cgroup.freeze

这个挂载时机是 boot_completed,实际上这时framework已经初始化完成了,所以那个报错太正常了。

着手分析,直接把framework拉到jadx里看看(总不会框架都要混淆吧)

getFrozenPids就是那个报错的方法了。

往下看看有啥其他方法发现。很明显,freezePid就是向cgroup写入进程的pid(v1的方法)

嗯,这也是个很好的hook点,于是经过一番frida 的hook

Java.perform(function () {
    let FreezeUtils = Java.use("com.miui.server.greeze.FreezeUtils");
    FreezeUtils.DEBUG.value = true
    // FreezeUtils.getFrozenPids.implementation = function () {
    //     console.log('getFrozenPids is called');
    //     let ret = this.getFrozenPids();
    //     console.log('getFrozenPids ret value is ' + ret);
    //     return ret;
    // };
    function printstack() {
        console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
    }
    // FreezeUtils.freezePid(8524)
    FreezeUtils.freezePid.overload('int', 'int').implementation = function (pid, uid) {
        console.log(`freezePid(${pid},${uid})`);
        let ret = this.freezePid(pid, uid);
        console.log('freezePid ret value is ' + ret);
        return ret;
    };
    FreezeUtils.freezePid.overload('int').implementation = function (pid) {
        console.log(`freezePid(${pid})`);
        printstack();
        let ret = this.freezePid(pid);
        console.log('freezePid ret value is ' + ret);
        return ret;
    };
    let Stub = Java.use("miui.greeze.IGreezeManager$Stub");
    Stub.onTransact.implementation = function(code, data, reply, flags){
        console.log('get Ibinder!');
        let ret = this.onTransact(code, data, reply, flags);
      // 发现是服务时直接getCallingUid和Pid拿到pid和uid
        console.log("caller_uid:"+this.getCallingUid()+"pid"+this.getCallingPid())
        // console.log('onTransact ret value is ' + ret);
        return ret;
    };
    // let GreezeManagerService = Java.use("com.miui.server.greeze.GreezeManagerService");
    // GreezeManagerService.freezeUids.implementation = function (uids, timeout, fromWho, reason, checkAudioGps) {
    //     console.log('freezeUids is called');
    //     let ret = this.freezeUids(uids, timeout, fromWho, reason, checkAudioGps);
    //     console.log('freezeUids ret value is ' + ret);
    //     return ret;
    // };
});

这是典型的服务调用,直接找出了调用栈,通过iBinder并找出了谁在调用。

freezePid(11516)
java.lang.Throwable
        at com.miui.server.greeze.FreezeUtils.freezePid(Native Method)
        at com.miui.server.greeze.GreezeManagerService.freezeProcess(GreezeManagerService.java:594)
        at com.miui.server.greeze.GreezeManagerService.freezeUids(GreezeManagerService.java:705)
        at miui.greeze.IGreezeManager$Stub.onTransact(IGreezeManager.java:244)
        at miui.greeze.IGreezeManager$Stub.onTransact(Native Method)
        at android.os.Binder.execTransactInternal(Binder.java:1182)
        at android.os.Binder.execTransact(Binder.java:1146)

freezePid ret value is true
caller_uid:1000 pid: 5393

这个是谁呢,当之无愧的电池和性能(com.miui.poerkeeper)

着手下一步hook,目标电池与性能

Java.perform(function () {
    function printstack() {
        console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
    }
    let FreezeBinder = Java.use("com.miui.powerkeeper.millet.FreezeBinder");
    FreezeBinder.freezeUids.implementation = function (a, b, c, d) {
        console.log('freezeUids is called');
        printstack()
        let ret = this.freezeUids(a, b, c, d);
        // console.log('freezeUids ret value is ' + ret);
        return ret;
    };
    // let MilletHandler = Java.use("com.miui.powerkeeper.millet.MilletHandler");
    // MilletHandler.handleMessage.implementation = function(message){
    //     console.log('handleMessage is called');
    //     console.log('message.what='+message.what)
    //     let ret = this.handleMessage(message);

    //     // console.log('handleMessage ret value is ' + ret);
    //     return ret;
    // };
    // let MilletPolicy = Java.use("com.miui.powerkeeper.millet.MilletPolicy");
    // let MilletUidObserver = Java.use("com.miui.powerkeeper.millet.MilletUidObserver");
    // MilletPolicy.isAllowFreeze.implementation = function(i2){
    //     // console.log('isAllowFreeze is called');
    //     let ret = this.isAllowFreeze(i2);
    //     let PkgName = MilletUidObserver.getPkgNameByUid(i2)
    //     console.log(PkgName +": "+ ret);
    //     if(PkgName == 'com.huawei.hwid'){
    //         console.log("不冻结")
    //         return false;
    //     }
    //     // console.log(PkgName)
    //     return ret;
    // };
    // MilletUidObserver.getPkgNameByUid.implementation = function(i2){
    //     console.log('getPkgNameByUid is called');
    //     let ret = this.getPkgNameByUid(i2);
    //     console.log('getPkgNameByUid ret value is ' + ret);
    //     return ret;
    // };
});

继续找出调用服务冻结的方法

java.lang.Throwable
        at com.miui.powerkeeper.millet.FreezeBinder.freezeUids(Native Method)
        at com.miui.powerkeeper.millet.MilletHandler.frozen(Unknown Source:109)
        at com.miui.powerkeeper.millet.MilletHandler.handleMessage(Unknown Source:134)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loopOnce(Looper.java:210)
        at android.os.Looper.loop(Looper.java:299)
        at android.os.HandlerThread.run(HandlerThread.java:67)

嗯,就是这个方法了。

下面这个方法负责冻结

com.miui.powerkeeper.millet.MilletHandler.frozen

下面这个方法则负责接收一些事件,比如熄屏等。

com.miui.powerkeeper.millet.MilletHandler.handleMessage

3. 给华为推送添加一个白名单

上述冻结的效果是什么呢,就是没有在白名单的进程在锁屏后会被冻结。华为推送因此也会被冻结(就算在设置里关闭所有的限制)。

com.miui.powerkeeper.millet.MilletHandler.frozen 方法负责冻结,并且会调用com.miui.powerkeeper.millet.MilletPolicy.isAllowFreeze(uid)来决定是否冻结

思路很简单,

Hook com.miui.powerkeeper.millet.MilletPolicy.isAllowFreeze,如果uid是华为推送的uid就直接返回false。

借用com.miui.powerkeeper.millet.MilletUidObserver来通过uid获取包名,然后判断即可

Java.perform(function () {
    let MilletPolicy = Java.use("com.miui.powerkeeper.millet.MilletPolicy");
    let MilletUidObserver = Java.use("com.miui.powerkeeper.millet.MilletUidObserver");
    MilletPolicy.isAllowFreeze.implementation = function(i2){
        // console.log('isAllowFreeze is called');
        let ret = this.isAllowFreeze(i2);
        let PkgName = MilletUidObserver.getPkgNameByUid(i2)
        console.log(PkgName +": "+ ret);
        if(PkgName == 'com.huawei.hwid'){
            console.log("不冻结")
            return false;
        }
        // console.log(PkgName)
        return ret;
    };
});

4.hook的效果

之前,熄灭屏幕就被冻结的华为推送

hook之后,那个男人他没被冻结了!!我能及时收到QQ消息啦!!

5.总结

小米的确是做了很多比较好的设计的,比如这个millet机制,但用户缺少对其完全对控制权力。而且这个millet机制完全可以让app一到后台就被冻结的,但miui没有这么做,而且MIUI既然存在了这样一个机制了,安卓原生的暂停执行已缓存的进程开关还有必要吗?或者那个开关还有用?。最近的兴起的墓碑模块,本质上就是暂停不在前台的进程的执行,要么通过kill发送signal,要么是自己控制cgroup冻结的进程,但如果用在MIUI上的话,可能会因为和系统的冻结产生冲突而造成性能异常。


文章作者: f19
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 f19 !
  目录