[内核内存] [arm64] 内存回收4—shrink_node函数详解

  • Post author:
  • Post category:其他




1 shrink_node函数

shrink_node内存回收的核心函数,用于扫参数pgdat内存节点中所有的可回收页面,并进行回收处理。

static bool shrink_node(pg_data_t *pgdat, struct scan_control *sc)
{
	struct reclaim_state *reclaim_state = current->reclaim_state;
	unsigned long nr_reclaimed, nr_scanned;
	bool reclaimable = false;
	/*
	 *两层循环,外层循环每次是对一个节点进行一次lru页面扫描和回收处理,内部循环则是对
	 *节点所有的子系统分步进行lru页面扫描和回收处理
	 *1.外层循环,每次循环都会将pgdat对应的内存节点进行一次页面的扫描和回收处理
	 *2.外层循环结束条件是:通过should_continue_reclaim函数来判断.
	 *	a.若本节点已回收的页面数sc->nr_reclaimed小于(2 << sc->order)且节点非活跃lru链表上的页总数大于(2 << sc->order),则
	 *	  should_continue_reclaim函数返回true,继续对该节点进行扫描回收操作
	 *  b.若本节点已回收的页面数sc->nr_reclaimed大于等于(2 << sc->order)或者非活跃lru链表上的页总数小于等于(2 << sc->order)时,用
	 *    compaction_suitable函数来判断该节点上是否有适合做内存规整的zone区域,若有则可以退出本节点的内存回收操作,将希望交托给内存规整。
	 */
	do {
		struct mem_cgroup *root = sc->target_mem_cgroup;
		struct mem_cgroup_reclaim_cookie reclaim = {
			.pgdat = pgdat,
			.priority = sc->priority,
		};
		unsigned long node_lru_pages = 0;
		struct mem_cgroup *memcg;

		nr_reclaimed = sc->nr_reclaimed;
		nr_scanned = sc->nr_scanned;

		memcg = mem_cgroup_iter(root, NULL, &reclaim);
		/*
		 *遍历本节点的memory cgroup,对每个子系统进行可回收页扫描和回收(默认一个memory cgroup系统):
		 *	1.调用shrink_node_memcg回收子系统页面
		 *  2.调用shrink_slab来回收子系统slab对象
		 */
		do {
			unsigned long lru_pages;
			unsigned long reclaimed;
			unsigned long scanned;

			if (mem_cgroup_low(root, memcg)) {
				if (!sc->may_thrash)
					continue;
				mem_cgroup_events(memcg, MEMCG_LOW, 1);
			}

			reclaimed = sc->nr_reclaimed;
			scanned = sc->nr_scanned;
			//对对应的memcg内存子系统进行页扫描和可回收页回收操作
			shrink_node_memcg(pgdat, memcg, sc, &lru_pages);
			node_lru_pages += lru_pages;
			//针对本memcg内存子系统,调用其自己注册的sharker相关接口来回收子系统专属slab对象
			if (memcg)
				shrink_slab(sc->gfp_mask, pgdat->node_id,
					    memcg, sc->nr_scanned - scanned,
					    lru_pages);

			/* Record the group's reclaim efficiency */
			/*
			 *通过本次对memcg内存子系统扫描的页数(sc->nr_scanned - scanned)和回收的页
			 *数(sc->nr_reclaimed - reclaimed)的比例值,来判断memcg对应内存子系统的当前
			 *内存压力
			 */
			vmpressure(sc->gfp_mask, memcg, false,
				   sc->nr_scanned - scanned,
				   sc->nr_reclaimed - reclaimed);

			/*
			 * Direct reclaim and kswapd have to scan all memory
			 * cgroups to fulfill the overall scan target for the
			 * node.
			 *
			 * Limit reclaim, on the other hand, only cares about
			 * nr_to_reclaim pages to be reclaimed and it will
			 * retry with decreasing priority if one round over the
			 * whole hierarchy is not sufficient.
			 */
			//本次回收为非全局回收状态(节点),且已回收页数超过期望回收页面数,直接退出循环结束扫描
			if (!global_reclaim(sc) &&
					sc->nr_reclaimed >= sc->nr_to_reclaim) {
				mem_cgroup_iter_break(root, memcg);
				break;
			}
		} while ((memcg = mem_cgroup_iter(root, memcg, &reclaim)));

		/*
		 * Shrink the slab caches in the same proportion that
		 * the eligible LRU pages were scanned.
		 *在节点全局回收状态下,也就是系统没有使能CONFIG_MEMCG,以整个节点为视图,对该节点的slab obj进行扫描和回
		 *收处理
		 */
		if (global_reclaim(sc))
			shrink_slab(sc->gfp_mask, pgdat->node_id, NULL,
				    sc->nr_scanned - nr_scanned,
				    node_lru_pages);

		if (reclaim_state) {
			sc->nr_reclaimed += reclaim_state->reclaimed_slab;
			reclaim_state->reclaimed_slab = 0;
		}

		/* Record the subtree's reclaim efficiency */
		//整个节点目前内存压力估计
		vmpressure(sc->gfp_mask, sc->target_mem_cgroup, true,
			   sc->nr_scanned - nr_scanned,
			   sc->nr_reclaimed - nr_reclaimed);

		if (sc->nr_reclaimed - nr_reclaimed)
			reclaimable = true;

	} while (should_continue_reclaim(pgdat, sc->nr_reclaimed - nr_reclaimed,
					 sc->nr_scanned - nr_scanned, sc));//本节点回收结束的标志

	/*
	 * Kswapd gives up on balancing particular nodes after too
	 * many failures to reclaim anything from them and goes to
	 * sleep. On reclaim progress, reset the failure counter. A
	 * successful direct reclaim run will revive a dormant kswapd.
	 */
	if (reclaimable)//本次内存回收操作,回收到了内存页面,表明回收成功
		pgdat->kswapd_failures = 0;

	return reclaimable;
}



1.1 shrink_node_memcg

shrink_node_memcg是基于内存节点的页面回收函数。当该节点的memory controller未被使能,则该函数是对整个节点进行内页面回收操作,而当该节点的memory controller被使能,则该函数是对节点的memcg对应的内存子系统进行页面回收。


PS:本章介绍默认节点的memory controller未被使能

/*
 * This is a basic per-node page freer.  Used by both kswapd and direct reclaim.
 *param:
 * (1) pgdat    --->页面回收的内存节点
 * (2) memcg    --->若节点的memory controller使能,则在该节点的memcg这一内存子系统上进行页面回收
 * (3) sc       --->本次页面回收的控制器
 * (4) lru_pages--->本次内存回收已经扫描的页面数量
 *默认该节点的memory controller未使能
 */
static void shrink_node_memcg(struct pglist_data *pgdat, struct mem_cgroup *memcg,
			      struct scan_control *sc, unsigned long *lru_pages)
{
	//获取本节点的lru链表集合(若节点的memory controller使能,则是获取本节点memcg的lru链表集合)
    struct lruvec *lruvec = mem_cgroup_lruvec(pgdat, memcg);
    //nr数组记录本节点指定lru链表中还有多少页面需要被扫描
	unsigned long nr[NR_LRU_LISTS];
	unsigned long targets[NR_LRU_LISTS];
	unsigned long nr_to_scan;
	enum lru_list lru;
	unsigned long nr_reclaimed = 0;
	unsigned long nr_to_reclaim = sc->nr_to_reclaim;
	struct blk_plug plug;
	bool scan_adjusted;

	/*
	 *该函数时利用proc接口的swappiness和sc->priotity来计算本节点4个lru链表中分别应该扫描的页面数量,结果保存在nr数组中,用链表类型来进行索
	 *引(enum lru_list)
	 */
    get_scan_count(lruvec, memcg, sc, nr, lru_pages);

	/* Record the original scan target for proportional adjustments later */
    //targets记录nr的原始数据
	memcpy(targets, nr, sizeof(nr));

	/*
	 *对直接内存回收做的一个优化处理(目的让直接内存回收保持回收状态,多回收一些页面):
     *	因内存紧张触发了直接内存回收,若此时回收状态是全局内存回收(未使能CONFIG_MEMCG),且内存回收优先级处于DEF_PRIORITY状态。这时我们可
     *	以让直接内存回收机制保持在页面回收的状态,以此来多回收一些页,目的是防止kswap线程未能将需要回收的页回收完全。
	 */
	scan_adjusted = (global_reclaim(sc) && !current_is_kswapd() &&
			 sc->priority == DEF_PRIORITY);

	blk_start_plug(&plug);
   /*
	*对该节点所有的lru链表进行遍历,并通过shrink_list对完成对节点页面的扫描和回收操作:
	*	通过while循环结束的条件可以知:对该节点的页面回收主要处理的是节点的不活跃匿名页面,不活跃文件页面和活跃文件页面
	*循环对本节点进行页面扫描和回收操作,直到回收足够多的页面为止(3个lru链表中待扫描页面数都为0时)。
	*	(1)每次循环都会用shrink_list函数对节点的每个lru链表进行扫描回收处理(每次扫描的页框数通过nr数组维护).
	*    (2)通过while循环结束的条件可以知:对该节点的页面回收主要处理的是节点的不活跃匿名页面,不活跃文件页面和活跃文件页面
	*/
	while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] ||
					nr[LRU_INACTIVE_FILE]) {
		unsigned long nr_anon, nr_file, percentage;
		unsigned long nr_scanned;

		/*
		 *对该节点中所有的lru链表进行遍历,每次循环处理一种lru链表:
		 *	对于每次循环,调用shrink_list函数对指定的lru链表进行页面扫描和回收,每次扫描链表中的nr_to_scan个页框并将这些页框中可回收的页
		 *	面进行回收处理.shrink_list函数会返回本次扫描过程中回收的页面数量,赋值给nr_reclaimed.
		 */
        for_each_evictable_lru(lru) {
			if (nr[lru]) {
                //shrink_list函数回收该链表页面时,扫描的页面数nr_to_scan(不能超过32个页面)
				nr_to_scan = min(nr[lru], SWAP_CLUSTER_MAX);
                //更新nr数组数据,记录对应链表还需要扫描多少个页
				nr[lru] -= nr_to_scan;
				//调用shrink_list函数对指定lru链表进行页面回收,并更新页面回收总数nr_reclaimed
				nr_reclaimed += shrink_list(lru, nr_to_scan,
							    lruvec, sc);
			}
		}
		//页面回收执行太久,主动让出cpu,防止其他任务挂起过久
		cond_resched();
		/*
		 *若本次页面回收没有达到回收要求或者前期设置了scan_adjusted,都会继续对该节点进行页面回收操作.其中scan_adjusted被设置是一种优化手
		 *段,是为了回收更多页满足kswap页面回收不足的情况。
		 */
		if (nr_reclaimed < nr_to_reclaim || scan_adjusted)
			continue;
        
		/*
		 *完成本轮节点页面的回收指标,但后续仍然需要继续对节点进行扫描和回收操作,会通过对当前节点每个lru链表中剩余页面的数量和每个链表还需
		 *要被扫描页面数等数据的分析,来对后续本节点页面的回收策略做一个调整。
		 *(1)比较当前lru链表中匿名页面的数量和文件页面的数量,后续将扫描权重偏向于页面数量较多的lru链表。
		 *(2)根据已经完成扫描的页面数量和原本待扫描页面的数量,计算每种类型lru页面扫描覆盖率,最后通过该覆盖率重新计算每种lru链表待扫描的
		 *   页面数量(更新nr数组)
		 */  
        //nr_file表示还需要扫描多少文件页,而nr_anon记录还需要扫描多少匿名页
		nr_file = nr[LRU_INACTIVE_FILE] + nr[LRU_ACTIVE_FILE];
		nr_anon = nr[LRU_INACTIVE_ANON] + nr[LRU_ACTIVE_ANON];
		
        //文件页或匿名页的扫描指标完成,退出循环,不一定完成了回收指标
		if (!nr_file || !nr_anon)
			break;

		if (nr_file > nr_anon) {
			unsigned long scan_target = targets[LRU_INACTIVE_ANON] +
						targets[LRU_ACTIVE_ANON] + 1;
			lru = LRU_BASE;
			percentage = nr_anon * 100 / scan_target;
		} else {
			unsigned long scan_target = targets[LRU_INACTIVE_FILE] +
						targets[LRU_ACTIVE_FILE] + 1;
			lru = LRU_FILE;
			percentage = nr_file * 100 / scan_target;
		}      
		/* Stop scanning the smaller of the LRU */
		nr[lru] = 0;
		nr[lru + LRU_ACTIVE] = 0;
        /*
		 * Recalculate the other LRU scan count based on its original
		 * scan target and the percentage scanning already complete
		 */
		lru = (lru == LRU_FILE) ? LRU_BASE : LRU_FILE;
		nr_scanned = targets[lru] - nr[lru];
		nr[lru] = targets[lru] * (100 - percentage) / 100;
		nr[lru] -= min(nr[lru], nr_scanned);

		lru += LRU_ACTIVE;
		nr_scanned = targets[lru] - nr[lru];
		nr[lru] = targets[lru] * (100 - percentage) / 100;
		nr[lru] -= min(nr[lru], nr_scanned);
		//策略调整一次后,后续不再进行策略调整
		scan_adjusted = true;
	}
	blk_finish_plug(&plug);
    //将本次回收的页面数量加到sc->nr_reclaimed中
	sc->nr_reclaimed += nr_reclaimed;

    /*
     *若当前节点中不活跃lru匿名页链表中页面数量过少,通过shrink_active_list函数将活跃lru匿名页链表中的一部分页面
     *迁移到不活跃的lru匿名页链表中去
     */
	if (inactive_list_is_low(lruvec, false, sc))
		shrink_active_list(SWAP_CLUSTER_MAX, lruvec,
				   sc, LRU_ACTIVE_ANON);
}



