Low Memory Killer in Android

目录

低内存管理(Linux vs Android)

Linux内存回收

shrink_slab原理

shrink_zone原理

oom killer

oom killer设计原则

OOM killer具体实现

android的lmk(Low Memory Killer)

Android系统特点

oom killer在android中的不足

​​​​​​​LMK概述

LMK提供的接口

接口说明

​​​​​​​minfree与adj初始化

LMK实现

​​​​​​​LMK驱动实现

​​​​​​​android进程管理

​​​​​​​Android组件

​​​​​​​Android进程生命周期

进程adj调整

adj值

​​​​​​​adj调整原则

​​​​​​​adj调整时机

​​​​​​​adj调整算法

如何降低被kill概率


低内存管理(Linux vs Android)

Linux内存回收

低内存情况下,Linux内存回收有二种模式:一种是直接内存回收,通过伙伴系统分配一大块内存或者需要创建一个很大的缓冲区时,如果没有足够多的free pages,那么系统会尽快进行页面回收;另一种是定期内存回收,定期唤醒kswapd内核线程,当系统空闲内存小于阈值时则进行页面回收。具体的系统调用过程,请参考图1所示。

由图1可知,不管是直接页面回收,还是周期页面回收,最终都调用shrink_slab()和shrink_zone()。直接页面回收,在一定的约束下,如果最终没能释放所需page,则调用OOM(out of memory) killer。

shrink_slab原理

先向操作系统内核注册shrinker函数,会在内存较少时主动释放一些内存。shrink_slab()会遍历shrinker链表,回调所有注册了shrinker函数的处理内存的操作。当前,linux操作系统中主要的shrinker函数有:

  1. shrink_dcache_memory():负责dentry缓存。
  2. shrink_icache_memory():负责inode缓存。
  3. mb_cache_shrink_fn():负责用于文件系统元数据的缓存。

shrink_zone原理

shrink_zone()的主要工作可以分为三步:

  1. 通过shrink_active_list()将页面从active移到inactive list;
  2. 通过shrink_inactive_list()将inactive list的页放入临时链表;
  3. 最终调用shrink_page_list()回收页。回收算法如图2所示。

           图1 内存回收系统调用

            图2 回收页算法

oom killer

Oom killer是kernel在内存耗尽时的最后手段,使用oom_badness(),为每个进程计算得分,移除得分高的进程。

系统调用如下:

out_of_memory()->select_bad_process->oom_badness()

OOM killer计算进程得分的策略是:损失最少的工作,释放最大的内存同时不伤及无辜,并且杀掉的进程数尽量少。

oom killer设计原则

OOM killer简单粗暴地作风并不符合linux的风格,并且也不符合linux机制与策略分离的原则。然而当系统内存不足时,已经是将死之态,也无需讲究,暴力往往是最有效的解决方案。虽然粗暴,但是手段上也需要尽可能地“精致”,oom killer从提出到现在,也已经进行了几次优化,目前的设计遵循下面几方面:

  • 以进程所占物理内存作为判断依据

  进程实际所需的是物理内存,而早期以进程的虚拟地址空间大小为基准,显然不准确。

  • 可配置的用户建议策略

有些进程占用物理内存很大,但是也在做很重要的事情,如KDE桌面主进程。因此必须将一部分控制权交出给用户层,用户可以根据具体情况,对进程重要度加权。

  • 简单并且合理的默认策略

  将特权进程,免于oom killer机制;如果按现有选择策略,无法选择出需要被结束的进程,那么就直接panic,因为已经无能为力;另外,杀子不杀父,因为如果kill父进程,还需要为子进程托孤,需要做很多事情,而这会加重内存不足的情况,况且父进程往往是关键服务,而子进程往往是工作都线程,kill父进程对用户的影响更大。

OOM killer具体实现

  • 提供给用户层的接口:  /proc/<pid>/oom_adj & /proc/<pid>/oom_score_adj

    oom_adj是以前的接口,为了兼容低版本,现在还保留此接口,但是最好不要再使用了。

    oom_score_adj的值会影响各个进程的最终得分,范围是-1000(OOM_SCORE_ADJ_MIN)~1000(OOM_SCORE_ADJ_MAX)。用户空间可以调整进程的oom_score_adj值,来影响oom killer的行为,值越大,进程越容易被kill,-1000可以关闭oom killer对此进程的作用。

  • 算法

oom_badness是oom killer选择“bad”进程的核心算法,流程图如图3所示。

         图3 oom_badness流程图

android的lmk(Low Memory Killer)

 Linux 已经有oom killer了,那android为何又要引入LMK呢?

Android系统特点

Android是嵌入式系统,通常运行在内存很有限的设备上(如手机,平板)。这类设备,有一个特点就是“屏幕独占性”,即要调出一个任务,意味着必须退出或隐藏当前的任务。另外,此类设备同时运行的任务不会太多,并且影响用户使用体验的任务比较好识别。除此之外,“高交互”的系统特征,决定必须提高系统响应速度,因为这直接影响用户的使用体验。而在嵌入式系统中,资源相当有限,如何在低配置设备中,提高响应速度,是android需要重点考虑的。

Android使用的特点,决定设计的方案:

  1. Android进程并不主动退出,而是作为“空进程”保留在内存中,以便用户再次进入该应用时,可以提高响应速度;
  2. 因为进程不主动退出,必须有一套机制能实时按一定的策略回收内存;
  3. Android进程的重要度,随着用户操作的改变而改变,即实时改变;

oom killer在android中的不足

  • 启动时机不合适

OOM killer是在内存被耗尽时,启动的极端内存回收机制。Android中用户对系统反应快慢非常敏感。别说在内存被耗尽时,就是在内存不足时,系统出现,反应延迟,也会严重影响用户体验。再加上android进程的“不主动退出”机制,所以需要周期检查,进行内存管理。

  • 选择进程策略不合适

OOM killer根据进程所占物理内存为主要判断依据,加上oom_score_adj和其它一些因素调整。在android中,物理内存占用多少,不能成为其主要判断依据,而应该根据进程的重要程度,在相同重要度的情况下,才考虑内存因素。进程的重要度是相对于用户而言的。

  • 选择进程范围不合适

OOM killer候选的进程是除init进程,内核线程以及一些特权进程外的所有进程。而android只需要管理zygote启动之后的进程,之前的native进程都不需要考虑。

  • 用户层控制权的不合适

OOM killer提供了oom_score_adj的接口,程序可以自己设置值,降低OOM时的得分。一般而言,这个值不会频繁去变更。这对于android系统特点3)并不适用。在android系统中,用户层需要大部分的控制权限,并且能够根据用户操作实时变更进程的重要度。

​​​​​​​LMK概述

由于android有自己的应用场景,而OOM killer并不能满足其要求,因此引入了LMK机制。主要解决android中“进程不主动退出”机制所引起的内存回收问题。

LMK复用了linux低内存管理中的shrink_slab机制,将lmk注册到shrinker键表中,那么linux中无论是直接回收还是定期回收内存,都可以调到lmk的处理。

LMK还复用了用户层控制接口(/proc/<pid>/oom_score_adj),并将其作为选择进程的主要依据。FW层的AMS作为类似于“任务管理器”,根据用户行为管理进程。

LMK的整体结构如图4所示。

              图4 LMK整体结构

LMK提供的接口

接口说明

LMK将剩余内存分为几个等级,最多支持6个等级,分级的策略则交与用户层制定,其值写入以下文件,作为LMK驱动的参数。minfree值以页为单位(一页为4K)。

LMK按重要度(adj)为进程分组,最多支持6组,组数一般都设成与minfree对应。与minfree一样,分组的策略也由用户层制定,并将值写入以下文件,作为LMK驱动的参数。

这里需要注意,前面OOM killer中已经介绍过,adj是低版本的接口,现在已经换成oom_score_adj。但是由于android上层一直使用旧接口,修改上层太麻烦,因此android用户层仍然继续使用adj接口,只是在kernel层将adj自动转换成oom_score_adj。

上面二个接口,就是用户层制定的,即在什么的内存情况下,LMK开始工作,并且以什么标准工作。表1配置,当内存剩余6656*4K=26M时,LMK将kill所有 adj为9及以上的进程。

Minfree

3072

4096

4608

6656

8704

10752

adj

0

