使用内存回收技术对嵌入式系统进行性能优化
Ftrace训练营火热报名中:Ftrace训练营:站在设计者的角度来理解ftrace(限50人)。训练营第一期报名已圆满成功,好评如潮。第二期报名正在火爆进行中(咨询小月微信:linuxer2016)。
ARM安全架构训练营2期火热报名中:阅码场训练营:ARM安全架构之Trustzone/TEE实战--【介绍视频】。报名咨询客服(小月微信:linuxer2016)。
ARM架构与调优调试训练营火热报名中:阅码场训练营:ARM架构与调试调优。报名咨询客服(小月微信:linuxer2016)。
作者简介
小辉,一线码农,有多年丰富嵌入式操作系统开发工作经验。先后在南大富士通,某手机大厂和初创小公司任软件工程师,系统架构师等职务。对嵌入式系统开发和os性能优化工作很熟悉。
前传
嵌入式系统的内存回收还是比较重要的,因为这块涉及到程序运行性能。
嵌入式系统(比如平板,手机)会更加关注单机性能优化,因而会更加重视系统内存回收。
嵌入式系统不像互联网那种大型分布式服务器系统,他们往往内存和存储容量比较充裕,因而关注点在分布式方面,对单机性能不够重视。嵌入式系统,在有限的内存和存储空间因素制约下,会更加关注单机性能优化。
而内存回收这块是比较重要的,因为内存回收做的不好,内存压力得不到释放,最直接的是内存压力会转化为IO压力,对系统io性能造成影响。另外也会转换为cpu压力,影响程序的cpu资源使用。
所以结合我对内存回收方面的调研,想重点写下对Linux内核内存回收这块代码的理解,也想分享下我在这块的调研心得。
内存回收的重要性
这个内存回收方面的优化对系统到底有怎样的影响,我想举几个例子会详细透彻地讲下内存回收方面的优化,还有给系统带来的好处。
一 、内存回收在高负载系统性能方面的优化
1 问题现象:
移动设备上(比如平板,手机)后台u盘传输大批量数据时,前台桌面操作卡顿。
2 原因分析:
1: u盘拷贝数据量大时,会导致系统内存里面脏页变多,然后可用内存变少。
2: 然后前台平板操作时,会allocate_pages去分配内存页。这个时候会陷入slow path,去触发内存回收。
由于此时有大量脏页存在,内存回收会比较耗时,这样前台操作的内存分配性能也会变差。同时回收脏页时产生的io压力对前台操作时的io性能也会有影响。
因为前台的内存和io性能都受到了影响,所以前台平板操作才会变卡顿。
3 一点性能优化心得方面的精彩点评:
这是一个典型的单机系统里面比较特色的内存和io相互作用导致的性能问题。
1) 内存回收时,不得不做很多脏页回写工作
因为单机系统比如平板,手机,里面大都是buffer写,写到page cache里面就认为工作完成就成功返回了, 虽然省事,但是写到page cache里面必然会带来额外的内存消耗。
所以我们看到嵌入式系统里面很多场景下(比如上面的u盘拷贝场景),会造成某些时间段内系统page cache占内存多,其实是里面积累很多脏页。
pagecache占内存多,系统free内存就会变少。而偏偏系统的write back线程又不活跃(手机ext4文件系统里面开启延迟写后,系统每隔30s才回写一次脏页),这样就会造成 [内存分配 → 不得不做内存回收] 这样路径下,才会解决pagecache占内存多问题,具体就是触发做脏页回写工作。
2) 问题就在于:
系统的io写性能不好时,会影响到page cache里面的脏页释放。脏页一旦得不到有效释放,系统free内存越来越少,这样内存分配性能也会受到影响。
同时内存分配有问题或者回收效率不高,在处理脏页回写方面有缺陷时,也会产生额外的io压力。所以是内存和io相互影响,相互作用。
所以就有了内核社区每年那个storage and mm国际会议,单机系统里面出现性能问题时,内存和io往往需要同时去优化。
3) 先去优化内存,如果不行,再去优化io
系统的内存和io相互作用导致的性能问题出现时,最好用这种优化思路,原因很简单,内存优化成本低,风险小。
为什么国内有很多技术文章或者书籍都详细重点讲linux内核内存管理,为什么文件系统和io这块不去详细讲讲,其中有一个重要的原因是:
因为国内企业普遍看到了内存优化成本低,不行就多杀一些进程或者优化下内存分配和回收,即使是内存出问题,顶多重启一下系统,问题就没了。
但是io这块本身技术方面就比较复杂,光内核里面,从vfs到ext4具体文件系统,再到块设备层,再到存储bsp层,再到存储硬件芯片层,这些每个层优化的不好,都会出io性能问题,而且部署io性能优化方案,往往又要对这些每个层都要有所熟悉,这样才能保证优化副作用降到最小。
这还是讨论优化技术本身的,还没谈到,如果存储io出问题,手机上重要文件损坏,轻则用户发现文件丢失(这样的损坏和丢失是你再开机多少次都无法挽回的),重则无法开机事故就出来了。
详细可以见我的另外一篇技术文档:《手机Android存储性能优化架构分析》。
4 解决思路:
在社区高版本内核上面找了一些patch,这些patch是对内核内存回收方面的优化,重点也是优化脏页过多时,内存回收耗时问题的。
优化1:
1)优化原理
内存回收时,提前唤醒write-back内核线程,提前把所有的脏页都写入到磁盘上。这样交给专门的内核回写线程来做脏页回写工作,这样效率更高。
因为内存回收路径中做的脏页回收是离散写,具体是调用pageout → 文件系统writepage函数,这样每回只写一个page,在lru链表上积累的脏页比较多时,这样回写效率并不高,因此也影响了内存回收。
提前唤醒writeback线程后,writeback线程写会集中写,具体是以inode为单元,会把每个inode上的脏页数据通过ext4的writepages函数,写到磁盘上。
如果写的脏页数据量都一样,集中写会做io请求合并,减少io请求处理耗时,这样显然比内存回收自己离散写脏页性能要好多了。
具体性能优化数据可见我的另外一篇文档:那些年解的疑难性能问题 - ext4碎片整理。
2)patch内容
具体是社区这个patch: mm/vmscan: wake up flushers for legacy cgroups too.
在内存回收必经路径shrink_inactive_list函数里面,判断stat.nr_unqueued_dirty == nr_taken的话,说明此时内核的inactive lru list里面积累了大量脏页,需要唤醒writeback线程去集中大批量地写一次。
优化2:
1) 优化原理
在内存回收路径里面尽量少回收脏页,少触发io操作,这样会降低内存回收direct reclaim路径的耗时,也会间接优化内存分配slow_path的耗时。
同时为了保证多回收内存,增加更多free page, 会多回收些干净页。核心工作是:
把active list上更多的page(比如clean page)加入到inactive list里面,这样虽然会造成比如这些active clean page被回收后,很有可能还要被重新读入内存。但是这个负作用比起 陷入缓慢的write back脏页操作不能立刻满足前台内存分配需求 要轻得多。
因为存储芯片读是比写要快很多的,所以上面虽然有那个负作用,但是不足为虑。
2) patch内容
具体是社区这2个patch:
mm: vmscan: only write dirty pages that the scanner has seen twice.
在shrink_inactive_list函数里面,如果direct reclaim,则做以下工作:
是待回收页是脏页时,进一步判断如果该页没有设置reclaim标记,那么就仅仅设置下relcaim标记,重新放回active list上,而不去回收它。然后等到第2次再碰到该页时,如果还是脏页,再去回收它。
mm: vmscan: move dirty pages out of the way until they're flushed
在lru_add_drain里面的pagevec_move_tail_fn函数里面修改,搞成不管该page是否active,都尽可能地把它放到inactive list上去。
这样会尽量把active list上的page往inactive list上转移,因为上面patch是在inactive list上碰到脏页放到active list上的,所以再带上这个patch,那么最终效果是更多的clean page被搬到了inactive list上去,这样就会有更多的page被回收掉。
这样就会充分释放了内存,接下来的内存分配性能就会得到优化。
二、 内存回收里面的boost watermark优化改造
改造1
1 问题现象
嵌入式设备里面有时候会出现app热启动慢,抓trace分析后,是启动时的io读性能差,差的原因是很多读不是从pagecache中读,而是直接从磁盘上读。
2 原因分析
有些嵌入式设备内存并不充裕,然后从log中看到,出问题的内核里面都开启了boost watermark。这个特性一旦被开启,就会使得内核内存回收变得更加活跃,并且是只回收干净文件页的,脏页和匿名页都不回收。
这个特性推出的目的本来是为了降低系统内存碎片的(详见我另外一篇文档:android内存碎片优化梳理),但是结果在低内存设备上副作用更加明显,更加大于它的收益,把app启动时io读成功的文件页都给回收了,这样就会造成系统的整体io读性能变差。
3 优化思路
低内存嵌入式设备上因为io读性能差问题严重,所以可以关闭该boost watermark优化,高内存设备上保留。
ps:
性能优化有时就是这样一种折衷,优化有时候会难免有一些副作用,怎么针对具体问题场景,做到衡量评估好收益和副作用的平衡和折衷,是关键的。
改造2
1 问题现象
移动设备上(比如手机,平板)相机场景里整机内存压力大,会造成相机相关进程内存分配性能差,出现相机操作卡顿问题。
2 原因分析
相机某些操作场景下,相机自身进程会不可避免有高峰值内存分配的需求。当陡然切换到这些操作场景时,由于系统没有做好应对准备,满足不了相机内存分配需求,就会造成相机内存性能变差。
3 优化思路
相机场景下可以部署下主动内存回收方案,在高峰值内存场景下,可以缓解相机内存分配压力。
主动内存回收必须快捷高效,能短时间内释放出大量可用内存出来。这样可以借鉴下boost watermark的思想,先回收下文件页,因为文件页比匿名页回收要省时多了。
另外一点是相机自身进程的io读需求比较少,io读压力不大,压力大的是内存和cpu,所以为了短时间内释放内存压力,是可以多回收些文件页的。
同时改造下boost watermark,把它绑到小核上去干活,避免和相机进程争用cpu。
三、 从系统全局考虑部署高效内存回收方案
1 问题背景
嵌入式系统,诸如手机,平板,前面提过,内存和存储容量受限,但用户对它的使用需求却在日益膨胀,几乎当成电脑一样在用。
所以单机系统里面,整机内存压力是比较大的,而内存方面的优化涉及面广,拿android系统来说,从app到fwk, 再到native层的glibc库,再到底层内核,都会有内存优化空间。
内核内存优化比较麻烦耗时些,而且有些内存性能问题,从上层入手优化,反而更加高效快捷些,所以需要从系统全局考虑出发,去优化整机内存。
2 原生android已有的内存回收方案
1)用户态的lmkd + 内核的内存psi两者结合,高效杀进程
杀进程是释放整机内存压力最好的方式,系统整机free内存很少时,通过杀掉一些手机后台低优先级进程,可以快速地腾出可用内存,供前台app使用。
另外基于内存psi感知,这样可以更灵敏地感知到程序有性能问题时,就去及时杀进程。
2)Lmkd杀进程缺陷
目前的缺陷是,光根据adj来进行杀进程优先级排序还不够,有些用户经常使用到的app还是会被频繁杀掉,这样会影响到用户体验。
另外不区分主进程和子进程,杀掉主进程就会影响到后台app驻留。
所以还需要在fwk层做些优化工作,因为fwk层最能感知到app业务层的变化,在这里最能根据用户体验来部署优化方案。
3)其他的一些优化方法
从性能优化工作角度出发,感觉到目前的linux内核内存管理这块,更倾向于服务互联网业务场景。嵌入式单机设备场景比如android手机,想往内核主线分支进性能优化changes,会发现比较困难。
所以现在我们看到的比较新的版本上内核内存回收这块还是有很多性能问题,有很多待优化空间的。
所以一些诸如高通芯片原厂,在linux内核主线版本上,会打上一些关于嵌入式系统方面的优化(比如process reclaim等),才会交给我们使用。
Linux内核内存回收的一些问题和待优化空间
一、 内存回收目标和收益方面的不确定性
1 内存回收目标
问题1)
direct reclaim时,nr_to_reclaim是它的回收目标,但这个现在固定死了是32个page(详见__alloc_pages_direct_reclaim → try_to_free_pages里面的 nr_to_reclaim被强制赋值为SWAP_CLUSTER_MAX),
那么如果内存分配只需要1 - 4个page时,陷入到slow path里面做内存回收,客户只需要回收1 - 4个page就行了,但是内存回收这块会多回收出额外的31个page出来。额外的回收工作必然会导致回收要多耗时点。
打开底层ftrace,会经常看到前台操作app时,对应的缺页异常里面会每次只分配1到4个page,说明在android系统里面分配少量page的需求还是很多的。
原因
这个地方定成32,可能是考虑到系统中有很多进程在做并发direct reclaim的,所以为了权衡系统整体reclaim的压力和避免更多有用内存页被回收掉,这个地方就定成了32.
问题2)
内核内存回收direct reclaim的必经函数shrink_node_memcg最下面,完成回收目标后,还要做rebalance the anon lru active/inactive ratio工作,势必会增加direct reclaim进程的耗时。
另外shrink_inactive_list里面还要做too_many_isolated工作,这个会导致direct reclaim进程睡眠。
原因
可能是为了服务器场景考虑的,为了优化系统全局内存状态,做一些balance工作。但是嵌入式场景下,该内存回收架构并未区分前台后,这样前后台进程在direct reclaim方面一视同仁,
都要做很多balance系统的工作,这样的话,会导致前台app的内存分配耗时增加。
总结:
上面一些问题的发现,说明内存回收这块目前的架构设计是为服务器场景考虑的,而嵌入式场景,比如手机,比较关注前台app进程的响应性能的情况下(详见我的文档:手机前后台io分组优化调研),
上面内存回收的一些耗时不确定性问题,至少说明在内存回收这个地方,嵌入式设备尤其是相机场景大内存分配的情况下,还是会有优化空间的。
2 内存回收收益计算
问题
内核在做内存回收过程中,要调用shrink_slab去回收系统的一些缓存。但是内核drivers/staging目录下面一些缓存,比如ion缓存,就没有计算这部分缓存实际被回收的page数量。
这样会造成direct reclaim中,比如回收目标是32个page,本来带上ion cached部分,就达到回收目标了,不需要再进行新的一轮回收工作了。
但是由于漏统计了ion cached的回收部分,还得再多做一轮回收工作,直到回收够32个page再结束。
原因
shrink_slab时,实际回收成功的文件系统缓存都可以被内核内存回收模块统计到,但是ion cached这块,可能是位于staging分支的缘故,内核主线代码对它的管理不是很到位而造成的。
二、 rmap查找的耗时
这个是目前内核内存回收中一个比较头疼的性能问题。
1 问题现象
在android系统里面,用perf工具抓下kswapd的性能profile, 会发现内核内存回收这块有很多工作花在了 page_referenced -> page_vma_mapped_walk这个地方了。
之前XXX的系统高负载时匿名页回收性能调查博客里面,也提到了相机app进程会卡在shrink_page_list → page_referenced函数里面时间还比较长。
2 原因分析
1)内核内存回收搞了个二次机会法,增加了rmap反查耗时。
二次机会法意味着所有的内存页在进入inactive list后,几乎都要做一遍page_check_references工作,这样才能被真正回收掉。
这个地方的工作确实有必要,因为lru list只是保证了用户进程先分配使用的内存页放到tail,后分配使用的内存页放到了head.
但是在系统运行过程中,如果放到了inactive list tail的内存页被进程再次访问到,那么就会重新变回active,那么就得靠二次机会法来把它移到active list里面。
二次机会法靠这个page_check_references工作,来保证内核内存回收的都是最近不经常被使用到的page.
2) android系统的共享页很多,也增加了rmap反查耗时。
android里面的app进程都是从zygote进程fork出来的,并且app进程自身也会fork出很多线程。这样造成了android里面共享页的确很多,同一个page被共享进程的数目也很多。
这样就会带来page_referenced的反查耗时工作。
3) lru active list上也加了page_referenced的反查工作。
这部分反查工作一方面是为了recent_rotated计数,最后是为了get_scan_count里面的确定file list和anon list的查找倾斜度。
高内存压力场合,get_scan_count里面也推荐用SCAN_EQUAL,而不是SCAN_FRACT,所以这种场合还是否有必要做上面倾斜度确定工作,需要再看看是否能进一步优化下。
另外一方面主要是为了在active page被降级加入到inactive lru list之前,做下清除映射该page的各用户态进程pte的referenced标记工作。
因为要加入到inactive list上,至少说明此时该page已经是stale page, 已经被降级下来准备要被回收了,所以应该清除下映射该page的pte里面的referenced标记了。
内核内存回收方面的一些梳理:
其实从这些内存回收设计方面可以看出,linux内存回收这块还是基于一些经验主义的判断,不可能做到特别准确地识别出最久不被使用的内存页。
就是靠不断地做rmap反查和一些经验主义判断(文件页先放到inactive list上,匿名页先放到active list,内核认为匿名页都是比文件页更活跃的)
来最大程度上保证hot page(即每个进程的working-set内存页)放到active list上,cold page(进程的非woking set内存页)放到inactive list上去。
其中在每个page被回收之前,匿名页一般被反查2次,文件页一般被反查3次,反查的确比较耗时。
不过linux kernel在性能方面是针对通用系统,重点是为服务器系统设计的,所以嵌入式方面还是有不少特殊场景(比如相机高内存峰值出现场景)可以做内核内存回收方面的性能优化的。
3 优化方法
或者用app compact回收单进程,或者借鉴下lwn上的这篇优化文章:The multi-generational LRU,讲的rmap反查的弊端和优化方法。
或者我现在正在调研的思路:相机场景下,高效内存回收思路。
如果要回收的内存数量多,比如回收1 - 2G,那么其实可以识别出相机自身相关进程使用的内存page后,对非相机使用的page可以减少上面rmap的反查工作, 理由如下:
1) 因为非相机进程即使发生page refault,也是仅仅影响的是非相机进程的性能。
2) 在主动内存回收之前,内核的全局lru链表中已经大概率是一个比较成熟,比较完美地冷热内存页分离的状况了(hot page放在active list,cold page放在inactive list上),这样充分利用内核的lru list,先回收冷页。
3) 最重要此时相机操作需要大量内存,而系统处于内存紧缺状况,需要短时间内回收出大量内存出来。
而此时结合下面的调研,就算做上面耗时的rmap反查工作,也会很容易出现比如几秒钟前才被访问过的页面可能都不会被视作活跃,而被回收掉(因为此时回收过程中页扫描会出现地异常频繁,会加速页面老化)。
那还不如不做rmap反查,直接回收算了。
三 、内核匿名页回收冷热分离做的还不够好,导致回收效率低
1匿名内存和文件内存的各自特点
1)匿名内存
内核认为匿名内存大部分都是active的,不会有太多used once或者short-lived的匿名内存。
就是说内核假定:匿名内存,比如malloc,一旦被用户创建出来分配好对应的物理内存,那么该块物理内存就会被频繁使用,如果不被用的话,用户会及时free该内存的(否则的话,就算是内存泄漏了)。
但是相机场景下,不好说,应该会存在一些并不是被频繁使用的匿名内存,用户创建出来后,是为全局考虑的,会出现有一段时间会频繁使用,使用完并不释放,而是过一段稍微长时间后,还会继续用它。
2)文件内存
内核假定文件内存里面有相当一部分是used-once或者short-lived内存(因为文件预读会引入很多不必要内存页),当然也有另外一部分是被频繁使用的内存,两部分交织在一块。
2 内核对文件页和匿名页在inactive/active分离方面的处理方式
1)匿名内存
因为内核认为匿名内存普通都是hot/active的,只要该块内存存在,就会被频繁用到的,所以把新创建的匿名页加入到了active list上。
同时二次机会法也不适用它,在inactive list上,只要匿名page被用户进程访问到了,就会被立刻晋升到active list上去。
同时inactive_list_is_low函数里面也规定了active list长度要大于inactive的,另外重要的是匿名页也没有做working-set识别,转换和保护。
2)文件内存
上面说的其实是文件内存里面都是inactive和active内存页混合在一起,所以内核认为文件内存不一定都是active的,所以新创建的文件内存就会被加入到inactive list上去,
然后进行进一步地通过二次机会法来进行筛选,把频繁访问的,即active内存晋升到active list上去,inactive的则被留下在内存不足时优先被回收掉。
这样随着事件的推移,进程的working-set内存页就会几乎全部集中在active list,而不会出现used-once/short-lived这种inactive内存跑到active list上,给进程内存页的冷热分离带来困难。
这样进程的working-set内存页就会被建立起来,并且不容易被内存不足时回收掉。
另外进程的working-set在特定时间段内是固定的,但是时间长了,肯定会切换到另外一个working-set里面。
在working-set切换时,文件页有refault distance算法来防止出现切换后,频繁出现page thrashing.
3 最后结论
文件页回收有很多优化算法在保证尽量做到冷热分离和减小page thrashing,但是匿名页却没有这些算法保证,所以匿名页回收效率感觉比文件页低,导致的一个不好影响就是匿名页发生的page thrashing应该比较多于文件页。
具体仅仅这里是通过代码分析发现的,还得通过实验数据验证。
四、 内核的内存回收从来都是被动的回收
1 具体特点
1) 内存回收都是内存分配不能满足需求时,才不得不陷入到slow path分配中做回收,这样对内存分配性能是会有差的影响的。
2) 就算陷入到slow path中,触发的kswapd线程,也是在balance式地回收,只要内存水位稍微达到一定阀值,内存处于balanced状态后,就停止工作了。
3) 内存页面的老化,只有在lru list被scan时,才会老化。也就是说LRU里面的老化时间流逝跟自然时间是没有关系的。
扫描才是推动历史车轮前进的动力。而扫描又是由于达不到balanced而被触发的,可见页面老化的速度跟系统中内存的紧缺程度是相关的。
内存紧缺的时候,1分钟前才被访问过的页面可能都不会被视作活跃;反过来,如果内存不紧缺,长期不需要进行回收,那么几小时前访问过的页面又可能都会被视作活跃,并且在这段时间内被访问过一次和被访问过多次的页面会被同等对待
看过内存回收二次机会法,才会对这个点有深入理解。
4) 目前的防止出现内存thrashing而做的workingset保护,只是保护了文件页,匿名页还未进行保护(好像内核5.9版本上才有)。其实匿名页也需要进行workingset保护,减小thrashing。
2 导致的问题
移动设备相机场景下,经常有突发性的高峰值内存分配需求出现。如果还是上面这种内存回收特点的话,是会导致相机内存分配性能差,从而出现用户操作卡顿,杀后台现象比较严重。
3 解决方法
制定相机场景下的杀进程管理策略,还有主动内存回收策略,从而释放整机内存压力,优化相机内存分配性能。
感兴趣可进群与作者直接沟通交流
精华文章:【精华】Linux阅码场原创精华文章汇总
阅码场付费会员专业交流群
会员招募:各专业群会员费为88元/季度,权益包含群内提问,线下活动8折,全年不定期群技术分享(普通用户直播免费,分享后每次点播价为19元/次),有意加入请私信客服小月(小月微信号:linuxer2016)
专业群介绍:
彭伟林-阅码场内核性能与稳定性本群定位内核性能与稳定性技术交流,覆盖云/网/车/机/芯领域资深内核专家,由阅码场资深讲师彭伟林主持。甄建勇-性能优化与体系结构
本群定位Perf、cache和CPU架构技术交流,覆盖云/网/车/机/芯领域资深用户,由阅码场资深讲师甄建勇主持。
李春良-Xenomai与实时优化
本群定位Xenomai与实时优化技术交流,覆盖云/网/车/机/芯领域资深用户,由阅码场资深讲师李春良和彭伟林共同主持。
周贺贺-Tee和ARM架构
本群定位Tee和ARM架构技术交流,覆盖云/网/车/机/芯领域资深用户,由阅码场资深讲师周贺贺主持。
谢欢-Linux tracers
本群定位Linux tracers技术交流,覆盖云/网/车/机/芯领域资深用户,由阅码场资深讲师谢欢主持。
✦
✦