Android LowmemoryKiller机制

为什么引入LowmemoryKiller?

进程的启动分冷启动和热启动,当用户退出某一个进程的时候,并不会真正的将进程退出,而是将这个进程放到后台,以便下次启动的时候可以马上启动起来,这个过程名为热启动,这也是Android的设计理念之一。这个机制会带来一个问题,每个进程都有自己独立的内存地址空间,随着应用打开数量的增多,系统已使用的内存越来越大,就很有可能导致系统内存不足。为了解决这个问题,系统引入LowmemoryKiller(简称lmk)管理所有进程,根据一定策略来kill某个进程并释放占用的内存,保证系统的正常运行。

LMK基本原理

所有应用进程都是从zygote孵化出来的,记录在AMS中mLruProcesses列表中,由AMS进行统一管理,AMS中会根据进程的状态更新进程对应的oom_adj值,这个值会通过文件传递到kernel中去,kernel有个低内存回收机制,在内存达到一定阀值时会触发清理oom_adj值高的进程腾出更多的内存空间,这就是Lowmemorykiller工作原理。

LMK基本实现方案

所以根据不同手机的配置,就有对应的杀进程标准,这个标准用minfree和adj两个文件来定义:
/sys/module/lowmemorykiller/parameters/minfree:里面是以","分割的一组数,每个数字代表一个内存级别。
/sys/module/lowmemorykiller/parameters/adj:对应上面的一组数,每个数组代表一个进程优先级级别

root@FG:/sys/module/lowmemorykiller/parameters # cat adj
0,58,117,176,529,1000
root@FG:/sys/module/lowmemorykiller/parameters # cat minfree
9216,12288,15360,18432,21504,25600
root@FG:/sys/module/lowmemorykiller/parameters #

minfree中数值的单位是内存中的页面数量,一般情况下一个页面是4KB,当内存低于25600的时候,系统会杀死adjj>=1000级别的进程,当内存低于21504的时候,系统会杀死adj>=529级别的进程。不同配置的机器这两个文件会有区别,把minfree文件中的值理解成五个水位线,而adj这个文件中的值与minfree文件中的数值一一对应,意味着到达什么样的水位线,杀死对应数值的进程。
对于应用进程来说,也需要有自身的adj,由AMS负责更新。定义在oom_adj和oom_score_adj文件中:
/proc/pid/oom_adj:代表当前进程的优先级,这个优先级是kernel中的优先级。
/proc/pid/oom_score_adj:这个是AMS上层的优先级,与ProcessList中的优先级对应

比如查看一下高德地图进程的adj值,如下

root@FG:/ # ps | grep amap
u0_a25 3024 289 1192920 155528 ffffffff f6e8b33c S com.autonavi.amapauto
u0_a25 3078 289 1026776 44824 ffffffff f6e8b33c S com.autonavi.amapauto:adiu
u0_a25 3093 289 1043776 58620 ffffffff f6e8b33c S com.autonavi.amapauto:locationservice
u0_a25 5603 289 1040044 55336 ffffffff f6e8b33c S com.autonavi.amapauto:push
root@FG:/ # cat proc/3024/oom_adj
0
root@FG:/ # cat proc/3024/oom_score_adj
0
root@FG:/ # cat proc/3024/oom_adj
7
root@FG:/ # cat proc/3024/oom_score_adj
411
root@FG:/ # cat proc/3024/oom_adj
9
root@FG:/ # cat proc/3024/oom_score_adj
529

当高德位于前台进程的时候oom_adj值为0,oom_score_adj值也是0
按home返回桌面时oom_adj为7,oom_score_adj为411
退出成为后台进程的时候,oom_adj值为9,oom_score_adj值是529

其实oom_adj与oom_score_adj这两个值是有换算关系的。

kernel/drivers/staging/android/lowmemorykiller.c
static short lowmem_oom_adj_to_oom_score_adj(short oom_adj)
{
 if (oom_adj == OOM_ADJUST_MAX)
     return OOM_SCORE_ADJ_MAX;
else
     return (oom_adj * OOM_SCORE_ADJ_MAX) / -OOM_DISABLE;
}

