一文搞懂Linux内核页框回收(Page Frame Reclamation)

页替换策略(Page Replacement Policy)

每当讨论页替换策略,提及最多的就是基于LRU(Least Recently Used)的算法,但严格来说这是不对的因为这些lists并不是严格按照LRU的顺序来维护的。在Linux中LRU有两个list组成,分别是active_list和inactive_list。avtive_list的目标是包含所有进程工作使用的page而inactive_list的目标是包含回收候选者的page。因为所有可回收的page都包含在这两个list中,因此任何进程的page都有可能被回收,而不是仅仅回收属于出错进程的page,因此页回收策略是全局的。

这两个队列就像是一个简单的LRU 2Q,这两个被维护的队列分别叫Am和A1。在使用LRU 2Q时,第一次被分配的page将被放入到一个名叫A1的FIFO队列。如果某个page在A1中并且被人引用,然后改page就会被放入到一个正规的LRU管理的队列Am中。这就是像使用lru_cache_add()将page放置到inactive_list(A1)中,然后使用mark_page_accessed()将page移动到active_list(Am)中。LRU 2Q中的算法描述了这两个list的size如何调节但是Linux使用了一种简单的方法:通过使用refill_inactive()函数将page从active_list的地步移动到inactive_list中来保证active_list的size是整个page cache的2/3. 下图说明了这两个list如何结构化,page如何在这两个list中移动:

一文搞懂Linux内核页框回收(Page Frame Reclamation)_第1张图片

2Q算法中描述两个list中预设Am是一个LRU list但是在Linux中更像是一个时钟算法,其中周期就是active list的size。当一个page到达active list的底部的时候,就会来检查reference flag会被检查,如果被置位,该page将会active list的顶部然后接着检查下一个page。如果reference bit被清除,则需要将该page移动到inactive_list

资料直通车:最新Linux内核源码资料文档+视频资料

内核学习地址:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

尽管Move-To-Front的启示意味着这两个列表像是在一LRU的方式在运行但是Linux替换策略和LRU还是有很多的不同,不能将其认为是一个stack算法。尽管我们可以忽略分析多程序系统的问题和每个进程使用的memory size是不同的这个事实,但是替换策略还是不能满足inclusion property是因为每个page在list中的位置取决于list的大小和上次引用的时间。这些list并没有按优先级排序因为这会使得每次对page的引用都要更新list。当被换出进程地址空间时,这两个列表几乎被忽略,因为与被换出决策相关的是page在进程虚拟地址空间中的位置而不是在page list中的位置。(这句话不太好理解)

总结下,Linux中替换算法并不是和LRU的行为一样并且被一个benchmark在实际测试中表现不错。当前仅仅有两种case在该算法下表现得比较糟糕。

first:当要被回收的候选page是匿名page时

在这种case中,Linux在线性扫描进程页表来搜索要回收的page之前,将持续检查大量的page,但是这种场景相当少。

second:单个进程中有许多文件映射的page在inactive_list中,并且经常被写入。

该进程和kswapd进程可能进入一种不断地转换这些page的循环中并把它们放入到inactive_list中但不释放任何东西。在这种case下,只有很少的page能够从active_list移动到inactive_list因为这两个列表大小比例始终没有很大变化。

Page Cache

page cache是一组数据结构其包含一些pages如有文件映射或者块设备映射或者swap的page。当前有四种最基本类型的page存在于page cache中:

  • 通过读取内存映射的文件而产生page fault的page
  • 被称作buffer page的page:从文件系统或者块设备中读取的数据块到特定的page
  • 存在于swap cache中的匿名page
  • 属于共享内存空间的page,其和匿名page的处理方式差不多。它们唯一的不同点是共享page被加入到swap cache后当它第一次被写入,它在其存储介质的空间上会立刻保留。

存在page cache的理由是减少不必要的磁盘访问。从磁盘中读取的page被存储在page hash table中,属于struct address_space.在每次访问磁盘前,都会在page cache中搜索其在磁盘中的偏移。下面的API是用来操作page cache的:

  • void add_to_page_cache(struct page * page, struct address_space * mapping, unsigned long offset)

通过调用lru_cache_add()将page加入到LRU中,然后再将page计入到inode queue和page hash table中

  • void add_to_page_cache_unique(struct page * page, struct address_space *mapping, unsigned long offset, struct page **hash)

该函数和上一个函数很相似,除了会检查该page是否已经在page cache中存在,该函数要求其调用者不能是由pagecache_lock自旋锁

  • int page_cache_read(struct file * file, unsigned long offset)

如果offset对应的page不在page cache中时,会增加一个page,必要时会通过address_space_operations->readpage从磁盘读取数据

Page Cache Hash Table

有一个需求:page cache中的page需要快速被找到。为了实现此需求,page被插入到一个page_hash_table。page->next_hash和page->pprev_hash被用来决绝冲突。(貌似kernel2.6中已经没有page_hash_table)

在kernel2.6中使用索引树来进行page存储用于快速查找。

Adding Pages to the Page Cache

从文件或者块设别中读出来的page通常会被添加到page cache从而避免更多的磁盘IO。大多数文件系统使用generic_file_read()

当作它们的file_operations->read()函数。一般来讲文件系统是通过page来执行它们的IO操作。下面就来说明下generic_file_read()是如何操作的以及它是如何将page添加到page cache。

对于普通的IO来说,generic_file_read()在调用do_generic_read()之中会先做一些基本的检查。通过find_get_page()来查询该page是否已经在page cache中存在,如果不存在,调用page_cache_alloc_cold()从cpu code list中分配page。然后再调用add_to_page_cache_lru()将page加入到page cache同时将page加入到lru list中。如果page一旦在page cache中存在,就会调用page_cache_readahead()从磁盘中读取数据。

一文搞懂Linux内核页框回收(Page Frame Reclamation)_第2张图片

没用从进程空间映射的匿名page将被添加到swap cache中,这将在后续的章节来讨论。匿名page在尝试将它们换出之前,它们没有address_space作为一个映射或者是一个文件偏移将它们加入到page cache中。所以这些page仍然留在LRU list中。一旦进入page cache,匿名page和file backed page的真正不同点是匿名page将swapper_space作为address_space。

共享内存的pages在下列两种case下会被加入到page cache中。

第一种case是:当page第一次从swap中获取或者第一次被分配并且第一次被引用的时候加入到page cache。即shmem_getpage()中完成。

第二种case是当swap code调用shmem_unuse()。当一个swap area正在被deactive,并且发现一个page并且在swapper_address中并且没有一个进程在使用。

Reclaiming Pages from the LRU Lists

函数shrink_cache()是替换算法的一部分,它从inactive_list获取page并决定如何将它们换出。该函数是一个大循环,从inactive_list底部最多扫描max_scan个page来释放nr_pages个page,直到inactive_list为空。

每种不同类型的page,释放的时候具体的做法不同。具体的处理顺序如下:

page被lock了且PG_launder被置位:该page在IO中被lock,因此需要跳过此page。但是如果PG_launder被置位,这就意味着该page是第二次被发现上锁了,因此最好等待IO完成然后不管它。如果一个page通过page_cache_get()被引用因此该page不会被过早地释放并调用wait_on_page()进入睡眠知道IO完成。一旦IO结束调用 page_cache_release()来减少它的引用计数。当引用计数为0,则该page可以被回收了。

page是网页且没有被任何进程映射且没有buffer且属于文件映射或者设备:因为该page属于一个文件映射或者设备映射,因此它拥有合法的writepage()函数可用:page->mapping->a_ops->writepage.PG_dirty被清空并且PG_launder被置位说明它准备IO。在调用writepage()前需要调用page_cache_get()来增加它的引用计数来。值得注意的是这种case同样适用于处于swap cache中的匿名page。该page仍然在LRU中,当它再次被发现后,如果IO完成那么就将它简单滴释放,并且page被回收。如果IO没有完成,kernel会等待IO完成。

page拥有buff:将会调用try_to_release_page()来引用它,并尝试将它释放。如果成功了,那么它是一个匿名page,它将从LRU中移除并减少它的引用计数。匿名page存在buff只有一种情况:它指向一个swap file因为该page需要写出block-sized chunk。换句话说如果该page直接有一个file映射,那么就将其简单地减少它的引用计数,如果引用计数为0,则可以直接释放了。

匿名page且被映射到多个进程:调用swap_out()

没有进程在引用的page:如果page在swap cache中,那么将从swap cache删除。如果是一个file的一部分,那么它将从page cache中删除和释放。

Shrinking all caches

shrink_caches()的作用是释放多个cache。在Linux2.6中一次遍历各个zone,然后调用shrink_zone()来回收页框。

如果有多个释放任务,这就需要给每个释放任务设定一个优先级,priority默认是DEF_PRIORITY,如果每次释放的page数达不到预期,那么就将priority从最高优先级递减1。

你可能感兴趣的:(Linux内核,linux,运维,Linux内核,嵌入式开发,页框回收)