系统实现低内存下-进程保活-App保活-白名单- LowMemoryKiller保活机制

提示:实现App 保活-进程保活机制

文章目录


前言-需求-场景

需求

Android体系下,类似的保活需求、应用进程保活、应用进程不被杀死等需求,其实就是保活相关需求。更细一点的就是App中的服务不被杀死等,早年前特别是国产RK、全志等soc 平台在商显领域需求特别多,因为内存紧张、机器性能本身不高。为了稳健性,保活机制还是需要的。

比如如下 客户需求:系统在监测到/data/system/cmhi_permissions.xml 中 allow-app-alive-inlmkd 的应用后,直接将 app 添加到 LowMemoryKiller 白名单中,保证 app 不会在低内存时被 LowMemoryKiller 杀死进程

思路

这里思路一般有3种,都是实践中的解决方案:

  • 最low 的方案:系统服务中开一个定时检测机制,轮训检测App 在这里插入代码片进程是否还活着,如果死掉了就重启进程或者通知进程。
  • AMS 处理方案:让system_server进程针对对应包名把adj值强制调低,但是这样可能只能说大大提升自己不被杀概率。实际上是AMS来记录这个adj 值,低内存下会通知底层来处理kill 方案。
  • 直接在底层模块中来实现判断包名或者进程名,如果是白名单不去杀死不就行了吗。

分析:如上,第一种方案还是会杀死进程的,需要重启且不具备时效性;第二种方案知识大概率降低被杀死概率;第三种方案才是最优解,绝不杀死。

涉及到知识点

这里面涉及到知识点其实蛮多的,下面简要说明:

  • 分区概念,如上需求中涉及到 /data/system 目录下的文件,这个是data 分区,实现方案中一定是从已有分区拷贝文件到 data分区的
  • 属性概念:白名单数据一般都是以包名为主体,那么这些白名单放在哪里呢? 不会 直接读取文件数据吧,而且最终是需要守护进程c 代码中获取数据的,所以我们必须用属性来实现数据同步。
  • 在守护进程里面处理低内存拦截机制,那么必须具体 lmk-lowmemorykiller 知识点,即lmk 守护进程和lowmemorykiller机制

一、参考资料

Android 系统属性添加篇
RK-Android11-系统增加一个属性值
Framework 层属性机制Settings.System, Settings.Secure和Settings.Global存储及应用

系统拷贝文件到data分区-/data/system目录-实战拷贝资源到vendor分区

如何避免重要app进程被系统lmk-lowmemorykiller杀掉-实战源码

lmk内存压力测试工具mem-pressure源码剖析

二、lmk杀进程相关知识

这块知识整体比较复杂,不过总结一下主要就分为以下几个部分:

  • 1、systemserver进程的AMS,它的职责是:策略收集并更新各个app进程的adj值,并且把各个进程adj值通过socket传递给远端守护进程lmkd

  • 2、远端lmkd守护进程,这块其实分为两个新老版本阶段

    在老版本安卓kernel是有lmk模块,所以lmkd其实不负责任何的杀进程动作,只负责写入相关的进程adj值到字节节点

    新版本安卓kernel统一没有了lmk模块,这个时候lmkd就需要负责相关的检查内存,遍历进程,杀进程等业务

二、涉及到文件修改

修改的文件

java 复制代码
\device\mediatek\system\common\device.mk    预置资源到system 分区、属性定义
\system\memory\lmkd\lmkd.cpp                守护进程-实现白名单过滤匹配,实现白名单机制
\system\core\rootdir\init.rc                系统第一次启动时候,拷贝system 分区中预拷贝的资源文件到data分区
\frameworks\base\services\core\java\com\android\server\am\ActivityManagerService.java      AMS 读取配置资源,设置属性文件值

新增的文件

java 复制代码
\device\mediatek\system\common\fise\cmhi_permissions.xml           待 拷贝资源文件,存储源默认数据
\frameworks\base\services\core\java\com\android\server\utils\PermissionConfigParser.java     解析资源文件工具类,就是一个XML 工具

三、实现方案

1、配置文件-cmhi_permissions.xml

路径:\device\mediatek\system\common\fise\cmhi_permissions.xml ,其中 allow-app-alive-inlmkd 节点为守护 ap白名单

java 复制代码
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?>
<permissions>
    <allow-app-alive-inlmkd package="com.ss.android.ugc.live" />
    <allow-app-alive-inlmkd package="com.android.permissioncontroller" />
    <allow-app-alive-inlmkd package="com.android.webview:sandboxed_process0:org.chromium.content.app.SandboxedP" />
    <allow-app-alive-inlmkd package="com.qiyi.video.pad" />
    <allow-app-alive-inlmkd package="com.abupdate.fota_demo_iot" />
    <allow-app-alive-inlmkd package="com.sohu.inputmethod.sogou" />
    <allow-system-alert-window package="com.deling.launcher"/>
    <allow-system-alert-window package="aaaa.bbb.cccc"/>
    <allow-system-alert-window package="eee.fff.ddd"/>
    <allow-app-self-update package="com.mqqqqq.jj"/>
    <allow-app-self-update package="com.bo.test"/>
    <allow-app-self-update package="com.jijiji.qqqq"/>
