当前位置: 首页 > news >正文

Linux 内存管理之LRU链表

文章目录

  • 前言
  • 一、pagecache_get_page
  • 二、LRU 链表
    • 2.1 核心数据结构
      • 2.1.1 lru_list
      • 2.1.2 lruvec
      • 2.1.3 pglist_data
      • 2.1.4 pagevec
    • 2.2 file lru list
  • 三、lru 相关操作
    • 3.1 lru_cache_add
      • 3.1.1 lru_pvecs
      • 3.1.2 pagevec_add_and_need_flush
      • 3.1.3 __pagevec_lru_add
      • 3.1.4 __pagevec_lru_add_fn
      • 3.1.5 add_page_to_lru_list
    • 3.2 lru_cache_add的调用
  • 四、mark_page_accessed
    • 4.1 activate_page
      • 4.1.1 pagevec_lru_move_fn
      • 4.1.2 __activate_page
    • 4.2 __lru_cache_activate_page
  • 五、lru_pvecs相关函数
    • 5.1 deactivate_file_page
      • 5.1.1 lru_deactivate_file_fn
      • 5.1.2 使用场景
    • 5.2 deactivate_page
      • lru_deactivate_fn
    • 5.3 mark_page_lazyfree
      • lru_lazyfree_fn
  • 参考资料

前言

这篇文章描述了page cache相关知识:Linux 内存管理之page cache

而page cache 页在Linux内核中由两个数据结构管理:
(1)address_space,基数树
(2)LRU 链表

本文主要描述 LRU 链表 管理 page cache 页。

一、pagecache_get_page

/*** pagecache_get_page - Find and get a reference to a page.* @mapping: The address_space to search.* @index: The page index.* @fgp_flags: %FGP flags modify how the page is returned.* @gfp_mask: Memory allocation flags to use if %FGP_CREAT is specified.** Looks up the page cache entry at @mapping & @index.** @fgp_flags can be zero or more of these flags:** * %FGP_ACCESSED - The page will be marked accessed.* * %FGP_LOCK - The page is returned locked.* * %FGP_HEAD - If the page is present and a THP, return the head page*   rather than the exact page specified by the index.* * %FGP_ENTRY - If there is a shadow / swap / DAX entry, return it*   instead of allocating a new page to replace it.* * %FGP_CREAT - If no page is present then a new page is allocated using*   @gfp_mask and added to the page cache and the VM's LRU list.*   The page is returned locked and with an increased refcount.* * %FGP_FOR_MMAP - The caller wants to do its own locking dance if the*   page is already in cache.  If the page was allocated, unlock it before*   returning so the caller can do the same dance.* * %FGP_WRITE - The page will be written* * %FGP_NOFS - __GFP_FS will get cleared in gfp mask* * %FGP_NOWAIT - Don't get blocked by page lock** If %FGP_LOCK or %FGP_CREAT are specified then the function may sleep even* if the %GFP flags specified for %FGP_CREAT are atomic.** If there is a page cache page, it is returned with an increased refcount.** Return: The found page or %NULL otherwise.*/
struct page *pagecache_get_page(struct address_space *mapping, pgoff_t index,int fgp_flags, gfp_t gfp_mask)
{struct page *page;repeat://从 address_space 寻找page cachepage = mapping_get_entry(mapping, index);if (!page)goto no_page;......no_page://如果在address_space 没有找到 page cache,创建一个 page cachepage = __page_cache_alloc(gfp_mask);if (!page)return NULL;/* Init accessed so avoid atomic mark_page_accessed later */if (fgp_flags & FGP_ACCESSED)__SetPageReferenced(page);//将新创建的page cache加入 address_space 和 LRU 链表err = add_to_page_cache_lru(page, mapping, index, gfp_mask);return page;
}
EXPORT_SYMBOL(pagecache_get_page);

Linux内核调用pagecache_get_page来在 address_space 中查找一个 page cache。

pagecache_get_page调用mapping_get_entry在 address_space 中查找一个 page cache ,如果没有找到,则调用__page_cache_alloc函数分配一个page cache,然后调用add_to_page_cache_lru将page cache加 address_space 和 LRU链表里面。

int add_to_page_cache_lru(struct page *page, struct address_space *mapping,pgoff_t offset, gfp_t gfp_mask)
{__add_to_page_cache_locked(page, mapping, offset,gfp_mask, &shadow);......lru_cache_add(page);}
EXPORT_SYMBOL_GPL(add_to_page_cache_lru);

我们这里只讨论加入LRU链表:

/*** lru_cache_add - add a page to a page list* @page: the page to be added to the LRU.** Queue the page for addition to the LRU via pagevec. The decision on whether* to add the page to the [in]active [file|anon] list is deferred until the* pagevec is drained. This gives a chance for the caller of lru_cache_add()* have the page added to the active list using mark_page_accessed().*/
void lru_cache_add(struct page *page)
{struct pagevec *pvec;VM_BUG_ON_PAGE(PageActive(page) && PageUnevictable(page), page);VM_BUG_ON_PAGE(PageLRU(page), page);get_page(page);local_lock(&lru_pvecs.lock);pvec = this_cpu_ptr(&lru_pvecs.lru_add);if (pagevec_add_and_need_flush(pvec, page))__pagevec_lru_add(pvec);local_unlock(&lru_pvecs.lock);
}
EXPORT_SYMBOL(lru_cache_add);

二、LRU 链表

2.1 核心数据结构

2.1.1 lru_list

Linux 使用多级 LRU 链表管理页缓存,关键数据结构如下:

// v5.15/source/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,				// 非活跃文件页LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,		// 活跃文件页LRU_UNEVICTABLE,										// 不可回收页NR_LRU_LISTS
};struct lruvec {struct list_head		lists[NR_LRU_LISTS];			// 5种LRU链表......
};/** On NUMA machines, each NUMA node would have a pg_data_t to describe* it's memory layout. On UMA machines there is a single pglist_data which* describes the whole memory.** Memory statistics and page replacement data structures are maintained on a* per-zone basis.*/
typedef struct pglist_data {....../** NOTE: THIS IS UNUSED IF MEMCG IS ENABLED.** Use mem_cgroup_lruvec() to look up lruvecs.*/struct lruvec		__lruvec; // 每个内存节点有自己的LRU结构......
}

lru是双向链表: 内核根据页面类型(匿名页和文件页)、活跃性(活跃和不活跃)、可回收型(可回收和不可回收), 分成5种类型lru链表。
在这里插入图片描述
如下图所示:
在这里插入图片描述

层级关系:通过 LRU_BASE、LRU_ACTIVE、LRU_FILE 的算术组合确保:
活跃链表始终位于对应非活跃链表之后(ACTIVE > INACTIVE)。
文件页链表始终位于匿名页链表之后(FILE > ANON)。

每个内存节点pglist_data有自己的LRU结构 struct lruvec 。

这一约束确保页面状态转换(如从非活跃到活跃)和统计计数(如 vmstat 中的页面数量)的逻辑一致性。

$ cat /proc/vmstat
nr_free_pages 1664866
nr_zone_inactive_anon 4212041
nr_zone_active_anon 6082
nr_zone_inactive_file 4947212
nr_zone_active_file 4878944
nr_zone_unevictable 1438
.......
nr_inactive_anon 4212041
nr_active_anon 6082
nr_inactive_file 4947212
nr_active_file 4878944
nr_unevictable 1438

设计细节:链表顺序的意义
(1)活跃 > 非活跃:
例如 LRU_ACTIVE_ANON(值为 1)在 LRU_INACTIVE_ANON(值为 0)之后,LRU_ACTIVE_FILE(值为 3)在 LRU_INACTIVE_FILE(值为 2)之后。这使得内核可以通过简单的算术运算(如 +1)从非活跃链表定位到对应活跃链表(用于页面激活操作)。

(2)文件 > 匿名:
LRU_INACTIVE_FILE(值为 2)在 LRU_INACTIVE_ANON(值为 0)之后,LRU_ACTIVE_FILE(值为 3)在 LRU_ACTIVE_ANON(值为 1)之后。这确保文件类页面和匿名类页面的统计与回收逻辑可以批量处理(如优先回收文件页以减少 I/O 影响)。

2.1.2 lruvec

struct lruvec {struct list_head		lists[NR_LRU_LISTS];			// 5种LRU链表......
};

核心作用:为一组 LRU 链表提供容器,每个 lists 数组元素对应 enum lru_list 中的一种链表类型(如 lists[LRU_INACTIVE_ANON] 即为非活跃匿名页链表)。

struct list_head:双向链表头,页面(struct page)通过 lru 字段挂载到链表上,便于插入、删除和遍历(如回收时从链表尾部移除页面)。

2.1.3 pglist_data

/** On NUMA machines, each NUMA node would have a pg_data_t to describe* it's memory layout. On UMA machines there is a single pglist_data which* describes the whole memory.** Memory statistics and page replacement data structures are maintained on a* per-zone basis.*/
typedef struct pglist_data {....../** NOTE: THIS IS UNUSED IF MEMCG IS ENABLED.** Use mem_cgroup_lruvec() to look up lruvecs.*/struct lruvec		__lruvec; // 每个内存节点有自己的LRU结构......
}

