Android的LMK機制學習筆記

一、文章背景

????最近在分析一個應(yīng)用進后臺經(jīng)常被殺死問題,問題發(fā)生在一個定制的系統(tǒng)中,存在多個內(nèi)存動態(tài)清理工具(類似手機衛(wèi)士等工具,是廠商開發(fā)的預(yù)裝應(yīng)用,具備內(nèi)存不足時的進程查殺能力)。于是需要逐一排查,最終定位到是被Android系統(tǒng)的LMK機制kill掉的,但是因為對于LMK機制的不熟悉,導(dǎo)致定位了兩天時間,這里做一個學習筆記以供總結(jié)復(fù)盤。如有理解失當支持悉聽指教。
注:
(1) 文中的AMS皆指ActivityManager;
(2) LMK指LowMemoryKiller機制,其對應(yīng)的進程名是lmkd;
(3) 所有Android源碼基于Android SDK 28(Android9/Android P),kernel層基于3.18版本

1.1 LMK中kill進程的關(guān)鍵log(原生系統(tǒng)):

LMK中kill進程的關(guān)鍵log

1.2 內(nèi)核log信息(定制系統(tǒng),原生系統(tǒng)看上面的截圖里的log):

lowmemorykiller: Killing 'main' (1222) (tgid 1222), adj 100,\x0a   
to free 97596kB on behalf of 'kworker/u8:6' (3975) because\x0a   cache 19876kB is 
below limit 20480kB for oom_score_adj 100\x0a   defrag_free 3220kB\x0a  
 Free memory is -40312kB above reserved

二、粗略分析lowmemorykill.c中的實現(xiàn)邏輯

2.1 基本實現(xiàn)思路

  • Android的LMK機制是基于linux的OOM killer修改而來,具體實現(xiàn)是在驅(qū)動層,核心功能實現(xiàn)是在<font color=red>drivers/staging/android/lowmemorykiller.c</font>文件中,linux中的驅(qū)動實現(xiàn)都是以文件為載體的。
  • LowMemoryKiller會周期性的檢查當前系統(tǒng)的可用內(nèi)存,當剩余可用內(nèi)存較低時,便會觸發(fā)進程查殺策略,根據(jù)不同的可用內(nèi)存的閾值來殺掉相應(yīng)優(yōu)先級的進程。
  • 關(guān)鍵字:LowMemoryKiller 可用內(nèi)存閾值 進程優(yōu)先級 oom_score_adj

2.2 是一定條件滿足時觸發(fā)or系統(tǒng)啟動就開始執(zhí)行了?

2.3 核心邏輯

查看lowmemorykiller.c完整源碼
task_struct 定義在/include/linux/sched.h中:

2.3.1 定義一些“常量“:規(guī)則的量化標準
// 低內(nèi)存時lowmem_adj分級
static short lowmem_adj[6] = {
    0,
    1,
    6,
    12,
};
// 共分了4個檔次
static int lowmem_adj_size = 4;
// 4個檔次下的可用內(nèi)存預(yù)值
static int lowmem_minfree[6] = {
    3 * 512,    /* 6MB */
    2 * 1024,   /* 8MB */
    4 * 1024,   /* 16MB */
    16 * 1024,  /* 64MB */
};
static int lowmem_minfree_size = 4;
  • 疑問:lowmem_minfree數(shù)組中定義的內(nèi)存閾值是這么算的?
2.3.2 遍歷進程->查殺流程:lowmem_scan()函數(shù)
   // lowmem_scan()函數(shù)掃描進程,對比系統(tǒng)剩余內(nèi)存大小值,計算出當前屬于哪個閾值等級
