Android後臺殺死系列之三:後臺殺死原理LowMemoryKiller(4.3-6.0)

看書的小蝸牛發表於2017-01-13

本篇是Android後臺殺死系列的第三篇,前面兩篇已經對後臺殺死注意事項殺死恢復機制做了分析,本篇主要講解的是Android後臺殺死原理。相對於後臺殺死恢復,LowMemoryKiller原理相對簡單,並且在網上還是能找到不少資料的,不過,由於Android不同版本在框架層的實現有一些不同,網上的分析也多是針對一個Android版本,本文簡單做了以下區分對比。LowMemoryKiller(低記憶體殺手)是Andorid基於oomKiller原理所擴充套件的一個多層次oomKiller,OOMkiller(Out Of Memory Killer)是在Linux系統無法分配新記憶體的時候,選擇性殺掉程式,到oom的時候,系統可能已經不太穩定,而LowMemoryKiller是一種根據記憶體閾值級別觸發的記憶體回收的機制,在系統可用記憶體較低時,就會選擇性殺死程式的策略,相對OOMKiller,更加靈活。在詳細分析其原理與執行機制之前,不妨自己想一下,假設讓你設計一個LowMemoryKiller,你會如何做,這樣一個系統需要什麼功能模組呢?

  • 程式優先順序定義:只有有了優先順序,才能決定先殺誰,後殺誰
  • 程式優先順序的動態管理:一個程式的優先順序不應該是固定不變的,需要根據其變動而動態變化,比如前臺程式切換到後臺優先順序肯定要降低
  • 程式殺死的時機,什麼時候需要挑一個,或者挑多個程式殺死
  • 如何殺死

以上幾個問題便是一個MemoryKiller模組需要的基本功能,Android底層採用的是Linux核心,其程式管理都是基於Linux核心,LowMemoryKiller也相應的放在核心模組,這也意味著使用者空間對於後臺殺死不可見,就像AMS完全不知道一個APP是否被後臺殺死,只有在AMS喚醒APP的時候,才知道APP是否被LowMemoryKiller殺死過。其實LowmemoryKiller的原理是很清晰的,先看一下整體流程圖,再逐步分析:

Android後臺殺死系列之三:後臺殺死原理LowMemoryKiller(4.3-6.0)
App操作影響程式優先順序

先記住兩點 :

  1. LowMemoryKiller是被動殺死程式
  2. Android應用通過AMS,利用proc檔案系統更新程式資訊

Android應用程式優先順序及oomAdj

Android會盡可能長時間地保持應用存活,但為了新建或執行更重要的程式,可能需要移除舊程式來回收記憶體,在選擇要Kill的程式的時候,系統會根據程式的執行狀態作出評估,權衡程式的“重要性“,其權衡的依據主要是四大元件。如果需要縮減記憶體,系統會首先消除重要性最低的程式,然後是重要性略遜的程式,依此類推,以回收系統資源。在Android中,應用程式劃分5級(摘自Google文件):Android中APP的重要性層次一共5級:

  • 前臺程式(Foreground process)
  • 可見程式(Visible process)
  • 服務程式(Service process)
  • 後臺程式(Background process)
  • 空程式(Empty process)

前臺程式

使用者當前操作所必需的程式。如果一個程式滿足以下任一條件,即視為前臺程式:

  • 包含正在互動的Activity(resumed
  • 包含繫結到正在互動的Activity的Service
  • 包含正在“前臺”執行的Service(服務已呼叫startForeground())
  • 包含正執行一個生命週期回撥的Service(onCreate()、onStart() 或 onDestroy())
  • 包含一個正執行其onReceive()方法的BroadcastReceiver

通常,在任意給定時間前臺程式都為數不多。只有在記憶體不足以支援它們同時繼續執行這一萬不得已的情況下,系統才會終止它們。 此時,裝置往往已達到記憶體分頁狀態,因此需要終止一些前臺程式來確保使用者介面正常響應。

可見程式

沒有任何前臺元件、但仍會影響使用者在螢幕上所見內容的程式。 如果一個程式滿足以下任一條件,即視為可見程式:

  • 包含不在前臺、但仍對使用者可見的 Activity(已呼叫其 onPause() 方法)。例如,如果前臺 Activity 啟動了一個對話方塊,允許在其後顯示上一Activity,則有可能會發生這種情況。
  • 包含繫結到可見(或前臺)Activity 的 Service。

可見程式被視為是極其重要的程式,除非為了維持所有前臺程式同時執行而必須終止,否則系統不會終止這些程式。

服務程式

正在執行已使用 startService() 方法啟動的服務且不屬於上述兩個更高類別程式的程式。儘管服務程式與使用者所見內容沒有直接關聯,但是它們通常在執行一些使用者關心的操作(例如,在後臺播放音樂或從網路下載資料)。因此,除非記憶體不足以維持所有前臺程式和可見程式同時執行,否則系統會讓服務程式保持執行狀態。

後臺程式

包含目前對使用者不可見的 Activity 的程式(已呼叫 Activity 的 onStop() 方法)。這些程式對使用者體驗沒有直接影響,系統可能隨時終止它們,以回收記憶體供前臺程式、可見程式或服務程式使用。 通常會有很多後臺程式在執行,因此它們會儲存在 LRU (最近最少使用)列表中,以確保包含使用者最近檢視的 Activity 的程式最後一個被終止。如果某個 Activity 正確實現了生命週期方法,並儲存了其當前狀態,則終止其程式不會對使用者體驗產生明顯影響,因為當使用者導航回該 Activity 時,Activity會恢復其所有可見狀態。 有關儲存和恢復狀態、或者異常殺死恢復可以參考前兩篇 文章。

空程式

不含任何活動應用元件的程式。保留這種程式的的唯一目的是用作快取,以縮短下次在其中執行元件所需的啟動時間,這就是所謂熱啟動 。為了使系統資源在程式快取和底層核心快取之間保持平衡,系統往往會終止這些程式。

根據程式中當前活動元件的重要程度,Android會將程式評定為它可能達到的最高階別。例如,如果某程式託管著服務和可見 Activity,則會將此程式評定為可見程式,而不是服務程式。此外,一個程式的級別可能會因其他程式對它的依賴而有所提高,即服務於另一程式的程式其級別永遠不會低於其所服務的程式。 例如,如果程式 A 中的內容提供程式為程式 B 中的客戶端提供服務,或者如果程式 A 中的服務繫結到程式 B 中的元件,則程式 A 始終被視為至少與程式B同樣重要。

通過Google文件,對不同程式的重要程度有了一個直觀的認識,下面看一下量化到記憶體是什麼樣的呈現形式,這裡針對不同的重要程度,做了進一步的細分,定義了重要級別ADJ,並將優先順序儲存到核心空間的程式結構體中去,供LowmemoryKiller參考:

ADJ優先順序 優先順序 對應場景
UNKNOWN_ADJ 16 一般指將要會快取程式,無法獲取確定值
CACHED_APP_MAX_ADJ 15 不可見程式的adj最大值(不可見程式可能在任何時候被殺死)
CACHED_APP_MIN_ADJ 9 不可見程式的adj最小值(不可見程式可能在任何時候被殺死)
SERVICE_B_AD 8 B List中的Service(較老的、使用可能性更小)
PREVIOUS_APP_ADJ 7 上一個App的程式(比如APP_A跳轉APP_B,APP_A不可見的時候,A就是屬於PREVIOUS_APP_ADJ)
HOME_APP_ADJ 6 Home程式
SERVICE_ADJ 5 服務程式(Service process)
HEAVY_WEIGHT_APP_ADJ 4 後臺的重量級程式,system/rootdir/init.rc檔案中設定
BACKUP_APP_ADJ 3 備份程式(這個不太瞭解)
PERCEPTIBLE_APP_ADJ 2 可感知程式,比如後臺音樂播放<
>VISIBLE_APP_ADJ 1 可見程式(可見,但是沒能獲取焦點,比如新程式僅有一個懸浮Activity,Visible process)
FOREGROUND_APP_ADJ 0 前臺程式(正在展示的APP,存在互動介面,Foreground process)
PERSISTENT_SERVICE_ADJ -11 關聯著系統或persistent程式
PERSISTENT_PROC_ADJ -12 系統persistent程式,比如電話
SYSTEM_ADJ -16 系統程式
NATIVE_ADJ -17 native程式(不被系統管理 )

以上介紹的目的只有一點:Android的應用程式是有優先順序的,它的優先順序跟當前是否存在展示介面,以及是否能被使用者感知有關,越是被使用者感知的的應用優先順序越高(系統程式不考慮)。

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本身是沒喲死掉的,這就是所謂的熱啟動,先看下大體的流程:

Android後臺殺死系列之三:後臺殺死原理LowMemoryKiller(4.3-6.0)
App操作影響程式優先順序

現在直接去AMS看原始碼:

ActivityManagerService

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棧裡應經空了,就回退到上一個應用或者桌面程式,喚醒流程就不在講解了,因為在AMS恢復異常殺死APP的那篇已經說過,這裡要說的是喚醒之後對這個即將退回後臺的APP的操作,這裡注意與startActivity不同的地方,看下面程式碼:

ActivityStack

 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

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)) {
        ...
}複製程式碼

Process中setOomAdj是一個native方法,原型在android_util_Process.cpp中

android_util_Process.cpp

jboolean android_os_Process_setOomAdj(JNIEnv* env, jobject clazz,
                                      jint pid, jint adj)
{
#ifdef HAVE_OOM_ADJ
    char text[64];
    sprintf(text, "/proc/%d/oom_adj", pid);
    int fd = open(text, O_WRONLY);
    if (fd >= 0) {
        sprintf(text, "%d", adj);
        write(fd, text, strlen(text));
        close(fd);
    }
    return true;
#endif
    return false;
}複製程式碼

可以看到,在native程式碼裡,就是通過proc檔案系統修改核心資訊,這裡就是動態更新程式的優先順序oomAdj,以上是針對Android4.3系統的分析,之後會看一下5.0之後的系統是如何實現的。下面是4.3更新oomAdj的流程圖,注意紅色的執行點:

Android後臺殺死系列之三:後臺殺死原理LowMemoryKiller(4.3-6.0)
LowMemoryKiller更新程式oomAdj

Android5.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有什麼改變,其實大部分流程跟之前4.3原始碼類似,我們只看一下不同地方

ActivityManagerService

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服務端如何處理的呢,init.rc裡配置的服務是在開機時啟動的,來看看lmkd服務的入口:main函式

lmkd.c函式

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不同的地方

Android後臺殺死系列之三:後臺殺死原理LowMemoryKiller(4.3-6.0)
Android5.0之後的LMKD服務

以上就分析完了使用者空間的操作如何影響到程式的優先順序,並且將新的優先順序寫到核心中。最後看一下LomemoryKiller在什麼時候、如何根據優先順序殺死程式的:

LomemoryKiller核心部分:如何殺死

LomemoryKiller屬於一個核心驅動模組,主要功能是:在系統記憶體不足的時候掃描程式佇列,找到低優先順序(也許說價效比低更合適)的程式並殺死,以達到釋放記憶體的目的。對於驅動程式,入口是__init函式,先看一下這個驅動模組的入口:

static int __init lowmem_init(void)
{
    register_shrinker(&lowmem_shrinker);
    return 0;
}複製程式碼

可以看到在init的時候,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是如何找到低優先順序程式,並殺死的:入口函式就是init時候註冊的lowmem_shrink函式(4.3原始碼,後面的都有微調但原理大概類似):

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 %d\n", nr_to_scan, gfp_mask, rem);
    read_unlock(&tasklist_lock);
    return rem;
}複製程式碼

先看關鍵點1:其實就是確定當前低記憶體對應的閾值;關鍵點2 :找到比該閾值優先順序低或者相等,並且記憶體佔用較多的程式(tasksize = get_mm_rss(p->mm)其實就是獲取記憶體佔用)),將其殺死。如何殺死的呢?很直接,通過Linux的中的訊號量,傳送SIGKILL訊號直接將程式殺死。到這就分析完了LomemoryKiller核心部分如何工作的。其實很簡單,一句話:被動掃描,找到低優先順序的程式,殺死。

總結

通過本篇文章,希望大家能有以下幾點認知:

  • Android APP程式是有優先順序的的,與程式是否被使用者感知有直接關係
  • APP切換等活動都可能造成程式優先順序的變化,都是利用AMS,並通過proc檔案設定到核心的
  • LowmemoryKiller執行在核心,在記憶體需要縮減的時候,會選擇低優先順序的程式殺死

至於更加細節的記憶體的縮減、優先順序的計算也許將來會放到單獨的文章中說明,本文的目的是:能讓大家對LowmemoryKiller的概念以及執行機制有個簡單瞭解。

Android 後臺殺死系列之一:FragmentActivity 及 PhoneWindow 後臺殺死處理機制
Android後臺殺死系列之二:ActivityManagerService與App現場恢復機制 Android後臺殺死系列之三:後臺殺死原理LowMemoryKiller(4.3-6.0)
Android後臺殺死系列之四:Binder訃告原理
Android後臺殺死系列之五:Android程式保活-自“裁”或者耍流氓
僅供參考,歡迎指正

參考文件

Android應用程式啟動過程原始碼分析
Android Framework架構淺析之【近期任務】
Android Low Memory Killer介紹
Android開發之InstanceState詳解
對Android近期任務列表(Recent Applications)的簡單分析
Android 作業系統的記憶體回收機制
Android LowMemoryKiller原理分析 精
Android程式生命週期與ADJ
Linux下/proc目錄簡介
Android系統中的程式管理:程式的建立 精
Google文件--程式和執行緒

相關文章