Android LowMemoryKiller 原理分析

Gityuan發表於2016-09-28

一. 概述

Android的設計理念之一,便是應用程式退出,但程式還會繼續存在系統以便再次啟動時提高響應時間. 這樣的設計會帶來一個問題, 每個程式都有自己獨立的記憶體地址空間,隨著應用開啟數量的增多,系統已使用的記憶體越來越大,就很有可能導致系統記憶體不足, 那麼需要一個能管理所有程式,根據一定策略來釋放程式的策略,這便有了lmk,全稱為LowMemoryKiller(低記憶體殺手),lmkd來決定什麼時間殺掉什麼程式.

Android基於Linux的系統,其實Linux有類似的記憶體管理策略——OOM killer,全稱(Out Of Memory Killer), OOM的策略更多的是用於分配記憶體不足時觸發,將得分最高的程式殺掉。而lmk則會每隔一段時間檢查一次,當系統剩餘可用記憶體較低時,便會觸發殺程式的策略,根據不同的剩餘記憶體檔位來來選擇殺不同優先順序的程式,而不是等到OOM時再來殺程式,真正OOM時系統可能已經處於異常狀態,系統更希望的是未雨綢繆,在記憶體很低時來殺掉一些優先順序較低的程式來保障後續操作的順利進行。

二. framework層

位於ProcessList.java中定義了3種命令型別,這些檔案的定義必須跟lmkd.c定義完全一致,格式分別如下:

LMK_TARGET <minfree> <minkillprio> ... (up to 6 pairs)
LMK_PROCPRIO <pid> <prio>
LMK_PROCREMOVE <pid>
功能 命令 對應方法 觸發時機
更新oom_adj LMK_TARGET updateOomLevels AMS.updateConfiguration
設定程式adj LMK_PROCPRIO setOomAdj AMS.applyOomAdjLocked
移除程式 LMK_PROCREMOVE remove AMS.handleAppDiedLocked/cleanUpApplicationRecordLocked

在前面文章Android程式排程之adj演算法中有講到AMS.applyOomAdjLocked,接下來以這個過程為主線開始分析。

2.1 AMS.applyOomAdjLocked

private final boolean applyOomAdjLocked(ProcessRecord app, boolean doingAll, long now,
        long nowElapsed) {
    ...
    if (app.curAdj != app.setAdj) {
        //【見小節2.2】
        ProcessList.setOomAdj(app.pid, app.info.uid, app.curAdj);
        app.setAdj = app.curAdj;
    }
    ...
}

2.2 PL.setOomAdj

public static final void setOomAdj(int pid, int uid, int amt) {
    //當adj=16,則直接返回
    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);
    //將16Byte位元組寫入socket【見小節2.3】
    writeLmkd(buf);
    long now = SystemClock.elapsedRealtime();
    if ((now-start) > 250) {
        Slog.w("ActivityManager", "SLOW OOM ADJ: " + (now-start) + "ms for pid " + pid
                + " = " + amt);
    }
}

buf大小為16個位元組,依次寫入LMK_PROCPRIO(命令型別), pid(程式pid), uid(程式uid), amt(目標adj),將這些位元組通過socket傳送給lmkd.

2.3 PL.writeLmkd

private static void writeLmkd(ByteBuffer buf) {
    //當socket開啟失敗會嘗試3次
    for (int i = 0; i < 3; i++) {
        if (sLmkdSocket == null) {
                //開啟socket 【見小節2.4】
                if (openLmkdSocket() == false) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ie) {
                    }
                    continue;
                }
        }
        try {
            //將buf資訊寫入lmkd socket
            sLmkdOutputStream.write(buf.array(), 0, buf.position());
            return;
        } catch (IOException ex) {
            try {
                sLmkdSocket.close();
            } catch (IOException ex2) {
            }
            sLmkdSocket = null;
        }
    }
}
  • 當sLmkdSocket為空,並且開啟失敗,重新執行該操作;
  • 當sLmkdOutputStream寫入buf資訊失敗,則會關閉sLmkdSocket,重新執行該操作;

這個重新執行操作最多3次,如果3次後還失敗,則writeLmkd操作會直接結束。嘗試3次,則不管結果如何都將退出該操作,可見writeLmkd寫入操作還有可能失敗的。

2.4 PL.openLmkdSocket