其中OOM_ADJUST_MAX=-15,OOM_SCORE_ADJ_MAX=1000,OOM_DISABLE=-17,那么换算就是:oom_score_adj=12*1000/17=700。高版本的内核都不在使用oom_adj,而是用oom_score_adj,oom_score_adj是一个向后兼容
综上总结一下LMK的基本原理,如下
在这里插入图片描述
用户在启动一个进程之后,通常伴随着启动一个Activity游览页面或者一个Service播放音乐等等,这个时候此进程的adj被AMS提高,LMK就不会杀死这个进程,当这个进程要做的事情做完了,退出后台了,此进程的adj很快又被AMS降低。当需要杀死一个进程释放内存时,一般先根据当前手机剩余内存的状态,在minfree节点中找到当前等级,再根据这个等级去adj节点中找到这个等级应该杀掉的进程的优先级, 之后遍历所有进程并比较进程优先级adj与优先级阈值,并杀死优先级低于阈值的进程,达到释放内存的目的。本文不讨论adj的计算,只讨论lmk原理。

LowmemoryKiller机制剖析

总的来说,Framework层通过调整adj的值和阈值数组,输送给kernel中的lmk,为lmk提供杀进程的原材料,因为用户空间和内核空间相互隔离,就采用了文件节点进行通讯,用socket将adj的值与阈值数组传给lmkd(5.0之后不在由AMS直接与lmk通信,引入lmkd守护进程),lmkd将这些值写到内核节点中。lmk通过读取这些节点,实现进程的kill,所以整个lmk机制大概可分成三层。
在这里插入图片描述

1. Framework层
AMS中与adj调整的有三个核心的方法,如下

AMS.updateConfiguration:更新窗口配置,这个过程中,分别向/sys/module/lowmemorykiller/parameters目录下的minfree和adj节点写入相应数值;
AMS.applyOomAdjLocked:应用adj,当需要杀掉目标进程则返回false;否则返回true,这个过程中,调用setOomAdj(),向/proc/pid/oom_score_adj写入oom_adj后直接返回;
AMS.cleanUpApplicationRecordLocked & AMS.handleAppDiedLocked:进程死亡后,调用remove(),直接返回;

- AMS.updateConfiguration

 public boolean updateConfiguration(Configuration values) {
       synchronized(this) {
           if (values == null && mWindowManager != null) {
               // sentinel: fetch the current configuration from the window manager
               values = mWindowManager.computeNewConfiguration(DEFAULT_DISPLAY);
           }

           if (mWindowManager != null) {
               // Update OOM levels based on display size.
               mProcessList.applyDisplaySize(mWindowManager);
           }
        .....
       }
   }

mProcessList是ProcessList对象,调用applyDisplaySize方法,基于屏幕尺寸,更新LMK的水位线

/frameworks/base/services/core/java/com/android/server/am/ProcessList.java
void applyDisplaySize(WindowManagerService wm) {
        if (!mHaveDisplaySize) {
            Point p = new Point();
            wm.getBaseDisplaySize(Display.DEFAULT_DISPLAY, p);
            if (p.x != 0 && p.y != 0) {
            //传入屏幕的尺寸
                updateOomLevels(p.x, p.y, true);
                mHaveDisplaySize = true;
            }
        }
    }

传入屏幕的尺寸更新水位线,逻辑很简单