1.1.1 get_scan_count函数

get_scan_count函数会利用proc接口中的swappiness数据和sc->priority数据来计算本节点的4个lru链表中分别应该扫描多少个页面来进行内存回收操作,应该扫描的页面数量记录在nr数组中,通过对应lru链表类型enum lru_list来索引。

//mm/vmscan.c
/*
 *param:
 * (1)lruvec--->本节点的lru链表集
 * (2)memcg --->本节点的内存子系统,需要CONFIG_MEMCG使能,本次默认CONFIG_MEMCG未使能
 * (3)sc    --->本次页面回收的控制器
 * (4)nr    --->记录节点内本次操作中,每个lru链表中还有多少个页面需要被扫描,用enum lru_list来索引
 * 				nr[0] = anon inactive pages to scan; nr[1] = anon active pages to scan
 * 				nr[2] = file inactive pages to scan; nr[3] = file active pages to scan
 */
static void get_scan_count(struct lruvec *lruvec, struct mem_cgroup *memcg,
			   struct scan_control *sc, unsigned long *nr,
			   unsigned long * )
{	
    //获取/proc/sys/vm/swapiness值
	int swappiness = mem_cgroup_swappiness(memcg);
	struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
	u64 fraction[2];
	u64 denominator = 0;	/* gcc */
	struct pglist_data *pgdat = lruvec_pgdat(lruvec);
	unsigned long anon_prio, file_prio;
	enum scan_balance scan_balance;
	unsigned long anon, file;
	bool force_scan = false;
	unsigned long ap, fp;
	enum lru_list lru;
	bool some_scanned;
	int pass;

	/*
	 * If the zone or memcg is small, nr[l] can be 0.  This
	 * results in no scanning on this priority and a potential
	 * priority drop.  Global direct reclaim can go to the next
	 * zone and tends to have no problems. Global kswapd is for
	 * zone balancing and it needs to scan a minimum amount. When
	 * reclaiming for a memcg, a priority drop can cause high
	 * latencies, so it's better to scan a minimum amount there as
	 * well.
	 */
	if (current_is_kswapd()) {
		if (!pgdat_reclaimable(pgdat))
			force_scan = true;
		if (!mem_cgroup_online(memcg))
			force_scan = true;
	}
	if (!global_reclaim(sc))
		force_scan = true;

	/* If we have no swap space, do not bother scanning anon pages. */
    //若系统没有交换分区或交换空间,则不需要扫描匿名页
	if (!sc->may_swap || mem_cgroup_get_nr_swap_pages(memcg) <= 0) {
		scan_balance = SCAN_FILE;
		goto out;
	}

	/*
	 * Global reclaim will swap to prevent OOM even with no
	 * swappiness, but memcg users want to use this knob to
	 * disable swapping for individual groups completely when
	 * using the memory controller's swap limit feature would be
	 * too expensive.
	 *swappiness等于0,且未使能CONFIG_MEMCG,只扫描文件页
	 */
	if (!global_reclaim(sc) && !swappiness) {
		scan_balance = SCAN_FILE;
		goto out;
	}

	/*
	 * Do not apply any pressure balancing cleverness when the
	 * system is close to OOM, scan both anon and file equally
	 * (unless the swappiness setting disagrees with swapping).
	 *若sc->priority等于0,且swappiness!=0,文件页和匿名页对等进行扫描
	 *
	 */
	if (!sc->priority && swappiness) {
		scan_balance = SCAN_EQUAL;
		goto out;
	}

	/*
	 * Prevent the reclaimer from falling into the cache trap: as
	 * cache pages start out inactive, every cache fault will tip
	 * the scan balance towards the file LRU.  And as the file LRU
	 * shrinks, so does the window for rotation from references.
	 * This means we have a runaway feedback loop where a tiny
	 * thrashing file LRU becomes infinitely more attractive than
	 * anon pages.  Try to detect this based on file LRU size.
	 *当节点的空闲页框数(pgdatfree) + 节点lru上文件页个数(pgdatfile)小于等于节点中每个zone的
	 *high_wmark页框数时,只扫描匿名页面
	 *
	 */
	if (global_reclaim(sc)) {
		unsigned long pgdatfile;
		unsigned long pgdatfree;
		int z;
		unsigned long total_high_wmark = 0;

		pgdatfree = sum_zone_node_page_state(pgdat->node_id, NR_FREE_PAGES);
		pgdatfile = node_page_state(pgdat, NR_ACTIVE_FILE) +
			   node_page_state(pgdat, NR_INACTIVE_FILE);

		for (z = 0; z < MAX_NR_ZONES; z++) {
			struct zone *zone = &pgdat->node_zones[z];
			if (!managed_zone(zone))
				continue;

			total_high_wmark += high_wmark_pages(zone);
		}

		if (unlikely(pgdatfile + pgdatfree <= total_high_wmark)) {
			scan_balance = SCAN_ANON;
			goto out;
		}
	}

	/*
	 * If there is enough inactive page cache, i.e. if the size of the
	 * inactive list is greater than that of the active list *and* the
	 * inactive list actually has some pages to scan on this priority, we
	 * do not reclaim anything from the anonymous working set right now.
	 * Without the second condition we could end up never scanning an
	 * lruvec even if it has plenty of old anonymous pages unless the
	 * system is under heavy pressure.
	 *若果节点的lru链表上非活跃文件页数量大于活跃文件页数量,则只对文件页进行扫描
	 */
	if (!inactive_list_is_low(lruvec, true, sc) &&
	    lruvec_lru_size(lruvec, LRU_INACTIVE_FILE, sc->reclaim_idx) >> sc->priority) {
		scan_balance = SCAN_FILE;
		goto out;
	}
	//该状态是根据swappiness值和一些其他数据,分别扫描适量的文件页和匿名页
	scan_balance = SCAN_FRACT;

	/*
	 * With swappiness at 100, anonymous and file have the same priority.
	 * This scanning priority is essentially the inverse of IO cost.
	 */
	anon_prio = swappiness;
	file_prio = 200 - anon_prio;

	/*
	 * OK, so we have swap space and a fair amount of page cache
	 * pages.  We use the recently rotated / recently scanned
	 * ratios to determine how valuable each cache is.
	 *
	 * Because workloads change over time (and to avoid overflow)
	 * we keep these statistics as a floating average, which ends
	 * up weighing recent references more than old ones.
	 *
	 * anon in [0], file in [1]
	 */

	anon  = lruvec_lru_size(lruvec, LRU_ACTIVE_ANON, MAX_NR_ZONES) +
		lruvec_lru_size(lruvec, LRU_INACTIVE_ANON, MAX_NR_ZONES);
	file  = lruvec_lru_size(lruvec, LRU_ACTIVE_FILE, MAX_NR_ZONES) +
		lruvec_lru_size(lruvec, LRU_INACTIVE_FILE, MAX_NR_ZONES);

	spin_lock_irq(&pgdat->lru_lock);
	if (unlikely(reclaim_stat->recent_scanned[0] > anon / 4)) {
		reclaim_stat->recent_scanned[0] /= 2;
		reclaim_stat->recent_rotated[0] /= 2;
	}

	if (unlikely(reclaim_stat->recent_scanned[1] > file / 4)) {
		reclaim_stat->recent_scanned[1] /= 2;
		reclaim_stat->recent_rotated[1] /= 2;
	}

	/*
	 * The amount of pressure on anon vs file pages is inversely
	 * proportional to the fraction of recently scanned pages on
	 * each list that were recently referenced and in active use.
	 */
	ap = anon_prio * (reclaim_stat->recent_scanned[0] + 1);
	ap /= reclaim_stat->recent_rotated[0] + 1;

	fp = file_prio * (reclaim_stat->recent_scanned[1] + 1);
	fp /= reclaim_stat->recent_rotated[1] + 1;
	spin_unlock_irq(&pgdat->lru_lock);

	fraction[0] = ap;
	fraction[1] = fp;
	denominator = ap + fp + 1;
out:
	some_scanned = false;
	/* Only use force_scan on second pass. */
	for (pass = 0; !some_scanned && pass < 2; pass++) {
		*lru_pages = 0;
		for_each_evictable_lru(lru) {
			int file = is_file_lru(lru);
			unsigned long size;
			unsigned long scan;

			size = lruvec_lru_size(lruvec, lru, sc->reclaim_idx);
			scan = size >> sc->priority;

			if (!scan && pass && force_scan)
				scan = min(size, SWAP_CLUSTER_MAX);

			switch (scan_balance) {
			case SCAN_EQUAL:
				/* Scan lists relative to size */
				break;
			case SCAN_FRACT:
				/*
				 * Scan types proportional to swappiness and
				 * their relative recent reclaim efficiency.
				 */
				scan = div64_u64(scan * fraction[file],
							denominator);
				break;
			case SCAN_FILE:
			case SCAN_ANON:
				/* Scan one type exclusively */
				if ((scan_balance == SCAN_FILE) != file) {
					size = 0;
					scan = 0;
				}
				break;
			default:
				/* Look ma, no brain */
				BUG();
			}

			*lru_pages += size;
			nr[lru] = scan;

			/*
			 * Skip the second pass and don't force_scan,
			 * if we found something to scan.
			 */
			some_scanned |= !!scan;
		}
	}
}

get_scan_count函数对节点lru链表扫描规则定义如下:

  1. 若linux os目前没有交换分区或交换空间,则只会对文件页进行扫描
  2. 令pgdatfree表示当前节点的空闲页框数,pgdatfile表示当前节点lru链表上文件页框数,而total_high_wmark表示当前节点所有zone区域的

    高位水线内存和对应的页框数。若pgdatfree + pgdatfile <=total_high_wmark,则只扫描匿名页面.
  3. 若节点的lru链表上非活跃文件页框数大于活跃文件页框数,则只对lru上的文件页进行扫描.
  4. 其他情况会对lru上的两种页面都进行扫描.

