简介
内存回收是内存管理最为复杂的机制,本文主要在广义上介绍回收方法,而不拘泥于细节。随着系统的运行,内存会逐渐被消耗,而内存主要占用的部分有两个,分别是高速缓存以及用户进程,内核高速缓存包括磁盘高速缓存,slab缓存等。
首先介绍内核中内存回收的几种方式:
- 直接内存回收
当我们调用内存分配函数,比如alloc_pages时,发现此时对应zone管理区中的watermark低于min值,就会触发内存紧缺回收,如果该内存分配的上下文允许休眠,那么会进行休眠,并执行内存回收操作,同时触发周期回收。 - 内存规整
内存规整是要先于直接内存回收的,如果内存不足,会用优先进行内存规整,所谓内存规整,就是把使用过的内存迁移到另一个位置,尽量把空闲内存合并成连续的内存,这样减少了内存碎片化,也就能够增加内存分配成功的概率。如果内存规整后依然无法满足内存需求,那就需要进一步执行内存回收了。当然也并不是所有的已用内存页都支持内存迁移,有一些UNMOVEABLE的页是不能被迁移的。目前只有匿名页和page cache支持内存迁移。 - 周期内存回收(kswapd)
前面也提到过,当内存低于low watermark,会启动一个线程kswapd,周期性的对内存进行回收,直到内存高于high watermark才停止。为什么需要周期回收,因为有些情况申请内存是不允许休眠等待的,也就不能执行直接回收操作,所以此时内存回收只能交给周期回收。 - OOM killer
如果内存规整/内存回收都无法满足系统需要,说明内存已经极度紧缺了,内存耗尽将导致系统无法继续运行下去,所以到此时系统也只能够“断臂求生”了,内核会选择一个process进程(select_bad_process),这个进程是需要满足内核的选择标准才会被选出来,需要注意的是swapper/init和其他内核线程是不允许被kill的。 - swap交换分区
交换分区的时候,经常在安装ubuntu或者其他Linux发行版的时候,我们在分区划分时,都会选择一个/swap分区,它就是我们所说的交换分区,当执行内存回收时,有一些特殊的页,比如用户匿名页,他们没有对应的文件可以写回,但是可以被置换一个特定的分区上,从而把内存释放出来被回收使用。交换分区带来一个问题,如果反复执行换入/换出操作,那么带来的系统开销是得不偿失的,因此,和其他文件系统缓存一样,swap分区也实现了对应swap高速缓存机制,因为磁盘操作是比较耗时的,为了防止并发操作引起的问题,在同时申请换出换入时,实际上是在交换缓冲区中取入取出,而没有实际操作磁盘。
内存回收的时机
- 内存紧缺时回收
- 周期性回收(kswapd/reap_work)
- 系统睡眠时回收
反向映射(RMAP)
我们知道对于内存页的回收,根据页的不同,需要做的操作也不同,针对用户态匿名页,内核需要执行的回收操作是把它换出到交换区(swap分区),而针对用户空间映射页,内核需要做的是把该物理页的内容同步到磁盘上文件中。但是除了这些操作后,对于这两种页,内存回收还有一个重要的步骤,那就是需要定位到页page所对应的所有页表项,内核只有把所有的页表项都清除后才能允许对该页被回收重新使用。而反向映射介绍的就是如何根据page来反向的查找到对应的页表,特别是对于共享页,很可能会对应多个页表,通过反向映射,我们需要找到所有的相关页表项。
- 匿名页反向映射
匿名页一般是几个进程共享的,比如父进程克隆一个子进程,父子进程共享地址空间和页框,依赖与一个结构体:struct anon_vma.
include/linux/mm_types.h:
struct page {
...
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
...
};
根据这个anon_vma可以找到与之匹配的vm_area_struct,再根据此结构找到mm_struct进程管理结构体,可能有多个匹配的进程,进而找到进程对应的用户空间页表,从而找到页表项。
- 映射页反向映射
为什么要把映射页的映射和匿名页分开来处理,那是因为系统中的映射页存在的会比较多,最终页表项的匹配是需要扫描进程页表的,如果每个进程都扫描一遍,效率会很低,所以这里更换了另一个算法来提高效率,那就是PST优先搜索树算法来查找物理页page对应的所有页表项。
LRU链表
LRU(Least Recently Used)链表,就是最近最少访问链表,分为active和inactive两种类型的链表,最近访问的page放到active链表,这些不用被回收算法回收,如果长时间未访问的page则放到inactive链表,根据页是否被访问,该page可能在两个链表中移动。在每个memory node结构体中都存在对应的LRU链表结构:
/* Fields commonly accessed by the page reclaim scanner */
spinlock_t lru_lock;
struct lruvec lruvec;
对应的lruvec结构体:
struct lruvec {
struct list_head lists[NR_LRU_LISTS];
struct zone_reclaim_stat reclaim_stat;
#ifdef CONFIG_MEMCG
struct zone *zone;
#endif
};
- 从inactive链表移动到active链表
对于inactive中的page,会有两次访问才会移动到active链表,如果一次访问后间隔一定时间没有继续访问第二次,那么该page访问次数会被重置。 - 从active链表移动到inactive链表
refill_inactive_zone函数是做该项工作的,在内存回收的时候会调用shrink_zone,该函数会调用到refill_inactive_zone函数来填充inactive链表,然后从inactive链表中去回收内存页。因此refill_inactive_zone函数至关重要,
它的实现决定了哪些page可以被回收。