NUMA 架构适配:在多 NUMA 节点系统中,每个节点的内存独立管理,pglist_data 描述一个 NUMA 节点的内存布局,包含该节点的所有 LRU 链表(__lruvec)。

“若启用 memcg,则 __lruvec 未被直接使用”,而是通过 mem_cgroup_lruvec() 获取对应控制组的 lruvec,实现按进程组隔离的 LRU 管理。

2.1.4 pagevec

// v5.15/source/include/linux/pagevec.h/* 15 pointers + header align the pagevec structure to a power of two */
#define PAGEVEC_SIZE	15struct pagevec {unsigned char nr;bool percpu_pvec_drained;struct page *pages[PAGEVEC_SIZE];
};

pagevec 是 Linux 内核中用于批量操作页面(page)的缓存结构,其设计核心在于:

批量处理:通过数组缓存多个页面(默认15个),减少锁操作开销
对齐优化:PAGEVEC_SIZE=15 使得结构体大小对齐到2的幂(15指针+头部=16单位)
原子性保证:通过percpu_pvec_drained标记防止并发问题

(1)nr:当前页面数量
类型为 unsigned char,表示当前 pagevec 中已存储的页面数量,范围为 0 到 PAGEVEC_SIZE(即 0~15)。
作为数组 pages 的实际有效长度,用于遍历或批量处理时判断边界(例如:for (i = 0; i < pvec->nr; i++))。

(2)percpu_pvec_drained:per-CPU 页面向量的状态标记
用于 per-CPU 页面向量(每个 CPU 维护一个 pagevec 实例,减少跨 CPU 竞争)的场景。
标记该 pagevec 中的页面是否已被 “排空”(drained),即是否已完成批量处理并清空,避免重复操作。

(3)pages[PAGEVEC_SIZE]:页面指针数组
存储指向物理页面(struct page)的指针,数组大小由 PAGEVEC_SIZE 定义(固定为 15)。
PAGEVEC_SIZE 取值为 15 的原因:
注释提到 “15 个指针 + 头部对齐使 pagevec 结构大小为 2 的幂”。在 64 位系统中,struct page* 为 8 字节,15 个指针共 120 字节,加上 nr(1 字节)和 percpu_pvec_drained(1 字节),总大小为 122 字节,通过对齐补全为 128 字节(2^7),便于内存分配和缓存优化(减少 CPU 缓存行浪费)。

struct page 是描述物理页面的核心结构,每个物理页面通过 lru 字段(struct list_head)挂载到 LRU 链表,而 pagevec 中的 pages 数组存储指向 struct page 的指针,形成 “临时批量管理” 与 “长期链表管理” 的衔接:
短期存储:pagevec 作为临时缓冲区,在操作期间持有页面指针。
长期管理:操作完成后,页面被挂载到 LRU 链表(通过 lru 字段),进入内核的页面生命周期管理。

2.2 file lru list

页缓存(page cache)中的页面属于 LRU_INACTIVE_FILE 或 LRU_ACTIVE_FILE 链表,其生命周期与 LRU 操作紧密相关:
(1)页面加入:当文件数据首次加载到页缓存时,页面被添加到 LRU_INACTIVE_FILE 链表(初始状态为非活跃)。
(2)访问激活:若页面被再次访问(如读操作),内核会将其从非活跃链表移至 LRU_ACTIVE_FILE 链表(标记为活跃,减少被回收概率)。
(3)回收候选:当系统内存不足时,页面回收算法(如 shrinker)优先扫描 LRU_INACTIVE_FILE 链表尾部的页面,将其写回磁盘(若为脏页)后释放内存。
(4)状态转换:若活跃页面长时间未被访问,会被移回非活跃链表,等待回收。

三、lru 相关操作

3.1 lru_cache_add

/*** lru_cache_add - add a page to a page list* @page: the page to be added to the LRU.** Queue the page for addition to the LRU via pagevec. The decision on whether* to add the page to the [in]active [file|anon] list is deferred until the* pagevec is drained. This gives a chance for the caller of lru_cache_add()* have the page added to the active list using mark_page_accessed().*/
void lru_cache_add(struct page *page)
{struct pagevec *pvec;// 断言检查:页面不能同时是活跃且不可回收的VM_BUG_ON_PAGE(PageActive(page) && PageUnevictable(page), page);// 断言检查:页面不能在已LRU链表中的VM_BUG_ON_PAGE(PageLRU(page), page);// 增加页面引用计数,防止在缓存期间被释放get_page(page);// 获取每CPU缓存锁(防止中断上下文竞争)local_lock(&lru_pvecs.lock);// 获取当前CPU的pagevec缓存pvec = this_cpu_ptr(&lru_pvecs.lru_add);// 添加页面到缓存,判断是否需要立即刷新if (pagevec_add_and_need_flush(pvec, page))__pagevec_lru_add(pvec); // 执行批量LRU添加// 释放每CPU锁local_unlock(&lru_pvecs.lock);
}

当内核分配新页面(如文件缓存、匿名内存)或页面状态变化时,需将其加入 LRU 链表以便后续内存回收。

lru_cache_add() 是 Linux 内存管理中将页面添加到 LRU 链表的关键接口,其核心行为是:
延迟加入:通过每 CPU 的 pagevec 缓存页面,批量处理以减少锁竞争
决策推迟:允许调用者在页面真正加入 LRU 前通过 mark_page_accessed() 升级为活跃页

lru_cache_add 是 Linux 内核中将物理页面(struct page)加入 LRU 链表的核心函数,其作用是通过 pagevec 批量机制,将新分配或新使用的页面添加到 LRU 管理体系中,为后续的页面回收(如内存不足时优先回收不常用页面)提供基础。

当内核分配新页面(如页缓存加载文件数据、匿名页分配给堆 / 栈)时,需要将页面纳入 LRU(最近最少使用)链表进行生命周期管理,以便在内存不足时按 “最近最少使用” 原则回收。

lru_cache_add 的核心设计是 “延迟批量添加”
不直接将页面插入 LRU 链表,而是先暂存到 pagevec(批量页面容器)中。
当 pagevec 填满(达到 PAGEVEC_SIZE,即 15 页)时,再批量插入 LRU 链表,减少锁竞争和链表操作开销。

关键参数与数据结构:
struct page *page:待添加到 LRU 的物理页面,包含页面类型(匿名页 / 文件页)、状态(活跃 / 非活跃)等信息。
struct pagevec *pvec:per-CPU 的 pagevec 实例(每个 CPU 维护一个),用于暂存待添加的页面。
lru_pvecs:全局 per-CPU 变量,存储每个 CPU 的 lru_add 向量(pagevec),避免跨 CPU 竞争。

获取 per-CPU 的 pagevec 并添加页面:

local_lock(&lru_pvecs.lock);  // 保护 per-CPU 的 pagevec 操作
pvec = this_cpu_ptr(&lru_pvecs.lru_add);  // 获取当前 CPU 的 lru_add 向量
if (pagevec_add_and_need_flush(pvec, page))  // 添加页面到 pagevec__pagevec_lru_add(pvec);  // 若 pagevec 满,批量添加到 LRU 链表
local_unlock(&lru_pvecs.lock);

(1)this_cpu_ptr(&lru_pvecs.lru_add):获取当前 CPU 专属的 pagevec(lru_add 向量),避免多 CPU 竞争同一 pagevec。

(2)添加页面到 pagevec 并检查是否需要批量处理
pagevec_add_and_need_flush(pvec, page):
将 page 加入 pvec->pages 数组,pvec->nr 计数加 1。
若 pvec->nr 达到 PAGEVEC_SIZE(15),返回 true,表示需要批量处理。

(3)批量添加到 LRU 链表(若 pagevec 满)
__pagevec_lru_add(pvec):核心批量处理函数,根据页面类型(匿名 / 文件)和状态,将 pagevec 中的所有页面插入对应的 LRU 链表:
匿名页 → LRU_INACTIVE_ANON 或 LRU_ACTIVE_ANON。
文件页 → LRU_INACTIVE_FILE 或 LRU_ACTIVE_FILE。
插入后清空 pagevec(pvec->nr = 0),为下次批量添加做准备。

特点:
(1)延迟批量添加,减少锁开销
若每次添加页面都直接操作 LRU 链表,需频繁获取 lruvec->lru_lock(保护 LRU 链表的自旋锁),导致锁竞争激烈。
pagevec 批量机制将 15 次单页操作合并为 1 次批量操作,锁开销降低 15 倍,显著提升高并发场景性能。

(2)per-CPU 设计,避免跨 CPU 竞争
每个 CPU 维护独立的 pagevec(lru_pvecs.lru_add),页面添加操作本地化,减少缓存一致性流量(如 CPU 间的缓存同步)。
仅当 pagevec 满时才批量操作全局 LRU 链表,进一步降低竞争。

