linux内存反向映射,Linux页面回收与反向映射机制总结篇——深圳培训linux

本文,将会概括总结今天文章介绍Linux页面回收与反向映射机制的如何在两个链表之间移动页面,以及

LRU 缓存在其中起到的作用,还会做一个最后的总结。

1. 页面在 LRU 链表之间移动示意图

其中,1 表示函数 mark_page_accessed(),2

表示函数 page_referenced(),3 表示函数 activate_page(),4 表示函数

shrink_active_list()。

页面回收的实现

Linux 操作系统进行页面回收需要考虑的方面很多,下图列出了 Linux

操作系统进行页面回收的关键代码流程图,该图给出了实现页面回收的关键代码函数名,并说明它们之间是如何彼此链接的。

2. 页面回收关键代码流程图

上文提到 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()

返回的是回收成功的页面数目。概括来说,对于可进行回收的页面,该函数主要做了这样几件事情,其代码流程图如下所示

3. 函数 shrink_page_list()

实现的关键功能

●对于匿名页面来说,在回收此类页面时,需要将其数据写入到交换区。如果尚未为该页面分配交换区槽位,则先分配一个槽位,并将该页面添加到交换缓存。同时,将相关的

page

实例加入到交换区,这样,对该页面的处理就可以跟其他已经建立映射的页面一样;

●如果该页面已经被映射到一个或者多个进程的页表项中,那么必须找到所有引用该页面的进程,并更新页表中与这些进程相关的所有页表项。在这里,Linux

2.6 操作系统会利用反向映射机制去检查哪些页表项引用了该页面,关于反向映射的内容在后边会有介绍;

●如果该页面中的数据是脏的,那么数据必须要被回写;

●释放页缓存中的干净页面。

函数 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 函数负责用于文件系统元数据的缓存。

反向映射(reverse mapping)

前文介绍过,在回收一个物理页面之前,需要查找到所有关联了该物理页面的页表项,并逐一更新这些页表项。Linux 2.6

使用了反向映射这种机制用于快速定位那些引用了某个物理页面的所有页表项。Linux

操作系统为物理页面建立一个链表,用于指向引用了该物理页面的所有页表项。其基本思想如下图所述:

4. 反向映射的基本思想

反向映射技术的发展历史

在 Linux 2.4

中,为了确定某个要回收的物理页面都被哪些页表项引用,必须要遍历所有进程,这是一项非常耗资源和时间的工程。为了更加有效地回收一个共享页面,Linux

在 2.5

版本的开发期间引入了反向映射这样一种机制。这种机制建立了物理页面和所有映射了该物理页面的页表项之间的一种关联,从而让操作系统可以快速定位引用了该物理页面的所有页表项。在

Linux 2.6 版本中,反向映射算法又经历了大量改进。

在 Linux 2.5

版本中,反向映射技术的实现主要是基于页表项链表。操作系统为每一个物理页面都维护了一个链表,所有与该物理页面关联的页表项都会被放到这个链表上。这种方法会存在一些问题:

●空间资源的消耗:为每个物理页面维护这样一个链表,需要占用大量的内存空间。

●时间资源的消耗:回收一个物理页面的时候,需要先获取该链表上的锁,然后遍历相应的反向映射链表,链表上的项越多,需要的时间就越多。

后来,Linux 2.6

引入了基于对象的反向映射机制。这种方法也是为物理页面设置一个用于反向映射的链表,但是链表上的节点并不是引用了该物理页面的所有页表项,而是相应的虚拟内存区域(

vm_area_struct 结构),虚拟内存区域通过内存描述符( mm_struct

结构)找到页全局目录,从而找到相应的页表项。相对于前一种方法来说,用于表示虚拟内存区域的描述符比用于表示页面的描述符要少得多,所以遍历后边这种反向映射链表所消耗的时间也会少很多。

基于对象的反向映射的实现

数据结构

page 结构中与基于对象的反向映射相关的关键字段有两个:_mapcount 和

mapping。

struct page {

atomic_t _mapcount;

union {

……

struct {

……

struct address_space *mapping;

};

……

};

●字段 _mapcount

表明共享该物理页面的页表项的数目。该计数器可用于快速检查该页面除所有者之外有多少个使用者在使用,初始值是 -1,每增加一个使用者,该计数器加 1。

字段 mapping

用于区分匿名页面和基于文件映射的页面,如果该字段的最低位被置位了,那么该字段包含的是指向 anon_vma 结构(用于匿名页面)的指针;否则,该字段包含指向 address_space

结构的指针(用于基于文件映射的页面)。

匿名页面和文件映射页面分别采用了不同的底层数据结构去存放与页面相关的虚拟内存区域。对于匿名页面来说,与该页面相关的虚拟内存区域存放在结构

anon_vma 中定义的双向链表中。结构 anon_vma 定义很简单,如下所示:

struct anon_vma {

spinlock_t lock;

struct list_head head;

};而对于基于文件映射的页面来说,与匿名页面不同的是,与该页面相关的虚拟内存区域的存放是利用了优先级搜索树这种数据结构的。这是因为对于匿名页面来说,页面虽然可以是共享的,但是一般情况下,共享匿名页面的使用者的数目不会很多;而对于基于文件映射的页面来说,共享页面的使用者的数目可能会非常多,使用优先级搜索树这种结构可以更加快速地定位那些引用了该页面的虚拟内存区域。操作系统会为每一个文件都建立一个优先级搜索树,其根节点可以通过结构

address_space 中的 i_mmap 字段获取。

struct address_space {

……

struct prio_tree_root i_mmap;

……

}Linux 2.6 中使用 (radix,size,heap)

来表示优先级搜索树中的节点。其中,radix

表示内存区域的起始位置,heap 表示内存区域的结束位置,size 与内存区域的大小成正比。在优先级搜索树中,父节点的 heap 值一定不会小于子节点的 heap

值。在树中进行查找时,根据节点的 radix 值进行。程序可以根据

size 值区分那些具有相同 radix 值的节点。

在用于表示虚拟内存区域的结构 vm_area_struct

中,与上边介绍的双向链表和优先级搜索树相关的字段如下所示:

struct vm_area_struct {

struct mm_struct * vm_mm;

……

union {

struct {

struct list_head list;

void *parent;

struct vm_area_struct *head;

} vm_set;

struct raw_prio_tree_node prio_tree_node;

} shared;

struct list_head anon_vma_node;

struct anon_vma *anon_vma;

};与匿名页面的双向链表相关的字段是

anon_vma_node 和 anon_vma。union shared

则与文件映射页面使用的优先级搜索树相关。字段 anon_vma 指向

anon_vma 表;字段

anon_vma_node

将映射该页面的所有虚拟内存区域链接起来;union shared 中的

prio_tree_node

结构用于表示优先级搜索树的一个节点;在某些情况下,比如不同的进程的内存区域可能映射到了同一个文件的相同部分,也就是说这些内存区域具有相同的

(radix,size,heap)值,这个时候 Linux 就会在树上相应的节点(树上原来那个具有相同 (radix,size,heap) 值的内存区域)上接一个双向链表用来存放这些内存区域,这个链表用 vm_set.list 来表示;树上那个节点指向的链表中的第一个节点是表头,用 vm_set.head 表示;vm_set.parent 用于表示是否是树结点。下边给出一个小图示简单说明一下

vm_set.list 和 vm_set.head。

5. vm_set.list 和 vm_set.head

通过结构 vm_area_struct 中的 vm_mm 字段可以找到对应的 mm_struct

结构,在该结构中找到页全局目录,从而定位所有相关的页表项。

使用反向映射

在进行页面回收的时候,Linux 2.6 在前边介绍的 shrink_page_list() 函数中调用 try_to_unmap()

函数去更新所有引用了回收页面的页表项。其代码流程如下所示:

6. 实现函数 try_to_unmap()

的关键代码流程图

函数 try_to_unmap() 分别调用了两个函数 try_to_unmap_anon() 和 try_to_unmap_file(),其目的都是检查并确定都有哪些页表项引用了同一个物理页面,但是,由于匿名页面和文件映射页面分别采用了不同的数据结构,所以二者采用了不同的方法。

函数 try_to_unmap_anon() 用于匿名页面,该函数扫描相应的

anon_vma 表中包含的所有内存区域,并对这些内存区域分别调用

try_to_unmap_one() 函数。

函数 try_to_unmap_file()

用于文件映射页面,该函数会在优先级搜索树中进行搜索,并为每一个搜索到的内存区域调用 try_to_unmap_one() 函数。

两条代码路径最终汇合到 try_to_unmap_one()

函数中,更新引用特定物理页面的所有页表项的操作都是在这个函数中实现的。该函数实现的关键功能如下图所示:

7. 函数 try_to_unmap_one()

实现的关键功能

对于给定的物理页面来说,该函数会根据计算出来的线性地址找到对应的页表项地址,并更新页表项。对于匿名页面来说,换出的位置必须要被保存下来,以便于该页面下次被访问的时候可以被换进来。并非所有的页面都是可以被回收的,比如被

mlock()

函数设置过的内存页,或者最近刚被访问过的页面,等等,都是不可以被回收的。一旦遇上这样的页面,该函数会直接跳出执行并返回错误代码。如果涉及到页缓存中的数据,需要设置页缓存中的数据无效,必要的时候还要置位页面标识符以进行数据回写。该函数还会更新相应的一些页面使用计数器,比如前边提到的

_mapcount

字段,还会相应地更新进程拥有的物理页面数目等。

使用反向映射的优缺点

使用反向映射机制所带来的好处是显而易见的:可以快速定为引用了某个物理页面的所有页表项,这极大地方便了操作系统进行页面回收。相对于之前的遍历方法来说,反向映射机制在很大程度上减少了操作系统在页面回收上所占用的

CPU 时间。

但是,使用反向映射所面临的挑战也是很明显的,不管采用上述介绍的哪种方法建立反向映射,都不可避免地要消耗掉一定的内存空间,区别就在于用哪种方法占用的空间会更少,整体性能会更好。

总结

页面回收是 Linux

内存管理中比较复杂的一个部分,涉及到的相关内容非常多,本文也不是面面俱到。反向映射是 Linux

2.5 开发过程中一个比较大的亮点,该技术在后续 Linux 2.6

版本中又得到了更进一步的发展。本文的目的是想帮助读者理清 Linux 2.6

中的页面回收和反向映射机制,本文通过相关的数据结构和关键的代码流程介绍了 Linux 操作系统如何利用反向映射机制有效地进行页面回收。关于 Linux

操作系统如何建立反向映射的内容,本文没有做详尽介绍,感兴趣的读者可以自行参考内核源代码。

收集编辑于互联网,仅供学习交流!转载请注明出处!

更多相关文章:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值