閱讀386 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Android 進程回收之LowMemoryKiller原理篇

在前麵的文章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 %d\n", nr_to_scan, gfp_mask, rem);
    read_unlock(&tasklist_lock);
    return rem;
}

上麵的邏輯很清楚,通過給應用設置內存對應的閾值,通過Linux的中的信號量,發送SIGKILL信號直接將進程殺死。關於LomemoryKiller和Linux底層通信的原理,請大家自行學習相關的文章介紹。

附: Android 7.0 ActivityManagerService分析

最後更新:2017-09-11 11:32:23

  上一篇:go  Centos7安裝配置ELK(Elasticsearch + Logstash + Kibana)分析Nginx日誌簡單單點配置
  下一篇:go  CRP升級到RDC,遷移指南