Android进程系列第六篇---LowmemoryKiller机制分析(上)

一、内容预览

概要.png

二、概述

前面进程系列已经更新了五篇,本文(基于Android O源码),梳理LMK杀进程机制上篇,主要总结AMS和LowmemoryKiller通信的方式以及LowmemoryKiller的原理。
Android进程系列第一篇---进程基础
Android进程系列第二篇---Zygote进程的创建流程
Android进程系列第三篇---SystemServer进程的创建流程
Android进程系列第四篇---SystemServer进程的启动流程
Android进程系列第五篇---应用进程的创建流程

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

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

3、LMK基本实现方案
所以根据不同手机的配置,就有对应的杀进程标准,这个标准用minfree和adj两个文件来定义:

/sys/module/lowmemorykiller/parameters/minfree:里面是以","分割的一组数,每个数字代表一个内存级别。
/sys/module/lowmemorykiller/parameters/adj:对应上面的一组数,每个数组代表一个进程优先级级别

用小米note3举例:

wangjing@wangjing-OptiPlex-7050:~$ adb root
restarting adbd as root
wangjing@wangjing-OptiPlex-7050:~$ adb shell
jason:/ # cat /sys/module/lowmemorykiller/parameters/minfree
18432,23040,27648,32256,55296,80640
jason:/ # 
jason:/ # cat /sys/module/lowmemorykiller/parameters/adj
0,100,200,300,900,906
jason:/ # 

minfree中数值的单位是内存中的页面数量,一般情况下一个页面是4KB,当内存低于80640的时候,系统会杀死adjj>=906级别的进程,当内存低于55296的时候,系统会杀死adj>=900级别的进程。不同配置的机器这两个文件会有区别,我把minfree文件中的值理解成五个水位线,而adj这个文件中的值与minfree文件中的数值一一对应,意味着到达什么样的水位线,杀死对应数值的进程。

对于应用进程来说,也需要有自身的adj,由AMS负责更新。定义在oom_adj和oom_score_adj文件中:
/proc/pid/oom_adj:代表当前进程的优先级,这个优先级是kernel中的优先级。
/proc/pid/oom_score_adj:这个是AMS上层的优先级,与ProcessList中的优先级对应

比如查看一下头条进程的adj值,如下:

jason:/ # ps -ef |grep news                                                                                                                                                                                
u0_a159       7113  1119 8 15:21:12 ?     00:00:11 com.ss.android.article.news
u0_a159       7188  1119 0 15:21:12 ?     00:00:00 com.ss.android.article.news:ad
u0_a159       7299  1119 1 15:21:16 ?     00:00:02 com.ss.android.article.news:push
u0_a159       7384  1119 1 15:21:17 ?     00:00:00 com.ss.android.article.news:pushservice
root          7838  6429 3 15:23:35 pts/0 00:00:00 grep news
jason:/ # cat proc/7113/oom_adj                                                                                                                                                                            
0
jason:/ # cat proc/7113/oom_score_adj                                                                                                                                                                      
0
jason:/ # cat proc/7113/oom_adj                                                                                                                                                                            
12
jason:/ # cat proc/7113/oom_score_adj                                                                                                                                                                      
700
jason:/ # 

当头条位于前台进程的时候oom_adj值为0,oom_score_adj值也是0,当退出成为后台进程的时候,oom_adj值为12,oom_score_adj值是700。

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

kernel/drivers/staging/android/lowmemorykiller.c
271static short lowmem_oom_adj_to_oom_score_adj(short oom_adj)
272{
273 if (oom_adj == OOM_ADJUST_MAX)
274     return OOM_SCORE_ADJ_MAX;
275 else
276     return (oom_adj * OOM_SCORE_ADJ_MAX) / -OOM_DISABLE;
277}

其中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的基本原理,如下

LMK基本原理.png

用户在启动一个进程之后,通常伴随着启动一个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机制大概可分成三层。

LMK三层架构.png
3.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(),直接返回;

