目录
android的lmk(Low Memory Killer)
低内存管理(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函数有:
- shrink_dcache_memory():负责dentry缓存。
- shrink_icache_memory():负责inode缓存。
- mb_cache_shrink_fn():负责用于文件系统元数据的缓存。
shrink_zone原理
shrink_zone()的主要工作可以分为三步:
- 通过shrink_active_list()将页面从active移到inactive list;
- 通过shrink_inactive_list()将inactive list的页放入临时链表;
- 最终调用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使用的特点,决定设计的方案:
- Android进程并不主动退出,而是作为“空进程”保留在内存中,以便用户再次进入该应用时,可以提高响应速度;
- 因为进程不主动退出,必须有一套机制能实时按一定的策略回收内存;
- 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(前台进程)
用户正在操作的进程。下面任何一个条件成立,都可以认为是前台进程。
- 包含用户正在交互的Activity(Activity的onResume()已经被执行);
- 包含Service,并且这个Service绑定于用户正在交互的activity;
- 包含Service,并且这个Service运行在前台-Service已经执行startForeground();
- 包含Service,并且这个Service正在执行其生命周期的callbacks(onCreate(),onStart(),or onDestroy());
- 包启BroadcastReceiver,并且其正在执行onReceive()方法。
在给定的任一时间,通常只有小部分Foreground process(前台进程)。
2.Visible process(可见进程)
没有任何前台组件,但是用户在屏幕上仍然可以看到。下面任何一个条件成立,都可以认为是可见进程。
- 包含一个不在前台Activity,但是用户仍可以看到(其onPause()方法已经被执行);这可能发生,例如,前台activity起了一个dialog,此时后台的activity不在前台,但是用户仍能看到。
- 包含一个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属性。