Linux 内存管理:LRU 链表 (1)

文章目录

  • [1. 前言](#1. 前言)
  • [2. LRU 链表的作用](#2. LRU 链表的作用)
  • [3. LRU 链表的实现概要](#3. LRU 链表的实现概要)
    • [3.1 LRU 链表主要数据结构](#3.1 LRU 链表主要数据结构)
    • [3.2 LRU 链表的初始化](#3.2 LRU 链表的初始化)
    • [3.3 添加 page 到 LRU 链表](#3.3 添加 page 到 LRU 链表)
      • [3.3.1 添加 page 到 per-cpu 的 `lru_add_pvec` 向量](#3.3.1 添加 page 到 per-cpu 的 lru_add_pvec 向量)
      • [3.3.2 添加 page 到 per-cpu 的 `lru_rotate_pvecs` 向量](#3.3.2 添加 page 到 per-cpu 的 lru_rotate_pvecs 向量)
      • [3.3.3 添加 page 到 per-cpu 的 `lru_deactivate_file_pvecs` 向量](#3.3.3 添加 page 到 per-cpu 的 lru_deactivate_file_pvecs 向量)
      • [3.3.4 添加 page 到 per-cpu 的 `lru_lazyfree_pvecs` 向量](#3.3.4 添加 page 到 per-cpu 的 lru_lazyfree_pvecs 向量)
      • [3.3.5 添加 page 到 per-cpu 的 `activate_page_pvecs` 向量](#3.3.5 添加 page 到 per-cpu 的 activate_page_pvecs 向量)
    • [3.4 page 在 LRU 链表间的迁移](#3.4 page 在 LRU 链表间的迁移)
      • [3.4.1 新增](#3.4.1 新增)
      • [3.4.2 迁移](#3.4.2 迁移)
      • [3.4.3 手动触发 page 新增 和 迁移 情形](#3.4.3 手动触发 page 新增 和 迁移 情形)
  • [4. 小结](#4. 小结)
  • [5. 参考链接](#5. 参考链接)

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. LRU 链表的作用

什么是 的 LRU(Least Recently Used)?学习过操作系统理论的读者,对此应该不会感到陌生,在 Linux 中,LRU 指最近最少使用的内存,通过选择性的将一些可能参与内存回收的页面加入不同类型的 LRU 链表,然后在内存回收过程中,按不同优先级排列的各类型 LRU 链表中挑选要回收的页面。想要理解 Linux 内存回收过程,就跳不过 Linux 页面的 LRU 链表管理。

3. LRU 链表的实现概要

本文基于 Linux 4.14.111 + ARM 架构 分析 LRU 页面链表的实现概要。

3.1 LRU 链表主要数据结构

LRU 列表是基于每 NUMA 节点的:

c 复制代码
// include/linux/mmzone.h

/*
 * We do arithmetic on the LRU lists in various places in the code,
 * so it is important to keep the active lists LRU_ACTIVE higher in
 * the array than the corresponding inactive lists, and to keep
 * the *_FILE lists LRU_FILE higher than the corresponding _ANON lists.
 *
 * This has to be kept in sync with the statistics in zone_stat_item
 * above and the descriptions in vmstat_text in mm/vmstat.c
 */
#define LRU_BASE 0
#define LRU_ACTIVE 1
#define LRU_FILE 2

enum lru_list {
	LRU_INACTIVE_ANON = LRU_BASE, /* 未激活的匿名页面链表 */
	LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE, /* 活动的匿名页面链表 */
	LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE, /* 未激活的文件 page cache 页面链表 */
	LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE, /* 活动的文件 page cache 页面链表 */
	LRU_UNEVICTABLE, /* 不可回收的页面链表, 包括 文件 page cache 和 匿名 页面 */
	NR_LRU_LISTS
};

struct lruvec {
	struct list_head		lists[NR_LRU_LISTS];
	struct zone_reclaim_stat	reclaim_stat;
	/* Evictions & activations on the inactive file list */
	atomic_long_t			inactive_age;
	/* Refaults at the time of last reclaim cycle */
	unsigned long			refaults;
#ifdef CONFIG_MEMCG
	struct pglist_data *pgdat;
#endif
};

typedef struct pglist_data {
	...
	spinlock_t		lru_lock; /* 每 NUMA 节点所有类型 LRU 链表公用的锁  */

	...
	/* Fields commonly accessed by the page reclaim scanner */
	struct lruvec		lruvec; /* 每 NUMA 节点各类型的 LRU 链表 */
	...
};

将上面每 NUMA 节点 LRU page 链表结构图像化,如下图:

另外,从上面数据结构了解到,一个 NUMA 节点各类型 LRU 链表公用了一把锁(pglist_data::lru_lock),将导致 NUMA 节点的 LRU 链表操作的激烈锁竞争,为缓解这个问题,引入 per-cpu 的 page 向量:先将 page 添加到 per-cpu page 向量,等 per-cpu LRU 向量填满了,再一次性的将这些 page 添加到 NUMA 节点对应类型的 LRU 链表,如此避免大量的 LRU 锁竞争。

per-cpu page 向量用数据结构 struct pagevec 描述:

c 复制代码
// include/linux/pagevec.h

/* 14 pointers + two long's align the pagevec structure to a power of two */
#define PAGEVEC_SIZE	14

struct pagevec {
	unsigned long nr;
	unsigned long cold;
	struct page *pages[PAGEVEC_SIZE];
};

再看一下定义的 5per-cpu page 向量

c 复制代码
// mm/swap.c

static DEFINE_PER_CPU(struct pagevec, lru_add_pvec);
static DEFINE_PER_CPU(struct pagevec, lru_rotate_pvecs);
static DEFINE_PER_CPU(struct pagevec, lru_deactivate_file_pvecs);
static DEFINE_PER_CPU(struct pagevec, lru_lazyfree_pvecs);
#ifdef CONFIG_SMP
static DEFINE_PER_CPU(struct pagevec, activate_page_pvecs);
#endif

3.2 LRU 链表的初始化

  • 每 NUMA LRU 链表初始化

BOOT 期间,内存管理系统初始化将 NUMA LRU page 链表初始化为空:

c 复制代码
bootmem_init()
	zone_sizes_init()
		free_area_init_node()
			free_area_init_core()

static void __paginginit free_area_init_core(struct pglist_data *pgdat)
{
	...
	/* 初始化 NUMA 节点的所有 5 种类型(LRU_INACTIVE_ANON,...) 的 LRU 列表为空 */
	lruvec_init(node_lruvec(pgdat));
	...
}
  • per-cpu LRU 向量初始化

per-cpu LRU page 向量初始化为空:

c 复制代码
// mm/swap.c

static DEFINE_PER_CPU(struct pagevec, lru_add_pvec);
static DEFINE_PER_CPU(struct pagevec, lru_rotate_pvecs);
static DEFINE_PER_CPU(struct pagevec, lru_deactivate_file_pvecs);
static DEFINE_PER_CPU(struct pagevec, lru_lazyfree_pvecs);
#ifdef CONFIG_SMP
static DEFINE_PER_CPU(struct pagevec, activate_page_pvecs);
#endif

3.3 添加 page 到 LRU 链表

3.3.1 添加 page 到 per-cpu 的 lru_add_pvec 向量

通过 __lru_cache_add() 将 page 添加到 per-cpu 的 lru_add_pvec 向量,如果以下两种情形之一:

  • lru_add_pvec 向量满
  • page 为复合页(compound)

则将 page 迁移到 NUMA 对应类型的 LRU 列表:

c 复制代码
static void __lru_cache_add(struct page *page)
{
	struct pagevec *pvec = &get_cpu_var(lru_add_pvec); /* per-cpu 的 LRU 向量 */

	get_page(page);
	/* 添加 page 导致 per-cpu LRU 向量满了 或 page 为 复合页, 都要将 page 迁移到 NUMA 的 LRU 列表 */
	if (!pagevec_add(pvec, page) || PageCompound(page))
		__pagevec_lru_add(pvec);
	put_cpu_var(lru_add_pvec);
}

/*
 * Add the passed pages to the LRU, then drop the caller's refcount
 * on them.  Reinitialises the caller's pagevec.
 */
void __pagevec_lru_add(struct pagevec *pvec)
{
	pagevec_lru_move_fn(pvec, __pagevec_lru_add_fn, NULL);
}

接下来看看有哪些类型的 page 会添加到 per-cpu 的 lru_add_pvec 向量。

  • lru_cache_add_anon() 添加 {inactive,anon} page 到 lru_add_pvec
c 复制代码
void lru_cache_add_anon(struct page *page)
{
	if (PageActive(page))
		ClearPageActive(page); /* 清除 PG_active 标记 */
	__lru_cache_add(page);
}

如共享内存 page。

  • lru_cache_add_file() 添加 {inactive, file} page 到 lru_add_pvec
c 复制代码
void lru_cache_add_file(struct page *page)
{
	if (PageActive(page))
		ClearPageActive(page); /* 清除 PG_active 标记 */
	__lru_cache_add(page);
}

如文件 read ahead page cache。

  • lru_cache_add() 添加 {[in]active, [file|anon]} page 到 lru_add_pvec

lru_cache_add() 添加到 lru_add_pvec 的 page 可能是 {[in]active, [file|anon]} 组合的任一类型,page 添加到 lru_add_pvec 后,可能在某处通过 mark_page_accessed() 添加 page 到 per-cpu 的 activate_page_pvecs,并最终添加到 NUMA 的 active 类型 LRU 链表。

lru_cache_add() 添加到 lru_add_pvec 的 page 最终的类型 ({[in]active, [file|anon]}),将推迟到从 per-cpu page 向量移动到 NUMA 的 LRU 链表时确定,因为在此之前,都可能改变 page 的类型。

3.3.2 添加 page 到 per-cpu 的 lru_rotate_pvecs 向量

  • rotate_reclaimable_page() 添加 {inactive, file} page 到 lru_rotate_pvecs

同步或异步内存回收时,将 page cache 的 dirty page 写往磁盘时,意味着该 page 即将可被回收,调用 rotate_reclaimable_page(){inactive, file} page 添加到 lru_rotate_pvecs,如果添加 page 导致 lru_rotate_pvecs 或 page 是复合页,将 page 从 lru_rotate_pvecs 迁移到 NUMA 对应类型的 LRU 链表。

c 复制代码
end_page_writeback()
	rotate_reclaimable_page()

/*
 * Writeback is about to end against a page which has been marked for immediate
 * reclaim.  If it still appears to be reclaimable, move it to the tail of the
 * inactive list.
 */
void rotate_reclaimable_page(struct page *page)
{
	if (!PageLocked(page) && !PageDirty(page) &&
	    !PageUnevictable(page) && PageLRU(page)) {
		struct pagevec *pvec;
		unsigned long flags;

		get_page(page);
		local_irq_save(flags);
		pvec = this_cpu_ptr(&lru_rotate_pvecs);
		/* 添加 page 导致 per-cpu LRU 向量满了 或 page 为 复合页, 都要将 page 迁移到 NUMA 的 LRU 列表 */
		if (!pagevec_add(pvec, page) || PageCompound(page))
			pagevec_move_tail(pvec);
		local_irq_restore(flags);
	}
}

3.3.3 添加 page 到 per-cpu 的 lru_deactivate_file_pvecs 向量

通过 deactivate_file_page() 调用,将可被回收的、处于 active 的 file page cache,转为 inactive,添加到 per-cpu 的 lru_deactivate_file_pvecs 向量:

c 复制代码
/**
 * deactivate_file_page - forcefully deactivate a file page
 * @page: page to deactivate
 *
 * This function hints the VM that @page is a good reclaim candidate,
 * for example if its invalidation fails due to the page being dirty
 * or under writeback.
 */
void deactivate_file_page(struct page *page)
{
	/*
	 * In a workload with many unevictable page such as mprotect,
	 * unevictable page deactivation for accelerating reclaim is pointless.
	 */
	if (PageUnevictable(page))
		return;

	if (likely(get_page_unless_zero(page))) {
		struct pagevec *pvec = &get_cpu_var(lru_deactivate_file_pvecs);

		/* 添加 page 导致 per-cpu LRU 向量满了 或 page 为 复合页, 都要将 page 迁移到 NUMA 的 LRU 列表 */
		if (!pagevec_add(pvec, page) || PageCompound(page))
			pagevec_lru_move_fn(pvec, lru_deactivate_file_fn, NULL);
		put_cpu_var(lru_deactivate_file_pvecs);
	}
}

所以,per-cpu 的 lru_deactivate_file_pvecs 向量中存放的是作为回收候选的、 {active,file} page,如遇 lru_deactivate_file_pvecs 向量存满 或 page 为 复合页,则将 page 转为 {inactive,file} page 并移动到 NUMA 对应类型的 LRU 链表。

3.3.4 添加 page 到 per-cpu 的 lru_lazyfree_pvecs 向量

madvise(MADV_DONTNEED)madvise(MADV_FREE) 建议释放的物理 page,这些 page 可能即将可用,mark_page_lazyfree() 将这些 {invative, anno} page 添加到 per-cpu 的 lru_lazyfree_pvecs 向量,,如果添加 page 导致 lru_lazyfree_pvecs 或 page 是复合页,将 page 从 lru_lazyfree_pvecs 迁移到 NUMA 对应类型的 LRU 链表。

c 复制代码
/**
 * mark_page_lazyfree - make an anon page lazyfree
 * @page: page to deactivate
 *
 * mark_page_lazyfree() moves @page to the inactive file list.
 * This is done to accelerate the reclaim of @page.
 */
void mark_page_lazyfree(struct page *page)
{
	if (PageLRU(page) && PageAnon(page) && PageSwapBacked(page) &&
	    !PageSwapCache(page) && !PageUnevictable(page)) {
		struct pagevec *pvec = &get_cpu_var(lru_lazyfree_pvecs);

		get_page(page);
		/* 添加 page 导致 per-cpu LRU 向量满了 或 page 为 复合页, 都要将 page 迁移到 NUMA 的 LRU 列表 */
		if (!pagevec_add(pvec, page) || PageCompound(page))
			pagevec_lru_move_fn(pvec, lru_lazyfree_fn, NULL);
		put_cpu_var(lru_lazyfree_pvecs);
	}
}

3.3.5 添加 page 到 per-cpu 的 activate_page_pvecs 向量

c 复制代码
void activate_page(struct page *page)
{
	page = compound_head(page);
	if (PageLRU(page) && !PageActive(page) && !PageUnevictable(page)) {
		struct pagevec *pvec = &get_cpu_var(activate_page_pvecs);

		get_page(page);
		/* 添加 page 导致 per-cpu LRU 向量满了 或 page 为 复合页, 都要将 page 迁移到 NUMA 的 LRU 列表 */
		if (!pagevec_add(pvec, page) || PageCompound(page))
			pagevec_lru_move_fn(pvec, __activate_page, NULL);
		put_cpu_var(activate_page_pvecs);
	}
}
  • page swap in
c 复制代码
static int handle_pte_fault(struct vm_fault *vmf)
{
	...
	if (!pte_present(vmf->orig_pte))
		return do_swap_page(vmf); /* swap 页面 换入 */
	...
}

int do_swap_page(struct vm_fault *vmf)
{
	...
	if (page == swapcache) {
		do_page_add_anon_rmap(page, vma, vmf->address, exclusive);
		...
		activate_page(page);
	} else { /* ksm created a completely new copy */
		...
	}
	...
}
  • mark_page_accessed() 调用场景

如 file read ahead page。

c 复制代码
/*
 * 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);
	if (!PageActive(page) && !PageUnevictable(page) &&
			PageReferenced(page)) {

		/*
		 * If the page is on the LRU, queue it for activation via
		 * activate_page_pvecs. Otherwise, assume the page is on a
		 * pagevec, mark it active and it'll be moved to the active
		 * LRU on the next drain.
		 */
		if (PageLRU(page)) /* 语义上相当于 if (page->flags & PG_lru) */
			activate_page(page);
		else
			...
		ClearPageReferenced(page);
		...
	} else if (!PageReferenced(page)) {
		...
	}
	...
}

3.4 page 在 LRU 链表间的迁移

有了 3.3 小节的基础,我们就可以分析 page 在 LRU 列表间的迁移了。可以分为新增迁移两大类:

  • 新增

    page 当前不在 NUMA LRU,然后从 per-cpu lru_add_pvec 向量添加到 NUMA 对应类型的 LRU 列表,即:page => lru_add_pvec => 某类型 NUMA LRU

  • 迁移

    page 当前位于 NUMA LRU,然后从当前类型的 NUMA LRU 列表,以某个 per-cpu page 向量为跳板,迁移到另一种类型的 NUMA LRU,即:类型 A NUMA LRU => per-cpu page 向量 => 类型 A NUMA LRU

3.4.1 新增

从前面 3.3.1 分析了解到,lru_cache_add() 将 page 添加到 per-cpu lru_add_pvec 向量,lru_add_pvec 向量可能包含以下所有 5 种类型的 page:

c 复制代码
enum lru_list {
	LRU_INACTIVE_ANON = LRU_BASE,
	LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
	LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
	LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
	LRU_UNEVICTABLE,
	NR_LRU_LISTS
};

然后在 lru_add_pvec 向量满 或 添加复合页 时,通过 pagevec_lru_move_fn() 回调 __pagevec_lru_add_fn()lru_add_pvec 向量中的 page 移入 NUMA 对应类型的 LRU 链表:

c 复制代码
/* 将 page 从 per-cpu 的 pagevec @pvec 移动到 page 所属 NUMA 节点的 LRU 列表 */
static void pagevec_lru_move_fn(struct pagevec *pvec,
	void (*move_fn)(struct page *page, struct lruvec *lruvec, void *arg),
	void *arg)
{
	int i;
	struct pglist_data *pgdat = NULL;
	struct lruvec *lruvec;
	unsigned long flags = 0;

	/*
	 * 逐个 page 移动: 
	 * per-cpu pagevec => page 对应的 NUMA 节点的 LRU 列表.
	 *
	 * 为什么要逐个 page 移动? 因为 @pvec 上每个 page 可能
	 * 分别属于不同的 NUMA 节点.
	 */
	for (i = 0; i < pagevec_count(pvec); i++) {
		struct page *page = pvec->pages[i];
		struct pglist_data *pagepgdat = page_pgdat(page); /* page 关联 (NUMA 节点) 的 pg_data_t */

		if (pagepgdat != pgdat) {
			if (pgdat)
				spin_unlock_irqrestore(&pgdat->lru_lock, flags);
			pgdat = pagepgdat;
			spin_lock_irqsave(&pgdat->lru_lock, flags);
		}

		lruvec = mem_cgroup_page_lruvec(page, pgdat); /* NUMA 节点的 LRU 向量 */
		/* 将 @page 从 per-cpu 的 pagevec 移动到 NUMA 节点的 LRU 列表 */
		/*
		 * lru_add_pvec => NUMA LRU: __pagevec_lru_add_fn()
		 * lru_rotate_pvecs => NUMA LRU: pagevec_move_tail_fn()
		 * lru_deactivate_file_pvecs => NUMA LRU: lru_deactivate_file_fn()
		 * lru_lazyfree_pvecs => NUMA LRU: lru_lazyfree_fn()
		 * activate_page_pvecs => NUMA LRU: __activate_page()
		 */
		(*move_fn)(page, lruvec, arg);
	}
	if (pgdat)
		spin_unlock_irqrestore(&pgdat->lru_lock, flags);
	/* 将 @pages 中的 page 引用计数减 1, 对引用计数归 0 的 page, 归还给 buddy */
	release_pages(pvec->pages, pvec->nr, pvec->cold);
	/* @pvec 的 page 已经悉数移动到 NUMA 节点的 LRU 列表, 因此清零其 page 计数 */
	pagevec_reinit(pvec);
}

static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec,
				 void *arg)
{
	int file = page_is_file_cache(page);
	int active = PageActive(page);
	/*
	 * 确定 page 应该放入的 LRU 列表类型:
	 * . PageUnevictable(page) => LRU_UNEVICTABLE
	 * . file cache
	 *   . PageActive(page) => LRU_ACTIVE_FILE
	 *   . !PageActive(page) => LRU_INACTIVE_FILE
	 * . anon
	 *   . PageActive(page) => LRU_ACTIVE_ANON
	 *   . !PageActive(page) => LRU_INACTIVE_ANON
	 */
	enum lru_list lru = page_lru(page);

	VM_BUG_ON_PAGE(PageLRU(page), page); /* (1) page 不应该已经进入 LRU, 这里应该是新添加的 */

	SetPageLRU(page); /* 标记 page 位于 LRU 列表: page->flags |= PG_lru */
	add_page_to_lru_list(page, lruvec, lru); /* (2) 将 page 添加到 @lru 类型 LRU 列表 (lruvec->lists[lru]) 头部 */
	...
}

上面代码注释 (1) 告诉我们,当前 page 不在 NUMA LRU 链表内;注释 (2)add_page_to_lru_list() 新增 page 到对应的 NUMA LRU 链表:

c 复制代码
/* 将 page 添加到 @lru 类型 LRU 列表 (lruvec->lists[lru]) 头部 */
static __always_inline void add_page_to_lru_list(struct page *page,
				struct lruvec *lruvec, enum lru_list lru)
{
	update_lru_size(lruvec, lru, page_zonenum(page), hpage_nr_pages(page));
	list_add(&page->lru, &lruvec->lists[lru]);
}

因为是新增操作,意味 page 当前不在任何 NUMA LRU 链表内,所以没有从 __pagevec_lru_add_fn() 没有从旧 LRU 链表移除的操作,这是和后面的 page 在 LRU 链表间迁移过程最大的区别。

3.4.2 迁移

page 在不同类型 LRU 链表间迁移分为 4 种场景,我们一一讨论。

  • rotate:可回收 page 从同一 LRU 链表的当前位置移动到尾部

3.3.2 中了解到,rotate_reclaimable_page() 将位于某类型 LRU 链表的 page,添加到 per-cpu 的 lru_rotate_pvecs 向量中,这时候 page 同时位于 NUMA LRU 链表和 lru_rotate_pvecs 向量,直到 lru_rotate_pvecs 向量满或添加了复合页,此时触发 page 移动:

c 复制代码
void rotate_reclaimable_page(struct page *page)
{
	if (!PageLocked(page) && !PageDirty(page) &&
	    !PageUnevictable(page) && PageLRU(page)) {
		struct pagevec *pvec;
		unsigned long flags;

		...
		pvec = this_cpu_ptr(&lru_rotate_pvecs);
		/* 添加 page 导致 per-cpu LRU 向量满了 或 page 为 复合页, 都要将 page 迁移到 NUMA 的 LRU 列表 */
		if (!pagevec_add(pvec, page) || PageCompound(page))
			pagevec_move_tail(pvec);
		...
	}
}

static void pagevec_move_tail(struct pagevec *pvec)
{
	int pgmoved = 0;

	pagevec_lru_move_fn(pvec, pagevec_move_tail_fn, &pgmoved);
	...
}

static void pagevec_move_tail_fn(struct page *page, struct lruvec *lruvec,
				 void *arg)
{
	int *pgmoved = arg;

	if (PageLRU(page) && !PageUnevictable(page)) {
		del_page_from_lru_list(page, lruvec, page_lru(page)); /* page 从当前位置移除 */
		ClearPageActive(page); /* 清除 PG_active */
		add_page_to_lru_list_tail(page, lruvec, page_lru(page)); /* page 添加到同一 LRU 尾部 */
		(*pgmoved)++;
	}
}

pagevec_move_tail_fn() 可见,page 从当前位置移动到同一 LRU 链表尾部,这主要是在那些文件脏页回写时,放在 {inactive, file} LRU 链表尾部,可以更快的被回收(页面回收从 inactive LRU 链表尾部开始)。注意,pagevec_move_tail_fn() 是在 pagevec_lru_move_fn() 的 pagevec 循环中被回调,所以 rotate 的是整个 lru_rotate_pvecs 向量中 page,即将 lru_rotate_pvecs 向量中的 page 倒序放置了。

  • file page: active => inactive

3.3.3 中了解到,deactivate_file_page(){active, file} 的 page,转换为 {inactive, file},以 lru_deactivate_file_pvecs 向量为跳板,通过 pagevec_lru_move_fn() 回调 lru_deactivate_file_fn(),将 lru_deactivate_file_pvecs 向量中的所有 page,从 NUMA 节点的 LRU_ACTIVE_FILE 类型 LRU 链表,迁移到 LRU_INACTIVE_FILE 类型 LRU 链表:

c 复制代码
static void lru_deactivate_file_fn(struct page *page, struct lruvec *lruvec,
			      void *arg)
{
	int lru, file;
	bool active;

	if (!PageLRU(page)) /* page 当前不位于 LRU, 则什么都不做 */
		return;

	if (PageUnevictable(page)) /* page 不能被回收 */
		return;

	/* Some processes are using the page */
	if (page_mapped(page)) /* page 还在某些进程使用, 不能作为回收候选页 */
		return;

	active = PageActive(page);
	file = page_is_file_cache(page);
	lru = page_lru_base_type(page);

	del_page_from_lru_list(page, lruvec, lru + active); /* 从 旧的 {lru + active} 类型 LRU 链表移除 */
	ClearPageActive(page); /* 清除 PG_active */
	ClearPageReferenced(page); /* 清除 PG_referenced */
	add_page_to_lru_list(page, lruvec, lru); /* 添加到 新的 {lru} 类型 LRU 链表 */

	if (PageWriteback(page) || PageDirty(page)) { /* 如果是 回写页 或 脏页 */
		/*
		 * PG_reclaim could be raced with end_page_writeback
		 * It can make readahead confusing.  But race window
		 * is _really_ small and  it's non-critical problem.
		 */
		SetPageReclaim(page); /* 标记为可回收 */
	} else {
		/*
		 * The page's writeback ends up during pagevec
		 * We moves tha page into tail of inactive.
		 */
		list_move_tail(&page->lru, &lruvec->lists[lru]);
		...
	}

	...
}
  • file page: {anno} => {inactive, file}

3.3.4 中了解到,mark_page_lazyfree()madvise() 标记的可释放 page,以 lru_lazyfree_pvecs 为跳板,通过 pagevec_lru_move_fn() 回调 lru_lazyfree_fn(),将 lru_lazyfree_pvecs 中的所有 page,从 anno 类型 LRU 链表,迁移到 {inactive, file} LRU,作为可回收内存 page:

c 复制代码
static void lru_lazyfree_fn(struct page *page, struct lruvec *lruvec,
			    void *arg)
{
	if (PageLRU(page) && PageAnon(page) && PageSwapBacked(page) &&
	    !PageSwapCache(page) && !PageUnevictable(page)) {
		bool active = PageActive(page);

		del_page_from_lru_list(page, lruvec,
				       LRU_INACTIVE_ANON + active); /* 从旧的 匿名页类型 LRU 链表移除 */
		ClearPageActive(page); /* 清除 PG_active */
		ClearPageReferenced(page); /* 清除 PG_referenced */
		/*
		 * lazyfree pages are clean anonymous pages. They have
		 * SwapBacked flag cleared to distinguish normal anonymous
		 * pages
		 */
		ClearPageSwapBacked(page); /* 清除 PG_swapbacked */
		add_page_to_lru_list(page, lruvec, LRU_INACTIVE_FILE); /* 添加到新 {inactive, file} 类型 LRU 链表 */

		...
	}
}
  • inactive => active

3.3.5 中了解到,activate_page(){inactive} page 转为 {active} page,也即以 activate_page_pvecs 向量为跳板,通过 pagevec_lru_move_fn() 回调 __activate_page(),将 activate_page_pvecs 向量中的所有 page ,从 {inactive} 类型 LRU 链表迁移到 {active} LRU 链表:

c 复制代码
static void __activate_page(struct page *page, struct lruvec *lruvec,
			    void *arg)
{
	if (PageLRU(page) && !PageActive(page) && !PageUnevictable(page)) {
		int file = page_is_file_cache(page);
		int lru = page_lru_base_type(page);

		del_page_from_lru_list(page, lruvec, lru); /* 从 inactive(LRU_INACTIVE_ANON,LRU_INACTIVE_FILE) 类型 LRU 链表移除 */
		SetPageActive(page); /* 标记为活动 page, 即设置 PG_active 标志 */
		/*
		 * 添加到 active 类型 LRU: 
		 * - LRU_INACTIVE_ANON => LRU_ACTIVE_ANON
		 * - LRU_INACTIVE_FILE => LRU_ACTIVE_FILE
		 */
		lru += LRU_ACTIVE;
		add_page_to_lru_list(page, lruvec, lru); /* 添加到 active(LRU_ACTIVE_ANON,LRU_ACTIVE_FILE) 类型 LRU 链表 */
		...
	}
}

典型的如 swap in 场景。

3.4.3 手动触发 page 新增 和 迁移 情形

前面两小节讨论的 page 新增迁移,都是在 per-cpu 向量满或操作复合页时自动触发,有时候可能需要手动触发,内核提供了下列主要接口来支持:

  • lru_add_drain_cpu()
c 复制代码
void lru_add_drain_cpu(int cpu)
{
	struct pagevec *pvec = &per_cpu(lru_add_pvec, cpu);

	if (pagevec_count(pvec))
		__pagevec_lru_add(pvec); /* (1) lru_add_pvec => NUMA LRU */

	pvec = &per_cpu(lru_rotate_pvecs, cpu);
	if (pagevec_count(pvec)) {
		unsigned long flags;

		/* No harm done if a racing interrupt already did this */
		local_irq_save(flags);
		pagevec_move_tail(pvec); /* (2) rotated lru_rotate_pvecs => NUMA LRU */
		local_irq_restore(flags);
	}

	pvec = &per_cpu(lru_deactivate_file_pvecs, cpu);
	if (pagevec_count(pvec))
		pagevec_lru_move_fn(pvec, lru_deactivate_file_fn, NULL); /* (3) */

	pvec = &per_cpu(lru_lazyfree_pvecs, cpu);
	if (pagevec_count(pvec))
		pagevec_lru_move_fn(pvec, lru_lazyfree_fn, NULL); /* (4) */

	activate_page_drain(cpu); /* (5) */
}

函数的逻辑一目了然,上面的注释 (1),(2),(3),(4),(5) 处,将 5 类 per-cpu 向量缓存的 page,移动到 NUMA LRU 链表中。

  • lru_add_drain_all_cpuslocked()
c 复制代码
void lru_add_drain_all_cpuslocked(void)
{
	static DEFINE_MUTEX(lock);
	static struct cpumask has_work;
	int cpu;

	/*
	 * Make sure nobody triggers this path before mm_percpu_wq is fully
	 * initialized.
	 */
	if (WARN_ON(!mm_percpu_wq))
		return;

	mutex_lock(&lock);
	cpumask_clear(&has_work);

	for_each_online_cpu(cpu) {
		struct work_struct *work = &per_cpu(lru_add_drain_work, cpu); /* per-cpu 的 drain work */

		if (pagevec_count(&per_cpu(lru_add_pvec, cpu)) ||
		    pagevec_count(&per_cpu(lru_rotate_pvecs, cpu)) ||
		    pagevec_count(&per_cpu(lru_deactivate_file_pvecs, cpu)) ||
		    pagevec_count(&per_cpu(lru_lazyfree_pvecs, cpu)) ||
		    need_activate_page_drain(cpu)) {
			INIT_WORK(work, lru_add_drain_per_cpu);
			queue_work_on(cpu, mm_percpu_wq, work); /* per-cpu 的 drain work 入队 */
			cpumask_set_cpu(cpu, &has_work);
		}
	}

	for_each_cpu(cpu, &has_work)
		flush_work(&per_cpu(lru_add_drain_work, cpu)); /* 执行 per-cpu 的 drain work */

	mutex_unlock(&lock);
}

执行 work 回调 lru_add_drain_per_cpu()

c 复制代码
lru_add_drain_per_cpu()
	lru_add_drain()
		lru_add_drain_cpu(get_cpu());
		put_cpu();

lru_add_drain_cpu() 前面已经分析过了,这里不再赘述。

  • activate_page_drain()
c 复制代码
static void activate_page_drain(int cpu)
{
	struct pagevec *pvec = &per_cpu(activate_page_pvecs, cpu);

	if (pagevec_count(pvec))
		pagevec_lru_move_fn(pvec, __activate_page, NULL);
}

__activate_page() 前面已经分析过了,这里不再赘述。

其它还有几个变体接口,最终都会落到这 3 个接口中来,这里就不一一展开了。

4. 小结

最后,用下图来小结本文的内容:

5. 参考链接

1 (十)Linux内存管理 - zoned page frame allocator - 5

2 linux内存管理 第037篇 LRU链表详解

相关推荐
A小辣椒2 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao3 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠4 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush44 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5204 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩4 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言