内存管理基础学习笔记 - 5.1 页面回收 - 概述

1. 前言

本专题我们开始学习内存管理部分,本文为页面回收处理相关学习笔记。本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。
Linux页面回收主要考虑对文件映射页面和匿名映射页面的回收,由于文件映射页面大多不需要做任何处理,不像匿名页面需要写入swap分区,因此优先回收文件映射页面。

kernel版本:5.10
平台:arm64

2. 两种页面置换算法

2.1 LRU经典链表算法

在经典LRU链表中,新分配的页面加入到LRU链表的开头,将LRU链表中现存的页面向后移动一个位置。当系统内存短缺时,LRU链表尾部的页面将会离开并被换出。当系统再次需要这些页面,这些页面会重新置于LRU链表的开头。它没考虑到该页面的使用情况是否频繁,频繁使用的页面会因为在LRU链表末尾而被换出。

  1. LRU链表是最近最少使用的缩写,假定最近不使用的页面,将来一段时间也不会使用,内存不足时,这些页面将成为被换出的候选者
  2. 内核使用双向链表定义LRU链表,根据页面类型分为LRU_ANON和LRU_FILE两种链表
  3. 每种链表根据页面的活跃性分为活跃LRU和非活跃LRU,系统运行过程中页面总是在活跃和不活跃LRU链表之间迁移
  4. 内核一共有5个LRU链表:
    不活跃匿名LRU链表、活跃匿名LRU链表、不活跃文件LRU链表、活跃文件LRU链表、不可回收页面链表
  5. 内存紧缺时,优先换出page cache,因为page cache很少需要写回磁盘,匿名页面需要写回磁盘才能换出
  6. LRU链表按照node来配置,每个node都有一整套LRU链表,node中数据成员lruvec指向这些链表
    注:在Linux4.8改成基于node的LRU链表,之前是基于zone来配置
  7. LRU链表实现插入LRU链表时插入到链表头,摘除节点时从LRU尾部,实现FIFO
  8. 系统运行过程中,在活跃的LRU链表和不活跃的LRU链表之间迁移,最不常用页面慢慢移动到不活跃LRU链表的末尾

2.2 第二次机会法

  1. 第二次机会法是对LRU的改善,当选择置换页面时,和LRU一样选择最早置入链表的页面,即链表末尾的页面
  2. 设置了一个访问状态位(硬件控制比特位),检查页面的访问位
  3. 如果访问位为0,就淘汰页面,如果访问位为1,就给第二次机会,访问位清0,选择下一个页面换出;
  4. 如果该页再次被访问则访问位置1,清零后如果没有再访问,需要的时候就可以换出页面了
  5. 使用PG_active(表示该页是否活跃)和PG_reference(表示该页是否被引用过)这两个标志位来实现第二次机会法

3. 页面置换算法基础API

lru_cache_add

lru_cache_add(page)
    |--struct pagevec *pvec
    |--get_page(page)
    |--pvec = this_cpu_ptr(&lru_pvecs.lru_add)
    |  //尝试将page添加到pagevec
    \--if (!pagevec_add(pvec, page) || PageCompound(page)) 
           //如果pagevec已经满了,则添加到lru链表
           pagevec_add(pvec, page) 
			    \--__pagevec_lru_add(pvec)
			           \--pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL)
					           \--__pagevec_lru_add_fn(page, lruvec,arg)
					                  |--SetPageLRU(page)
					                  \--add_page_to_lru_list(page, lruvec, lru)
					                         \--list_add(&page->lru, &lruvec->lists[lru])

将页面添加到node对应的lru链表

lru_to_page

#define lru_to_page(head) (list_entry((head)->prev, struct page, lru))

实现从LRU链表摘取页面 ,通过链表的末尾摘取页面,实现了先进先出

mark_page_accessed(todo)

mark_page_accessed对page有如下的转换关系:
inactive,unreferenced -> inactive,referenced
inactive,referenced -> active,unreferenced
active,unreferenced -> active,referenced
可以看出一个inactive的page经过两次访问才能变为active;

page_check_references

page_check_references(page, sc)
    |--if (vm_flags & VM_LOCKED)
    |      return PAGEREF_RECLAIM; 
    |  //计算该页面访问引用了多少pte
    |--referenced_ptes = page_referenced(page,...)
    |  //读取page引用标志位,并清除标志位
    |--referenced_page = TestClearPageReferenced(page)
    |  //>>>>>>有PTE引用的页面
    |--if (referenced_ptes)
    |      //设置Referenced标志位,如对于第二次访问的page cache页增加引用计数
    |      SetPageReferenced(page)
    |      //有引用标志位,或者共享的page cache,重新链接到active list
    |      if (referenced_page || referenced_ptes > 1)
    |          return PAGEREF_ACTIVATE
    |      //可执行文件页面,重新连接到active list
    |      if ((vm_flags & VM_EXEC) && !PageSwapBacked(page))
    |          return PAGEREF_ACTIVATE;
    |      //如果有引用pte但引用数不大于1,则继续保留在inactive list,如只读一次的文件页面
    |      return PAGEREF_KEEP;
    |  //页面没有引用pte,则可以尝试回收
    |--if (referenced_page && !PageSwapBacked(page))
    |      return PAGEREF_RECLAIM_CLEAN
    |  //>>>>>>没有PTE引用的页面可回收
    \--return PAGEREF_RECLAIM

page_check_references:决定页面是否可以被回收.

1.如果有访问引用PTE
(1)如果页面的引用计数大于0或访问引用PTE的个数大于1,则加入活跃链表
此处referenced_page可以过滤大量只读一次的文件页面迁移到active list? 由于对于page cache页面不会调用mark_page_accessed设置PG_referenced,因此第一次访问referenced_page为0,不会加入活跃链表,第二次访问通过page_check_references->SetPageReferenced会设置PG_referenced标志,referenced_page不为0,因此会将page加入活跃链表
(2)可执行文件的page cache 加入活跃链表
(3)最近第二次访问的page cache或shared page cache加入活跃链表
(4)除以上情况继续留在不活跃链表,如第一次访问的page cache
2.如果没有访问引用PTE
可以尝试回收页面

page_referenced(todo)

1.利用RMAP系统遍历所有映射该页面的PTE
2.对于每个pte,如果PTE_AF置位,说明之前被访问过,referenced加1,然后清空PTE_AF
3.返回referenced计数,表示该页有多少个访问引用PTE

4. __alloc_pages_slowpath

前面在说明伙伴系统alloc_pages函数时,只说明了get_page_from_freelist分配成功的情况,对于get_page_from_freelist分配失败,如所有zone水位低于min, 或者内存碎片化导致不能满足order的分配需求,此时就会走到内存分配的慢速路径,这会涉及到内存的回收,这里就来说明了慢速分配的历程

__alloc_pages_slowpath(gfp_mask, order, ac)
    |--alloc_flags = gfp_to_alloc_flags(gfp_mask)
    |--ac->preferred_zoneref=first_zones_zonelist(ac->zonelist,ac->highest_zoneidx,ac->nodemask)
    |  //>>>>>>>唤醒页面回收线程来实现回收
    |--if (alloc_flags & ALLOC_KSWAPD)
    |      wake_all_kswapds(order, gfp_mask, ac)
    |  //>>>>>>>调整分配标志后再次尝试分配
    |--page=get_page_from_freelist(gfp_mask, order, alloc_flags, ac)
    |--if (page) goto got_pg;
    |  //>>>>>>>对于order较大的大块内存,通过内存规整后分配内存
    |--page = __alloc_pages_direct_compact(gfp_mask, order,alloc_flags, ac...)
    |--if (page) goto got_pg;
    |--retry:-----------------------------------------------------------------------再次尝试
    |  //>>>>>>>防止回收线程睡眠,再次唤醒
    |--if (alloc_flags & ALLOC_KSWAPD)
    |      wake_all_kswapds(order, gfp_mask, ac); 
    |--reserve_flags = __gfp_pfmemalloc_flags(gfp_mask);
    |--根据reserve_flags调整alloc_flags和ac->preferred_zoneref
    |  //>>>>>>>调整分配标志和preferred_zoneref后再次尝试分配
    |--page = get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
    |--if (page) goto got_pg;
    |  //>>>>>>>尝试直接回收后分配
    |--page = __alloc_pages_direct_reclaim(gfp_mask, order, alloc_flags, ac, &did_some_progress)
    |--if (page) goto got_pg;
    |  //>>>>>>>尝试直接规整后分配
    |--page=__alloc_pages_direct_compact(gfp_mask,order,alloc_flags,ac,compact_priority,...)
    |--if (page) goto got_pg;
    |--关于nopage和需要retry情况判断
    |  //>>>>>>>回收失败,开始通过杀死某些进程来回收
    |--page = __alloc_pages_may_oom(gfp_mask, order, ac, &did_some_progress)
    |--if (page) goto got_pg;
    |--nopage:----------------------------------------------------------------------没有页面
    |  //>>>>>>>ALLOC_HARDER将采用更大的力度进行分配
    |--page = __alloc_pages_cpuset_fallback(gfp_mask, order, ALLOC_HARDER, ac)
    |--if (page) goto got_pg;
    |--gotpg:-----------------------------------------------------------------------分配成功
    |--return page

参考文档

奔跑吧,LInux内核

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值