</permissions>

2、资源拷贝-先拷贝到system分区-机器第一次开机再拷贝到data分区

强烈建议参考之前笔记 系统拷贝文件到data分区-/data/system目录-实战拷贝资源到vendor分区

拷贝资源到system分区- 配置属性key

路径:\device\mediatek\system\common\device.mk

配置拷贝资源脚本,如下:

java 复制代码
# modify by fangchen start whitelist
PRODUCT_COPY_FILES += $(LOCAL_PATH)/fise/cmhi_permissions.xml:$(TARGET_COPY_OUT_SYSTEM)/etc/cmhi_permissions.xml:mtk
# modify by fangchen end whitelist

persist.sys.allow_app_alive= \

机器第一次开机-从system分区拷贝资源到data分区

路径:\system\core\rootdir\init.rc ,脚本如下:

java 复制代码
on post-fs-data

    mark_post_data

    .......

    mkdir /data/system 0771 system system
    chmod 0771 /data/system
    chown system system /data/system
   
    copy /system/etc/cmhi_permissions.xml /data/system/cmhi_permissions.xml
    chmod 0644 /data/system/cmhi_permissions.xml
    chown system system /data/system/cmhi_permissions.xml

3、在AMS中读取配置文件-解析-写数据到属性中

解析xml 工具类-PermissionConfigParser

路径:\frameworks\base\services\core\java\com\android\server\utils\PermissionConfigParser.java

java 复制代码
package com.android.server.utils;

// PermissionConfigParser.java
//package com.android.server.pm;

import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;

import android.util.Log;
import android.util.Slog;

public class PermissionConfigParser {
    private static final String TAG = "PermissionConfigParser";
    private static final String CONFIG_FILE = "/data/system/cmhi_permissions.xml";
    
 
    public static ArrayList<String> parsePermissionPackages(String permissionTag) {
        ArrayList<String> packageList = new ArrayList<>();
        File configFile = new File(CONFIG_FILE);
        
        if (!configFile.exists()) {
            Slog.w(TAG, "Config file not exist: " + CONFIG_FILE);
            return packageList;
        }
        
        FileReader fileReader = null;
        try {
            fileReader = new FileReader(configFile);
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(fileReader);
            
            int eventType = parser.getEventType();
            while (eventType != XmlPullParser.END_DOCUMENT) {
                if (eventType == XmlPullParser.START_TAG) {
                    String tagName = parser.getName();
                    if (permissionTag.equals(tagName)) {
                        String packageName = parser.getAttributeValue(null, "package");
                        if (packageName != null && !packageName.isEmpty()) {
                            packageList.add(packageName);
                        }
                    }
                }
                eventType = parser.next();
            }
        } catch (Exception e) {
            Slog.e(TAG, "Failed to parse config file", e);
        } finally {
            if (fileReader != null) {
                try {
                    fileReader.close();
                } catch (Exception e) {
                    // ignore
					
                }
            }
        }
        
        return packageList;
    }
    
    
    public static ArrayList<String> getAllowAppAlivePackages() {
        return parsePermissionPackages("allow-app-alive-inlmkd");
    }
     
    public static ArrayList<String> getAllowAppAlertPackages() {
        return parsePermissionPackages("allow-system-alert-window");
    }
     
    public static ArrayList<String> getAllowAppUpdatePackages() {
        return parsePermissionPackages("allow-app-self-update");
    }
    
    
    public static String toPropertyString(ArrayList<String> packages) {
        if (packages == null || packages.isEmpty()) {
            return "";
        }
        
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < packages.size(); i++) {
            sb.append(packages.get(i));
            if (i < packages.size() - 1) {
                sb.append(",");
            }
        }
        return sb.toString();
    }
}

AMS中解析xml资源-设置白名单数据到属性中去

路径:\frameworks\base\services\core\java\com\android\server\am\ActivityManagerService.java

