Android进程系列第八篇---LowmemoryKiller机制分析(下)

目录概览.png

前面进程系列已经更新了七篇,本文(基于kernel 3.18),基于前两篇博客,继续梳理LMK杀进程机制下篇,主要总结LowmemoryKiller的中kernel的原理部分。
Android进程系列第一篇---进程基础
Android进程系列第二篇---Zygote进程的创建流程
Android进程系列第三篇---SystemServer进程的创建流程
Android进程系列第四篇---SystemServer进程的启动流程
Android进程系列第五篇---应用进程的创建流程
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 {
49  unsigned long (*count_objects)(struct shrinker *,
50                     struct shrink_control *sc);
51  unsigned long (*scan_objects)(struct shrinker *,
52                    struct shrink_control *sc);
53
54  int seeks;  /* seeks to recreate an obj */
55  long batch; /* reclaim batch size, 0 = default */
56  unsigned long flags;
57
58  /* These are for internal use */
59  struct list_head list;
60  /* objs pending delete, per node */
61  atomic_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进程结构体对象
82  struct task_struct *tsk;
  //我们需要选择一个进程杀掉,这个selected用来保存不幸中奖的那个进程
83  struct task_struct *selected = NULL;
84  unsigned long rem = 0;
85  int tasksize;
86  int i;
        // OOM_SCORE_ADJ_MAX = 1000
87  short min_score_adj = OOM_SCORE_ADJ_MAX + 1;
88  int minfree = 0;
  //中奖的进程的内存占用大小
89  int selected_tasksize = 0;
  //中奖的进程的oom_score_adj值
90  short selected_oom_score_adj;
91  int array_size = ARRAY_SIZE(lowmem_adj);
  //global_page_state可以获取当前系统可用的(剩余)内存大小
92  int other_free = global_page_state(NR_FREE_PAGES) - totalreserve_pages;
93  int other_file = global_page_state(NR_FILE_PAGES) -
94                      global_page_state(NR_SHMEM) -
95                      total_swapcache_pages();
96
97  if (lowmem_adj_size < array_size)
98      array_size = lowmem_adj_size;
99  if (lowmem_minfree_size < array_size)
100     array_size = lowmem_minfree_size;
        // 遍历lowmem_minfree数组找出相应的最小adj值,目的就是根据剩余内存的大小,确定当前剩余内存的级别的adj
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
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     //系统的空闲内存数,根据上面的逻辑判断出,low memory killer需要对adj高于多少(min_adj)的进程进行分析是否释放。
        //发现min_score_adj值为OOM_SCORE_ADJ_MAX + 1了,说明当前系统很好,不需要杀进程来释放内存了
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     //内核一种同步机制 -- RCU同步机制
121 rcu_read_lock();
        //遍历所有进程
122 for_each_process(tsk) {
123     struct task_struct *p;
124     short oom_score_adj;
125             //内核线程kthread
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     }
139     oom_score_adj = p->signal->oom_score_adj;
                // 如果当前找到的进程的oom_score_adj比当前需要杀的最小优先级还低,不杀
140     if (oom_score_adj < min_score_adj) {
141         task_unlock(p);
142         continue;
143     }
       /获取进程的占用内存大小(rss值),也就是进程独占内存 + 共享库大小
144     tasksize = get_mm_rss(p->mm);
145     task_unlock(p);
146     if (tasksize <= 0)
147         continue;
       //第一次循环,selected一定是null的
148     if (selected) {
       //如果这个进程的oom_score_adj小于我们已经选中的那个进程的oom_score_adj,
              //或者这个进程的oom_score_adj等于我们已经选中的那个进程的oom_score_adj,
              // 但其所占用的内存大小tasksize小于我们已经选中的那个进程所占用内存大小,则继续寻找下一个进程
149         if (oom_score_adj < selected_oom_score_adj)
150             continue;
151         if (oom_score_adj == selected_oom_score_adj &&
152             tasksize <= selected_tasksize)
153             continue;
154     }
          //已经找到了需要寻找的进程,更新它的tasksize与oom_score_adj
155     selected = p;
156     selected_tasksize = tasksize;
157     selected_oom_score_adj = oom_score_adj;
158     lowmem_print(2, "select '%s' (%d), adj %hd, size %d, to kill\n",
159              p->comm, p->pid, oom_score_adj, tasksize);
160 }
    //selected非null,说明已经找到了
161 if (selected) {
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);
    //关键打印
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);
    //更新lowmem_deathpending_timeout
177     lowmem_deathpending_timeout = jiffies + HZ;
     //设置进程的标记是TIF_MEMDIE
178     set_tsk_thread_flag(selected, TIF_MEMDIE);
     //发送SIGKILL信号,杀死这个进程
179     send_sig(SIGKILL, selected, 0);
     //更新一下rem值,杀死了一个进程所释放的内存加上去
180     rem += selected_tasksize;
181 }
182
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}

上面代码就是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 int  size;
   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("phone  mem 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
24049        if (app == mHomeProcess) {
24050            if (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.
24053                adj = ProcessList.HOME_APP_ADJ;
24054                schedGroup = ProcessList.SCHED_GROUP_BACKGROUND;
24055                app.cached = false;
24056                app.adjType = "home";
24057                if (DEBUG_OOM_ADJ_REASON || logUid == appUid) {
24058                    reportOomAdjMessageLocked(TAG_OOM_ADJ, "Raise adj to home: " + app);
24059                }
24060            }
24061            if (procState > ActivityManager.PROCESS_STATE_HOME) {
24062                procState = ActivityManager.PROCESS_STATE_HOME;
24063                app.adjType = "home";
24064                if (DEBUG_OOM_ADJ_REASON || logUid == appUid) {
24065                    reportOomAdjMessageLocked(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来看哪些对象占用内存大,在决定是否释放。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容