get_scan_count函数对内存节点每个lru链表还需要扫描页框数量的计算方法(令scan表示本节点需要在lru对应的链表上扫描多少个页框):

  1. 若只扫描一种页面(只对节点的lru链表进行扫描)

    scan = lru上的页面总数 >> sc->priority
    nr[lru] = scan
    
  2. 若要扫描两种页面:

    在介绍规则前需要了解一个重要数据结构:struct zone_reclaim_stat.

    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]
    	 */
    	unsigned long		recent_rotated[2];
    	unsigned long		recent_scanned[2];
    };
    #lruvec为指定内存节点的lru链表集合struct lruvec *lruvec,是内存节点描述符的成员
    struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
    

    reclaim_stat->recent_scanned记录的是节点lru链表上最近被扫描过的页框数量.其中reclaim_stat->recent_scanned[0]存储

    的是最近在lru链表上扫描的匿名页框总数,而reclaim_stat->recent_scanned[1]存储的是最近在lru链表上扫描的文件页框总

    数.另外在内存节点上扫描不活跃的LRU链表时,os会把那些最近移回活跃lru链表的页面数量保存在

    reclaim_stat->recent_rotated,同样reclaim_stat->recent_rotated[0]保存的是匿名页的数量,

    reclaim_stat->recent_rotated[1]保存的是文件页框的数量.这些数据的更新主要是在函数shrink_inactive_list()和函数

    shrink_active_list()中。

    os设置struct zone_reclaim_stat结构体的目的就是为了评估页框被缓存的价值.recent_rotated/recent_scanned的值越大,

    说明这些页面被缓存的价值越大,更应该被留在lru链表。

    通过上面介绍,下面列出对应lru链表上需要被扫描页框数的计算规整.

    令在lru链表上需要被扫描的页框总数为scan,则scan的计算规则如下:

    scan = lru上页面的总数 >> sc->priority
    
    anon_prio = swappiness;
    file_prio = 200 - anon_prio;
    
    ap = anon_prio * (reclaim_stat->recent_scanned[0] + 1);
    ap /= reclaim_stat->recent_rotated[0] + 1;
    
    fp = file_prio * (reclaim_stat->recent_scanned[1] + 1);
    fp /= reclaim_stat->recent_rotated[1] + 1;
    
    if(if_file_lru(lru))
    	scan = (scan * fp) / (ap + fp);
    else
    	scan = (scan * ap) / (ap + fp); 
    

通过上面计算规则的介绍可以发现,用户能够利用/proc/sys/mm/swappiness接口根据实际情况调节内存回收匿名页和文件页的比例情况.

1.swappiness值的取值范围为0-100,默认值是60:该值取值越高,内存回收时会越优先选择匿名页;该值越低,内存回收时会越优先选择文件页。
2.当swappiness==0时内存回收只选择文件页.
3.当swappness==100,回收匿名页和文件页的权重相同。



1.1.2 shrink_list函数

shrik_list函数会根据传入lru链表的类型调用不同的函数来处理对应链表:

  1. 当传入的是活跃的lru链表,且其对应的不活跃lru链表中页框数量较少时,会通过shrink_active_list函数将活跃的lru链表中的页框迁移到其对应的不活跃lru链表中
  2. 当传入的是非活跃lru链表,则调用shrink_inactive_list函数对非活跃链表上的页框进行扫描,并将需要回收的页框进行回收操作(部分页面会迁移到活跃lru链表,还有部分需要保留在lru链表)。
static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan,
				 struct lruvec *lruvec, struct scan_control *sc)
{
	//传入的lru为活跃的lru链表
    if (is_active_lru(lru)) {
        //判断活跃lru链表对应的非活跃链表上的页框数量是否较少
		if (inactive_list_is_low(lruvec, is_file_lru(lru), sc))
			shrink_active_list(nr_to_scan, lruvec, sc, lru);
		return 0;
	}

	return shrink_inactive_list(nr_to_scan, lruvec, sc, lru);
}


1.1.2.1 inactive_list_is_low函数

inactive_list_is_low函数目的是判断传入的lru链表类型对应的不活跃lru链表上的页框数量是否过低.函数返回True表明过低.

/*
 * The inactive anon list should be small enough that the VM never has
 * to do too much work.
 *
 * The inactive file list should be small enough to leave most memory
 * to the established workingset on the scan-resistant active list,
 * but large enough to avoid thrashing the aggregate readahead window.
 *
 * Both inactive lists should also be large enough that each inactive
 * page has a chance to be referenced again before it is reclaimed.
 *
 * The inactive_ratio is the target ratio of ACTIVE to INACTIVE pages
 * on this LRU, maintained by the pageout code. A zone->inactive_ratio
 * of 3 means 3:1 or 25% of the pages are kept on the inactive list.
 *
 * total     target    max
 * memory    ratio     inactive
 * -------------------------------------
 *   10MB       1         5MB
 *  100MB       1        50MB
 *    1GB       3       250MB
 *   10GB      10       0.9GB
 *  100GB      31         3GB
 *    1TB     101        10GB
 *   10TB     320        32GB
 */
static bool inactive_list_is_low(struct lruvec *lruvec, bool file,
						struct scan_control *sc)
{
	unsigned long inactive_ratio;
	unsigned long inactive, active;
	enum lru_list inactive_lru = file * LRU_FILE;
	enum lru_list active_lru = file * LRU_FILE + LRU_ACTIVE;
	unsigned long gb;

	/*
	 * If we don't have swap space, anonymous page deactivation
	 * is pointless.
	 */
	if (!file && !total_swap_pages)
		return false;

	inactive = lruvec_lru_size(lruvec, inactive_lru, sc->reclaim_idx);
	active = lruvec_lru_size(lruvec, active_lru, sc->reclaim_idx);

	gb = (inactive + active) >> (30 - PAGE_SHIFT);
	if (gb)
		inactive_ratio = int_sqrt(10 * gb);
	else
		inactive_ratio = 1;

	return inactive * inactive_ratio < active;
}

通过该函数的注释可知:linux os通常希望不活跃的匿名页面数量应该少一些,因为这样保证VM没有太多的工作可做(可以让页面回收的工作变少).同时linux os也希望不活跃的文件页面数量尽量少一些,这样可以给活跃的文件页lru链表预留给多的内存(更多的page cache,更高效的文件读取).但是站在内存回收的角度考虑,linux os也希望不活跃lru链表上的页面数量尽可能的多一些,这样可以在回收页面前更多的页面有第二次机会被访问到,这样会给那些在不活跃lru链表但经常被访问的页面再一次重生的机会(第二次机会法)。

为了平衡上述关系,linux os提出了一个不活跃比例(inactive_ratio),以此让同类型lru链在活跃页面数量和不活页页面数量的比例达到平衡状态.

#在某类型lru链表:令活跃页面数量为active,不活跃页面数量为inactive,不活跃比率为inactive_ratio.
1. 若  inactive * inactive_ratio < active:该类型lru链表不活跃页面数量过低
2. 若  inactive * inactive_ratio = active 理想状态
3. 若  inactive * inactive_ratio > active 该类型lru链表活跃页面数量过低

linux os的lru链表不活跃比率表如下所示(计算方式见函数inactive_list_is_low):

#既包括匿名页lru链表页包括文件页lru链表
total     target    max
 memory    ratio     inactive
 -------------------------------------
   10MB       1         5MB
  100MB       1        50MB
    1GB       3       250MB
   10GB      10       0.9GB
  100GB      31         3GB
    1TB     101        10GB
   10TB     320        32GB

下面以文件页为例:

linux os中内存为10GB,通过上表可知此时系统的不活跃比例为10,也就是在理想状态下os的活跃文件页lru链表上页面的数量和不活跃文件页lru链表上的页表数量的比例为10:1,则在理想状态下系统的不活跃页面占用的内存应该是:
						10 * 1/(10 + 1= 0.9 GB


1.1.2.2 shrink_active_list函数

shrink_active_list函数是对活跃的lru链表进行扫描(从链表尾往头扫描),扫描该链表中固定数量的页(nr_to_scan),并将这nr_to_scan个被扫描页根据其状态信息分别迁移到对应的LRU链表中去:

  1. 若该页是不可回收的页,则将该页迁移到对应内存节点的不可回收LRU链表中.
  2. 若该页最近被引用访问(对应PTE页表项AF位为1)且是缓存可执行文件内容的文件页,则将该页继续保留在对应的活跃LRU链表中,但会从链表尾移动到链表头,且映射该页所有pte页表项的硬件访问文AF被置位为0.
  3. 若该页是不属于1和2状态下的页,则清除该页的active标志,将其从活跃LRU链表尾部迁移到对应的非活跃lru链表头部。

ps:为了降低自旋锁竞争压力,页迁移过程中会先将同一类型的页缓存在临时的lru链表中,然后批量地将页进行迁移(1.先从活跃lru链表的尾部往头部扫描,批量将nr_to_scan个页面迁移到l_hold临时链表中。2.再对临时链表l_hold进行扫描,根据每个页的状态将页分别迁移到临时的l_active链表和l_inactive链表中。3.最后批量地将l_active链表和l_inactive链表中的页分别迁移到其节点对应的活跃LRU链表和非活跃LRU链表中)

/*
 *param:
 *	1.nr_to_scan:表示要在对应活跃的lru链表中扫描多少个页
 *  2.lruvec:表示对应节点的lru链表集合(用于获取被扫描活跃lru链表和其对应的不活跃lru链表)
 *  3.sc:本节点页面回收控制器
 *  4.lru:被扫描活跃lru链表的类型
 */
static void shrink_active_list(unsigned long nr_to_scan,
			       struct lruvec *lruvec,
			       struct scan_control *sc,
			       enum lru_list lru)
{
	unsigned long nr_taken;
	unsigned long nr_scanned;
	unsigned long vm_flags;
	LIST_HEAD(l_hold);	/* The pages which were snipped off */
	LIST_HEAD(l_active);
	LIST_HEAD(l_inactive);
	struct page *page;
	struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;
	unsigned long nr_rotated = 0;
	isolate_mode_t isolate_mode = 0;
	int file = is_file_lru(lru);
	struct pglist_data *pgdat = lruvec_pgdat(lruvec);
	/*
	 *将原先加入到各个Per-CPU变量pagevec缓存中的page都转移到它们真正应该处
	 *lru链表上,同时将当时它们加入pagevec时递增的计数减下去(加入pagevec
	 *就说明有一条pagevec这个路径在引用这个page)
	 */
	lru_add_drain();

	if (!sc->may_unmap)
		isolate_mode |= ISOLATE_UNMAPPED;
	if (!sc->may_writepage)
		isolate_mode |= ISOLATE_CLEAN;

	//保护本节点lru链表的自旋锁
    spin_lock_irq(&pgdat->lru_lock);

	/*
	 *批量将对应的LRU链表上一部分页面迁移到l_hold链表中:
	 *	1.一部分页面是指:将lru对应的LRU链表从尾向头的扫描nr_to_scan个页面,在扫描的这些页面中挑选出满足isolate_mode的页框
	 *  2.nr_scanned表示此次扫描LRU链表上页框的总数
	 *  3.nr_taken:表示迁移到l_hold链表的页框总数
	 *  4.批量迁移到临时链表目的:缩短加锁时间
	 */	
    nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold,
				     &nr_scanned, sc, isolate_mode, lru);

	//增加内存节点的NR_ISOLATED_ANON计算
    __mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, nr_taken);
    /*
     *增加节点对应LRU链表的recent_scanned[]计数,表示本节点文件页或匿名页LRU链表最近被访问页的总数量.该数据在get_scan_count()计算各个lru
     *链表应该被扫描页的数量是会被用到
     */
	reclaim_stat->recent_scanned[file] += nr_taken;

	if (global_reclaim(sc))//未使能CONFIG_MEMCG
		__mod_node_page_state(pgdat, NR_PAGES_SCANNED, nr_scanned);
	__count_vm_events(PGREFILL, nr_scanned);

	//页面完全迁移到l_hold临时链表后,释放节点LRU链表自旋锁
    spin_unlock_irq(&pgdat->lru_lock);
	//遍历临时链表l_hold中的页
	while (!list_empty(&l_hold)) {
		cond_resched();
        //取出l_hold链表尾部的页
		page = lru_to_page(&l_hold);
		list_del(&page->lru);
		//若取出的页为不可回收页,则将该页加入到节点的不可回收LRU链表表头
		if (unlikely(!page_evictable(page))) {
			putback_lru_page(page);
			continue;
		}

		if (unlikely(buffer_heads_over_limit)) {
			if (page_has_private(page) && trylock_page(page)) {
				if (page_has_private(page))
					try_to_release_page(page, 0);
				unlock_page(page);
			}
		}
		/*
		 *若该页是最近被访问,引用过的且是可执行文件的缓存文件页,则将其从l_hold临时链表取出放置在l_active临时链表中,continue本次循环然后
		 *继续扫描l_hold链表.
		 *	a.page_referenced函数返回的是该页面最近访问,引用pte的个数.返回0表示该页面最近没有被访问过.
		 *  b.从该if条件判断可知,活跃页面在被扫描时,只有最近被访问了的可执行文件的缓存页面才用机会继续留在活跃的lru链表,其他的的活跃面
		 *    会被迁移到非活跃的lru链表中.
		 *  c.page_referenced函数是利用反向映射找出映射该页的所有pte,并查看pte页表项的AF位是否置位,置位则表明该页被访问过,AF位被访问后
		 *    会立即被设置为0.而page_referenced函数返回值的是表示映射该页的所有pte中AF位被置位的pte个数。
		 */
		if (page_referenced(page, 0, sc->target_mem_cgroup,
				    &vm_flags)) {
			nr_rotated += hpage_nr_pages(page);
			/*
			 * Identify referenced, file-backed active pages and
			 * give them one more trip around the active list. So
			 * that executable code get better chances to stay in
			 * memory under moderate memory pressure.  Anon pages
			 * are not likely to be evicted by use-once streaming
			 * IO, plus JVM can create lots of anon VM_EXEC pages,
			 * so we ignore them here.
			 */
			if ((vm_flags & VM_EXEC) && page_is_file_cache(page)) {
				list_add(&page->lru, &l_active);
				continue;
			}
		}
		//清除该页的active标志,因为该页经过前面的赛选,表明后续将会被迁移到节点的非活跃lru链表中.
		ClearPageActive(page);	/* we are de-activating */
        //若该页为活跃LRU链表上的匿名页或普通文件页,则将该页添加到临时的l_inactive链表中去
		list_add(&page->lru, &l_inactive);
	}

	/*
	 * Move pages back to the lru list.
	 */
    //将l_inactive和l_active两链表中的页进行批量迁移(只需要对节点的lru链表加锁一次)
	spin_lock_irq(&pgdat->lru_lock);
	/*
	 *将最近活跃lru链表上被引用访问的页面数量统计到recent_rotated对应的数组项中。后续扫描lru链表时,该数据会被用来计算每个lru链表需要被扫
	 *描页框数量(见get_scan_count函数)。
	 */
	reclaim_stat->recent_rotated[file] += nr_rotated;
	/*
	 *将l_active链表中的页全部迁移到节点指定类型的活跃LRU链表中去(l_active链表尾取出页,插入内存节点活跃LRU链表
	 *头).需要注意的是函数会在页被转移到活跃lru链表上后,去检查该页是否被进程引用(&page->_refcount),若没有则会
	 *将该页从节点活跃的lru链表中取出,取出后再次装入l_hold临时链表.后面会将l_hold链表里面的页批量释放到伙伴系统
	 *中去.
	 */
	move_active_pages_to_lru(lruvec, &l_active, &l_hold, lru);
    //将l_inactive链表中的页全部迁移到节点指定类型的非非活跃LRU中去.后续操作同上
	move_active_pages_to_lru(lruvec, &l_inactive, &l_hold, lru - LRU_ACTIVE);
	__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, -nr_taken);
	spin_unlock_irq(&pgdat->lru_lock);

	mem_cgroup_uncharge_list(&l_hold);
    //若当前l_hold链表中还有页存在,则直接将这些页释放到伙伴系统中去
	free_hot_cold_page_list(&l_hold, true);
}

