Linux内核-内存回收逻辑和算法(LRU)
Linux内核内存回收逻辑和算法(LRU)
LRU 链表
在 Linux 中,操作系统对 LRU 的实现主要是基于一对双向链表:active 链表和 inactive 链表,这两个链表是 Linux 操作系统进行页面回收所依赖的关键数据结构,每个内存区域都存在一对这样的链表。顾名思义,那些经常被访问的处于活跃状态的页面会被放在 active 链表上,而那些虽然可能关联到一个或者多个进程,但是并不经常使用的页面则会被放到 inactive 链表上。页面会在这两个双向链表中移动,操作系统会根据页面的活跃程度来判断应该把页面放到哪个链表上。页面可能会从 active 链表上被转移到 inactive 链表上,也可能从 inactive 链表上被转移到 active 链表上,但是,这种转移并不是每次页面访问都会发生,页面的这种转移发生的间隔有可能比较长。那些最近最少使用的页面会被逐个放到 inactive 链表的尾部。进行页面回收的时候,Linux 操作系统会从 inactive 链表的尾部开始进行回收。
用于描述内存区域的 struct zone() 中关于这两个链表以及相关的关键字段的定义如下所示:
struct zone {
……
spinlock_t lru_lock;
struct list_head active_list;
struct list_head inactive_list;
unsigned long nr_active;
unsigned long nr_inactive;
……
}
各字段含义如下所示:
lru_lock:active_list 和 inactive_list 使用的自旋锁。
active_list:管理内存区域中处于活跃状态的页面。
inactive_list:管理内存区域中处于不活跃状态的页面。
nr_active:active_list 链表上的页面数目。
nr_inactive:inactive_list 链表上的页面数目。
如何在两个LRU 链表之间移动页面
Linux 引入了两个页面标志符 PG_active 和 PG_referenced 用于标识页面的活跃程度,从而决定如何在两个链表之间移动页面。
PG_active 用于表示页面当前是否是活跃的,如果该位被置位,则表示该页面是活跃的。PG_referenced 用于表示页面最近是否被访问过,每次页面被访问,该位都会被置位。
Linux 必须同时使用这两个标志符来判断页面的活跃程度,假如只是用一个标志符,在页面被访问时,置位该标志符,之后该页面一直处于活跃状态,如果操作系统不清除该标志位,那么即使之后很长一段时间内该页面都没有或很少被访问过,该页面也还是处于活跃状态。为了能够有效清除该标志位,需要有定时器的支持以便于在超时时间之后该标志位可以自动被清除。然而,很多 Linux 支持的体系结构并不能提供这样的硬件支持,所以 Linux 中使用两个标志符来判断页面的活跃程度。
Linux 2.6 中这两个标志符密切合作,其核心思想如下所示:
如果页面被认为是活跃的,则将该页的 PG_active 置位;否则,不置位。当页面被访问时,检查该页的 PG_referenced 位,若未被置位,则置位之;若发现该页的 PG_referenced 已经被置位了,则意味着该页经常被访问,这时,若该页在 inactive 链表上,则置位其 PG_active 位,将其移动到 active 链表上去,并清除其 PG_referenced位的设置;如果页面的 PG_referenced 位被置位了一段时间后,该页面没有被再次访问,那么 Linux 操作系统会清除该页面的 PG_referenced 位,因为这意味着这个页面最近这段时间都没有被访问。
PG_referenced 位同样也可以用于页面从 active 链表移动到 inactive 链表。对于某个在 active 链表上的页面来说,其 PG_active 位被置位,如果 PG_referenced 位未被置位,给定一段时间之后,该页面如果还是没有被访问,那么该页面会被清除其 PG_active 位,挪到 inactive 链表上去。
Linux 中实现在 LRU 链表之间移动页面的关键函数如下所示(本文涉及的源代码均是基于 Linux 2.6.18.1 版本的):
mark_page_accessed():当一个页面被访问时,则调用该函数相应地修改PG_active和PG_referenced。page_referenced():当操作系统进行页面回收时,每扫描到一个页面,就会调用该函数设置页面的PG_referenced位。如果一个页面的PG_referenced位被置位,但是在一定时间内该页面没有被再次访问,那么该页面的PG_referenced位会被清除。activate_page():该函数将页面放到 active 链表上去。shrink_active_list():该函数将页面移动到 inactive 链表上去。
LRU 缓存
前边提到,页面根据其活跃程度会在 active 链表和 inactive 链表之间来回移动,如果要将某个页面插入到这两个链表中去,必须要通过自旋锁以保证对链表的并发访问操作不会出错。为了降低锁的竞争,Linux 提供了一种特殊的缓存:LRU 缓存,用以批量地向 LRU 链表中快速地添加页面。有了 LRU 缓存之后,新页不会被马上添加到相应的链表上去,而是先被放到一个缓冲区中去,当该缓冲区缓存了足够多的页面之后,缓冲区中的页面才会被一次性地全部添加到相应的 LRU 链表中去。Linux 采用这种方法降低了锁的竞争,极大地提升了系统的性能。
LRU 缓存用到了 pagevec 结构,如下所示 :
struct pagevec {
unsigned long nr;
unsigned long cold;
struct page *pages[PAGEVEC_SIZE];
};
pagevec 这个结构就是用来管理 LRU 缓存中的这些页面的。该结构定义了一个数组,这个数组中的项是指向 page 结构的指针。一个 pagevec 结构最多可以存在 14 个这样的项(PAGEVEC_SIZE 的默认值是 14)。当一个 pagevec 的结构满了,那么该 pagevec 中的所有页面会一次性地被移动到相应的 LRU 链表上去。
用来实现 LRU 缓存的两个关键函数是 lru_cache_add() 和 lru_cache_add_active()。前者用于延迟将页面添加到 inactive 链表上去,后者用于延迟将页面添加到 active 链表上去。这两个函数都会将要移动的页面先放到页向量 pagevec 中,当 pagevec 满了(已经装了 14 个页面的描述符指针),pagevec 结构中的所有页面才会被一次性地移动到相应的链表上去。
下图概括总结了上文介绍的如何在两个链表之间移动页面,以及 LRU 缓存在其中起到的作用:
页面在 LRU 链表之间移动示意图

其中,
1 表示函数 mark_page_accessed()
2 表示函数 page_referenced()
3 表示函数 activate_page()
4 表示函数 shrink_active_list()。
PFRA具体实现
PFRA必须处理多种属于用户态进程、磁盘高速缓存和内存高速缓存的页,而且必须遵照几条试探法准则。PFRA的大部分函数如下:

如上图在分配VFS缓冲区或缓冲区首部时,内核调用free_more_memory();而当从伙伴系统分配一个或多个页框时,调用try_to_free_pages()。
页面回收关键代码流程图

上文提到 Linux 中页面回收主要是通过两种方式触发的,一种是由“内存严重不足”事件触发的;一种是由后台进程 kswapd 触发的,该进程周期性地运行,一旦检测到内存不足,就会触发页面回收操作。对于第一种情况,系统会调用函数 try_to_free_pages() 去检查当前内存区域中的页面,回收那些最不常用的页面。对于第二种情况,函数 balance_pgdat() 是入口函数。
当 NUMA 上的某个节点的低内存区域调用函数 try_to_free_pages() 的时候,该函数会反复调用 shrink_zones() 以及 shrink_slab() 释放一定数目的页面,默认值是 32 个页面。如果在特定的循环次数内没有能够成功释放 32 个页面,那么页面回收会调用 OOM killer 选择并杀死一个进程,然后释放它占用的所有页面。函数 shrink_zones() 会对内存区域列表中的所有区域分别调用 shrink_zone() 函数,后者是从内存回收最近最少使用页面的入口函数。
对于定期页面检查并进行回收的入口函数 balance_pgdat() 来说,它主要调用的函数是 shrink_zone() 和 shrink_slab()。从上图中我们也可以看出,进行页面回收的两条代码路径最终汇合到函数 shrink_zone()和函数 shrink_slab() 上。
函数 shrink_zone()
其中,shrink_zone() 函数是 Linux 操作系统实现页面回收的最核心的函数之一,它实现了对一个内存区域的页面进行回收的功能,该函数主要做了两件事情:
将某些页面从 active 链表移到 inactive 链表,这是由函数 shrink_active_list() 实现的。
从 inactive 链表中选定一定数目的页面,将其放到一个临时链表中,这由函数 shrink_inactive_list() 完成。该函数最终会调用 shrink_page_list() 去回收这些页面。
函数 shrink_page_list() 返回的是回收成功的页面数目。概括来说,对于可进行回收的页面,该函数主要做了这样几件事情,其代码流程图如下所示:
函数 shrink_page_list() 实现的关键功能