3.1.1、 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
198    void applyDisplaySize(WindowManagerService wm) {
199        if (!mHaveDisplaySize) {
200            Point p = new Point();
201            // TODO(multi-display): Compute based on sum of all connected displays' resolutions.
202            wm.getBaseDisplaySize(Display.DEFAULT_DISPLAY, p);
203            if (p.x != 0 && p.y != 0) {
                     //传入屏幕的尺寸
204                updateOomLevels(p.x, p.y, true);
205                mHaveDisplaySize = true;
206            }
207        }
208    }

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

210    private void updateOomLevels(int displayWidth, int displayHeight, boolean write) {
211        // Scale buckets from avail memory: at 300MB we use the lowest values to
212        // 700MB or more for the top values.
213        float scaleMem = ((float)(mTotalMemMb-350))/(700-350);
214
215        //根据屏幕大小计算出scale
216        int minSize = 480*800;  //  384000
217        int maxSize = 1280*800; // 1024000  230400 870400  .264
218        float scaleDisp = ((float)(displayWidth*displayHeight)-minSize)/(maxSize-minSize);
            //google代码就是这么写的,表示不好评价了
219        if (false) {
220            Slog.i("XXXXXX", "scaleMem=" + scaleMem);
221            Slog.i("XXXXXX", "scaleDisp=" + scaleDisp + " dw=" + displayWidth
222                    + " dh=" + displayHeight);
223        }
224
225        float scale = scaleMem > scaleDisp ? scaleMem : scaleDisp;
226        if (scale < 0) scale = 0;
227        else if (scale > 1) scale = 1;
228        int minfree_adj = Resources.getSystem().getInteger(
229                com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAdjust);
230        int minfree_abs = Resources.getSystem().getInteger(
231                com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAbsolute);
232        if (false) {
233            Slog.i("XXXXXX", "minfree_adj=" + minfree_adj + " minfree_abs=" + minfree_abs);
234        }
235
236        final boolean is64bit = Build.SUPPORTED_64_BIT_ABIS.length > 0;
237       //通过下面的运算,将mOomMinFreeLow和mOomMinFreeHigh经过运算
         // 最后得出的 值存入mOomMinFree中,而如何计算这个值,是根据当前屏幕的分辨率和内存大小来
238        for (int i=0; i<mOomAdj.length; i++) {
239            int low = mOomMinFreeLow[i];
240            int high = mOomMinFreeHigh[i];
241            if (is64bit) {
242                // 64-bit机器会high增大
243                if (i == 4) high = (high*3)/2;
244                else if (i == 5) high = (high*7)/4;
245            }
246            mOomMinFree[i] = (int)(low + ((high-low)*scale));
247        }
              .......
287
288        if (write) {
289            ByteBuffer buf = ByteBuffer.allocate(4 * (2*mOomAdj.length + 1));
290            buf.putInt(LMK_TARGET);
291            for (int i=0; i<mOomAdj.length; i++) {
292                buf.putInt((mOomMinFree[i]*1024)/PAGE_SIZE);//五个水位线
293                buf.putInt(mOomAdj[i]);//与上面水位线对应的五个adj数值
294            }
295            //将AMS已经计算好的值通过socket发送到lmkd
296            writeLmkd(buf);
297            SystemProperties.set("sys.sysctl.extra_free_kbytes", Integer.toString(reserve));
298        }
299        // GB: 2048,3072,4096,6144,7168,8192
300        // HC: 8192,10240,12288,14336,16384,20480
301    }

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

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

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

3.1.2、 AMS.applyOomAdjLocked

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

Android M
Android M之后

可以看到M之后的adj数值变大的,为什么呢


image.png

因为这样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越小。

再次科普一下,我们可以用下面的两个办法随时查看adj的值 。

1、cat proc/<pid>/oom_score_adj
2、adb shell dumpsys activity o/p

好了,现在来看AMS调用applyOomAdjLocked更新adj。

