安卓手机内存优化调研

一 背景

    现在安卓手机在相机操作下,不可避免要大量消耗内存资源,同时当今手机如同电脑一样,前台相机,后台各种app进程,都会消耗内存。因而会出现相机场景下,系统可用内存不足,内存压力增加不少,对相机性能会带来严重的影响。因此需要调查相机场景下的内存优化方法。

二 memcg调研

memcg的好处:

1 监控进程使用内存

站在一个普通Linux开发者的角度,如果能控制一个或者一组进程所能使用的内存数,那么就算代码有bug,内存泄漏也不会对系统造成影响,因为可以设置内存使用量的上限,当到达这个值之后可以将进程重启。

我们可以通过cgroup的事件通知机制来实现对内存的监控,当内存使用量穿过(变得高于或者低于)我们设置的值时,我们的程序就会收到通知。
ps: 应该是用Memory thresholds接口来进行监控的。

有个实际应用场景就是:比如可以我们的camera程序代码中设置监听接口,如果camera相关进程耗内存大于设置的阀值时,camera程序会及时收到相应的事件通知。

2 抑制进程使用内存

当某个mem cgroup里面物理内存达到上限(memory.limit_in_bytes)后,系统的默认行为是kill掉该cgroup中继续申请内存的进程,那么怎么控制这样的行为呢?答案是配置memory.oom_control。
这个文件里面包含了一个控制是否为当前cgroup启动OOM-killer的标识。如果写0到这个文件,将启动OOM-killer,当内核无法给进程分配足够的内存时,将会直接kill掉该进程;如果写1到这个文件,表示不启动OOM-killer,当内核无法给进程分配足够的内存时,将会暂停该进程直到有空余的内存之后再继续运行。
ps: memcg里面可以有选择的开启oom-killer,当不启动oom-killer时,如上面说的,就可以达到抑制进程内存使用效果吧。

3 体验系统对整机内存的优化思路

从kernel memcg的设计思路,结合手机Android场景,可以看到整机内存分3大块:

1) 用户态进程管理的内存

典型的特点是该内存可以在系统有内存压力时,被系统随时回收掉的。具体思路就是该内存必须挂到内核的Lru链表上,然后文件页或者被回写,或者被抛弃掉,匿名页被压缩写到zram中。

另外该内存可以被oom时通过杀进程方式直接释放掉的。

2) 内核管理的内存

比如内核态文件系统做io操作自身申请的内存,还有进程fork时需要内核申请的内存等这些都是属于内核管理的内存。

这部分内存直接特点就是:cached部分可以在有系统内存压力时,被回收掉(通过那个shrink_slab函数来做),非cached部分则无论怎么有系统压力,也不能被回收掉了。

所以内核态不要出现内存泄露问题,否则系统很难纠正。用户态出现内存泄露了,系统可以纠正错误的,直接oom或者通过Lru链表被匿名页回收了。

这部分内存管理,memcg是通过专门的memcg.kmem节点进行管理的,有区别于1)中的内存memcg管理。

3) 一些ion或者gpu驱动自身申请的内存

前两部分,内核态的内存管理都能cover到,memcg也能负责管理到。但是第3部分内存,memcg不能管理到,内核态内存管理仅仅在有内存压力时,会释放掉其中的cached内存。

并且这部分代码多位于driver/stanging目录下面,不是内核主线代码,所以内核内存管理方面对这部分也关注的不多,会有些bug问题的。

memcg缺陷:

1 通用的缺陷

当一个进程从一个cgroup移动到另一个cgroup时,默认情况下,该进程已经占用的内存还是统计在原来的cgroup里面,不会占用新cgroup的配额,但新分配的内存会统计到新的cgroup中(包括swap out到交换空间后再swap in到物理内存中的部分)。
我们可以通过设置memory.move_charge_at_immigrate让进程所占用的内存随着进程的迁移一起迁移到新的cgroup中。
注意:迁移内存占用数据是比较耗时的操作。还有可能会失败的。

