Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android資訊 >> Android LowMemoryKiller 原理分析

Android LowMemoryKiller 原理分析

編輯:Android資訊

一. 概述

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的進程。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved