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 节点的:
// 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 2enum 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_headlists[NR_LRU_LISTS]; struct zone_reclaim_statreclaim_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 lruveclruvec; /* 每 NUMA 节点各类型的 LRU 链表 */ ...};
将上面每 NUMA 节点 LRU page 链表结构如下图:

另外,从上面数据结构了解到,一个 NUMA 节点各类型 LRU 链表公用了一把锁(pglist_data::lru_lock),这将导致 NUMA 节点的 LRU 链表操作的激烈锁竞争,为缓解这个问题,引入 per-cpu 的 page 向量:先将 page 添加到 per-cpu page 向量,等 per-cpu page 向量填满了,再一次性的将这些 page 移动到 NUMA 节点对应类型的 LRU 链表,如此可以避免大量的 LRU 锁竞争。
per-cpu page 向量用数据结构 struct pagevec 描述:
// include/linux/pagevec.h/* 14 pointers + two long's align the pagevec structure to a power of two */#define PAGEVEC_SIZE 14struct pagevec {unsigned long nr;unsigned long cold;struct page *pages[PAGEVEC_SIZE];};
再看一下定义的 5 个 per-cpu page 向量:
// mm/swap.cstatic 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_SMPstatic DEFINE_PER_CPU(struct pagevec, activate_page_pvecs);#endif
3.2 LRU 链表的初始化
BOOT 期间,内存管理系统初始化 NUMA LRU page 链表为空:
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 page 向量初始化为空:
// mm/swap.cstatic 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_SMPstatic 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 向量,如果以下两种情形之一:
则将 page 迁移到 NUMA 对应类型的 LRU 列表:
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
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
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 链表。
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 向量:
/** * 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 链表。
/** * 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 向量
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); }}
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。
/* * 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:
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 链表:
/* 将 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 链表:
/* 将 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 移动:
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 链表:
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:
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 链表 */ ... }}
从 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 链表:
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 向量满或操作复合页时自动触发,有时候可能需要手动触发,内核提供了下列主要接口来支持:
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()
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():
lru_add_drain_per_cpu() lru_add_drain() lru_add_drain_cpu(get_cpu()); put_cpu();
lru_add_drain_cpu() 前面已经分析过了,这里不再赘述。
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. 小结
最后,用下图来小结本文的内容: