目录
低内存管理(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函数有:
- 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属性。