Android LowMemoryKiller 原理分析
一. 概述
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, ¶m); //初始化【見小節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
目錄下的minfree
和adj
節點寫入相應資訊;
四. 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
的程式。
相關文章
- Android LowMemoryKiller 簡介Android
- Lowmemorykiller記憶體洩露分析記憶體洩露
- Android JNI原理分析Android
- 【Android Jetpack教程】ViewModel原理分析AndroidJetpackView
- Android深色模式適配原理分析Android模式
- Android UI 顯示原理分析小結AndroidUI
- Android 常用換膚方式以及原理分析Android
- Android進階5:SurfaceView實現原理分析AndroidView
- Android Hook框架Xposed原理與原始碼分析AndroidHook框架原始碼
- Android相容Java 8語法特性的原理分析AndroidJava
- Android外掛化原理分析(基於Neptune框架)Android框架
- Android Messenger原理AndroidMessenger
- Android Handler原理Android
- Android AIDL原理AndroidAI
- 藉助 AIDL 理解 Android Binder 機制——AIDL 的使用和原理分析AIAndroid
- Android官方架構元件Lifecycle:生命週期元件詳解&原理分析Android架構元件
- Android的延遲實現的幾種解決方案以及原理分析Android
- KVO原理分析
- ThreadLocal原理分析thread
- ReentrantLock原理分析ReentrantLock
- Xposed原理分析
- AST 原理分析AST
- SpringIOC原理分析Spring
- ThreadLocal 原理分析thread
- SparseArray原理分析
- SparseIntArray原理分析
- Promise原理分析Promise
- Handler原理分析
- mysqldump原理分析MySql
- HSF原理分析
- Android熱修復原理Android
- Android View 的工作原理AndroidView
- Android Animation 執行原理Android
- Docker 工作原理分析Docker
- Bitmap、RoaringBitmap原理分析
- ThreadLocalRandom類原理分析threadrandom
- isMemberOfClass、isKindOfClass原理分析
- JavaScript ==原理與分析JavaScript
- redis client原理分析Redisclient