Bootstrap

lru缓存、lru链表、内存回收内核源码讲解

一说起内存回收,就想起来以前看内存回收源码时,一脸懵逼、头脑发胀,shrink_zone那些函数看了一遍又一遍就是看不懂,自信心第一次受到严重打击。内存回收源码涉及的知识点太多:lru缓存、lru链表、page cache、脏页回写、伙伴系统、page映射、内存分配、swap页交换、逆向映射等等,基本跟内存管理扯上关系的知识点都有涉及到。需要有个循序渐进的学习方法,才能一点点搞懂内存回收的原理。

在内存紧张时,就会尝试回收文件页和匿名页page。大部分系统默认应该都没有配置swap交换,此时内存回收并不会涉及到swap页交换。内存回收大部分针对文件页page,本文主要谈谈内存回收涉及的lru缓存、lru链表、文件页内存回收这几个知识点,希望读者看完后能对内存回收有个整体框架的理解,知道它是怎么运作的,学习到内存回收的核心流程。内核源码基于3.10.96,详细源码注释见GitHub - dongzhiyan-stack/kernel-code-comment: 3.10.96 内核源代码注释。  在看本文前最好看下前一篇讲解page状态变化的文章《PageDirty、PageWriteback、PageReclaim、PageReferenced、PageUptodate等page的各个状态源码讲解

1 lru缓存与lru链表

本节先简单介绍lru缓存与lru链表相关数据结构,然后介绍二者的联系和作用。

struct  pagevec表示lru缓存,它的成员struct page *pages[]保存page指针,成员nr表示该结构目前保存了多少个page指针。

  1. #define PAGEVEC_SIZE    14
  2. struct pagevec {
  3.     unsigned long nr;//表示当前lru缓存保存了多少个page
  4.     unsigned long cold;
  5.     //lru缓存中的page指针存放在该pages[]数组
  6.     struct page *pages[PAGEVEC_SIZE];
  7. };

列一下常见的lru缓存成员lru_add_pvecslru_rotate_pvecslru_deactivate_pvecs,不同内核版本略有差异,定义在mm/swap.c中

  1. //新的page先添加到lru_add_pvecs,见__lru_cache_add()
  2. static DEFINE_PER_CPU(struct pagevec[NR_LRU_LISTS], lru_add_pvecs);
  3. //该链表page将来是被添加到inactive lru链表尾,见rotate_reclaimable_page()
  4. static DEFINE_PER_CPU(struct pagevec, lru_rotate_pvecs);
  5. //该链表page将来被添加到inactive lru链表头,见deactivate_page()
  6. static DEFINE_PER_CPU(struct pagevec, lru_deactivate_pvecs);

struct  lruvec是lru链表结构,如下:

  1. struct lruvec {
  2.     //5lru链表:LRU_INACTIVE_ANONLRU_ACTIVE_ANONLRU_INACTIVE_FILELRU_ACTIVE_FILELRU_UNEVICTABLE
  3.     struct list_head lists[NR_LRU_LISTS];
  4.     struct zone_reclaim_stat reclaim_stat;//update_page_reclaim_stat()中增加
  5. #ifdef CONFIG_MEMCG
  6.     struct zone *zone;
  7. #endif
  8. };

它的成员struct  list_head  lists[NR_LRU_LISTS]是个结构体指针数组,都是lru链表,依次保存LRU_INACTIVE_ANON、LRU_ACTIVE_ANON、LRU_INACTIVE_FILE、LRU_ACTIVE_FILE、LRU_UNEVICTABLE5这5类page指针。LRU_INACTIVE_ANON和LRU_ACTIVE_ANON这两个lru链表分别保存inactive/active 匿名页(应用程序堆栈、tmpfs、shmem等分配的内存页),LRU_INACTIVE_FILE和LRU_ACTIVE_FILE这两个lru链表分别保存inactive/active 文件页。

当新分配一个匿名页/文件页 page,是先添加到 inactive anon/file lru链表,然后随着page被访问,这个page将被移动到avtive anon/file  lru链表。anon是匿名页的简称,file是文件页的简称。内存回收过程将page将会在inactive anon/file lru和avtive anon/file  lru链表之间来回转换,比如从avtive anon/file lru链表转移page到inactive anon/file lru链表。有一点需要强调,内存回收肯定是从inactive anon/file lru链表尾取出page,看这个page能否回收。下文会详细介绍这些知识点。

struct  lruvec是stuct  zone和struct mem_cgroup_per_zone的成员。stuct  zone就是常见的dma、normal、high内存zone,struct mem_cgroup_per_zone是memory cgroup的引入的概念。我的理解是,它用来管理memory cgroup在dma、normal、high这几个内存zone分配的内存page,在内存回收查找lruvec时会用到。这里需要说明的一点是,当向lru链表lruvec中添加page时,需要先执行spin_lock_irqsave(&zone->lru_lock, flags)加锁,因为要修改lruvec的struct  list_head lists[]链表。

以内核读文件时执行的do_generic_file_read函数为例。该函数里,如果对应文件索引index的文件页还没有分配page结构,则会执行page = page_cache_alloc_cold(mapping)类似函数分配一个struct page结构,然后执行add_to_page_cache_lru(page, mapping,index, GFP_KERNEL)函数把该page添加到lru缓存。看下add_to_page_cache_lru()函数的源码:

  1. int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
  2.                 pgoff_t offset, gfp_t gfp_mask)
  3. {
  4.     int ret;
  5.     //page按照索引index添加到radix tree
  6.     ret = add_to_page_cache(page, mapping, offset, gfp_mask);
  7. if (ret == 0)
  8. // page添加到当前cpulru缓存page链表,LRU_INACTIVE_FILE属性的
  9.         lru_cache_add_file(page);
  10.     return ret;
  11. }

首先执行add_to_page_cache()把page按照索引添加到radix tree,然后执行lru_cache_add_file()把该page添加到lru缓存或者lru链表。

  1. //page添加到当前cpulru缓存page链表,LRU_INACTIVE_FILE属性的
  2. static inline void lru_cache_add_file(struct page *page)
  3. {
  4.     __lru_cache_add(page, LRU_INACTIVE_FILE);
  5. }
  6. //尝试把page添加到lru_add_pvecs[lru]这个lru缓存,lru缓存满了则把lru缓存的page添加到lru链表
  7. void __lru_cache_add(struct page *page, enum lru_list lru)
  8. {
  9.     //得到当前cpulru_add_pvecs链表上lru编号指定属性的lru缓存pagevec
  10.     struct pagevec *pvec = &get_cpu_var(lru_add_pvecs)[lru];
  11.     page_cache_get(page);
  12.     //如果当前cpu lru缓存已经存放了14page,满了,先把这14page移动到lru链表,下边再把这个新的page添加到lru缓存
  13.     if (!pagevec_space(pvec))
  14.         __pagevec_lru_add(pvec, lru);
  15.    
  16.     //page添加到lru缓存链表,其实只是把page指针保存到pvec->pages[]数组而已,即pvec->pages[pvec->nr++] = page
  17.     pagevec_add(pvec, page);
  18.     put_cpu_var(lru_add_pvecs);
  19. }

__lru_cache_add()函数是重点,if (!pagevec_space(pvec))是判断lru缓存是否已经保存了14个page指针。如果是则执行__pagevec_lru_add()把lru缓存的14个page移动到lru链表。__lru_cache_add()函数最后执行pagevec_add(pvec, page)把当前的page添加到lru缓存。

到这里,lru缓存和lru链表到底是什么关系?lru缓存是per cpu变量,当内核为文件页或匿名页分配page结构,原本是要把这些page添加到inactive file/anon lru链表的(新分配的page先添加到inactive lru链表)。但实际是先把这些page指针添加到本地cpu的lru缓存,然后等本次cpu的lru缓存添加够14个page指针,才把这14 page指针一次性移动到lru链表。

lru链表是多核CPU共享的,当向lru链表添加page,需要执行spin_lock_irqsave(&zone->lru_lock, flags)加锁。如果多进程向同一个lru链表频繁添加/剔除page,加锁操作很影响性能,这种情况很常见。因此引出了lru缓存,它是个per cpu变量,每个cpu独一个。当向lru链表添加page时,先添加到本地cpu lru缓存,向lru缓存添加够14个page再一次性把这14个page移动到lru链表,这样可以避免频繁的spin_lock_irqsave(&zone->lru_lock, flags)加锁。

这里还要啰嗦一下__lru_cache_add->__pagevec_lru_add函数,将lru缓存中的14个page移动到inactive lru链表。

  1. void __pagevec_lru_add(struct pagevec *pvec, enum lru_list lru)
  2. {
  3.     VM_BUG_ON(is_unevictable_lru(lru));
  4.     //pvec这个lru缓存中14page添加到对应lru链表,增加page统计计数,lruactive anon/file
  5.     pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, (void *)lru);
  6. }
  7. static void pagevec_lru_move_fn(struct pagevec *pvec,
  8.     void (*move_fn)(struct page *page, struct lruvec *lruvec, void *arg),
  9.     void *arg)
  10. {
  11.     int i;
  12.     struct zone *zone = NULL;
  13.     struct lruvec *lruvec;
  14.     unsigned long flags = 0;
  15.     //遍历lru缓存上的PAGEVEC_SIZEpage
  16.     for (i = 0; i < pagevec_count(pvec); i++) {
  17.         //依次取出lru缓存保存的page
  18.         struct page *page = pvec->pages[i];
  19.         struct zone *pagezone = page_zone(page);
  20.         //上一个page和这个page不属于同一个zone,则要把上个zonelock锁解除再把当前page所在zone加锁
  21.         if (pagezone != zone) {
  22.             if (zone)
  23.                 spin_unlock_irqrestore(&zone->lru_lock, flags);
  24.             zone = pagezone;
  25.             spin_lock_irqsave(&zone->lru_lock, flags);
  26.         }
  27.         //根据page返回lruvec
  28.         lruvec = mem_cgroup_page_lruvec(page, zone);
  29.         //设置page属性,把page添加到对应属性的lru链表(active/inactive file/anon)mem cgroup中增加page个数统计,增加lru链表中page计数。常见的move_fn函数是__pagevec_lru_add_fn/__activate_page/pagevec_move_tail_fn
  30.         (*move_fn)(page, lruvec, arg);
  31.     }
  32.     if (zone)
  33.         spin_unlock_irqrestore(&zone->lru_lock, flags);
  34.     release_pages(pvec->pages, pvec->nr, pvec->cold);
  35.     pagevec_reinit(pvec);
  36. }

在pagevec_lru_move_fn()函数里调用move_fn函数指针(我们以__pagevec_lru_add_fn为例),__pagevec_lru_add_fn函数将lru缓存中的14个page添加到inactive/active  file/anon lru链表。源码如下:

  1. //设置pagelruactive属性,把page添加到lru编号指定属性的lru链表,mem cgroup中增加page个数统计,增加lru编号指定属性的page计数
  2. static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
  3.                  void *arg)//arg就是lruactiveinactivefileanon属性编号
  4. {
  5.     enum lru_list lru = (enum lru_list)arg;
  6.     int file = is_file_lru(lru);//文件页?匿名页?
  7.     int active = is_active_lru(lru);//active?inactive?
  8.     //设置pagelru属性
  9.     SetPageLRU(page);
  10.     if (active)//设置pageactive属性
  11.         SetPageActive(page);
  12.     //page添加到lru编号指定属性的lru链表,mem cgroup中增加page个数统计,增加lru编号指定属性的page计数
  13.     add_page_to_lru_list(page, lruvec, lru);
  14.     update_page_reclaim_stat(lruvec, file, active);
  15. }

add_page_to_lru_list函数具体将一个page指针添加到lru链表lruvec->lists[LRU_INACTIVE_FILE]。

  1. //page添加到lru编号指定属性的lru链表,mem cgroup中增加page个数统计,增加lru编号指定属性的page计数
  2. static __always_inline void add_page_to_lru_list(struct page *page,
  3.                 struct lruvec *lruvec, enum lru_list lru)
  4. {
  5.     int nr_pages = hpage_nr_pages(page);
  6.     //mem cgroup中增加page个数统计
  7.     mem_cgroup_update_lru_size(lruvec, lru, nr_pages);
  8.     //page添加到指定属性的lru链表,如lruvec->lists[LRU_INACTIVE_FILE]
  9.     list_add(&page->lru, &lruvec->lists[lru]);
  10.     //增加lru编号指定属性的page计数
  11.     __mod_zone_page_state(lruvec_zone(lruvec), NR_LRU_BASE + lru, nr_pages);
  12. }

如上显示的是将一个page添加到lru缓存(lru_add_pvecs)或者lru链表(lruvec->lists[])的过程。文件页或者匿名页page添加到lru缓存或者lru链表过程都是类似的,可以看下lru_cache_add_anon()和lru_cache_add_file()函数,分别演示新分配的匿名页和文件页page添加到lru缓存和lru链表。

2 内存回收分析

在内核alloc_pages直接分配内存失败,就会触发直接内存内存,也会唤醒kswapd进程进行内存回收,依据剩下的内存跟内存zone水位water+内核预留内存的关系。还有一种情况是容器场景,容器的内存消耗大于容器memory cgroup内存上限,也会触发内存回收(mem_cgroup_reclaim->try_to_free_mem_cgroup_pages->do_try_to_free_pages)。不管是何种形式的内存回收,最后都会执行shrink_zone()回收文件页和匿名页page,当然也会执行shrink_slab()回收slab内存。本文主要讲解shrink_zone()回收文件页page,相对来说会简单点。先把内存整体流程图贴下,内存回收核心函数shrink_inactive_list()的流程图随着源码讲解再贴出。

2.1 shrink_zone、shrink_lruvec、shrink_list 源码讲解

  1. static void shrink_zone(struct zone *zone, struct scan_control *sc)
  2. {
  3.     unsigned long nr_reclaimed, nr_scanned;
  4.     do {
  5.         struct mem_cgroup *root = sc->target_mem_cgroup;
  6.         struct mem_cgroup_reclaim_cookie reclaim = {
  7.             .zone = zone,
  8.             .priority = sc->priority,
  9.         };
  10.         struct mem_cgroup *memcg;
  11.         //初始回收page
  12.         nr_reclaimed = sc->nr_reclaimed;
  13.         //初始扫描page
  14.         nr_scanned = sc->nr_scanned;
  15.         //memory cgroup内存回收,返回的应该是root_mem_cgroup,管理的是内存zone所有的page。否则返回的memcg管理的page只是该memory cgroup在该内存zone分配的内存page
  16.         memcg = mem_cgroup_iter(root, NULL, &reclaim);
  17.         do {
  18.             struct lruvec *lruvec;
  19.             //返回页lru链表结构lruvec,从该lruveclru链表取出文件页/匿名页 进行内存回收
  20.             lruvec = mem_cgroup_zone_lruvec(zone, memcg);
  21.             shrink_lruvec(lruvec, sc);
  22.             /*如果不是全局内存回收,并且回收的page数达到预期,则if成立break,否则下边mem_cgroup_iter()返回下一个memcg。如果是全局内存回收,if不成立,直接执行下边的mem_cgroup_iter()返回该zone的下一个memcg*/
  23.             if (!global_reclaim(sc) &&
  24.                     sc->nr_reclaimed >= sc->nr_to_reclaim) {
  25.                 mem_cgroup_iter_break(root, memcg);
  26.                 break;
  27.             }
  28.            
  29.             //返回下一个mem cgroup
  30.             memcg = mem_cgroup_iter(root, memcg, &reclaim);
  31.         } while (memcg);
  32.         vmpressure(sc->gfp_mask, sc->target_mem_cgroup,
  33.                sc->nr_scanned - nr_scanned,
  34.                sc->nr_reclaimed - nr_reclaimed);
  35.     //sc->nr_reclaimed - nr_reclaimed本轮循环回收的page数,sc->nr_scanned - nr_scanned是本轮循环扫描page数。这是判断内存回收是否足够,足够则结束内存回收
  36.     } while (should_continue_reclaim(zone, sc->nr_reclaimed - nr_reclaimed,
  37.                      sc->nr_scanned - nr_scanned, sc));
  38. }

该函数首先找到lru链表数据结构lruvec,然后执行shrink_lruvec()开始扫描该lruvec结构inactive file、active file、active anon、inactive anon 这4个lru链表上非活动文件页page、活动文件页page、活动匿名页page、非活动匿名页page,看那个page可以被释放掉。本文主要讲解inactive file/active file 链表上的非活动文件页page、活动文件页page的内存回收。

说下struct  scan_control结构,是内存回收必然要传递的一个结构。看下kswapd内存回收传递的scan_control结构。

  1.     struct scan_control sc = {
  2.         .gfp_mask = GFP_KERNEL,
  3.         .may_writepage= 1,//0则不能将page脏数据回写到磁盘,应该包含文件页和匿名页page
  4.         .may_unmap = 0,//为0表示不能回收mmap的文件页
  5.         .may_swap = 1,//为0不会对匿名页进行回收
  6.         .nr_to_reclaim = ULONG_MAX,
  7.         .order = order,//内存分配时的order
  8.         .target_mem_cgroup = NULL,//memory cgroup内存回收
  9.     };

进入shrink_lruvec函数,先执行get_scan_count(lruvec, sc, nr)计算要扫描的各个lru链表(active/inactive anon/file  lru链表)的page数,并把要扫描的lru链表的page数保存到nr[]数组,然后循环执行shrink_list()函数扫描、回收active/inactive anon/file lru链表的page。

  1. static void shrink_lruvec(struct lruvec *lruvec, struct scan_control *sc)
  2. {
  3.     unsigned long nr[NR_LRU_LISTS];
  4.     unsigned long nr_to_scan;
  5.     enum lru_list lru;
  6.     unsigned long nr_reclaimed = 0;
  7.     unsigned long nr_to_reclaim = sc->nr_to_reclaim;
  8.     struct blk_plug plug;
  9.     //计算本次要扫描的active/inactive anon/file lru链表的page数保存到nr[]数组
  10.     get_scan_count(lruvec, sc, nr);
  11.     blk_start_plug(&plug);
  12.     //一直循环,直到预期要扫描的inactive anonactive fileinactive file lru链表上的page全扫描过
  13.     while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] ||
  14.                     nr[LRU_INACTIVE_FILE]) {
  15.         //for循环,lru依次是:LRU_INACTIVE_ANON LRU_ACTIVE_ANON  LRU_INACTIVE_FILE LRU_ACTIVE_FILE
  16.         for_each_evictable_lru(lru) {
  17.             //不为0表示预期的page数还没扫描完
  18.             if (nr[lru]) {
  19.                 //本轮要扫描的page数,最多32
  20.                 nr_to_scan = min(nr[lru], SWAP_CLUSTER_MAX);
  21.                 //nr[lru]减去扫描的page
  22.                 nr[lru] -= nr_to_scan;
  23.                 //内存回收在这里
  24.                 nr_reclaimed += shrink_list(lru, nr_to_scan,
  25.                                 lruvec, sc);
  26.             }
  27.         }
  28.         //回到page数大于预期,并且设定的优先级小于DEF_PRIORITY,结束内存回收
  29.         if (nr_reclaimed >= nr_to_reclaim &&
  30.             sc->priority < DEF_PRIORITY)
  31.            
  32.             break;
  33.     }
  34.     //启动block io派发,这里才会令page页数据刷入磁盘
  35.     blk_finish_plug(&plug);
  36.     //sc->nr_reclaimed累加回收page
  37.     sc->nr_reclaimed += nr_reclaimed;
  38.     //如果inactive anon lru链表上的page太少
  39.     if (inactive_anon_is_low(lruvec))
  40.         //active anon lru链表移动pageinactive anon lru链表
  41.         shrink_active_list(SWAP_CLUSTER_MAX, lruvec,
  42.                    sc, LRU_ACTIVE_ANON);
  43.                   
  44.     //太多脏页则休眠
  45.     throttle_vm_writeout(sc->gfp_mask);
  46. }

直接进入shrink_list()函数

  1. static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan,
  2.                  struct lruvec *lruvec, struct scan_control *sc)
  3. {
  4.     //lruactive anonactive file时,就是说lru代表avtive lru链表时if才成立
  5.     if (is_active_lru(lru)) {
  6.         //inactive anon/file lru链表上的page数太少
  7.         if (inactive_list_is_low(lruvec, lru))
  8.             //active anon/file lru链表向inactive anon/file lru链表头移动page
  9.             shrink_active_list(nr_to_scan, lruvec, sc, lru);
  10.         //注意,lruactive anon/file时,直接在这里返回0。内存回收只是针对inactive file/anon
  11.         return 0;
  12.     }
  13.    
  14.     //lruinactive anon/file时,从inactive file/anon lru链表尾部扫描page,尝试回收内存
  15.     return shrink_inactive_list(nr_to_scan, lruvec, sc, lru);
  16. }

注意,shrink_list()函数会在shrink_lruvec()函数中被多次调用,只是shrink_list()传入的struct lruvec *lruvec 不同,可能依次代表inactive file、active file、active anon、inactive anon 这4个lru链表。当代表active file、active anon时,if (is_active_lru(lru))成立,接着就会执行if (inactive_list_is_low(lruvec, lru))判断inactive anon/file lru链表上的page数是否太少,是的话则执行shrink_active_list(nr_to_scan, lruvec, sc, lru)尝试从active anon/file lru链表向inactive anon/file lru链表移动一定数目的page,然后就return 0返回了。内存回收并不会尝试对active file、active anon lru链表上的page尝试回收,只会对inactive anon/file  lru链表上的page进行回收,再执行shrink_inactive_list()函数完成回收。

2.2 shrink_active_list讲解

先看下shrink_active_list()函数源码:

  1. static void shrink_active_list(unsigned long nr_to_scan,
  2.                    struct lruvec *lruvec,
  3.                    struct scan_control *sc,
  4.                    enum lru_list lru)//此时lru肯定是LRU_ACTIVE_ANON或者LRU_ACTIVE_FILE
  5. {
  6.     unsigned long nr_taken;
  7.     unsigned long nr_scanned;
  8.     unsigned long vm_flags;
  9.     LIST_HEAD(l_hold);  /* The pages which were snipped off */
  10.     LIST_HEAD(l_active);
  11.     LIST_HEAD(l_inactive);
  12.     struct page *page;
  13.     struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
  14.     unsigned long nr_rotated = 0;
  15.     isolate_mode_t isolate_mode = 0;
  16.     int file = is_file_lru(lru);//返回值是 LRU_INACTIVE_FILE LRU_ACTIVE_FILE
  17.     struct zone *zone = lruvec_zone(lruvec);
  18.    
  19.     //cpu本地所有lru缓存上的page移动到 active/inactive lru链表
  20.     lru_add_drain();
  21.     if (!sc->may_unmap)
  22.         isolate_mode |= ISOLATE_UNMAPPED;
  23.     if (!sc->may_writepage)
  24.         isolate_mode |= ISOLATE_CLEAN;
  25.     spin_lock_irq(&zone->lru_lock);
  26.     //根据isolate_mode隔离条件,依次尝试将active anon/file lru链表尾巴上符合隔离条件的page移动到l_hold链表,返回值是成功隔离的page
  27.     nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold,
  28.                      &nr_scanned, sc, isolate_mode, lru);
  29.    
  30.     if (global_reclaim(sc))
  31.         zone->pages_scanned += nr_scanned;
  32.     reclaim_stat->recent_scanned[file] += nr_taken;//累加隔离的page
  33.     __count_zone_vm_events(PGREFILL, zone, nr_scanned);
  34.    
  35.     __mod_zone_page_state(zone, NR_LRU_BASE + lru, -nr_taken);//LRU_ACTIVE_ANONLRU_ACTIVE_FILE lru链表的page数减少nr_taken
  36.     __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, nr_taken);//隔离的page数增加nr_taken
  37.     spin_unlock_irq(&zone->lru_lock);
  38.     //遍历l_hold隔离出来的page,把符合条件的page移动l_inactive临时链表
  39.     while (!list_empty(&l_hold)) {
  40.         cond_resched();
  41.         page = lru_to_page(&l_hold);
  42.         list_del(&page->lru);
  43.         if (unlikely(!page_evictable(page))) {//page不可回收返回0
  44.             putback_lru_page(page);//page添加到unevictable lru list
  45.             continue;
  46.         }
  47.         //bh太多了,应该就是说文件缓存page太多了
  48.         if (unlikely(buffer_heads_over_limit)) {
  49.             //if成立条件 1:pagePAGE_FLAGS_PRIVATE标记  2:page加锁成功
  50.             if (page_has_private(page) && trylock_page(page)) {
  51.                 if (page_has_private(page))
  52.                     try_to_release_page(page, 0);//释放page
  53.                
  54.                 unlock_page(page);//清理pagelock标记,并唤醒在page PG_locked等待队列的休眠的进程
  55.             }
  56.         }
  57.        
  58.         //如果page最近被访问过
  59.         if (page_referenced(page, 0, sc->target_mem_cgroup,
  60.                     &vm_flags)) {
  61.             nr_rotated += hpage_nr_pages(page);
  62.             //pageelv文件代码段映射
  63.             if ((vm_flags & VM_EXEC) && page_is_file_cache(page)) {
  64.                 list_add(&page->lru, &l_active);//page移动到临时l_active链表
  65.                 continue;
  66.             }
  67.         }
  68.        
  69.         //到这里,page最近没被访问过,则把page先移动到临时l_inactive
  70.         ClearPageActive(page);  /* we are de-activating */
  71.         list_add(&page->lru, &l_inactive);
  72.     }
  73.     spin_lock_irq(&zone->lru_lock);
  74.     reclaim_stat->recent_rotated[file] += nr_rotated;//最近被访问过的page
  75.     //l_active链表上的page移动到 active file/anon lru链表,如果page的引用计数是1,则把page移动到l_hold链表,马上就释放掉该page
  76.     move_active_pages_to_lru(lruvec, &l_active, &l_hold, lru);
  77.     //l_inactive链表上的page移动到 inactive file/anon lru链表,如果page的引用计数是1,则把page移动到l_hold链表,马上就释放掉该page
  78.     move_active_pages_to_lru(lruvec, &l_inactive, &l_hold, lru - LRU_ACTIVE);
  79.     //不可回收page减少nr_taken
  80.     __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, -nr_taken);
  81.     spin_unlock_irq(&zone->lru_lock);
  82.     //释放l_hold上的page到伙伴系统
  83.     free_hot_cold_page_list(&l_hold, 1);
  84. }

总结一下该函数关键步骤:

1 调用lru_add_drain()把所有lru缓存上的page移动的lru链表

2 先调用isolate_lru_pages()把active anon/file lru链表尾巴上符合隔离条件的page移动到l_hold临时链表。

3 然后再依次判断l_hold上的这些page最近是否被访问过、是否是代码段映射的page。如果是则把page移动到l_active临时链表。如果不是则把page移动到l_inactive链表。

4 执行move_active_pages_to_lru()函数把l_active临时链表上的page移动到active file/anon lru链表头,把l_inactive临时链表上的page移动到inactive file/anon lru链表头。如果移动过程page引用计数是1,则再把page移动到l_hold临时链表。

5 执行free_hot_cold_page_list(&l_hold, 1)把l_hold临时链表上的page释放到伙伴系统。

总体来说,先遍历active file/anon lru链表上的page,先把符合条件的page隔离到l_hold链表。接着,如果这些page最近被访问过等原因,再还会再把该page移动到active file/anon lru链表头,否则把page移动到inactive file/anon lru链表头。如果这些page引用计数只有1,则会把该page释放会伙伴系统。

2.3 shrink_inactive_list函数讲解

接着看内存回收的关键函数shrink_inactive_list()函数:

  1. static noinline_for_stack unsigned long
  2. shrink_inactive_list(unsigned long nr_to_scan, struct lruvec *lruvec,
  3.              struct scan_control *sc, enum lru_list lru)
  4. {
  5.     LIST_HEAD(page_list);
  6.     unsigned long nr_scanned;
  7.     unsigned long nr_reclaimed = 0;
  8.     unsigned long nr_taken;
  9.     unsigned long nr_dirty = 0;
  10.     unsigned long nr_writeback = 0;
  11.     isolate_mode_t isolate_mode = 0;
  12.     int file = is_file_lru(lru);//lruLRU_INACTIVE_FILE LRU_ACTIVE_FILE时返回1
  13.     struct zone *zone = lruvec_zone(lruvec);
  14.     struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
  15.     while (unlikely(too_many_isolated(zone, file, sc))) {
  16.         congestion_wait(BLK_RW_ASYNC, HZ/10);
  17.         if (fatal_signal_pending(current))
  18.             return SWAP_CLUSTER_MAX;
  19.     }
  20.     //cpu本地所有lru缓存上的page移动到 active/inactive lru链表
  21.     lru_add_drain();
  22.     if (!sc->may_unmap)
  23.         isolate_mode |= ISOLATE_UNMAPPED;//只隔离没有映射过的page
  24.     if (!sc->may_writepage)
  25.         isolate_mode |= ISOLATE_CLEAN;//只隔离干净页,不隔离脏页或者正在脏页回写的页
  26.     spin_lock_irq(&zone->lru_lock);
  27.     /*根据isolate_mode隔离条件,依次尝试将inactive anon/file lru链表尾巴上符合隔离条件的page移动到page_list临时链表。返回值nr_taken是成功隔离的page数。注意,是从inactive anon/file lru链表隔离page并添加到page_list临时链表*/
  28.     nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &page_list,
  29.                      &nr_scanned, sc, isolate_mode, lru);//nr_scanned是扫描page
  30.     //inactive file/anon lru链表上的page减去nr_taken
  31.     __mod_zone_page_state(zone, NR_LRU_BASE + lru, -nr_taken);
  32.     //隔离的page数增加nr_taken
  33.     __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, nr_taken);
  34.     if (global_reclaim(sc)) {//全局内存回收
  35.         zone->pages_scanned += nr_scanned;//累加扫描pagenr_scanned
  36.        
  37.         if (current_is_kswapd())//kswapd进程的内存回收
  38.             __count_zone_vm_events(PGSCAN_KSWAPD, zone, nr_scanned);
  39.         else//直接内存回收
  40.             __count_zone_vm_events(PGSCAN_DIRECT, zone, nr_scanned);
  41.     }
  42.     spin_unlock_irq(&zone->lru_lock);
  43.     if (nr_taken == 0)//隔离page数是0直接返回
  44.         return 0;
  45.      //在这里真正尝试内存回收
  46.     nr_reclaimed = shrink_page_list(&page_list, zone, sc, TTU_UNMAP,
  47.                     &nr_dirty, &nr_writeback, false);
  48.     spin_lock_irq(&zone->lru_lock);
  49.     reclaim_stat->recent_scanned[file] += nr_taken;
  50. ...........
  51.     //page_list中的page按照条件移动到对应的lru链表,如果前边的shrink_page_list()函数对page_list链表上某些page标记Active(比如page最近被访问了),则把该page移动active lru链表头,否则移动到inactive lru链表头
  52.     putback_inactive_pages(lruvec, &page_list);
  53.     //更新file/anon NR_ISOLATED_ANONpage个数
  54.     __mod_zone_page_state(zone, NR_ISOLATED_ANON + file, -nr_taken);
  55.     spin_unlock_irq(&zone->lru_lock);
  56.     //这里把page_list链表还残留的page释放到伙伴系统,1表示释放到冷页,这里的page->_count应用计数应该是0
  57.     free_hot_cold_page_list(&page_list, 1);
  58.     //如果内存回收的page,全都在进行脏页回写,则执行wait_iff_congested(),可能会休眠
  59.     if (nr_writeback && nr_writeback >=
  60.             (nr_taken >> (DEF_PRIORITY - sc->priority)))
  61.         //如果bdizone同时拥堵,则休眠100ms
  62.         wait_iff_congested(zone, BLK_RW_ASYNC, HZ/10);
  63.    ...........
  64.     return nr_reclaimed;
  65. }

shrink_inactive_list()函数首先执行isolate_lru_pages()函数:根据isolate_mode隔离条件,依次尝试将inactive anon/file lru链表尾巴上符合隔离条件的page移动到page_list临时链表。到这里可能读者会有疑惑,前边执行shrink_active_list()函数,不是已经将active lru链表上的page刚执行isolate_lru_pages()隔离一次,然后把隔离出来符合条件的page移动到inactive anon/file lru链表。现在inactive anon/file lru链表上的page也执行isolate_lru_pages()尝试隔离到page_list临时链表,此时隔离出来的page有一部分应该是刚从active lru链表隔离出来并移动到inactive anon/file lru链表的page。

接着执行shrink_inactive_list()函数尝试对page_list临时链表上的page尝试内存回收,该链表上的page并不能全部被回收,如果某各page正好又被访问了等因素,则就要令该page停留在page_list临时链表。shrink_inactive_list()函数继续执行putback_inactive_pages(lruvec, &page_list)函数,将page_list中残留的page按照条件移动到对应的lru链表,如果该page最近被访问了,则该page已经被标记了” Active”,该page就会被移动active lru链表。否则该page依然被移动到inactive lru链表。最后执行free_hot_cold_page_list(&page_list, 1)函数,把page_list链表依然还残留的page释放到伙伴系统,这种page的引用计数是0,可以被释放。

好的,shrink_inactive_list()函数大体流程已经讲解完了,接着重点讲解该函数里的shrink_page_list()函数,它才是内存回收最重要的函数。

2.4 shrink_page_list函数讲解

这个函数真的复杂,我们只讲解与文件页内存回收那部分,尽可能多举例,源码有删减。

  1. static unsigned long shrink_page_list(struct list_head *page_list,
  2.                       struct zone *zone,//page_listinactive anon/file lru链表扫描出来符合内存回收条件的page
  3.                       struct scan_control *sc,
  4.                       enum ttu_flags ttu_flags,
  5.                       unsigned long *ret_nr_dirty,
  6.                       unsigned long *ret_nr_writeback,
  7.                       bool force_reclaim)
  8. {
  9.     LIST_HEAD(ret_pages);
  10.     LIST_HEAD(free_pages);
  11.     int pgactivate = 0;
  12.     unsigned long nr_dirty = 0;
  13.     unsigned long nr_congested = 0;
  14.     unsigned long nr_reclaimed = 0;
  15.     unsigned long nr_writeback = 0;
  16.     while (!list_empty(page_list)) {
  17.         struct address_space *mapping;
  18.         struct page *page;
  19.         int may_enter_fs;//是否允许文件系统类的IO操作
  20.         enum page_references references = PAGEREF_RECLAIM_CLEAN;
  21.        
  22.         //page_list链表尾取出page
  23.         page = lru_to_page(page_list);
  24.         list_del(&page->lru);
  25.        
  26.         sc->nr_scanned++;//扫描page数加1
  27.        
  28.          //如果本次内存回收设定 不能回收mmap的page,但该page却是,则不能回收
  29.         if (!sc->may_unmap && page_mapped(page))
  30.             goto keep_locked;
  31.        
  32.         //如果page正在进行脏页回写
  33.         if (PageWriteback(page)) {
  34.             //if成立条件 1:如果是全局内存回收  2:page没有被设置Reclaim内存回收标记  3:may_enter_fs0,即不进行IO操作
  35.             if (global_reclaim(sc) || !PageReclaim(page) || !may_enter_fs) {
  36.                 //设置page Reclaim内存回收标记
  37.                 SetPageReclaim(page);
  38.                 //nr_writeback1
  39.                 nr_writeback++;
  40.                 goto keep_locked;//跳到keep_locked分支,遍历下一个page
  41.             }
  42.             //page PG_writeback等待队列上休眠,等待该page脏页回写完成
  43.             wait_on_page_writeback(page);
  44.         }
  45.        
  46.         if (!force_reclaim)//shrink_inactive_list触发的内存回收,force_reclaimfalse,这是非强制内存回收
  47.             references = page_check_references(page, sc);//检查最近是否被访问
  48.        
  49.         /*基本上只要page最近被访问过,返回PAGEREF_ACTIVATEpage将被移动到active list,或者返回PAGEREF_KEEP,本轮内存回收不回收它。如果page最近没被访问过,返回PAGEREF_RECLAIMPAGEREF_RECLAIM_CLEAN,该page将被回收*/
  50.         switch (references) {//非强制内存回收时成立
  51.         case PAGEREF_ACTIVATE: //page移动到active lru链表,不回收它
  52.             goto activate_locked;
  53.         case PAGEREF_KEEP: //page保持在inactive lru链表,不回收它
  54.             goto keep_locked;
  55.         case PAGEREF_RECLAIM: //page要内存回收
  56.         case PAGEREF_RECLAIM_CLEAN: //page要内存回收
  57.             ; /* try to reclaim the page below */
  58.         }
  59.         /*有多个进程映射了此内存page,尝试解除映射。对匿名页和文件页的处理不一样,对于文件页的处理是清空进程的页表项,每清空一个进程的映射page->_count1,这里边还牵涉到反向映射。对于文件页,进程mmap文件映射该page时,page_mapped(page)才成立。正常文件读写产生的page cache,没有进程映射吧,page_mapped(page)不成立*/
  60.         if (page_mapped(page) && mapping) {
  61.             switch (try_to_unmap(page, ttu_flags)) {
  62.             case SWAP_FAIL:
  63.                 goto activate_locked;
  64.             case SWAP_AGAIN:
  65.                 goto keep_locked;
  66.             case SWAP_MLOCK:
  67.                 goto cull_mlocked;
  68.                
  69.             case SWAP_SUCCESS://这里成功解除映射了此page的进程
  70.                 ; /* try to free the page below */
  71.             }
  72.         }
  73.        
  74.         //如果page是脏页
  75.         if (PageDirty(page)){
  76.             nr_dirty++;//脏页数加1
  77.             /*只有kswapd内存回收可以触发脏页刷回磁盘,目的是避免栈溢出。并且只有脏页太多kswapd进程才会触发脏页刷回磁盘*/
  78.             if (page_is_file_cache(page) &&//如果page是文件缓存
  79.                     (!current_is_kswapd() ||//并且不是kswapd触发的内存回收
  80.                      sc->priority >= DEF_PRIORITY - 2)) {
  81.                 //增加NR_VMSCAN_IMMEDIATEpage数,优先回收的page
  82.                 inc_zone_page_state(page, NR_VMSCAN_IMMEDIATE);
  83.                 //设置pageReclaim标记
  84.                 SetPageReclaim(page);
  85.                 /*page不参与本次内存回收,等这个page脏页回写完成,软中断回调函数里执行end_page_writeback函数才会清理Reclaim标记,然后把该page再移动inactive lru list链表尾部,等下轮内存就会释放该page*/
  86.                 goto keep_locked;
  87.             }
  88.             //如果前边执行page_check_references返回PAGEREF_RECLAIM_CLEAN,说明此页最近没被访问过,但page被置位PG_referenced
  89.             if (references == PAGEREF_RECLAIM_CLEAN)
  90.                 goto keep_locked;
  91.             //不允许文件系统类的IO操作,则该page不进行内存回收,goto keep_locked
  92.             if (!may_enter_fs)
  93.                 goto keep_locked;
  94.             //不能进行脏页回写操作,则该page不进行内存回收,goto keep_locked
  95.             if (!sc->may_writepage)
  96.                 goto keep_locked;
  97.                
  98.             //page标记Reclaim,调用文件系统write接口把page数据异步刷入磁盘,这个过程会对page标记"writeback"。只有kswsap进程能这样操作,memcg内存回收不行,直接内存回收也不行
  99.             switch (pageout(page, mapping, sc)) {
  100.             case PAGE_KEEP: //failed to write page out, page is locked。如果bdi拥堵则会返回PAGE_KEEP
  101.                 nr_congested++;//拥堵page数加1
  102.                 goto keep_locked;//page本轮不能被回收,又是goto keep_locked
  103.                 
  104.             case PAGE_ACTIVATE: //page不能被回写到,之后该page将被移动到active lru链表
  105.                 goto activate_locked;
  106.                
  107.             case PAGE_SUCCESS://成功调用文件系统write接口把page数据异步刷入磁盘,
  108.                 //page数据还没刷入磁盘,刷入磁盘产生中断后才会清除"writeback"标记
  109.                 if (PageWriteback(page))
  110.                     goto keep;//goto keep_locked处理差不多
  111.                 //page有脏页标记,应该不太可能吧??????????
  112.                 if (PageDirty(page))
  113.                     goto keep;//goto keep_locked处理差不多
  114.                 //尝试对page加锁,如果page之前已经其他进程被加锁则加锁失败返回0,否则当前进程对page加锁成功并返回1
  115.                 if (!trylock_page(page))
  116.                     goto keep;//goto keep_locked处理差不多
  117.                 //上锁成功后,再判断一次page是脏页或者正在回写的脏页,如果还是有这些标记goto keep_locked
  118.                 if (PageDirty(page) || PageWriteback(page))
  119.                     goto keep_locked;
  120.                 mapping = page_mapping(page);
  121.             case PAGE_CLEAN:
  122.                 ; /* try to free the page below */
  123.             }
  124.         }
  125.         ...........
  126.         //如果page没有进程映射,或者成功page引用计数是2if不成立,这个page才可以被回收,不理解!!!!!!!!!!
  127.         if (!mapping || !__remove_mapping(mapping, page))
  128.             goto keep_locked;
  129.        
  130.                 //清除pagePG_locked标记
  131.         __clear_page_locked(page);
  132. free_it:
  133.         nr_reclaimed++;//内存回收成功的page数加1
  134.         //page添加到free_pages临时链表,下边就要释放free_pages链表上的page到伙伴系统,真正内存回收
  135.         list_add(&page->lru, &free_pages);
  136.         continue;
  137. cull_mlocked:
  138.         if (PageSwapCache(page))
  139.             try_to_free_swap(page);
  140.         unlock_page(page);
  141.         list_add(&page->lru, &ret_pages);
  142.         continue;
  143. activate_locked:
  144.         /* Not a candidate for swapping, so reclaim swap space. */
  145.         if (PageSwapCache(page) && vm_swap_full())
  146.             try_to_free_swap(page);
  147.         VM_BUG_ON(PageActive(page));
  148.         //设置pageActive标记,将来该page将被移动到active lru链表
  149.         SetPageActive(page);
  150.         pgactivate++;
  151. /*goto keep_locked这个分支的page,都不参与本轮内存回收,不符合条件。下边先把这个page移动到page_list,该函数返回后,再把page_list上的page按照实际情况移动到 active/inactive file/anon lru list。有些page是脏页,等脏页回写完成,在软中断回调函数再把该page移动inactive lru链表尾。等下次内存回收,优先回收这个page*/
  152. keep_locked:
  153.        
  154.         unlock_page(page);//清除page PG_locked标记,唤醒在page PG_locked等待队列的休眠的进程
  155. keep:
  156.         //page移动到ret_pages临时链表,函数最后再把ret_pages上的page移动到page_list
  157.         list_add(&page->lru, &ret_pages);
  158.     }
  159.    
  160.     //标记dirty的脏页,且所在块设备都是拥堵的(nr_dirty == nr_congested),并且是全局内存回收,if成立,则标记zone拥堵。
  161.     if (nr_dirty && nr_dirty == nr_congested && global_reclaim(sc))
  162.         zone_set_flag(zone, ZONE_CONGESTED);
  163.     //释放free_pages临时链表上的page到伙伴系统
  164.     free_hot_cold_page_list(&free_pages, 1);
  165.     //ret_pages临时链表上的page移动到page_list链表
  166.     list_splice(&ret_pages, page_list);
  167.     count_vm_events(PGACTIVATE, pgactivate);
  168.     mem_cgroup_uncharge_end();
  169.     //脏页总数
  170.     *ret_nr_dirty += nr_dirty;
  171.     //正在回写的脏页数
  172.     *ret_nr_writeback += nr_writeback;
  173.     //返回内存回收成功的page
  174.     return nr_reclaimed;
  175. }

shrink_page_list()循环从page_list链表取出page尝试回收,该函数真的复杂,即便我们只讲解文件页的内存回收!首先,page_list链表上page是从inactive file/ lru链表上隔离出来的,符合内存回收条件的。对于文件页来说,我觉得有以下几种情况:

  • 1 mmap与用户空间直接构成文件映射的文件页,这类文件页page内存回收时还得想办法解除文件映射关系。shrink_page_list()函数就有执行page_mapped(page)尝试解除进程用户空间与该page的文件映射关系。
  • 2 文件缓存pagecache中的page,这些page没有直接与用户空间buf构成映射。这部分文件页可能是clean的,就是进程读过这些文件页后并没有再对对该文件页有写操作。也有可能是该page有被写过,被标记了脏页”dirty”,或者这个脏页正在回写被标记了” writeback”。可以发现,shrink_page_list()有多次执行PageWriteback(page)、PageDirty(page)判断该page是否有”dirty”标记、” writeback”标记,然后判断该page能否被回收。

继续,对文件页尝试内存回收时,会有多种处理结果

1  goto activate_locked分支。这个文件页page最近被访问过,不能被回收,则goto activate_locked跳到activate_locked分支:执行SetPageActive(page)标记该page“Active”,等从shrink_page_list()返回,执行shrink_inactive_list()函数里的putback_inactive_pages()函数,再把该page移动到active lru链表头

2 goto keep_locked分支。该page暂时还不能回收,但是可能马上可能会被回收掉,则goto keep_locked跳到keep_locked分支,把page留在page_list链表。等从shrink_page_list()返回,执行shrink_inactive_list()函数里的putback_inactive_pages()函数,可能把该page添加到inactive lru链表头。这个page是脏页,正在被回写,则shrink_page_list()函数里会把该page标记“PageReclaim”。等回写完成,软中断回调函数最后执行end_page_writeback(),清除该page的“writeback”和"Reclaim"标记,再把该page移动到inactive lru链表尾。等下轮内存回收,会优先回收inactive lru链表尾的page

3 goto free_it分支。该page符合内存回收条件,则goto free_it跳到free_it分支,首先nr_reclaimed++表示成功回收一个page,然后执行list_add(&page->lru, &free_pages)把该page暂时放到free_pages链表,在shrink_page_list()函数最后执行free_hot_cold_page_list(&free_pages, 1),最终把这些符合内存回收条件的page释放会伙伴系统。

内存回收还有其他情况,最后把shrink_inactive_list 和 shrink_page_list两个函数的流程图列下,希望小伙伴能快速理解。

2.5 page 在inactive lru链表和active lru链表之间的移动

在本文最后,总结一下page 在inactive lru链表和active lru链表之间的漂泊过程。

2.5.1 当page被访问触发转移

新分配的一个文件页或者匿名页page,是加入lru缓存或者inactive lru链表。当读写page文件时,其实就是访问page,最后都执行mark_page_accessed(),可能会把page从inactive lru链表移动到active lru链表。整理的几个流程如下:

  • vfs_read->...... ->do_generic_file_read->mark_page_accessed
  • ext4_readdir->ext4_bread->ext4_getblk->sb_getblk->__getblk->__find_get_block->touch_buffer->mark_page_accessed

还有写文件的过程vfs_write->do_sync_write->ext4_file_write->generic_file_aio_write->generic_file_buffered_write->generic_perform_write,看下源码:

  1. static ssize_t generic_perform_write(struct file *file,
  2.                 struct iov_iter *i, loff_t pos)
  3. {
  4.     ext4_write_begin->lock_page(page);
  5.     //write系统调用传入的最新文件数据从用户空间buf复制到page文件页
  6.     copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
  7.     mark_page_accessed(page);//这里标记page最近被访问
  8.     ext4_write_end->unlock_page(page);
  9.     balance_dirty_pages_ratelimited(mapping);//脏页平衡
  10. } 

mark_page_accessed源码如下:

  1. void mark_page_accessed(struct page *page)
  2. {
  3.     //pageinactive的、page"Referenced"标记、page可回收、page lru链表
  4.     if (!PageActive(page) && !PageUnevictable(page) &&
  5.             PageReferenced(page) && PageLRU(page)) {
  6.         //pageinactive lru链表移动到active lru链表
  7.         activate_page(page);
  8.         //清理page"Referenced"标记
  9.         ClearPageReferenced(page);
  10.     } else if (!PageReferenced(page)) {//page之前没有"Referenced"标记
  11.         SetPageReferenced(page);//设置page"Referenced"标记
  12.     }
  13. }

显然,随着page随着被访问的次数增加,page的referenced状态就会发生改变,并且page也会在inactive/active lru链表之间迁移,主要有如下3步:

  • 1 page在inactive lru链表且page无Referenced标记,则设置page的Referenced标记。
  • 2 page在inactive lru链表且page有Referenced标记,则把page移动到active lru链表,并清理掉Referenced标记
  • 3 page在active lru链表且无referenced标记,则把仅仅标记该page的Referenced标记

   Referenced标记表示该page被访问了,上边这3步表示了page的3个状态的顺序变迁。一个page在inactive lru链表并且长时间未被访问,第一次有进程访问该page,则只是把page标记Referenced。第2次进程再访问该page,则把该page移动到active lru链表,但清理掉Referenced标记。第3次再有进程访问该page,则标记该page Referenced。如下是转移过程:pageinactive lru(unreferenced)----->pageinactive lru(referenced) ----->pageactive lru(unreferenced) ----->pageactive lru(referenced)

显然,page被访问的次数越多,page所处的档次越来越高,从最低级的pageinactive lru链表的unreferenced状态到最高级的pageactive lru链表的referenced状态

2.5.2 从active lru链表隔离page后移动到inactive lru链表

内存回收执行到shrink_active_list,先遍历active file/anon lru链表上的page,先把符合条件的page隔离到l_hold链表。接着,如果这些page最近被访问过等原因,则执行move_active_pages_to_lru()函数再把该page移动到active file/anon lru链表头。否则执行move_active_pages_to_lru()函数把page移动到inactive file/anon lru链表头

2.5.3 shrink_inactive_list回收的page移动到active lru链表

shrink_inactive_list->shrink_page_list函数中,回收从inactive lru链表尾取出的page,如果该page被访问过等原因,不符合内存回收条件,则标记该page “Active”。之后回到shrink_inactive_list函数,执行move_active_pages_to_lru函数将该page移动到active lru链表头

2.5.4 shrink_inactive_list回收的page移动到inactive lru链表

 shrink_inactive_list->shrink_page_list函数中,回收从inactive lru链表尾取出的page,如果该page正在被脏页回写等原因,则保持在inactive lru链表。回到shrink_inactive_list函数,执行move_active_pages_to_lru函数将该page移动到inactive lru链表。相当于只是从inactive lru链表尾移动到inactive lru链表头

2.5.5 end_page_writeback将page移动到inactive lru链表尾

当page脏页数据刷回磁盘,产生中断,在软中断回调函数里blk_update_request->bio_endio->ext4_end_bio->ext4_finish_bio->end_page_writeback,执行rotate_reclaimable_page将page移动到inactive lru链表尾(或lru缓存)。等下轮内存回收,优先从inactive lru链表尾取出该page回收到伙伴系统。源码如下

  1. void end_page_writeback(struct page *page)
  2. {
  3.     //如果该page被设置了"Reclaim"标记位,
  4.     if (TestClearPageReclaim(page))
  5.         rotate_reclaimable_page(page);
  6.    
  7.     //清除掉page writeback标记
  8.     if (!test_clear_page_writeback(page))
  9.         BUG();
  10.     smp_mb__after_clear_bit();
  11.     //唤醒在该pagePG_writeback等待队列休眠的进程
  12.     wake_up_page(page, PG_writeback);
  13. }
  14. /*内存回收完成后,被标记"reclaimable"page的数据刷入了磁盘,执行rotate_reclaimable_page->end_page_writeback把该page移动到inactive lru链表尾,下轮内存回收就会释放该page到伙伴系统*/
  15. void rotate_reclaimable_page(struct page *page)
  16. {
  17.     //page没有上PG_lockedpage不是脏页,page要有acive标记,page没有设置不可回收标记,page要在lru链表
  18.     if (!PageLocked(page) && !PageDirty(page) && !PageActive(page) &&
  19.         !PageUnevictable(page) && PageLRU(page)) {
  20.         struct pagevec *pvec;
  21.         unsigned long flags;
  22.         //page->count ++
  23.         page_cache_get(page);
  24.         local_irq_save(flags);
  25.         //取出本地cpu lru缓存pagevec
  26.         pvec = &__get_cpu_var(lru_rotate_pvecs);
  27.        
  28.         //先尝试把page添加到本地cpu lru缓存pagevec,如果添加后lru缓存pagevec满了,则把lru缓存pagevec中的所有page移动到inactive lru链表
  29.         if (!pagevec_add(pvec, page))
  30.             pagevec_move_tail(pvec);
  31.         local_irq_restore(flags);
  32.     }
  33. }
;