private static boolean openLmkdSocket() {
    try {
        sLmkdSocket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET);
        //與遠端lmkd守護程式建立socket連線
        sLmkdSocket.connect(
            new LocalSocketAddress("lmkd",
                    LocalSocketAddress.Namespace.RESERVED));
        sLmkdOutputStream = sLmkdSocket.getOutputStream();
    } catch (IOException ex) {
        Slog.w(TAG, "lowmemorykiller daemon socket open failed");
        sLmkdSocket = null;
        return false;
    }
    return true;
}

sLmkdSocket採用的是SOCK_SEQPACKET,這是型別的socket能提供順序確定的,可靠的,雙向基於連線的socket endpoint,與型別SOCK_STREAM很相似,唯一不同的是SEQPACKET保留訊息的邊界,而SOCK_STREAM是基於位元組流,並不會記錄邊界。

舉例:本地通過write()系統呼叫向遠端先後傳送兩組資料:一組4位元組,一組8位元組;對於SOCK_SEQPACKET型別通過read()能獲知這是兩組資料以及大小,而對於SOCK_STREAM型別,通過read()一次性讀取到12個位元組,並不知道資料包的邊界情況。

常見的資料型別還有SOCK_DGRAM,提供資料包形式,用於udp這樣不可靠的通訊過程。

再回到openLmkdSocket()方法,該方法是開啟一個名為lmkd的socket,型別為LocalSocket.SOCKET_SEQPACKET,這只是一個封裝,真實型別就是SOCK_SEQPACKET。先跟遠端lmkd守護程式建立連線,再向其通過write()將資料寫入該socket,再接下來進入lmkd過程。

三. lmkd

lmkd是由init程式,通過解析init.rc檔案來啟動的lmkd守護程式,lmkd會建立名為lmkd的socket,節點位於/dev/socket/lmkd,該socket用於跟上層framework互動。

service lmkd /system/bin/lmkd
    class core
    critical
    socket lmkd seqpacket 0660 system system
    writepid /dev/cpuset/system-background/tasks

lmkd啟動後,接下里的操作都在platform/system/core/lmkd/lmkd.c檔案,首先進入main()方法

3.1 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);
    //初始化【見小節3.2】
    if (!init())
        mainloop(); //成功後進入loop [見小節3.3]
    ALOGI("exiting");
    return 0;
}

3.2 init

static int init(void) {
    struct epoll_event epev;
    int i;
    int ret;
    page_k = sysconf(_SC_PAGESIZE);
    if (page_k == -1)
        page_k = PAGE_SIZE;
    page_k /= 1024;
    //建立epoll監聽檔案控制程式碼
    epollfd = epoll_create(MAX_EPOLL_EVENTS);

    //獲取lmkd控制描述符
    ctrl_lfd = android_get_control_socket("lmkd");
    //監聽lmkd socket
    ret = listen(ctrl_lfd, 1);

    epev.events = EPOLLIN;
    epev.data.ptr = (void *)ctrl_connect_handler;

    //將檔案控制程式碼ctrl_lfd,加入epoll控制程式碼
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, ctrl_lfd, &epev) == -1) {
        return -1;
    }

    maxevents++;
    //該路徑是否具有可寫的許可權
    use_inkernel_interface = !access(INKERNEL_MINFREE_PATH, W_OK);
    if (use_inkernel_interface) {
        ALOGI("Using in-kernel low memory killer interface");
    } else {
        ret = init_mp(MEMPRESSURE_WATCH_LEVEL, (void *)&mp_event);
        if (ret)
            ALOGE("Kernel does not support memory pressure events or in-kernel low memory killer");
    }

    for (i = 0; i <= ADJTOSLOT(OOM_SCORE_ADJ_MAX); i++) {
        procadjslot_list[i].next = &procadjslot_list[i];
        procadjslot_list[i].prev = &procadjslot_list[i];
    }
    return 0;
}

這裡,通過檢驗/sys/module/lowmemorykiller/parameters/minfree節點是否具有可寫許可權來判斷是否使用kernel介面來管理lmk事件。預設該節點是具有系統可寫的許可權,也就意味著use_inkernel_interface=1.

3.3 mainloop