1

3

9

11

15

          表1 mk配置​​​​​​​

​​​​​​​minfree与adj初始化

Android原生对minfree和adj的初始化,放在ProcessList:updateOomLevels()中。系统默认adj配置如下所示:

  系统给minfree给了二个标准配置,mOomMinFreeLow与mOomMinFreeHigh​​​​​​​

然后根据内存屏幕大小,决定用lowminfree还是highminfree。内存在300~700M之间用lowminfree;内存在700M及以上,用highminfree;屏幕在480*800~1280*800之间用lowminfree;屏幕1280*800及以下,用highminfree。内存与屏幕,只要有一个需要用highminfree ,最终就用highminfree。

除此之外,如果是64bit设备,minfree*1.5。还可以根据设备配置config:

对minfree微调,adj与minfree的值,最终调用lmkd写入module/lowmemorykiller/parameters/,

作为参数传入lowmemorykiller驱动。

LMK实现

    LMK整套机制,可以分为二部分,一个是LMK驱动,一个是AM对进程的管理。

​​​​​​​LMK驱动实现

前面已经提到,LMK是通过shrinker机制,将自己整合进kernel。在内存不足或者定期检查内存时,都会通过shrink_slab,回调lmk操作。Lowmem_shrink()就是lmk驱动的核心。具体操作流程参考图5。

          图5 lowmem_shrink流程图

​​​​​​​android进程管理

Lmk驱动只是机制,实现非常简单。Android运行时的进程管理才是LMK的复杂点,因为需要在运行时实时为每个进程打分(设置adj)。

​​​​​​​Android组件

Android中,进程是由组件组成,每个组件都扮演不同的角色。组件的不同状态决定进程的adj。四大组件分别是activity,services,Broadcast,Content Providers。

1.Activity

   Activity是屏幕上单独的虚拟UI。通常,activity是应用与用户交互的主要组件。在activity的生命周期中,有create,running,pause,stop,destroy之些状态,当在running状态时,表示用户正在操作这个activity。

          图6 Activity生命周期

2.Services

Services通常是在后台运行的组件,但是也可以在前台启动。应用一旦在后台启动service,即使用户切换到其它应用,service还会在后台运行。服务没有用户交互界面。

         图7 service生命周期

3.Content Providers

Content Providers为不同的应用提供内容(数据),支持在文件或数据库中存储结构化数据。其它应用可以通过content resolver访问数据。

4.Broadcast Receiver

可以接收系统范围的广播。应用可以发起广播信息给另外一个应用,如文件下载已经完成等。它没有任何用户界面,但是会在status bar上形成注意信息。

​​​​​​​Android进程生命周期

Android系统试图尽可能地保持应用进程,但是最终需要为新的或者更重要的进程回收内存,而移除老的进程。为了决定kill哪个进程,系统根据进程中运行的组件和这些组件的状态,给每个进程一个重要度。系统总是先kill最不重要的进程,来回收系统资源。

重要度分为5个level:(the first is most important and is killed last)

1.Foreground process(前台进程)

用户正在操作的进程。下面任何一个条件成立,都可以认为是前台进程。

  1. 包含用户正在交互的Activity(Activity的onResume()已经被执行);
  2. 包含Service,并且这个Service绑定于用户正在交互的activity;
  3. 包含Service,并且这个Service运行在前台-Service已经执行startForeground();
  4. 包含Service,并且这个Service正在执行其生命周期的callbacks(onCreate(),onStart(),or onDestroy());
  5. 包启BroadcastReceiver,并且其正在执行onReceive()方法。

在给定的任一时间,通常只有小部分Foreground process(前台进程)。

2.Visible process(可见进程)

没有任何前台组件,但是用户在屏幕上仍然可以看到。下面任何一个条件成立,都可以认为是可见进程。

  1. 包含一个不在前台Activity,但是用户仍可以看到(其onPause()方法已经被执行);这可能发生,例如,前台activity起了一个dialog,此时后台的activity不在前台,但是用户仍能看到。
  2. 包含一个Service,并且此Service绑定到visible/foreground activity。

3.Service process(服务进程)