java 复制代码
    public ActivityManagerService(Context systemContext, ActivityTaskManagerService atm) {
        LockGuard.installLock(this, LockGuard.INDEX_ACTIVITY);
        mInjector = new Injector(systemContext);
        mContext = systemContext;
       
       .............
        initHjqPermissionsReceiver(mContext);   //新增解析方法
  }

	 private void parseWhiteListPermission() {
           Log.d(TAG,"========parseWhiteListPermission==========");
	        new Thread(() -> {
            try {
                    Log.d(TAG,"======deal Thread =====================");
                    ArrayList<String> alivePackages = PermissionConfigParser.getAllowAppAlivePackages();   //解析xml 获取到数据
                    String propAliveValue = PermissionConfigParser.toPropertyString(alivePackages);      //集合数据转String数据
                    SystemProperties.set("persist.sys.allow_app_alive", propAliveValue);                //设置白名单到属性中存储

                    ArrayList<String> alertPackages = PermissionConfigParser.getAllowAppAlertPackages();
                    String propAlertValue = PermissionConfigParser.toPropertyString(alertPackages);
                    SystemProperties.set("persist.sys.allow_app_alert", propAlertValue);


                    for (String packageName : alertPackages) {
                        Thread.sleep(100);
                        boolean isSuccess = AppOpsReflection.setSystemAlertWindowMode(
                                mContext,
                                packageName,
                                true
                        );

                        Log.d(TAG, packageName+"   alert window permissions is set result:"+isSuccess);
                    }
                    
                    ArrayList<String> updatePackages = PermissionConfigParser.getAllowAppUpdatePackages();
                    String propUpdateValue = PermissionConfigParser.toPropertyString(updatePackages);
                    SystemProperties.set("persist.sys.allow_app_update", propUpdateValue);



                     Log.d(TAG, "Set allow_app_alive property: " + propAliveValue);
                     Log.d(TAG, "persist.sys.allow_app_alert: " + propAlertValue);
                     Log.d(TAG, "persist.sys.allow_app_update: " + propUpdateValue);



            } catch (Exception e) {
                Thread.currentThread().interrupt();
                Log.e(TAG, "exception=============");
            }

        }).start(); 
		   
      
    }

4、在守护进程中根据白名单过滤-防止白名单应用被杀掉

路径:\system\memory\lmkd\lmkd.cpp

新增白名单方法

读取属性中去,然后判断当前 task_name 也就是进程名是不是白名单内部的值,决定是否拦截不被杀死。

java 复制代码
bool is_in_white_list(char* task_name) {
    char buf[500] = {};
     int len = property_get("persist.allow_app_alive_inlmkd.pkg", buf, "");
     if (len > 0) {
        if (strstr(buf,task_name)) {
            ALOGE("is_in_white_list buf %s contain of task_name %s", buf,task_name);
            return true;
        }
     }
    return false;
}

拦截机制

kill_one_process 方法中进行是否是白名单判断

java 复制代码
/* Kill one process specified by procp.  Returns the size (in pages) of the process killed */
static int kill_one_process(struct proc* procp, int min_oom_score, struct kill_info *ki,
                            union meminfo *mi, struct wakeup_info *wi, struct timespec *tm) {
    int pid = procp->pid;
    int pidfd = procp->pidfd;
    uid_t uid = procp->uid;
    char *taskname;
   
   .............................

    taskname = proc_get_name(pid, buf, sizeof(buf));
    // taskname will point inside buf, do not reuse buf onwards.
    if (!taskname) {
        goto out;
    }
	
	 ALOGE(" will kill_one_process   taskname==wfc= %s",taskname);
	// modify by fang chen start 
	 if (is_in_white_list(taskname)) {
        ALOGE(" not kill_one_process is_in_white_list  taskname %s",taskname);
        goto out;
     }
...................
}

四、低内存下-监控守护进程实验

就是复现低内存情况下看日志打印,是否可以拦截。可参考内存压测关联知识点 lmk内存压力测试工具mem-pressure源码剖析

压测方法:

  • 自己在源码中编译压测 工具类,push 到机器,然后进行压测
  • 下载安装大量消耗资源的应用,比如视频类、K歌类、游戏类 并安装各种apk,并逐步打开各个apk软件,达到消耗内存的目的。

会看到如下打印日志: 这些日志不就是我们在守护进程代码里面打印的日志吗? 说明实验是没有问题的。

java 复制代码
lowmemorykiller:  will kill_one_process   taskname==wfc= com.sohu.inputmethod.sogou:home
lowmemorykiller:  will kill_one_process   taskname==wfc= com.abupdate.fota_demo_iot
lowmemorykiller:  will kill_one_process   taskname==wfc= com.qiyi.video.pad:downloader
lowmemorykiller:  will kill_one_process   taskname==wfc= com.android.webview:sandboxed_process0:org.chromium.content.app.SandboxedProcessService0:0
lowmemorykiller:  will kill_one_process   taskname==wfc= com.android.permissioncontroller
lowmemorykiller:  will kill_one_process   taskname==wfc= com.ss.android.ugc.live

总结

  • app保活,其实是一个常规需求,但是很多情况下我们并没有在系统中真正实现保活,基本上都是死掉后重启机制实现,有一定的时效性和弊端
  • 这里实现保活其实涉及到很多知识点,这里只是通过需求把各个知识点串联起来了
  • 涉及到属性定义是基本需求、文件拷贝到对应分区的具体实现机制根据不同情况来具体实现,这里面主要是分区的知识点。
  • 守护进程才是真正实现需求,我们直接在守护进程里面完成白名单过滤拦截的。
相关推荐
凯文的内存8 个月前
Android14 app被冻结导致进程间通信失败
android·oomadjuster·activitymanager·freezapp·进程保活