static void mainloop(void) {
    while (1) {
        struct epoll_event events[maxevents];
        int nevents;
        int i;
        ctrl_dfd_reopened = 0;

        //等待epollfd上的事件
        nevents = epoll_wait(epollfd, events, maxevents, -1);
        if (nevents == -1) {
            if (errno == EINTR)
                continue;
            continue;
        }
        for (i = 0; i < nevents; ++i) {
            if (events[i].events & EPOLLERR)
                ALOGD("EPOLLERR on event #%d", i);
            // 當事件到來,則呼叫ctrl_connect_handler方法 【見小節3.4】
            if (events[i].data.ptr)
                (*(void (*)(uint32_t))events[i].data.ptr)(events[i].events);
        }
    }
}

主迴圈呼叫epoll_wait(),等待epollfd上的事件,當接收到中斷或者不存在事件,則執行continue操作。當事件到來,則 呼叫的ctrl_connect_handler方法,該方法是由init()過程中設定的方法。

3.4 ctrl_connect_handler

static void ctrl_connect_handler(uint32_t events __unused) {
    struct epoll_event epev;
    if (ctrl_dfd >= 0) {
        ctrl_data_close();
        ctrl_dfd_reopened = 1;
    }
    ctrl_dfd = accept(ctrl_lfd, NULL, NULL);
    if (ctrl_dfd < 0) {
        ALOGE("lmkd control socket accept failed; errno=%d", errno);
        return;
    }
    ALOGI("ActivityManager connected");
    maxevents++;
    epev.events = EPOLLIN;
    epev.data.ptr = (void *)ctrl_data_handler;

    //將ctrl_lfd新增到epollfd
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, ctrl_dfd, &epev) == -1) {
        ALOGE("epoll_ctl for data connection socket failed; errno=%d", errno);
        ctrl_data_close();
        return;
    }
}

當事件觸發,則呼叫ctrl_data_handler

3.5 ctrl_data_handler

static void ctrl_data_handler(uint32_t events) {
    if (events & EPOLLHUP) {
        //ActivityManager 連線已斷開
        if (!ctrl_dfd_reopened)
            ctrl_data_close();
    } else if (events & EPOLLIN) {
        //[見小節3.6]
        ctrl_command_handler();
    }
}

3.6 ctrl_command_handler

static void ctrl_command_handler(void) {
    int ibuf[CTRL_PACKET_MAX / sizeof(int)];
    int len;
    int cmd = -1;
    int nargs;
    int targets;
    len = ctrl_data_read((char *)ibuf, CTRL_PACKET_MAX);
    if (len <= 0)
        return;
    nargs = len / sizeof(int) - 1;
    if (nargs < 0)
        goto wronglen;
    //將網路位元組順序轉換為主機位元組順序
    cmd = ntohl(ibuf[0]);
    switch(cmd) {
    case LMK_TARGET:
        targets = nargs / 2;
        if (nargs & 0x1 || targets > (int)ARRAY_SIZE(lowmem_adj))
            goto wronglen;
        cmd_target(targets, &ibuf[1]);
        break;
    case LMK_PROCPRIO:
        if (nargs != 3)
            goto wronglen;
        //設定程式adj【見小節3.7】
        cmd_procprio(ntohl(ibuf[1]), ntohl(ibuf[2]), ntohl(ibuf[3]));
        break;
    case LMK_PROCREMOVE:
        if (nargs != 1)
            goto wronglen;
        cmd_procremove(ntohl(ibuf[1]));
        break;
    default:
        ALOGE("Received unknown command code %d", cmd);
        return;
    }
    return;
wronglen:
    ALOGE("Wrong control socket read length cmd=%d len=%d", cmd, len);
}

CTRL_PACKET_MAX 大小等於 (sizeof(int) * (MAX_TARGETS * 2 + 1));而MAX_TARGETS=6,對於sizeof(int)=4的系統,則CTRL_PACKET_MAX=52。 獲取framework傳遞過來的buf資料後,根據3種不同的命令,進入不同的分支。 接下來,繼續以前面傳遞過來的LMK_PROCPRIO命令來往下講解,進入cmd_procprio過程。

3.7 cmd_procprio

