一、概述
1.1 Android
Andorid的Low Memory Killer是在标准的linux kernel的OOM基础上修改而来的一种内存管理机制。当系统内存不足时,杀死不必要的进程释放其内存。不必要的进程的选择根据有2个:oom_adj和占用的内存的大小。
oom_adj代表进程的优先级,数值越高,优先级越低,越容易被杀死;对应每个oom_adj都可以有一个空闲内存的阀值。Android Kernel每隔一段时间会检测当前空闲内存是否低于某个阀值。假如是,则杀死oom_adj最大的不必要的进程,如果有多个,就根据oom_score_adj去杀死进程,直到内存恢复低于阀值的状态。
LowMemoryKiller的值的设定,主要保存在2个文件之中,分别是:
/sys/module/lowmemorykiller/parameters/adj
/sys/module/lowmemorykiller/parameters/minfree
例如:
# cat /sys/module/lowmemorykiller/parameters/adj
0,100,200,300,900,906
# cat /sys/module/lowmemorykiller/parameters/minfree
9216,11520,13824,16128,18432,23040
Android按照进程的不同类别,分为了6个等级
分别对应的空闲内存阈值,在frameworks\base\services\core\java\com\android\server\am\ProcessList.java中的函数updateOomLevels计算得来。ADJ在ProcessList.java中定义,不用做修改。
在Android系统里面,当app的状态发生变化,如:创建,收到广播,唤醒,放入后台等, ActivityManagerService的updateOomAdjLocked() 会computeOomAdjLocked(),然后,通过applyOomAdjLocked()把app的oom_adj值写入到Linux Kernel。
那么AN是如何计算每个app最终的oom_adj值的呢?
oom_adj的范围是[-16, 15],AN根据app进程的特性进行了分类,不同的类别,对应不同的数值。
- 9 ~ 15: 是缓存到后台的app。
- 8: service B 列表, 长时间未使用的service进程。
- 7: 前一个app。
- 6: Home app。
- 5: 包含service的app 进程。
- 4: 高权重的应用--隐藏的属性,AN P以上版本才能配置:android:cantSaveState=“true”
- 3: backup app--正在被备份的app。
- 2: 用户可感知的后台进程,如:后台背景音乐播放器。
- 1: 前台app启动的一些可见的组件,如: 弹出的Email activity。
- 0: 前台app.
- -11: persistent service -- android:persistent:=true
- -12: 常驻内存的系统app--如:系统自带的拨号app。
- -16: 系统进程--如:system_server进程。
- -17: 不受oom_adj的管理,该进程不会被杀死,也native process的默认值。
1.2 lowmemorykiller Driver部分
lowmemorykiller driver 位于 drivers/staging/android/lowmemorykiller.c
LMK通过注册shrinker来实现,shrinker是Linux kernel标准的回收page的机制,由内核线程kswapd负责监控。参见mm/vmscan.c中的kswapd。 当需要分配内存,发现可用内存不足时,则内核会阻塞请求分配内存的进程,进入slow path的内存申请逻辑进行回收(包括ZRAM的内存压缩),参见mm/page_alloc.c中的__alloc_pages_slowpath。 LMK核心思想:
LowMemoryKiller注册了shrinker--Linux Kernel的一个内存管理工具,当kernel需要回收内存时,会回调LowMemoryKiller的lowmem_shrink(),它先检查kernel 剩下多少内存,根据剩下的内存数量来匹配数组 lowmem_minfree[], 找到数组索引值,然后,再使用该索引值,从 lowmem_adj[]这个数组里面就得到目标oom_adj值,最终,在大于等于该目标oom_adj的进程中,杀死拥有最大oom_adj值的进程--send_sig(SIGKILL, selected, 0) 。算法其实很简单,就是两个一维数组的映射。
lowmemorykiller.c 中的lowmem_shrink注册到vm
static struct shrinker lowmem_shrinker = {
.shrink = lowmem_shrink,
.seeks = DEFAULT_SEEKS * 16
};
static int __init lowmem_init(void)
{
register_shrinker(&lowmem_shrinker);
return 0;
}
/*
* Add a shrinker callback to be called from the vm
*/
void register_shrinker(struct shrinker *shrinker)
{
atomic_long_set(&shrinker->nr_in_batch, 0);
down_write(&shrinker_rwsem);
list_add_tail(&shrinker->list, &shrinker_list);
up_write(&shrinker_rwsem);
}
然后,当Linux内存管理模块线程kswapd被调度时,就会通过 kswapd_shrink_node > ……>scan_objects 来触发lowmem_shrink内存扫描及执行lowmem killer。lowmem_shrink根据当前系统free内存和每个进程的oom_score_adj来决定当前哪个进程将会被killed。
内存回收时机
内存中有三个水位min <low < high,当内存达到low水位时,kswapd开始回收内存,直到内存达到high水位时停止kswapd;
如果kswapd回收速度小于内存消耗速度,内存水位下降到min水位,则direct reclaim开始回收内存,并会阻塞应用程序。 内存换出到swap分区的过程:
kswapd()-->balance_pgdat()-->shrink_zone()-->shrink_inactive_list()
Watermark的设置
每个zone有单独的水位,可以在/proc/sys/vm/min_free_kbytes中设置min水位,这个参数本身决定了系统中每个zone的watermark[min]的值大小。 然后内核根据min的大小并参考每个zone的内存大小分别算出每个zone的low水位和high水位值 通过命令查看zone的watermark:
XXXXX:/ # cat /proc/zoneinfo
Node 0, zone DMA
per-node stats
………….
pages free 285389
min 1392
low 2054
high 2716
node_scanned 0
…………..
相关代码见/mm/page_alloc.c: __setup_per_zone_wmarks
二、配置修改
关于lowmemkiller可配置项有
android\frameworks\base\core\res\res\values\config.xml
<!-- config_lowMemoryKillerMinFreeKbytesAbsolute和config_lowMemoryKillerMinFreeKbytesAdjust修改minfree的默认值 -->
<!-- config_lowMemoryKillerMinFreeKbytesAbsolute按照和MAX_ADJ的minfree比例,等比例计算个等级的空闲内存阈值。-->
<integer name="config_lowMemoryKillerMinFreeKbytesAbsolute">-1</integer>
<!-- config_lowMemoryKillerMinFreeKbytesAdjust按照和MAX_ADJ的minfree比例,等比例计算各等级的空闲内存阈值的加减量,然后默认值加或减加减量 -->
<integer name="config_lowMemoryKillerMinFreeKbytesAdjust">0</integer>
<!-- 修改/proc/sys/vm/extra_free_kbytes的值,这个参数告诉VM在后台回收程序开始工作的门限值和直接回收(内存分配进程做的)的门限值之间保持额外的free memory。需要低延迟的内存分配和在内存分配具有突发性的情况很有用,例如一个实时应用,接受和发送最大的信息可能达到200MB的网络数据(导致内核内存分配),这就需要200MB的额外的可用内存来避免直接回收内存相关的延迟。-->
<!-- config_extraFreeKbytesAbsolute替换默认值 -->
<integer name="config_extraFreeKbytesAbsolute">-1</integer>
<!-- config_extraFreeKbytesAdjust,在默认值的基础上加或减配置值 -->
<integer name="config_extraFreeKbytesAdjust">0</integer>
计算代码如下:
int minfree_adj = Resources.getSystem().getInteger(
com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAdjust);
int minfree_abs = Resources.getSystem().getInteger(
com.android.internal.R.integer.config_lowMemoryKillerMinFreeKbytesAbsolute);
if (minfree_abs >= 0) {
for (int i=0; i<mOomAdj.length; i++) {
mOomMinFree[i] = (int)((float)minfree_abs * mOomMinFree[i]
/ mOomMinFree[mOomAdj.length - 1]);
}
}
if (minfree_adj != 0) {
for (int i=0; i<mOomAdj.length; i++) {
mOomMinFree[i] += (int)((float)minfree_adj * mOomMinFree[i]
/ mOomMinFree[mOomAdj.length - 1]);
if (mOomMinFree[i] < 0) {
mOomMinFree[i] = 0;
}
}
}
...
int reserve_adj = Resources.getSystem().getInteger(com.android.internal.R.integer.config_extraFreeKbytesAdjust);
int reserve_abs = Resources.getSystem().getInteger(com.android.internal.R.integer.config_extraFreeKbytesAbsolute);
if (reserve_abs >= 0) {
reserve = reserve_abs;
}
if (reserve_adj != 0) {
reserve += reserve_adj;
if (reserve < 0) {
reserve = 0;
}
}
在updateOomLevels中,oom_adj和minfree通过socket传给lmkd服务,服务设置给驱动。
if (write) {
ByteBuffer buf = ByteBuffer.allocate(4 * (2*mOomAdj.length + 1));
buf.putInt(LMK_TARGET);
for (int i=0; i<mOomAdj.length; i++) {
// 注意:设置到kernel的minfree是内存页数,每个内存页大小为PAGE_SIZE(4KB)
buf.putInt((mOomMinFree[i]*1024)/PAGE_SIZE);
buf.putInt(mOomAdj[i]);
}
writeLmkd(buf); // 写给lmkd服务
SystemProperties.set("sys.sysctl.extra_free_kbytes", Integer.toString(reserve)); // 触发init.rc中on事件
}
reserve_adj 在init.rc中通过如下方式设置
on property:sys.sysctl.extra_free_kbytes=*
write /proc/sys/vm/extra_free_kbytes ${sys.sysctl.extra_free_kbytes}
注意: 设置到kernel的minfree是内存页数,每个内存页大小为PAGE_SIZE(4KB)
三、实践
开发的设备内存比较小,总共1G,可用动态分配只有770,952KB。
monkey煲机,发现很容易死机,log如下
[ 1875.198095] match process is mediaserver ,not kill it!
[ 1875.204129] match process is android.icetech ,not kill it!
[ 1875.210333] BUG: scheduling while atomic: flushcache.sh/9931/0x40000028
[ 1875.217600] Modules linked in: itech_rfid itech_led itech_wiegand bcmdhd mali(O) nand(O)
[ 1875.226864] CPU: 1 PID: 9931 Comm: flushcache.sh Tainted: G O 3.10.65 #18
[ 1875.235638] [<c0017fc4>] (unwind_backtrace+0x0/0xec) from [<c0014238>] (show_stack+0x20/0x24)
[ 1875.245261] [<c0014238>] (show_stack+0x20/0x24) from [<c06aa4c4>] (dump_stack+0x20/0x28)
[ 1875.254376] [<c06aa4c4>] (dump_stack+0x20/0x28) from [<c06a7818>] (__schedule_bug+0x54/0x6c)
[ 1875.263890] [<c06a7818>] (__schedule_bug+0x54/0x6c) from [<c06ae4a4>] (__schedule+0x80/0x754)
[ 1875.273513] [<c06ae4a4>] (__schedule+0x80/0x754) from [<c0054208>] (__cond_resched+0x20/0x2c)
[ 1875.283111] [<c0054208>] (__cond_resched+0x20/0x2c) from [<c06aec1c>] (_cond_resched+0x40/0x50)
[ 1875.292900] [<c06aec1c>] (_cond_resched+0x40/0x50) from [<c00e5cec>] (shrink_slab+0x29c/0x398)
[ 1875.302609] [<c00e5cec>] (shrink_slab+0x29c/0x398) from [<c0162e34>] (drop_caches_sysctl_handler+0x84/0xa0)
[ 1875.313536] [<c0162e34>] (drop_caches_sysctl_handler+0x84/0xa0) from [<c016e9e0>] (proc_sys_call_handler+0x94/0xb0)
[ 1875.325265] [<c016e9e0>] (proc_sys_call_handler+0x94/0xb0) from [<c016ea20>] (proc_sys_write+0x24/0x2c)
[ 1875.335874] [<c016ea20>] (proc_sys_write+0x24/0x2c) from [<c0117200>] (vfs_write+0xe8/0x184)
[ 1875.345388] [<c0117200>] (vfs_write+0xe8/0x184) from [<c01175a0>] (SyS_write+0x50/0x78)
[ 1875.354403] [<c01175a0>] (SyS_write+0x50/0x78) from [<c000f9e0>] (ret_fast_syscall+0x0/0x30)
[ 1935.389865] INFO: rcu_preempt self-detected stall on CPU { 0} (t=6000 jiffies g=68818 c=68817 q=1590)
[ 1935.390012] CPU: 0 PID: 9931 Comm: flushcache.sh Tainted: G W O 3.10.65 #18
[ 1935.390012] [<c0017fc4>] (unwind_backtrace+0x0/0xec) from [<c0014238>] (show_stack+0x20/0x24)
[ 1935.390012] [<c0014238>] (show_stack+0x20/0x24) from [<c06aa4c4>] (dump_stack+0x20/0x28)
[ 1935.390012] [<c06aa4c4>] (dump_stack+0x20/0x28) from [<c0015be4>] (smp_send_all_cpu_backtrace+0x64/0xd4)
[ 1935.390012] [<c0015be4>] (smp_send_all_cpu_backtrace+0x64/0xd4) from [<c0010da8>] (arch_trigger_all_cpu_backtrace+0x18/0x1c)
[ 1935.390012] [<c0010da8>] (arch_trigger_all_cpu_backtrace+0x18/0x1c) from [<c00a2e1c>] (rcu_check_callbacks+0x280/0x71c)
[ 1935.390012] [<c00a2e1c>] (rcu_check_callbacks+0x280/0x71c) from [<c00346a4>] (update_process_times+0x4c/0x78)
[ 1935.390012] [<c00346a4>] (update_process_times+0x4c/0x78) from [<c0074684>] (tick_sched_handle+0x58/0x64)
[ 1935.390012] [<c0074684>] (tick_sched_handle+0x58/0x64) from [<c0074930>] (tick_sched_timer+0x54/0x84)
[ 1935.390012] [<c0074930>] (tick_sched_timer+0x54/0x84) from [<c004a678>] (__run_hrtimer+0x1b8/0x2d0)
[ 1935.390012] [<c004a678>] (__run_hrtimer+0x1b8/0x2d0) from [<c004b314>] (hrtimer_interrupt+0x148/0x2b4)
[ 1935.390012] [<c004b314>] (hrtimer_interrupt+0x148/0x2b4) from [<c049099c>] (arch_timer_handler_phys+0x38/0x40)
从log看是触发了内除回收,结合出发前剩余内存信息,此时可用内存46364KB(此处log未体现),按照lowmemkiller的机制,此时的可用内存应该小于最小可用内存的阈值,影响了前台进程运行,因此触发内存压缩,回收内存。但是设备并没有配置内存压缩分区,内存不能再分配,因此崩溃。
查看不同oom_adj对应的minfree
xxxx:/ # cat /sys/module/lowmemorykiller/parameters/minfree
18432,23040,27648,32256,36864,46080
对应的字节数是(内存页数*4,即KB)
73728,92160,110592,1299024,147456,,184320
剩余内存的确小于了前台进程对应的可用内存阈值73728,触发了内存压缩。设备配置内存本来比较小,没有必要把阈值配置的很高,由于使用的是默认值,没有做修改,也没有打开low_mem,从代码看low_mem打开会牺牲一些性能(实测如此),因此没有打开,那么现在需要修改不同oom_adj对应的可用内存。
通过配置config_lowMemoryKillerMinFreeKbytesAdjust,把各阈值减少为
xxxx:/ # cat /sys/module/lowmemorykiller/parameters/minfree
9216,11520,13824,16128,18432,23040
对应KB值为
36864,46080,55296,64512,73728,92160
通过log,5s前的可用内存是87176KB,现在是46364KB,内存跳跃较大(此处应该有异常),查看
/proc/sys/vm/extra_free_kbytes
xxxx:/ # cat /proc/sys/vm/extra_free_kbytes
7200
这个值不是内存页数,单位是KB,因此只有7M,通过多次可用内存分析,内存需求较大是在20M左右,为系统稳定不卡顿运行,该值设置为25M,修改config_extraFreeKbytesAbsolute为25600即可。
通过修改后,做相同的压测,系统稳定性提高很多,已煲机21小时,还在煲机中。=_=