函数 shrink_slab()
函数 shrink_slab() 是用来回收磁盘缓存所占用的页面的。Linux 操作系统并不清楚这类页面是如何使用的,所以如果希望操作系统回收磁盘缓存所占用的页面,那么必须要向操作系统内核注册 shrinker 函数,shrinker 函数会在内存较少的时候主动释放一些该磁盘缓存占用的空间。函数 shrink_slab() 会遍历 shrinker 链表,从而对所有注册了 shrinker 函数的磁盘缓存进行处理。
从实现上来看,shrinker 函数和 slab 分配器并没有固定的联系,只是当前主要是 slab 缓存使用 shrinker 函数最多。
注册 shrinker 是通过函数 set_shrinker() 实现的,解除 shrinker 注册是通过函数 remove_shrinker() 实现的。当前,Linux 操作系统中主要的 shrinker 函数有如下几种:
shrink_dcache_memory():该 shrinker 函数负责 dentry 缓存。
shrink_icache_memory():该 shrinker 函数负责 inode 缓存。
mb_cache_shrink_fn():该 shrinker 函数负责用于文件系统元数据的缓存。
具体的源代码实现细节有时间再做分析。后面将谈论交换。
Linux内核-内存回收逻辑和算法(LRU)的更多相关文章
- linux 内核 内存管理 slub算法 (一) 原理
http://blog.csdn.net/lukuen/article/details/6935068
- Linux内核内存管理算法Buddy和Slab: /proc/meminfo、/proc/buddyinfo、/proc/slabinfo
slabtop cat /proc/slabinfo # name <active_objs> <num_objs> <objsize> <objpersla ...
- Linux的内存回收和交换
Linux的内存回收和交换 版权声明: 本文章内容在非商业使用前提下可无需授权任意转载.发布. 转载.发布请务必注明作者和其微博.微信公众号地址,以便读者询问问题和甄误反馈,共同进步. 微博ID:or ...
- linux kernel内存回收机制
转:http://www.wowotech.net/linux_kenrel/233.html linux kernel内存回收机制 作者:itrocker 发布于:2015-11-12 20:37 ...
- linux内核内存分配(三、虚拟内存管理)
在分析虚拟内存管理前要先看下linux内核内存的具体分配我開始就是困在这个地方.对内核内存的分类不是非常清晰.我摘录当中的一段: 内核内存地址 ============================ ...
- LINUX内核内存屏障
================= LINUX内核内存屏障 ================= By ...
- Linux内核——内存管理
内存管理 页 内核把物理页作为内存管理的基本单位.内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址)通常以页为单位进行处理.MMU以页大小为单位来管理系统中的页表. 从虚拟内存的角度看,页就是 ...
- linux内核--内存管理(二)
一.进程与内存 所有进程(执行的程序)都必须占用一定数量的内存,它或是用来存放从磁盘载入的程序代码,或是存放取自用户输入的数据等等.不过进程对这些内存的管理方式因内存用途不一而不尽相同,有些内 ...
- linux内核 内存管理
以下内容汇总自网络. 在早期的计算机中,程序是直接运行在物理内存上的.换句话说,就是程序在运行的过程中访问的都是物理地址. 如果这个系统只运行一个程序,那么只要这个程序所需的内存不要超过该机器的物理内 ...
随机推荐
- mciSendString详解(转)
做个mp3播放器,用realplay和WMP做出来的程序内存占用太大.如果你仅仅是播放MP3,建议使用API函数mciSendString,我把该函数的详细资料罗列如下供你参考.Option Expl ...
- python3 使用http.server模块 搭建一个简易的http服务器
from http.server import HTTPServer, BaseHTTPRequestHandler import json data = {'result': 'this is a ...
- libjpeg交叉编译
下载libjpeg http://libjpeg.sourceforge.net/ 解压tar -zxf jpegsrc.v6b.tar.gz cd jpeg-6b cp /usr/bin/libto ...
- [NOI2004]小H的小屋 贪心
神仙贪心,洛谷没有插图导致我题一开始都没看懂.容易发现,块越多越优秀,然后为了满足题意,所以假如不能整除,就分为两个部分(能整除就直接均分就行了).前一部分是n/m,后一部分是n/m+1.数量也是固定 ...
- POJ2208 Pyramids 四面体体积
POJ2208给定四面体六条棱(有序)的长度 求体积 显然用高中立体几何的方法就可以解决. 给出代码 #include<iostream> #include<cstdio> # ...
- RDA 多屏参流程
一.RDA MAKEFILE的本地变量 在介绍多屏参之前,先看一下./code/env.conf的包含过程,通过./code/Makefile.project加载,env.conf中所有的变量,都变为 ...
- sql复杂查询语句总结
转自:http://blog.csdn.net/fengfeng91/article/details/15029173 create table student( sno varchar2(10) p ...
- Tomcat6和7版本对web.xml中taglib标签的配置差异
原来部署在Tomcat6中的应用在Tomcat7中运行时报错如下错误: java.lang.IllegalArgumentException: taglib definition not consis ...
- 全面学习ORACLE Scheduler特性(7)Scheduler抛出的Events
四.使用Events Event直译对应的中文解释是指事件,不过单纯讲事件毕竟太抽象了,举个示例来形容吧.A(对应某个应用程序,或者是ORACLE中的进程)在干活时突然眉头一皱说道,不好,前方有情况, ...
- The Chosen One
https://www.hackerrank.com/contests/101hack45/challenges/the-chosen-one 找出一个数字,使得,数组中只有一个数字不是这个数的约数, ...