static void cmd_procprio(int pid, int uid, int oomadj) {
    struct proc *procp;
    char path[80];
    char val[20];
    ...
    snprintf(path, sizeof(path), "/proc/%d/oom_score_adj", pid);
    snprintf(val, sizeof(val), "%d", oomadj);
    //向節點/proc/<pid>/oom_score_adj寫入oomadj
    writefilestring(path, val);

    //當使用kernel方式則直接返回
    if (use_inkernel_interface)
        return;
    procp = pid_lookup(pid);
    if (!procp) {
            procp = malloc(sizeof(struct proc));
            if (!procp) {
                // Oh, the irony.  May need to rebuild our state.
                return;
            }
            procp->pid = pid;
            procp->uid = uid;
            procp->oomadj = oomadj;
            proc_insert(procp);
    } else {
        proc_unslot(procp);
        procp->oomadj = oomadj;
        proc_slot(procp);
    }
}

向節點“/proc//oom_score_adj`寫入oomadj。由於use_inkernel_interface=1,那麼再接下里需要看看kernel的情況

3.8 小節

use_inkernel_interface該值後續應該會逐漸採用使用者空間策略。不過目前仍為 use_inkernel_interface=1則有:

  • LMK_PROCPRIO: 向/proc/<pid>/oom_score_adj寫入oomadj,則直接返回;
  • LMK_PROCREMOVE:不做任何事,直接返回;
  • LMK_TARGET:分別向/sys/module/lowmemorykiller/parameters目錄下的minfreeadj節點寫入相應資訊;

四. Kernel層

lowmemorykiller driver位於 drivers/staging/Android/lowmemorykiller.c

4.1 lowmemorykiller初始化

static struct shrinker lowmem_shrinker = {
	.scan_objects = lowmem_scan,
	.count_objects = lowmem_count,
	.seeks = DEFAULT_SEEKS * 16
};

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

static void __exit lowmem_exit(void)
{
	unregister_shrinker(&lowmem_shrinker);
}

module_init(lowmem_init);
module_exit(lowmem_exit);

通過register_shrinker和unregister_shrinker分別用於初始化和退出。

4.2 shrinker

LMK驅動通過註冊shrinker來實現的,shrinker是linux kernel標準的回收記憶體page的機制,由核心執行緒kswapd負責監控。

當記憶體不足時kswapd執行緒會遍歷一張shrinker連結串列,並回撥已註冊的shrinker函式來回收記憶體page,kswapd還會週期性喚醒來執行記憶體操作。每個zone維護active_list和inactive_list連結串列,核心根據頁面活動狀態將page在這兩個連結串列之間移動,最終通過shrink_slab和shrink_zone來回收記憶體頁,有興趣想進一步瞭解linux記憶體回收機制,可自行研究,這裡再回到LowMemoryKiller的過程分析。

4.3 lowmem_count

static unsigned long lowmem_count(struct shrinker *s,
				  struct shrink_control *sc)
{
	return global_page_state(NR_ACTIVE_ANON) +
		global_page_state(NR_ACTIVE_FILE) +
		global_page_state(NR_INACTIVE_ANON) +
		global_page_state(NR_INACTIVE_FILE);
}

ANON代表匿名對映,沒有後備儲存器;FILE代表檔案對映; 記憶體計算公式= 活動匿名記憶體 + 活動檔案記憶體 + 不活動匿名記憶體 + 不活動檔案記憶體

4.4 lowmem_scan

當觸發lmkd,則先殺oom_adj最大的程式, 當oom_adj相等時,則選擇oom_score_adj最大的程式.

