linux内存管理-页面回收之LRU链表

在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表示可以尝试回收该页面。

相关推荐
A小辣椒21 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux