Linux内存管理--内存回收(1)


内存的回收在Linux内存管理中占据非常重要的地位,系统的内存毕竟是有限的,跑的进程成百上千,系统内存越来越小,我们必须选择一些内存进行回收,以满足别的任务的需求。在内存回收过程中,有哪些内存可以回收,什么时候进行回收,回收内存时如何尽可能的减少对系统性能的影响,回收内存的策略,这些是我们着重要关注的问题,也是本文主要阐述的重点。

1.1 内存回收的目标


不是所有的物理内存都可以参与回收的,比如要是把内核代码段的内存给回收了,系统就无法正常运行了,一般内核代码段,数据段,内核kmalloc()出来的内存,内核线程占用的内存等都是不可以回收的,除此之外的内存都是我们要回收的目标。

回收的内存主要是由用户态进程占用的内存和内核自己在运行时所使用的一些内存组成。用户态进程占用的内存主要是我们常见的进程代码段,数据段,堆栈等,内核运行使用的内存主要是磁盘高速缓存(如索引节点,目录项高速缓存),页面高速缓存(访问文件时系统生成的页面cache),mmap()文件时所用的有名映射所使用的物理内存。后面的这些内才能虽然也是内核管理使用的内存,但对其进行回收的时候,顶多影响内核的性能,而不会导致系统无法运行。

1.2 内存回收的时机


1、内存紧缺回收:grow_buffers()无法获取缓冲区页,alloc_page_buffers()无法获取页临时缓冲区首部,__alloc_pages()无法再给定的内存区分配一组连续页框。

2、周期回收:必要时,激活相应内核线程执行内存回收算法:kswapd()内核线程,检查某个内存管理区的空闲页框数是否已低于pages_high值的标高。events内核线程,一个工作者线程,回收位于高速内存缓存中的所有空闲的slab。

1.3 内存回收的策略


1.3.1 内存回收的分类

内存回收主要是要回收两类内存:最近最少使用的内存以及高速内存缓存中空闲的slab。前者主要包括用户态进程的代码段,数据段,堆栈,文件映射内存,页高速内存,后者主要包括磁盘高速缓存及一些其他的空闲内存高速缓存。

最近最少使用内存存放在一个lru链表上,每个内存管理区zone都有一个lru结构,里面含有active和inactive两个链表头,active链表上记录当前的活跃的报文,inactive用来记录当前不活跃的报文。一般我们回首lru上的inactive链表上的内存页。同时,在内存回收的过程中,会从active链表向inactive链表上补充对应的最近最少使用内存页。每个内存页的内核数据结构page上有一个标记位PG_referenced,该标记位使得一个页从"不活动“状态转为”活动“状态的时间加倍,反之亦然。比如:一个页面可能1个小时内没人反应,不能因为偶然的一次访问就认为它是活跃的,得两次才认为它是一个活跃的页面。下面是页面在inactive和active链表上转移的变化图。

Slab内存高速缓存中经常会有一些完全空闲的slab,这些是我们回收的另一个目标。

1.3.2 反向映射

对于可以通过用户态线性地址空间可以直接访问到的物理页来说,可以分为匿名页和文件映射页两类,匿名页指的是不与具体文件对应映射的物理页,比如代码段,堆栈等使用的物理页,映射页指的是映射到文件某一部分的物理页,通常使用mmap()来进行相关的映射。

对于匿名映射和文件映射来说,可能一段物理内存会在多个进程的页表中使用,比如对于匿名映射,fork()一个进程,一开始会共用父进程的物理内存,对于文件映射,多个进程可能同时映射到一个文件的同一部分文件。所以在页面回收时,需要将该页面在所有的页表引用中给去除掉。这种手段称为反向映射。想要找到使用这些物理页的页表项的话,需要先找到引用他们的页表,而页表的地址记录在每个进程的内存描述符里面,同时用来描述进程用户态地址空间的每个vm_area_struct都记录了一个指针,指向所属的内存描述符。因此只要通过物理页找到引用他们的vm_area_struct,就能找到内存描述符,从而找到页表,找到对应的页表项。

匿名页的反向映射:

对于匿名页来说,每个页面的mapping字段指向一个anon_vma描述符,anon_vma描述符中存在一个链表头,所有引用该页面的vm_area_struct都存放在里面。page,anon_vma,vm_area_struct这些数据结构的关系如下图所示:

对于匿名页来说,其被别的地址空间引用,基本上都是因为fork)进程时,子进程复制父进程的地址空间,从而被引用的。各个vm_area_struct加入anon_vma的链表的过程如下:

假设当前一个进程p,后来fork出一个子进程c。

1、当进程P为某个vm_area_struct加入第一个物理页时,比如说发生了缺页异常,动态分配一个anon_vma的数据结构,将vm_area_struct加入该anon_vma所管理的链表,vm_area_struct结构中的anon_vma字段指向该anon_vma,同时把该页面中的mapping字段赋值为该anon_vma.对于后续为该vm_area_struct申请的物理页面,其mapping字段都赋值为该anon_vma。

2、当该进程p执行fork()时,在fork的处理过程中,会调用dup_mmap()来复制进程p的线性地址空间,在dup_mmap()会复制进程p的每一个vm_area_struct,加入到自己的地址空间中,并将vm_area_struct加入到anon_vma所管理的链表中,参看anon_vma_link()。此时为进程p申请的页面被进程c共享,通过页面的mapping字段可以找到anon_vma,从anon_vma可以遍历进程p,c。

3、考虑一个问题,在进程c中才触发缺页异常被申请的内存页,其mmapping被赋值为所属vm_area_struct的vma_anon,但进程p并没有使用到该页,所以一个物理页mapping字段指向的vma_anon所下挂的vm_area_struct可能并不包含该物理页。

文件映射页的反向映射:

对于每个文件映射页,其page mapping字段指向的是对应文件的address_space数据结构,address_space中有个 struct prio_tree_root i_mmap 字段,指向一个优先树,优先树里面会把所有映射该文件内容的vm_area_struct 给组织起来。在该树中,其树的节点基地址和堆地址分别是映射的文件内容的起始地址和结束地址,要是多个进程同时映射该地址段,会用链表在该节点上将vm_area_struct串起来。

1.3.3 内存回收流程介绍     

睡眠回收我们不关注,主要介绍内存紧缺回收及周期回收:

1、内存紧缺回收主要函数是try_to_free_pages(),该函数会执行一个循环,按照优先级从12到0,依次调用shrink_caches(),shrink_slab()来回收页面,直到回收至少32个内存页面。

依次调用以下辅助函数:

shrink_caches():调用shrink_zone()对传入的zone链表中的每个zone,进行lru上面的页面回收。

shrink_slab():对磁盘索引节点cache和目录项索引节点等磁盘高速缓存进行回收,由于磁盘索引节点和目录项索引节点都是从slab高速缓存中分配的,这样就会导致空闲slab的产生,空闲slab后续会在周期性回收的cache_reap工作队列中被回收。估计也就是因为最终会清零空闲slab,才会起这么一个函数名。^_^

shrink_zone():对内存管理区上的lru链表中的非活跃页面进行回收,在非活跃页面不足的时候,调用refill_inactive_zone()对lru上的inactive链表补充非活跃页面,同时shrink_zone()调用shrink_cache()来进行页面的回收,该函数的具体解析可以参照下面的源码浅析。

shrink_list():该辅助函数在shrink_cache()中被调用,该函数对在shrink_cache)中传入的非活跃page列表进行遍历,对每个页面进行回收工作,该函数的具体解析可以参考下面的源码解析。

refill_inactie_zone():该辅助函数根据一定的规则将处于lru active链表上的活跃页面移动到inactive链表上,以补充可以回收的页面,在lru链表里有两类页,一类是属于用户态空间的页,比如用户态进程的代码段,数据段,一类是在页高速缓存中的页,系统为了降低对应用程序的影响,将要优先将页高速缓存页进行回收,同时为了系统整体性能也会适当回收用户态进程页。按照如下经验公式进行选择:

交换倾向值=映射比率/2+负荷值+交换值

2、kswapd进程一般会在系统中睡眠,但当__alloc_page()发现各个管理区的剩余页面都低于警告值(由内存管理描述符的pages_low字段和protection字段推算出来)时,会激活kswapd进程进行页面回收,直到回收的页面使得管理区的剩余页面高于zone->pages_high时才停止回收,本质上也是调用了shrink_zone)和shrink_slab()。

3、cache_reap工作队列定期运行来回收slab高速缓存中空闲的slab占用的页。


相关内容