/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
22000    private final boolean applyOomAdjLocked(ProcessRecord app, boolean doingAll, long now, long nowElapsed) {
             .........
22009
22010        if (app.curAdj != app.setAdj) {
                    //之前的adj不等于计算的adj,需要更新
22011            ProcessList.setOomAdj(app.pid, app.uid, app.curAdj);
22012            if (DEBUG_SWITCH || DEBUG_OOM_ADJ || mCurOomAdjUid == app.info.uid) {
22013                String msg = "Set " + app.pid + " " + app.processName + " adj "
22014                        + app.curAdj + ": " + app.adjType;
22015                reportOomAdjMessageLocked(TAG_OOM_ADJ, msg);
22016            }
22017            app.setAdj = app.curAdj;
22018            app.verifiedAdj = ProcessList.INVALID_ADJ;
22019        }
                .........
22020
}
/frameworks/base/services/core/java/com/android/server/am/ProcessList.java
630    public static final void setOomAdj(int pid, int uid, int amt) {
631        if (amt == UNKNOWN_ADJ)
632            return;
633
634        long start = SystemClock.elapsedRealtime();
635        ByteBuffer buf = ByteBuffer.allocate(4 * 4);
636        buf.putInt(LMK_PROCPRIO);
637        buf.putInt(pid);
638        buf.putInt(uid);
639        buf.putInt(amt);
              //将AMS已经计算好的adj值通过socket发送到lmkd
640        writeLmkd(buf);
641        long now = SystemClock.elapsedRealtime();
642        if ((now-start) > 250) {
643            Slog.w("ActivityManager", "SLOW OOM ADJ: " + (now-start) + "ms for pid " + pid
644                    + " = " + amt);
645        }
646    }

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

3.1.3、 AMS.cleanUpApplicationRecordLocked & AMS.handleAppDiedLocked

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

/frameworks/base/services/core/java/com/android/server/am/ProcessList.java
651    public static final void remove(int pid) {
652        ByteBuffer buf = ByteBuffer.allocate(4 * 2);
653        buf.putInt(LMK_PROCREMOVE);
654        buf.putInt(pid);
655        writeLmkd(buf);
656    }

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

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

658    private static boolean openLmkdSocket() {
659        try {
660            sLmkdSocket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET);
661            sLmkdSocket.connect(
662                new LocalSocketAddress("lmkd",
663                        LocalSocketAddress.Namespace.RESERVED));
664            sLmkdOutputStream = sLmkdSocket.getOutputStream();
665        } catch (IOException ex) {
666            Slog.w(TAG, "lowmemorykiller daemon socket open failed");
667            sLmkdSocket = null;
668            return false;
669        }
670
671        return true;
672    }

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

674    private static void writeLmkd(ByteBuffer buf) {
675       //尝试三次
676        for (int i = 0; i < 3; i++) {
677            if (sLmkdSocket == null) {
678                    if (openLmkdSocket() == false) {
679                        try {
680                            Thread.sleep(1000);
681                        } catch (InterruptedException ie) {
682                        }
683                        continue;
684                    }
685            }
686
687            try {
688                sLmkdOutputStream.write(buf.array(), 0, buf.position());
689                return;
690            } catch (IOException ex) {
691                Slog.w(TAG, "Error writing to lowmemorykiller socket");
692
693                try {
694                    sLmkdSocket.close();
695                } catch (IOException ex2) {
696                }
697
698                sLmkdSocket = null;
699            }
700        }
701    }
702}

四、总结

这篇文章主要是总结lmk的初步的工作原理,如何为系统的资源保驾护航。核心原理就是Framework层通过调整adj的值和阈值数组,输送给kernel中的lmk,为lmk提供杀进程的原材料。通过前面的分析AMS中给lmkd发送数据原材料有三个入口,携带的命令协议也有三种,如下。

功能 AMS对应方法 命令 内核对应函数
LMK_PROCPRIO PL.setOomAdj() 设置指定进程的优先级,也就是oom_score_adj cmd_procprio
LMK_TARGET PL.updateOomLevels() 更新/sys/module/lowmemorykiller/parameters/中的minfree以及adj cmd_target
LMK_PROCREMOVE PL.remove() 移除进程 cmd_procremove

关于kenel中的工作流程,下面一篇分解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270