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_activePG_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_activePG_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)的更多相关文章

  1. linux 内核 内存管理 slub算法 (一) 原理

    http://blog.csdn.net/lukuen/article/details/6935068

  2. Linux内核内存管理算法Buddy和Slab: /proc/meminfo、/proc/buddyinfo、/proc/slabinfo

    slabtop cat /proc/slabinfo # name <active_objs> <num_objs> <objsize> <objpersla ...

  3. Linux的内存回收和交换

    Linux的内存回收和交换 版权声明: 本文章内容在非商业使用前提下可无需授权任意转载.发布. 转载.发布请务必注明作者和其微博.微信公众号地址,以便读者询问问题和甄误反馈,共同进步. 微博ID:or ...

  4. linux kernel内存回收机制

    转:http://www.wowotech.net/linux_kenrel/233.html linux kernel内存回收机制 作者:itrocker 发布于:2015-11-12 20:37 ...

  5. linux内核内存分配(三、虚拟内存管理)

    在分析虚拟内存管理前要先看下linux内核内存的具体分配我開始就是困在这个地方.对内核内存的分类不是非常清晰.我摘录当中的一段: 内核内存地址 ============================ ...

  6. LINUX内核内存屏障

    =================                          LINUX内核内存屏障                          ================= By ...

  7. Linux内核——内存管理

    内存管理 页 内核把物理页作为内存管理的基本单位.内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址)通常以页为单位进行处理.MMU以页大小为单位来管理系统中的页表. 从虚拟内存的角度看,页就是 ...

  8. linux内核--内存管理(二)

    一.进程与内存     所有进程(执行的程序)都必须占用一定数量的内存,它或是用来存放从磁盘载入的程序代码,或是存放取自用户输入的数据等等.不过进程对这些内存的管理方式因内存用途不一而不尽相同,有些内 ...

  9. linux内核 内存管理

    以下内容汇总自网络. 在早期的计算机中,程序是直接运行在物理内存上的.换句话说,就是程序在运行的过程中访问的都是物理地址. 如果这个系统只运行一个程序,那么只要这个程序所需的内存不要超过该机器的物理内 ...

随机推荐

  1. 满足qps 同时兼顾 数据生产速率

    满足qps 同时兼顾  数据生产速率

  2. 怎么样关掉红米note开发者选项

    进 系统设置\应用 ,找到“设置”点进去,清一下数据,再打开“设置”查看,就没有“开发者选项”了

  3. POJ1673 ZOJ1776 三角形四心模板

    POJ1673 题中所述点即为三角形的垂心,用向量法可以轻松证明. 垂心 重心 外心 均位于三角形的欧拉线上,且三者有线性关系,于是,求出重心和外心即可求得垂心. 重心就是三点的平均值,外心可以通过解 ...

  4. 你想知道的关于JavaScript作用域的一切

    原文: https://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/ JavaScript中有许多章节是关于s ...

  5. Eclipse导入Java 的jar包的方法

    打开eclipse1.右击要导入jar包的项目,点properties 2.左边选择java build path,右边选择libraries 3.选择add External jars 4.选择ja ...

  6. [Qt Creator 快速入门] 第0篇 开始学习Qt 与Qt Creator

    Qt官方信息 Qt官网:http://qt.digia.com/ Qt开源官网:http://qt-project.org/ Qt最新版本下载:http://qt-project.org/downlo ...

  7. F - System Overload(约瑟夫环变形)

    Description Recently you must have experienced that when too many people use the BBS simultaneously, ...

  8. 记录从数据库把数据初始化mongodb缓存的一些坑

    在项目启动时,需要做一些项目启动后的预操作,比如初始化数据进缓存等等. 这时就需要写listener,等监听.在项目启动时把数据缓存进mongodb. 但是这会有一个问题.项目一般都是把各种bean交 ...

  9. 301 Remove Invalid Parentheses 删除无效的括号

    删除最小数目的无效括号,使输入的字符串有效,返回所有可能的结果.注意: 输入可能包含了除 ( 和 ) 以外的元素.示例 :"()())()" -> ["()()() ...

  10. 257 Binary Tree Paths 二叉树的所有路径

    给定一个二叉树,返回从根节点到叶节点的所有路径.例如,给定以下二叉树:   1 /   \2     3 \  5所有根到叶路径是:["1->2->5", " ...