在linux系统中,当内存有盈余时,内核会尽量多地使用内存作为文件缓存(page cache),从而提高系统的性能。文件缓存页面会加入到文件类型的LRU链表中,当系统内存紧张时,文件缓存页面会被丢弃,或者被修改的文件缓存会被回写到存储设备中,与块设备同步之后便可释放出物理内存。
1、LRU链表
LRU是least recently used(最近最少使用)的缩写,LRU假定最近不使用的页在较短的时间内也不会频繁使用。在内存不足时,这些页面将成为被换出的候选者。内核使用双向链表表来定义LRU链表,并且根据页面的类型分为LRU_ANON和LRU_FILE。每种类型根据页面的活跃性分为活跃LRU和不活跃LRU,所以内核中一共有如下5个LRU链表。
- 不活跃匿名链表 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指向这些链表。枚举类型变量lru_list列举了上述各种LRU链表的类型,struct lruvec数据结构中定义了上述各种LRU类型的链表。
include/linux/mmzone.h
/*
LRU(Least Recently Used,最近最少使用)页面列表类型的枚举,用于内存回收(Page Reclaim)时跟踪不同状态的物理页面
*/
enum lru_list {
/*非活跃匿名页列表,非活跃(Inactive),近期未被访问,优先被回收*/
LRU_INACTIVE_ANON = LRU_BASE,
/*活跃匿名页列表,活跃(Active),近期被访问过,暂不回收(需先降级到非活跃列表)*/
LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
/*非活跃文件页列表*/
LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
/*活跃文件页列表*/
LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
/*
不可驱逐页面列表
被锁定(如mlock())、内核特殊页面(如ramfs)、或硬件保护页面(如GPU显存)
不参与常规LRU轮换,不会被页面回收机制回收(除非显式解锁)
*/
LRU_UNEVICTABLE,
NR_LRU_LISTS
};
/*
记录特定内存区域(Zone)或LRU向量(lruvec)的页面回收相关统计
*/
struct zone_reclaim_stat {
/*
* The pageout code in vmscan.c keeps track of how many of the
* mem/swap backed and file backed pages are referenced.
* The higher the rotated/scanned ratio, the more valuable
* that cache is.
*
* The anon LRU stats live in [0], file LRU stats in [1]
*/
/*
记录最近一段时间内被"旋转"(Rotated)的页面数量
"旋转"定义:页面在LRU列表间的迁移(如从"非活跃列表"移到"活跃列表",表示页面被重新访问,
或从"活跃列表"移到"非活跃列表",表示页面不再活跃)
数组索引:[0]对应匿名页(Anonymous Page),
[1]对应文件页(File-backed Page)
*/
unsigned long recent_rotated[2];
/*
记录最近一段时间内被扫描过的页面数量(即内存回收线程遍历的页面总数)
数组索引:与recent_rotated一致,[0]匿名页,[1]文件页
*/
unsigned long recent_scanned[2];
};
/*
管理特定内存区域内所有LRU列表的容器,代表一个LRU管理单元
*/
struct lruvec {
/*LRU 列表数组(按类型分组)*/
struct list_head lists[NR_LRU_LISTS];
/*回收统计信息*/
struct zone_reclaim_stat reclaim_stat;
#ifdef CONFIG_MEMCG
/*指向所属内存区域(Zone)(仅 MemCG 配置下有效)*/
struct zone *zone;
#endif
};
1.1、页面缓存机制
由于在回收时对页面的扫描变得非常频繁,为了减少竞争,使用缓存机制,将需要加入到LRU链表的页面先加入对应的缓存区,待达到阈值后再同步到LRU链表上,这在内核中是非常常见的机制。
include/linux/pagevec.h
#define PAGEVEC_SIZE 14
/*
批量管理物理页面(struct page)的轻量级容器
核心作用是通过"缓存一批页面"减少频繁操作的开销(如锁竞争、函数调用),提升内存管理(如页面回收、文件缓存操作)的效率
其设计思想是"小批量暂存,集中处理",常见于文件系统、页面缓存、LRU管理等场景
*/
struct pagevec {
unsigned long nr; /*实际存放的页面数量,pages数组中有效元素的个数*/
/*
用于标记pagevec中页面的"冷热属性"(Cold/Hot),或作为页面在LRU列表中的偏移量
内核将页面分为"热页面"(近期被访问)和"冷页面"(长期未访问),冷页面优先被回收,cold成员用于批量传递这一属性
*/
unsigned long cold;
struct page *pages[PAGEVEC_SIZE/*14*/]; /*页面指针数组*/
};
static inline void pagevec_init(struct pagevec *pvec, int cold)
{
pvec->nr = 0;
pvec->cold = cold;
}
static inline void pagevec_reinit(struct pagevec *pvec)
{
pvec->nr = 0;
}
static inline unsigned pagevec_count(struct pagevec *pvec)
{
return pvec->nr;
}
static inline unsigned pagevec_space(struct pagevec *pvec)
{
return PAGEVEC_SIZE/*14*/ - pvec->nr;
}
/*
* Add a page to a pagevec. Returns the number of slots still available.
*/
static inline unsigned pagevec_add(struct pagevec *pvec, struct page *page)
{
pvec->pages[pvec->nr++] = page;
return pagevec_space(pvec); /*返回剩余数量*/
}
注意,因为lru_add_pvec为per-cpu变量,所以这里nr++没有使用锁进行保护。
static inline void pagevec_release(struct pagevec *pvec)
{
if (pagevec_count(pvec))
__pagevec_release(pvec);
}
void __pagevec_lru_add(struct pagevec *pvec)
{
pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL);
}
static void pagevec_lru_move_fn(struct pagevec *pvec,
void (*move_fn)(struct page *page, struct lruvec *lruvec, void *arg),
void *arg)
{
int i;
struct zone *zone = NULL;
struct lruvec *lruvec;
unsigned long flags = 0;
/*遍历page 缓存*/
for (i = 0; i < pagevec_count(pvec); i++) {
struct page *page = pvec->pages[i];
struct zone *pagezone = page_zone(page);
if (pagezone != zone) {
if (zone) /*zone发生改变需要释放自旋锁*/
spin_unlock_irqrestore(&zone->lru_lock, flags);
zone = pagezone; /*zone发生改变需要获取自旋锁*/
spin_lock_irqsave(&zone->lru_lock, flags);
}
lruvec = mem_cgroup_page_lruvec(page, zone); /*返回 &zone->lruvec*/
(*move_fn)(page, lruvec, arg); /*执行函数 __pagevec_lru_add_fn,将page添加到lruvec->lists[lru]对应类型的链表上*/
}
if (zone) /*释放锁*/
spin_unlock_irqrestore(&zone->lru_lock, flags);
/*释放页面引用计数为0的页面*/
release_pages(pvec->pages, pvec->nr, pvec->cold);
pagevec_reinit(pvec); /*pvec->nr = 0*/
}
static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
void *arg)
{
int file = page_is_file_cache(page); /*是否为文件页*/
int active = PageActive(page);
enum lru_list lru = page_lru(page); /*返回页面对应的lru链表类型*/
/*
若页面已在LRU列表中(PageLRU(page)=1),再次调用lru_cache_add会导致重复添加
*/
VM_BUG_ON_PAGE(PageLRU(page), page); /*检查页面是否位于LRU列表中*/
SetPageLRU(page); /*设置page->flags PG_lru*/
/*将page添加到lruvec->lists[lru]对应类型的链表上*/
add_page_to_lru_list(page, lruvec, lru);
update_page_reclaim_stat(lruvec, file, active);
trace_mm_lru_insertion(page, lru);
}
static __always_inline void add_page_to_lru_list(struct page *page,struct lruvec *lruvec, enum lru_list lru)
{
int nr_pages = hpage_nr_pages(page); /*透明页情况下的page数量,常规为1*/
mem_cgroup_update_lru_size(lruvec, lru, nr_pages);
/*
将page添加到lruvec->lists[lru]对应类型的链表上
*/
list_add(&page->lru, &lruvec->lists[lru]);
/*更新数据*/
__mod_zone_page_state(lruvec_zone(lruvec), NR_LRU_BASE + lru, nr_pages);
}
2、检测页面最近是否被访问过机制
在进行页面回收时,如果页面最近被访问过的页面肯定是不能立马进行回收的,因为根据局部性原理,该页面可能也会马上被再次访问到。那么如何检查页面最近是否被访问过呢?
下面介绍两个页面回收中非非常重要的标志位:
PG_active用于标记页面是否位于活跃LRU链表(Active List)。活跃链表存放近期被频繁访问的"热页面",回收优先级低。不活跃链表(Inactive List)存放"冷页面",回收优先级高。
PG_referenced用于记录页面是否被近期访问(软件层面)。它通常与硬件页表项的Accessed位(CPU自动置位)协同,判断页面活跃度。
2.1、mark_page_accessed()函数
下面看下mark_page_accessed()函数内部是如何实现页面访问检测的.
/*标记物理页面已被访问,并根据页面当前状态(活跃性、引用状态)更新其在LRU链表中的位置,实现页面活跃度的动态调整*/
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)) /*页面在LRU链表,但不在活跃链表中,直接激活*/
activate_page(page); /*激活页面,就是将页面从inactive LRU链表中删除再放入active LRU链表中*/
else /*页面不再LRU链表中,应该在pacevec中*/
__lru_cache_activate_page(page); /*在pagevec中找到page则激活页面*/
ClearPageReferenced(page); /*清除引用标志,方便下一轮统计*/
if (page_is_file_cache(page))
workingset_activation(page); /*文件缓存页,更新工作集*/
} else if (!PageReferenced(page)) { /*页面未被引用,注意非活跃页面第一次被访问时只设置其引用标志位,第二次访问时才放入活跃LRU链表中*/
SetPageReferenced(page); /*设置引用标志*/
}
}
void activate_page(struct page *page)
{
struct zone *zone = page_zone(page);
spin_lock_irq(&zone->lru_lock);
/*激活页面,就是将页面从inactive LRU链表中删除再放入active LRU链表中*/
__activate_page(page, mem_cgroup_page_lruvec(page, zone), NULL);
spin_unlock_irq(&zone->lru_lock);
}
/*激活页面,就是将页面从inactive LRU链表中删除再放入active LRU链表中*/
static void __activate_page(struct page *page, struct lruvec *lruvec,
void *arg)
{
if (PageLRU(page) && !PageActive(page)/*未在激活链表*/ && !PageUnevictable(page)/*可回收*/) {
int file = page_is_file_cache(page);
int lru = page_lru_base_type(page); /*非激活LRU链表类型*/
/*将page从非激活链表中删除*/
del_page_from_lru_list(page, lruvec, lru);
/*设置PG_active标志*/
SetPageActive(page);
lru += LRU_ACTIVE;
/*将page添加到active链表上*/
add_page_to_lru_list(page, lruvec, lru);
trace_mm_lru_activate(page);
__count_vm_event(PGACTIVATE);
update_page_reclaim_stat(lruvec, file, 1);
}
}
static void __lru_cache_activate_page(struct page *page)
{
struct pagevec *pvec = &get_cpu_var(lru_add_pvec);
int i;
/*
* Search backwards on the optimistic assumption that the page being
* activated has just been added to this pagevec. Note that only
* the local pagevec is examined as a !PageLRU page could be in the
* process of being released, reclaimed, migrated or on a remote
* pagevec that is currently being drained. Furthermore, marking
* a remote pagevec's page PageActive potentially hits a race where
* a page is marked PageActive just after it is added to the inactive
* list causing accounting errors and BUG_ON checks to trigger.
*/
/*倒序遍历*/
for (i = pagevec_count(pvec) - 1; i >= 0; i--) {
struct page *pagevec_page = pvec->pages[i];
if (pagevec_page == page) {
SetPageActive(page); /*找到则激活页面*/
break;
}
}
put_cpu_var(lru_add_pvec);
}
在mark_page_accessed函数中调用ClearPageReferenced()清除PG_referenced标志位的目的是Linux内核页面活跃度状态机的关键设计,目的是完成"不活跃到活跃"状态转换时重置临时访问标记,确保标志位与页面实际状态一致。
PG_referenced的"临时标记"语义
PG_referenced是短期访问标记(软件位),用于记录页面"近期被访问过",但不直接决定页面是否"活跃"。
它的核心作用是:
- 触发激活:当不活跃页面(!PageActive)的PG_referenced=1时,表明页面被二次访问,需从"不活跃链表"移到"活跃链表"
- 辅助回收判断:与硬件Accessed位配合,判断页面是否应暂缓回收
- 关键特性:PG_referenced是"一次性"标记,一旦触发状态转换(如激活页面),其使命即完成,需立即清除,避免干扰后续状态判断
2.2、page_referenced()函数
下面来看page_referenced()函数
mm/rmap.c
/*
检测页面是否被访问过,通过遍历页面的反向映射(Reverse Mapping)统计有效引用次数,并收集关联VMA的特性标志
*/
int page_referenced(struct page *page,
int is_locked/*调用方是否已持有页面锁*/,
struct mem_cgroup *memcg,
unsigned long *vm_flags/*输出参数,返回页面关联的所有VMA标志的按位或结果*/)
{
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, /*pra作为参数传递*/
.anon_lock = page_lock_anon_vma_read,
};
*vm_flags = 0;
if (!page_mapped(page)) /*返回page->_mapcount,0表示无PTE映射*/
return 0; /*无引用*/
if (!page_rmapping(page)) /*page->mapping对应的anon_vma是否存在,0表示无反向映射*/
return 0; /*无引用*/
if (!is_locked && (!PageAnon(page)/*非匿名页*/ || PageKsm(page))/*KSM合并页*/) {
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;
}
/*反向映射查找映射page的VMA*/
ret = rmap_walk(page, &rwc);
*vm_flags = pra.vm_flags;
if (we_locked)
unlock_page(page);
return pra.referenced; /*返回引用次数*/
}
rmap_walk()函数实现在反向映射部分有详细描述,这里不再赘述。
rmap_walk会遍历红黑树中所有和page存在重叠的VMA,并对每个VMA依次调用page_referenced_one()检查最近是否访问过该page。
page_referenced()函数判断page是否被访问引用过,返回访问引用pte的个数,即访问和引用(referenced)这个页面的用户进程空间虚拟页面的个数。核心思想是利用方向映射系统来统计访问引用pte的用户的个数。
struct page_referenced_arg {
int mapcount;
int referenced;
unsigned long vm_flags;
struct mem_cgroup *memcg;
};
/*
* arg: page_referenced_arg will be passed
*/
/*检查单个虚拟内存映射(VMA)对页面的访问状态,通过检测页表项的访问位(Accessed Bit)判断页面是否被访问过*/
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().
*/
/*透明页(2M)只需要PMD即可*/
pmd = page_check_address_pmd(page, mm, address,
PAGE_CHECK_ADDRESS_PMD_FLAG, &ptl);
if (!pmd) /*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().
*/
/*检查address是否映射,返回对应软件pte*/
pte = page_check_address(page, mm, address, &ptl, 0);
if (!pte)
return SWAP_AGAIN; /*PTE 无效*/
if (vma->vm_flags & VM_LOCKED) { /*锁定内存页*/
pte_unmap_unlock(pte, ptl);
pra->vm_flags |= VM_LOCKED;
return SWAP_FAIL; /* To break the loop 终止遍历 */
}
/*
返回pte是否设置了L_PTE_YOUNG,然后清空pte的L_PTE_YOUNG,清空硬件pte,下一次再次访问时触发缺页异常,设置page的referenced
从而实现周期检查page近期是否被访问过。
*/
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) {
pra->referenced++;
pra->vm_flags |= vma->vm_flags;
}
pra->mapcount--;/*ptep_clear_flush_young_notify中清空page对应的硬件pte,对其引用次数减一*/
if (!pra->mapcount)
return SWAP_SUCCESS; /* To break the loop */
return SWAP_AGAIN; /*返回SWAP_AGAIN 继续解除与下一个VMA的映射*/
}
page_check_address()函数在反向映射章节详述过,这里不再赘述。
上面代码中:
if (likely(!(vma->vm_flags & VM_SEQ_READ))) /*排除顺序读取*/
referenced++; /*检测到访问,引用数量增加*/
这里会排除顺序读的情况,因为顺序读的page cache是被回收的最佳候选者,因此对这些page cache做了弱访问引用处理(weak reference),
而其余情况都会当做pte被引用。
当page_reference()函数计算访问引用PTE的页面个数时,通过RMAP反向映射遍历每个PTE,然后调用ptep_clear_young_notify()函数来检查每个PTE最近是否被访问过。
#define ptep_clear_flush_young_notify ptep_clear_flush_young
mm/pgtable-generic.c
int ptep_clear_flush_young(struct vm_area_struct *vma,
unsigned long address, pte_t *ptep)
{
int young;
/*清除L_PTE_YOUNG,清空硬件pte,返回ptep是否设置L_PTE_YOUNG*/
young = ptep_test_and_clear_young(vma, address, ptep);
if (young) /*硬件pte被清空,刷新tlb*/
flush_tlb_page(vma, address);
return young;
}
判断该pte entry最近是否被访问过,如果访问过,L_PTE_YOUNG比特位会被自动置位,并清空PTE中的L_PTE_YOUNG比特位。
在x86处理器中指的是_PAGE_ACCESSED比特位;在ARM32 linux中,硬件上没有L_PTE_YOUNG比特位,那么ARM32 linux如何模拟这个Linux版本的L_PTE_YOUNG呢?
include/asm-generic/pgtable.h
static inline int ptep_test_and_clear_young(struct vm_area_struct *vma,unsigned long address,
pte_t *ptep)
{
pte_t pte = *ptep;
int r = 1; /*初始值为1*/
if (!pte_young(pte)) /*L_PTE_YOUNG未置位*/
r = 0;
else /*L_PTE_YOUNG置位,清除L_PTE_YOUNG*/
set_pte_at(vma->vm_mm, address, ptep/*旧值*/, pte_mkold(pte)/*清除L_PTE_YOUNG后的值*/);
return r;
}
arch/arm/include/asm/pgtable.h
#define pte_young(pte) (pte_isset((pte), L_PTE_YOUNG))
#define pte_isset(pte, val) ((u32)(val) == (val) ? pte_val(pte) & (val) \
: !!(pte_val(pte) & (val)))
arch/arm/include/asm/pgtable.h
static inline pte_t pte_mkold(pte_t pte)
{
return clear_pte_bit(pte, __pgprot(L_PTE_YOUNG));
}
static inline pte_t clear_pte_bit(pte_t pte, pgprot_t prot)
{
pte_val(pte) &= ~pgprot_val(prot);
return pte;
}
ptep_test_and_clear_young()函数首先利用pte_young宏来判断linux版本的页表项是否包含L_PTE_YOUNG比特位,如果没有设置该比特位,则返回0,
表示映射PTE最近没有被访问引用过。如果L_PTE_YOUNG比特位置位,那么需要调用pte_mkold()宏来清这个比特位,然后调用set_pte_at()函数来写入ARM硬件页表。
arch/arm/include/asm/pgtable.h
static inline void set_pte_at(struct mm_struct *mm, unsigned long addr,
pte_t *ptep, pte_t pteval)
{
unsigned long ext = 0;
if (addr < TASK_SIZE && pte_valid_user(pteval)/*L_PTE_VALID,L_PTE_USER L_PTE_YOUNG都置位*/) {
if (!pte_special(pteval))
__sync_icache_dcache(pteval);
ext |= PTE_EXT_NG;
}
/*设置硬件pte为0,下次访问该页时触发缺页中断*/
set_pte_ext(ptep, pteval, ext);
}
2.3、page_check_references()函数
下面来看page_check_references()函数
mm/vmscan.c
enum page_references {
/*页面近期无引用,可直接尝试回收*/
PAGEREF_RECLAIM,
/*近期有硬件访问但无软件映射(pte)的干净文件页,因为文件页有后备存储,干净页可直接丢弃,无需回写,回收成本低*/
PAGEREF_RECLAIM_CLEAN,
/*
有软件映射但近期无硬件访问的文件页,内核会暂时保留它在非活跃列表,给它"第二次机会"
如果它在下次扫描前被再次访问,就会被激活,否则将被回收
*/
PAGEREF_KEEP,
/*
匿名页只要有映射就激活(加入激活LRU链表)
文件页则需满足"多次引用"或"可执行映射"等更强条件
激活意味着将其移回活跃LRU列表,避免被回收
*/
PAGEREF_ACTIVATE,
};
/*
判断一个物理页面的活跃度,并决定这个页面在内存回收过程中的命运
是页面回收算法在扫描LRU列表时,评估一个页面是否应该被回收、保留还是激活的关键决策点
*/
static enum page_references page_check_references(struct page *page,
struct scan_control *sc)
{
int referenced_ptes, referenced_page;
unsigned long vm_flags;
/*
返回引用该page的pte VMA个数,并且清除page的L_PTE_YOUNG标志,设置page对应的硬件pte无效,
当再次访问该page时触发缺页中断时会再次标记L_PTE_YOUNG标志来实现检测页面最近是否有被访问过与否
*/
referenced_ptes = page_referenced(page, 1, sc->target_mem_cgroup, &vm_flags);
/*
页面的软件访问标志PG_referenced
*/
referenced_page = TestClearPageReferenced(page);
/*
* Mlock lost the isolation race with us. Let try_to_unmap()
* move the page to the unevictable list.
*/
/*
VM_LOCKED表示页面被mlock()系统调用锁定在内存中
这类页面不应该被回收,但如果出现在回收扫描中,说明之前隔离失败
*/
if (vm_flags & VM_LOCKED)
return PAGEREF_RECLAIM; /*返回PAGEREF_RECLAIM,让后续操作将其移入不可回收列表*/
/*
匿名页没有磁盘后备文件,一旦换出,再访问时必然产生磁盘I/O
决策逻辑,只要有进程映射(referenced_ptes > 0),立即激活
原因,匿名页的换出成本极高,宁可"误留"也避免"误杀
*/
if (referenced_ptes) { /*多个进程映射该page*/
if (PageSwapBacked(page)) /*匿名页*/
return PAGEREF_ACTIVATE;
/*
* All mapped pages start out with page table
* references from the instantiating fault, so we need
* to look twice if a mapped file page is used more
* than once.
*
* Mark it and spare it for another trip around the
* inactive list. Another page table reference will
* lead to its activation.
*
* Note: the mark is set for activated pages as well
* so that recently deactivated but used pages are
* quickly recovered.
*/
/*文件页的处理复杂得多,因为有磁盘后备文件,回收成本相对较低*/
/*
为页面重新设置PG_referenced标志
相当于给页面一个"复活标记",如果近期被访问,下次扫描会发现
*/
SetPageReferenced(page);
/*
referenced_page为真,页面在上次设置标记后又被访问过
referenced_ptes > 1,被多个进程/地址同时映射,说明是热点数据
满足任一条件就激活,这是"频繁使用的页面应该保护"的原则
*/
if (referenced_page || referenced_ptes > 1)
return PAGEREF_ACTIVATE;
/*
* Activate file-backed executable pages after first usage.
*/
/*
可执行页面(包含程序代码的页面)的优化
激活原因:
代码通常有较好的局部性,被访问后可能再次被访问
避免缺页中断对程序性能的严重影响
代码页通常是只读的,回收后重新从磁盘加载成本确定
*/
if (vm_flags & VM_EXEC)
return PAGEREF_ACTIVATE;
/*
默认情况,给予第二次机会
这是最精妙的设计,有映射但近期未被访问的文件页保留在非活跃列表,等待"第二次机会"
如果在下次扫描前被访问,PG_referenced会被设置,页面会被激活
如果在下次扫描时仍未被访问,就会被回收
*/
return PAGEREF_KEEP;
}
/*下面为无进程映射的情况(referenced_ptes == 0)*/
/* Reclaim if clean, defer dirty pages to writeback */
/*
这是一个特别的优化,回收成本最低的页面优先
文件页在磁盘有备份
如果是干净的(未修改),可以直接丢弃,无需回写
回收成本极低,是最理想的回收目标
*/
if (referenced_page/*最近有硬件访问*/ && !PageSwapBacked(page)/*文件页*/)
return PAGEREF_RECLAIM_CLEAN;
/*既无软件引用(pte),又无近期硬件访问,最典型的回收候选者*/
return PAGEREF_RECLAIM;
}
在扫描不活跃LRU链表时,page_check_reference()会被调用,返回值是一个page_references的枚举类型。
PAGEREF_ACTIVE表示该页面会被迁移到活跃链表,PAGEREF_KEEP表示会继续保留在不活跃链表中,
PAGEREF_RECLAIM和PAGEREF_RECLAIM_CLEAN表示可以尝试回收该页面。