private void updateOomLevels(int displayWidth, int displayHeight, boolean write) {
        ....
        
        // Scale buckets from avail memory: at 300MB we use the lowest values to
        // 700MB or more for the top values.
        float scaleMem = ((float)(mTotalMemMb-350))/(700-350);
        
		根据屏幕大小计算出scale
        // Scale buckets from screen size.
        int minSize = 480*800;  //  384000
        int maxSize = 1280*800; // 1024000  230400 870400  .264
        float scaleDisp = ((float)(displayWidth*displayHeight)-minSize)/(maxSize-minSize);
      ...

        float scale = scaleMem > scaleDisp ? scaleMem : scaleDisp;
        if (scale < 0) scale = 0;
        else if (scale > 1) scale = 1;
        int minfree_adj = Resources.getSystem().getInteger(
                com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAdjust);
        int minfree_abs = Resources.getSystem().getInteger(
                com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAbsolute);

        final boolean is64bit = Build.SUPPORTED_64_BIT_ABIS.length > 0;
         //通过下面的运算,将mOomMinFreeLow和mOomMinFreeHigh经过运算
         // 最后得出的 值存入mOomMinFree中,而如何计算这个值,是根据当前屏幕的分辨率和内存大小来
        for (int i=0; i<mOomAdj.length; i++) {
            int low = mOomMinFreeLow[i];
            int high = mOomMinFreeHigh[i];
            if (is64bit) {
                // Increase the high min-free levels for cached processes for 64-bit
                if (i == 4) high = (high*3)/2;
                else if (i == 5) high = (high*7)/4;
            }
            mOomMinFree[i] = (int)(low + ((high-low)*scale));
        }
    ....

        if (write) {
            ByteBuffer buf = ByteBuffer.allocate(4 * (2*mOomAdj.length + 1));
            buf.putInt(LMK_TARGET);
            for (int i=0; i<mOomAdj.length; i++) {
                buf.putInt((mOomMinFree[i]*1024)/PAGE_SIZE);//五个水位线
                buf.putInt(mOomAdj[i]);//与上面水位线对应的五个adj数值
            }
         //将AMS已经计算好的值通过socket发送到lmkd
            writeLmkd(buf);
            SystemProperties.set("sys.sysctl.extra_free_kbytes", Integer.toString(reserve));
        }
        // GB: 2048,3072,4096,6144,7168,8192
        // HC: 8192,10240,12288,14336,16384,20480
    }

这里携带的命令协议是LMK_TARGET,它对应到kernel里面执行的函数是cmd_target,要求kernel干的事情就是更新这两个文件

/sys/module/lowmemorykiller/parameters/minfree
/sys/module/lowmemorykiller/parameters/adj

这两个文件的作用我已经在开头说过了,我把minfree文件中的值理解成五个水位线,而adj这个文件中的值与minfree文件中的数值一一对应,意味着到达什么样的水位线,杀死对应数值的进程。而AMS里面就是通过调用applyDisplaySize方法,基于屏幕尺寸以及机器的CPU位数,更新LMK的水位线的。

  • AMS.applyOomAdjLocked
    这个方法的作用是应用adj,这个过程中,调用setOomAdj(),向/proc/pid/oom_score_adj写入oom_adj 后直接返回;系统中更新adj的操作很频繁,四大组件的生命周期都会影响着adj的值。而更新adj一般由applyOomAdjLocked完成。在看代码之前,在回温一下AMS中adj的定义,Android M与之后的adj定义有所区别。
    下图是Android M(6.0)中:
    Android M
    下图是Android M(6.0)之后
    在这里插入图片描述
    在这里插入图片描述

可以看到M之后的adj数值变大的,因为这样adj可以更加细化了,即使相同进程,不同任务栈的adj也可以不一样。从Android P开始,进一步细化ADJ级别,增加了VISIBLE_APP_LAYER_MAX(99),是指VISIBLE_APP_ADJ(100)跟PERCEPTIBLE_APP_ADJ(200)之间有99个槽,则可见级别ADJ的取值范围为[100,199]。 算法会根据其所在task的mLayerRank来调整其ADJ,100加上mLayerRank就等于目标ADJ,layer越大,则ADJ越小。

/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
private final boolean applyOomAdjLocked(ProcessRecord app,
            ProcessRecord TOP_APP, boolean doingAll, long now) {
        boolean success = true;
        if (app.curRawAdj != app.setRawAdj) {
            app.setRawAdj = app.curRawAdj;
        }
        int changes = 0;
        if (app.curAdj != app.setAdj) {
        //之前的adj不等于计算的adj,需要更新
            ProcessList.setOomAdj(app.pid, app.info.uid, app.curAdj);
            if (DEBUG_SWITCH || DEBUG_OOM_ADJ) Slog.v(
                TAG, "Set " + app.pid + " " + app.processName +
                " adj " + app.curAdj + ": " + app.adjType);
            app.setAdj = app.curAdj;
        }
        ....
}
public static final void setOomAdj(int pid, int uid, int amt) {
        if (amt == UNKNOWN_ADJ)
            return;
        long start = SystemClock.elapsedRealtime();
        ByteBuffer buf = ByteBuffer.allocate(4 * 4);
        buf.putInt(LMK_PROCPRIO);
        buf.putInt(pid);
        buf.putInt(uid);
        buf.putInt(amt);
        //将AMS已经计算好的adj值通过socket发送到lmkd
        writeLmkd(buf);
        long now = SystemClock.elapsedRealtime();
        if ((now-start) > 250) {
            Slog.w("ActivityManager", "SLOW OOM ADJ: " + (now-start) + "ms for pid " + pid
                    + " = " + amt);
        }
    }

