Android 进程回收之LowMemoryKiller原理篇

时间:2022-04-24
本文章向大家介绍Android 进程回收之LowMemoryKiller原理篇,主要内容包括OOMKiller、LowMemoryKiller、android 优先级更新、LomemoryKiller内核区块、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

在前面的文章Android进程保活一文中,对于LowMemoryKiller的概念做了简单的提及。LowMemoryKiller简称低内存杀死机制。简单来说,LowMemoryKiller(低内存杀手)是Andorid基于oomKiller原理所扩展的一个多层次oomKiller,OOMkiller(Out Of Memory Killer)是在Linux系统无法分配新内存的时候,选择性杀掉进程,到oom的时候,系统可能已经不太稳定,而LowMemoryKiller是一种根据内存阈值级别触发的内存回收的机制,在系统可用内存较低时,就会选择性杀死进程的策略,相对OOMKiller,更加灵活。

在讲解LowMemoryKiller之前,先看另一个概念:OOMKiller。

Linux下有一种OOM KILLER 的机制,它会在系统内存耗尽的情况下,启用自己算法有选择性的kill 掉一些进程。

OOMKiller

当我们启动应用时,需要向系统申请内存,即进行malloc的操作,进行malloc操作如果返回一个非NULL的操作表示申请到了可用的内部你。事实上,这个地方是可能存在bug的。Linux有一种内存优化机制,即:允许程序申请比系统可用内存更多的内存(术语:overcommit),但是Linux并不保证这些内存马上可用,如果凑巧你申请到的内存中在你需要使用的时候还没有完全释放出来,这个时候就会触发OOM killer了。内核代码为:mm/oom_kill.c,其调用顺序为:

malloc -> _alloc_pages -> out_of_memory() -> select_bad_process() -> badness()

然而,系统的物理内存往往是有限的,这就需要在使用过程中杀掉一些无用的进程以腾出新的内存。在Android系统中,AmS需要和Linux操作系统有个约定,即将要谈到的Linux内核的内存管理控制系统是如何通知AMS内存不足的。

Java虚拟机运行时都有各自独立的内存空间,应用程序A发生Out Of Memory并不意味着应用程序B也会发生Out Of Memory,很有可能仅仅是A程序用光了自己内存的上限,而系统内存却还是有的。所以说,单纯的AmS是无法获知系统内存是否低的。

那么,Android系统是如何知道”系统内存低”或者”系统内存不够用”呢?从Android底层的Linux来讲,由于其并未采用磁盘虚拟内存机制,所以应用程序能够使用的内存大小完全取决于实际物理内存的大小,所以,”内存低”的含义就是实际物理内存已经被用得所剩无几了。看下面一幅图:

在Android中运行了一个OOM 进程,即Out Of Memory。该进程启动时会首先向Linux内核中把自己注册为一个OOM Killer,即当Linux内核的内存管理模块检测到系统内存低的时候就会通知已经注册的OOM进程,然后这些OOM Killer就可以根据各种规则进行内存释放了,当然也可以什么都不做。

Android中的OOM Killer进程是仅仅适用于Android应用程序的,该进程在运行时,AmS需要把每一个应用程序的oom_adj值告知给Killer。这个值的范围在-16到15,值越低,说明越重要,这个值类似于Linux系统中的进程nice值,只是在标准的Linux中,有其自己的一套Killer机制。

重要:当发生低内存的条件时,Linux内核管理模块通知OOM Killer,Killer则根据AmS所告知的优先级,强制退出优先级低的应用进程。

LowMemoryKiller

前面,我们谈到了OOMKiller的一些知识,在理解OOMKiller的时候注意两点:

  1. LowMemoryKiller是被动杀死进程;
  2. Android应用通过AMS,利用proc文件系统更新进程信息。

进程优先级及oomAdj

关于这方面的知识,前文有过介绍Android进程保活。这里在简单的介绍下。 Android会尽可能长时间地保持应用存活,但为了新建或运行更重要的进程,可能需要移除旧进程来回收内存,在选择要Kill的进程的时候,系统会根据进程的运行状态作出评估,权衡进程的“重要性“,其权衡的依据主要是四大组件。如果需要缩减内存,系统会首先消除重要性最低的进程,然后是重要性略逊的进程,依此类推,以回收系统资源。在Android中,应用进程划分5级:

  • 前台进程(Foreground process)
  • 可见进程(Visible process)
  • 服务进程(Service process)
  • 后台进程(Background process)
  • 空进程(Empty process)

前台进程(Foreground process)

用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:

  • 包含正在交互的Activity(如resumed)
  • 包含绑定到正在交互的Activity的Service
  • 包含正在“前台”运行的Service(服务已调用startForeground())
  • 包含正执行一个生命周期回调的Service(onCreate()、onStart() 或 onDestroy())
  • 包含一个正执行其onReceive()方法的BroadcastReceiver