78 // struct shrinker *s:
79 // struct shrink_control *sc: 
80 static unsigned long lowmem_scan(struct shrinker *s, struct shrink_control *sc)
81{
82  struct task_struct *tsk;
83  struct task_struct *selected = NULL;
84  unsigned long rem = 0;
85  int tasksize;
86  int i;
87  // score_adj的值越大優(yōu)先級越低
87  short min_score_adj = OOM_SCORE_ADJ_MAX + 1;
88  int minfree = 0;
89  int selected_tasksize = 0;
90  short selected_oom_score_adj;
91  // lowmem_adj數(shù)組的size=4
91  int array_size = ARRAY_SIZE(lowmem_adj);
92  // other_free: 獲取剩余內(nèi)存大小
92  int other_free = global_page_state(NR_FREE_PAGES) - totalreserve_pages;
93  // other_file
93  int other_file = global_page_state(NR_FILE_PAGES) -
94                      global_page_state(NR_SHMEM) -
95                      total_swapcache_pages();
96  // step1: 確認內(nèi)存閾值分級數(shù)的size,目前看是4個等級;
97  if (lowmem_adj_size < array_size)
98      array_size = lowmem_adj_size; // 4
99  if (lowmem_minfree_size < array_size)
100     array_size = lowmem_minfree_size; // 4
101 // step2: 
    // 從6、8、16、64M中按從小到大逐個對比,找到當前系統(tǒng)剩余內(nèi)存大小屬于哪個檔次
102 // 例如:當剩余內(nèi)存低于64M時會得到min_score_adj = 12
101 for (i = 0; i < array_size; i++) {
102     minfree = lowmem_minfree[i];
103     if (other_free < minfree && other_file < minfree) {
104         min_score_adj = lowmem_adj[i];
105         break;
106     }
107 }
108 // 這里的log信息在分析進程被kill原因時也很關(guān)鍵!!
109 lowmem_print(3, "lowmem_scan %lu, %x, ofree %d %d, ma %hd\n",
110         sc->nr_to_scan, sc->gfp_mask, other_free,
111         other_file, min_score_adj);
112 // min_score_adj == OOM_SCORE_ADJ_MAX + 1代表未從內(nèi)存閾值數(shù)組中找到等級
    // 說明剩余內(nèi)存至少大于64M,當前系統(tǒng)剩余內(nèi)存未達到需要kill某個進程的地步,
    // 所以直接return結(jié)束本次掃描
113 if (min_score_adj == OOM_SCORE_ADJ_MAX + 1) {
114     lowmem_print(5, "lowmem_scan %lu, %x, return 0\n",
115              sc->nr_to_scan, sc->gfp_mask);
116     return 0;
117 }
118 
119 selected_oom_score_adj = min_score_adj;
120
121 rcu_read_lock();
    // step3: 遍歷
122 for_each_process(tsk) {
123     struct task_struct *p;
124     short oom_score_adj;
125
126     if (tsk->flags & PF_KTHREAD)
127         continue;
128     // 代表進程的指針
129     p = find_lock_task_mm(tsk);
130     if (!p)
131         continue;
132
133     if (test_tsk_thread_flag(p, TIF_MEMDIE) &&
134         time_before_eq(jiffies, lowmem_deathpending_timeout)) {
135         task_unlock(p);
136         rcu_read_unlock();
137         return 0;
138     }
        // 從進程的結(jié)構(gòu)體中讀取oom分數(shù):oom_score_adj,這個值是否和上面定義的lowmem_adj屬于相同的取值范圍??在哪里賦值的??
139     oom_score_adj = p->signal->oom_score_adj;
        // 從進程中讀到的oom_score_adj < min_score_adj,
        // 說明p進程的優(yōu)先級是高于當前低內(nèi)存閾值對應(yīng)的優(yōu)先級的,不進行kill處理,跳過
140     if (oom_score_adj < min_score_adj) {
141         task_unlock(p);
142         continue;
143     }
        // 獲取這個p進程所占用的內(nèi)存大小tasksize ,如果小于比我們當前選出進程的內(nèi)存,
        // 則無視。如果大于則選中這個進程。
144     tasksize = get_mm_rss(p->mm);
145     task_unlock(p);
146     if (tasksize <= 0)
147         continue;
        // 首次走到這里時因為selected初始值為NULL,所以直接走else邏輯
148     if (selected) {
            // p進程的優(yōu)先級是oom_score_adj,
            // 如果比當前內(nèi)存閾值對應(yīng)的oom_score_adj還小,
            // 代表p的優(yōu)先級是高于當前的內(nèi)存閾值等級的,也直接跳過
149         if (oom_score_adj < selected_oom_score_adj)
150             continue;
            // selected_tasksize初始值為0
            // oom_score_adj == selected_oom_score_adj代表當前進程p的oom_score_adj
            // 和內(nèi)存閾值等級相等,也跳過,說明比較優(yōu)先級時是不包含剛好優(yōu)先級相等,而是要低于的
151         if (oom_score_adj == selected_oom_score_adj &&
152             tasksize <= selected_tasksize)
153             continue;
154     }
        // 優(yōu)先級條件滿足,給selected變量賦值,用于后面對進程繼續(xù)操作
155     selected = p;
156     selected_tasksize = tasksize;
157     selected_oom_score_adj = oom_score_adj;
        // 這里的log信息也能用于分析當前滿足kill條件的進程是哪個!!
158     lowmem_print(2, "select '%s' (%d), adj %hd, size %d, to kill\n",
159              p->comm, p->pid, oom_score_adj, tasksize);
160     }
161 if (selected) {
        // PAGE_SIZE代表一個分頁的大小,等于4k
        // !!到這里也能反過來推出other_file和min_free單位是頁,下面為了轉(zhuǎn)成KB單位,
        // 所以乘以了每個單頁的KB大小,比如:一個分頁是4KB,PAGE_SIZE / 1024 = 4,
        // 再用other_file * 4結(jié)果就是剩余內(nèi)存有多少KB
162     long cache_size = other_file * (long)(PAGE_SIZE / 1024);
163     long cache_limit = minfree * (long)(PAGE_SIZE / 1024);
164     long free = other_free * (long)(PAGE_SIZE / 1024);
165     trace_lowmemory_kill(selected, cache_size, cache_limit, free);
        // !!流程走到這里,下一步就是發(fā)送SIGKILL信號了,
        // 看到下面這條log就能確定進程即將被kill
166     lowmem_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",
170              selected->comm, selected->pid,
171              selected_oom_score_adj,
172              selected_tasksize * (long)(PAGE_SIZE / 1024),
173              current->comm, current->pid,
174              cache_size, cache_limit,
175              min_score_adj,
176              free);
177     lowmem_deathpending_timeout = jiffies + HZ;
178     set_tsk_thread_flag(selected, TIF_MEMDIE);
179     send_sig(SIGKILL, selected, 0);
180     rem += selected_tasksize;
181 }
182 // rem
183 lowmem_print(4, "lowmem_scan %lu, %x, return %lu\n",
184          sc->nr_to_scan, sc->gfp_mask, rem);
185 rcu_read_unlock();
186 return rem;
187}

