Android程序系列第六篇---LowmemoryKiller機制分析(上)
一、內容預覽
二、概述
前面程序系列已經更新了五篇,本文(基於Android O原始碼),梳理LMK殺程序機制上篇,主要總結AMS和LowmemoryKiller通訊的方式以及LowmemoryKiller的原理。
ofollow,noindex">Android程序系列第一篇---程序基礎
Android程序系列第二篇---Zygote程序的建立流程
Android程序系列第三篇---SystemServer程序的建立流程
Android程序系列第四篇---SystemServer程序的啟動流程
Android程序系列第五篇---應用程序的建立流程1、為什麼引入LowmemoryKiller?
程序的啟動分冷啟動和熱啟動,當用戶退出某一個程序的時候,並不會真正的將程序退出,而是將這個程序放到後臺,以便下次啟動的時候可以馬上啟動起來,這個過程名為熱啟動,這也是Android的設計理念之一。這個機制會帶來一個問題,每個程序都有自己獨立的記憶體地址空間,隨著應用開啟數量的增多,系統已使用的記憶體越來越大,就很有可能導致系統記憶體不足。為了解決這個問題,系統引入LowmemoryKiller(簡稱lmk)管理所有程序,根據 一定策略 來kill某個程序並釋放佔用的記憶體,保證系統的正常執行。
2、 LMK基本原理?
所有應用程序都是從zygote孵化出來的,記錄在AMS中mLruProcesses列表中,由AMS進行統一管理,AMS中會根據程序的狀態更新程序對應的oom_adj值,這個值會通過檔案傳遞到kernel中去,kernel有個低記憶體回收機制,在記憶體達到一定閥值時會觸發清理oom_adj值高的程序騰出更多的記憶體空間,這就是Lowmemorykiller工作原理。
3、LMK基本實現方案
所以根據不同手機的配置,就有對應的殺程序標準,這個標準用minfree和adj兩個檔案來定義:
/sys/module/lowmemorykiller/parameters/minfree:裡面是以","分割的一組數,每個數字代表一個記憶體級別。
/sys/module/lowmemorykiller/parameters/adj:對應上面的一組數,每個陣列代表一個程序優先順序級別
用小米note3舉例:
wangjing@wangjing-OptiPlex-7050:~$ adb root restarting adbd as root wangjing@wangjing-OptiPlex-7050:~$ adb shell jason:/ # cat /sys/module/lowmemorykiller/parameters/minfree 18432,23040,27648,32256,55296,80640 jason:/ # jason:/ # cat /sys/module/lowmemorykiller/parameters/adj 0,100,200,300,900,906 jason:/ #
minfree中數值的單位是記憶體中的頁面數量,一般情況下一個頁面是4KB,當記憶體低於80640的時候,系統會殺死adjj>=906級別的程序,當記憶體低於55296的時候,系統會殺死adj>=900級別的程序。不同配置的機器這兩個檔案會有區別,我把minfree檔案中的值理解成五個水位線,而adj這個檔案中的值與minfree檔案中的數值一一對應,意味著到達什麼樣的水位線,殺死對應數值的程序。
對於應用程序來說,也需要有自身的adj,由AMS負責更新。定義在oom_adj和oom_score_adj檔案中:
/proc/pid/oom_adj:代表當前程序的優先順序,這個優先順序是kernel中的優先順序。
/proc/pid/oom_score_adj:這個是AMS上層的優先順序,與ProcessList中的優先順序對應
比如檢視一下頭條程序的adj值,如下:
jason:/ # ps -ef |grep news u0_a15971131119 8 15:21:12 ?00:00:11 com.ss.android.article.news u0_a15971881119 0 15:21:12 ?00:00:00 com.ss.android.article.news:ad u0_a15972991119 1 15:21:16 ?00:00:02 com.ss.android.article.news:push u0_a15973841119 1 15:21:17 ?00:00:00 com.ss.android.article.news:pushservice root78386429 3 15:23:35 pts/0 00:00:00 grep news jason:/ # cat proc/7113/oom_adj 0 jason:/ # cat proc/7113/oom_score_adj 0 jason:/ # cat proc/7113/oom_adj 12 jason:/ # cat proc/7113/oom_score_adj 700 jason:/ #
當頭條位於前臺程序的時候oom_adj值為0,oom_score_adj值也是0,當退出成為後臺程序的時候,oom_adj值為12,oom_score_adj值是700。
其實oom_adj與oom_score_adj這兩個值是有換算關係的。
kernel/drivers/staging/android/lowmemorykiller.c 271static short lowmem_oom_adj_to_oom_score_adj(short oom_adj) 272{ 273 if (oom_adj == OOM_ADJUST_MAX) 274return OOM_SCORE_ADJ_MAX; 275 else 276return (oom_adj * OOM_SCORE_ADJ_MAX) / -OOM_DISABLE; 277}
其中OOM_ADJUST_MAX=-15,OOM_SCORE_ADJ_MAX=1000,OOM_DISABLE=-17,那麼換算就是:oom_score_adj=12*1000/17=700。高版本的核心都不在使用oom_adj,而是用oom_score_adj,oom_score_adj是一個向後相容。
綜上總結一下LMK的基本原理,如下

LMK基本原理.png
使用者在啟動一個程序之後,通常伴隨著啟動一個Activity遊覽頁面或者一個Service播放音樂等等,這個時候此程序的adj被AMS提高,LMK就不會殺死這個程序,當這個程序要做的事情做完了,退出後臺了,此程序的adj很快又被AMS降低。當需要殺死一個程序釋放記憶體時,一般先根據當前手機剩餘記憶體的狀態,在minfree節點中找到當前等級,再根據這個等級去adj節點中找到這個等級應該殺掉的程序的優先順序, 之後遍歷所有程序並比較程序優先順序adj與優先順序閾值,並殺死優先順序低於閾值的程序,達到釋放記憶體的目的。本文不討論adj的計算,只討論lmk原理。
三、LowmemoryKiller機制剖析
總的來說, Framework層通過調整adj的值和閾值陣列,輸送給kernel中的lmk,為lmk提供殺程序的原材料 ,因為使用者空間和核心空間相互隔離,就採用了檔案節點進行通訊,用socket將adj的值與閾值陣列傳給lmkd(5.0之後不在由AMS直接與lmk通訊,引入lmkd守護程序),lmkd將這些值寫到核心節點中。lmk通過讀取這些節點,實現程序的kill,所以整個lmk機制大概可分成三層。

LMK三層架構.png
3.1、Framework層
AMS中與adj調整的有三個核心的方法,如下
-
AMS.updateConfiguration:更新視窗配置,這個過程中,分別向/sys/module/lowmemorykiller/parameters目錄下的minfree和adj節點寫入相應數值;
-
AMS.applyOomAdjLocked:應用adj,當需要殺掉目標程序則返回false;否則返回true,這個過程中,呼叫setOomAdj(),向/proc/pid/oom_score_adj寫入oom_adj 後直接返回;
-
AMS.cleanUpApplicationRecordLocked & AMS.handleAppDiedLocked:程序死亡後,呼叫remove(),直接返回;
3.1.1、 AMS.updateConfiguration
public boolean updateConfiguration(Configuration values) { synchronized(this) { if (values == null && mWindowManager != null) { // sentinel: fetch the current configuration from the window manager values = mWindowManager.computeNewConfiguration(DEFAULT_DISPLAY); } if (mWindowManager != null) { // Update OOM levels based on display size. mProcessList.applyDisplaySize(mWindowManager); } ..... } }
mProcessList是ProcessList物件,呼叫applyDisplaySize方法,基於螢幕尺寸,更新LMK的水位線
/frameworks/base/services/core/java/com/android/server/am/ProcessList.java 198void applyDisplaySize(WindowManagerService wm) { 199if (!mHaveDisplaySize) { 200Point p = new Point(); 201// TODO(multi-display): Compute based on sum of all connected displays' resolutions. 202wm.getBaseDisplaySize(Display.DEFAULT_DISPLAY, p); 203if (p.x != 0 && p.y != 0) { //傳入螢幕的尺寸 204updateOomLevels(p.x, p.y, true); 205mHaveDisplaySize = true; 206} 207} 208}
傳入螢幕的尺寸更新水位線,邏輯很簡單
210private void updateOomLevels(int displayWidth, int displayHeight, boolean write) { 211// Scale buckets from avail memory: at 300MB we use the lowest values to 212// 700MB or more for the top values. 213float scaleMem = ((float)(mTotalMemMb-350))/(700-350); 214 215//根據螢幕大小計算出scale 216int minSize = 480*800;//384000 217int maxSize = 1280*800; // 1024000230400 870400.264 218float scaleDisp = ((float)(displayWidth*displayHeight)-minSize)/(maxSize-minSize); //google程式碼就是這麼寫的,表示不好評價了 219if (false) { 220Slog.i("XXXXXX", "scaleMem=" + scaleMem); 221Slog.i("XXXXXX", "scaleDisp=" + scaleDisp + " dw=" + displayWidth 222+ " dh=" + displayHeight); 223} 224 225float scale = scaleMem > scaleDisp ? scaleMem : scaleDisp; 226if (scale < 0) scale = 0; 227else if (scale > 1) scale = 1; 228int minfree_adj = Resources.getSystem().getInteger( 229com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAdjust); 230int minfree_abs = Resources.getSystem().getInteger( 231com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAbsolute); 232if (false) { 233Slog.i("XXXXXX", "minfree_adj=" + minfree_adj + " minfree_abs=" + minfree_abs); 234} 235 236final boolean is64bit = Build.SUPPORTED_64_BIT_ABIS.length > 0; 237//通過下面的運算,將mOomMinFreeLow和mOomMinFreeHigh經過運算 // 最後得出的 值存入mOomMinFree中,而如何計算這個值,是根據當前螢幕的解析度和記憶體大小來 238for (int i=0; i<mOomAdj.length; i++) { 239int low = mOomMinFreeLow[i]; 240int high = mOomMinFreeHigh[i]; 241if (is64bit) { 242// 64-bit機器會high增大 243if (i == 4) high = (high*3)/2; 244else if (i == 5) high = (high*7)/4; 245} 246mOomMinFree[i] = (int)(low + ((high-low)*scale)); 247} ....... 287 288if (write) { 289ByteBuffer buf = ByteBuffer.allocate(4 * (2*mOomAdj.length + 1)); 290buf.putInt(LMK_TARGET); 291for (int i=0; i<mOomAdj.length; i++) { 292buf.putInt((mOomMinFree[i]*1024)/PAGE_SIZE);//五個水位線 293buf.putInt(mOomAdj[i]);//與上面水位線對應的五個adj數值 294} 295//將AMS已經計算好的值通過socket傳送到lmkd 296writeLmkd(buf); 297SystemProperties.set("sys.sysctl.extra_free_kbytes", Integer.toString(reserve)); 298} 299// GB: 2048,3072,4096,6144,7168,8192 300// HC: 8192,10240,12288,14336,16384,20480 301}
這裡攜帶的命令協議是LMK_TARGET,它對應到kernel裡面執行的函式是cmd_target,要求kernel乾的事情就是更新兩面兩個檔案
/sys/module/lowmemorykiller/parameters/minfree /sys/module/lowmemorykiller/parameters/adj
這兩個檔案的作用我已經在開頭說過了,我把minfree檔案中的值理解成五個水位線,而adj這個檔案中的值與minfree檔案中的數值一一對應,意味著到達什麼樣的水位線,殺死對應數值的程序。而AMS裡面就是通過呼叫applyDisplaySize方法,基於螢幕尺寸以及機器的CPU位數,更新LMK的水位線的。
3.1.2、 AMS.applyOomAdjLocked
在看applyOomAdjLocked方法,這個方法的作用是應用adj,這個過程中,呼叫setOomAdj(),向/proc/pid/oom_score_adj寫入oom_adj 後直接返回;系統中更新adj的操作很頻繁,四大元件的生命週期都會影響著adj的值。而更新adj一般由applyOomAdjLocked完成。
/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java 22000private final boolean applyOomAdjLocked(ProcessRecord app, boolean doingAll, long now, long nowElapsed) { ......... 22009 22010if (app.curAdj != app.setAdj) { //之前的adj不等於計算的adj,需要更新 22011ProcessList.setOomAdj(app.pid, app.uid, app.curAdj); 22012if (DEBUG_SWITCH || DEBUG_OOM_ADJ || mCurOomAdjUid == app.info.uid) { 22013String msg = "Set " + app.pid + " " + app.processName + " adj " 22014+ app.curAdj + ": " + app.adjType; 22015reportOomAdjMessageLocked(TAG_OOM_ADJ, msg); 22016} 22017app.setAdj = app.curAdj; 22018app.verifiedAdj = ProcessList.INVALID_ADJ; 22019} ......... 22020 }
/frameworks/base/services/core/java/com/android/server/am/ProcessList.java 630public static final void setOomAdj(int pid, int uid, int amt) { 631if (amt == UNKNOWN_ADJ) 632return; 633 634long start = SystemClock.elapsedRealtime(); 635ByteBuffer buf = ByteBuffer.allocate(4 * 4); 636buf.putInt(LMK_PROCPRIO); 637buf.putInt(pid); 638buf.putInt(uid); 639buf.putInt(amt); //將AMS已經計算好的adj值通過socket傳送到lmkd 640writeLmkd(buf); 641long now = SystemClock.elapsedRealtime(); 642if ((now-start) > 250) { 643Slog.w("ActivityManager", "SLOW OOM ADJ: " + (now-start) + "ms for pid " + pid 644+ " = " + amt); 645} 646}
這裡攜帶的命令協議是LMK_PROCPRIO,對應kernel裡面cmd_procprio函式,要求kernel乾的事情是---把AMS傳送過來的adj值更新到下面的檔案中去。這樣記憶體緊張的時候,LMK就會遍歷核心中程序列表,殺死相應adj的程序了。
/proc/<pid>/oom_score_adj
3.1.3、 AMS.cleanUpApplicationRecordLocked & AMS.handleAppDiedLocked
程序死掉後,會呼叫該程序的ProcessList.remove方法,也會通過Socket通知lmkd更新adj。
/frameworks/base/services/core/java/com/android/server/am/ProcessList.java 651public static final void remove(int pid) { 652ByteBuffer buf = ByteBuffer.allocate(4 * 2); 653buf.putInt(LMK_PROCREMOVE); 654buf.putInt(pid); 655writeLmkd(buf); 656}
這裡攜帶的命令協議是LMK_PROCREMOVE,對應kernel裡面的cmd_procremove函式,要求kernel乾的事情是,當程序死亡了,刪除/proc/<pid>下面的檔案。
上面三大方法最後都是通過writeLmkd與lmkd通訊,現在看看writeLmkd中怎麼和lmkd通訊的,首先需要開啟與lmkd通訊的socket,lmkd建立名稱為lmkd的socket,節點位於/dev/socket/lmkd
658private static boolean openLmkdSocket() { 659try { 660sLmkdSocket = new LocalSocket(LocalSocket.SOCKET_SEQPACKET); 661sLmkdSocket.connect( 662new LocalSocketAddress("lmkd", 663LocalSocketAddress.Namespace.RESERVED)); 664sLmkdOutputStream = sLmkdSocket.getOutputStream(); 665} catch (IOException ex) { 666Slog.w(TAG, "lowmemorykiller daemon socket open failed"); 667sLmkdSocket = null; 668return false; 669} 670 671return true; 672}
當sLmkdSocket建立之後,就用它來發送資料到對端(lmkd)
674private static void writeLmkd(ByteBuffer buf) { 675//嘗試三次 676for (int i = 0; i < 3; i++) { 677if (sLmkdSocket == null) { 678if (openLmkdSocket() == false) { 679try { 680Thread.sleep(1000); 681} catch (InterruptedException ie) { 682} 683continue; 684} 685} 686 687try { 688sLmkdOutputStream.write(buf.array(), 0, buf.position()); 689return; 690} catch (IOException ex) { 691Slog.w(TAG, "Error writing to lowmemorykiller socket"); 692 693try { 694sLmkdSocket.close(); 695} catch (IOException ex2) { 696} 697 698sLmkdSocket = null; 699} 700} 701} 702}
四、總結
這篇文章主要是總結lmk的初步的工作原理,如何為系統的資源保駕護航。核心原理就是Framework層通過調整adj的值和閾值陣列,輸送給kernel中的lmk,為lmk提供殺程序的原材料。通過前面的分析AMS中給lmkd傳送資料原材料有三個入口,攜帶的命令協議也有三種,如下。
功能 | AMS對應方法 | 命令 | 核心對應函式 | |
---|---|---|---|---|
LMK_PROCPRIO | PL.setOomAdj() | 設定指定程序的優先順序,也就是oom_score_adj | cmd_procprio | |
LMK_TARGET | PL.updateOomLevels() | 更新/sys/module/lowmemorykiller/parameters/中的minfree以及adj | cmd_target | |
LMK_PROCREMOVE | PL.remove() | 移除程序 | cmd_procremove |
關於kenel中的工作流程,下面一篇分解。