运行着Service但不属于前台和可见的进程,且此service已经执行过startService()。尽管服务进程与用户可见的无关,但是却正在做用户关心的事(如正在后台播放音乐或者从网上下载数据),因此系统会尽可能地保持他们运行。

4.Background process(后台进程)

包含一个activity,当前对用户不可见(activity的onStop()已经被执行)。这些进程不会直接影响用户体验,系统会为前台/可见/服务进程回收内存,随时会kill他们。通常会有很多后台进程在运行,他们被保存在LRU(least recently used)列表中,确保用户最后使用的最后被kill。如果一个activity正确地实现了其生命周期方法,并且保存了其正确地状态,那么它被kill,就不会影响用户体验,因为当用户回到这个activity时,由于其已经保存了其状态,用户看到地还是原来的。

5.Empty process(空进程)

不包含任何存活应用组件的进程。保存这些进程,纯粹就是为了缓存,方便下次组件运行在此进程时,加速启动时间。系统为了在进程缓存和kernel缓存间平衡系统资源,总是会kill这些进程。

进程adj调整

adj值

  adj的有效范围从-17~15,各值所代表的意思,如表2所示:

adj

意思

UNKNOWN_ADJ

16

实现需要,一般不会给进程设置,只做为临时值过渡

CACHED_APP_MAX_ADJ

15

进程不可见,并且只包启activity组件

CACHED_APP_MIN_ADJ

9

进程不可见,并且只包含activity组件

SERVICE_B_ADJ

8

包含service组件的进程分成二组,比起A,B组中的service相对比较陈旧,对用户影响小。

PREVIOUS_APP_ADJ

7

用户使用的前一个应用的进程。提高此进程优先级,是因为按back键返回到前一个应用的操作非常普通。

HOME_APP_ADJ

6

Home进程

SERVICE_ADJ

5

含serivce组件的进程

HEAVY_WEIGHT_APP_ADJ

4

重量级应用的进程

BACKUP_APP_ADJ

3

正在备份操作的进程

PERCEPTIBLE_APP_ADJ

2

进程含有用户可感知的组件,如后台音乐播放器

VISIBLE_APP_ADJ

1

进程含有用户可见的activity

FOREGROUND_APP_ADJ

0

前台进程

PERSISTENT_SERVICE_ADJ

-11

系统进程或长驻进程绑定的进程

PERSISTENT_PROC_ADJ

-12

系统长驻进程,如电话。

SYSTEM_ADJ

-16

系统进程,默认设为-16

NATIVE_ADJ

-17

系统不管的native进程,统一设为-17

​​​​​​​adj调整原则

Android基于进程中存活着组件的重要度,尽可能地提高进程的重要水平。例如,一个进程既有一个service,又有一个visible activity,那么进程会被标为可见进程,而非服务进程。

另外,如果其它进程依赖一个进程,那么这个进程的重要度也可能提高-提供服务的进程,其重要度不能低于其服务的进程。例如:进程A为进程B提供content provider,或者进程A的Service绑定于进程B的组件,那么进程A的重要度至少等同于进程B。

因为运行service的进程,其重要度比运行后台activity的进程高,所以如果activity需要长时间的操作(尤其这个操作比activity存活时间长),那么启动service比简单的创建工作线程,效果好。例如,网页上上传图片的activity,应该启动service来执行上传操作,这样即使用户leave这个activity,上传操作仍旧会在后台继续。使用service,其操作的优先级至少是“服务进程”,而不需要关心activity。同理,broadcast receivers也推荐使用service,而非将耗时操作放在线程里。

​​​​​​​adj调整时机

  只要组件状态发生变化,就会进行adj调整,具体看下面列出的各个函数。

  • Serivce状态变化

        bindServiceLocked

        unbindServiceLocked

        realStartServiceLocked

        sendServiceArgsLocked

        bringDownServiceLocked

        removeConnectionLocked

        serviceDoneExecutingLocked

  • Contentprovider处理

        getContentProviderImpl

        removeContentProvider

        removeContentProviderExternalUnchecked

        publishContentProviders

  • Broadcast处理

        processCurBroadcastLocked

        deliverToRegisteredReceiverLocked

        processNextBroadcast

  • Activity状态变更

        realStartActivityLocked

        resumeTopActivityInnerLocked

        finishSubActivityLocked

        finishVoiceTask

        finishCurrentActivityLocked

        destroyActivityLocked

  • Application调整

        addAppLocked

        attachApplicationLocked

        appDiedLocked

        setSystemProcess

        setProcessForeground

        updateProcessForegroundLocked

        killAllBackgroundProcesses

        killPackageProcessesLocked

        foregroundTokenDied

        trimApplications

        bindBackupAgent

        unbindBackupAgent

