Linux page flags

想要了解更多Linux 内存相关知识敬请关注:Linux 内存笔记公众号,期待与大家的更多交流与学习。
原创文章,转载请注明出处。

一、背景

​ 系统的业务是复杂的、不断变化的所以内存状态也是复杂的;内存以page为单位进行描述,在不同的场景下page的状态是不同的,准确的理解page的状态对内存分配、回收、迁移等不同内存场景有极大的帮助。

二、分析

struct page中的flags成员是页面的标志位集合,这些标志位是内存管理中非常重要的部分,具体定义在include/linux/page-flags.h 文件当中; 通过enum pageflags枚举结构体详细的描述page的状态、属性;

enum pageflags {
	PG_locked,		/* Page is locked. Don't touch. */
	PG_referenced,
	PG_uptodate,
	PG_dirty,
	PG_lru,
	PG_active,
	PG_workingset,
	PG_waiters,		/* Page has waiters, check its waitqueue. Must be bit #7 and in the same byte as "PG_locked" */
	PG_error,
	PG_slab,
	PG_owner_priv_1,	/* Owner use. If pagecache, fs may use*/
	PG_arch_1,
	PG_reserved,
	PG_private,		/* If pagecache, has fs-private data */
	PG_private_2,		/* If pagecache, has fs aux data */
	PG_writeback,		/* Page is under writeback */
	PG_head,		/* A head page */
	PG_mappedtodisk,	/* Has blocks allocated on-disk */
	PG_reclaim,		/* To be reclaimed asap */
	PG_swapbacked,		/* Page is backed by RAM/swap */
	PG_unevictable,		/* Page is "unevictable"  */
#ifdef CONFIG_MMU
	PG_mlocked,		/* Page is vma mlocked */
#endif
#ifdef CONFIG_ARCH_USES_PG_UNCACHED
	PG_uncached,		/* Page has been mapped as uncached */
#endif
#ifdef CONFIG_MEMORY_FAILURE
	PG_hwpoison,		/* hardware poisoned page. Don't touch */
#endif
#if defined(CONFIG_IDLE_PAGE_TRACKING) && defined(CONFIG_64BIT)
	PG_young,
	PG_idle,
#endif
#ifdef CONFIG_64BIT
	PG_arch_2,
#endif
	__NR_PAGEFLAGS,

	/* Filesystems */
	PG_checked = PG_owner_priv_1,

	/* SwapBacked */
	PG_swapcache = PG_owner_priv_1,	/* Swap page: swp_entry_t in private */

	/* Two page bits are conscripted by FS-Cache to maintain local caching
	 * state.  These bits are set on pages belonging to the netfs's inodes
	 * when those inodes are being locally cached.
	 */
	PG_fscache = PG_private_2,	/* page backed by cache */

	/* XEN */
	/* Pinned in Xen as a read-only pagetable page. */
	PG_pinned = PG_owner_priv_1,
	/* Pinned as part of domain save (see xen_mm_pin_all()). */
	PG_savepinned = PG_dirty,
	/* Has a grant mapping of another (foreign) domain's page. */
	PG_foreign = PG_owner_priv_1,
	/* Remapped by swiotlb-xen. */
	PG_xen_remapped = PG_owner_priv_1,

	/* SLOB */
	PG_slob_free = PG_private,

	/* Compound pages. Stored in first tail page's flags */
	PG_double_map = PG_workingset,

	/* non-lru isolated movable page */
	PG_isolated = PG_reclaim,

	/* Only valid for buddy pages. Used to track pages that are reported */
	PG_reported = PG_uptodate,
};

如何使用

常规情况下通过API位操作来设置、清除或检查page状态;需要强调的是这类接口都是原子操作的,在SMP多核系统当中必须使用此类结构操作

PageXXX(page) //检查page是否设置了PG_XXX位
SetPageXXX(page) //设置page的PG_XXX位
ClearPageXXX(page) //清除page的PG_XXX位
TestSetPageXXX(page) //设置page的PG_XXX位,并返回原值
TestClearPageXXX(page) //清除page的PG_XXX位,并返回原值

PG_dirty为例

SetPageDirty设置page状态为脏页(页面内容已被修改),当进行内存回收时需要将数据写回到磁盘

PageDirty检查page是否为脏页,在内存回收阶段会频繁检查page的状态true则代表page为脏页、false则不是脏页

ClearPageDirty清除page的脏页标志;在内存回收过程中当一个脏页面(即内容已被修改的页面)被写回磁盘后,内核会使用这个 宏来清除页面的PG_dirty 标志,表示页面现在与磁盘上的数据是同步的。

TestSetPageDirty检查页面是否已经被标记为脏,如果页面尚未被标记为脏,TestSetPageDirty 会设置 PG_dirty 标志,并返回旧 的标志值。TestSetPageDirty 返回false:这意味着页面之前没有被标记为脏,现在宏已经将其设置为脏; 如果返回true,这意味 着页面已经被标记为脏,不需要再次设置。

TestClearPageDirty检查页面是否已经被标记为脏并清理Dirty标志,如果true说明page已经被设置为脏页,现在清除PG_dirty标志 并返回之前的标志状态;如果返回false说明该page不是脏页则不需要而外做清除操作。

**include/linux/page-flags.h中对上述接口有详细的描述,通过set_bit、clear_bit、test_bit、test_and_set_bit、test_and_clear_bit**进行page状态的调整。Notice:因为set_bit、clear_bit、test_bit、test_and_set_bit、test_and_clear_bit是原子操作,所以上述类型的操作都是原子的,在SMP多核系统中至关重要。如果是__set_bit等这类接口则该操作就不是原子的。其中的区别通过阅读:include/asm-generic/bitops/atomic.h、include/asm-generic/bitops/non-atomic.h可以有所了解。

/*
 * Macros to create function definitions for page flags
 */