(3)3. 状态延迟决策,兼容页面访问标记
函数注释提到:延迟决定页面加入哪个 LRU 链表(活跃 / 非活跃),为调用者通过 mark_page_accessed() 将页面标记为活跃提供机会。
例如:若页面在添加到 LRU 前被访问(如刚分配的页被写入),mark_page_accessed 会将其标记为 “活跃”,后续批量添加时直接放入活跃 LRU 链表。
这种 “延迟决策” 确保页面初始状态准确反映其访问模式。

lru_cache_add 是页面进入 LRU 管理体系的 “入口”,后续页面的生命周期(活跃 / 非活跃转换、回收)均基于 LRU 链表:
(1)初始状态:新页面通过 lru_cache_add 暂存到 pagevec,批量添加时默认进入非活跃 LRU 链表(LRU_INACTIVE_ANON 或 LRU_INACTIVE_FILE)。
(2)活跃升级:若页面被访问(如 mark_page_accessed 调用),会从非活跃链表迁移到活跃 LRU 链表(减少被回收概率)。
(3)回收候选:内存不足时,页面回收算法优先扫描非活跃链表尾部的页面(最近最少使用),通过 lru_cache_add 加入的页面成为回收候选。

3.1.1 lru_pvecs

/** The following struct pagevec are grouped together because they are protected* by disabling preemption (and interrupts remain enabled).*/
struct lru_pvecs {local_lock_t lock;struct pagevec lru_add;   				//待加入 LRU 的页面struct pagevec lru_deactivate_file;		//待去激活的file页面 activate->inactivatestruct pagevec lru_deactivate;			//待去激活的file/匿名页面 activate->inactivatestruct pagevec lru_lazyfree;			//用于延迟释放匿名交换页 activate->inactivate
#ifdef CONFIG_SMPstruct pagevec activate_page;			//待激活的页面 inactivate->activate
#endif};
static DEFINE_PER_CPU(struct lru_pvecs, lru_pvecs) = {.lock = INIT_LOCAL_LOCK(lock),
};

page 可以加入到 lru 链表,并且根据条件在 active/inactive 链表间移动。

struct lru_pvecs 是 Linux 内核中用于管理 per-CPU 批量页面操作 的核心数据结构,其设计目的是通过将高频页面操作(如添加到 LRU、激活、去激活)批量暂存到 pagevec 中,减少锁竞争和链表操作开销,提升内存管理效率。

每个CPU维护5个不同类型的struct pagevec。

在多 CPU 系统中,若多个 CPU 同时操作全局 LRU 链表(如添加、迁移页面),会导致严重的锁竞争(如 lruvec->lru_lock)。为解决这一问题,内核为每个 CPU 分配独立的 lru_pvecs 结构,将页面操作先在本地 CPU 的 pagevec 中暂存,达到一定条件后再批量提交到全局 LRU 链表,从而:
减少跨 CPU 竞争:本地 CPU 操作本地 pagevec,无需频繁获取全局锁。
降低操作频率:批量处理替代单次操作,减少链表插入 / 删除的次数。

每个字段都是 struct pagevec 类型(批量页面容器),针对不同页面操作场景分类暂存:
(1)lock:本地操作锁
类型为 local_lock_t(轻量级锁),用于保护当前 CPU 对 lru_pvecs 中 pagevec 的操作,防止中断或同一 CPU 上的其他线程干扰(如避免页面重复添加)。
相比全局自旋锁,local_lock 开销更低,仅作用于当前 CPU。

(2)lru_add:待加入 LRU 的页面
暂存新分配的页面(如页缓存、匿名页),等待批量添加到 LRU 链表(通过 __pagevec_lru_add 函数)。
对应操作:lru_cache_add 函数将页面加入此 pagevec,满后批量提交。

(3)lru_deactivate_file 与 lru_deactivate:待去激活的页面
去激活:当活跃页面长时间未访问时,从 “活跃 LRU 链表” 迁移到 “非活跃 LRU 链表”(提升回收优先级)。
lru_deactivate_file:针对文件映射页, LRU_ACTIVE_FILE → LRU_INACTIVE_FILE。

lru_deactivate:针对文件映射页/匿名页
LRU_ACTIVE_FILE → LRU_INACTIVE_FILE。
LRU_ACTIVE_ANON → LRU_INACTIVE_ANON。

对应操作:deactivate_page 函数将页面加入这些 pagevec,满后批量迁移。

(4)lru_lazyfree:待延迟释放的页面
暂存需要异步释放的页面(如内存压缩后的原页面、COW 复制后的旧页面),由内核线程(如 kcompactd)批量处理,避免同步释放阻塞用户进程。

(5)activate_page(SMP 专用):待激活的页面
仅在多 CPU 系统(CONFIG_SMP)中存在,暂存需要从 “非活跃 LRU 链表” 迁移到 “活跃 LRU 链表” 的页面(如被重新访问的非活跃页)。
对应操作:mark_page_accessed 等函数标记页面为活跃后,加入此 pagevec 批量迁移。

3.1.2 pagevec_add_and_need_flush

/* return true if pagevec needs to drain */
static bool pagevec_add_and_need_flush(struct pagevec *pvec, struct page *page)
{bool ret = false;if (!pagevec_add(pvec, page) ||    // 条件1:缓存是否已满PageCompound(page) ||          // 条件2:是否复合页lru_cache_disabled())          // 条件3:LRU缓存是否禁用ret = true;return ret;
}

在内核中,频繁操作单个页面(如添加到 LRU 链表)会导致大量锁竞争和链表操作开销。
pagevec 作为批量容器,通过累积多个页面后一次性处理,可显著提升性能。
在这里插入图片描述

pagevec_add_and_need_flush 的设计目标是:
添加页面到 pagevec:将新页面加入 pagevec 的页面数组。
触发批量处理条件:当满足特定条件时(如 pagevec 已满、页面特殊类型),返回 true 触发批量处理,减少后续操作延迟。

关键参数与返回值
struct pagevec *pvec:目标 pagevec 容器,用于暂存待处理的页面。
struct page *page:待添加的物理页面。

返回值:
true:表示需要立即将 pagevec 中的所有页面 “排空”(如批量添加到 LRU 链表)。
false:表示页面已成功加入 pagevec,无需立即处理。

(1)
pagevec_add 函数:将 page 加入 pvec->pages 数组,并递增 pvec->nr(当前页面计数)。
若 pvec->nr 已达到 PAGEVEC_SIZE(通常为 15),则无法添加,返回 false。
若成功添加,返回 true。

static inline unsigned pagevec_space(struct pagevec *pvec)
{return PAGEVEC_SIZE - pvec->nr;
}/** Add a page to a pagevec.  Returns the number of slots still available.*/
static inline unsigned pagevec_add(struct pagevec *pvec, struct page *page)
{pvec->pages[pvec->nr++] = page;return pagevec_space(pvec);
}

pagevec_add() 返回 0 表示缓存已满(默认 15 个页面)。

(2)PageCompound(page) 为真
复合页(Compound Page):由多个连续物理页组成的大页(如透明大页 THP),通常用于高性能场景。

触发逻辑:
复合页的管理与普通页不同(如迁移、回收需特殊处理),因此不适合批量操作,需立即处理。

(3)lru_cache_disabled() 为真
LRU 缓存禁用:当系统内存压力极大或处于特殊模式(如紧急回收)时,内核可能临时禁用 LRU 链表的正常管理。

3.1.3 __pagevec_lru_add

/** 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)
{int i;struct lruvec *lruvec = NULL;unsigned long flags = 0;for (i = 0; i < pagevec_count(pvec); i++) {struct page *page = pvec->pages[i];lruvec = relock_page_lruvec_irqsave(page, lruvec, &flags);__pagevec_lru_add_fn(page, lruvec);}if (lruvec)unlock_page_lruvec_irqrestore(lruvec, flags);release_pages(pvec->pages, pvec->nr);pagevec_reinit(pvec);
}

在内核中,频繁操作单个页面(如添加到 LRU 链表)会导致大量锁竞争和链表操作开销。通过 pagevec 批量累积多个页面后,使用 __pagevec_lru_add 一次性处理,可显著提升性能。

该函数的核心目标是:
(1)批量添加页面到 LRU 链表:根据页面类型(匿名 / 文件)和状态(活跃 / 非活跃),将 pagevec 中的所有页面添加到对应的 LRU 链表。
(2)释放页面引用计数:添加完成后,释放调用者对这些页面的引用计数(put_page),允许页面在后续内存压力下被回收。
(3)重置 pagevec:清空 pagevec,为下一次批量添加做准备。

3.1.4 __pagevec_lru_add_fn

static void __pagevec_lru_add_fn(struct page *page, struct lruvec *lruvec)
{//清除 PG_mlocked 标志并获取页面数量int was_unevictable = TestClearPageUnevictable(page);int nr_pages = thp_nr_pages(page);VM_BUG_ON_PAGE(PageLRU(page), page);/** Page becomes evictable in two ways:* 1) Within LRU lock [munlock_vma_page() and __munlock_pagevec()].* 2) Before acquiring LRU lock to put the page to correct LRU and then*   a) do PageLRU check with lock [check_move_unevictable_pages]*   b) do PageLRU check before lock [clear_page_mlock]** (1) & (2a) are ok as LRU lock will serialize them. For (2b), we need* following strict ordering:** #0: __pagevec_lru_add_fn		#1: clear_page_mlock** SetPageLRU()				TestClearPageMlocked()* smp_mb() // explicit ordering	// above provides strict*					// ordering* PageMlocked()			PageLRU()*** if '#1' does not observe setting of PG_lru by '#0' and fails* isolation, the explicit barrier will make sure that page_evictable* check will put the page in correct LRU. Without smp_mb(), SetPageLRU* can be reordered after PageMlocked check and can make '#1' to fail* the isolation of the page whose Mlocked bit is cleared (#0 is also* looking at the same page) and the evictable page will be stranded* in an unevictable LRU.*///设置 PG_lru 标志并添加内存屏障SetPageLRU(page);smp_mb__after_atomic();//判断页面是否可回收并处理//可回收页面if (page_evictable(page)) {//可回收页面若之前是不可回收的,记录统计事件 if (was_unevictable)__count_vm_events(UNEVICTABLE_PGRESCUED, nr_pages);} else { //不可回收页面//清除 PG_active 标志ClearPageActive(page);//重新设置 PG_mlocked 标志SetPageUnevictable(page);if (!was_unevictable)__count_vm_events(UNEVICTABLE_PGCULLED, nr_pages);}//添加页面到 LRU 链表并触发跟踪add_page_to_lru_list(page, lruvec);trace_mm_lru_insertion(page);
}

__pagevec_lru_add_fn 是 Linux 内核中将物理页面添加到 LRU 链表的核心函数,其核心作用是根据页面的可回收性(evictable)和活跃状态,将页面正确添加到对应的 LRU 链表。

执行流程详解:
(1)清除 PG_mlocked 标志并获取页面数量

int was_unevictable = TestClearPageUnevictable(page);
int nr_pages = thp_nr_pages(page);

TestClearPageUnevictable(page):
检查并清除页面的 PG_mlocked 标志(表示页面是否被锁定)。
返回值 was_unevictable 记录页面之前是否为不可回收状态。

thp_nr_pages(page):
若页面是透明大页(THP),返回其包含的物理页数量(通常为 1 或多个 4KB 页),用于统计。

(2)设置 PG_lru 标志并添加内存屏障

SetPageLRU(page);
smp_mb__after_atomic();

SetPageLRU(page):
设置页面的 PG_lru 标志,表示页面已加入 LRU 链表管理。

smp_mb__after_atomic():
内存屏障,确保 SetPageLRU 的写操作对其他 CPU 可见,避免与后续的 page_evictable() 检查重排序。
此屏障是防止并发问题的关键,确保状态更新的顺序性。

代码中的内存屏障 smp_mb__after_atomic() 是为了防止以下并发问题:
场景描述:
CPU0 正在执行 __pagevec_lru_add_fn,将页面加入 LRU 链表。
CPU1 同时执行 clear_page_mlock,尝试清除页面的 PG_mlocked 标志。

潜在问题:
若没有内存屏障,SetPageLRU 可能被重排序到 PageMlocked() 检查之后。
CPU1 可能看不到 PG_lru 标志已设置,导致错误地认为页面未加入 LRU,从而未正确处理其状态。

内存屏障的作用:
确保 SetPageLRU 的写操作对其他 CPU 可见,且严格先于后续的 PageMlocked() 检查,避免页面状态混乱

(3)判断页面是否可回收并处理

if (page_evictable(page)) {if (was_unevictable)__count_vm_events(UNEVICTABLE_PGRESCUED, nr_pages);
} else {ClearPageActive(page);SetPageUnevictable(page);if (!was_unevictable)__count_vm_events(UNEVICTABLE_PGCULLED, nr_pages);
}

可回收页面(page_evictable(page) 为真):
若页面之前是不可回收的(was_unevictable 为真),记录统计事件 UNEVICTABLE_PGRESCUED,表示页面从不可回收状态 “获救”,可被加入正常 LRU 链表。

不可回收页面(page_evictable(page) 为假):
ClearPageActive(page):清除 PG_active 标志,确保不可回收页不会出现在活跃 LRU 链表中。
SetPageUnevictable(page):重新设置 PG_mlocked 标志(之前被 TestClearPageUnevictable 清除),表示页面不可回收。
若页面之前是可回收的,记录统计事件 UNEVICTABLE_PGCULLED,表示页面被 “拉入” 不可回收状态。

在这里插入图片描述
(4)添加页面到 LRU 链表并触发跟踪

add_page_to_lru_list(page, lruvec);
trace_mm_lru_insertion(page);

add_page_to_lru_list(page, lruvec):
根据页面类型(匿名 / 文件)和状态(活跃 / 非活跃、可回收 / 不可回收),将页面添加到对应的 LRU 链表。

该函数根据页面状态将其添加到不同的 LRU 链表:
可回收且活跃:LRU_ACTIVE_ANON 或 LRU_ACTIVE_FILE。
可回收但非活跃:LRU_INACTIVE_ANON 或 LRU_INACTIVE_FILE。
不可回收:LRU_UNEVICTABLE。

这些链表共同构成内核的页面回收机制,当内存不足时,内核优先从非活跃链表回收页面,而不可回收链表中的页面(如被 mlock() 锁定的页面)不会被回收。

trace_mm_lru_insertion(page):
触发内核跟踪点,记录页面加入 LRU 链表的事件,用于性能分析和调试。

3.1.5 add_page_to_lru_list

/*** page_lru - which LRU list should a page be on?* @page: the page to test** Returns the LRU list a page should be on, as an index* into the array of LRU lists.*/
static __always_inline enum lru_list page_lru(struct page *page)
{enum lru_list lru;VM_BUG_ON_PAGE(PageActive(page) && PageUnevictable(page), page);if (PageUnevictable(page))return LRU_UNEVICTABLE;lru = page_is_file_lru(page) ? LRU_INACTIVE_FILE : LRU_INACTIVE_ANON;if (PageActive(page))lru += LRU_ACTIVE;return lru;
}static __always_inline void add_page_to_lru_list(struct page *page,struct lruvec *lruvec)
{enum lru_list lru = page_lru(page);update_lru_size(lruvec, lru, page_zonenum(page), thp_nr_pages(page));list_add(&page->lru, &lruvec->lists[lru]);
}

add_page_to_lru_list 是 Linux 内存管理中实现页面添加到 LRU 链表的最底层函数。

在内核的 LRU 页面管理体系中,不同类型和状态的页面需要被分类存储到不同的 LRU 链表中,以便内存回收时能按优先级处理。该函数的核心目标是:
判断页面是否不可回收:若页面被 mlock() 锁定(PG_mlocked 标志置位),则应放入LRU_UNEVICTABLE 链表。
区分页面类型:判断页面是匿名页(如堆、栈内存)还是文件映射页(如文件缓存)。
判断页面活跃状态:根据 PG_active 标志,确定页面是活跃还是非活跃,从而选择对应的活跃 / 非活跃链表。

(1)确定目标 LRU 链表:根据页面的类型(匿名 / 文件)和状态(活跃 / 非活跃、可回收 / 不可回收),选择对应的 LRU 链表。

static __always_inline enum lru_list page_lru(struct page *page)
{enum lru_list lru;// 调试断言:页面不能同时是活跃且不可回收的VM_BUG_ON_PAGE(PageActive(page) && PageUnevictable(page), page);// 优先处理不可回收页if (PageUnevictable(page))return LRU_UNEVICTABLE;// 基础判断:文件页还是匿名页lru = page_is_file_lru(page) ? LRU_INACTIVE_FILE : LRU_INACTIVE_ANON;// 升级活跃页if (PageActive(page))lru += LRU_ACTIVE;return lru;
}

a:判断是否为不可回收页:

