Android Low memory killer

2018/07/31 AOSP android-8.1.0_r31

Android尽可能多的缓存进程,当用户下次再去使用该进程时,就会直接使用缓存的进程,从而避免了开启进程这样的消耗,提高响应速度。

但是随着Android缓存的进程越来越多,系统内存就会越来越少。所以Android又会去杀掉一些缓存的进程来释放一些内存, 而这就是low memory killer.

可以看出,Android缓存的进程数量,与触发Low memory killer是相关的,
当缓存/empty的进程数量,就会触发android framework层面的低杀
或使用的内存超过一个阈值时,就会触发lmkd或kernel里的low memory killer.

本文主要分析怎样计算一个进程的oom adj, 然后android framework对缓存的进程进行低杀,接着会去lmkd以及kernel里继续分析low memory killer.

一 计算 oom adj

Android Framework计算一个进程的 oom adj 是在computeOomAdjLocked函数中进行的,由于该函数很庞大,所以就不贴代码了。大体把流程图画出来了,

computeOomAdjLocked
service_promotion
provider_promotion
system_adj

上图是系统定义的adj level, 其中

  • system和persistent进程
    这类进程的 adj < 0, computeOomAdjLocked并不会调整这类进程的adj, 也就意味着,这些进程很重要,不到万不得已的情况下是不会kill这些进程的

  • 前端进程
    这类进程包括TOP APP, 也包括那些正在接收广播,正在进行service callback的进程,以及在run instrumentation的进程.

  • Visible 进程
    visible进程并不一定是前端进程, 它是指app中有些activity是可见的,比如被TOP app覆盖后,依然可见的那些进程

  • 用户可感知的进程
    这些进程包括那些 activity 状态为 PAUSING/PAUSED/STOPPING的进程, 以及有 overlay、前端service的进程.
    以及被设置成forcingToImportant的进程,这类进程主要正在显示Toast的进程

Toast.png
  • previous进程
    这里特别注意,previous进程把HOME进程排除了,也就是如果从launcher启动一个APP, 那么照理说previous进程应该是HOME进程才对,但是AMS在更新previous进程时,将HOME进程排除在外了,所以HOME进程永远不会是previous进程
void updatePreviousProcessLocked(ActivityRecord r) {
        // ...
        // Now set this one as the previous process, only if that really
        // makes sense to. 排除了home进程
        if (r.app != null && fgApp != null && r.app != fgApp
                && r.lastVisibleTime > mService.mPreviousProcessVisibleTime
                && r.app != mService.mHomeProcess) {
            mService.mPreviousProcess = r.app;
            mService.mPreviousProcessVisibleTime = r.lastVisibleTime;
        }
    }

其它的进程就参见上面的表格吧!

另外,一个进程里的 services 或 provider是有可能会提升该进程的oom adj
比如一个oomadj更小的进程(也就是更重要的进程)正绑定了oomadj更大的进程,且在绑定的时候允许对host service的adj进行promotion, 此时,hosting service的进程就有可能会被promote到与client进程相同的oomadj, 也就是说,如果host service进程的oomadj太大了,那它可能会被kill掉,而此时更重要的client进程还绑定在该service上,所以这时就有可能出现混乱。对于provider同理。

引用AOSP对于foreground App的定义, https://developer.android.com/about/versions/oreo/background#services

An app is considered to be in the foreground if any of the following is true:
*   It has a visible activity, whether the activity is started or paused.
*   It has a foreground service.
*   Another foreground app is connected to the app, either by binding to one of its services or by making 
    use of one of its content providers. For example, the app is in the foreground if another app binds to its:
    *   [IME](https://developer.android.com/guide/topics/text/creating-input-method.html)
    *   Wallpaper service
    *   Notification listener
    *   Voice or text service

二 native与kernel oomadj

applyOOMadj
lmkd

当算出了一个进程的 oomadj 后,就会往lkmd里去更新该oomadj, 前提是最新的与上一次的oomadj不一样的时候。
最终是将oomadj写入到 /proc/%d/oom_score_adj 中.

Android中进行low memory killer根据一些配置有可能发生在lmkd中,也有可能发生在kernel里
具体是

#define INKERNEL_MINFREE_PATH "/sys/module/lowmemorykiller/parameters/minfree"
has_inkernel_module = !access(INKERNEL_MINFREE_PATH, W_OK);
use_inkernel_interface = has_inkernel_module && !is_go_device;

如果不能访问 "/sys/module/lowmemorykiller/parameters/minfree", 且不是 go device.在这种情况下,使用kernel的 low memory killer, 否则使用lmkd的 find_and_kill_process

2.1 lmkd的find_and_kill_process

如果使用的是lmkd的查杀功能,那么它进行如下的步骤进行操作

  • 监听pressure_level
    将/dev/memcg/memory.pressure_level的fd以及它所监听的level水平,如critical或medium写入到/dev/memcg/cgroup.event_control, 当有事件发生了,便会触发mp_event或mp_event_critical, 最终都会调用mp_event_common

  • 计算内存使用情况
    /dev/memcg/memory.usage_in_bytes
    /dev/memcg/memory.memsw.usage_in_bytes
    获得系统内存以及swap内存的使用情况,决定是否触发find_and_kill_process去查找进程并kill

  • find_and_kill_process
    find_and_kill_process就比较简单了,从oomadj最高往下依次查找,如果是critical,则查找到ro.lmk.critical 默认是0; 如果是medium,则查找到ro.lmk.medium, 默认是800;
    找到一个就kill掉,然后就不往下查找了。测试find_and_kill_process只会杀一个进程,如果在kill掉一个进程后,内存还是不满足,则kernel会触发下一次事件... 这样不断的轮询

2.2 kernel的low memory killer

drivers/staging/android/lowmemorykiller.c
这里面通过lowmem_scan遍历所有的进程task_struct,然后挑选出oomadj最大值, 如果两个oomadj值相同,则比较两个进程的内存使用,选择占用内存大的进程进行kill

    //找oomadj最大的进程
    for_each_process(tsk) {
        struct task_struct *p; 
        short oom_score_adj;

        if (tsk->flags & PF_KTHREAD)
            continue;

        p = find_lock_task_mm(tsk);
        if (!p)
            continue;

        if (task_lmk_waiting(p) &&
            time_before_eq(jiffies, lowmem_deathpending_timeout)) {
            task_unlock(p);
            rcu_read_unlock();
            return 0;
        }   
        oom_score_adj = p->signal->oom_score_adj;
        if (oom_score_adj < min_score_adj) {
            task_unlock(p);
            continue;
        }   
        tasksize = get_mm_rss(p->mm);
        task_unlock(p);
        if (tasksize <= 0)
            continue;
        if (selected) {
            if (oom_score_adj < selected_oom_score_adj)
                continue;
            if (oom_score_adj == selected_oom_score_adj &&
                tasksize <= selected_tasksize)
                continue;
        }   
        selected = p;
        selected_tasksize = tasksize;
        selected_oom_score_adj = oom_score_adj;
        lowmem_print(2, "select '%s' (%d), adj %hd, size %d, to kill\n",
                 p->comm, p->pid, oom_score_adj, tasksize);
    }
    //如果已经找到,则向进程发送 SIGKILL 信号
    if (selected) {
        long cache_size = other_file * (long)(PAGE_SIZE / 1024);
        long cache_limit = minfree * (long)(PAGE_SIZE / 1024);
        long free = other_free * (long)(PAGE_SIZE / 1024);

        task_lock(selected);
        send_sig(SIGKILL, selected, 0);
        if (selected->mm)
            task_set_lmk_waiting(selected);
        task_unlock(selected);
        trace_lowmemory_kill(selected, cache_size, cache_limit, free);
        lowmem_deathpending_timeout = jiffies + HZ;
        rem += selected_tasksize;
    }

在PIXEL手机中测试其实是没有mp_event/mp_event_critical,也就是kill process这个动作由kernel去完成,而不是lmkd去完成。

三 Android Framework kill掉cached empty进程

lmkd或kernel都可能会去kill掉进程,除此之外android framework也会试着去kill cached/empty进程

    final void updateOomAdjLocked() {
        //计算出 empty和cached的进程的大小限制
        final int emptyProcessLimit = mConstants.CUR_MAX_EMPTY_PROCESSES;
        final int cachedProcessLimit = mConstants.CUR_MAX_CACHED_PROCESSES - emptyProcessLimit;   
        ...
        //注意,这里是遍历LRU缓存的进程,从最新使用的进程开始
        for (int i=N-1; i>=0; i--) {
            ProcessRecord app = mLruProcesses.get(i);
            if (!app.killedByAm && app.thread != null) {
                app.procStateChanged = false;
                
                //计算该进程的 oomadj 和 curProcState
                computeOomAdjLocked(app, ProcessList.UNKNOWN_ADJ, TOP_APP, true, now);
                ...
                
                //将 oomadj 更新到 lmkd中
                applyOomAdjLocked(app, true, now, nowElapsed);

                
                // Count the number of process types.
                switch (app.curProcState) {
                    case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
                    case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
                        //如果进入该分支,说明是cached的进程,更新numCached
                        mNumCachedHiddenProcs++;
                        numCached++;
                        //如果cached的进程超过限制了,调用 app.kill 该进程
                        if (numCached > cachedProcessLimit) {
                            app.kill("cached #" + numCached, true);
                        }
                        break;
                    case ActivityManager.PROCESS_STATE_CACHED_EMPTY:
                        //进入该分支是表明是 EMPTY 进程, 如果超过限制,也直接kill掉
                      
                        if (numEmpty > mConstants.CUR_TRIM_EMPTY_PROCESSES
                                && app.lastActivityTime < oldTime) {
                            //这里超过了CUR_TRIM_EMPTY_PROCESSES这个限制
                            //且距离上一次使用的时间超过30分钟,就kill掉,否则进入else,
                            app.kill("empty for "
                                    + ((oldTime + ProcessList.MAX_EMPTY_TIME - app.lastActivityTime)
                                    / 1000) + "s", true);
                        } else {
                            numEmpty++;
                            //增加numEmpty, 如果缓存的大小大于 emptyProcessLimit也直接kill掉
                            if (numEmpty > emptyProcessLimit) {
                                app.kill("empty #" + numEmpty, true);
                            }
                        }
                        break;
                    default:
                        mNumNonCachedProcs++;
                        break;
                }

                //如果app已经被置为 isolated了,且没有services
                if (app.isolated && app.services.size() <= 0) {
                    app.kill("isolated not needed", true);
                } else {
                    ...
                }

                //计算出可以进入 memory trim的进程数量,前提是
                // curProcState 要高于 HOME 的那些进程
                if (app.curProcState >= ActivityManager.PROCESS_STATE_HOME
                        && !app.killedByAm) {
                    numTrimming++;
                }
            }
        }
    ...

updateOomAdjLocked函数会对LRU里的进程依次计算各个进程的oomadj, 以及进程的curProcState, 在设置对应进程的oomadj后,会根据进程的curProcState的状态,来决定是否kill一些进程,如上面代码所示,从LRU里最近最常使用的进程开始遍历,如果curProcState是CACHED_ACTIVITY, 且缓存的进程数量超过了 cachedProcessLimit, 就会触发 Process.kill 将该进程kill掉。 同理对于 CACHED_EMPTY进程一样,只不过他们的limit不一样而已。

        //计算出 empty和cached的进程的大小限制
        final int emptyProcessLimit = mConstants.CUR_MAX_EMPTY_PROCESSES;
        final int cachedProcessLimit = mConstants.CUR_MAX_CACHED_PROCESSES - emptyProcessLimit;  
default_cached_empty_limit

上图是AOSP默认的limit.

四 如何调试lmkd

在 Developer Options -> Apps -> Background process limit

Options Values
Standard limit -1
No background processes 0
At most 1 process 1
At most 2 process 2
At most 3 process 3
At most 4 process 4

选择一项,最终会触发 setProcessLimit

    public void setProcessLimit(int max) {
        synchronized (this) {
            mConstants.setOverrideMaxCachedProcesses(max);
        }
        trimApplications();
    }
    public void setOverrideMaxCachedProcesses(int value) {
        mOverrideMaxCachedProcesses = value;
        updateMaxCachedProcesses();
    }
    private void updateMaxCachedProcesses() {
        CUR_MAX_CACHED_PROCESSES = mOverrideMaxCachedProcesses < 0
                ? MAX_CACHED_PROCESSES : mOverrideMaxCachedProcesses;
        CUR_MAX_EMPTY_PROCESSES = computeEmptyProcessLimit(CUR_MAX_CACHED_PROCESSES);
        final int rawMaxEmptyProcesses = computeEmptyProcessLimit(MAX_CACHED_PROCESSES);
        CUR_TRIM_EMPTY_PROCESSES = rawMaxEmptyProcesses/2;
        CUR_TRIM_CACHED_PROCESSES = (MAX_CACHED_PROCESSES-rawMaxEmptyProcesses)/3;
    }

如果选择 No background processes, 则第三小节中 updateOomAdjLockedcachedProcessLimitemptyProcessLimit 将会为0,
则表明如果一个进程被计算出来是CACHED或者EMPTY的进程, 就会被直接kill掉。

五 参考

https://www.cnblogs.com/tiger-wang-ms/p/6445213.html

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

推荐阅读更多精彩内容

  • 现在的人喜欢买了新房放着,或者是买新房当成结婚用房,然后就一直空着,也没人入住,因此引发了关于不入住是否要交物业费...
    重名这么多阅读 168评论 0 0
  • 我如这水面上的一叶浮萍,从北到南,跌跌撞撞。头顶的阳光在我每次失意时给我前行的力量。这一路上的自己倔强着、任性着、...
    简陌琴森阅读 245评论 0 0
  • 午后的阳光洒在水面上 一双情侣就依偎着岸上的栏杆 风刚带着水浪 在他们身旁哗哗作响 我的耳畔就飘来了 情人的蜜语 ...
    更向远行阅读 194评论 0 1