#define TESTPAGEFLAG(uname, lname, policy)				\
static __always_inline int Page##uname(struct page *page)		\
	{ return test_bit(PG_##lname, &policy(page, 0)->flags); }

#define SETPAGEFLAG(uname, lname, policy)				\
static __always_inline void SetPage##uname(struct page *page)		\
	{ set_bit(PG_##lname, &policy(page, 1)->flags); }

#define CLEARPAGEFLAG(uname, lname, policy)				\
static __always_inline void ClearPage##uname(struct page *page)		\
	{ clear_bit(PG_##lname, &policy(page, 1)->flags); }

#define TESTSETFLAG(uname, lname, policy)				\
static __always_inline int TestSetPage##uname(struct page *page)	\
	{ return test_and_set_bit(PG_##lname, &policy(page, 1)->flags); }

#define TESTCLEARFLAG(uname, lname, policy)				\
static __always_inline int TestClearPage##uname(struct page *page)	\
	{ return test_and_clear_bit(PG_##lname, &policy(page, 1)->flags); }

#define __SETPAGEFLAG(uname, lname, policy)				\
static __always_inline void __SetPage##uname(struct page *page)		\
	{ __set_bit(PG_##lname, &policy(page, 1)->flags); }

#define __CLEARPAGEFLAG(uname, lname, policy)				\
static __always_inline void __ClearPage##uname(struct page *page)	\
	{ __clear_bit(PG_##lname, &policy(page, 1)->flags); }

Key Flag

flagdescription
PG_locked表示页面已经被锁定,此时有其他模块正在使用该page,内存管理的其他模块不应该访问这个页面,以防止发生竞争条件。
PG_referenced表示页面被访问过,是内存回收过程及page在inactive、active链表的切换的重要依据。
PG_uptodate表示页面的数据是最新的,即与磁盘上的数据一致;该标志主要应用在文件读取过程中。
PG_dirty表示页面内容已经被修改,在内存回收过程中如果要回收此类page则需要将数据写回到磁盘当中。
PG_lru表示页面已经添加到LRU链表中,后续内存在active、inactive链表当中切换。
PG_writeback表示页面正处于写回过程中,通常当页面内存被修改后变成Dirty,脏页的内容写回到磁盘当中就是writeback
PG_reclaim表示页面可以被回收,在内存回收流程中系统会尽快完成此类page的回收。
PG_swapbacked表示页面可以存储在RAM/Swap中,描述匿名页
PG_swapcache表示页面在swap cache当中,描述匿名页
PG_unevictable表示页面不能被内存管理系统逐出或交换到磁盘上,这类页面在LRU_UNEVICTABLE链表当中管理。
PG_mlocked表示vma被锁,对应的页面也被锁入到系统当中避免这类页面被交换、迁移、回收。

针对核心flag进一步分析介绍Flag功能的细节以及flag处理的流程,对flag的深刻理解有利于提高对内存管理的理解,增强系统优化能力。

PG_lru

​ Page的管理是通过LRU(Least Recently Used)实现的,在用户态通过fault机制完成内存的访问在这个过程中申请的page就被添加LRU链表进行管理;LRU在内存分配、内存回收当中发挥重要作用。

PAGEFLAG(LRU, lru, PF_HEAD) __CLEARPAGEFLAG(LRU, lru, PF_HEAD)

#define PAGEFLAG(uname, lname, policy)					\
	TESTPAGEFLAG(uname, lname, policy)				\
	SETPAGEFLAG(uname, lname, policy)				\
	CLEARPAGEFLAG(uname, lname, policy)

PageLRU判断page是否在LRU链表中;SetPageLRU用户态申请page后设置LRU表明page在LRU链表当中;ClearPageLRU、__ClearPageLRU内存回收过程中回收该页面后清理LRU标志。

PG_lru、PG_active属性是存在关联的,内存管理中链表分为inactive lru、active lru ,page的迁移、流转、老化就是在这两个链表当中不断的变换,理解page的状态就能准确的控制page在inactive lru、active lru的流转,推动系统内存管理的高效进行。

PAGEFLAG(Active, active, PF_HEAD) __CLEARPAGEFLAG(Active, active, PF_HEAD)
	TESTCLEARFLAG(Active, active, PF_HEAD)

PageActive判断page是否在active lru链表,在内存回收阶段有重要意义;SetPageActive在workingset流程、内存回收过程中页面老化迁移流程有重要意义;ClearPageActive、__ClearPageActive页面老化过程比如page需要从active lru迁移到inactive lru过程、在内存回收流程都有重要意义。TestClearPageActive主要应用在页面迁移流程当中先PageActive如果为true则ClearPageActive。

do_anonymous_page()
|		alloc_zeroed_user_highpage_movable()
|		|		lru_cache_add_inactive_or_unevictable()
|		|		|		lru_cache_add()
|		|		|		|	 	__pagevec_lru_add()
|		|		|		|		|		__pagevec_lru_add_fn()
|		|		|		|		|		|		SetPageLRU()

用户态通过page fault申请内存时新申请的page需要在LRU链表当中管理,在page申请完毕后添加到对inactive lru时就需要SetPageLRU

PG_locked

Page被锁定通常发生需要确保内存中的数据保持一致性的场景中,特别是在进行某些需要保证数据连续性和完整性的操作时,在内存的申请、数据读取、数据写回、内存回收等多个场景有重要意义。当page被锁定时不能被系统进行交换、回收等操作,需要等到该page unlock后才能对该page进行操作。

//Lock the page
static inline int trylock_page(struct page *page);
static inline void lock_page(struct page *page);
static inline int lock_page_killable(struct page *page);
static inline int lock_page_async(struct page *page, struct wait_page_queue *wait);

//Wait for a page to be unlocked
static inline void wait_on_page_locked(struct page *page);
static inline int wait_on_page_locked_killable(struct page *page);
static int wait_on_page_locked_async(struct page *page, struct wait_page_queue *wait);

static inline int lock_page_or_retry(struct page *page, struct mm_struct *mm, unsigned int flags);

1、trylock_page:

如果return false代表获取该page锁失败,说明page已经被其他进程锁定,其他进程正在使用该page此时;return true则说明该page此时没有被锁定,在代码此处获取锁成功,给该page成功上锁。之前强调过test_and_set_bit如果return true则说明page已被置位此时返回的是page的old状态,如果return false则说明page还没有被置位此事需要针对flag给page置位。

!test_and_set_bit_lock(PG_locked, &page->flags)

如果page已经被锁则test_and_set_bit_lock = true,则!test_and_set_bit_lock = false此时page被其他进程所锁,无法获取锁

如果page未被锁定test_and_set_bit_lock = false则!test_and_set_bit_lock = true此时page未被其他进程所锁,获取锁成功

需要强调:该锁是轻量级的,尝试获取锁失败则立即返回不会存在休眠等待等操作,这也就意味着此种方式获取锁失败的概率是极高的。

2、lock_page:

申请页面锁,如果trylock_page获取锁失败说明此时page被其他进程锁住所以才会出现获取锁失败的问题,当获取锁失败后

void __lock_page(struct page *__page)
{
	struct page *page = compound_head(__page);
	wait_queue_head_t *q = page_waitqueue(page);
	wait_on_page_bit_common(q, page, PG_locked, TASK_UNINTERRUPTIBLE,
				EXCLUSIVE);
}

page将进入等待队列同时设置进程不可中断的休眠等待该page解锁,该page解锁后唤醒进程获取锁成功则**EXCLUSIVE**。lock_page的使用不当会造成进程的休眠从而导致性能的衰退。

3、lock_page_killable:

申请获取页面锁,如果trylock_page获取锁失败说明此时page被其他进程锁住所以才会出现获取锁失败的问题,当获取锁失败后进入等待队列,休眠等待page解锁,page解锁后成功获取锁唤醒该进程。lock_page_killable、lock_page是不同的,lock_page休眠后不能被唤醒需要等待page解锁后才能唤醒进程,如果该page一直不解锁则进程无法被唤醒就会产生阻塞;lock_page_killable休眠后可以通过终止信号唤醒进程,优点就是可以防止page无法解锁导致的进程被阻塞。

int __lock_page_killable(struct page *__page)
{
	struct page *page = compound_head(__page);
	wait_queue_head_t *q = page_waitqueue(page);
	return wait_on_page_bit_common(q, page, PG_locked, TASK_KILLABLE,
					EXCLUSIVE);
}

4、lock_page_async :

异步锁,同样trylock_page获取锁失败, 说明该page被其他进程使用所以获取失败,获取锁失败后将该进程添加到等待队列当中

lock_page_async
|		!trylock_page(page)
|				 __lock_page_async(page, wait);
|				|		__wait_on_page_locked_async(page, wait, true);  

static int __wait_on_page_locked_async(struct page *page,
				       struct wait_page_queue *wait, bool set)
{
	struct wait_queue_head *q = page_waitqueue(page);
	int ret = 0;

	wait->page = page;
	wait->bit_nr = PG_locked;

	spin_lock_irq(&q->lock);
	__add_wait_queue_entry_tail(q, &wait->wait);
	SetPageWaiters(page);
	if (set)
		ret = !trylock_page(page);
	else
		ret = PageLocked(page);
	/*
	 * If we were succesful now, we know we're still on the
	 * waitqueue as we're still under the lock. This means it's
	 * safe to remove and return success, we know the callback
	 * isn't going to trigger.
	 */
	if (!ret)
		__remove_wait_queue(q, &wait->wait);
	else
		ret = -EIOCBQUEUED;
	spin_unlock_irq(&q->lock);
	return ret;
}

将page添加到等待队列尾部,然后再**trylock_page**尝试获取锁:

​ 1、如果获取锁失败ret = trueret = -EIOCBQUEUED解锁最终获取锁失败,在等待队列休眠异步等待获取锁成功,这就是一个异步流程。

​ 2、如果获取锁成功ret = false__remove_wait_queue(q, &wait->wait)获取锁成功将进程从等待队列当中移除,**避免**进程在等待队列当中发生休眠动作。

static int wait_on_page_locked_async(struct page *page,
				     struct wait_page_queue *wait)
{
	if (!PageLocked(page))
		return 0;
	return __wait_on_page_locked_async(compound_head(page), wait, false);
}

wait_on_page_locked_async 是解锁动作,如果page没有被锁直接return 0说明page已解锁。当PageLocked = true则进入__wait_on_page_locked_async(compound_head(page), wait, false) 根据源码可知将page对应的进程添加到等待队列当中,此时再通过PageLocked判断page是否依然被锁(double check)

​ 1、如果判断page依然被锁则ret = -EIOCBQUEUED 最终return -EIOCBQUEUED 此时page对应的进程在等待队列当中等待解锁成功然后唤醒进程,这就是一个异步流程。

​ 2、如果判断page已经解锁则ret = false最终__remove_wait_queue(q, &wait->wait)将进程从等待队列当中移除,**避免**进程在等待队列当中发生休眠动作。

lock_page_async、wait_on_page_locked_async是对应lock page、unlock page过程,是对称使用的。

5、wait_on_page_locked:

判断PageLocked(page)是否被锁,这里与trylock_page不同并没有要获取锁的动作只是单纯的判断page是否被锁,如果page处于被锁状态则wait_on_page_bit进入TASK_UNINTERRUPTIBLE不可唤醒休眠等待page解锁然后唤醒进程。

wait_on_page_locked、lock_page的差异在于SHARED(check the bit)、EXCLUSIVE(take the bit) 所以**wait_on_page_locked是page解锁后唤醒进程check page是否解锁,而lock_page是page解锁后进程被唤醒然后setting PG_locked获取锁。** wait_on_page_locked与lock_page是对应关系。

6、wait_on_page_locked_killable:

判断PageLocked(page)是否被锁,这里与trylock_page不同并没有要获取锁的动作只是单纯的判断page是否被锁,如果page处于被锁状态则wait_on_page_bit_killable进入TASK_KILLABLE休眠状态等待page解锁并唤醒进程,同时当进程收到终止信号后也会唤醒进程不再休眠。wait_on_page_locked_killable与lock_page_killable是对应关系。

7、lock_page_or_retry:

尝试trylock_page或者是__lock_page_or_retry,如果trylock_page获取锁成功则直接return true返回;如果获取锁失败return false说明其他进程正在持有这个page当前进程无法获取该page的锁,在持续运行过程当中就需要能够使用可以休眠的方式获取锁也就是__lock_page_or_retry

static inline int lock_page_or_retry(struct page *page, struct mm_struct *mm,
				     unsigned int flags)
{
	might_sleep();
	return trylock_page(page) || __lock_page_or_retry(page, mm, flags);
}

static inline bool fault_flag_allow_retry_first(unsigned int flags)
{
	return (flags & FAULT_FLAG_ALLOW_RETRY) &&
	    (!(flags & FAULT_FLAG_TRIED));
}

int __lock_page_or_retry(struct page *page, struct mm_struct *mm,
			 unsigned int flags)
{
  /* want to try to avoid taking the mmap_lock for too long a time */
  /* 判断是否允许反复尝试解锁,目的在于解决mmap_lock锁page太久导致的阻塞问题
   * 如果允许retry就解锁该page,针对page进行重复设置,这也给我们揭示了一个结论:
   * unlock page效率是高于lock page*/
	if (fault_flag_allow_retry_first(flags)) {
		/*
		 * CAUTION! In this case, mmap_lock is not released
		 * even though return 0.
		 */
    /* 如果是非等待retry方式则直接return 0结束 */
		if (flags & FAULT_FLAG_RETRY_NOWAIT)
			return 0;
		/* mmap_lock解锁 */
		mmap_read_unlock(mm);
    /* 如果是FAULT_FLAG_KILLABLE:
     * 终止信号唤醒进程解锁则通过wait_on_page_locked_killable
     * 这种方式避免(avoid)进程陷入不可中断的休眠导致进程mmap_lock too long 
     * 如果是非FAULT_FLAG_KILLABLE方式:
     * 进程会陷入TASK_UNINTERRUPTIBLE导致进程陷入长久的阻塞当中。
     */
		if (flags & FAULT_FLAG_KILLABLE)
			wait_on_page_locked_killable(page);
		else
			wait_on_page_locked(page);
		return 0;
	} else {
    /* 如果 fault过程不允许retry就必须尝试获取锁,尝试获取锁有两种方式:
     * flag是否设置FAULT_FLAG_KILLABLE
     * 如果为true则使用__lock_page_killable当获取锁失败后进程进入休眠状态,但是当获得SIG_KILL终止信号后唤醒进程检查是否解锁
     * 如果为false则使用__lock_page当获取锁失败后进程进入TASK_UNINTERRUPTIBLE不可中断休眠状态,直到page解锁后唤醒进程并获取该		 * page的锁
     */
		if (flags & FAULT_FLAG_KILLABLE) {
			int ret;

			ret = __lock_page_killable(page);
			if (ret) {
				mmap_read_unlock(mm);
				return 0;
			}
		} else
			__lock_page(page);
		return 1;
	}
}

**lock_page_or_retry锁的应用场景发生在fault过程当中的do_swap_page**过程。

/*
 * The default fault flags that should be used by most of the
 * arch-specific page fault handlers.
 */
#define FAULT_FLAG_DEFAULT  (FAULT_FLAG_ALLOW_RETRY | \
			     FAULT_FLAG_KILLABLE | \
			     FAULT_FLAG_INTERRUPTIBLE)

do_page_fault
|		unsigned int mm_flags = FAULT_FLAG_DEFAULT
|  	    mmap_read_trylock(mm);
|		fault = __do_page_fault(mm, addr, mm_flags, vm_flags, regs);
|		|		handle_mm_fault(vma, addr & PAGE_MASK, mm_flags, regs);
|		|		|		__handle_mm_fault(vma, address, flags);
|		|		|		|		handle_pte_fault
|  	    |		|		|		|		if (!pte_present(vmf->orig_pte))
|  	    |		|		|		|		|		do_swap_page(vmf);
|		|		|		|		|		|		|		locked = lock_page_or_retry(page, vma->vm_mm, vmf->flags);
|		|		|		|		|		|		|		...
|    	mmap_read_unlock(mm);
PG_referenced

系统运行中当硬件置位 PTE ACCESS 位,代表 page 被访问过,对page的描述就会设置PG_referenced所以总结:PG_referenced代表page被访问过,该flag在page的访问、迁移、回收等流程中起到至关重要的作用。

**1、PageReferenced:**判断page是否设置referenced标志,如果设置则说明该page被访问过。

2、__SetPageReferenced、SetPageReferenced:当page被访问时设置PG_referenced;区别__SetPageReferenced no-atomic操作,SetPageReferenced是atomic操作在多核心,并行流程当中发挥作用。 在实际应用当中通常使用SetPageReferenced

**3、ClearPageReferenced、TestClearPageReferenced:**清除PG_referenced标志,在page不使用或进行内存回收时就需要清理掉该标志,差异:ClearPageReferenced直接清理PG_referenced; TestClearPageReferenced首先test确认是否PageReferenced如果为true则ClearPageReferenced清理PG_referenced、如果test没有置位则直接return false;该函数目前只在内存回收流程当中使用。

**PG_referenced核心应用场景有两处源码:mark_page_accessed、page_referenced**理解这两处源码的设计对于理解page在系统中的流转迁移是至关重要的。

/*
 * 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)
{
	page = compound_head(page);
	
  /*
  *	当page没有被访问过时存在两种场景:
	* 1、如果page在inactive list当中那么page的初始状态就是inactive, unreferenced
	* 当page被访问后page状态演变成inactive, referenced,此时page从inactive链表的尾部迁移到inactive链表的头部
	* 对应注释当中:inactive,unreferenced	->	inactive,referenced
	*	2、如果page在active list当中那么page的初始状态就是active, unreferenced
	* 当page被访问后page状态演变成active, referenced, 此时page从active链表的尾部迁移到active链表头部
  * 对应注释当中:active,unreferenced		->	active,referenced
  */
	if (!PageReferenced(page)) {
		SetPageReferenced(page);
	} else if (PageUnevictable(page)) {
    /* LRU_UNEVICTABLE不可驱逐,该类型page在实际的使用过程当中是无法迁移、旋转;
     * 在内存回收阶段这类page是无法被回收的。*/
		/*
		 * Unevictable pages are on the "LRU_UNEVICTABLE" list. But,
		 * this list is never rotated or maintained, so marking an
		 * evictable page accessed has no effect.
		 */
	} else if (!PageActive(page)) {
		/*
		 * If the page is on the LRU, queue it for activation via
		 * lru_pvecs.activate_page. Otherwise, assume the page is on a
		 * pagevec, mark it active and it'll be moved to the active
		 * LRU on the next drain.
		 */
   	/* 
   	* PageReferenced为true且page在inactive list链表当中,此时page的初始状态inactive, referenced
   	*	当page被访问后page状态演变成active, unreferenced。此时page从inactive list的尾部迁移到active的头部
		* 对应注释中:inactive,referenced		->	active,unreferenced
		*/
		if (PageLRU(page))
			activate_page(page);
		else
			__lru_cache_activate_page(page);
		ClearPageReferenced(page);
		workingset_activation(page);
	}
	if (page_is_idle(page))
		clear_page_idle(page);
}

整个page的流转过程整体上如下:该图非常完善的描述了page在系统当中的流转迁移过程。
在这里插入图片描述
简洁的描述:page fault the new page inactive unreferenced -> inactive referenced -> active unreferenced -> active referenced

reclaim、migrate核心流程就是page_referenced决定page是否迁移、回收。进程内存是有多个vma组成的,每个vma当中的内存地址都可能影射同一个物理page,所以在rmap反向影射时就需要检查page对应的每个vma,确认没有任何的vma访问该page,最终决定是否释放内存。page_referenced的返回值的含义:指定page在所有vma虚拟内存空间中被访问过的次数(必须强调不是被引用过的次数,是引用后被访问过的次数也就是pte_young(pte)=true的次数)
在这里插入图片描述
**针对文件页来说就是VMA, 针对匿名页来说是AV -> AVC -> VMA -> PTE. **

/**
 * 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 we_locked = 0;
  /* 通过page_referenced获得page的核心属性信息page_mapcount,memcg */
	struct page_referenced_arg pra = {
		.mapcount = total_mapcount(page),
		.memcg = memcg,
	};
  /* 
  * rwc->rmap_one = page_referenced_one核心的回调函数,在rmap_walk(page, &rwc)
  * 中rmap_walk_anon、rmap_walk_file核心流程发挥重要作用。
  */
	struct rmap_walk_control rwc = {
		.rmap_one = page_referenced_one,
		.arg = (void *)&pra,
		.anon_lock = page_lock_anon_vma_read,
	};

	*vm_flags = 0;
  /* pra.mapcount = false 说明该page没有被任何进程访问过所以直接return 0(referenced为0)*/
	if (!pra.mapcount)
		return 0;
	/* 判断page是否建立影射关系应为文件页、匿名页的不同分别指向address_space、anon_vma
   * 如果没有建立影射关系则说明该page没有被访问过直接return 0 */
	if (!page_rmapping(page))
		return 0;

  /* 
  * is_locked在不同的流程是存在差异的,
  * 在shrink_activ_list流程当中page是没有被锁
  * 该流程当中针对page的reference检查就需要lock page,然后再做page_referenced检查
  * 主要针对文件页尝试获取锁,如果获取锁失败则说明该page正在被其他进程持有锁也就是该page正在
  * 被其他进程使用,这种状态下没有必要再进行referenced检查,对流程起到了优化作用,return 1
  * 说明该page正在被其他进程使用。
  * 如果获取锁成功则进行referenced检查,完成检查后需要unlock_page(page)对page进行解锁。
  * 
  * 在shrink_page_list流程当中page在回收阶段page已经被上锁,所以这里就不在尝试获取锁
  */
	if (!is_locked && (!PageAnon(page) || PageKsm(page))) {
		we_locked = trylock_page(page);
		if (!we_locked)
			return 1;
	}
  .....
	rmap_walk(page, &rwc);
	*vm_flags = pra.vm_flags;

	if (we_locked)
		unlock_page(page);

	return pra.referenced;
}

**page_referenced**核心流程rmap_walk根据文件页、匿名页的不同使用不同的方式进行反向的walk操作,这两者基本逻辑是相同的,细节部分根据匿名页、文件页的特性不同采用不同的方式。常规的套路如下:

rmap_walk_anon
在这里插入图片描述

static void rmap_walk_anon(struct page *page, struct rmap_walk_control *rwc, bool locked)
{
  ...
  /* anon page lock */
	anon_vma = rmap_walk_anon_lock(page, rwc);

	pgoff_start = page_to_pgoff(page);
	pgoff_end = pgoff_start + thp_nr_pages(page) - 1;
  /* AV -> AVC -> VMA -> PTE判断是否访问确定referenced状态 */
	anon_vma_interval_tree_foreach(avc, &anon_vma->rb_root,
			pgoff_start, pgoff_end) {
		...
		if (!rwc->rmap_one(page, vma, address, rwc->arg))
			break;
		...
	}
	/* anon page unlock */
	anon_vma_unlock_read(anon_vma);
}

rmap_walk_file
在这里插入图片描述

static void rmap_walk_file(struct page *page, struct rmap_walk_control *rwc, bool locked)
{
	...
  /* file page lock */
  i_mmap_lock_read(mapping);
	...
	pgoff_start = page_to_pgoff(page);
	pgoff_end = pgoff_start + thp_nr_pages(page) - 1;
	vma_interval_tree_foreach(vma, &mapping->i_mmap,
			pgoff_start, pgoff_end) {
		...
		if (!rwc->rmap_one(page, vma, address, rwc->arg))
			goto done;
		...
	}
	...
  /* file page unlock */
	i_mmap_unlock_read(mapping);
}

rmap_walk通用流程**page_referenced_one**完成referenced检查且每次rmap_walk检查一个page。page_referenced_one核心实现是 page_vma_mapped_walk遍历一个给定page在给定虚拟内存区域VMA中的映射,详细讲就是: 遍历给定VMA区域中address对应的所有页表项PTE, 并判断页表项PTE与给定page的关系, 如果PTE指向给定的page则return true说明该给定VMA与该给定的page存在映射关系。

bool page_vma_mapped_walk(struct page_vma_mapped_walk *pvmw)
{
	struct mm_struct *mm = pvmw->vma->vm_mm;
	struct page *page = pvmw->page;
	unsigned long end;
	pgd_t *pgd;
	p4d_t *p4d;
	pud_t *pud;
	pmd_t pmde;

  ...
	/* 判断page是否为复合页 */
	end = PageTransCompound(page) ?
		vma_address_end(page, pvmw->vma) :
		pvmw->address + PAGE_SIZE;
  /* 在通过mm,address反向查询对应page的pte之前先做一个检查
   * 如果pte已经存在就没有必要再使用address做pgd->p4d->pud->pmd,降低查询PTE过程当中的负载;
   * 如果pte不存在就需要按照常规流程使用address做pgd->p4d->pud->pmd查询VMA address对应的PTE;
   */
	if (pvmw->pte)
		goto next_pte;
restart:
	do {/* while (pvmw->address < end) */
		/* pgd->p4d->pud->pmd条目,但是如果没有条目则返回false */
		pgd = pgd_offset(mm, pvmw->address);
		p4d = p4d_offset(pgd, pvmw->address);
		pud = pud_offset(p4d, pvmw->address);
		pvmw->pmd = pmd_offset(pud, pvmw->address);
		...
    /* 通过pvmw->pmd、address计算出pvmw->pte
     * 如果获取pte失败则直接调整到next_pte,寻找下vma区域中下一个pte;
     * 如果获取pte成功则需要检查pte与特地page的关系,如果pte对应的就是该page则直接return true
     * 通过page_referenced_one流程检查pte是否被访问,这就是一个rmap反向查找的流程。
     */
		if (!map_pte(pvmw))
			goto next_pte;
this_pte:
    /* 用于判断在该vma区域中address对应的pte是否指向需要检查的特定page
     * check_pte(pvmw);
     * |	pfn = pte_pfn(*pvmw->pte);
     * |	pfn_is_match(pvmw->page, pfn);
     * |    |	page_pfn = page_to_pfn(page)
     * |	|	return page_pfn == pfn;
     * 如果该PTE指向该page说明该VMA与该page存在映射关系,该VMA使用了该page此时return true结束循环准备遍历下一个VMA
     */
		if (check_pte(pvmw))
			return true;
next_pte:
		do {
      	/* 以PAGE_SIZE对齐依次累加,对应的address指向的下一个PTE
      	 * 所以pvmw->pte++依次累加 */
        pvmw->address += PAGE_SIZE;
        if (pvmw->address >= end)
          	return not_found(pvmw);
				...
        pvmw->pte++;
				...
    /* pte_none判断pte是否为空:
     * 如果页表项为空, 则return true;
     * 如果页表项不为空, 则return false;
     * 用在此处就是要寻找一个存在的PTE然后跳出循环到this_pte通过check_pte(pvmw)确认pte是否指向特定的page 
     */
		} while (pte_none(*pvmw->pte));
		...
		goto this_pte;
    ...
	} while (pvmw->address < end);
	...
	return false;
}

**page_referenced_one**检查给定页面是否被引用并更新页面引用计数,判断给定page是否可以被迁移或回收,在内存回收流程中发挥重要作用。

static bool page_referenced_one(struct page *page, struct vm_area_struct *vma,
			unsigned long address, void *arg)
{
	struct page_referenced_arg *pra = arg;
	struct page_vma_mapped_walk pvmw = {
		.page = page,
		.vma = vma,
		.address = address,
	};
	int referenced = 0;

  /* 遍历给定VMA区域中address对应的所有页表项PTE, 
   * 并判断页表项PTE与给定page的关系,如果VMA虚拟内存区域当中没有PTE指向给定page则说明该VMA当中没有访引用该page
   * 直接跳出循环即可, 对下一个VMA区域再进行筛选 */
	while (page_vma_mapped_walk(&pvmw)) {
		address = pvmw.address;
		/* VM_LOCKED对应PG_mlocked, 
		 * 如果vma->vm_flags & VM_LOCKED = true;
		 * 说明此VMA对应的page被锁在系统当中无法被回收此时直接return false结束检索,减少不必要的检索流程
     */
		if (vma->vm_flags & VM_LOCKED) {
			page_vma_mapped_walk_done(&pvmw);
			pra->vm_flags |= VM_LOCKED;
			return false; /* To break the loop */
		}
		/* VMA虚拟内存区域虚拟内存地址页表项PTE指向该PAGE,说明该VMA引用该给定PAGE
     * 但是VMA是否访问了该PAGE本质上来讲是不确定的,这个时候就需要判断VMA所在进程是否真的
     * 访问了给定page;此时PTE young将发挥作用。
     * PTE_Young代表页面最近被访问过,通过该标记即可明确page是否被该vma访问,最终决定是否回收给定page。
     */
		if (pvmw.pte) {
      /* 
      * vma在引用给定page的基础上,是否访问给定page,如何判断page是否被访问则需要依赖PTE_Young标志
      * 通过ptep_test_and_clear_young判断并清除pte_young
      * 如果true:1、test pte young is true; 2、clear pte young flag;说明该页表项对用的page被vma访问过(在被引用的基础上)
      * 如果false:1、test pte young is false;说明该页表项对应的page没有被vma访问过(在被引用的基础上)
      */
			if (ptep_clear_flush_young_notify(vma, address,
						pvmw.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.
				 */
        /* 判断vma访问的模式:
         * 如果vma访问采用顺序读的方式则跳过referenced++ 
         * 这样做的优点是不打破顺序读的连续性,提高io读取的效率(顺序读性能 > 随机读性能)
         * 如果vma访问采用乱序读取方式则referenced++说明vma页表项pte对应的给定的page被访问过;
         */
				if (likely(!(vma->vm_flags & VM_SEQ_READ)))
					referenced++;
			}
		} else if (IS_ENABLED(CONFIG_TRANSPARENT_HUGEPAGE)) {
			if (pmdp_clear_flush_young_notify(vma, address,
						pvmw.pmd))
				referenced++;
		} else {
			/* unexpected pmd-mapped page? */
			WARN_ON_ONCE(1);
		}

		pra->mapcount--;
	}
	/* 当vma页表项pte对应的page被访问过时, 设置page idle状态表明该page此时没有被其他vma访问
   * 该过程给新的一轮page fault或页面的迁移做铺垫 */
	if (referenced)
		clear_page_idle(page);
	if (test_and_clear_page_young(page))
		referenced++;
	
  /* 完成全部VMA页表项pte检索后,统计出给定page在多少个vma虚拟内存区域中被访问 */
	if (referenced) {
    /* 给定page在全部vma页表项当中被访问的次数 */
		pra->referenced++;
		pra->vm_flags |= vma->vm_flags;
	}
	/* 如果完成所有的VMA虚拟内存区域, 
	 * 发现所有VMA页表项pte都没有访问给定page则直接return false:表示给定page没有任何vma或进程中被访问过(仅仅是存在引用关系) */
	if (!pra->mapcount)
		return false; /* To break the loop */
	/* 
	* 完成page_referenced_one 给定page的反向检查工作
  * 表现在page_referenced函数中return pra.referenced表示给定page被vma访问过的次数 */
	return true;
}

**perf火焰图采集kswapd运行过程可以看到page_referenced**存在较大的性能瓶颈, 优化这个流程对内存的回收效率将有较大的优化。
在这里插入图片描述

pte_young

用于检查页面表项(Page Table Entry,PTE)中的 PTE_YOUNG 标志位,该标志位表示页面被被访问过;pte_young是内存访问,内存回收流程的重要组成部分,通过对页面状态的判断做出最合理的操作。

pte_young:检查给定的 PTE 是否设置了 PTE_YOUNG 标志,页面状态年轻表示页面最近被访问过;

pte_mkyoung内存申请流程中page被访问时设置young,表明页面最近被访问过;

pte_mkold清除 PTE 的 PTE_YOUNG 标志,将页面标记为old。通常页面已经存在一段时间且没有被访问的情况下做清除动作。、

在内存回收流程中youngpage被保留在系统当中,page在系统中生命周期能够更长;没有被访问的page在内存回收流程当中会被尽快回收保证系统的正常运行。通常情况下页面YOUNG回被认为是HOT页。

handle_pte_fault
		/*do file/anonymous page fault*/
		pte_mkyoung 
PG_uptodate

文件访问是将文件从磁盘读取到内存的过程,该过程中内存存储的文件与磁盘原文件是否一致是至关重要; PG_uptodate 即用于描述page与磁盘数据一致性。

**1、SetPageUptodate、__SetPageUptodate:**从磁盘当中读取文件到内存当中,当完成数据读取后内存中的数据与磁盘中数据一致,此时设置PG_uptodate标志;当再次访问同一个磁盘文件时判断内存数据带有PG_uptodate说明数据一致且是最新的,就避免了再次读取而引入的IO问题。

**2、PageUptodate:**判断page页面数据是否与磁盘文件是否一致,如果一致说明page页面中的数据是最新的没有必要重新从磁盘当中读取避免了IO的产生。如果不是最新的数据就需要重新从磁盘中读取或者从内存中写回数据磁盘当中。

**3、ClearPageUptodate:**清理PG__uptodate标志,当页面数据与磁盘数据不一致此时就需要将PG_uptodate标志清除,以保证能够顺利进行数据的写回或者数据的重新加载。

SetPageUptodate应用场景:

generic_file_buffered_read
|		error = mapping->a_ops->readpage(filp, page);
|		f2fs_read_data_page
|		|		if (f2fs_has_inline_data(inode))
|       |		|		f2fs_read_inline_data
|		|		|		|		f2fs_do_read_inline_data
|		|		|		|		|		/* 如果为true则直接return=说明内存数据与磁盘数据一致且为最新数据则没有必要重新度取减小IO */
|		|		|		|		|		if (PageUptodate(page))
|		|		|		|		|			return;
|		|		|		|		|		/* Copy the whole inline data block */
|		|		|		|		|		memcpy_to_page(page, 0, inline_data_addr(inode, ipage), MAX_INLINE_DATA(inode));
|		|		|		|		|		if (!PageUptodate(page))
|		|		|		|		|			SetPageUptodate(page);
|		|		|		|		if (!PageUptodate(page))
|		|		|		|				SetPageUptodate(page);
|       |		f2fs_mpage_readpages
|		|		|		/* read file page */
|		|		|		...
|		|		|		if (!PageUptodate(page))
|		|		|			SetPageUptodate(page);
|		|		if (!PageUptodate(page))
|		|				SetPageUptodate(page);

在文件读取时为了保证数据的一致性, 文件读取操作后都会校验PageUptodate如果读取的页面还没有设置PG_uptodata则设置SetPageUptodate(page)表明该页面已经是最新的且内存中的数据与磁盘数据一致。在保护并发过程中PG_locked发挥重要作用。

针对匿名页存在相同的场景读取swap devices 交换设备流程:内存访问发生在page fault流程当中,针对匿名页的page fault当中do_swap_page即从swap device当中读取交换出去的匿名页,这个过程同样需要保持数据的一致性:读取成功后SetPageUptodate表明内存当中的数据与swap devices设备中数据一致。

do_swap_page
|		alloc_page_vma
|		swap_readpage
|		|		/* read page */
|		|		...
|		|		bio = get_swap_bio(GFP_KERNEL, page, end_swap_bio_read);
|		|		end_swap_bio_read
|		|		|		//如果读取失败,则可能是页面数据错误需要将清除PG_uptodate,需要重新读取
|		|		|		if (bio->bi_status) {
|		|		|				SetPageError(page);
|		|		|				ClearPageUptodate(page);
|		|		|		}
|		|		|		SetPageUptodate(page);//读取成功, 设置PG_uptodate表明page数据与磁盘数据一致的是最新的

PageUptodate应用场景:

文件读取过程中当PageUptodate = true说明该page数据是最新的、与磁盘数据是一致的,在这种场景下就无需从磁盘当中重新加载文件,这样就减少了文件读取过程中的IO;

generic_file_buffered_read
|		error = mapping->a_ops->readpage(filp, page);
|		f2fs_read_data_page
|		|		if (f2fs_has_inline_data(inode))
|       |		|		f2fs_read_inline_data
|		|		|		|		f2fs_do_read_inline_data
|		|		|		|		|		/* 如果PageUptodate(page) = true说明该page存储的数据是最新的是与磁盘数据一致的,
|		|		|		|		|		 * 所以就没有必要重新从磁盘当中读取数据,这样就减少了IO的产生 */
|		|		|		|		|		if (PageUptodate(page))
|		|		|		|		|			return;

ClearPageUptodate应用场景:

常见的用法就是文件读取失败后无论page之前是否被设置PG_uptodate都会做清除动作,在下一轮的数据读取过程当中需要重新从磁盘当中读取该数据。

swap devices读取:

do_swap_page
|		alloc_page_vma
|		swap_readpage
|		|		/* read page */
|		|		...
|		|		bio = get_swap_bio(GFP_KERNEL, page, end_swap_bio_read);
|		|		end_swap_bio_read
|		|		|		//如果读取失败,则可能是页面数据错误需要将清除PG_uptodate;
|		|		|		if (bio->bi_status) {
|		|		|				SetPageError(page);
|		|		|				ClearPageUptodate(page);

f2fs文件读取:

static void __read_end_io(struct bio *bio, bool compr, bool verity)
{
	struct page *page;
	struct bio_vec *bv;
	struct bvec_iter_all iter_all;

	bio_for_each_segment_all(bv, bio, iter_all) {
		page = bv->bv_page;
		...
		/* PG_error was set if any post_read step failed */
		if (bio->bi_status || PageError(page)) {
			ClearPageUptodate(page);
			/* will re-read again later */
			ClearPageError(page);
		} else {
			SetPageUptodate(page);
		}
		dec_page_count(F2FS_P_SB(page), __read_io_type(page));
		unlock_page(page);
	}
}
PG_dirty

page中存放的数据如果被进程修改且page修改的内容还未写回到磁盘该page状态就是Dirty,此时page存储的数据与磁盘不一致,在内存回收过程当中如果需要回收这类page就需要将修改的数据写回到磁盘当中。在内存管理、文件系统写操作中作用是至关重要的。

**1、PageDirty:**判断page状态是否为脏页,在内存回收流程中需要将数据写回到磁盘当中保证数据的一致性,对于内存回收有一定的筛选作用。

**2、SetPageDirty、TestSetPageDirty:**设置页面标记为脏页,表示页面内容已被修改,需要将数据写回到磁盘;TestSetPageDirty返回值true表示页面为脏页不需要再做SET操作、返回值false表示页面不为脏页需要SET设置为 脏页。主要的应用场景就是文件系统的写操作当中

**3、ClearPageDirty、TestClearPageDirty:**表示页面数据已经被成功写回磁盘不再是脏页,清除页面的脏状态;TestClearPageDirty

返回值true表示页面为脏页现在清除PG_dirty标志、返回值false表示页面不为脏页无需清理标志。在内存回收、页面写回场景当中当数据成功完成写回后使用 Clear dirty 来更新页面状态

vfs_write
|		new_sync_write
|		|		call_write_iter
|  	    |		|		f2fs_file_write_iter
|  	    |		|		|		__generic_file_write_iter
|		|		|		|		|		/* Copying data from user space to the kernel space file buffer 
|		|		|		|		|		 * (or directly into the page cache), 
|		|		|		|		|		 * potentially marking the page as dirty so that it can be written back to disk later. */
|  	    |		|		|		|		generic_perform_write
|		|		|		|		|		|   ...
|		|		|		|		|		|		f2fs_write_end //write data to page/swap cache
|		|		|		|		|		|		|		set_page_dirty(page);//TestSetPageDirty
|		|		|		|		|		/* Initiating write-back operations for all dirty pages within a specified file area, 
|		|		|		|		|		* and waiting for these write-back operations to complete. */				
|		|		|		|		|	  filemap_write_and_wait_range
|		|		|		|		|		|		__filemap_fdatawrite_range
|		|		|		|		|		|		|		do_writepages
|		|		|		|		|		|		|		|		f2fs_write_data_pages
|		|		|		|		|		|		|		|		|		__f2fs_write_data_pages
|		|		|		|		|		|		|		|		|		|		f2fs_write_cache_pages
|		|		|		|		|		|		|		|		|		| 	|		...
|		|		|		|		|		|		|		|		|		| 	|		if (PageWriteback(page))
|		|		|		|		|		|		|		|		|		| 	|				f2fs_wait_on_page_writeback
|		|		|		|		|		|		|		|		|		|		|		/* Clearing the PG_dirty flag after data is written to disk. */
|		|		|		|		|		|		|		|		|		|		|		clear_page_dirty_for_io //TestClearPageDirty
PG_writeback

page存储数据正在从内存写回到磁盘时设置该标志;适用于页面为脏页(PG_dirty)写回到磁盘的过程。

1、PageWriteback:判断页面是否正在将数据写回到磁盘,也就是判断该page正在进行写回操作

**2、TestSetPageWriteback:**表示页面开始写回操作时(页面写回操作前),为确保页面的写回状态设置PG_writeback标志;返回值true表示页面正在写回不需要再做SET操作、返回值false表示页面状态此时不是写回状态则需要SET设置为写回状态。

**3、TestClearPageWriteback:**表示页面数据写回到磁盘的操作已经完成,完成写回操作后清理PG_writeback标志;返回值true表示page之前处于写回状态现在已经完成写回清理PG_writeback标志、返回值false表示page状态不是写回状态无需做任何操作;

PG_writeback应用场景大体上两类:

**1、脏页写回:**在周期性写回操作流程中,将脏页写回到磁盘。在写回操作开始时设置 PG_writeback 标志位,完成写回后清除该标志位

**2、内存回收:**根据 PG_writeback 标志位来判断page是否可以被回收。如果页面正在写回,需等待page写回完成后才能回收该page。

set_page_writeback、test_clear_page_writeback封装设置、清除PG_writeback标志函数。

filemap_write_and_wait_range
|		__filemap_fdatawrite_range
|		|		do_writepages
|		|		f2fs_write_data_pages
|		|		|		__f2fs_write_data_pages
|		|		|		|		f2fs_write_cache_pages
|		|		|		| 	    |		...
|		|		|		|		|		/* page正在写回到磁盘,休眠等待page完成写回操作数据被写入到磁盘当中,PG_writeback标志位被清除
|		|		|		|		|		* 如果为false说明该page还没有进行写回操作则继续向下运行 */
|		|		|		| 	    |		if (PageWriteback(page))
|		|		|		| 	    |				f2fs_wait_on_page_writeback
|		|		|		|		|		...
|		|		|		|		|		/* Data pages are written from memory to the flash storage device. */
|		|		|		|		|		f2fs_write_single_data_page
|		|		|		|		|		|		f2fs_do_write_data_page /* Writing memory data to the disk. */
|		|		|		|		|		|		|		set_page_writeback  /* 内存数据写回磁盘之前设置PG_writeback表明page需要写回 */
|		|		|		|		|		|		|		f2fs_inplace_write_data /* 开始内存写回 */
|		|		|		|		|		|		|		|		f2fs_submit_page_bio /* 提交BIO */
|		|		|		|		|		|		|		|		|		__bio_alloc
|		|		|		|		|		|		|		|		|		|		f2fs_write_end_io /* 等带写回完成 */
|		|		|		|		|		|		|		|		|		|		|		end_page_writeback /* 写回结束后清除PG_writeback标志完成流程闭环 */
|		|		|		|		|		|		|		|		|		|		|		|		test_clear_page_writeback /* 清除PG_wrireback标志 */
PG_reclaim

page在内存回收过程当中能被尽快回收,在 shrink_page_list内存回收流程中尽快对PageReclaim进行回收释放可用内存。

**1、PageReclaim:**判断page是否正在被回收

**2、SetPageReclaim:**标记页面需要尽快的被回收

**3、ClearPageReclaim:**表明页面不需要被回收或者已经被回收。

应用的场景主要分布在内存回收、内存页面写回过程:本质上来讲核心的操作是内存回写,在内存回收过程中需要将脏页回写到磁盘当中,此时设置PG_writeback因为整个流程发生在内存回收阶段所以为了有所区分设置PG_reclaim标签标明page正在被回收;在新一轮的内存回收过程中检测到PageReclaim则表明page正在被回收且正在做写回操作,这样就不在重复检测该page的状态,降低重复扫描的操作提高内存的回收效率。在应用上总结就是:避免重复扫描、优先回收其他页面达到提高内存回收效率的作用。在**shrink_page_list**

内存回收中判断**PageDirty时需要将缓存的数据写回到磁盘当中就需要使用pageout**完成数据写回;在内存回收过程中调用pageout流程中需要进行数据写回时先SetPageReclaim,调用writepage完成数据写回,当数据写回后ClearPageReclaim清除PG_reclaim标志。

static pageout_t pageout(struct page *page, struct address_space *mapping)
{
	...
	if (clear_page_dirty_for_io(page)) {
		...
		SetPageReclaim(page);
    /*
    * set_page_writeback
    * Writing memory data to the disk
    * test_clear_page_writeback
    */
		res = mapping->a_ops->writepage(page, &wbc);
		...
    /* Clear the writeback flag after the writeback is completed. */
		if (!PageWriteback(page)) {
			/* synchronous write or broken a_ops? */
			ClearPageReclaim(page);
		}
		...
		return PAGE_SUCCESS;
	}
	...
	return PAGE_CLEAN;
}
PG_unevictable

表示该页面不可驱逐,在内存回收中页面不能被回收。标志常用于特殊page的内存管理当中或者mlock系统调用锁定内存等场景当中。

**1、PageUnevictable:**判断该页面是否为不可驱逐页面,如果是内存回收过程中就需要skip跳过,无法回收所以不做多余的扫描。

**2、SetPageUnevictable:**在内存申请时如果使用场景特殊则设置PG_unevictable表明该page无法被回收或交换。

**3、ClearPageUnevictable、TestClearPageUnevictable:**页面不需要被保护则通过清除PG_unevictable,页面将能正常交换或者回收

**PG_unevictable**最常见的用法时mlock锁定页面,对应的page则被设置PG_unevictable举个例子如下:

mlock
|		do_mlock
|		|		apply_vma_lock_flags
|		|		|		mlock_fixup
|		|		|		|		munlock_vma_pages_range
|		|		|		|		|		follow_page
|		|		|		|		|		__munlock_pagevec
|		|		|		|		|		|		__munlock_isolated_page
|		|		|		|		|		|		|		putback_lru_page
|		|		|		|		|		|		|		|		lru_cache_add
|		|		|		|		|		|		|		|		|		__pagevec_lru_add
|		|		|		|		|		|		|		|		|		|		__pagevec_lru_add_fn
|		|		|		|		|		|		|		|		|		|		|		SetPageUnevictable(page);

基于源码mlock锁定的page最终就会被设置PG_unevictable,proc/meminfo也能佐证以上分析,看到Unevictable、Mlocked是相同的。

OP5287:/ $ cat /proc/meminfo | grep -E "Mlocked|Unevictable"                                                    Unevictable:      217212 kB
Mlocked:          217212 kB

内存回收过程需要从inactive list迁移可以回收的page到page_list当检测到page是不可驱逐类型时skip跳过该page,避免被回收。

shrink_inactive_list
|		isolate_lru_pages
|		|		__isolate_lru_page
|		|		|		if (PageUnevictable(page) && !(mode & ISOLATE_UNEVICTABLE))
|		|		|				return ret;
PG_mlocked

表明页面已经被 mlock() 系统调用锁定。当一个页面被标记为 PG_mlocked表明数据是关键的必须保存在系统当中,所以即便是在内存回收流程中该页面无法被交换或回收。

**1、PageMlocked:**判断页面是否被mlock锁定,如果锁定则page无法被回收。

**2、TestSetPageMlocked:**设置PG_mlcoked标志锁定页面

**3、TestClearPageMlocked:**清除PG_mlocked标志解锁页面,在内存回收过程中能够回收或交换内存。

**PG_mlocked通过mlock等系统调用设置,当进程内存关键数据被锁定,在内存回收流程中就可以避免关键数据被回收;当锁定内存使用完毕通过munlock**完成内存的解锁,内存解锁后page可以进行正常的回收或交换。

mlock
|		do_mlock
|		|		apply_vma_lock_flags
|		|		|		mlock_fixup
|		|		|		|		munlock_vma_pages_range
|		|		|		|		|		follow_page
|		|		|		|		|		|		follow_page_mask
|		|		|		|		|		|		|		follow_p4d_mask
|		|		|		|		|		|		|		|		follow_pud_mask
|		|		|		|		|		|		|		|		|		follow_pmd_mask
|		|		|		|		|		|		|		|		|		|		follow_page_pte
|		|		|		|		|		|		|		|		|		|		|		mlock_vma_page
|		|		|		|		|		|		|		|		|		|		|		|		TestSetPageMlocked(page)
PG_swapbacked

表明内存页面page可以被交换到其他设备;内存page分为不同的类型:匿名页、文件页;在不杀死进程的情况下匿名页的回收只能先将内存中的数据交换到磁盘设备当中,然后再完成内存页面的回收;设置PG_swapbacked就表明该匿名页数据支持交换。

**1、PageSwapBacked:**检查PG_swapbacked确认该匿名页面是否支持内存与磁盘交换操作,在内存回收当中有较多应用。

**2、__SetPageSwapBacked:**设置PG_swapbacked标志该内存页面支持与磁盘交换操作,应用在do_anonymous_page、do_swap_page

匿名页的page fault流程当中

**3、ClearPageSwapBacked:**清除PG_swapbacked标志表明page不支持内存磁盘交换操作或page已经完成内存与磁盘的交换操作。

**PG_swapbacked**是针对匿名页而设计的,我们认为应用的执行是离不开匿名页的,在内存回收过程当中就不能像文件页的回收那样将脏数据写回到磁盘当中后直接回收。因此在内存回收的流程当中我们会创建一个磁盘设备zram、swapfile,在回收匿名页时将数据交换到磁盘当中保存起来,当再次使用到改数据时重新从磁盘当中将所需数据重新读区到内存当中。基于这样的设计在anonymous page申请时给申请成功的匿名页设置PG_swapbacked标志,为后续anonymous page内存回收做好铺垫。

handle_pte_fault
|		do_anonymous_page
|		|		alloc_zeroed_user_highpage_movable
|		|		page_add_new_anon_rmap
|		|		|		__SetPageSwapBacked(page)
|		do_swap_page
|		|		alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address)
|		|		__SetPageSwapBacked(page);

匿名页page fault流程中针对新申请的page就需要设置PG_swapbacked标志。该标志在内存回收阶段将发挥作用:将匿名页内存数据交换到swap device中并完成页面的内存回收。通过**lazyfree**模式完成内存回收后清除ClearPageSwapBacked标志http://lkml.kernel.org/r/2f87063c1e9354677b7618c647abde77b07561e5.1487965799.git.shli@fb.com

shrink_page_list
|		if (PageAnon(page) && PageSwapBacked(page) && !PageSwapCache(page))
|		|		add_to_swap
|		|		|		get_swap_page
|		|		|		add_to_swap_cache
PG_swapcache

用于标记page是否在交换设备的缓存当中(swap cache),匿名页与文件页类似的:文件页对应的是page cache,匿名页是swap cache。当应用需要重新读取swap device数据时因为page存储的数据在swap cache当中,此时数据读取的对象从原有的swap devices变成了swap cache,提高了swapin的效率,避免读取swap devices产生IO的问题。

**1、PageSwapCache:**判断page是否正在交换到swap cache当中,主要应用在匿名页的内存回收、匿名页page fault过程发挥作用。

**2、SetPageSwapCache:**设置PG_swapcache标志,表明要将该匿名页交换到swap devices cache 当中。

**3、ClearPageSwapBacked:**匿名页完成交换动作后,对应的匿名页已经被交换到swap cache中,此时清除PG_swapcache标志。

在内存回收阶段需要回收匿名页时需要将匿名页数据交换到swap device设备当中,首先是将数据交换到swap cache当中然后再将数据写回到swap device当中(写回到磁盘当中)。可以看到如果该page已经在swap cache当中则不在做交换操作,提高swapin效率减少IO。

shrink_page_list
|		/* page是匿名页,支持swap交换功能且该page不在swap cache当中 */
|		if (PageAnon(page) && PageSwapBacked(page) && !PageSwapCache(page))
|		|		add_to_swap
|		|		|		get_swap_page
|		|		|		add_to_swap_cache
|		|		|		|		SetPageSwapCache
|		|		|		|		/* swap anon page to swap devices cache */
|		|		|		|		...
|		|		|		|		ClearPageSwapCache

三、总结

基于对page flags的梳理,可以看到系统当中page是有各种类型、各种状态的;在内存管理当中为保持数据的一致性,安全性等针对不同的场景、流程等对page进行最恰当的处理。如果要凝练的描述Linux内存管理的核心理念:**对不同类型的不同状态的page作出最恰当处理的过程这就是Linux内存管理。**理解不同状态的page对于我们的内存系统管理有重要的指导意义。

四、性能思考

PG_uptodate

  1. 标志的核心作用在于判断page页面存储的数据是否为最新的、是否与磁盘数据保持一致; PG_uptodate能很好的保证内存数据与磁盘数据的一致性,在文件访问、分布式存储等众多场景存在重要意义。
  2. 扩展:在文件读取加载过程中当page页面数据与磁盘数据一致则数据无需重复读取加载,这样就降低了 IO、降低了文件访问的网络延迟等等。

思考:在memory reclaim内存回收流程*PG_uptodate*要如何处理?

页面状态是复杂的,不能单纯的通过一个PG_uptodate决定是否回收该page,通过PG_dirty、PG_writeback等flag综合判定是否可以被回收,如果被回收的页面没有PG_uptodate则不做而外处理,如果被回收的页面有PG_uptodate则清除该标志为下一轮的内存访问做好铺垫,保证数据的一致性。

PG_dirty、PG_writeback

PG_dirty、PG_writeback主要应用在文件系统当中,文件被写入修改后则page变成脏页,为了保证数据的一致性则需要将数据同步到磁盘文件当中这就是写回的过程。把内存理解成一个缓存内存数据被修改,但是这个修改仅仅局限在内存当中(cache)此时page状态就演变为脏则设置PG_dirty;被修改后的数据通常需要同步到磁盘当中这就需要写回操作,通常写回操作是异步的为了判断page是否完成写回则设置PG_writeback,完成写回操作后数据被保存到磁盘当中则清除PG_writeback; 完成数据写回操作后页面数据与磁盘数据保持一致内存页面不再为脏所以就需要清除PG_dirty标志完成整个写入链路。

通常来讲:PageWriteBack(page) = true则该page一定被设置PG_dirty,因为如果不是脏页说明数据是一致的没有必要写回;反之这个过程并不成立一个page可能只是脏页,但是这个脏页可能没有必要写回所以就不需要做写回操作。

PG_mlocked、PG_unevictable

在特殊场景当中page需要保护在内存当中不能被回收,这类页面需要设置PG_unevictablemlock仅仅是众多不可驱逐page的一部分,如果系统应用中除去mlcok没有其他特殊应用则PG_mlocked = PG_unevictable。在内存回收流程当中要skip跳过这两类page提高内存的扫描效率,提高内存的回收效率。

/**
 * page_evictable - test whether a page is evictable
 * @page: the page to test
 *
 * Test whether page is evictable--i.e., should be placed on active/inactive
 * lists vs unevictable list.
 *
 * Reasons page might not be evictable:
 * (1) page's mapping marked unevictable
 * (2) page is part of an mlocked VMA
 *
 */
static inline bool page_evictable(struct page *page)
{
	bool ret;

	/* Prevent address_space of inode and swap cache from being freed */
	rcu_read_lock();
	ret = !mapping_unevictable(page_mapping(page)) && !PageMlocked(page);
	rcu_read_unlock();
	return ret;
}

PG_swapbacked、PG_swapcache

  1. 对于匿名页**PG_swapbacked、PG_swapcache**是互补的、是递进的;当page fault从buddy当中申请一个新的page时,新的匿名页设置PG_swapbacked表明该page可以被交换到swap devices设备当中,在内存回收阶段为了提高读取性能优先将匿名页交换到swap cache当中,其次才会将swap cache数据写回到swap device磁盘当中。
  2. PG_swapbacked 描述的是匿名页的属性表明匿名页支持交换功能,可以将匿名页交换到swap devices设备当中;**PG_swapcache**则描述的是状态,判断该page是否在swap cache交换设备缓存当中;

这二者是一个递进的关系,所以在常规的应用当中我们可以下一个定论:当页面被设置为PG_swapcache时,通常也会被设置为PG_swapbacked,因为交换缓存中的页面必定是可以交换的。

五、参考

https://www.kernel.org/doc/gorman/html/understand/understand005.html

https://www.kernel.org/doc/html/latest/mm/unevictable-lru.html

https://lwn.net/Articles/75198/

https://lwn.net/Articles/23732/

https://lwn.net/Articles/383162/

  • 18
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值