Android程序系列第八篇---LowmemoryKiller機制分析(下)

目錄概覽.png
前面程序系列已經更新了七篇,本文(基於kernel 3.18),基於前兩篇部落格,繼續梳理LMK殺程序機制下篇,主要總結LowmemoryKiller的中kernel的原理部分。
ofollow,noindex">Android程序系列第一篇---程序基礎
Android程序系列第二篇---Zygote程序的建立流程
Android程序系列第三篇---SystemServer程序的建立流程
Android程序系列第四篇---SystemServer程序的啟動流程
Android程序系列第六篇---LowmemoryKiller機制分析(上)
Android程序系列第七篇---LowmemoryKiller機制分析(中)上文說到如果lmkd.c中的use_inkernel_interface等於1,那麼就執行kernel空間的邏輯,lmkd中資料結構也不用更新,也不用lmkd中殺程序的邏輯,全部都交給LowmemoryKiller完成。在正式進入之前,思考幾個問題。
- LowmemoryKiller殺程序的策略具體是怎麼樣的?記憶體低到什麼情況下,LowmemoryKiller開始幹活呢?
- 有沒有永遠也殺不死的程序?
- minfree水位線和對應的adj,應用開發者能不能擅自修改,讓自己不易被殺死?
- lmkd擔當著AMS到LowmemoryKiller的橋樑,那lmkd程序會不會被自己或者LowmemoryKiller殺了呢?
- 應用開發者如果使得程序活的更好?
下面先整體過一遍LowmemoryKiller的機制,在回頭整理這些問題。
一、lowmemorykiller低記憶體時觸發程序查殺
1.1、基本原理
在linux中,有一個名為kswapd的核心執行緒,當linux回收存放分頁的時候,kswapd執行緒將會遍歷一張shrinker連結串列,並執行回撥,或者某個app啟動,發現可用記憶體不足時,則核心會阻塞請求分配記憶體的程序分配記憶體的過程,並在該程序中去執行lowmemorykiller來釋放記憶體。雖然之前沒有接觸過,大體的理解就是向系統註冊了這個shrinker回撥函式之後,當系統空閒記憶體頁面不足時會呼叫這個回撥函式。 struct shrinker的定義在linux/kernel/include/linux/shrinker.h中:
http://androidxref.com/kernel_3.18/xref/include/linux/shrinker.h 48struct shrinker { 49unsigned long (*count_objects)(struct shrinker *, 50struct shrink_control *sc); 51unsigned long (*scan_objects)(struct shrinker *, 52struct shrink_control *sc); 53 54int seeks;/* seeks to recreate an obj */ 55long batch; /* reclaim batch size, 0 = default */ 56unsigned long flags; 57 58/* These are for internal use */ 59struct list_head list; 60/* objs pending delete, per node */ 61atomic_long_t *nr_deferred; 62}; 63#define DEFAULT_SEEKS 2 /* A good number if you don't know better. */ 64 65/* Flags */ 66#define SHRINKER_NUMA_AWARE (1 << 0) 67 68extern int register_shrinker(struct shrinker *); 69extern void unregister_shrinker(struct shrinker *); 70#endif 71
shrinker的註冊與反註冊
http://androidxref.com/kernel_3.18/xref/drivers/staging/android/lowmemorykiller.c 189static struct shrinker lowmem_shrinker = { 190 .scan_objects = lowmem_scan, 191 .count_objects = lowmem_count, 192 .seeks = DEFAULT_SEEKS * 16 193};
http://androidxref.com/kernel_3.18/xref/drivers/staging/android/lowmemorykiller.c 195static int __init lowmem_init(void) 196{ 197 register_shrinker(&lowmem_shrinker); 198 return 0; 199} 200 201static void __exit lowmem_exit(void) 202{ 203 unregister_shrinker(&lowmem_shrinker); 204}
註冊完成之後,就回調lowmem_scan,這個基本上是lmk核心的程式碼
http://androidxref.com/kernel_3.18/xref/drivers/staging/android/lowmemorykiller.c 80static unsigned long lowmem_scan(struct shrinker *s, struct shrink_control *sc) 81{ //tsk程序結構體物件 82struct task_struct *tsk; //我們需要選擇一個程序殺掉,這個selected用來儲存不幸中獎的那個程序 83struct task_struct *selected = NULL; 84unsigned long rem = 0; 85int tasksize; 86int i; // OOM_SCORE_ADJ_MAX = 1000 87short min_score_adj = OOM_SCORE_ADJ_MAX + 1; 88int minfree = 0; //中獎的程序的記憶體佔用大小 89int selected_tasksize = 0; //中獎的程序的oom_score_adj值 90short selected_oom_score_adj; 91int array_size = ARRAY_SIZE(lowmem_adj); //global_page_state可以獲取當前系統可用的(剩餘)記憶體大小 92int other_free = global_page_state(NR_FREE_PAGES) - totalreserve_pages; 93int other_file = global_page_state(NR_FILE_PAGES) - 94global_page_state(NR_SHMEM) - 95total_swapcache_pages(); 96 97if (lowmem_adj_size < array_size) 98array_size = lowmem_adj_size; 99if (lowmem_minfree_size < array_size) 100array_size = lowmem_minfree_size; // 遍歷lowmem_minfree陣列找出相應的最小adj值,目的就是根據剩餘記憶體的大小,確定當前剩餘記憶體的級別的adj 101 for (i = 0; i < array_size; i++) { 102minfree = lowmem_minfree[i]; 103if (other_free < minfree && other_file < minfree) { 104min_score_adj = lowmem_adj[i]; 105break; 106} 107 } 108 109 lowmem_print(3, "lowmem_scan %lu, %x, ofree %d %d, ma %hd\n", 110sc->nr_to_scan, sc->gfp_mask, other_free, 111other_file, min_score_adj); 112 //系統的空閒記憶體數,根據上面的邏輯判斷出,low memory killer需要對adj高於多少(min_adj)的程序進行分析是否釋放。 //發現min_score_adj值為OOM_SCORE_ADJ_MAX + 1了,說明當前系統很好,不需要殺程序來釋放記憶體了 113 if (min_score_adj == OOM_SCORE_ADJ_MAX + 1) { 114lowmem_print(5, "lowmem_scan %lu, %x, return 0\n", 115sc->nr_to_scan, sc->gfp_mask); 116return 0; 117 } 118 119 selected_oom_score_adj = min_score_adj; 120 //核心一種同步機制 -- RCU同步機制 121 rcu_read_lock(); //遍歷所有程序 122 for_each_process(tsk) { 123struct task_struct *p; 124short oom_score_adj; 125 //核心執行緒kthread 126if (tsk->flags & PF_KTHREAD) 127continue; 128 129p = find_lock_task_mm(tsk); 130if (!p) 131continue; 132 133if (test_tsk_thread_flag(p, TIF_MEMDIE) && 134time_before_eq(jiffies, lowmem_deathpending_timeout)) { 135task_unlock(p); 136rcu_read_unlock(); 137return 0; 138} 139oom_score_adj = p->signal->oom_score_adj; // 如果當前找到的程序的oom_score_adj比當前需要殺的最小優先順序還低,不殺 140if (oom_score_adj < min_score_adj) { 141task_unlock(p); 142continue; 143} /獲取程序的佔用記憶體大小(rss值),也就是程序獨佔記憶體 + 共享庫大小 144tasksize = get_mm_rss(p->mm); 145task_unlock(p); 146if (tasksize <= 0) 147continue; //第一次迴圈,selected一定是null的 148if (selected) { //如果這個程序的oom_score_adj小於我們已經選中的那個程序的oom_score_adj, //或者這個程序的oom_score_adj等於我們已經選中的那個程序的oom_score_adj, // 但其所佔用的記憶體大小tasksize小於我們已經選中的那個程序所佔用記憶體大小,則繼續尋找下一個程序 149if (oom_score_adj < selected_oom_score_adj) 150continue; 151if (oom_score_adj == selected_oom_score_adj && 152tasksize <= selected_tasksize) 153continue; 154} //已經找到了需要尋找的程序,更新它的tasksize與oom_score_adj 155selected = p; 156selected_tasksize = tasksize; 157selected_oom_score_adj = oom_score_adj; 158lowmem_print(2, "select '%s' (%d), adj %hd, size %d, to kill\n", 159p->comm, p->pid, oom_score_adj, tasksize); 160 } //selected非null,說明已經找到了 161 if (selected) { 162long cache_size = other_file * (long)(PAGE_SIZE / 1024); 163long cache_limit = minfree * (long)(PAGE_SIZE / 1024); 164long free = other_free * (long)(PAGE_SIZE / 1024); 165trace_lowmemory_kill(selected, cache_size, cache_limit, free); //關鍵列印 166lowmem_print(1, "Killing '%s' (%d), adj %hd,\n" \ 167"to free %ldkB on behalf of '%s' (%d) because\n" \ 168"cache %ldkB is below limit %ldkB for oom_score_adj %hd\n" \ 169"Free memory is %ldkB above reserved\n", 170selected->comm, selected->pid, 171selected_oom_score_adj, 172selected_tasksize * (long)(PAGE_SIZE / 1024), 173current->comm, current->pid, 174cache_size, cache_limit, 175min_score_adj, 176free); //更新lowmem_deathpending_timeout 177lowmem_deathpending_timeout = jiffies + HZ; //設定程序的標記是TIF_MEMDIE 178set_tsk_thread_flag(selected, TIF_MEMDIE); //傳送SIGKILL訊號,殺死這個程序 179send_sig(SIGKILL, selected, 0); //更新一下rem值,殺死了一個程序所釋放的記憶體加上去 180rem += selected_tasksize; 181 } 182 183 lowmem_print(4, "lowmem_scan %lu, %x, return %lu\n", 184sc->nr_to_scan, sc->gfp_mask, rem); 185 rcu_read_unlock(); 186 return rem; 187}
上面程式碼就是lowmemorykiller核心原理的實現,可以小總結一下。
- 首先呼叫global_page_state,可以獲取當前系統可用的(剩餘)記憶體大小
- 遍歷lowmem_minfree陣列,根據other_free和other_file的剩餘記憶體的大小,確定當前剩餘記憶體的最小級別的min_score_adj
- 有了上面的adj之後,遍歷所有程序,獲取每個程序的rss大小,然後不斷迴圈,每次比較程序佔用的記憶體大小tasksize以及小於oom_score_adj,就能確定最終殺死哪個程序了
- 確定的程序儲存在selected變數中,對他傳送SIGKILL訊號殺死,並且更新rem大小
所以通過上面的總結已經可以回答我們第一個問題,“LowmemoryKiller殺程序的策略具體是怎麼樣的?記憶體低到什麼情況下,LowmemoryKiller開始幹活呢?”
1.2、拓展思考
1.2.1有沒有永遠也殺不死的程序呢?
要回答這個問題要看從哪個角度了,如果從native程序的角度回答,確實是存在的,我們通過前面幾篇的總結了解到,AMS自己會殺死程序,在記憶體緊張的時候也會通過lmkd請求lmk來殺程序。如果我們寫一個native程序同樣做到不死忙,比如我們給測試寫一個記憶體加壓的程式。因為要給對手機記憶體加壓,要保證這個加壓程序不被殺死,即使低記憶體,即使上層systemui執行一鍵清理,都能存活,如何做到呢?
int main(int argc, char *argv[]) { char text[100]; unsigned intsize; int percentage = atoi(argv[1]); //外面傳一個引數進來,佔用百分之多少的記憶體,最高門檻是60% if (percentage >= 0 && percentage <= 60) { printf("Memory footprint %d%%\n", percentage); } else { printf("Memory footprint %d%% error!! must be in range of 0-60%%\n", percentage); return 0; } //修改oom_score_adj為-1000 sprintf(text, "/proc/%d/oom_score_adj", getpid()); int fd = open(text, O_WRONLY); if (fd >= 0) { sprintf(text, "%d", -1000);//讓自己不被殺死 write(fd, text, strlen(text)); close(fd); } char task_name[50]; char *pid = (char*)calloc(10,sizeof(char)); strcpy(task_name, "logcat"); sprintf(text, "/proc/%s/oom_score_adj", pid); fd = open(text, O_WRONLY); if (fd >= 0) { sprintf(text, "%d", -1000);//讓logcat程序不被殺死 write(fd, text, strlen(text)); close(fd); } size = (unsigned int)mygetsize(); mallocflag(percentage, size); //佔用記憶體 while(1) {//等待正常退出 sleep(3); if((access("/sdcard/finishflag",F_OK)) == 0) { printf("create memroy process now end.......\n"); free(addr); addr = NULL; break; } } free(pid); return 0; } void mallocflag(int percentage, unsigned int size) { int i = 0; if(addr != NULL) { free(addr); addr = NULL; } printf("size= %d kb percentage=%d\n",size,percentage); float s=(float) size/1024/1024; printf("phonemem size %.2f G \n",s); float p =(float)percentage/100; printf("p %.2f rate\n",p); int sum=s*p*1204*1024*1024; printf("sum %d bye",sum); printf(" will malloc %d%% size = %d kb\n",percentage, sum); addr = (char *)malloc(sum); printf("malloc %d%% size = %dM\n",percentage, sum/1024/1024); system("echo 2 start >> /sdcard/memory_log"); if(addr == NULL) { printf("malloc %d%% fail \n", percentage); system("echo 2 fail >> /sdcard/memory_log"); exit(0); //如果申請失敗,嘗試申請一般的記憶體 } else { myMalloc(addr, sum); printf("malloc %d%% success \n", percentage); system("echo 2 success >> /sdcard/memory_log"); } }
然後寫Android.mk在原始碼下面編譯就行了,其實這麼長一段程式碼,核心的地方就幾行,即把oom_score_adj修改為-1000
//修改oom_score_adj為-1000 sprintf(text, "/proc/%d/oom_score_adj", getpid()); int fd = open(text, O_WRONLY); if (fd >= 0) { sprintf(text, "%d", -1000);//讓自己不被殺死 write(fd, text, strlen(text)); close(fd); }
因為-1000是最小的adj值,即使lmk把其他的程序都殺光了,都不會輪到自己,即使自己的佔用的記憶體很多,其次AMS也監控不到,因為native程式是kernel管理的。當我們把編譯好的程式放到system/bin下面就能被上層的APP所使用的,當然這需要Root許可權。所以從另外一個角度來講,對於市面上沒有Root許可權的APP來說存活手段就比較難了,因為你沒有修改adj值的機會。唉,要修改oom_score_adj結點同樣需要Root許可權,且下次開機就沒有效果了。在系統中,有對一些APP保駕護航,比如Home。
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java 24049if (app == mHomeProcess) { 24050if (adj > ProcessList.HOME_APP_ADJ) { 24051// This process is hosting what we currently consider to be the 24052// home app, so we don't want to let it go into the background. 24053adj = ProcessList.HOME_APP_ADJ; 24054schedGroup = ProcessList.SCHED_GROUP_BACKGROUND; 24055app.cached = false; 24056app.adjType = "home"; 24057if (DEBUG_OOM_ADJ_REASON || logUid == appUid) { 24058reportOomAdjMessageLocked(TAG_OOM_ADJ, "Raise adj to home: " + app); 24059} 24060} 24061if (procState > ActivityManager.PROCESS_STATE_HOME) { 24062procState = ActivityManager.PROCESS_STATE_HOME; 24063app.adjType = "home"; 24064if (DEBUG_OOM_ADJ_REASON || logUid == appUid) { 24065reportOomAdjMessageLocked(TAG_OOM_ADJ, "Raise procstate to home: " + app); 24066} 24067} 24068}
HOME_APP_ADJ的值是600,這個相對來說很小了,系統中很多的程序是900+,所以系統中對於重要的程序一般都會加以保護,比如Home程序的adj與排程組都得到了一定的優先。
1.2.2、lmkd會不會被自己殺了呢?
對於這個疑問,我們檢視一下lmkd程序的oom_score_adj檔案就好了
2|sakura:/proc/589 # cat oom_adj -17 sakura:/proc/589 # cat oom_score_adj -1000
值也是-1000,顯然不能被自己殺死
1.2.3、給應用開發者的建議
對於App開發者來說,怎麼來存活呢,市面上保活手段很多,我之前也總結過 Android程序保活的一般套路 ,感興趣可以看一看。對於系統來說,對這麼多的APP。一碗水得端平了,不偏袒誰,資源的分配策略希望每一個app都能遵守,不要在做什麼其他的保活手段,在必要的情況一下,系統也給了一些措施,ActivityManagerService會根據系統記憶體以及應用的狀態通過app.thread.scheduleTrimMemory傳送通知給應用程式,App中的onTrimMemory(int level) 和onLowMemory() 就會被回撥,而Activity, Service, ContentProvider和Application都實現了這個介面,在回撥中我們可以做一些記憶體釋放的操作,這樣在同adj的時候,我們的程序就不會被中獎了。
應用處於Runnig狀態可能收到的level級別
TRIM_MEMORY_RUNNING_MODERATE 表示系統記憶體已經稍低
TRIM_MEMORY_RUNNING_LOW 表示系統記憶體已經相當低
TRIM_MEMORY_RUNNING_CRITICAL 表示系統記憶體已經非常低,你的應用程式應當考慮釋放部分資源
應用的可見性發生變化時收到的級別
TRIM_MEMORY_UI_HIDDEN 表示應用已經處於不可見狀態,可以考慮釋放一些與顯示相關的資源
應用處於後臺時可能收到的級別
TRIM_MEMORY_BACKGROUND 表示系統記憶體稍低,你的應用被殺的可能性不大。但可以考慮適當釋放資源
TRIM_MEMORY_MODERATE 表示系統記憶體已經較低,當記憶體持續減少,你的應用可能會被殺死
TRIM_MEMORY_COMPLETE 表示系統記憶體已經非常低,你的應用即將被殺死,請釋放所有可能釋放的資源
那麼我們一般需要釋放哪些資源呢? Android程式碼記憶體優化建議-OnTrimMemory優化
-
快取 快取包括一些檔案快取,圖片快取等,在使用者正常使用的時候這些快取很有作用,但當你的應用程式UI不可見的時候,這些快取就可以被清除以減少記憶體的使用.比如第三方圖片庫的快取.
-
一些動態生成動態新增的View.這些動態生成和新增的View且少數情況下才使用到的View,這時候可以被釋放,下次使用的時候再進行動態生成即可.比如原生桌面中,會在OnTrimMemory的TRIM_MEMORY_MODERATE等級中,釋放所有AppsCustomizePagedView的資源,來保證在低記憶體的時候,桌面不會輕易被殺掉.
-
最好的辦法是用TraceView或者Memrroy Monitor來看哪些物件佔用記憶體大,在決定是否釋放。