ps: 系统缺省情况下,没有设置这个memory.move_charge_at_immigrate。因为如上面说的,频繁更换mem cgroup的话,会导致内核态工作异常耗时。

进一步确认:

xref: /Android-r/kernel/msm-4.19/Documentation/cgroup-v1/memory.txt文档里面核对过,上面的说法是正确的
(Note: If we cannot find enough space for the task in the destination cgroup, we try to make space by reclaiming memory. Task migration may fail if we cannot make enough space.
Note: It can take several seconds if you move charges much.)

2 安卓场景下的特有缺陷

安卓里面有很多的多进程间的共享页,比如app创建后要和zygote进程共享很多内存页。这个memcg并没有提供对共享页的特殊操作接口,只是规定:由多个 memcg 共享的一页内存被交换出去。当该页被换回时,它会被计数到起初分配它的 memcg,即使这个 memcg 已经很久不用该页。

这样的话,如果某进程要动态迁移到其他新group时,如果不设置move_charge_at_immigrate的话,该进程原来分配的内存页就不能有效继续监控或者限制了。

设置这个move_charge_at_immigrate的话,那么跟原group内其他重要进程,比如zygote共享的内存页也会随之迁移到新group内,虽然zygote进程并没有迁移到新group内。

优化方法1:前后台分组调研

最理想的优化方法是对手机实现前后台分组,相机前台操作时,能够动态回收和抑制住后台app的内存使用,而不是一味的杀掉后台所有app。

其实结合上面提出的memcg缺陷,可以推测出:

为啥谷歌和高通在手机Android系统上不推崇设置memcg前后台分组。即使io,cpu现在最新的安卓r版本都已经做到了前后台分组了。估计是因为Android上面前后台进程不像互联网公司那种docker环境一样,对进程进行分组后,该进程估计会比较稳定地呆在这个组里面的。
Android里面前后台里面进程在时刻的迁移变化,一旦某进程变化了自己的归属mem cgroup,不做内存迁移的话,会有下面提到的问题。做内存迁移的话,就会比较耗时了。

进一步结合下面搜到的社区邮件讨论,可以再详细地发掘到高通不做memcg前后台分组的原因。

[PATCH] memcg: Provide knob for force OOM into the memcg

https://lkml.org/lkml/2014/12/16/190

该邮件讨论的是高通开发提的memcg patch内容,高通意识到在新进程要加入到新mem cgroup里面的话,如果该group已经达到limit的话,新进程要加入到该group就困难了。因此高通提了这个patch,想在进程加入group之前,先杀掉该group里面的几个进程,给新加入者腾点内存空间。

下面摘取了跟后来高通为啥不搞memcg前后台分组原因相关的重要讨论:

1) Johannes Weiner‘s reply:

Rather than scanning thousands or millions of page table entries to relocate a task and its private memory to another configuration domain, wouldn't it be easier to just keep the task in a dedicated cgroup and reconfigure that instead?
ps: 迁移task到新group时,需要移走task原来的内存页,这个比较耗时啊。

2) 高通开发回复:
Your suggestion is good. But in specific cases, we may have no choice but to migrate.

Take a case of an Android system where a process/app will never gets killed until there is really no scope of holding it any longer in RAM.
So, when that process was running as a foreground process, it has to belong to a group which has no memory limit and cannot be killed. Now, when the same process goes into background and sits idle, it can be
compressed and cached into some space in RAM. These cached processes are ever growing list and can be capped with some limit. Naturally, these processes belongs to different category and hence different cgroup which just controls such cached processes.
ps: 说的是高通意识到需要对手机上的进程进行内存前后台分组,前台mem cgroup里面没有mem limit,后台group里面需要设置mem limit,以抑制后台进程使用内存。但是这种前后台分组肯定需要做task migrate.

3) Johannes Weiner‘s reply:

Likewise, if you move the task but fail to migrate some pages, do you leave them behind in the foreground group where they are exempt from reclaim?
ps: 这个又说了task migrate的弊端。说的可能就是为啥高通后来不想进行内存前后台分组的原因了。

然后提出了per_app_memcg:
Your configuration can be rephrased using this: by putting all apps into their own groups, and setting the lower boundary to infinity for the foreground apps and to zero for the background apps, the kernel will always reclaim and OOM kill the background apps first.

重点总结

ps: 整个邮件大致意思是ohannes Weiner‘s建议高通放弃做Android上的memcg前后台分组,告知了若干风险点后,又建议高通开发者做一个per_app_memcg的方案,这样可以成功利用memcg也可以达到抑制后台进程的内存使用。

所以结合上面的一些资料查阅,可以看到在手机做memcg前后台分组是比较麻烦,有风险的。

优化方法2:per_app_memcg

这个特性我看了,大致思路是从app进程一创建时就进行memcg分组,而且是每个app进程一个分组,系统有多少个app,就有多少个分组。刚创建时就分组,这样可以避免上面提的进程迁移group的缺陷问题。

这样分组后的好处:

系统可以根据每个app的adj值,评估该app的重要性,从而对每个app都可以设置不同的该group使用内存最大上限。一旦超过该上限后,系统就会迫使该app自身做内存回收工作,或者超过hard limit时,还可以借助memcg抑制住该进程的内存使用。

这样在相机场景下,一旦抑制住手机后台进程的内存使用,对于相机自身性能提升,还有减少相机后台杀进程方面都有大的帮助。

不好的地方:

分的组太多,造成内核层每个group的lru链表都比较短,下面会提的由此造成的负作用。

Android r版本上实现per_app_memcg的思路:

xref: /android-r/system/core/libprocessgroup/processgroup.cpp
里面createProcessGroup函数里面引入了UsePerAppMemcg,并且可以创建per app级别的uid_pid_path,还做了 setProcessGroupSoftLimit函数。

ps: libprocessgroup库里面实现了创建per_app_memcg的接口函数。

/android-r/frameworks/base/core/jni/com_android_internal_os_Zygote.cpp#1703里面:
大概是每进程创建成功时,会call createProcessGroup(uid, getpid())去创建对应的per_app_memcg。

lmkd里面会设置完每app group的soft limit,最终在内核发挥作用是靠:mem_cgroup_soft_limit_reclaim这个函数,它在balance_pgdat函数(kswapd触发回收)和shrink_zones函数(直接内存回收)中均被调用到了,就是具体在调用shrink_node_memcg去回收系统全局的内存页之前,先回收下每app memcg中的超过soft limit的内存页。。

ps: 那这么大致看来,内核文档里面说soft limit是仅仅在系统有内存压力时,才会做超过该limit的group里面的内存回收。上面的内存压力触发,就是指的kswapd回收和直接内存回收被触发了。

per_app_memcg的缺陷

Query on per app memory cgroup

https://www.spinics.net/lists/linux-mm/msg121665.html

这是高通望社区提的实现per_app_memcg功能的邮件讨论,里面提到应用per_app_memcg的弊端:

Is there a way to mitigate this problem of small lru sizes, priority drop and kswapd cpu consumption.

ps: 上面说的就是per_app_memcg应用的负作用。

Soft limit setting can be tricky, but my advise is to set it based on how much you see a particular cgroup using when the system is under memory pressure.
这个地方说了设置soft limit的弊端吧。

总结

可能是考虑到上面的负作用,所以最后仅仅在low ram device上部署了per_app_memcg功能

三 app compact调研

从上面看到,在Android平台上部署memcg分组有很多局限性,但是为了达到能够成功抑制后台进程使用内存,解放前台进程内存使用压力,高通不得不仅在low ram device上部署了memcg分组。

但是非low ram device设备上,如何比如在相机场景下,做到抑制后台进程内存使用,解放相机进程使用内存压力呢?

安卓r版本上,看到谷歌和高通的发力点:一个是基于psi的用户态lmkd杀进程,另外一个就是app compact。

lmkd杀进程缺陷

lmkd杀进程这个是把内核态的lowmem killer上移到用户态,通过psi提前感知到系统内存分配压力,及时杀进程,来释放内存压力。

这样的思路虽然没问题,杀进程的确是最快地释放内存压力的方法。但是实际应用中,存在以下问题:

1:后台app进程有各种保活黑科技,严重情况下,比如在双开场景中,曾出现过高adj值的双开进程频繁被杀后,再被重启。这样lmkd一直杀该进程,从而阻挡住了lmkd杀其他进程,这种场景中,杀进程策略等同于失效了。

2:后台三方app有很多手段会突破系统封锁,到达adj visible和perce级别,这样persistent,visible和perce 3个级别的进程数量和耗内存量越来越多,同时lmkd不敢轻易杀这个级别的进程,以免被用户投诉,可感知重要app没了。

所以光靠lmkd杀进程来达到优化内存,提升相机性能还不行。

app compact

app compact是基于以前内核里面就有的process reclaim接口,谷歌和高通对其做了优化,重点是fwk层对这个接口做了封装,可以在进程adj发生变化时,对低优先级进程进行内存回收,从而为高优先级进程提供更多的可用内存。

使用app compact的好处

1:属于通用的主动内存回收的好处,能够在内存降低低阀值前,对内存进行主动回收,可以使得lmkd杀进程的水位线提升(具体到lmkd代码是other_free进一步增多),这样使得系统杀进程阀值提高,减少杀后台高优先级进程的概率。

2:可以在用户态配置策略,挑选低优先级进程进行回收, 避免对内存进行全局回收,从而影响到高优先级进程的内存使用。

不足之处:

1:进行匿名页回收时,会比较耗时点,和回收干净文件页相比。另外一旦做完回收后,如果进程又继续活跃使用内存,那么被回收的内存会被page refault,这样前面做的回收工作会部分失效。

2:只是在进程级别,进程粒度上对内存进行回收的,对于系统中unused内存页无法及时free掉(比如已死亡进程,但是还残留在pagecache中的内存页)。

3:对进程做app compact时,该进程会进入到D状态,被block住,而且回收时,和直接杀进程相比,是要多耗点cpu资源的。

app compact的有效部署

谷歌为了降低app compact耗时,以及减少对系统的负面影响,在fwk ams模块层做了不少工作。详见下面文件中MemCompactionHandler的具体实现:

/android-r/frameworks/base/services/core/java/com/android/server/am/CachedAppOptimizer.java

还有同目录的oomadjuster.java文件里面的筛选待回收的进程方面工作。

同时内核层process reclaim接口里面也有待优化点,比如限制只有page_mapcount(page) == 1时,才能对该page进行回收,这样会减轻匿名页的回收耗时。

四 非相机进程内存抑制方面的设想

上面无论是Android的per_app_memcg,还是app compact优化思路都是在重在做内存回收。

但是相机场景下,很容易存在后台高优先级进程异常活跃,lmkd不敢轻易杀,但是app compact如果对其做内存回收的话,会存在回收完后,就被这些活跃进程重新以page refault形式加载到内存里面,继续消耗内存,这样的话,app compact工作的确会打折扣。

完美的方法是对后台这些进程做内存分配抑制。这牵扯到要对系统做Qos管理,相机场景下,如果系统可用内存很少的话,优先满足相机相关进程的内存分配需求。

最近的一些设想思路:

借鉴kernel memcg 里面如何对进程进行内存抑制的,然后在系统为用户态进程分配物理内存的必经路径:缺页异常里面判断如果不是相机相关进程的话,并且系统可用内存很少的情况下,就主动抑制该用户态进程的内存分配需求。

该抑制肯定是弹性的,要结合手机的业务需求来的,比如:

用户在操作相机拍照,与此同时后台有进程在播放音乐。

这种场景下,要优先照顾相机性能,听音乐需求倒是其次的,所以在提前感知相机分配内存困难,会影响到相机性能的时候,要抑制住音乐播放进程的内存分配需求。如果相机拍完照,系统内存不再有压力,则立刻解除音乐播放进程的内存分配限制。

因此该抑制也肯定得是策略和机制相互分离的,内核提供内存抑制接口,用户态Android业务层做策略配置,决定什么抑制开始和结束。

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值