通过仔细阅读shrink_active_list函数的源码可以发现,linux os在扫描内存节点的活跃LRU链表时,只会将最近被访问过的缓存可执行文件内容的文件页继续保留在对应的活跃LRU链表中。其他的页面不论最近是否被访问过,都会统一迁移到其对应的非活跃lru链表中去,这是为什么呢???

1.对于一个内存很大的系统,当系统内存用完后,系统中的每个页面都会被访问引用,这个时候,linux系统不仅没有时间去扫描活跃的lru链表,还要去设置
  页面的软件访问位(PG_referenced),这些信息目前并没有什么用处.所有linux os在扫描活跃lru链表时会将所有扫描的页面都迁移到不活跃的lru链表中
 (最近被访问应用了的缓存执行文件内容的文件页除外).迁移后只需要清除该页面所有的硬件访问位(page_referenced).而后续去扫描不活跃的lru链表
  时,若检测到上面被迁移的页面的硬件 访问位AF有一个被置位,则会立即将这些页面再次迁移到活跃的lru链表中去.

2.让最近被访问,引用的缓存可执行文件内容的文件页,继续保存在活跃lru链表的原因是:让这些文件页被缓存得更久一些,以此来增强用户进程的交互体验.
  因为lru链表的扫描顺序是先扫描不活跃lru链表,再扫描活跃lru链表,且扫描不活跃lru链表的速度快于扫描活跃lru链表的速度,所以将这些最近被访问引
  用的缓存可执行文件内容的文件页,继续保持在活跃lru链表上,会让这些文件页获得更多的时间不被回收释放.(判断一个页为缓存可执行文件内容的文件
  页--->通过struct page反向映射找到映射该页的一个VMA,查看该VMA的属性是否被标记为VM_EXEC,这些页通常包括可执行文件和他们链接的库文件).


小结:

在扫描内存节点活跃lru链表时:

  1. 若将活跃链表中的页迁移到非活页链表:页的active标志会被设置为0,页的软件访问标志(PG_referenced)会保持不变,页映射的所有pte页表项的AF位被设置为0(该AF为硬件访问位,由page_referenced函数完成数据统计,该位被page_referenced访问后会立即设置为0)。
  2. 若活跃链表中的页由链表尾部迁移到链表头部:页的active标志不变,页的软件访问标志(PG_referenced)不变,页映射的所有pte表项的AF位被设置为0

ps:硬件访问位,是映射该页的pte页表项的AF位,而软件访问标志是该页对应的struct page的flag成员中的某一个比特位.



1.1.2.3 shrink_inactive_list函数

shrink_inactive_list函数扫描节点的不活跃lru链表上特定个数(nr_to_scan)的页面,并尝试回收这些页面,函数最后返回本次扫描回收的页框个数.

/*
 *param:
 * 1.nr_to_scan:待扫描页面数量(nr[lru])
 * 2.lruvec:节点的lru链表集合
 * 3. sc:页面回收控制器
 * 4. lru:待扫描lru链表的类型
 */
/*
 * shrink_inactive_list() is a helper for shrink_node().  It returns the number
 * of reclaimed pages
 */
static noinline_for_stack unsigned long
shrink_inactive_list(unsigned long nr_to_scan, struct lruvec *lruvec,
		     struct scan_control *sc, enum lru_list lru)
{
	//定义一个临时链表
	LIST_HEAD(page_list);
	//本次被扫描页面总数
	unsigned long nr_scanned;
	//本次回收页面总数
	unsigned long nr_reclaimed = 0;
	unsigned long nr_taken;
	unsigned long nr_dirty = 0;
	unsigned long nr_congested = 0;
	unsigned long nr_unqueued_dirty = 0;
	unsigned long nr_writeback = 0;
	unsigned long nr_immediate = 0;
	isolate_mode_t isolate_mode = 0;
	//判断待扫描链表是非活跃文件页lru链表还是匿名页lru链表
	int file = is_file_lru(lru);
	//lru链表集对应的内存节点结构描述符
	struct pglist_data *pgdat = lruvec_pgdat(lruvec);
	struct zone_reclaim_stat *reclaim_stat = &lruvec->reclaim_stat;

	if (!inactive_reclaimable_pages(lruvec, sc, lru))
		return 0;

	/*
	 *该while循环目的是:
	 *	1.too_many_isolated会做两个判断:
	 *		a.判断当前回收者是kswapd还是直接页面回收(direct reclaimer)
	 *		b.判断当前分离的页面数量是否大于不活跃的页面数量。
	 *	2.若当页面回收者是直接页面回收且有大量已经分离的页面,表明可能有很多进程正在做页面回收 ,且有大量的进程已经触发了直接页面回收机制,
	 *	  这样会导致不必要的内存抖动并出发oom,此时可以让当前的直接内存回收进程sleep HZ/10.
	 */
	while (unlikely(too_many_isolated(pgdat, file, sc))) {
		congestion_wait(BLK_RW_ASYNC, HZ/10);

		/* We are about to die and free our memory. Return now. */
		if (fatal_signal_pending(current))
			return SWAP_CLUSTER_MAX;
	}
	/*
	 *将原先加入到各个Per-CPU变量pagevec缓存中的page都转移到它们真正应该处lru链表上,同时将当时它们加入pagevec时递增的计数减下去(加入
	 *pagevec就说明有一条pagevec这个路径在引用这个page)
	 */
	lru_add_drain();

	if (!sc->may_unmap)
		isolate_mode |= ISOLATE_UNMAPPED;
	if (!sc->may_writepage)
		isolate_mode |= ISOLATE_CLEAN;

	spin_lock_irq(&pgdat->lru_lock);

	/*
	 *批量将对应的非活跃LRU链表上一部分页面迁移到临时的page_list链表中:
	 *	1.一部分页面是指:将lru对应的LRU链表从尾向头的扫描nr_to_scan个页面,在扫描的这些页面中挑选出满足isolate_mode的页框
	 *  2.nr_scanned表示此次扫描LRU链表上页框的总数
	 *  3.nr_taken:表示迁移到page_list链表的页框总数
	 *  4.批量迁移到临时链表目的:缩短加锁时间,减缓锁竞争
	 */	
	nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &page_list,
				     &nr_scanned, sc, isolate_mode, lru);

	//增加(NR_ISOLATED_ANON+file)计算,file为0表示隔离匿名页总数量,为1表示隔离文件页总数量
	__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, nr_taken);
	//最近lru链表页扫描数量计算,用于get_scan_count函数计算。
	reclaim_stat->recent_scanned[file] += nr_taken;

	//未使能CONFIG_MEMCG,全局页面回收时的相关内存计算统计
	if (global_reclaim(sc)) {
		__mod_node_page_state(pgdat, NR_PAGES_SCANNED, nr_scanned);
		if (current_is_kswapd())
			__count_vm_events(PGSCAN_KSWAPD, nr_scanned);
		else
			__count_vm_events(PGSCAN_DIRECT, nr_scanned);
	}
	spin_unlock_irq(&pgdat->lru_lock);
	//页隔离失败,函数返回0.
	if (nr_taken == 0)
		return 0;
	/*
	 *此处是通过shrink_page_list函数来扫描临时链表page_list上的页面,并进行回收处理,函数返回值是此次回收成功的页面数,记录在nr_reclaimed
	 *中。后面会详细分析该换上
	 */
	nr_reclaimed = shrink_page_list(&page_list, pgdat, sc, TTU_UNMAP,
				&nr_dirty, &nr_unqueued_dirty, &nr_congested,
				&nr_writeback, &nr_immediate,
				false);
	spin_lock_irq(&pgdat->lru_lock);
	//相关内存统计计算更新
	if (global_reclaim(sc)) {
		if (current_is_kswapd())
			__count_vm_events(PGSTEAL_KSWAPD, nr_reclaimed);
		else
			__count_vm_events(PGSTEAL_DIRECT, nr_reclaimed);
	}
	//将临时链表page_list剩余的页迁移回对应的不活跃的lru链表中
	putback_inactive_pages(lruvec, &page_list);
	//减少NR_ISOLATED_ANOD_file计数(因为page_list中被隔离的页都被迁移出来了)
	__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, -nr_taken);

	spin_unlock_irq(&pgdat->lru_lock);

	mem_cgroup_uncharge_list(&page_list);
	/*
	 *将page_list中页放回lru链表后,会对页做一个用户引用计数判断(&page->_refcount),会将那些未被用户引用的页再次返回给page_list,并在此
	 *处统一释放到伙伴系统中
	 */
	free_hot_cold_page_list(&page_list, true);
    ......
	......
    ......
    //返回此处扫描成功回收的页框数量
	return nr_reclaimed;
}