    if (PageUnevictable(page))return LRU_UNEVICTABLE;

PageUnevictable(page):检查页面的 PG_mlocked 标志。
返回值:若置位,返回 LRU_UNEVICTABLE,表示该页面不可被换出,应单独管理。

b:区分页面类型:判断页面是匿名页(如堆、栈内存)还是文件映射页(如文件缓存)。

static inline int page_is_file_lru(struct page *page)
{return !PageSwapBacked(page);
}lru = page_is_file_lru(page) ? LRU_INACTIVE_FILE : LRU_INACTIVE_ANON;

匿名页:PageSwapBacked=true(可交换到swap)
文件页:PageSwapBacked=false

page_is_file_lru(page):判断页面是否为文件映射页。
若是,基础链表为 LRU_INACTIVE_FILE(非活跃文件页)。
若否,基础链表为 LRU_INACTIVE_ANON(非活跃匿名页)。

c:判断页面活跃状态:根据 PG_active 标志,确定页面是活跃还是非活跃,从而选择对应的活跃 / 非活跃链表。

if (PageActive(page))lru += LRU_ACTIVE;

若页面活跃,调整为活跃链表
PageActive(page):检查页面的 PG_active 标志。

算术原理:若页面活跃,在基础链表索引上加上 LRU_ACTIVE(值为 1),将其映射到对应的活跃链表。
LRU_INACTIVE_FILE(2) + LRU_ACTIVE(1) = LRU_ACTIVE_FILE(3)
LRU_INACTIVE_ANON(0) + LRU_ACTIVE(1) = LRU_ACTIVE_ANON(1)

该函数通过组合判断,将页面映射到 5 种 LRU 链表之一:
在这里插入图片描述

(2)更新统计信息:记录该内存节点(zone)下各 LRU 链表的页面数量,用于内存压力评估。

static __always_inline void update_lru_size(struct lruvec *lruvec,enum lru_list lru, enum zone_type zid,int nr_pages)
{struct pglist_data *pgdat = lruvec_pgdat(lruvec);__mod_lruvec_state(lruvec, NR_LRU_BASE + lru, nr_pages);__mod_zone_page_state(&pgdat->node_zones[zid],NR_ZONE_LRU_BASE + lru, nr_pages);
#ifdef CONFIG_MEMCGmem_cgroup_update_lru_size(lruvec, lru, zid, nr_pages);
#endif
}

在内核的 LRU 页面管理体系中,准确统计各类型 LRU 链表的页面数量对内存管理至关重要。该函数的核心目标是:
更新节点级统计:记录特定内存节点(node)中各 LRU 链表的页面数量。
更新区域级统计:记录特定内存区域(zone)中各 LRU 链表的页面数量。
支持内存控制组(Memory CGroups):若启用该功能,同步更新 cgroup 相关统计。

a:获取内存节点(node)指针:

struct pglist_data *pgdat = lruvec_pgdat(lruvec);

lruvec_pgdat(lruvec):
通过 LRU 控制结构 lruvec 获取其所属的内存节点(node)指针 pgdat。
在 NUMA 架构中,系统包含多个内存节点,每个节点有独立的 pglist_data 结构。

static inline struct pglist_data *lruvec_pgdat(struct lruvec *lruvec)
{
#ifdef CONFIG_MEMCGreturn lruvec->pgdat;
#elsereturn container_of(lruvec, struct pglist_data, __lruvec);
#endif
}

b: 更新节点级 LRU 统计

__mod_lruvec_state(lruvec, NR_LRU_BASE + lru, nr_pages);

功能:原子性地修改 LRU 控制结构中的统计计数器。

参数:
lruvec:目标 LRU 控制结构。
NR_LRU_BASE + lru:统计计数器索引(将 LRU 类型映射到计数器数组)。
nr_pages:要增加或减少的页面数量。

作用:更新该内存节点中指定 LRU 链表的页面总数。

c:更新zone级 LRU 统计

__mod_zone_page_state(&pgdat->node_zones[zid],NR_ZONE_LRU_BASE + lru, nr_pages);

功能:原子性地修改特定内存区域(zone)的页面状态统计。

参数:
&pgdat->node_zones[zid]:目标内存区域(zone)的指针。
NR_ZONE_LRU_BASE + lru:统计计数器索引(将 LRU 类型映射到区域统计数组)。
nr_pages:要增加或减少的页面数量。

作用:更新该内存区域中指定 LRU 链表的页面总数。

d: 内存控制组(Memory CGroups)支持(可选)

#ifdef CONFIG_MEMCG
mem_cgroup_update_lru_size(lruvec, lru, zid, nr_pages);
#endif

功能:若启用了内存控制组(CGroups),同步更新 cgroup 的 LRU 统计信息。
作用:支持按 cgroup 隔离和统计内存使用,用于容器化环境中的内存限制和监控。

(3)挂载页面到链表:将页面添加到目标 LRU 链表的头部,确保链表按最近使用顺序排列。

list_add(&page->lru, &lruvec->lists[lru]);
struct page {union {struct {	/* Page cache and anonymous pages *//*** @lru: Pageout list, eg. active_list protected by* lruvec->lru_lock.  Sometimes used as a generic list* by the page owner.*/struct list_head lru;/* See page-flags.h for PAGE_MAPPING_FLAGS */struct address_space *mapping;pgoff_t index;		/* Our offset within mapping. */}
}
struct lruvec {struct list_head		lists[NR_LRU_LISTS];......
};

在这里插入图片描述

3.2 lru_cache_add的调用

当内核分配新页面(如文件缓存、匿名内存)或页面状态变化时,需将其加入 LRU 链表以便后续内存回收。

(1)
页面缓存处理:
当文件数据被读入内存时,新创建的页面会被添加到页面缓存
这些页面通常也会通过 lru_cache_add 被添加到 LRU 链表中

// v5.15/source/mm/filemap.cadd_to_page_cache_lru()-->lru_cache_add

(2)
匿名页面处理:
匿名页面 (anonymous pages) 如进程堆栈、堆分配的内存
这些页面被换出时也会被添加到 LRU 链表

do_swap_page()-->lru_cache_add

(3)页面从 “不可回收” 转为 “可回收”

(4)透明大页(THP)合并
将连续的普通页面合并为透明大页,如果连续的普通页面在LRU链表里面,那么连续的普通页面要从LRU链表摘除,合并的透明大页要加入到LRU链表中。

(5)页面状态转换
a:页面激活/停用
活跃 ↔ 非活跃转换:
活跃页面变为非活跃: deactivate_page()
非活跃页面重新激活: mark_page_accessed()

LRU 处理: 状态变化时会调整页面在 LRU 链表中的位置

b:页面回收时
转换路径: 活跃 LRU → 非活跃 LRU → 被回收

关键函数:
shrink_active_list(): 将活跃页移到非活跃列表
shrink_inactive_list(): 回收非活跃页

四、mark_page_accessed

/** 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);//如果页面未被引用过(!PageReferenced),直接设置 PG_referenced 标志位。if (!PageReferenced(page)) {//对应状态转换:inactive,unreferenced → inactive,referenced。//对应状态转换:active,unreferenced → active,referencedSetPageReferenced(page);} else if (PageUnevictable(page)) { //处理不可回收页面/** Unevictable pages are on the "LRU_UNEVICTABLE" list. But,* this list is never rotated or maintained, so marking an* evictable page accessed has no effect.*/} else if (!PageActive(page)) {  //激活非活跃页面/** If the page is on the LRU, queue it for activation via* lru_pvecs.activate_page. Otherwise, assume the page is on a* pagevec, mark it active and it'll be moved to the active* LRU on the next drain.*///若页面在 LRU 链表上(PageLRU),调用 activate_page() 将其移至活跃链表。if (PageLRU(page))activate_page(page);//若页面在临时缓存(如 pagevec)中,通过 __lru_cache_activate_page() 标记,待后续加入活跃链表。else__lru_cache_activate_page(page);//状态转换:inactive,referenced → active,unreferenced。ClearPageReferenced(page);workingset_activation(page);}if (page_is_idle(page))clear_page_idle(page);
}
EXPORT_SYMBOL(mark_page_accessed);

mark_page_accessed() 函数的实现,负责管理页面的访问状态和 LRU (Least Recently Used) 链表位置。

该函数用于标记一个页面被访问过,并根据其当前状态调整在 LRU 链表中的位置,影响内核的内存回收策略。页面状态转换遵循以下规则:

inactive,unreferenced → inactive,referenced
inactive,referenced → active,unreferenced
active,unreferenced → active,referenced

调用路径示例
页面首次访问:
mark_page_accessed() → SetPageReferenced(page)。

页面二次访问:
mark_page_accessed() → activate_page() → pagevec_lru_move_fn() → __activate_page()。

(1) 处理复合页

page = compound_head(page);

如果页面是复合页(如透明大页 THP),获取其头部页,确保操作作用于整个大页。

(2) 首次标记为“被引用”

if (!PageReferenced(page)) {SetPageReferenced(page);
}

如果页面未被引用过(!PageReferenced),直接设置 PG_referenced 标志位。
对应状态转换:inactive,unreferenced → inactive,referenced。
对应状态转换:active,unreferenced → active,referenced

(3) 处理不可回收页面

else if (PageUnevictable(page)) {/* 不操作 */
}

如果页面在 LRU_UNEVICTABLE 链表(如被 mlock() 锁定的页面),则忽略访问标记,因为这些页面不会被回收。

(4) 激活非活跃页面

else if (!PageActive(page)) {if (PageLRU(page))activate_page(page);else__lru_cache_activate_page(page);ClearPageReferenced(page);workingset_activation(page);
}

条件:页面已被引用过(PageReferenced)且不在活跃链表(!PageActive)。

操作:
若页面在 LRU 链表上(PageLRU),调用 activate_page() 将其移至活跃链表。
若页面在临时缓存(如 pagevec)中,通过 __lru_cache_activate_page() 标记,待后续加入活跃链表。

状态转换:inactive,referenced → active,unreferenced。

清理与统计:
清除 PG_referenced 标志(因已激活)。
workingset_activation() 更新内核工作集统计,优化内存回收策略。

(5) 清除空闲标记

if (page_is_idle(page))clear_page_idle(page);

若页面被标记为空闲(通过 Idle Page Tracking 机制),清除该标记,避免被错误回收。

4.1 activate_page

若页面在 LRU 链表上(PageLRU),调用 activate_page() 将其移至活跃链表。

#ifdef CONFIG_SMP
static void activate_page(struct page *page)
{page = compound_head(page);if (PageLRU(page) && !PageActive(page) && !PageUnevictable(page)) {struct pagevec *pvec;local_lock(&lru_pvecs.lock);pvec = this_cpu_ptr(&lru_pvecs.activate_page);get_page(page);if (pagevec_add_and_need_flush(pvec, page))pagevec_lru_move_fn(pvec, __activate_page);local_unlock(&lru_pvecs.lock);}
}

activate_page() 函数,用于将符合条件的页面激活并移动到活跃 LRU 链表。

核心功能:将非活跃且可回收的页面(!PageActive && !PageUnevictable)标记为活跃状态,并将其移动到活跃 LRU 链表。

优化设计:通过每 CPU 的 pagevec 缓存页面,实现批量操作以减少锁竞争。

(1) 处理复合页

page = compound_head(page);

如果页面是复合页(如透明大页 THP),获取其头部页,确保操作作用于整个大页。

(2) 检查页面状态

if (PageLRU(page) && !PageActive(page) && !PageUnevictable(page))

条件:
PageLRU:页面必须在 LRU 链表上。
!PageActive:页面当前不在活跃链表。
!PageUnevictable:页面必须可回收(不在 LRU_UNEVICTABLE 链表)。

(3) 获取本地 CPU 的 pagevec

local_lock(&lru_pvecs.lock);
pvec = this_cpu_ptr(&lru_pvecs.activate_page);

local_lock:获取每 CPU 的本地锁(防止当前 CPU 上的抢占或中断并发访问)。
this_cpu_ptr:获取当前 CPU 的 activate_page pagevec 指针。

/** The following struct pagevec are grouped together because they are protected* by disabling preemption (and interrupts remain enabled).*/
struct lru_pvecs {local_lock_t lock;struct pagevec lru_add;   				//待加入 LRU 的页面struct pagevec lru_deactivate_file;		//待去激活的file页面 activate->inactivatestruct pagevec lru_deactivate;			//待去激活的file/匿名页面 activate->inactivatestruct pagevec lru_lazyfree;			//用于延迟释放匿名交换页 activate->inactivate
#ifdef CONFIG_SMPstruct pagevec activate_page;			//待激活的页面 inactivate->activate
#endif};
static DEFINE_PER_CPU(struct lru_pvecs, lru_pvecs) = {.lock = INIT_LOCAL_LOCK(lock),
};

(4) 增加页面引用计数

get_page(page);

增加页面的引用计数,防止在操作过程中被释放。

(5) 添加页面到 pagevec 并判断是否需要刷新

if (pagevec_add_and_need_flush(pvec, page))pagevec_lru_move_fn(pvec, __activate_page);

pagevec_add_and_need_flush:
将页面添加到当前 CPU 的 activate_page pagevec。
如果 pagevec 已满(默认 15 个页面),返回 true 触发批量处理。

pagevec_lru_move_fn:
若 pagevec 已满,调用 __activate_page 回调函数,将 pagevec 中的所有页面移动到活跃 LRU 链表。

(6) 释放锁

local_unlock(&lru_pvecs.lock);

释放本地锁,允许其他操作访问当前 CPU 的 pagevec。

4.1.1 pagevec_lru_move_fn

static void pagevec_lru_move_fn(struct pagevec *pvec,void (*move_fn)(struct page *page, struct lruvec *lruvec))
{int i;struct lruvec *lruvec = NULL;unsigned long flags = 0;//遍历 pagevec 中的所有页面for (i = 0; i < pagevec_count(pvec); i++) {struct page *page = pvec->pages[i];/* block memcg migration during page moving between lru *///TestClearPageLRU() 检查页面是否在 LRU 链表中并清除该标记//允许页面暂时处于 "LRU 标记清除但仍在 LRU 链表中" 的状态//如果页面不在 LRU 链表中,则跳过该页面if (!TestClearPageLRU(page))continue;//获取 LRU 锁并执行移动操作lruvec = relock_page_lruvec_irqsave(page, lruvec, &flags);//调用传入的回调函数(如 __activate_page)执行实际的 LRU 移动操作(*move_fn)(page, lruvec);//重新设置 LRU 标记:SetPageLRU(page);}//释放锁并恢复中断状态:if (lruvec)unlock_page_lruvec_irqrestore(lruvec, flags);//释放页面引用并重置 pagevec//减少页面引用计数,允许页面在必要时被释放release_pages(pvec->pages, pvec->nr);//重置 pagevec,准备接收新的页面pagevec_reinit(pvec);
}

这段代码实现了 pagevec_lru_move_fn() 函数,用于批量处理页面向量(pagevec)中的页面,并将它们从一个 LRU 链表移动到另一个 LRU 链表(例如从非活跃 LRU 移动到活跃 LRU)。这个函数在内核内存管理中起着关键作用,通过批量操作提高了效率。

批量处理 pagevec 中的页面,减少锁获取和释放的开销
通过回调函数 move_fn 实现操作的通用性
通过 TestClearPageLRU() 和 SetPageLRU() 确保页面状态的一致性

其中TestClearPageLRU:合并检查和清除操作PG_LRU操作,将页面 LRU 状态检查和清除操作合并为原子操作。

作用:
原子性地检查页面是否在 LRU 链表上(PageLRU),并清除 PG_lru 标志。
返回值为真(1):页面原本在 LRU 链表上,标志位已被清除。
返回值为假(0):页面原本不在 LRU 链表上。

// v5.15/source/include/linux/page-flags.hstatic __always_inline int TestClearPage##uname(struct page *page)	\{ return test_and_clear_bit(PG_##lname, &policy(page, 1)->flags); }

这是一个通用宏,用于原子性地测试并清除页面的某个标志位(flag),返回操作前的标志状态
等于以下操作:

#define TestClearPageLRU(page) ({       unsigned long flags;                int ret;                            flags = page->flags;                ret = flags & PAGE_LRU;             //页面是否在LRU链表上if (ret)  //页面在LRU链表上,清除PAGE_LRU                 page->flags = flags & ~PAGE_LRU; //返回操作前的标志状态ret;                                
})

注意返回操作前的标志状态,虽然清除PAGE_LRU标志了,但是还是返回。

在页面隔离期间,允许页面暂时处于 “LRU 标记清除但仍在 LRU 链表中” 的状态。
隔离期间(TestClearPageLRU后),页面可能 临时 无PG_LRU 标志却仍在链表上(需后续删除)。
必须 先清除PG_LRU 标志,再从链表中删除页面。

4.1.2 __activate_page

static void __activate_page(struct page *page, struct lruvec *lruvec)
{//确保页面当前不在活跃链表。//确保页面是可回收的if (!PageActive(page) && !PageUnevictable(page)) {//获取页面的实际页数//透明大页(THP)处理:若页面是复合页(如 2MB 大页),返回其包含的基页数量(如 512 个 4KB 页)。int nr_pages = thp_nr_pages(page);//从当前 LRU 链表移除页面,将页面从其当前所属的 LRU 链表(非活跃)中删除。del_page_from_lru_list(page, lruvec);//标记页面为活跃状态,置页面的 PG_active 标志位,表示其属于活跃链表。SetPageActive(page);// 将页面加入活跃 LRU 链表,将页面插入到 lruvec 对应的活跃 LRU 链表中。add_page_to_lru_list(page, lruvec);// 触发事件追踪,记录页面激活事件,供内核追踪工具(如 ftrace)分析。trace_mm_lru_activate(page);//更新系统的页面激活计数。__count_vm_events(PGACTIVATE, nr_pages);// 更新对应内存控制组(memcg)的计数。__count_memcg_events(lruvec_memcg(lruvec), PGACTIVATE,nr_pages);}
}

__activate_page() 函数,用于将页面从非活跃 LRU 链表移动到活跃 LRU 链表。

核心功能:将符合条件的页面从非活跃(inactive)LRU 链表迁移到活跃(active)LRU 链表。
触发场景:当页面被二次访问时(首次访问仅标记 PG_referenced,二次访问触发激活)。

透明大页支持:
通过 thp_nr_pages() 统一处理普通页和大页的计数。

/*** thp_nr_pages - The number of regular pages in this huge page.* @page: The head page of a huge page.*/
static inline int thp_nr_pages(struct page *page)
{VM_BUG_ON_PGFLAGS(PageTail(page), page);if (PageHead(page))return HPAGE_PMD_NR;return 1;
}

必须先从原链表删除,再添加到新链表,防止链表 corruption。

4.2 __lru_cache_activate_page

static void __lru_cache_activate_page(struct page *page)
{struct pagevec *pvec;int i;//获取锁并定位 pagevec://获取本地锁保护 LRU 操作local_lock(&lru_pvecs.lock);//获取当前 CPU 的 lru_pvecs.lru_add 页面向量,该向量用于缓存待添加到 LRU 的页面pvec = this_cpu_ptr(&lru_pvecs.lru_add);/** Search backwards on the optimistic assumption that the page being* activated has just been added to this pagevec. Note that only* the local pagevec is examined as a !PageLRU page could be in the* process of being released, reclaimed, migrated or on a remote* pagevec that is currently being drained. Furthermore, marking* a remote pagevec's page PageActive potentially hits a race where* a page is marked PageActive just after it is added to the inactive* list causing accounting errors and BUG_ON checks to trigger.*///从 pagevec 末尾向前搜索,因为最新添加的页面更可能在尾部for (i = pagevec_count(pvec) - 1; i >= 0; i--) {struct page *pagevec_page = pvec->pages[i];//找到目标页面后,将其标记为活跃状态(SetPageActive)if (pagevec_page == page) {SetPageActive(page);break;}}//释放本地锁,允许其他 CPU 执行类似操作local_unlock(&lru_pvecs.lock);
}

__lru_cache_activate_page() 函数,用于在页面向量(pagevec)中激活指定页面。这个函数与 __activate_page() 不同,它不直接操作 LRU 链表,而是在页面被添加到 LRU 之前(即在 pagevec 缓存中)将其标记为活跃状态。

优化搜索策略:
反向搜索策略基于 “最近添加的页面最可能被激活” 的假设。
减少平均搜索时间,提高函数执行效率。

与 LRU 操作协同:
与 activate_page() 函数互补,处理不同阶段的页面激活需求。
被标记为活跃的页面在 pagevec 刷新时会被正确添加到活跃 LRU 链表。

五、lru_pvecs相关函数

/** The following struct pagevec are grouped together because they are protected* by disabling preemption (and interrupts remain enabled).*/
struct lru_pvecs {local_lock_t lock;struct pagevec lru_add;   				//待加入 LRU 的页面struct pagevec lru_deactivate_file;		//待去激活的file页面 activate->inactivatestruct pagevec lru_deactivate;			//待去激活的file/匿名页面 activate->inactivatestruct pagevec lru_lazyfree;			//用于延迟释放匿名交换页 activate->inactivate
#ifdef CONFIG_SMPstruct pagevec activate_page;			//待激活的页面 inactivate->activate
#endif};
static DEFINE_PER_CPU(struct lru_pvecs, lru_pvecs) = {.lock = INIT_LOCAL_LOCK(lock),
};

5.1 deactivate_file_page

/*** 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.*///若页面被标记为 Unevictable(如通过 mlock() 锁定),则直接返回,不进行降级操作。if (PageUnevictable(page))return;//获取页面引用if (likely(get_page_unless_zero(page))) {struct pagevec *pvec;//获取当前 CPU 的 lru_deactivate_file pagevec。local_lock(&lru_pvecs.lock);pvec = this_cpu_ptr(&lru_pvecs.lru_deactivate_file);//将页面添加到 当前 CPU 的 lru_deactivate_file pageveif (pagevec_add_and_need_flush(pvec, page))//若 pagevec 已满(默认 15 个页面),触发 pagevec_lru_move_fn 批量处理。//回调函数:lru_deactivate_file_fn 实际执行页面降级逻辑(将页面移出活跃 LRU 链表)。pagevec_lru_move_fn(pvec, lru_deactivate_file_fn);local_unlock(&lru_pvecs.lock);}
}

deactivate_file_page() 函数用于强制将文件映射页面标记为非活跃状态,向内存管理系统(VM)提示该页面是潜在的内存回收候选对象。通常在页面失效(invalidation)失败(如页面为脏页或正在回写)时调用,加速这类页面的回收流程。

添加页面到本地 CPU 的 pagevec:
(1)添加页面并检查刷新:
pagevec_add_and_need_flush(pvec, page):将页面添加到 pvec 中,若 pvec 已满(默认容量为 15),返回真。
(2)若 pvec 已满,调用 pagevec_lru_move_fn(pvec, lru_deactivate_file_fn) 批量处理 pvec 中的页面:通过回调函数 lru_deactivate_file_fn 将这些页面从活跃 LRU 移至非活跃 LRU(或调整位置)。

批量处理优化:
利用 pagevec(页面向量)批量缓存待处理页面,减少频繁操作 LRU 链表的开销。当 pagevec 满时才批量执行 lru_deactivate_file_fn,降低锁竞争和链表操作的次数。

每 CPU 隔离:
通过 this_cpu_ptr 获取当前 CPU 的 lru_deactivate_file 实例,避免跨 CPU 访问带来的缓存一致性问题和锁竞争,提高并发效率。

5.1.1 lru_deactivate_file_fn

/** If the page can not be invalidated, it is moved to the* inactive list to speed up its reclaim.  It is moved to the* head of the list, rather than the tail, to give the flusher* threads some time to write it out, as this is much more* effective than the single-page writeout from reclaim.** If the page isn't page_mapped and dirty/writeback, the page* could reclaim asap using PG_reclaim.** 1. active, mapped page -> none* 2. active, dirty/writeback page -> inactive, head, PG_reclaim* 3. inactive, mapped page -> none* 4. inactive, dirty/writeback page -> inactive, head, PG_reclaim* 5. inactive, clean -> inactive, tail* 6. Others -> none** In 4, why it moves inactive's head, the VM expects the page would* be write it out by flusher threads as this is much more effective* than the single-page writeout from reclaim.*/
static void lru_deactivate_file_fn(struct page *page, struct lruvec *lruvec)
{bool active = PageActive(page);int nr_pages = thp_nr_pages(page);//不可回收页面:跳过被标记为 Unevictable 的页面(如 mlock 锁定的页面)。if (PageUnevictable(page))return;/* Some processes are using the page *///映射页面:跳过仍被进程映射的页面(无法立即回收)。if (page_mapped(page))return;//将页面从当前 LRU 链表中移除。del_page_from_lru_list(page, lruvec);//清除活跃(PG_active)标志位。ClearPageActive(page);//引用(PG_referenced)标志位。ClearPageReferenced(page);//根据页面状态重新加入链表//脏页/回写页: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.*///移动到非活跃链表 头部,优先被刷盘线程(flusher)处理。add_page_to_lru_list(page, lruvec);//设置 PG_reclaim 标志, 标记为“待回收”。SetPageReclaim(page);} else { //干净页:/** The page's writeback ends up during pagevec* We move that page into tail of inactive.*///移动到非活跃链表 尾部,优先被直接回收。add_page_to_lru_list_tail(page, lruvec);__count_vm_events(PGROTATED, nr_pages);}//活跃页面降级统计if (active) {__count_vm_events(PGDEACTIVATE, nr_pages);__count_memcg_events(lruvec_memcg(lruvec), PGDEACTIVATE,nr_pages);}
}

lru_deactivate_file_fn 是 Linux 内核中用于处理文件映射页面 LRU 状态转换的核心函数,其作用是将符合条件的页面从活跃 LRU 链表移至非活跃 LRU 链表,或调整其在非活跃 LRU 中的位置,以优化内存回收效率。

核心功能:将文件页从活跃 LRU 链表降级到非活跃链表,或调整其在非活跃链表中的位置,以优化内存回收效率。

设计目标:
提高脏页/回写页的刷新效率(通过移动到非活跃链表头部,优先被刷盘线程处理)。
加速干净页的回收(移动到非活跃链表尾部,优先被回收)

注意:
根据页面状态重新插入 LRU 链表:
依据页面是否为脏页或正在回写,分两种情况处理:
(1)情况 1:页面是脏页或正在回写(PageWriteback 或 PageDirty)

add_page_to_lru_list(page, lruvec);  // 插入非活跃LRU链表的头部
SetPageReclaim(page);  // 标记为“待回收”

设计意图:脏页或正在回写的页面需要先写入磁盘才能回收。将其插入非活跃 LRU 头部,可让后台刷新线程(如flusher线程)优先批量处理这些页面(批量 IO 效率远高于内存回收时的单页 IO)。
PG_reclaim标记:提示内核该页面是回收候选,即使回写过程中存在微小竞争(如与end_page_writeback的并发),也不影响整体逻辑。

(2)情况 2:页面是干净页(非脏页且不在回写)

add_page_to_lru_list_tail(page, lruvec);  // 插入非活跃LRU链表的尾部
__count_vm_events(PGROTATED, nr_pages);  // 统计页面“旋转”事件

设计意图:干净页无需回写即可回收,将其放在非活跃 LRU 尾部。

干净页(已与磁盘内容一致)可以直接释放,无需触发 IO 操作。将其放在非活跃 LRU 尾部(最久未使用位置),意味着当内存紧张时,这些 “低回收成本” 的页面会被优先回收,避免先回收需要 IO 的脏页(脏页需先写入磁盘,回收成本高)。

在 LRU(最近最少使用)机制中,非活跃 LRU 链表的尾部是优先被回收的页面(因为尾部是 “最久未使用” 的页面)。

状态处理规则:
在这里插入图片描述

5.1.2 使用场景

deactivate_file_page() 的核心使用场景可归纳为:
(1)文件页失效受阻时(脏/回写页):当内核尝试使某个文件映射页面失效(如文件被截断、文件系统卸载时),若页面当前为脏页或正在回写(无法立即失效),则调用此函数将其移至非活跃 LRU 链表头部,加速回收。

(2)用户主动提示可回收时(如 madvise)。

(3)内存回收过程中优化脏页处理时。

5.2 deactivate_page

/** deactivate_page - deactivate a page* @page: page to deactivate** deactivate_page() moves @page to the inactive list if @page was on the active* list and was not an unevictable page.  This is done to accelerate the reclaim* of @page.*/
void deactivate_page(struct page *page)
{//页面必须位于 LRU 链表上。//页面当前在活跃链表。//页面必须是可回收的(非 mlock 锁定的页面)。if (PageLRU(page) && PageActive(page) && !PageUnevictable(page)) {struct pagevec *pvec;local_lock(&lru_pvecs.lock);//获取当前 CPU 的 lru_deactivate pagevec,用于批量处理页面。pvec = this_cpu_ptr(&lru_pvecs.lru_deactivate);get_page(page);//将页面添加到当前 CPU 的 lru_deactivate pagevec。if (pagevec_add_and_need_flush(pvec, page))//若 pagevec 已满(默认 15 个页面),返回 true 触发批量处理。//调用 lru_deactivate_fn 回调函数,将 pagevec 中的页面移动到非活跃链表。pagevec_lru_move_fn(pvec, lru_deactivate_fn);local_unlock(&lru_pvecs.lock);}
}

deactivate_page() 函数用于将特定页面从活跃 LRU 链表移至非活跃 LRU 链表,加速其回收流程。与之前分析的 deactivate_file_page() 不同,此函数不区分页面类型(文件映射页或匿名页),只要满足条件就执行停用操作。

lru_deactivate_fn

static void lru_deactivate_fn(struct page *page, struct lruvec *lruvec)
{//确保页面当前在活跃链表。//排除不可回收的页面(如 mlock 锁定的页面)。if (PageActive(page) && !PageUnevictable(page)) {//若页面是透明大页,返回其包含的基页数量(如 2MB 大页返回 512);普通页返回 1。int nr_pages = thp_nr_pages(page);//将页面从其当前所属的活跃 LRU 链表中删除。del_page_from_lru_list(page, lruvec);//移除活跃状态标志。ClearPageActive(page);//清除引用标记ClearPageReferenced(page);//将页面添加到非活跃 LRU 链表(默认插入头部)。add_page_to_lru_list(page, lruvec);//更新统计事件__count_vm_events(PGDEACTIVATE, nr_pages);__count_memcg_events(lruvec_memcg(lruvec), PGDEACTIVATE,nr_pages);}
}

在这里插入图片描述

5.3 mark_page_lazyfree

/*** 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)
{//页面必须在 LRU 链表上。//必须是匿名页(非文件页)。//页面有交换空间支持(PageSwapBacked,即已被交换到磁盘)//未在交换缓存中(避免重复处理)。//页面必须是可回收的。if (PageLRU(page) && PageAnon(page) && PageSwapBacked(page) &&!PageSwapCache(page) && !PageUnevictable(page)) {struct pagevec *pvec;local_lock(&lru_pvecs.lock);//获取当前 CPU 的 lru_lazyfree pagevec,用于批量处理页面。pvec = this_cpu_ptr(&lru_pvecs.lru_lazyfree);get_page(page);//将页面添加到当前 CPU 的 lru_lazyfree pagevec。if (pagevec_add_and_need_flush(pvec, page))//若 pagevec 已满(默认 15 个页面),返回 true 触发批量处理。//调用 lru_lazyfree_fn 回调函数,实际执行页面迁移。pagevec_lru_move_fn(pvec, lru_lazyfree_fn);local_unlock(&lru_pvecs.lock);}
}

mark_page_lazyfree() 函数用于将特定的匿名页面(anonymous page)标记为 “延迟释放(lazyfree)” 状态,加速其回收过程。该函数主要处理匿名且已交换到磁盘的页面,通过将它们移至非活跃 LRU 链表,为内存回收做准备。

核心功能:将符合条件的匿名页移动到非活跃文件链表(inactive file list),加速其回收过程。
设计目的:针对不需要立即释放但可以延迟处理的匿名页(如用户空间通过 MADV_FREE 指定的页),优化内存回收效率。

lru_lazyfree_fn

static void lru_lazyfree_fn(struct page *page, struct lruvec *lruvec)
{/*匿名页(PageAnon):如进程堆、栈等非文件映射的内存区域。有交换空间支持(PageSwapBacked):页面已被交换到磁盘,有对应的交换项。不在交换缓存中(!PageSwapCache):避免重复操作已在交换缓存中的页面。可回收(!PageUnevictable):未被 mlock 等机制锁定*/if (PageAnon(page) && PageSwapBacked(page) &&!PageSwapCache(page) && !PageUnevictable(page)) {int nr_pages = thp_nr_pages(page);// 从当前LRU链表中移除del_page_from_lru_list(page, lruvec);// // 清除活跃标记,转为非活跃状态ClearPageActive(page);// // 清除引用标记,降低保留优先级ClearPageReferenced(page);/** Lazyfree pages are clean anonymous pages.  They have* PG_swapbacked flag cleared, to distinguish them from normal* anonymous pages*/// 清除交换空间关联标记ClearPageSwapBacked(page);// 重新插入非活跃LRU链表头部add_page_to_lru_list(page, lruvec);//统计延迟释放事件__count_vm_events(PGLAZYFREE, nr_pages);__count_memcg_events(lruvec_memcg(lruvec), PGLAZYFREE,nr_pages);}
}

lru_lazyfree_fn 是 Linux 内核中用于处理匿名页面延迟释放(lazyfree)的核心回调函数。其主要作用是将符合条件的匿名交换页从活跃状态转为非活跃状态,并清除其交换空间关联,从而加速内存回收。

交换空间优化:
通过 ClearPageSwapBacked 清除交换空间关联,避免页面被再次交换到磁盘,节省 IO 资源。
这类页面通常是 “干净的匿名页”(已与交换空间同步),无需额外写回操作即可释放。

与 lru_deactivate_fn 的区别在于,此函数额外清除了 SwapBacked 标记,明确指示页面可直接释放。

参考资料

linux 5.15
https://blog.csdn.net/pwl999/article/details/116028868
https://www.cnblogs.com/LoyenWang/p/11827153.html

http://www.dtcms.com/a/273943.html

相关文章:

  • 蓝牙协议栈高危漏洞曝光,攻击可入侵奔驰、大众和斯柯达车载娱乐系统
  • HTTPS安全机制:从加密到证书全解析
  • 意识边界的算法战争—脑机接口技术重构人类认知的颠覆性挑战
  • React 的常用钩子函数在Vue中是如何设计体现出来的。
  • 苹果UI 设计
  • 前端面试专栏-算法篇:23. 图结构与遍历算法
  • 4.丢出异常捕捉异常TryCatch C#例子
  • 使用gdal读取shp及filegdb文件
  • C/C++动态内存管理函数详解:malloc、calloc、realloc与free
  • Launcher3桌面页面布局结构
  • JavaScript加强篇——第四章 日期对象与DOM节点(基础)
  • 基于 HT 技术的智慧交通三维可视化技术架构与实践
  • 全球化 2.0 | 印尼金融科技公司通过云轴科技ZStack实现VMware替代
  • Spring的事务控制——学习历程
  • Kuberneres高级调度01
  • 如何使用Fail2Ban阻止SSH暴力破解
  • ICCV2025接收论文速览(1)
  • 导出word并且插入图片
  • 【C++ 深入解析 C++ 模板中的「依赖类型」】
  • 「Linux命令基础」Shell命令基础
  • PC网站和uniapp安卓APP、H5接入支付宝支付
  • 基于ASP.NET+SQL Server实现(Web)企业进销存管理系统
  • 《探索电脑麦克风声音采集多窗口实时可视化技术》
  • 【Springboot】Bean解释
  • Jenkins 自动触发执行的配置
  • Ntfs!NtfsCheckpointVolume函数中的Ntfs!LfsFlushLfcb函数对Lfcb->LogHeadBuffer进行了赋值--重要
  • 冒泡、选择、插入排序:三大基础排序算法深度解析(C语言实现)
  • 模型训练的常用方法及llama-factory支持的数据训练格式
  • [论文阅读] 人工智能 + 软件工程 | LLM辅助软件开发:需求如何转化为代码?
  • GPT和MBR分区