​​​​​​​adj调整算法

Adj值调整,主要在AMS:computeOomAdjLocked()方法中实现。算法实现如图8所示。

                     图8 调adj算法

如果进程中有service和contentprovider组件,那么进程的adj还需要随client进程adj而调整。

以bindService启动service时,会有bind flag,某些flag值会影响client进程与service宿主进程。如表2所示:

Bind flag

影响

BIND_WAIVE_PRIORITY

此次binding服务,不影响进程(包含service的进程)调度与内存管理优先级。

BIND_ALLOW_OOM_MANAGEMENT

允许进程(包含service的进程)通过其正常的内存管理。

BIND_NOT_FOREGROUND

不允许此次连接提高service进程的优先级到“foreground调度优先级”,但是service进程的“内存优先级”还是会被提高到与client进程一样(即client没被kill之前,service绝不会被kill),但是为了cpu调度的目的,service进程会被留在后台。这个flag只会影响这种情况:client是前台进程,但是service是后台进程。

BIND_ADJUST_WITH_ACTIVITY

如果从activity发起binding服务,进程的优先级会随着activity的优先级调整而调整。

BIND_ABOVE_CLIENT

认为连接的service比client进程重要,如果oom,会先kill client进程。

BIND_IMPORTANT

对于client来说,这个服务非常重要,如果client是前台进程,那么服务所在进程也应该是前台进程。通常情况下,即使client是前台的,进程也只能提高到visibility的优先级。

BIND_NOT_VISIBLE

即使client是visible,也不考虑进程为visible

           表2 serivce绑定flag

如何降低被kill概率

    进程要做到完全不被kill,基本也不可能。除非进程是系统进程,由init启动,那么就可以继承init的adj(-17),这样即使system_server进程被kill了,也不会被kill,不过可以做到尽可能不被lmk选中。

  • 提供进程优先级

后台操作尽可能用service来实现,而不用线程实现,因为包含service的进程优先级比普通进程高。

重载系统back按键事件,使activity在后台运行,而不是被destory。

依赖于其他优先级高的进程。

  • 修改进程属性

    如phone进程,设置persistent属性。

  • 54
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Android内存机制在不同版本之间有着较大的演变,其中最主要的就是内存管理策略的变化。在早期的Android版本中,内存管理采用的是进程优先级的方式,即根据进程的重要性和消耗的资源量来确定内存使用的权重,但这种方式容易导致内存不足的情况,进而导致应用程序崩溃。 后来,Google在Android 2.0中引入了LowMemoryKiller机制,通过监控系统内存使用情况,当内存不足时自动清理不必要的进程,以释放内存资源。LowMemoryKiller机制的实现是通过kernel的oom-killer机制来实现的,当系统内存不足时,通过oom_adj值来判断哪些进程可以杀掉,以释放内存资源。在Android 2.x中,LowMemoryKiller机制主要依赖于进程oom_adj值的设置,以及进程的重要性和消耗的资源量来判断哪些进程可以被杀掉。 随着Android版本的不断升级,Google也对LowMemoryKiller机制进行了多次优化,主要包括: 1. Android 3.0中引入了memcg机制,通过将进程的内存资源划分为多个cgroup,实现对内存资源的精细化管理。 2. Android 4.0中引入了Lmkd机制,通过对进程的内存资源进行动态调整,以更加精准地释放内存资源。 3. Android 4.4中引入了Zram机制,通过将一部分物理内存作为压缩内存使用,提高内存使用效率。 4. Android 6.0中引入了Doze机制,通过限制应用程序的后台运行,以降低系统内存负载。 总的来说,Android的内存管理机制是不断演变和优化的过程,不断追求更加高效和精细化的内存管理方式,以保证系统的稳定性和性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

塞外totem

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值