static unsigned long lowmem_scan(struct shrinker *s, struct shrink_control *sc)
{
	struct task_struct *tsk;
	struct task_struct *selected = NULL;
	unsigned long rem = 0;
	int tasksize;
	int i;
	short min_score_adj = OOM_SCORE_ADJ_MAX + 1;
	int minfree = 0;
	int selected_tasksize = 0;
	short selected_oom_score_adj;
	int array_size = ARRAY_SIZE(lowmem_adj);
  //獲取當前剩餘記憶體大小
	int other_free = global_page_state(NR_FREE_PAGES) - totalreserve_pages;
	int other_file = global_page_state(NR_FILE_PAGES) -
						global_page_state(NR_SHMEM) -
						total_swapcache_pages();
  //獲取陣列大小
	if (lowmem_adj_size < array_size)
		array_size = lowmem_adj_size;
	if (lowmem_minfree_size < array_size)
		array_size = lowmem_minfree_size;

  //遍歷lowmem_minfree陣列找出相應的最小adj值
	for (i = 0; i < array_size; i++) {
		minfree = lowmem_minfree[i];
		if (other_free < minfree && other_file < minfree) {
			min_score_adj = lowmem_adj[i];
			break;
		}
	}

	if (min_score_adj == OOM_SCORE_ADJ_MAX + 1) {
		return 0;
	}
	selected_oom_score_adj = min_score_adj;

	rcu_read_lock();
	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 (test_tsk_thread_flag(p, TIF_MEMDIE) &&
		    time_before_eq(jiffies, lowmem_deathpending_timeout)) {
			task_unlock(p);
			rcu_read_unlock();
			return 0;
		}
		oom_score_adj = p->signal->oom_score_adj;
    //小於目標adj的程式,則忽略
		if (oom_score_adj < min_score_adj) {
			task_unlock(p);
			continue;
		}
    //獲取的是程式的Resident Set Size,也就是程式獨佔記憶體 + 共享庫大小。
		tasksize = get_mm_rss(p->mm);
		task_unlock(p);
		if (tasksize <= 0)
			continue;

    //演算法關鍵,選擇oom_score_adj最大的程式中,並且rss記憶體最大的程式.
		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);
	}

	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);

		lowmem_deathpending_timeout = jiffies + HZ;
		set_tsk_thread_flag(selected, TIF_MEMDIE);
    //向選中的目標程式傳送signal 9來殺掉目標程式
		send_sig(SIGKILL, selected, 0);
		rem += selected_tasksize;
	}
	rcu_read_unlock();
	return rem;
}
  • 選擇oom_score_adj最大的程式中,並且rss記憶體最大的程式作為選中要殺的程式。
  • 殺程式方式:send_sig(SIGKILL, selected, 0)`向選中的目標程式傳送signal 9來殺掉目標程式。

另外,lowmem_minfree[]和lowmem_adj[]陣列大小個數為6,通過如下兩條命令:

module_param_named(debug_level, lowmem_debug_level, uint, S_IRUGO | S_IWUSR);    
module_param_array_named(adj, lowmem_adj, short, &lowmem_adj_size, S_IRUGO | S_IWUSR);

當如下節點資料傳送變化時,會通過修改lowmem_minfree[]和lowmem_adj[]陣列:

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

五、總結

本文主要從frameworks的ProcessList.java調整adj,通過socket通訊將事件傳送給native的守護程式lmkd;lmkd再根據具體的命令來執行相應操作,其主要功能 更新程式的oom_score_adj值以及lowmemorykiller驅動的parameters(包括minfree和adj);

最後講到了lowmemorykiller驅動,通過註冊shrinker,藉助linux標準的記憶體回收機制,根據當前系統可用記憶體以及parameters配置引數(adj,minfree)來選取合適的selected_oom_score_adj,再從所有程式中選擇adj大於該目標值的並且佔用rss記憶體最大的程式,將其殺掉,從而釋放出記憶體。

5.1 lmkd引數:

  • oom_adj:代表程式的優先順序, 數值越大,優先順序越低,越容易被殺. 取值範圍[-16, 15]
  • oom_score_adj: 取值範圍[-1000, 1000]
  • oom_score:lmk策略中貌似並沒有看到使用的地方,這個應該是oom才會使用。

想檢視某個程式的上述3值,只需要知道pid,檢視以下幾個節點:

/proc/<pid>/oom_adj
/proc/<pid>/oom_score_adj
/proc/<pid>/oom_score

對於oom_adj與oom_score_adj有一定的對映關係:

  • 當oom_adj = 15, 則oom_score_adj=1000;
  • 當oom_adj < 15, 則oom_score_adj= oom_adj * 1000/17;

5.2 driver引數

/sys/module/lowmemorykiller/parameters/minfree (代表page個數)
/sys/module/lowmemorykiller/parameters/adj (代表oom_score_adj)

例如:將1,6寫入節點/sys/module/lowmemorykiller/parameters/adj,將1024,8192寫入節點/sys/module/lowmemorykiller/parameters/minfree。策略:當系統可用記憶體低於8192個pages時,則會殺掉oom_score_adj>=6的程式;當系統可用記憶體低於1024個pages時,則會殺掉oom_score_adj>=1的程式。

相關文章