1 页框回收算法(PFRA)
linux内核的页框回收算法采取从用户态进程和内核高速缓存“窃取”页框的办法补充伙伴系统的空闲块列表。页框回收算法的目标之一就是保存最少的空闲页框池以便内核可以安全地从“内存紧缺”的情形中恢复过来。
1.1选择目标页
页框回收算法(PFRA)的目标就是获得页框并使之空闲。PFRA按照页框所含内容,以不同的方式处理页框。我们将他们区分成:不可回收页,可交换页,可同步页和可丢弃页,如下表所示:
页类型 | 说明 | 回收操作 |
不可回收页 | 空闲页(包含在伙伴系统列表中) 保留页(PG_reserved标志置位) 内核动态分配页 进程内核态堆栈页 临时锁定页(PG_locked标志置位) 内存锁定页(在现行区中且VM_LOCKED标志置位) | 不允许也无需回收 |
可交换页 | 用户态地址空间的匿名页 tmpfs文件系统的映射页(如IPC共享内存的页) | 将页的内容保存在交换区 |
可同步页 | 用户态地址空间的映射页 存有磁盘文件数据且在高速缓存中的页 块设备缓冲区页 某些磁盘高速缓存的页(如索引节点高速缓存) |
1.2 PFRA设计
PFRA采用的几个原则:
1) 首先释放“无害”页:在进程用户态地址空间的页回收之前,必须先回收没有被任何进程使用的磁盘与内存高速缓存中的页。
2) 将用户态进程的所有页定为可回收页:除了锁定页,FPRA必须能够窃得任何用户态进程页,包括匿名页。这样,睡眠较长时间的进程将逐渐失去所有页框。
3) 同时取消引用一个共享页框的所有页表项的映射,就可以回收该共享页框:当PFRA要释放几个进程共享的页框时,它就清空引用该页框的所有页表项,然后回收该页框。
4) 只回收“未用”页:使用简化的最近最少使用(LRU)置换算法,PFRA将页分为“在用”和“未用”。如果某页很长时间没有被访问,那么它将来被访问的可能性较小,就可以将它看作未用;另一方面,如果某页最近被访问过,那么它将来被访问的可能性较大,就必须将它看作在用。PFRA只回收未用页。
2 PFRA实现
页框回收算法的执行有三种基本情形:
1) 内存紧缺回收:内核发现内存紧缺
2) 睡眠回收
3) 周期回收:周期性激活内核线程执行内存回收算法。
2.1 最近最少使用(LRU)链表
2.1.1 LRU 链表
属于进程用户态地址空间或页高速缓存的所有页被分为两组:活动链表和非活动链表。它们统称LRU链表。活动链表用于存放最近被访问过的页,非活动链表存放有一段时间没有被访问过的页。显然,页必须从非活动链表中窃取。LRU链表根据页面类型又分为LRU_ANON和LRU_FILE。所以内核中一共有5个LRU链表,在<include/linux/mmzon.h>中定义如下:
#define LRU_BASE 0
#define LRU_ACTIVE 1
#define LRU_FILE 2
enum lru_list {
LRU_INACTIVE_ANON = LRU_BASE,
LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
LRU_UNEVICTABLE,
NR_LRU_LISTS
};
- LRU_INACTIVE_ANON:不活跃匿名页面链表
- LRU_ACTIVE_ANON:活跃匿名页面链表
- LRU_INACTIVE_FILE:不活跃文件映射页面链表
- LRU_ACTIVE_FILE:活跃文件映射页面链表
- LRU_UNEVICTABLE:不可回收页面链表
LRU链表之所以要这样分类,是因为当内存紧缺时总是优先换出page cache页面,而不是匿名页面。因为大多数情况下page cache页面不需要回写磁盘,除非页面内容被修改了,匿名页面总是要被写入交换分区才能被换出。LRU链表按照zone来配置,每个zone中都有一整套LRU链表,zone中的lruvec指向这些链表。 struct lruvec数据结构中定义了上述各种LRU类型的链表:
struct lruvec {
struct list_head lists[NR_LRU_LISTS];
struct zone_reclaim_stat reclaim_stat;
#ifdef CONFIG_MEMCG
struct zone *zone;
#endif
};
函数lru_cache_add将页面加入到LRU链表,代码如下:
/**
* lru_cache_add - add a page to a page list
* @page: the page to be added to the LRU.
*
* Queue the page for addition to the LRU via pagevec. The decision on whether
* to add the page to the [in]active [file|anon] list is deferred until the
* pagevec is drained. This gives a chance for the caller of lru_cache_add()
* have the page added to the active list using mark_page_accessed().
*/
void lru_cache_add(struct page *page)
{
VM_BUG_ON_PAGE(PageActive(page) && PageUnevictable(page), page);
VM_BUG_ON_PAGE(PageLRU(page), page);
__lru_cache_add(page);
}
static void __lru_cache_add(struct page *page)
{
struct pagevec *pvec = &get_cpu_var(lru_add_pvec);
page_cache_get(page);
if (!pagevec_space(pvec))
__pagevec_lru_add(pvec);
pagevec_add(pvec, page);
put_cpu_var(lru_add_pvec);
}
这里使用了页向量struct pagevec数据结构,用来保存特定数目的页,页向量会以“批处理的方式”执行,比单独处理一个页的效率更高。struct pagevec定义如下:
/* 14 pointers + two long's align the pagevec structure to a power of two */
#define PAGEVEC_SIZE 14
struct pagevec {
unsigned long nr;
unsigned long cold;
struct page *pages[PAGEVEC_SIZE];
};
struct pagevec包含14 个page,两个long变量,能够很好地匹配缓存行,易于计算。在将页面加入到lru时,会先将页面添加到struct pagevec页面向量中,如果该页面向量已经装满了需要添加到lru的页面,再将这些页面以“批处理的方式”添加到lru。
lru链表实现了先进先出(FIFO)算法,最先进入LRU链表的页面,在LRU中的时间会越长,老化时间也越长。在昔日运行过程中,页面总是在活跃LRU链表和不活跃LRU链表之间转移,随着时间推移,最不常用的页面将慢慢移动到不活跃LRU链表的末尾,这些页面正是页面回收中最合适的候选者。
2.1.2 第二次机会法
第二次机会法 在经典LRU算法上做了一些改进,是为了避免将经常使用的页面置换出去。第二次机会法设置了一个访问状态位,所以要检查页面的访问位。如果访问位是0,就淘汰,如果是1就给它第二次机会,并选择下一个页面来换出。当该页面得到第二次机会时他的访问位被清0,被移到非活动链表,如果在此期间,该页又被访问过,访问位将变成1,但是这一页仍然留在非活动链表中,第二次访问的时候发现这一页的访问位为1,就把该页移到活动链表,这样给了第二次机会的页将不会被淘汰。因此,如果一个页面经常被使用,其访问位总是保持为1,它就一直不会被淘汰出去。如果第一次访问之后在给定的时间间隔内没有被第二次访问,页框回收算法就会将访问位清0。
linux内核使用PG_active和PG_referenced这两个标志位来实现第二次机会法。PG_active表示该页是否活跃,PG_referenced表示该页是否被引用过。PFRA使用mark_page_accessed,page_referenced 和 page_check_references函数在LRU之间移动页。
1)mark_page_accessed
/*
* Mark a page as having seen activity.
*
* inactive,unreferenced -> inactive,referenced
* inactive,referenced -> active,unreferenced
* active,unreferenced -> active,referenced
*
* When a newly allocated page is not yet visible, so safe for non-atomic ops,
* __SetPageReferenced(page) may be substituted for mark_page_accessed(page).
*/
void mark_page_accessed(struct page *page)
{
if (!PageActive(page) && !PageUnevictable(page) &&
PageReferenced(page)) {
/*
* If the page is on the LRU, queue it for activation via
* activate_page_pvecs. Otherwise, assume the page is on a
* pagevec, mark it active and it'll be moved to the active
* LRU on the next drain.
*/
if (PageLRU(page))
activate_page(page);
else
__lru_cache_activate_page(page);
ClearPageReferenced(page);
if (page_is_file_cache(page))
workingset_activation(page);
} else if (!PageReferenced(page)) {
SetPageReferenced(page);
}
if (page_is_idle(page))
clear_page_idle(page);
}
mark_page_accessed函数中,如果page的PG_active, PG_unevictable标志未置位,且PG_reference标志置位,就将该页加入到活跃LRU链表中,清PG_reference标志,PG_reference标志未置位就设置页的PG_reference标志置位,给该页第二次机会。
2) page_referenced
/**
* page_referenced - test if the page was referenced
* @page: the page to test
* @is_locked: caller holds lock on the page
* @memcg: target memory cgroup
* @vm_flags: collect encountered vma->vm_flags who actually referenced the page
*
* Quick test_and_clear_referenced for all mappings to a page,
* returns the number of ptes which referenced the page.
*/
int page_referenced(struct page *page,
int is_locked,
struct mem_cgroup *memcg,
unsigned long *vm_flags)
{
int ret;
int we_locked = 0;
struct page_referenced_arg pra = {
.mapcount = page_mapcount(page),
.memcg = memcg,
};
struct rmap_walk_control rwc = {
.rmap_one = page_referenced_one,
.arg = (void *)&pra,
.anon_lock = page_lock_anon_vma_read,
};
*vm_flags = 0;
if (!page_mapped(page))
return 0;
if (!page_rmapping(page))
return 0;
if (!is_locked && (!PageAnon(page) || PageKsm(page))) {
we_locked = trylock_page(page);
if (!we_locked)
return 1;
}
/*
* If we are reclaiming on behalf of a cgroup, skip
* counting on behalf of references from different
* cgroups
*/
if (memcg) {
rwc.invalid_vma = invalid_page_referenced_vma;
}
ret = rmap_walk(page, &rwc);
*vm_flags = pra.vm_flags;
if (we_locked)
unlock_page(page);
return pra.referenced;
}
page_referenced函数判断page是否被访问过,并用反向映射法计算问引用pte的个数。rmap_walk函数通过调用page_referenced_one遍历所有映射该页的pte。page_referenced_one实现如下:
static int page_referenced_one(struct page *page, struct vm_area_struct *vma,
unsigned long address, void *arg)
{
struct mm_struct *mm = vma->vm_mm;
spinlock_t *ptl;
int referenced = 0;
struct page_referenced_arg *pra = arg;
if (unlikely(PageTransHuge(page))) {
pmd_t *pmd;
/*
* rmap might return false positives; we must filter
* these out using page_check_address_pmd().
*/
pmd = page_check_address_pmd(page, mm, address,
PAGE_CHECK_ADDRESS_PMD_FLAG, &ptl);
if (!pmd)
return SWAP_AGAIN;
if (vma->vm_flags & VM_LOCKED) {
spin_unlock(ptl);
pra->vm_flags |= VM_LOCKED;
return SWAP_FAIL; /* To break the loop */
}
/* go ahead even if the pmd is pmd_trans_splitting() */
if (pmdp_clear_flush_young_notify(vma, address, pmd))
referenced++;
spin_unlock(ptl);
} else {
pte_t *pte;
/*
* rmap might return false positives; we must filter
* these out using page_check_address().
*/
pte = page_check_address(page, mm, address, &ptl, 0);
if (!pte)
return SWAP_AGAIN;
if (vma->vm_flags & VM_LOCKED) {
pte_unmap_unlock(pte, ptl);
pra->vm_flags |= VM_LOCKED;
return SWAP_FAIL; /* To break the loop */
}
if (ptep_clear_flush_young_notify(vma, address, pte)) {
/*
* Don't treat a reference through a sequentially read
* mapping as such. If the page has been used in
* another mapping, we will catch it; if this other
* mapping is already gone, the unmap path will have
* set PG_referenced or activated the page.
*/
if (likely(!(vma->vm_flags & VM_SEQ_READ)))
referenced++;
}
pte_unmap_unlock(pte, ptl);
}
if (referenced)
clear_page_idle(page);
if (test_and_clear_page_young(page))
referenced++;
if (referenced) {
pra->referenced++;
pra->vm_flags |= vma->vm_flags;
}
pra->mapcount--;
if (!pra->mapcount)
return SWAP_SUCCESS; /* To break the loop */
return SWAP_AGAIN;
}