文章目录
- [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.3.1 添加 page 到 per-cpu 的 `lru_add_pvec` 向量](#3.3.1 添加 page 到 per-cpu 的
- [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];
};
再看一下定义的 5 个 per-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链表详解")