当前位置:首页>Linux>Linux 内存管理:LRU 链表 (1)

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

  • 2026-09-10 11:08:57
Linux 内存管理:LRU 链表 (1)

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 链表的初始化

  • 每 NUMA 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 向量初始化

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 向量,如果以下两种情形之一:

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

则将 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);  }}
  • page swap in
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 链表 */    ...  }}
  • 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 链表:

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()
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() 前面已经分析过了,这里不再赘述。

  • activate_page_drain()
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. 小结

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

最新文章

随机文章