可见进程(Visible process)

没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。 如果一个进程满足以下任一条件,即视为可见进程: 包含不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一Activity,则有可能会发生这种情况。 包含绑定到可见(或前台)Activity 的 Service。

服务进程(Service process)

正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

后台进程(Background process)

包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity会恢复其所有可见状态。

空进程(Empty process)

不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间,这就是所谓热启动 。为了使系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

根据进程中当前活动组件的重要程度,Android会对进程的优先级进行评定。下表是进程优先级的表(主要针对4.03-5.x)。

adj级别

解释

UNKNOWN_ADJ

16

预留的最低级别,一般对于缓存的进程才有可能设置成这个级别

CACHED_APP_MAX_ADJ

15

缓存进程,空进程,在内存不足的情况下就会优先被kill

CACHED_APP_MIN_ADJ

9

缓存进程,也就是空进程

SERVICE_B_ADJ

8

不活跃的进程

PREVIOUS_APP_ADJ

7

切换进程

HOME_APP_ADJ

6

与Home交互的进程

SERVICE_ADJ

5

有Service的进程

HEAVY_WEIGHT_APP_ADJ

4

高权重进程

BACKUP_APP_ADJ

3

正在备份的进程

PERCEPTIBLE_APP_ADJ

2

可感知的进程,比如那种播放音乐

VISIBLE_APP_ADJ

1

可见进程,如当前的Activity

FOREGROUND_APP_ADJ

0

前台进程

PERSISTENT_SERVICE_ADJ

-11

重要进程

PERSISTENT_PROC_ADJ

-12

核心进程

SYSTEM_ADJ

-16

系统进程

NATIVE_ADJ

-17

系统起的Native进程

android 优先级更新

APP中很多操作都可能会影响进程列表的优先级,比如退到后台、移到前台等,都会潜在的影响进程的优先级,我们知道Lowmemorykiller是通过遍历内核的进程结构体队列,选择优先级低的杀死,那么APP操作是如何写入到内核空间的呢?Linxu有用户间跟内核空间的区分,无论是APP还是系统服务,都是运行在用户空间,严格说用户控件的操作是无法直接影响内核空间的,更不用说更改进程的优先级。其实这里是通过了Linux中的一个proc文件体统,proc文件系统可以简单的看多是内核空间映射成用户可以操作的文件系统,当然不是所有进程都有权利操作,通过proc文件系统,用户空间的进程就能够修改内核空间的数据,比如修改进程的优先级,在Android家族,5.0之前的系统是AMS进程直接修改的,5.0之后,是修改优先级的操作被封装成了一个独立的服务-lmkd,lmkd服务位于用户空间,其作用层次同AMS、WMS类似,就是一个普通的系统服务。我们先看一下5.0之前的代码,这里仍然用4.3的源码看一下,模拟一个场景,APP只有一个Activity,我们主动finish掉这个Activity,APP就回到了后台,这里要记住,虽然没有可用的Activity,但是APP本身是没哟死掉的,这就是所谓的热启动,先看下大体的流程:

以上面描述的push Activity为例,来查看AMS源码:

public final boolean finishActivity(IBinder token, int resultCode, Intent resultData) {
     ...
    synchronized(this) {

        final long origId = Binder.clearCallingIdentity();
        boolean res = mMainStack.requestFinishActivityLocked(token, resultCode,
                resultData, "app-request", true);
     ...
    }
}

一开始的流程跟startActivity类似,首先是先暂停当前resume的Activity。相关代码如下:

final boolean finishActivityLocked(ActivityRecord r, int index, int resultCode,
            Intent resultData, String reason, boolean immediate, boolean oomAdj) {
         ...
            if (mPausingActivity == null) {
                if (DEBUG_PAUSE) Slog.v(TAG, "Finish needs to pause: " + r);
                if (DEBUG_USER_LEAVING) Slog.v(TAG, "finish() => pause with userLeaving=false");
                startPausingLocked(false, false);
            }
            ...
    }

pause掉当前Activity之后,还需要唤醒上一个Activity,如果当前APP的Activity栈里应经空了,就回退到上一个应用或者桌面程序。对于返回到桌面的情况这里不做深究。其实源码有一段代码是判断,当前的ActivityStack上面是否还有其他的Activity的代码。当Activity回退到后台状态后,系统做了什么事情呢?来看下面的代码:

