概述
目录
- 1. 前言
- 2. 两种页面置换算法
- 2.1 LRU经典链表算法
- 2.2 第二次机会法
- 3. 页面置换算法基础API
- lru_cache_add
- lru_to_page
- mark_page_accessed(todo)
- page_check_references
- page_referenced(todo)
- 4. __alloc_pages_slowpath
- 参考文档
1. 前言
本专题我们开始学习内存管理部分,本文为页面回收处理相关学习笔记。本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。
Linux页面回收主要考虑对文件映射页面和匿名映射页面的回收,由于文件映射页面大多不需要做任何处理,不像匿名页面需要写入swap分区,因此优先回收文件映射页面。
kernel版本:5.10
平台:arm64
2. 两种页面置换算法
2.1 LRU经典链表算法
在经典LRU链表中,新分配的页面加入到LRU链表的开头,将LRU链表中现存的页面向后移动一个位置。当系统内存短缺时,LRU链表尾部的页面将会离开并被换出。当系统再次需要这些页面,这些页面会重新置于LRU链表的开头。它没考虑到该页面的使用情况是否频繁,频繁使用的页面会因为在LRU链表末尾而被换出。
- LRU链表是最近最少使用的缩写,假定最近不使用的页面,将来一段时间也不会使用,内存不足时,这些页面将成为被换出的候选者
- 内核使用双向链表定义LRU链表,根据页面类型分为LRU_ANON和LRU_FILE两种链表
- 每种链表根据页面的活跃性分为活跃LRU和非活跃LRU,系统运行过程中页面总是在活跃和不活跃LRU链表之间迁移
- 内核一共有5个LRU链表:
不活跃匿名LRU链表、活跃匿名LRU链表、不活跃文件LRU链表、活跃文件LRU链表、不可回收页面链表 - 内存紧缺时,优先换出page cache,因为page cache很少需要写回磁盘,匿名页面需要写回磁盘才能换出
- LRU链表按照node来配置,每个node都有一整套LRU链表,node中数据成员lruvec指向这些链表
注:在Linux4.8改成基于node的LRU链表,之前是基于zone来配置 - LRU链表实现插入LRU链表时插入到链表头,摘除节点时从LRU尾部,实现FIFO
- 系统运行过程中,在活跃的LRU链表和不活跃的LRU链表之间迁移,最不常用页面慢慢移动到不活跃LRU链表的末尾
2.2 第二次机会法
- 第二次机会法是对LRU的改善,当选择置换页面时,和LRU一样选择最早置入链表的页面,即链表末尾的页面
- 设置了一个访问状态位(硬件控制比特位),检查页面的访问位
- 如果访问位为0,就淘汰页面,如果访问位为1,就给第二次机会,访问位清0,选择下一个页面换出;
- 如果该页再次被访问则访问位置1,清零后如果没有再访问,需要的时候就可以换出页面了
- 使用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内核
最后
以上就是谨慎老师为你收集整理的内存管理基础学习笔记 - 5.1 页面回收 - 概述1. 前言2. 两种页面置换算法3. 页面置换算法基础API4. __alloc_pages_slowpath参考文档的全部内容,希望文章能够帮你解决内存管理基础学习笔记 - 5.1 页面回收 - 概述1. 前言2. 两种页面置换算法3. 页面置换算法基础API4. __alloc_pages_slowpath参考文档所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复