1.1.2.3.1页面回收核心函数shrink_page_list

shrink_inactive_list函数中的核心函数是shrik_page_list().该函数是将隔离在临时链表page_list中的所有页,逐页进行赛选,最后将每个页放在适合自己的位置上(有些也被回收并释放到伙伴系统中,有些页被转移到其对应的活跃lru链表中,还有些页会被再次转移到先前的非活跃lru链表上)。该函数内容多,实现流程较复杂需要仔细琢磨源码.

/*
 * shrink_page_list() returns the number of reclaimed pages
 */
static unsigned long shrink_page_list(struct list_head *page_list,
				      struct pglist_data *pgdat,
				      struct scan_control *sc,
				      enum ttu_flags ttu_flags,
				      unsigned long *ret_nr_dirty,
				      unsigned long *ret_nr_unqueued_dirty,
				      unsigned long *ret_nr_congested,
				      unsigned long *ret_nr_writeback,
				      unsigned long *ret_nr_immediate,
				      bool force_reclaim)
{
	//初始化返回的链表,即把此次shrink无法回收的页面放入该链表中
	LIST_HEAD(ret_pages);
	//初始化回收的链表,即把此次shrink 可以回收的页面放入该链表中
	LIST_HEAD(free_pages);
	int pgactivate = 0;
	//计数器,统计脏页但不包括正在回写的页的个数
	unsigned long nr_unqueued_dirty = 0;
	//计数器,统计脏页包括正在回写的页的个数
	unsigned long nr_dirty = 0;
	unsigned long nr_congested = 0;
	unsigned long nr_reclaimed = 0;
	unsigned long nr_writeback = 0;
	unsigned long nr_immediate = 0;

	cond_resched();
	//while循环扫描page_list链表,链表成员从非活跃lru两边隔离而来
	while (!list_empty(page_list)) {
		struct address_space *mapping;
		struct page *page;
		int may_enter_fs;
		enum page_references references = PAGEREF_RECLAIM_CLEAN;
		bool dirty, writeback;
		bool lazyfree = false;
		int ret = SWAP_SUCCESS;

		cond_resched();
		//从page_list链表表尾取出一个不活跃页面(page_list是从非活跃lru链表上扫描出的需要回收的页)
		page = lru_to_page(page_list);
		list_del(&page->lru);

		//获取页面锁,获取失败,页继续保持在不活跃LRU链表
		if (!trylock_page(page))
			goto keep;

		//验证该页是不活跃页
		VM_BUG_ON_PAGE(PageActive(page), page);

		//扫描页次数计算器++
		sc->nr_scanned++;
		/*
		 *若该页是不可回收页(page's mapping marked unevictable or page is part of an mlocked VMA直接跳转到cull_mlocked标签处,后续会
		 *被移到不可回收lru链表
		 */
		if (unlikely(!page_evictable(page)))
			goto cull_mlocked;
		/*
		 *若当前回收不允许回收被映射了的页,剔除被映射了的页面(跳转到keep_locked标签处)
		 *1.sc->may_unmap==1 表示允许回收映射的页面
		 *2.page_mapped(page)使用于判断page->_mapcount是否大于0.大于0表示有一个或多个用户PTE映射到该页
		 *3.此处if表明当不允许回收映射的页面,且此时有用户PTE映射到该页面,则直接跳转到keep_locked标签处.
		 */
		if (!sc->may_unmap && page_mapped(page))
			goto keep_locked;

		/* Double the slab pressure for mapped and swapcache pages */
		//页面被用户pte映射或该page属于page swap(PG_swapbacked置位)
		if (page_mapped(page) || PageSwapCache(page))
			sc->nr_scanned++;

		may_enter_fs = (sc->gfp_mask & __GFP_FS) ||
			(PageSwapCache(page) && (sc->gfp_mask & __GFP_IO));

		//检查当前页是否为脏页或正在回写的页,并更新相关的统计计数器
		page_check_dirty_writeback(page, &dirty, &writeback);
		if (dirty || writeback)
			nr_dirty++;

		if (dirty && !writeback)
			nr_unqueued_dirty++;

		/*
		 *若当前页正在BDI回写页面,可能导致阻塞,增加nr_congested计数
		 */
		mapping = page_mapping(page);
		if (((dirty || writeback) && mapping &&
		     inode_write_congested(mapping->host)) ||
		    (writeback && PageReclaim(page)))
			nr_congested++;
		/*
		 *若该页正处于回写状态,需要考虑下面3中场景:
		 *(1) 如果当前页面正处于回写状态且属于可回收页类型,那么当当前页面回收者是kswapd线程且此时当前页对应的内存节点中有大量正在回写的页
		 *	  面时,linux os此时会增加nr_immediate计数,然后跳转到keep_locked标签处执行完当前页的后续安置操作,接着继续扫描page_list链
		 *	  表,而不是睡眠等待当前页的回写完成(若等待回写完成,可能kswapd线程会导致无限期等待,因为在页面回写时可能会出现磁盘I/O错误或
		 *	  者磁盘连接错误)
		 *(2) 若当前页面不是可回收页(无回收标记,通过PageReclaim判断)或者是当前页面分配器的调用者并未使用__GFP_FS或__GFP_IO分配标志。那
		 *	  么给当前页设置PG_PageReclaim标志位,并增加nr_writeback计数,然后跳转到keep_locked标签处,继续对page_list链表进行扫描
		 *(3) 除了上面两种情况外,若当前页正在回写,那么当前进程会睡眠等待当前页的回写完成
		 */
		if (PageWriteback(page)) {
			/* Case 1 above */
			if (current_is_kswapd() &&
			    PageReclaim(page) &&
			    test_bit(PGDAT_WRITEBACK, &pgdat->flags)) {
				nr_immediate++;
				goto keep_locked;

			/* Case 2 above */
			} else if (sane_reclaim(sc) ||
			    !PageReclaim(page) || !may_enter_fs) {
				SetPageReclaim(page);
				nr_writeback++;
				goto keep_locked;

			/* Case 3 above */
			} else {
				unlock_page(page);
				//等待当前页回写完成
				wait_on_page_writeback(page);
				//当前页回写完成后,将当前页加入page_list链表尾,continue后while循环又会扫到刚回写完成的当前页进行回收处理
				list_add_tail(&page->lru, page_list);
				continue;
			}
		}
       /*
        *1.通过page_referenced检查页面访问引用了多少个pte将结果记录在变量referenced_ptes中(主要是通过反向映射统计出所有与page建立映射关
        *  系的pte页表项中AF位被置位的页表项总数),该函数还会获映射该物理页的vma的vm_flags成员数据.需要注意的是函数在访问了每个pte页表项
        *  的AF硬件访问位后会立即将该AF位设置为0.
        *2.通过TestClearPageReferenced函数获取page的flag成员中的软件访问位是否被置位为1并将数据结果记录在referenced_page变量中,该函数在
        *  访问了page的flag成员的软件访问位PG_referenced后会立即将该访问位设置为0.
        *3.通过1,2获取到数据判断该page回收流程的后续走向:
        *	(1).若该页对应的vma的vm_flags中VM_LOCKED被置位,则后续通过try_to_unmap函数解除该页的相关map,并将该页转移到对应节点的
        *		LRU_UNEVICTABLE链表
 		*	(2).若该页是以swap作为后备存储的匿名页或shmem共享页...(PageSwapBacked(page)):
 		*		  (a).若有用户通过pte页表访问该页,则结束对该页的回收,后续将该页转移到对应节点的活跃匿名页lru链表(转移后page的
 		*			  referenced_ptes为0,referenced_page为0).
		*		  (b).没有用户通过pte页表访问该页,将该页列为可回收的备选页,继续进行后面的回收操作
 		*   (3).若该页是以磁盘文件作为备存储的文件页:
 		*		  (a).有用户通过pte页表访问该页:
 		*				(|)  该页为最近第二次访问的文件缓存页(PG_referend == 1)或则该页是共享的文件缓存页(referenced_ptes > 1),则将该
 		*					 页的软件访问位PG_referend设置为1,后续将该页迁移到对应内存节点的活跃文件页lru链表中去.
 		*				(||) 该页映射的文件为可执行文件(vm_flags & VM_EXEC),则将该页的软件访问位设置为1,后续将该页迁移到对应内存节点的活
 		*					 跃文件页lru链表中去
 		*				(|||)其余被访问引用文件页(该页为软件访问位为0,且只被一个用户通过页表访问的普通文件页),会将其软件访问位设置为
 		*					 1,然后继续保留在对应节点的非活跃lru链表中	 
	    *        (b).没有用户通过pte页表访问该页:将该页作为可回收的备选页,继续进行后面的回收操作
 		*/
		if (!force_reclaim)
			references = page_check_references(page, sc);
		switch (references) {
		case PAGEREF_ACTIVATE:
			goto activate_locked;
		case PAGEREF_KEEP:
			goto keep_locked;
		case PAGEREF_RECLAIM:
		case PAGEREF_RECLAIM_CLEAN:
			; /* try to reclaim the page below */
		}
		/*
		 *当前页是一个未被访问引用的匿名页,且该匿名页未被分配交换空间(通过page的flag中的PG_swapcache位来判断)
		 */
		if (PageAnon(page) && !PageSwapCache(page)) {
			/*
			 *该该匿名页分配者未使用__GFP_IO分配标志,则该匿名页不能swap out,
			 *不能被回收继续保持在不活跃LRU链表
			 */
			if (!(sc->gfp_mask & __GFP_IO))
				goto keep_locked;
			/*
			 *当前页面允许文件和磁盘io操作,能进行swap out,则调用add_to_swap函数给当前匿名页面分配交换空间:
			 *	a.若交换空间分配成功会对页的struct page成员做如下操作,然后继续后面的回收操作:
			 *		(|)设置页面的PG_swapcache标志位,并将该page插入swap cache对应的address_space结构的
			 *           page_tree中.
			 *		(||)让page->private存储该页对应的swap slot在交换空间的位置信息
			 *      (|||)匿名页page->mapping执行发生该变,有匿名页的anon_vma数据结构变成交换分区的
			 *             swapper_spaces.
			 *  b.若add_to_swap()函数执行失败,当前页将会被送到activate_locked标签处进行处理.
			 */
			if (!add_to_swap(page, page_list))
				goto activate_locked;
			lazyfree = true;
			may_enter_fs = 1;

			/* Adding to swap updated mapping */
			mapping = page_mapping(page);
		} else if (unlikely(PageTransHuge(page))) {//巨页,当前页仍保留在非活跃lru
			/* Split file THP */
			if (split_huge_page_to_list(page, page_list))
				goto keep_locked;
		}

		VM_BUG_ON_PAGE(PageTransHuge(page), page);

		/*
		 *若该页被一个或多个用户映射(page->mapcount>0),则调用try_to_unmap函数解除当前页与这些用户映射的pte间的关系(对于匿名页其每个pte
		 *页表项会记录前映射页在swap ).后续根据try_to_unmap函数的执行情况来决定当前页后续处理方式:
		 * (1) 若当前页成功解除所有映射(SWAP_SUCCESS):函数后续尝试释放当前页.
		 * (2) 若当前页在解除映射关系时所有的映射关系都解除失败(SWAP_FAIL),则跳转到activate_locked标签处.
		 * (3) 若当前页在解除映射关系时部分映射解除成功部分映射解除失败(SWAP_AGAIN),跳转到keep_locked标签处.
		 * (4) 若当前页在解除映射关系时有pte被其他进程锁住(SWAP_MLOCK),则跳转到cull_mlocked标签处.
		 * (5) 对于匿名页,解除映射成功,且page数据回写磁盘完成(SWAP_LZFREE),直接跳转到lazyfree标签处(后续解除page->mapping和swap 
		 * 	   cache的关系后就可释放回收)。
		 */
		if (page_mapped(page) && mapping) {
			switch (ret = try_to_unmap(page, lazyfree ?
				(ttu_flags | TTU_BATCH_FLUSH | TTU_LZFREE) :
				(ttu_flags | TTU_BATCH_FLUSH))) {
			case SWAP_FAIL:
				goto activate_locked;
			case SWAP_AGAIN:
				goto keep_locked;
			case SWAP_MLOCK:
				goto cull_mlocked;
			case SWAP_LZFREE:
				goto lazyfree;
			case SWAP_SUCCESS:
				; /* try to free the page below */
			}
		}
		//若当前页为脏页
		if (PageDirty(page)) {
			 /*
			  *如果当前页是脏的文件页:
			  *		a.若当前回收者是kswapd线程(current_is_kswapd()==true)且目前当前节点中脏页数量较多
			  *		  (test_bit(PGDAT_DIRTY, &pgdat->flags)==true)时,linux os才会对脏文件页进行回收。
			  *		b.其他情况下(也是普遍情况下)linux os并不会对脏文件页进行回收操作,而是将该文件页设置为可回收页,然后跳转到
			  *	 	  keep_locked标签处(后续将该页继续保留在非活跃的文件页lru链表中,给该脏文件页足够时间进行页回写,以便下次内存回收扫描
			  *		  到该页能成功回收到该页)。
			  *kswapd内核线程中对一个脏页面进行回写的做法是不可取的,以前这样做是因为往存储设备中回写页面内容的速度比CPU慢很多个数量级。目
			  *前的做法是kswapd内核线程不会对零星的几个内容缓存页面进行回写,除非遇到之前有很多还没有开始回写的脏页,当扫描完一轮后,发现
			  *有很多脏的内容缓存还没来得及添加到回写子系统中,那么给对应节点的flag成员设置PGDAT_DIRITY位,表示该节点的kswapd线程内回写脏
			  *页,否则一般情况不会回写脏的内容缓存.
			  */
			if (page_is_file_cache(page) &&
					(!current_is_kswapd() ||
					 !test_bit(PGDAT_DIRTY, &pgdat->flags))) {

				inc_node_page_state(page, NR_VMSCAN_IMMEDIATE);
				SetPageReclaim(page);
				goto keep_locked;
			}
			//当前页只能在干净状态下回收,跳转到keep_locked标签处
			if (references == PAGEREF_RECLAIM_CLEAN)
				goto keep_locked;
			//回收过程不允许文件系统相关操作和不允许磁盘操作,跳转到keep_locked标签处
			if (!may_enter_fs)
				goto keep_locked;
			//回收过程不允许页面回写操作,跳转到keep_locked标签处
			if (!sc->may_writepage)
				goto keep_locked;

			/*
			 *刷新TLB,保存页表和cpu快表内容一致
			 */
			try_to_unmap_flush_dirty();
			/*
			 *若当前页是脏匿名页,调用pageout函数进行写入交换分区操作,会根据pageout函数回状态来对对当前页进行处理(匿名页回写操作可阻塞
			 *的):
			 * 1.返回PAGE_KEEP,页面回写磁盘失败,跳转到keep_locked标签处.(执行完while循环该页本转移到非活跃LRU链表)
			 * 2.返回PAGE_ACTIVATE,页面可能正在回写或page被锁,跳转到activate_locked标签处(执行完while循环后,该页被转移到活跃LRU链
			 *   表)
			 * 3.返回PAGE_SUCCESS,表示页面内容已经成功写入存储设备.后续会对该页的状态进行再次验证,若验证通过则可以开始对该页进行释放操
			 *   作,验证不通过会跳转到对应标签处并等while循环结束后再次迁移到非活跃LRU链表
			 * 4.返回PAGE_CLEAN:表示该页面回写成功,已经干净,可以进行释放操作.
			 *少数情况下脏文件页页会调用pageout函数进行数据回写(当前线程是kswapd线程,且当前节点中脏页过多).
			 */
			switch (pageout(page, mapping, sc)) {
			case PAGE_KEEP:
				goto keep_locked;
			case PAGE_ACTIVATE:
				goto activate_locked;
			case PAGE_SUCCESS:
				if (PageWriteback(page))
					goto keep;
				if (PageDirty(page))
					goto keep;

				/*
				 * A synchronous write - probably a ramdisk.  Go
				 * ahead and try to reclaim the page.
				 */
				if (!trylock_page(page))
					goto keep;
				if (PageDirty(page) || PageWriteback(page))
					goto keep_locked;
				mapping = page_mapping(page);
			case PAGE_CLEAN:
				; /* try to free the page below */
			}
		}

		//处理页面用于buffer_head缓存的情况,try_to_release_page用于释放buffer_head缓存
		if (page_has_private(page)) {
			if (!try_to_release_page(page, sc->gfp_mask))
				goto activate_locked;
			if (!mapping && page_count(page) == 1) {
				unlock_page(page);
				if (put_page_testzero(page))
					goto free_it;
				else {
					/*
					 * rare race with speculative reference.
					 * the speculative reference will free
					 * this page shortly, so we may
					 * increment nr_reclaimed here (and
					 * leave it off the LRU).
					 */
					nr_reclaimed++;
					continue;
				}
			}
		}

lazyfree:
		/*
		 *__remove_mapping尝试分离page->mapping.程序执行到此处页面已经完成页面回收的大部分任务。
		 *__remove_mapping函数主要工作:
		 * 1.通过page_ref_freeze函数处理页面的_refcount.
		 * 2.然后分离page->mapping:
		 *	a.对于匿名页面(PG_swapcache置位的页面),用_delete_from_swap_cache来处理交换空间的相关问题.
		 *  b.对于内容缓存文件页,调用_delete_from_page_cache和mapping->a_ops-freepages()函数来分离.
		 */

		if (!mapping || !__remove_mapping(mapping, page, true))
			goto keep_locked;

		__ClearPageLocked(page);
free_it:
		/*
		 *执行到free_it标签处,先统计已回收页面计数,然后将当前页面加入到free_page临时链表中,然后continue退出当前页回收处理,继续遍历
		 *page_list其他页对其进行.页面回收处理。free_page链表中的页会在while循环结束后批量释放到伙伴系统
		 */
		if (ret == SWAP_LZFREE)
			count_vm_event(PGLAZYFREED);

		nr_reclaimed++;

		/*
		 * Is there need to periodically free_page_list? It would
		 * appear not as the counts should be low
		 */
		list_add(&page->lru, &free_pages);
		continue;

cull_mlocked://执行到cull_mlocked标签处
		/*
		 *1.判断该页是否在swap cache中,若在则移除该页与swap cache间的联系.
		 *2.移除swap cache对该页指向的条件是:该页缓存了swap area区域的某个slot中的数据,而该slot数据对应一个内存页。在swap out前,slot
		 *对应的内存页被N个进程共享。只有这N个共享进程都和该页再次建立了映射关系后,该页才能从其对应的swap cache中取出.(N这数据存储在存储
		 *在swap area的swap_map数组中,swap area中每个slot在swap_map中都有数组项与其对应)
		 */
		if (PageSwapCache(page))
			try_to_free_swap(page);
		unlock_page(page);
		
		//将该页隔离到临时链表ret_pages,continue跳过本次循环继续遍历回收链表中其他页.while循环结束后,当前页回个迁移到非活跃LRU链表中.
		list_add(&page->lru, &ret_pages);
		continue;

activate_locked://跳转执行到activate_locked标签处,表示当前页不能回收,会置位该页的active位,后续该页会被迁移到本节点的活跃lru链表
		/* Not a candidate for swapping, so reclaim swap space. */
		if (PageSwapCache(page) && mem_cgroup_swap_full(page))
			try_to_free_swap(page);
		VM_BUG_ON_PAGE(PageActive(page), page);
		SetPageActive(page);
		pgactivate++;
keep_locked://跳转执行到keep_locked标签处,会先将该页的页锁释放,后续被迁移到本节点非活页LRU链表

		unlock_page(page);
keep://跳转执行到keep标签处直接将页释放到本节点非活跃lru链表
		list_add(&page->lru, &ret_pages);
		VM_BUG_ON_PAGE(PageLRU(page) || PageUnevictable(page), page);
	}

	mem_cgroup_uncharge_list(&free_pages);
	//刷新TLB,保存页表和cpu快表内容一致
	try_to_unmap_flush();
	//批量释放fee_pages链表中的页
	free_hot_cold_page_list(&free_pages, true);

	//将ret_pages链表上保存的未成功释放的页迁移到page_list链表,退出函数后根据每页的状态,逐页迁移到对应的LRU链表中
	list_splice(&ret_pages, page_list);
	count_vm_events(PGACTIVATE, pgactivate);
	//统计计算更新
	*ret_nr_dirty += nr_dirty;
	*ret_nr_congested += nr_congested;
	*ret_nr_unqueued_dirty += nr_unqueued_dirty;
	*ret_nr_writeback += nr_writeback;
	*ret_nr_immediate += nr_immediate;
	//返回回收成功的页
	return nr_reclaimed;
}

