考虑到安卓手机mapped文件页占比相当高,故实现以文件为单位 低损耗、精准的回收mapped文件页是有意义的。一个文件的mapped文件页page有冷的、热的、mapcount较大的(有多个进程映射,内存回收容易失败)、内存回收后发生refault的等几类。同时,有些文件的冷文件页很多、有些文件的热文件页很多、有些文件的大部分文件页的mapcount都较大、有些文件的文件页内存回收后容易refault等等。而以文件为单位统一管理这些因素,可以做到只回收冷文件页多的文件的冷文件页,内存回收成功的概率高,回收后不容易refault,性能消耗还低。
之前在《linux内异步内存回收的另一个思路:基于冷热文件的冷热区域精准的回收冷文件页page(可做成内核ko)》已探索过以文件为单位回收read/write系统调用产生的文件页page,但是还没有实现以文件为单位回收mapped 文件页!考虑到安卓手机场景mapped 的文件页占比相当高,比如我的find X3 pro正常使用时,共有约3G的文件页,其中read/write系统调用产生的文件页和 mapped 的文件页基本各占一半,即1.5G。这1.5G的mapped 文件页可不一定全都是热页,有多少是冷页而可以回收掉呢?100M,300M,500M,甚至更多?不管结果如何,探索下以文件为单位回收mapped 文件页还是有实际价值的(shmem文件页一般很少,先不考虑)!
经过摸索,已经初步实现了以文件为单位回收mapped 文件页!已在红帽8和9系列的centos 8.3(内核版本4.18.0-240)和rocky 9.2(内核版本5.14.0-284.11.1)取得不错的效果,也可以做成内核ko,使用方便。参考源码见 https://github.com/dongzhiyan-stack/async_memory_reclaime_for_cold_file_area,下文我们详细介绍。
相对于read/write系统调用产生的文件页page,mapped 文件页page存在页表页目录映射,回收需要先unmap解除页表页目录映射,性能消耗偏大。并且,mapped 的文件页的冷热判断也是个麻烦事!另外,如果mapped 的文件页内存回收后发生refault怎么办?还有,有些文件的mapped 文件页page的page->_mapcount比较大(有多个进程映射),这种page内存回收特别容易失败(主要是unmap失败),最好不要参与内存回收,即便很长时间没有被访问。
经过对mapped 文件页内存回收的摸索,个人觉得可以把一个文件的mapped 文件页分成如下几类:
为什么要这样划分?因为热文件页page、mapcount较大的文件页page(有多个进程映射该page,page->_mapcount较大)、发生refault的文件页page(内存回收后短时间又被访问)、内存回收失败的文件页page,这几类文件页page内存回收失败的概率很大,内存回收时需尽可能要绕开他们,尽可能只回收冷文件页page。这样内存回收的成功率很大,并且性能损耗还低!那这种内存回收方案该怎么落地呢?只能以文件为单位,把该文件的mapped 文件页划分成热文件页page、冷文件页page、mapcount较大的文件页page、发生refault的文件页page 等等几类,如此就能做到尽可能只回收冷文件页page,尽可能避开内存回收容易失败的文件页page!
总之,以文件为单位回收mapped 文件页的目的是:以文件为单位,以较低的性能损耗判断出冷文件页page并回收掉,并能提前预测哪些mapped 文件页内存回收可能发生refault。一旦发生refault要能对refault的文件页单独管理,避免频繁refault。同时,也要提前预测哪些mapped 文件页可能会内存回收失败,比如mapcount较大的文件页page。针对这些现实问题,damon、grlru、双lru内存回收方案可能不太让人满意,mglru可能还好,但无法做到以文件为单位统一管理mapped 的文件页、对refault的page单独管理、提前预测哪些page内存回收容易失败等等。
进一步扩展,再把文件归类:普通文件(大部分文件页都很少访问)、大的冷文件(文件的文件页很多,并且大部分文件页都很少访问)、热文件(文件的大部分文件页都频繁访问)、mapcount文件(文件的大部分文件页page的page->_mapcount都很大)、refault文件(文件的大部分文件页都发生了refault),内存回收时优先回收大的冷文件的文件页,避开其他文件,有较大概率能回收到更多的冷文件页,性能损耗还低。
可以发现,这个思路跟《linux内异步内存回收的另一个思路:基于冷热文件的冷热区域精准的回收冷文件页page(可做成内核ko)》,针对read/write系统调用产生的文件页page的异步内存回收有相似之处,但又有很大差异。只能说,二者的基本思路一样,都是以文件为单位回收冷文件页!建议感兴趣的小伙伴先看下这篇文章,这样对理解本文内容会有很大帮助。
最后再提一点,如果要一直扫描mapped 文件页,判断冷热,性能损耗偏大,该怎么降低性能损耗呢?于是想到:当扫描完一个文件的所有mapped 文件页后,以扫描到的冷热文件页数量、成功回收的文件页数量等条件计算冷却期时间,然后令该文件进入冷却期。在冷却期内,不再扫描这个文件的mapped 文件页,这样能有效降低性能损耗!
1:以文件为单位回收mapped 文件页page基本思路
首先,进程mamp映射一个文件,分配虚拟地址空间。当读写这片虚拟地址空间后,便会发生缺页异常:分配文件页page,从指定文件地址读文件数据到page内存,建立虚拟地址空间跟page对应物理内存的页表页目录映射,如下图所示:
这片mmap文件映射的vma虚拟地址空间的范围是0x7ff8ef3c0000~0x7ff8ef3cc000,以4K为单位,通过页表页目录共映射了12个文件页page的物理内存,文件页索引是0~11。虚拟地址、页表页目录、文件页page 三者一一对应!这片虚拟地址范围内,有些地址会被频繁读写,有些读写一次后就不再访问了。频繁读写的虚拟地址映射的文件页page这里称为热页,读写一次后就不再访问的虚拟地址映射的文件页page称为冷页。冷热页演示如下:
红色表示频繁访问的地址及其映射的文件页page,剩余的是不怎么访问的地址以及映射的文件页page。内存回收时当然最好只回收不怎么访问的文件页page(冷page),避开频繁访问的文件页(热page)。现在有个问题,怎么判断冷热page呢?
我们知道,当进程读了一次某片虚拟地址,它映射的页表pte的access bit就会置1(cpu自动完成)。而通过逆向映射原理,可以通过文件页page指针找到映射该page物理内存的所有虚拟地址vma,有了vma就可以找到映射的页表pte,然后看access bit是否置位,表示该片虚拟地址是否被访问了。
这点可以参照内核内存回收执行的shrink_page_list()函数,针对mapped 的文件页,它要先执行page_check_references(page, sc)检测page是否访问了。里边正是根据page指针找到映射该page物理内存的所有虚拟地址vma,然后查看这些vma映射的页表pte的access bit是否置位了。如果这些pte的access bit全都没有置位,那说明映射这个page物理内存的所有所有虚拟地址空间,都没有被进程访问。此时page_check_references()函数返回0,然后才可以释放掉该page。否则,page_check_references()函数里清理掉页表pte的access bit,然后函数返回1,此时这个page就要移动到active lru链表,禁止回收。
判断冷热mapped文件页的基本思路是(其实也没啥特殊的,方法都类似):在异步内存回收线程里,周期性对mapped 的文件页page执行page_check_references(page, sc)检测映射该page物理内存的所有虚拟地址空间vma的页表pte的access bit是否置位了(这种方法性能损耗可能有点大,改进方案在第5节有介绍)。如果access bit置位了,用一个变量age记录当前的时间点,并清理掉页表pte的access bit。如果access bit没有置位,且发现age变量与当前时间相差很大,说明映射这个page物理内存的虚拟地址空间很长时间没有被进程访问了。于是判定这个page是冷page,然后就可以回收这个page了。
好的,冷热页的判断思路有了,但是代码该怎么落地呢?前边我们用一个age变量反应一个mapped 文件页page多久没有被访问了(即映射这个page物理内存的虚拟地址空间多长时间没有被访问,二者是同一个意思)。我们总不能为每个page都定义一个age变量吧,在struct page核心数据结构里增加一个age变量是不行的!于是想到了把文件页page分组管理,用一个age表示一组文件页page的冷热,如下所示(为了演示方便,把原图右半部分的磁盘文件部分略去):
如图,把前边提到文件的12个文件页page分成3组,每组分配一个file_area结构,用来统计4个索引连续的文件page的冷热信息,主要成员如下:
- struct file_area
- {
- //最近一次扫描到file_area里的4个page中的任何一个被访问时的全局age
- unsigned long file_area_age;
- //在某个时间段内,记录file_area里的4个page被访问的次数,用于热file_area的判断
- unsigned int access_count;
- }
同时,还定义一个全局age变量,在异步内存回收线程每隔1分钟加1,模拟系统时间。当扫描到file_area里的4个page中的任一个被访问了(映射该page物理内存的虚拟地址空间vma的页表pte的access bit置位了),则file_age结构的成员file_area_age(file_area的age)被赋值当前的全局age。如果file_area的age与全局age相差很大,说明这个file_area里的4个page很长一段时间都没有被进程访问了,file_area被判定为冷file_area,这4个page也被判定为冷page,下一步就回收这些冷page。
file_area的冷热代表了它对应的4个mapped 的文件页page的冷热!因为file_area里的4个page是索引连续的,映射这些page物理内存的虚拟地址空间也大概率是连续的,故这4个挨着的page的冷热信息也是接近的。这点跟damon内存回收方案比较接近,可以利用局部性原理更好的回收这些page。
2:mapped 文件页page内存回收的特殊性
上一节已经介绍过用file_area统计4个索引连续的mapped 文件页page的冷热信息,这点跟《linux内异步内存回收的另一个思路:基于冷热文件的冷热区域精准的回收冷文件页page(可做成内核ko)》介绍的思路基本是一样的,但是mapped 文件页page又有很多不一样的地方,这里总结一下。
read/write系统调用的文件页page和mapped 文件页page冷热的判断,以及回收流程是存在很大差异的。read/write系统调用访问文件页page时,直接令访问计数加1,据此可以直接判断这种文件页page的冷热,并且可以按照策略把page的file_area移动到file_stat->file_area_temp链表头(file_stat->file_area_temp下边介绍,保存file_area指针的链表),异步内存回收时,只用遍历file_stat->file_area_temp链表尾的冷file_area即可,不用遍历完整个链表,降低性能损耗。
而mapped 的文件页,在mmap映射后,进程直接访问它映射的虚拟内存即可,无法直接统计文件页的访问计数。只能异步内存回收线程里,执行page_check_references函数判断映射这个文件页page物理内存的虚拟地址vma的页表的pte access bit是否置位了,来判断这个文件页最近是否被访问了。如果长时间pte access bit没有置位,才能判定page是冷page,对应的file_area也被判定是冷的。这是一个很大的差异,这也导致mapped 文件页的回收很麻烦!当然,这也让内存回收的优化有很大空间。
2.1 mapcount大于1的mapped 文件页page
我们知道,内核shrink_page_list()函数里回收mapped 文件页的关键步骤是执行 try_to_unmap()函数解除所用映射该page物理内存的虚拟地址空间vma的页表页目录映射关系。如果解除成功该函数返回1,可以正常回收掉这个page,否则回收失败。
实际调试经常遇到mapped 的文件页page回收失败的情况,此时的函数流程是try_to_unmap->rmap_walk->rmap_walk_file->try_to_unmap_one->ptep_clear_flush_young_notify,ptep_clear_flush_young_notify()函数返回1导致解除页表页目录映射失败。这是什么情况?因为映射这个page的页表pte的access bit置位了,就是说这个page被访问了,那自然不能再回收这个page了。具体可以看下我写的文章为什么内核内存回收对page进行try_to_unmap会失败?-优快云博客!
这就很奇怪了,因为在执行try_to_unmap()函数前,会先执行references = page_check_references(page, sc)判断映射这个page的页表pte的access bit是否置位了,没有的话才会执行try_to_unmap()函数解除页表页目录映射。但执行到try_to_unmap->rmap_walk->rmap_walk_file->try_to_unmap_one->ptep_clear_flush_young_notify时,映射这个page的页表pte的access bit却置位了。这么短的时间这个page就被访问了!为什么会这样?把page->_mapcount打印出来,发现page->_mapcount都很大,在3~30之间。page->_mapcount较大的page因为有多个进程映射,自然被访问的更频繁、随机,出现这种问题自热也就可以解释了。
因此,内存回收应尽量回收page->_mapcount较小的page,避开page->_mapcount较大的page。据此,引入一个概念mapcount file_area。什么是mapcount file_area?就是这个file_area对应的4个page,至少有一个page的page->_mapcount较大,内存回收时要避开这种file_area。
2.2 冷热mapped 文件页page的判定
还是要依赖file_area!异步内存回收线程,每1分钟执行一次,每次先令全局age加1。然后遍历一个个文件,再遍历每个文件的一个个file_area。当扫描一个file_area时,对它的page执行page_check_references()检测映射该page物理内存的虚拟地址空间vma的页表pte的access bit是否置位了。如果置位了,说明这个page被访问了,同时page_check_references()最后会清理掉pte的access bit。接着,我们把全局age赋值给file_area的age,并且令file_area的access_count加1。
如果在规定时间内,file_area的access_count大于2,则判定为热file_area。自然热file_area的page也是热的。如果file_area的age与全局age差值大于某个阀值,则对file_area的access_count清0。简单说,如果file_area的page在规定时间内被访问次数大于2,则判定file_area和page是热的。这个判定比较简单,具体参数可以按照实际情况调整。
如果file_area的age与全局age相差很大,说明它的page很长时间都没有被访问了,是冷page。则file_area被判定为冷file_area,异步内存回收线程正是回收冷file_area的冷page。
3:file_area与文件的组织关系
如前文介绍,一个file_area代表了4个索引连续的mapped 文件页page,而page又有不同的属性:mapcount较大的page、冷page、热page、普通的page、内存回收后发生refault的page等等,这些不同属性的page都需要file_area一一管理。同时,一个文件会有很多的mapped 的文件页page,故file_area又会有很多个。为了管理每个文件数目繁多且不同属性的file_area,为每个文件分配一个file_stat数据结构,fille_stat的主要成员是多个链表list_head,管理各种file_area。如下示意图:
示意图说明:以 file_area1为例,它代表文件页索引是0~3的page,即page0~page3,其他的file_area类似。
代表文件的file_stat数据结构,主要成员是struct list_head file_area_temp、file_area_hot、 file_area_refault、file_area_free_temp、file_area_mapcount 几个链表头,不同属性的file_area分别链入这些链表:
- struct list_head file_area_temp:当为文件页page分配file_area后,默认添加到file_area_temp链表。异步内存回收线程只会遍历file_area_temp链表上的file_area,看是否有冷page,然后回收冷page。
- struct list_head file_area_hot:如果file_area的page在规定时间内被访问次数大于2(这个阀值可调节),判定为热file_area,同时file_area移动到file_area_hot链表。异步内存回收时较长时间不会遍历该链表上的file_area的page,以降低性能损耗。
- struct list_head file_area_refault:如果file_area的page在内存回收后短时间又被访问了,则把该file_area移动到file_area_refault链表。异步内存回收时较长时间不会遍历该链表上的file_area的page,以降低性能损耗。
- struct list_head file_area_free_temp:当file_area_temp链表上的file_area的page长时间不被访问后,file_area被判定为冷file_area,接着回收对应的page。然后把该file_area移动到file_area_free_temp链表。如果file_area_free_temp链表上的file_area的page在短时间内又被访问了,则把file_area移动到file_area_refault链表。如果file_area_free_temp链表上的file_area的长时间没有探测到page,说明长时间无人访问,则释放掉file_area结构。
- struct list_head file_area_mapcount:如果file_area的page有page->_mapcount大于1(这个阀值可以调节),判定为mapcount file_area,则把file_area移动到file_area_mapcount链表。异步内存回收时尽量不会遍历该链表上的file_area的page,以降低性能损耗。
下文分别以file_stat->file_area_temp、file_stat->file_area_hot、file_stat->file_area_refault、file_stat->file_area_free_temp、file_stat->file_area_mapcount简称这些链表。
注意,有一点需要说明一下。这里介绍的file_area都是保存在file_stat的各种链表上。同时,file_area指针还按照索引(file_area对应的page索引除以4)保存在radix tree,作用在文章最后介绍reverse_file_stat_radix_tree_hole函数时会提到。
好的,已经介绍了单个文件是怎么管理file_area的,linux系统有成百上千个文件,又该怎么管理这些文件呢?用如下示意图演示:
首先定义一个全局struct hot_cold_file_global结构体,它主要有struct list_head mmap_file_stat_temp_head、mmap_file_stat_temp_large_file_head、mmap_file_stat_hot_head、mmap_file_stat_mapcount_head、mmap_file_stat_uninit_head这几个链表头成员,含义如下:
- struct list_head mmap_file_stat_uninit_head:当探测一个mmap文件后,为该文件分配file_stat数据结构,并添加到mmap_file_stat_uninit_head链表。异步内存回收线程中,遍历完该文件file_stat所有的文件页page并分配file_area后,把该文件file_stat从mmap_file_stat_uninit_head链表移动到mmap_file_stat_temp_head链表。如果该文件的file_area超过阀值,说明文件页太多而判定为大文件,则是把file_stat移动到mmap_file_stat_temp_large_file_head链表。
- struct list_head mmap_file_stat_temp_head:普通文件在这个链表上,异步内存回收线程遍历mmap_file_stat_temp_head链表上的文件file_stat,然后再遍历该文件file_stat->file_area_temp链表上的file_area,查看该file_area对应的4个page是否被访问了。如果长时间没有被访问,则判定为冷file_area,然后回收对应的4个page。
- struct list_head mmap_file_stat_temp_large_file_head:如果文件的文件页超过阀值,也就是file_area个数超过阀值,判定为大文件,则该文件的file_stat移动到mmap_file_stat_temp_large_file_head链表。异步内存回收线程会首先遍历该链表上的文件file_stat,然后回收该file_stat->file_area_temp链表上的file_area,查看该file_area对应的4个page是否被访问了而决定是否回收掉。为什么要这样,因为mmap_file_stat_temp_large_file_head链表上的文件,拥有的文件页page更多,这样更容易回收到冷page。这也正常,比如文件1拥有10个文件页page,文件2拥有1000个文件页page,那肯定从文件2更容易回收到冷page,因为它的page更多,回收到冷page的概率更大。
- struct list_head mmap_file_stat_hot_head:如果一个文件file_stat的热file_area个数超过阀值,判定为热文件,则把file_stat移动到mmap_file_stat_hot_head链表。异步内存回收线程较长时间不会遍历该链表上的文件file_stat,也就不会回收这些文件的file_area的page。因为这些文件页page是频繁访问的,内存回收后容易发生refault。
- struct list_head mmap_file_stat_mapcount_head:如果file_area的文件页page的page->_mapcount大于1,则判定file_area为mapcount file_area。如果一个文件file_stat的mapcount file_area个数超过阀值,判定为mapcount文件。然后把file_stat移动到mmap_file_stat_mapcount_head链表。异步内存回收线程较长时间不会遍历该链表上的文件file_stat,也就不会回收这些文件的file_area的page。因为这些文件页page的物理内存与多个进程建立了页表页目录映射,对这些page内存回收容易失败或者内存回收后容易发生refault。
为了叙述方便,下文把struct hot_cold_file_global结构体的 struct list_head mmap_file_stat_temp_head、mmap_file_stat_temp_large_file_head、mmap_file_stat_hot_head、mmap_file_stat_mapcount_head、mmap_file_stat_uninit_head链表简称为global mmap_file_stat_temp_head、global mmap_file_stat_temp_large_file_head、global mmap_file_stat_hot_head、global mmap_file_stat_mapcount_head、global mmap_file_stat_uninit_head链表
4:以文件为单位回收mapped 文件页举例
本章节用示意图演示一个文件的mapped 文件页是如何回收的。演示的文件共98304大小,共分配了6个file_area,即file_area1~file_area6。file_area1表示索引是0~3的文件页page0~page3,其他file_area类推。file_area1到file_area5都添加到了file_stat->file_area_temp链表。file_area6的文件页page20~page32中,有page->_mapcount大于1,故file_area6是mapcount file_area,添加到file_stat->file_area_mapcount。最开始,第一个周期,全局age是0 。
然后来到周期6,全局age是5。异步内存回收线程里,从file_stat->file_area_temp链表尾开始遍历file_area(实际每次遍历的file_area个数有限制,也有遍历策略,不会一直无脑遍历)。检测file_area5和file_area2里的文件页page检测被访问了,则他们file_area的access_count加1,并把全局age赋值给file_area的age,最后移动到file_stat->file_area_temp链表头,如下图所示:
然后来到周期12,探测到file_area5里的文件页page规定时间内访问次数大于2(阀值可调整),如下图
于是判定为热file_area,把file_area5移动到file_stat-> file_area_hot链表,如下图:
好的,接着来到周期16,全局age是15。发现file_area3和file_area4的文件页page很长时间没被访问,于是把他们移动到file_stat-> file_area_free_temp链表,开始回收这些文件页page,如下图所示:
在内存回收时,发现file_area4的文件页page成功回收了。但是file_area3的文件页page回收失败了,原因是file_area3里边的文件页page竟然有的page->_mapcount大于1,大概率就是这个原因导致的内存回收失败,于是把file_area3移动到file_stat->file_area_mapcount链表。如下图所示:
好的,来的周期17,全局age 16。在file_stat-> file_area_free_temp链表上的file_area4的文件页page,在内存回收后,再去探测指定索引的page,发现page又被分配了,并且映射page物理内存的用户空间虚拟地址vma的页表pte access bit置位了。说明file_area4的文件页page在内存回收后又被访问了,发生了refault。于是把file_area4移动到file_stat-> file_area_refault链表,特殊管理,如下图所示:
好的,演示到这里就结束了。有一点需要注意,file_stat-> file_area_refault、file_stat-> file_area_hot、file_stat-> file_area_mapcount链表上file_area长时间不会参与内存回收,在遍历到该文件时,只会按照各自的策略少量遍历一些file_area。这样做的目的很简单,降低性能损耗,内存回收少做无用功!
5:以文件为单位回收mapped 文件页的优势
好的,到这里应该对mapped 内存回收方案有了大体了解,这里再深入聊聊这个内存回收方案的优势。由于回收mapped 文件的代价比较大,要执行page_check_references等函数判定page是否被访问了,要执行try_to_unmap解除page的页表页目录映射,这些都是比较消耗cpu的,要尽可能只回收不容易发生refault、不容易内存回收失败的冷文件页。要少做无用功,多做有用功。这里先总结本内存回收方案的优势。
- 1:内存回收时优先遍历有很多mapped 文件页的大文件(也是冷文件),有较大概率能回收到更多的冷文件页page,性能损耗更低。
- 2:如果mapped 文件页内存回收后发生了refault,则单独管理这些refault的文件页。后续较长时间内不再回收这些文件页,避免频繁refault,降低性能损耗。
- 3:以文件为单位进行内存回收,如果这个文件的大部分mapped 文件页都被判定是热的(热文件),则异步回收时禁止回收这个文件的mapped 文件页,否则容易内存回收失败或者内存回收后发生refault。
- 4:如果这个文件的大部分mapped 文件页的page->_mapcount都比较大(mapcount文件),异步回收时也禁止回收这个文件的mapped 文件页,否则这些文件页有较大概率内存回收时会失败或者内存回收后发生refault。
- 5:如果扫描过一个文件的mapped 文件页后,以扫描到的冷热mapped 文件页page数量、回收的page数量等条件计算冷却期时间,然后令文件进入冷却期。在冷却期内,不再扫描这个文件的所有mapped 文件页,这样可以显著降低性能损耗。
- 6:异步内存线程遍历一个文件file_stat->file_area_temp链表尾上的file_area时,不能保证一次遍历所有的file_area,因为无法保证file_stat->file_area_temp链表尾的都是冷file_area,链表头的是热file_area。而等遍历完一遍文件file_stat->file_area_temp链表上的file_area后,统计冷热file_area个数。下次遍历时,按照上次遍历统计到的冷热file_area个数,决定本轮遍历多少个file_area,不用遍历完所有file_stat->file_area_temp链表上的file_area,还是可以降低性能损耗。
总的来说,优势可以这样总结:以文件为单位,把每个文件mapped文件页page按照 page访问频次(冷热程度)、该page的page->_mapcount、内存回收后是否发生refault等因素,把mapped的文件页page进行分类。提前预测哪些文件页page内存回收会失败,内存回收后容易refault,以较低的性能损耗回收冷mapped文件页,内存回收后发生refault的概率还低。一旦发生refault能对这些page单独管理,长时间禁止再参与内存回收,避免频繁refault。并且,内存回收时优先回收大文件的mapped文件页,避开热文件、mapcount文件、refault文件(文件的大部分文件页都发生了refault,待实现)等,这样更容易回收到冷文件页,性能损耗还低。
可以发现,以文件为单位回收mapped 文件页,可以根据实际情况制定多种策略回收文件页。达到了精准回收到冷page、不容易内存回收失败、不容易refault、内存回收性能损耗低等目的。
最后,再聊下后续的改进点:
- 1:因为是以文件为单位,一个vma可能连续映射了多个索引连续的page,可以遍历一次页表页目录得到多个page的映射的页表pte,不用每个page都要遍历一次页表页目录,这样可以有效降低page_check_references函数检测映射page的pte的access bit带来的性能损耗。
- 2:一个file_area默认4个page,如果要判定file_area是冷的,必须file_area的4个page的pte access bit都长时间没有置位。是否可以只扫描里边的1个page呢,其他page不扫描判断pte access bit是否置位了?这样可以降低性能损耗,但会引入误判,因为没有扫描的page的可能一直被访问而导致pte access bit一直置位,这样它对应的file_area就不能判定是冷file_area了。或者可以折中,在内存回收时,这个page一定会回收失败,此时file_area的其他page已经回收掉了,而因为这个page回收失败了,则把file_area移入file_stat->file_area_refault链表单独管理即可。这样既可以回收到page,还能降低每次都扫描file_area的4个page的pte access bit带来的性能损耗。要知道,根据局部性原理,file_area的4个page因为索引连续,大概率冷热程度接近,因此大部分file_area都可以只用里边的一个page的冷热代表file_area的冷热。如果产生了误判,在内存回收file_are的4个page时,其他page将内存回收失败,然后把该file_area移入file_stat->file_area_refault单独管理即可。或者,再做一个改进,在第一次扫描file_area时,4个page都判断页表pte是否置位了,后续再扫描file_area时只扫描1个page的页表pte是否置位了。
可以发现,以文件为单位回收mapped 的文件页,优化空间特别大,能做的事很多!
6:源码简单介绍
实际源码比较复杂,这里不详细介绍,只是按照函数执行流程把每个函数的作用简要总结下:
1:mmap_file_handler_post和add_mmap_file_stat_to_list函数:在mmap映射一个文件后,kprobe捕捉到这个行为,然后执行mmap_file_handler_post->add_mmap_file_stat_to_list函数。接着,为kprobe探测到的mmap文件分配file_stat结构,然后把file_stat添加到全局global mmap_file_stat_uninit_head链表。目前仅支持对ext4、xfs文件系统文件的mmap映射探测,然后回收mapped文件页。主要是出于性能考虑,想支持其他文件系统很简单,具体可以看下mmap_file_handler_post函数。
2:scan_uninit_file_stat函数:遍历global mmap_file_stat_uninit_head链表上的一个个文件。每个文件都按照如下流程处理:首先找到每个保存文件文件页的radix/xarray tree,如果page存在则分配对应的file_area。比如索引0~3的page存在则分配file_area1(file_area索引是0),索引4~7的page存在则分配file_area2(file_area索引是1),索引8~11的page存在则分配file_area3(file_area索引是2)........其他类推。如果page的page->_mapcount大于1则判定为mapcount file_area并移动到file_stat->file_area_mapcount链表,同时令file_stat的mapcount file_area个数加1。否则file_area移动到file_stat->file_area_temp链表。当遍历完该文件的文件页page后,如果该文件的mapcount file_area超过阀值则把文件file_stat移动到global mmap_file_stat_mapcount_head 链表,该文件被判定为mapcount文件。否则移动到global mmap_file_stat_temp_head或mmap_file_stat_temp_large_file_head链表。
3:walk_throuth_all_mmap_file_area函数:异步内存回收线程入口函数,依次遍历global mmap_file_stat_uninit_head、global mmap_file_stat_temp_large_file_head、global mmap_file_stat_temp_head、global mmap_file_stat_hot_head、global mmap_file_stat_mapcount_head 这5个链表上文件file_stat,再遍历每个文件file_stat的不同链表上的file_area,具体作用可以看下对应函数。
4:get_file_area_from_mmap_file_stat_list函数:核心函数,遍历大文件global mmap_file_stat_temp_large_file_head、普通文件global mmap_file_stat_temp_head链表上指定数目的文件file_stat,再遍历file_stat-> file_area_temp链表尾的file_area。查看file_area的文件页page是否被访问了,长时间没有访问的话就要回收这些冷page。这个功能主要是里边执行traverse_mmap_file_stat_get_cold_page-> check_file_area_cold_page_and_clear函数实现的。
5:check_file_area_cold_page_and_clear函数:主要功能是遍历file_stat-> file_area_temp链表尾的file_area。执行check_one_file_area_cold_page_and_clear函数查看file_area的文件页page是否被访问了。如果被访问了,则把全局age赋值给file_area的age,并file_area的access_count加1。如果file_area的文件页page在规定时间内被访问次数大于2,则被判定为热file_area,并把file_area移动到file_stat->file_area_hot链表。同时令file_stat的热file_area个数加1,如果file_stat的热file_area个数大于阀值,则把file_stat移动到global mmap_file_stat_hot_head热文件链表。如果check_one_file_area_cold_page_and_clear函数检测到file_area的page长时间没有被访问,则check_file_area_cold_page_and_clear函数里执行cold_mmap_file_isolate_lru_pages函数对file_area的文件页page进行隔离,然后执行cold_file_shrink_pages函数对这些page进行内存回收。注意,当扫描完一遍file_stat-> file_area_temp链表上的file_area,该文件将进入冷却期,在规定时间内不再扫描这个文件的file_area,以此降低性能损耗。
接着,check_file_area_cold_page_and_clear函数中多次执行reverse_other_file_area_list函数依次遍历file_stat->file_area_free_temp 、file_stat->file_area_refault 、file_stat->file_area_mapcount、file_stat->file_area_hot链表上file_area做对应的处理:针对file_stat->file_area_free_temp链表上的file_area,如果它参与内存回收后短时间内对应索引的page又被访问了,则把file_area移动到file_stat->file_area_refault链表,否则过了较长时间被访问了则移动到file_stat->file_area_temp链表。如果过了很长时间file_area对应的page还是没再被访问则释放掉file_area;针对file_stat->file_area_refault和file_stat->file_area_hot链表上的file_area,处理相似,如果file_area的page长时间没有被访问则降级到file_stat->file_area_temp链表,同时令file_stat的热file_area个数减1;针对file_stat->file_area_mapcount链表上的file_area,如果file_area的page->_mapcount是1了,则降级到file_stat->file_area_temp链表,同时令文件file_stat的mapcount file_area个数减1。注意,当扫描完一遍file_stat->file_area_free_temp 、file_stat->file_area_refault 、file_stat->file_area_mapcount、file_stat->file_area_hot链表上的file_area,这些链表将进入冷却期,在规定时间内不再扫描这些链表上的file_area,还是为了降低性能损耗。
check_file_area_cold_page_and_clear函数最后,执行reverse_file_stat_radix_tree_hole函数。主要作用是:遍历保存文件的文件页page指针的radix/xarray tree,探测哪些page没有分配对应的file_area,探测到的话则分配file_area。因为最早scan_uninit_file_stat函数中探测保存文件的文件页page指针的radix/xarray tree时,有些文件页page物理内存映射虚拟地址可能还没有被访问。或者,file_stat->file_area_free_temp链表上的长时间没有访问的file_area释放后,可能对应索引的文件页page又被分配并访问了,此时也得靠reverse_file_stat_radix_tree_hole函数探测这些“空洞page”。
6:scan_mmap_hot_file_stat函数:主要是遍历global mmap_file_stat_hot_head链表上的热文件file_stat,如果file_stat的热file_area个数降低到阀值以下,则把file_stat移动到global mmap_file_stat_temp_head或global mmap_file_stat_temp_large_file_head链表,不再是热文件。
7:scan_mmap_mapcount_file_stat函数:主要是遍历global mmap_file_stat_mapcount_head链表上的mapcount文件file_stat,如果file_stat的mapcount file_area个数降低到阀值以下,则把file_stat移动到global mmap_file_stat_temp_head或global mmap_file_stat_temp_large_file_head链表,不再是mapcount文件。
8:solve_reclaim_fail_page函数:针对内存回收失败的page的处理,如果page的mapcount大于1则把它对应的file_area移动到file_stat->file_area_mapcount链表,并令file_stat的mapcount file_area个数加1。否则把file_area移动到file_stat-> file_area_refault链表。
还有很多其他函数细节,这里不再啰嗦了,感兴趣的伙伴可看下源码https://github.com/dongzhiyan-stack/async_memory_reclaime_for_cold_file_area 。水平有限,如有错误请指出。