private final void completePauseLocked() {
   ActivityRecord prev = mPausingActivity;

   if (prev != null) {
       if (prev.finishing) {
       1、 不同点
      <!--主动finish的时候,走的是这个分支,状态变换的细节请自己查询代码-->
           prev = finishCurrentActivityLocked(prev, FINISH_AFTER_VISIBLE, false);
       } 
       ...
       2、相同点       
    if (!mService.isSleeping()) {
       resumeTopActivityLocked(prev);
   }

看一下上面的两个关键点1跟2,1是同startActivity的completePauseLocked不同的地方,主动finish的prev.finishing是为true的,因此会执行finishCurrentActivityLocked分支,将当前pause的Activity加到mStoppingActivities队列中去,并且唤醒下一个需要走到到前台的Activity,唤醒后,会继续执行stop:

private final ActivityRecord finishCurrentActivityLocked(ActivityRecord r,
           int index, int mode, boolean oomAdj) {
       if (mode == FINISH_AFTER_VISIBLE && r.nowVisible) {
           if (!mStoppingActivities.contains(r)) {
               mStoppingActivities.add(r);
               ...
           }
              ....
           return r;
       }
       ...
   }

再回到resumeTopActivityLocked继续看,resume之后会回调completeResumeLocked函数,继续执行stop,这个函数通过向Handler发送IDLE_TIMEOUT_MSG消息来回调activityIdleInternal函数,最终执行destroyActivityLocked销毁ActivityRecord。

final boolean resumeTopActivityLocked(ActivityRecord prev, Bundle options) {
        ...
   if (next.app != null && next.app.thread != null) {                   ...
            try {
                。。。
                next.app.thread.scheduleResumeActivity(next.appToken,
                        mService.isNextTransitionForward());
                ..。
            try {
                next.visible = true;
                completeResumeLocked(next);
            }  
            ....
         }

在销毁Activity的时候,如果当前APP的Activity堆栈为空了,就说明当前Activity没有可见界面了,这个时候就需要动态更新这个APP的优先级,详细代码如下:

final boolean destroyActivityLocked(ActivityRecord r,
           boolean removeFromApp, boolean oomAdj, String reason) {
           ...
      if (hadApp) {
           if (removeFromApp) {
               // 这里动ProcessRecord里面删除,但是没从history删除
               int idx = r.app.activities.indexOf(r);
               if (idx >= 0) {
                   r.app.activities.remove(idx);
               }
               ...
               if (r.app.activities.size() == 0) {
                   // No longer have activities, so update oom adj.
                   mService.updateOomAdjLocked();
               ...
      }

最终会调用AMS的updateOomAdjLocked函数去更新进程优先级,在4.3的源码里面,主要是通过Process类的setOomAdj函数来设置优先级。ActivityManagerService更新优先级的代码在updateOomAdjLocked里面。

private final boolean updateOomAdjLocked(ProcessRecord app, int hiddenAdj,
        int clientHiddenAdj, int emptyAdj, ProcessRecord TOP_APP, boolean doingAll) {
    ...
    计算优先级
    computeOomAdjLocked(app, hiddenAdj, clientHiddenAdj, emptyAdj, TOP_APP, false, doingAll);
     。。。
     <!--如果不相同,设置新的OomAdj-->

    if (app.curAdj != app.setAdj) {
        if (Process.setOomAdj(app.pid, app.curAdj)) {
        ...
}

最后调用android_util_Process.cpp,通过proc文件系统修改内核信息来动态更新进程的优先级oomAdj,以上是针对Android4.3系统的分析。

Android 5.0的进程优先级更新-LMKD服务

Android5.0将设置进程优先级的入口封装成了一个独立的服务lmkd服务,AMS不再直接访问proc文件系统,而是通过lmkd服务来进行设置,从init.rc文件中看到服务的配置。相关配置如下:

service lmkd /system/bin/lmkd
    class core
    critical
    socket lmkd seqpacket 0660 system system

从配置中可以看出,该服务是通过socket与其他进行进程进行通信,其实就是AMS通过socket向lmkd服务发送请求,让lmkd去更新进程的优先级,lmkd收到请求后,会通过/proc文件系统去更新内核中的进程优先级。首先看一下5.0中这一块AMS有什么改变。

private final boolean updateOomAdjLocked(ProcessRecord app, int cachedAdj,
        ProcessRecord TOP_APP, boolean doingAll, long now) {
    ...
    computeOomAdjLocked(app, cachedAdj, TOP_APP, doingAll, now);
    ...
    applyOomAdjLocked(app, doingAll, now, SystemClock.elapsedRealtime());
}

private final boolean applyOomAdjLocked(ProcessRecord app, boolean doingAll, long now,
        long nowElapsed) {
    boolean success = true;

    if (app.curRawAdj != app.setRawAdj) {
        app.setRawAdj = app.curRawAdj;
    }

    int changes = 0;
      不同点1
    if (app.curAdj != app.setAdj) {
        ProcessList.setOomAdj(app.pid, app.info.uid, app.curAdj);
        if (DEBUG_SWITCH || DEBUG_OOM_ADJ) Slog.v(TAG_OOM_ADJ,
                "Set " + app.pid + " " + app.processName + " adj " + app.curAdj + ": "
                + app.adjType);
        app.setAdj = app.curAdj;
        app.verifiedAdj = ProcessList.INVALID_ADJ;
    }

从上面的不同点1可以看出,5.0之后是通过ProcessList类去设置oomAdj,其实这里就是通过socket与LMKD服务进行通信,向lmkd服务传递给LMK_PROCPRIO命令去更新进程优先级:

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);
    writeLmkd(buf);
    long now = SystemClock.elapsedRealtime();
      }    

private static void writeLmkd(ByteBuffer buf) {
        for (int i = 0; i < 3; i++) {
        if (sLmkdSocket == null) {
          if (openLmkdSocket() == false) {
            ...
        try {
            sLmkdOutputStream.write(buf.array(), 0, buf.position());
            return;
            ...
    }

其实就是openLmkdSocket打开本地socket端口,并将优先级信息发送过去,那么lmkd服务端如何处理的呢,来看看lmkd服务的入口main函数:

int main(int argc __unused, char **argv __unused) {
    struct sched_param param = {
            .sched_priority = 1,
    };

    mlockall(MCL_FUTURE);
    sched_setscheduler(0, SCHED_FIFO, &param);
    if (!init())
        mainloop();

    ALOGI("exiting");
    return 0;
}

很简单,打开一个端口,并通过mainloop监听socket,如果有请求到来,就解析命令并执行,刚才传入的LMK_PROCPRIO命令对应的操作就是cmd_procprio,用来更新oomAdj,其更新新机制还是通过proc文件系统。

static void cmd_procprio(int pid, int uid, int oomadj) {
    struct proc *procp;
    。。。
    还是利用/proc文件系统进行更新
    snprintf(path, sizeof(path), "/proc/%d/oom_score_adj", pid);
    snprintf(val, sizeof(val), "%d", lowmem_oom_adj_to_oom_score_adj(oomadj));
    writefilestring(path, val);
   。。。
}

与4.3版本相比,5.0的LMKD简化了很多AMS的东西。

LomemoryKiller内核区块

LomemoryKiller属于一个内核驱动模块,主要功能是:在系统内存不足的时候扫描进程队列,找到低优先级(也许说性价比低更合适)的进程并杀死,以达到释放内存的目的。对于驱动程序,入口是__init函数。

static int __init lowmem_init(void)
{
    register_shrinker(&lowmem_shrinker);
    return 0;
}

LomemoryKiller将自己的lowmem_shrinker入口注册到系统的内存检测模块去,作用就是在内存不足的时候可以被回调,register_shrinker函数是一属于另一个内存管理模块的函数。

void register_shrinker(struct shrinker *shrinker)
{
    shrinker->nr = 0;
    down_write(&shrinker_rwsem);
    list_add_tail(&shrinker->list, &shrinker_list);
    up_write(&shrinker_rwsem);
}

最后,看一下,当内存不足触发回调的时候,LomemoryKiller是如何找到低优先级进程,并杀死的。管家代码就在lowmem_shrink函数里面:

static int lowmem_shrink(int nr_to_scan, gfp_t gfp_mask)
{
    struct task_struct *p;
    。。。
    关键点1 找到当前的内存对应的阈值
    for(i = 0; i < array_size; i++) {
        if (other_free < lowmem_minfree[i] &&
            other_file < lowmem_minfree[i]) {
            min_adj = lowmem_adj[i];
            break;
        }
    }
    。。。
    关键点2 找到优先级低于这个阈值的进程,并杀死

    read_lock(&tasklist_lock);
    for_each_process(p) {
        if (p->oomkilladj < min_adj || !p->mm)
            continue;
        tasksize = get_mm_rss(p->mm);
        if (tasksize <= 0)
            continue;
        if (selected) {
            if (p->oomkilladj < selected->oomkilladj)
                continue;
            if (p->oomkilladj == selected->oomkilladj &&
                tasksize <= selected_tasksize)
                continue;
        }
        selected = p;
        selected_tasksize = tasksize;

    }
    if(selected != NULL) {
        force_sig(SIGKILL, selected);
        rem -= selected_tasksize;
    }
    lowmem_print(4, "lowmem_shrink %d, %x, return %dn", nr_to_scan, gfp_mask, rem);
    read_unlock(&tasklist_lock);
    return rem;
}

上面的逻辑很清楚,通过给应用设置内存对应的阈值,通过Linux的中的信号量,发送SIGKILL信号直接将进程杀死。关于LomemoryKiller和Linux底层通信的原理,请大家自行学习相关的文章介绍。

附: Android 7.0 ActivityManagerService分析