shrink_page_list函数将从当前节点中挑选出来的不活跃文件页和不活跃匿名页经过层层筛选和处理,最后将节点中适合回收的页统一释放到伙伴系统中,下面分别以匿名页和文件为视角,解析一个页是如何从一个不活跃lru链表中被取出,然后通过重重筛选和处理,最终进入伙伴系统的流程.



匿名页回收流程

了解你们页回写流程前可以先阅读该博文https://www.cnblogs.com/arnoldlu/p/8335508.html,介绍匿名页生命周期.

  1. 给page上锁(trylock_page(page)).

    |若page上锁失败,该页面保留在原先的非活跃LRU链表上
    |若page上锁失败执行步骤2
    
  2. 判断page是否为可回收页(通过函数page_evictable(page)判断,page被标记unevictable或是被锁的VMA的一部分则page为不可回收页).

    |若page为不可回收页:
    	|--->先判断该匿名页是否属于swap cache(PageSwapCache(page)|		|--->若属于需先通过函数try_to_free_swap函数解除该page与其swap cache的联系
    	|--->解除page页锁(unlock_page(page)),然后将page重新插入本节点非活跃LRU链表
    |若page为可回收页:
    	|--->执行步骤3
    
  3. 判断page是否被映射(通过page_mapped(page)函数判断,即是page->_mapcount是否大于0)

    |若该页被映射:
    	|--->判断本次内存回收进程是否允许回收被映射页( if(!sc->may_unmap) )
    			|--->本回收进程不允许回收被映射,先解除page页锁,然后将page重新插入本节点非活跃LRU链表
    			|--->本次回收进程允许回收被映射页,执行步骤4
    |该页未被映射:
    	|--->执行步骤4
    
  4. 检查page是否满足:page正在BDI设备中回写页面,或者page正在回写的过程中并且page马上将要被回收

    |上述条件满足
    	--->则该page可能会导致产生块设备回写堵塞,更新当前计算器*ret_nr_congested++,然后继续执行步骤5
    |上述条件不满足
    	--->执行步骤5
    
  5. 判断page是否正在回写(通过PageWriteback(page)函数判断):

    |若page处于正在回写状态:
    	|--->若page为可回收页(PageReclaim(page)) && 当前回收者是内核kswapd线程(current_is_kswapd()&& 此时PAGE对应的内存节点中有大量
    	|	 正在回写的页面(test_bit(PGDAT_WRITEBACK, &pgdat->flags))
    	|	 	  |--->解除page页锁(unlock_page(page)),然后将page重新插入本节点非活跃LRU链表
    	|--->若page没有被标记可回收(PageReclaim(page)判断) || page分配器的调用者没有使用__GFP_FS或者__GFP_IO.
    	|     	  |--->page设置PG_PageReclaim标志位(通过SetPageReclaim(page)函数),解除page页锁(unlock_page(page)),然后将page重新
    	|			   插入本节点非活跃LRU链表
    	|--->除上面两种情况外
    	|		  |--->当前回收进程睡眠等待页回收完成(wait_on_page_writeback(page)),当page回写完成后,会被重新
    	|			   插入回收扫描链表末尾,然后再次从步骤1开始进行回收操作.
    |若page不处于正在回写状态
    	|--->执行步骤6
    
  6. 执行下列3个操作:

    a. 通过page_referenced检查页面访问引用了多少个pte将结果记录在变量referenced_ptes中(主要是通过反向映射

    统计出所有与page建立映射关系的pte页表项中AF位被置位的页表项总数),该函数还会获映射该物理页的vma的

    vm_flags成员数据.需要注意的是函数在访问了每个pte页表项的AF硬件访问位后会立即将该AF位设置为0.

    b. 通过TestClearPageReferenced函数获取page的flag成员中的软件访问位并将数据结果记录在referenced_page变量

    中,该函数在访问了page的flag成员的软件访问位(PG_referenced)后会立即将该访问位设置为0.

    c. 最后利用a,b获取到的数据来判断该页后续回收流程的走向:

    |若该页对应的vma的vm_flags中VM_LOCKED被置位,执行步骤7(则后续try_to_unmap函数接触该页的相关map,并将该页转移到对应节点的			      
      LRU_UNEVICTABLE链表)
     
    |若该页是以swap作为后备存储的匿名页,shmem共享页...(PageSwapBacked(page)|--->若有用户通过pte页表访问该页,则结束对该页的回收,后续将该页转移到对应节点的活跃匿名页lru链表(转移后page的referenced_ptes为
    	|    0,referenced_page为0).
    	|--->没有用户通过pte页表访问该页,将该页列为可回收的备选页,继续进行后面的回收操作(执行步骤7)
    
  7. 判断该匿名页面page是否分配交换空间(PageSwapCache(page))

    |若page未分配交换空间
    	|--->判断page页面是否允许文件操作和磁盘I/O操作
    	|	    |--->若不允许文件和磁盘I/O操作:解除page页锁,后续将page插入到对应节点的非活跃LRU链表.
    	|		|--->若允许文件和磁盘I/O操作:通过add_to_swap为page分配交换空间.
    	|				|--->page分配交换空间分配失败:解除page页锁,并将该页面插入到对应节点的活跃LRU链表.
        |				|--->page分配交换空间成功:
        |						a.设置页面的PG_swapcache标志位,并将该page插入swap cache对应的address_space结构的page_tree中.
        |						b.让page->private存储该页对应的swap slot在交换空间的位置信息
        |						c.匿名页page->mapping执行发生该变,有匿名页的anon_vma数据结构变成交换分区的swapper_spaces.
        |         				d.并将page的flag成员的PG_dirty位置位(在下面的pageout中将页内容先写入到磁盘),继续执行步骤8
    |若page已经分配交换空间
    	|--->执行步骤8
    
  8. 判断该匿名页page是否被映射(page_mapping(page)不为空且page->_mapcount>=0, page_mapping(page)返回的是该匿名页page的privat成员指向其页槽所在交换区域的swap cache管理器address_space.):

    |若该匿名页page属于swap cache且被映射page->_mapcount>=0
    	|--->调用try_to_unmap()函数来解除page映射的所有pte页表项,并将每个pte页表项重新指向为page在swaparea分配的slot(页槽).
    	|		|--->若所有pte页表项与page映射解除失败时
    	|				|--->设置page的活跃位(SetPageActive(page)),然后解除page页锁,后续将该页面插入到对节点的活跃LRU链表
    	|		|--->若部分pte页表项映射解除成功,部分失败时
    	|				|--->解除page页锁(unlock_page(page)),然后将page重新插入本节点非活跃LRU链表
    	|		|--->若解除某个pte页表项映射,发现该pte页表项被其他进程锁住了时
    	|				|--->先判断该匿名页是否属于swap cache(PageSwapCache(page)|						|--->若属于需先通过函数try_to_free_swap函数解除page与其swap cache的联系,接着解除page页锁(unlock_page(page)),
    	|							 最后将page重新插入本节点非活跃LRU	 
    	|				        |--->若不属于swap cache,则直接解除page页锁(unlock_page(page)),然后将page重新插入本节点非活跃LRU链表	  
    	|						       
    	|		|---->解除映射成功,且page数据回写磁盘完成(SWAP_LZFREE),直接跳转到lazyfree标签处(后续解除page->mapping和swap cache的关系后就可释放回收)。
    	|		|---->若解除所有pte页表项映射成功时(此时映射page的所有pte页表项重新指向了swap area为page所分配的slot(页槽):执行步骤9
    |否则
    	|--->执行步骤9
    
  9. 判断page是否为脏页(PageDirty(page))

    |当前页为脏页(匿名页为脏页,则该匿名页属于swap cache)
    	|--->若page只能在干净状态下回收(page_check_references(page, sc) == PAGEREF_RECLAIM_CLEAN)或者page不允许文件和磁盘I/0操作或者
    	|		回收控制去要求回收过程page不能进行回写操作
    	|	 	|--->解除page页锁(unlock_page(page)),然后将page重新插入本节点非活跃LRU链表
    	|--->刷新TLB,保存页表和cpu块表内容一致
    	|--->调用pageout函数尝试将该脏匿名页page中的内容写入交换分区对应的slot中,根据函数返回值决定page后续回收走向:匿名页回收时刻被阻塞的.
    	|		|--->返回PAGE_KEEP,匿名页page回写数据到swap area对应slot失败:解除page页锁,然后将page重新插入本节点非活跃LRU链表
    	|		|--->返回PAGE_ACTIVATE,匿名页page回写数据到swap area对应slot失败:设置page的活跃位(SetPageActive(page)), 然后解除page
    	|			  页锁,后续将该页面插入到对节点的活跃LRU链表
    	|       |--->返回PAGE_SUCCESS,表示page内容回写swap area操作已经开始执行,该page是否能回收还需要判
    	|			  断page内容回写是否已经完成,通过对page进行PageWriteback(page)和PageDirty(page)验证来
    	|			  判断当前页是正处于回写状态还是已经回写完成.
    	|							--->若正处于回写状态,则解除页锁,并将page重新插入本节点非活跃LRU链表等回写完成后,下次扫描到再进行回收.
    	|							--->若回写完成则执行步骤10
    	|		|--->返回PAGE_CLEAN:表示page回写成功,已经干净,可以进行释放操作,执行步骤10
    |当前页不为脏页
    	|--->执行步骤10
    
  10. 判断page是否有自己的私有buffer(page_has_private(page))

    |若page有用于块设备的buffer_head缓存
    	|--->尝试用try_to_release_page(page, sc->gfp_mask)函数释放该buffer_head.
    	|		|--->若buffer_head释放失败
    	|				|--->设置page的活跃位(SetPageActive(page)),然后解除page页锁,后续将该页面插入到对节点的活跃LRU链表				 
    	|		|--->若buffer_head释放成功
    	|			|--->判断page->mapping为null且page->_refcount=1
    	|					|--->若判断为真,page处于伙伴系统刚分配的状态,则先解除该页的页锁,然后用 put_page_testzero(page)函数将
    	|						 page的引用计数设置为0
    	|								|--->若page的引用计数设置为0成功,则page就会被加入free_pages链表后续统一被释放到伙伴系统.				 
    	|								|--->若page的引用计数设置为0失败,该页回收到此结束,且该页页会脱离LRU链表 
    	|									  
    |若page没有用于块设备的buffer_head缓存
    	|--->执行步骤11
    
  11. 执行到此处,page的大部分回收工作已经完成,对于匿名页page,若PG_swapcache置位,通过__remove_mapping尝试分离匿名页page和其对应的swap cache间的关系.

    |若匿名页page的PG_swapcache置位
    	|--->调用__remove_mapping函数进行page和其对应的swap cache的分离工作(先用page_ref_freeze函数妥善处理page的引用计数,然后用
    	|	 __delete_from_swap_cache函数处理该匿名页page和其对应交换空间的相关问题),最后根据__remove_mapping函数的分离情况来觉得
    	|	 page回收的后续走向。
    	|		|--->__remove_mapping分离失败,则page先释放页锁,最后被重新插入对应的非活跃LRU链表
    	|		|--->分离成功,则先释放page的页锁,然后将该page插入free_pages链表后续统一释放到伙伴系统
    |若page未置位PG_swapcache
    	|--->先释放page的页锁,然后将该page插入free_pages链表后续统一释放到伙伴系统
    
  12. 最后回收进程会将free_pages链表上的所有匿名页通过free_hot_cold_page_list(&free_pages, true)函数批量释放到伙伴系统中去(起始在回收过程中那些被释放到本节点活跃LRU链表和非活跃LRU链表中的页,真实情况也是预先会被缓存在ret_pages临时链表,后续批量将这些页插入到其对应的LRU链表中)



文件页回收流程
  1. 给页面上锁(trylock_page(page)).

    |若page上锁失败,该页面保留在原先的非活跃LRU链表上
    |若page上锁失败执行步骤2
    
  2. 判断page是否为可回收页(通过函数page_evictable(page)判断,page被标记unevictable或是被锁的VMA的一部分则page为不可回收页).

    |若page为不可回收页:
    	|--->解除page页锁(unlock_page(page)),然后将page重新插入本节点非活跃LRU链表
    |若page为可回收页:
    	|--->执行步骤3
    
  3. 判断page是否被映射(通过page_mapped(page)函数判断,即是page->_mapcount是否大于0)

    |若该页被映射:
    	|--->判断本次内存回收进程是否允许回收被映射页( if(!sc->may_unmap) )
    			|--->本回收进程不允许回收被映射,先解除page页锁,然后将page重新插入本节点非活跃LRU链表
    			|--->本次回收进程允许回收被映射页,执行步骤4
    |该页未被映射:
    	|--->执行步骤4
    
  4. 检查page是否是脏页或正在回写页(通过函数page_check_dirty_writeback(page, …)函数检查):

    |若page是脏页或正在回写页
    	|--->本次回收计数器*ret_nr_dirty++
    |若page是脏页,单不是正在回写页
    	|--->本次回写计算器*ret_nr_unqueued_dirty++
    计数器更新后执行步骤5
    
  5. 检查page是否满足:page正在BDI设备中回写页面,或者page正在回写的过程中并且page马上将要被回收

    |上述条件满足
    	--->则该page可能会导致产生块设备回写堵塞,更新当前计算器*ret_nr_congested++,然后继续执行步骤6
    |上述条件不满足
    	--->执行步骤6
    
  6. 判断page是否正在回写(通过PageWriteback(page)函数判断):

    |若page处于正在回写状态:
    	|--->若page为可回收页(PageReclaim(page)) && 当前回收者是内核kswapd线程(current_is_kswapd()&& 此时PAGE对应的内存节点中有大量
    	|	   正在回写的页面(test_bit(PGDAT_WRITEBACK, &pgdat->flags))
    	|	 	  |--->解除page页锁(unlock_page(page)),然后将page重新插入本节点非活跃LRU链表
    	|--->若page没有被标记可回收(PageReclaim(page)判断) || page分配器的调用者没有使用__GFP_FS或者    
    	|      __GFP_IO.
    	|     	  |--->page设置PG_PageReclaim标志位(通过SetPageReclaim(page)函数),解除page页锁(unlock_page(page)),然后将page重新
    	|				插入本节点非活跃LRU链表
    	|--->除上面两种情况外
    	|   	  |--->当前回收进程睡眠等待页回收完成(wait_on_page_writeback(page)),当page回写完成后,会被重新插入回收扫描链表末尾,然后
    	|				 再次从步骤1开始进行回收操作.
    |若page不处于正在回写状态
    	|--->执行步骤6
    
  7. 执行下列3个操作:

    1. 通过page_referenced检查页面访问引用了多少个pte将结果记录在变量referenced_ptes中(主要是通过反向映射 统计出所有与page建立映射关系的pte页表项中AF位被置位的页表项总数),该函数还会获映射该物理页的vma的 vm_flags成员数据.需要注意的是函数在访问了每个pte页表项的AF硬件访问位后会立即将该AF位设置为0.

    2. 通过TestClearPageReferenced函数获取page的flag成员中的软件访问位并将数据结果记录在referenced_page变量中,该函数在访问了page的flag成员的软件访问位(PG_referenced)后会立即将该访问位设置为0.

    3. 最后利用a,b获取到的数据来判断该页后续回收流程的走向:

      |若该页对应的vma的vm_flags中VM_LOCKED被置位,执行步骤8(后续try_to_unmap函数解除该页的相关map,并将该页转移到对应节点
        LRU_UNEVICTABLE链表)。
      |若该页是以磁盘文件作为备存储的文件页
      	|--->有用户通过pte页表访问该页:
      	|       |--->该页映射的文件为可执行文件(vm_flags & VM_EXEC),则将该页的软件访问位设置为1,后续将该页迁移到对应内存节点的活跃
      	|			   文件页lru链表中去
      	|		|--->该页为最近第二次访问的文件缓存页(PG_referend == 1)或则该页是共享的文件缓存页(referenced_ptes > 1),则将该页的
      	|			  软件访问位PG_referend设置为1,后续将该页迁移到对应内存节点的活跃文件页lru链表中去.
      	|		|--->其余被访问引用文件页(该页为软件访问位为0,且只被一个用户通过页表访问的普通文件页),会将其软件访问位设置为1,然
      	|			   后继续保留在对应节点的非活跃lru链表中
      	|--->没有用户通过pte页表访问该页:将该页作为可回收的备选页,执行步骤8
      
  8. 判断page是否有一个或多个用户映射(page->_mapcount>=0,就是该page至少有一个pte页表项建立映射关系),且page->mapping不为空指向一个address_space:

    |若page->_mapcount>0且page->mapping指向一个addree_space
    	|--->调用try_to_unmap()函数来解除这些用户映射的pte
    	|		|--->若所有pte页表项与page映射解除失败时
    	|				|--->设置page的活跃位(SetPageActive(page)),然后解除page页锁,后续将该页面插入到对节点的活跃LRU链表
    	|		|--->若部分pte页表项与page映射解除成功,部分失败时
    	|				|--->解除page页锁(unlock_page(page)),然后将page重新插入本节点非活跃LRU链表
    	|		|--->若解除pte页表项映射时,发现现pte被其他进程锁住时				
    	|				|--->则直接解除page页锁(unlock_page(page)),然后将page重新插入本节点非活跃LRU链表
    	|		|---->若解除pte页表项映射全部成功时
    	|				|--->执行步骤9
    |否则
    	|--->执行步骤9
    
  9. 判断page是否为脏页(PageDirty(page))

    |若page为脏页(需要注意的是回收线程基本不会对脏文件页进行回写,回直接将脏文件页继续保留在不活跃lru链表中,只有一种情况会调用pageout函数
    	 对脏文件页进行回写:kswapd内核线程且page当前节点的PGDAT_DIRTY被置位)
    	|--->(!current_is_kswapd() ||!test_bit(PGDAT_DIRTY, &pgdat->flags)为真
    	|		|--->设置page为PG_reclaim,解除page页锁并将其重新插入对应节点的不活跃LRU链表
    	|--->若目前page回收者是kswapd内核线程且page当前节点的PGDAT_DIRTY被置位,表明节点有大量脏页(有且只有这一种情况linux才会对脏文件页
    	|	   进行回收,回收前还需进行一系列的验证才能真正回收page)
    	|		|--->若page只能在干净状态下回收(page_check_references(page, sc)==PAGEREF_RECLAIM_CLEAN 或者page不允许文件和磁盘I/0操
    	|			   作(may_enter_fs==0)或者回收控制器要求回收过程page不能进行回写操作(sc->may_writepage==0)
    	|				|--->解除page页锁(unlock_page(page)),然后将page重新插入本节点非活跃LRU链表
    	|		|--->刷新TLB,保存页表和cpu块表内容一致
    	|		|--->调用pageout函数尝试将该脏文件页page中的内容写入对应文件中(根据函数返回值决定page后续回收走向)
    	|				|--->返回PAGE_KEEP,页面回写文件失败:解除page页锁(unlock_page(page)),然后将page重新插入本节点非活跃LRU链表
    	|				|--->返回PAGE_ACTIVATE,页面可能正在回写且page被锁:设置page的活跃位(SetPageActive(page)),然后解除page页锁,后
    	|					   续将该页面插入到对节点的活跃LRU链表
    	|       		|--->返回PAGE_SUCCESS,表示page内容回写到文件的操作已经开始,若想对该page进行回收,还需要验证页回写是否已经完成
    	|					  (可能还在进行中),通过 PageWriteback(page)和PageDirty(page)来验证.
    	|							|--->验证通过则执行步骤10
    	|							|--->验证失败,则解除页锁,并将page重新插入本节点非活跃LRU链表,等待该页回写完成,下次扫描到该页若回
    	|								   写完成则可对其进行回收.
    	|				 |--->返回PAGE_CLEAN:表示page回写成功,已经干净,可以进行释放操作,执行步骤10	
    |若page不为脏页
    	|--->执行步骤10
    
  10. 判断page是否有自己的私有buffer(page_has_private(page))

    |若page有用于块设备的buffer_head缓存
    	|--->尝试用try_to_release_page(page, sc->gfp_mask)函数释放该buffer_head.
    	|		|--->若buffer_head释放失败
    	|				|--->设置page的活跃位(SetPageActive(page)),然后解除page页锁,后续将该页面插入到对节点的活跃LRU链表
    	|		|--->若buffer_head释放成功
    	|			|--->判断:page->mapping为null且page->_refcount==1
    	|					|--->若判断为真,page处于伙伴系统刚分配的状态,则先解除该页的页锁,然后用
    	|						  put_page_testzero(page)函数将page的引用计数设置为0
    	|								|--->若page的引用计数设置为0成功,则page就会被加入free_pages链表后续统一被释放到伙伴系统.
    	|								|--->若page的引用计数设置为0失败,该页回收到此结束,且该页页会脱离LRU链表
    	|					|--->若判断为假:执行步骤11
    |若page没有用于块设备的buffer_head缓存
    	|--->执行步骤11
    
  11. 执行到此处,page的部分回收工作已经完成.这里若page的mapping不为NULL,则调用__remove_mapping函数来对对page和其成员mapping进行分离。

    |若page->mapping不为NULL
    	|--->调用__remove_mapping函数进行page和其mapping成员的分离工作(先用page_ref_freeze函数妥善处理
    	|	 page的引用计数,然后用__delete_from_page_cache函数和page->mapping->a_ops->freepage函数处理
    	|    该page的文件缓存相关问题),最后根据__remove_mapping函数的分离情况来觉得page回收的后续走向。
    	|		|--->__remove_mapping分离失败,则page先释放页锁,最后被重新插入对应的非活跃LRU链表
    	|		|--->分离成功,则先释放page的页锁,然后将该page插入free_pages链表后续统一释放到伙伴系统
    |若page->mapping为NULL
    	|--->先释放page的页锁,然后将该page插入free_pages链表后续统一释放到伙伴系统
    
  12. 最后回收进程会将free_pages链表上的所有文件页通过free_hot_cold_page_list(&free_pages, true)函数批量释放到伙伴系统中去(起始在回收过程中那些被释放到本节点活跃LRU链表和非活跃LRU链表中的页,真实情况也是预先会被缓存在ret_pages临时链表,后续批量将这些页插入到其对应的LRU链表中)



1.2 shrink_slab函数

linux 会先向os内核注册一些shrinker函数,存储在shrink_list链表中,当os内存较少时会利用该函数主动释放一些缓存空间。

shrink_slab 函数会遍历shrinker_list链表,利用链表中的每个shrinker函数分别对各种不同的缓存进行回收处理.注册 shrinker 是通过函数 set_shrinker() 实现的,解除 shrinker 注册是通过函数 remove_shrinker() 实现的.下面举例Linux 操作系统一些常见的 shrinker 函数:

  1. shrink_dcache_memory():该 shrinker 函数负责 dentry 缓存。
  2. shrink_icache_memory():该 shrinker 函数负责 inode 缓存。
  3. mb_cache_shrink_fn():该 shrinker 函数负责用于文件系统元数据的缓存

对于shrink_list链表中的每个shrinker接口,shrinker->count_objects会返回该接口对应的slab cache中有多少空闲的缓存,而shrinker->scan_objects会扫描这些空闲的缓存并进行释放.

ps:shrinker函数和slab不是一定相关的,之所以使用shrinker_slab这个名字,是因为早期仅仅slab类型的cache需要被shrink。

/**
 * shrink_slab - shrink slab caches
 * @gfp_mask: allocation context
 * @nid: node whose slab caches to target
 * @memcg: memory cgroup whose slab caches to target
 * @nr_scanned: pressure numerator
 * @nr_eligible: pressure denominator
 *
 * Call the shrink functions to age shrinkable caches.
 *
 * @nid is passed along to shrinkers with SHRINKER_NUMA_AWARE set,
 * unaware shrinkers will receive a node id of 0 instead.
 *
 * @memcg specifies the memory cgroup to target. If it is not NULL,
 * only shrinkers with SHRINKER_MEMCG_AWARE set will be called to scan
 * objects from the memory cgroup specified. Otherwise, only unaware
 * shrinkers are called.
 *
 * @nr_scanned and @nr_eligible form a ratio that indicate how much of
 * the available objects should be scanned.  Page reclaim for example
 * passes the number of pages scanned and the number of pages on the
 * LRU lists that it considered on @nid, plus a bias in @nr_scanned
 * when it encountered mapped pages.  The ratio is further biased by
 * the ->seeks setting of the shrink function, which indicates the
 * cost to recreate an object relative to that of an LRU page.
 *
 * Returns the number of reclaimed slab objects.
 */
static unsigned long shrink_slab(gfp_t gfp_mask, int nid,
				 struct mem_cgroup *memcg,
				 unsigned long nr_scanned,
				 unsigned long nr_eligible)
{
	struct shrinker *shrinker;
	unsigned long freed = 0;

	if (memcg && (!memcg_kmem_enabled() || !mem_cgroup_online(memcg)))
		return 0;

	if (nr_scanned == 0)
		nr_scanned = SWAP_CLUSTER_MAX;

	if (!down_read_trylock(&shrinker_rwsem)) {
		/*
		 * If we would return 0, our callers would understand that we
		 * have nothing else to shrink and give up trying. By returning
		 * 1 we keep it going and assume we'll be able to shrink next
		 * time.
		 */
		freed = 1;
		goto out;
	}
	//遍历shrinker_list列表,提取shrinker
	list_for_each_entry(shrinker, &shrinker_list, list) {
		struct shrink_control sc = {
			.gfp_mask = gfp_mask,
			.nid = nid,
			.memcg = memcg,
		};

		/*
		 * If kernel memory accounting is disabled, we ignore
		 * SHRINKER_MEMCG_AWARE flag and call all shrinkers
		 * passing NULL for memcg.
		 */
		if (memcg_kmem_enabled() &&
		    !!memcg != !!(shrinker->flags & SHRINKER_MEMCG_AWARE))
			continue;

		if (!(shrinker->flags & SHRINKER_NUMA_AWARE))
			sc.nid = 0;
		/*
		 *以shrink_control和shrinker为参数进行对应slab cache收缩操作:主要是通过shrinker->scan_objects对该接口
		 *对应的slab cache中的shrinker->count_objects个空闲缓存进行扫描和释放操作
		 */
		freed += do_shrink_slab(&sc, shrinker, nr_scanned, nr_eligible);
	}

	up_read(&shrinker_rwsem);
out:
	cond_resched();
	return freed;
}

shrink_node函数流程比较复杂,本文档是我对该源码进行首次分析并结合网上相关资料而得出的一些心得,可能存在理解有误地方,后续会不断修正.

下面推荐一篇归纳性较强的内存回收相关博文:

https://www.cnblogs.com/LoyenWang/p/11827153.html



版权声明:本文为u010923083原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。