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

相关推荐
小米里的大麦2 小时前
01 在 CentOS 7 中安装 MySQL
linux·mysql·centos
子歌的宏定义2 小时前
主机vscode远程链接服务器开发方法
服务器·ide·vscode
驱动小百科2 小时前
如何连接共享打印机 4种方法一步到位
运维·服务器·共享打印机怎么连接·连接共享打印机方法·打印机共享设置·打印机连接教程
我不是程序猿儿2 小时前
【嵌入式】面向 STM32 的 ADC 与 DMA 学习路线
linux·stm32·单片机·嵌入式硬件·学习
VBsemi-专注于MOSFET研发定制3 小时前
AI训练服务器GPU功率链路设计实战:效率、可靠性与功率密度的平衡之道
运维·服务器·人工智能
whitelbwwww3 小时前
Linux操作系统基本操作
运维·服务器·网络
徐子元竟然被占了!!3 小时前
数字证书学习
linux·网络·学习
百结2143 小时前
LVS-DR 群集部署
运维·服务器·网络
langmeng1103 小时前
Linux安装Kafka3.8.0版本不使用zookeeper
linux·运维·服务器