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](https://www.cnblogs.com/LoyenWang/p/11827153.html "(十)Linux内存管理 - zoned page frame allocator - 5LoyenWang 2019-11-09 18:05 阅读:8322 评论:3 推荐:4") \[2\] [\[linux内存管理\] 第037篇 LRU链表详解](https://www.iliuqi.com/archives/linux-memory-management-037 "[linux内存管理] 第037篇 LRU链表详解")

相关推荐
Zhao136824553912 小时前
DP108B完全替代兼容进口的CM108B,USB 音频单芯片
linux·运维·音视频
攻城狮在此2 小时前
ping命令中TTL值是什么?详解与用法
linux·网络·windows
艾莉丝努力练剑2 小时前
【Linux信号】Linux进程信号
linux·运维·服务器·学习·操作系统·进程·信号
cqsztech2 小时前
基于ORACLE LINUX 10.1 MYSQL 8.4 源码安装
linux·mysql·oracle
齐齐大魔王2 小时前
linux-系统函数
linux·运维·microsoft
Lugas Luo2 小时前
利用 Claude 辅助 Linux 嵌入式开发的高阶工作流Top Steps
linux
the sun342 小时前
我的第一个字符驱动:基于Linux2.4之前版本的古法编程
linux·驱动开发
拾贰_C2 小时前
【Ubuntu | Nvidia | installition0】Ubuntu安装Nvidia驱动
linux·运维·ubuntu
零K沁雪2 小时前
内核定时器
linux·内核