經(jīng)過 for_each 的遍歷, selected 就是我們選出要釋放掉的bad進程,它具有下面兩個條件:
第一、Oom_adj大于當前警戒閾值并且最大;
第二、在同樣大小的oom_adj中,占用內(nèi)存最多。

  • 總結(jié)以上的查殺流程如下圖:


    lmk機制的查殺流程

三、幾個疑問的思考

3.1 為什么在計算剩余可用內(nèi)存時,有other_free和other_file兩個數(shù)值?

單位都是頁數(shù)
定義:

92  // other_free: 獲取剩余內(nèi)存大小
92  int other_free = global_page_state(NR_FREE_PAGES) - totalreserve_pages;
93  // other_file
93  int other_file = global_page_state(NR_FILE_PAGES) -
94                      global_page_state(NR_SHMEM) -
95                      total_swapcache_pages();

3.2 lowmem_adj數(shù)組中定義的oom_scrore_adj和task_struct->signal->oom_score_adj的值即然能比較,那進程中的oom_score_adj是在哪里賦值的??

存儲在/proc/pid/oom_score_adj文件中。
比如查看init進程的oom_score_adj文件:


查看init進程的oom_score_adj值

3.3 內(nèi)核中kill掉了應(yīng)用進程,那怎么通知被殺進程做后續(xù)的進程數(shù)據(jù)清理操作?如何通知過去的?

????AMS中有一個bindApplication方法,內(nèi)部會在Application執(zhí)行attachApplication()時,綁定IApplicationThread的實現(xiàn)類的邏輯,會有binder斷開的監(jiān)聽邏輯。

3.3.1 com.android.server.am.ActivityManagerService#attachApplication()
@Override
public final void attachApplication(IApplicationThread thread, long startSeq) {
    synchronized (this) {
        int callingPid = Binder.getCallingPid();
        final int callingUid = Binder.getCallingUid();
        final long origId = Binder.clearCallingIdentity();
        // 繼續(xù)看該方法實現(xiàn)-> 3.3.2
        attachApplicationLocked(thread, callingPid, callingUid, startSeq);
        Binder.restoreCallingIdentity(origId);
   }
}
3.3.2 com.android.server.am.ActivityManagerService#attachApplicationLocked()
private final boolean attachApplicationLocked(IApplicationThread thread,
            int pid, int callingUid, long startSeq) {
        // ...
        // If this application record is still attached to a previous
        // process, clean it up now.
        if (app.thread != null) {
            handleAppDiedLocked(app, true, true);
        }
       // ...
       // IBinder.DeathRecipient的實現(xiàn)類是AMS$AppDeathRecipient->3.3.3
       AppDeathRecipient adr = new AppDeathRecipient(app, pid, thread);
       // 這里的代碼寫法跟我們Context.bindService(),傳入的ServiceConnect回調(diào)中拿到了遠程進程的IBinder對象,
       // 然后為了監(jiān)聽服務(wù)端進程死亡后執(zhí)行相應(yīng)的處理邏輯(重試或直接停止邏輯),是一樣的用法。                      
       thread.asBinder().linkToDeath(adr, 0);            
       app.deathRecipient = adr;                    
       // ...     
}
3.3.3 com.android.server.am.ActivityManagerService.AppDeathRecipient
@Override
public void binderDied() {
    synchronized(ActivityManagerService.this) {
       appDiedLocked(mApp, mPid, mAppThread, true, null);
    }
}

????注意這時候的CS模型中,應(yīng)用程序(ActivityThread$IApplicationThread)是服務(wù)端(binder接口的實現(xiàn)方),而運行在system_server進程的AMS是客戶端。AMS在bindApplication時,傳入了IApplicationThread引用,然后每當需要AMS和應(yīng)用進程通信時,都是通過IApplicationThread對象轉(zhuǎn)調(diào)到應(yīng)用進程。

  • LowMemoryKiller殺死應(yīng)用后可能在應(yīng)用的日志文件查詢不到“ActivityManager : Killing”相關(guān)log,必要時需要查看內(nèi)核層的log。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,205評論 2 375