这里携带的命令协议是LMK_PROCPRIO,对应kernel里面cmd_procprio函数,要求kernel干的事情是—把AMS发送过来的adj值更新到下面的文件中去。这样内存紧张的时候,LMK就会遍历内核中进程列表,杀死相应adj的进程了。

  • AMS.cleanUpApplicationRecordLocked & AMS.handleAppDiedLocked

进程死掉后,会调用该进程的ProcessList.remove方法,也会通过Socket通知lmkd更新adj。

public static final void remove(int pid) {
        ByteBuffer buf = ByteBuffer.allocate(4 * 2);
        buf.putInt(LMK_PROCREMOVE);
        buf.putInt(pid);
        writeLmkd(buf);
    }

这里携带的命令协议是LMK_PROCREMOVE,对应kernel里面的cmd_procremove函数,要求kernel干的事情是,当进程死亡了,删除/proc/下面的文件。

上面三大方法最后都是通过writeLmkd与lmkd通信,现在看看writeLmkd中怎么和lmkd通信的,首先需要打开与lmkd通信的socket,lmkd创建名称为lmkd的socket,节点位于/dev/socket/lmkd

rivate static boolean openLmkdSocket() {
        try {
            sLmkdSocket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET);
            sLmkdSocket.connect(
                new LocalSocketAddress("lmkd",
                        LocalSocketAddress.Namespace.RESERVED));
            sLmkdOutputStream = sLmkdSocket.getOutputStream();
        } catch (IOException ex) {
            Slog.w(ActivityManagerService.TAG,
                   "lowmemorykiller daemon socket open failed");
            sLmkdSocket = null;
            return false;
        }

        return true;
    }

当sLmkdSocket创建之后,就用它来发送数据到对端(lmkd)

总结

核心原理就是Framework层通过调整adj的值和阈值数组,输送给kernel中的lmk,为lmk提供杀进程的原材料。通过前面的分析AMS中给lmkd发送数据原材料有三个入口,携带的命令协议也有三种,如下。

功能AMS对应方法命令内核对应函数
LMK_PROCPRIOPL.setOomAdj()设置指定进程的优先级,也就是oom_score_adjcmd_procprio
LMK_TARGETPL.updateOomLevels()更新/sys/module/lowmemorykiller/parameters/中的minfree以及adjcmd_target
LMK_PROCREMOVEPL.remove()移除进程cmd_procremove
Android内存机制在不同版本之间有着较大的演变,其中最主要的就是内存管理策略的变化。在早期的Android版本中,内存管理采用的是进程优先级的方式,即根据进程的重要性和消耗的资源量来确定内存使用的权重,但这种方式容易导致内存不足的情况,进而导致应用程序崩溃。 后来,Google在Android 2.0中引入了LowMemoryKiller机制,通过监控系统内存使用情况,当内存不足时自动清理不必要的进程,以释放内存资源。LowMemoryKiller机制的实现是通过kernel的oom-killer机制来实现的,当系统内存不足时,通过oom_adj值来判断哪些进程可以杀掉,以释放内存资源。在Android 2.x中,LowMemoryKiller机制主要依赖于进程oom_adj值的设置,以及进程的重要性和消耗的资源量来判断哪些进程可以被杀掉。 随着Android版本的不断升级,Google也对LowMemoryKiller机制进行了多次优化,主要包括: 1. Android 3.0中引入了memcg机制,通过将进程的内存资源划分为多个cgroup,实现对内存资源的精细化管理。 2. Android 4.0中引入了Lmkd机制,通过对进程的内存资源进行动态调整,以更加精准地释放内存资源。 3. Android 4.4中引入了Zram机制,通过将一部分物理内存作为压缩内存使用,提高内存使用效率。 4. Android 6.0中引入了Doze机制,通过限制应用程序的后台运行,以降低系统内存负载。 总的来说,Android的内存管理机制是不断演变和优化的过程,不断追求更加高效和精细化的内存管理方式,以保证系统的稳定性和性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值