Linux 内存管理之page cache

发布于:2025-06-26 ⋅ 阅读:(18) ⋅ 点赞:(0)

一、page cache

Page Cache(页缓存) 是 Linux 内核中用于缓存文件数据(包括普通文件、块设备文件等)的核心机制,它以内存页(通常为 4KB)为单位,将磁盘中的文件数据存储在物理内存中,从而减少对磁盘的 I/O 访问,提升系统整体性能。

对磁盘的数据进行缓存从而提高性能主要是基于两个因素:
(1)磁盘访问的速度比内存慢好几个数量级(毫秒和纳秒的差距)
(2)被访问过的数据,有很大概率会被再次访问。
如下图所示:
在这里插入图片描述

1.1 File-backed pages和Anonymous pages

并不是所有 page 都被组织为 Page Cache。Linux 系统上供用户可访问的内存分为两个类型,即:File-backed pages和Anonymous pages。

// v5.15/source/include/linux/mm_types.h

struct page {
	unsigned long flags;		/* Atomic flags, some possibly
					 * updated asynchronously */
	/*
	 * Five words (20/40 bytes) are available in this union.
	 * WARNING: bit 0 of the first word is used for PageTail(). That
	 * means the other users of this union MUST NOT use the bit to
	 * avoid collision and false-positive PageTail().
	 */
	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. */
			/**
			 * @private: Mapping-private opaque data.
			 * Usually used for buffer_heads if PagePrivate.
			 * Used for swp_entry_t if PageSwapCache.
			 * Indicates order in the buddy system if PageBuddy.
			 */
			unsigned long private;
		};
		......
}

(1)File-backed pages:文件备份页也就是 Page Cache 中的 page,对应于磁盘上的若干数据块;对于这些页最大的问题是脏页回盘;
关联磁盘文件:内容对应于磁盘上的文件(如程序.text段等)。
缓存机制:属于 Page Cache 的一部分,由内核自动缓存以加速文件访问。

struct page中mapping字段最低位为 0 表示文件页。mapping指向该文件页关联文件的struct address_space(被文件的 inode 所持有),pgoff_t index字段表示该文件页在struct address_space中的索引。内核会通过 index 字段从 struct address_space 中查找该文件页。

File-backed pages(Page Cache)的内存回收代价较低。Page Cache 通常对应于一个文件上的若干顺序块,因此可以通过顺序 I/O 的方式落盘。另一方面,如果 Page Cache 上没有进行写操作(所谓的没有脏页),甚至不会将 Page Cache 回盘,因为数据的内容完全可以通过再次读取磁盘文件得到。

(2)Anonymous pages:匿名页不对应磁盘上的任何磁盘数据块,它们是进程的运行时内存空间(例如进程的堆,栈空间等属性);
无磁盘关联:存储进程运行时动态分配的数据(如堆、栈、共享内存等),没有对应的磁盘文件,内容仅存在于内存。

struct page中mapping字段最低位为 1 表示匿名页。mapping指向该匿名页在进程虚拟内存空间中唯一对应的匿名映射区 struct anon_vma 结构体,用于物理内存到虚拟内存的反向映射。

// v5.15/source/include/linux/page-flags.h

#define PAGE_MAPPING_ANON	0x1

static __always_inline int PageAnon(struct page *page)
{
	page = compound_head(page);
	return ((unsigned long)page->mapping & PAGE_MAPPING_ANON) != 0;
}

Anonymous pages 的内存回收代价较高。这是因为 Anonymous pages 通常随机地写入持久化交换设备。另一方面,无论是否有更操作,为了确保数据不丢失,Anonymous pages 在 swap 时必须持久化到磁盘。

Anonymous pages不属于Page Cache。

在这里插入图片描述
swappiness是 Linux 内核控制 匿名页(Anonymous Pages) 与 文件缓存(File-backed Pages) 回收平衡的关键参数。

$ cat /proc/sys/vm/swappiness
60

定义:控制内核在内存不足时,优先回收文件缓存还是将匿名页换出到 Swap。
取值范围:0(禁用 Swap 倾向)到 100(积极使用 Swap,linux 5.8内核版本最大值改为200)。
默认值:60(大多数发行版的默认设置)。

// v5.8/source/mm/vmscan.c

/*
 * From 0 .. 200.  Higher means more swappy.
 */
int vm_swappiness = 60;

Linux内存回收业务当中更加偏向对文件页的回收,通过增大swappiness可以提高匿名页的扫描比例,进一步促进系统回收更多的匿名页。

1.2 page cache/slab cache

page cache 和 slab cache是两种cache。
文件 = 数据 + 元数据。
page cache对应的是文件系统中的文件数据(userdata),而inode cache对应的是文件系统中文件的元数据(metadata)。
在这里插入图片描述

如下图所示:
在这里插入图片描述

page cache缓冲硬盘中的内容,dcache、icache缓存文件系统的数据。这些内容是为了提升性能而设计的,还可以再次从硬盘中重新读取来构建对象,这部分内容可以在内存紧张的时候可以直接释放。

inode 缓存则主要用于缓存文件系统的元数据,比如文件名、文件权限、文件大小、文件的创建时间和修改时间等 。当我们要访问一个文件时,首先需要通过 inode 缓存找到文件的元数据,然后才能进一步读取文件的内容 。例如,当我们在命令行中输入ls命令查看目录下的文件时,系统会先在 inode 缓存中查找该目录下所有文件的元数据,然后将这些信息显示出来。如果没有 inode 缓存,每次执行ls命令都需要从磁盘中读取这些元数据,这将大大降低系统的响应速度 。inode 缓存通过将常用的元数据存储在内存中,减少了对磁盘的访问次数,提高了文件系统的性能 。

Page Cache 一般都是用户空间的程序在使用,但是是属于内核空间的内存,dcache、icache是内核空间在使用。
用户数据 Page Cache — reclaimable memory。
内核数据 Slab — reclaimable kernel memory。

1.3 读/写路径

(1)应用程序请求读取文件数据
内核首先检查 page cache 中是否存在所需数据
命中:直接从内存返回数据(快速路径)
未命中:发起磁盘 I/O,将数据读入 page cache 后再返回

当内核发起一个读请求时(例如进程发起read()请求),首先会检查请求的数据是否缓存到了Page Cache中。
如果有,那么直接从内存中读取,不需要访问磁盘,这被称为cache命中(cache hit);
如果cache中没有请求的数据,即cache未命中(cache miss),就必须从磁盘中读取数据。然后内核将读取的数据缓存到cache中,这样后续的读请求就可以命中cache了。
page可以只缓存一个文件部分的内容,不需要把整个文件都缓存进来。

(2)应用程序写入文件数据
数据首先被写入 page cache
写入方式:
Write-back(默认):延迟写入磁盘,定期或内存压力时同步。
而在Write Back模式下,数据更新仅在缓存中进行,不会立即写入主存储器。只有当缓存中的数据需要被新数据替换时,被修改的数据才会写回主存储器。这种方法的优点是提高了CPU执行的效率,因为不需要每次都写入慢速的主存储器。但缺点是实现起来技术较为复杂,且如果在更新后的数据未被写入主存储器时系统掉电,那么数据可能会丢失。
当内核发起一个写请求时(例如进程发起write()请求),同样是直接往cache中写入,后备存储中的内容不会直接更新(当服务器出现断电关机时,存在数据丢失风险)。
内核会将被写入的page标记为dirty,并将其加入dirty list中。内核会周期性地将dirty list中的page写回到磁盘上,从而使磁盘上的数据和内存中缓存的数据一致。

Write-through:同步写入磁盘和缓存。
在Write Through模式下,每当缓存中的数据被更新时,更改同时也会写入主存储器。这种方法的优点是简单且数据一致性较好,因为缓存和主存储器中的数据始终保持同步。但缺点是性能较低,因为每次写操作都需要访问主存储器,这会导致速度变慢。例如,如果一个程序频繁地修改一个局部变量,即使其他进程或线程不需要这些数据,CPU也会频繁地在缓存和主存储器之间交换数据,造成不必要的带宽损失。

1.4 脏页回写

当满足以下三个条件之一将触发脏数据刷新到磁盘操作:
(1)dirty_expire_centisecs
数据存在的时间超过了dirty_expire_centisecs时间;

cat /proc/sys/vm/dirty_expire_centisecs
3000

这个值的单位为 百分之一秒(centiseconds),因此 3000 表示 30 秒。

定义 脏页(被修改但未写入磁盘的数据) 在内存中的最长存活时间。
超过这个时间后,内核的 flush 线程(如 kworker 或旧版 pdflush)会将这些脏页写入磁盘(即使未达到 dirty_background_ratio 或 dirty_ratio 阈值)。
适用于 Writeback(写回)缓存策略(Linux 默认的文件写入方式)。

(2)dirty_background_ratio
脏数据所占内存 > dirty_background_ratio,也就是说当脏数据所占用的内存占总内存的比例超过dirty_background_ratio的时候会触发pdflush刷新脏数据。

cat /proc/sys/vm/dirty_background_ratio
10

表示当系统内存中的 脏页(Dirty Pages) 达到总内存的 10% 时,内核会 异步 启动后台进程(如 kworker 或 flush 线程)将脏页写入磁盘,而不会阻塞应用程序的 I/O 操作。

定义 触发后台异步刷脏的内存脏页比例(单位:百分比)。
默认值 10 表示当脏页占用超过 10% 的可用内存 时,内核开始 后台写入磁盘。
不影响应用程序性能,因为刷盘是异步进行的。

(3)dirty_ratio
脏数据所占内存 > dirty_ratio,也就是说当脏数据所占用的内存占总内存的比例超过dirty_ratio的时候,内核会 强制同步 将脏页写入磁盘,阻塞 所有新的写 I/O 操作,直到脏页比例降低。

cat /proc/sys/vm/dirty_ratio
20

dirty_ratio=20 是 Linux 内核控制 同步刷脏(Sync Writeback) 的关键参数,表示当系统内存中的 脏页(Dirty Pages) 达到总内存的 20% 时,内核会 强制同步 将脏页写入磁盘,阻塞 所有新的写 I/O 操作,直到脏页比例降低。

定义 触发同步刷脏的内存脏页比例(单位:百分比)。
默认值 20 表示当脏页占用超过 20% 的可用内存 时,内核会 强制同步刷盘,导致应用程序的写入操作被阻塞。
直接影响 I/O 性能,因为刷盘是同步进行的(相比 dirty_background_ratio 的异步刷盘)。

与 dirty_background_ratio 的关系:
dirty_background_ratio=10 异步刷盘(后台线程处理) 不影响应用性能。
dirty_ratio=20 同步刷盘(阻塞应用 I/O) 可能导致写入延迟增加。

还有参数dirty_writeback_centisecs:

cat /proc/sys/vm/dirty_writeback_centisecs
500

dirty_writeback_centisecs=500 是 Linux 内核控制 脏页回写(Writeback)线程唤醒频率 的参数,单位是 百分之一秒(centiseconds),默认值 500 表示 每 5 秒 唤醒一次 flush 线程(如 kworker)检查并回写脏页到磁盘。

控制内核 定期检查脏页 的频率,决定刷盘的 及时性。

默认 500(5 秒)表示:
每隔 5 秒,内核唤醒 flush 线程检查是否有脏页需要写入磁盘。
如果发现脏页 超过 dirty_expire_centisecs(默认 30 秒),则触发回写。
注意:该参数仅控制 检查频率,实际刷盘还受 dirty_background_ratio 和 dirty_ratio 影响。

在这里插入图片描述

查看脏页情况:

# 查看当前脏页大小(KB)
cat /proc/meminfo | grep Dirty

# 查看刷盘线程状态
grep -E 'dirty|writeback' /proc/vmstat

# 查看当前刷盘策略
sysctl -a | grep dirty
$ cat /proc/meminfo | grep Dirty
Dirty:                 8 kB

$ grep -E 'dirty|writeback' /proc/vmstat
nr_dirty 2
nr_writeback 0
nr_writeback_temp 0
nr_dirty_threshold 3056636
nr_dirty_background_threshold 1526452

$ sudo /sbin/sysctl -a | grep dirty
vm.dirty_background_bytes = 0
vm.dirty_background_ratio = 10
vm.dirty_bytes = 0
vm.dirty_expire_centisecs = 3000
vm.dirty_ratio = 20
vm.dirty_writeback_centisecs = 500
vm.dirtytime_expire_seconds = 43200

1.5 drop_caches

/proc/sys/vm/drop_caches 是一个用于 手动清理 Linux 内核缓存 的特殊文件,可以通过写入不同的值来释放 Page Cache、Slab 缓存等。

To free pagecache:
	echo 1 > /proc/sys/vm/drop_caches
To free reclaimable slab objects (includes dentries and inodes):
	echo 2 > /proc/sys/vm/drop_caches
To free slab objects and pagecache:
	echo 3 > /proc/sys/vm/drop_caches

在这里插入图片描述
仅在调试/测试时使用,生产环境优先依赖内核自动管理。
如需使用,务必先执行 sync 并评估性能影响。
这是一个调试工具,而非生产环境调优手段。内核自动回收机制比手动操作高效得多。

注意:比如使用 echo 2 > /proc/sys/vm/drop_caches reclaimable slab objects,回收了 inode slab obeject,那么 inode 对应的Page Cache也都会被回收掉,所以如果业务进程读取的文件对应的inode被回收了,那么该文件所有的Page Cache都会被释放掉。

进程会通过inode来找到文件的地址空间(address_space),然后结合文件偏移(会转换成page index)来找具体的Page。如果该Page存在,那就说明文件内容已经被读取到了内存;如果该Page不存在那就说明不在内存中,需要到磁盘中去读取。你可以理解为inode是Pagecache Page(页缓存的页)的宿主(host),如果inode不存在了,那么PageCache Page也就不存在了。

1.6 时间局部性与空间局部性

(1)时间局部性与 Page Cache
原理
定义:最近访问的数据很可能在短期内被再次访问。

Page Cache 实现:
内核保留最近访问的磁盘数据在内存中,后续重复访问可直接命中缓存。
LRU(最近最少使用)算法管理缓存回收,优先保留热点数据。

(2)空间局部性与预读(Readahead)
原理
定义:访问某一数据时,其相邻数据很可能被连续访问。

Page Cache 实现:
预读算法:检测到顺序访问模式时,异步提前读取后续磁盘块(如 readahead(2) 系统调用)。
预读窗口动态调整:根据应用访问模式自适应扩展/收缩。

1.7 Page Cache 的两种类型

(1) Discardable Pages(可丢弃页)
对应内容:只读文件段(如代码段 .text)
特点:
内容一致性:内存副本与磁盘完全一致,无修改可能。
回收代价:直接清空页表项(PTE)即可,无需 I/O 操作。
内核标记:PG_clean(无脏数据)。

(2) Syncable Pages(需同步页)
对应内容:可读写文件数据(如数据段 .data)

特点:
脏页标记:通过 SetPageDirty(page) 设置 PG_dirty 标志,同时 CPU 硬件会设置 PTE 的 Dirty 位。

回写触发:
周期性地由 flush 线程(如 kworker)扫描。
达到 dirty_expire_centisecs(默认 30 秒)或内存压力时触发。

1.8 关键数据结构

struct address_space {
    struct inode *host;      // 所属的inode
    struct radix_tree_root page_tree; // 基数树存储page cache页
    // ...
};

struct page {
    unsigned long flags;     // 页面标志位
    struct address_space *mapping; // 所属的address_space
    pgoff_t index;          // 在文件中的偏移(页为单位)
    // ...
};

一个address_space管理了一个文件在内存中缓存的所有pages,这部分缓存也是页高速缓存。

每个进程打开一个文件的时候,都会生成一个表示这个文件的struct file,但是文件的struct inode只有一个,inode才是文件的唯一标识,指向address_space的指针就是内嵌在inode结构体中的。在page cache中,每个page都有对应的文件,这个文件就是这个page的owner,address_space将属于同一owner的pages联系起来,将这些pages的操作方法与文件所属的文件系统联系起来。

二、Page Cache 的产生

在 Linux 中,Page Cache 主要通过两种机制产生:Buffered I/O(标准 I/O) 和 Memory-Mapped I/O(存储映射 I/O)。
如下图所示:
在这里插入图片描述

2.1 Buffered I/O(标准 I/O)

系统调用:通过 read()/write() 等标准文件操作接口。

磁盘文件 → Page Cache → 用户态缓冲区(如 `fread()` 的 buffer)→ 应用程序

内核参与:数据需在 用户态缓冲区 和 Page Cache 之间显式拷贝。

标准I/O是写的(write(2))用户缓冲区(Userpace Page对应的内存),然后再将用户缓冲区里的数据拷贝到内核缓冲区(Pagecache Page对应的内存);如果是读的(read(2))话则是先从内核缓冲区拷贝到用户缓冲区,再从用户缓冲区读数据,也就是buffer和文件内容不存在任何映射关系。

如下图所示:
在这里插入图片描述
这个过程大致可以描述为:首先往用户缓冲区buffer(这是Userspace Page)写入数据,然后buffer中的数据拷贝到内核缓冲区(这是Pagecache Page),如果内核缓冲区中还没有这个Page,就会发生Page Fault会去分配一个Page,拷贝结束后该Pagecache Page是一个Dirty Page(脏页),然后该Dirty Page中的内容会同步到磁盘,同步到磁盘后,该Pagecache Page变为Clean Page并且继续存在系统中。

2.2 Memory-Mapped I/O(mmap)

系统调用:通过 mmap() 直接将文件映射到进程地址空间。

磁盘文件 → Page Cache → 进程虚拟内存(直接映射,无拷贝)

内核参与:通过 缺页异常(Page Fault) 按需加载数据,对应用程序透明。

对于存储映射I/O而言,则是直接将Pagecache Page给映射到用户地址空间,用户直接读写Pagecache Page中内容。

三、page cache回收

page cache回收用于在系统内存不足时释放被缓存的文件数据所占用的内存。
page cache 中的页面和 anonymous page 都是可以被回收的。

对于内存管理,我们回收的目的主要是基于用户空间进行回收,其主要回收的策略如下:
(1)用户空间内存:原则上应该都可以参与内存回收,除非那些被进程锁定(mlock())的页。
(2)内核空间内存:一般内核代码段,数据段,内核kmalloc()/vmalloc()出来的内存,内核线程占用的内存等都是不可以回收的,除此之外的内存都是我们要回收,所以大致为磁盘高速缓存(如索引节点,目录项高速缓存),页面高速缓存(访问文件时系统生成的页面cache),mmap()文件时所用的有名映射所使用的物理内存。

3.1 回收模式

在这里插入图片描述
如下如所示:
在这里插入图片描述

zone:内存回收的基本单位

在这里插入图片描述
内存回收是以zone为单位进行的,zone的内存回收,它针对三样东西进程回收:slab、lru链表中的页、buffer_head。而系统判断一个zone需不需要进行内存回收,如上面所说,为zone设置一条线,当此zone的空闲页框不足以到达这条线时,就会对此zone进行内存回收,实际上一个zone有三条线,这三条线分别是最小阀值(WMARK_MIN),低阀值(WMARK_LOW),高阀值(WMARK_HIGH),它们都保存在zone的watermark[NR_WMARK]数组中,这个数组中保存的是各个阀值要求的页框数量,而每个阀值都会对内存回收造成影响。

内核为每个物理内存区域(zone)画了三条水位线:WMARK_MIN(页最小阈值), WMARK_LOW(页低阈值)和 WMARK_HIGH(页高阈值)。定义在 zone_watermarks 枚举中。

enum zone_watermarks {
	WMARK_MIN,
	WMARK_LOW,
	WMARK_HIGH,
	NR_WMARK
};

内存水位线是按区域(zone)维护的三个阈值,用于监控和管理系统内存使用状态:
(1)WMARK_MIN(最低水位线)
当可用内存低于此阈值时,系统进入紧急状态
触发直接内存回收(Direct Reclaim),可能导致应用程序短暂卡顿
优先保证关键内核操作的内存需求

(2)WMARK_LOW(低水位线)
当可用内存低于此阈值时,系统开始后台异步回收内存
唤醒 kswapd 内核线程进行页面回收(Page Reclaim)
维持系统内存的 “健康状态”

(3)WMARK_HIGH(高水位线)
当可用内存高于此阈值时,系统内存充足
停止后台内存回收,允许更宽松的内存分配策略
通常作为系统内存使用的 “安全上限”

内存回收策略
kswapd 后台回收:异步进行,优先回收不常用的匿名页和文件缓存。
直接内存回收:同步阻塞应用程序,强制回收内存以满足紧急需求。
内存不足(OOM):当所有回收手段无效时,触发 OOM 杀手选择进程终止。

阈值计算
水位线值通常基于区域总页数的百分比动态计算,例如:

min = zone->managed_pages * (sysctl_vm_min_free_kbytes / totalram_pages);
low = min * 5/4;
high = min * 3/2;

WMARK_MIN、WMARK_LOW和WMARK_HIGH 水位线都是通过内核参min_free_kbytes分别计算得到,使用sysctl可以动态设置这个参数,达到动态控制水位线的目的。

cat /proc/sys/vm/min_free_kbytes
67584

min_free_kbytes 是 Linux 内核中的一个关键参数,用于控制系统预留的最低空闲内存量。这个参数直接影响内存水位线机制的行为,对系统性能和稳定性有重要影响。

基本功能
min_free_kbytes 定义了系统中每个内存区域(zone)必须保留的最低空闲内存量(以千字节为单位)。
根据每个区域容量大小比例,从min_free_kbytes划分每个区域的 WMARK_MIN 水位线。
这个值直接决定了内存水位线中的 WMARK_MIN 阈值,而 WMARK_LOW 和 WMARK_HIGH 则基于 WMARK_MIN 按比例计算。

3.2 回收过程

page cache 中的页面和 anonymous page 都是可以被回收的。
在回收page cache文件页时,系统会先判断文件页的状态。如果文件页保存的内容与磁盘中文件对应内容一致,即该文件页是干净的,那么无需进行回写操作,可直接将其作为空闲页框释放到伙伴系统;反之,如果文件页保存的数据和磁盘中文件对应的数据不一致,这样的文件页被称为脏页,就需要先将其回写到磁盘中对应数据所在的位置,确保数据的一致性,然后才能作为空闲页框释放 。

anonymous page用于存储进程的堆、栈数据等 。当系统需要回收匿名页时,会筛选出那些访问频率较低、不经常使用的匿名页,将它们写入到 swap 分区中。swap 分区就像是内存的 “临时仓库”,当内存空间紧张时,把暂时不用的数据存放到这里,等需要时再取回来。写入 swap 分区后,这些匿名页就可以作为空闲页框释放到伙伴系统,供其他进程申请使用,从而有效缓解内存压力。

上面说到zone的内存回收,它针对三样东西进程回收:slab、lru链表中的页、buffer_head。这里只讨论内存回收针对lru链表中的页是如何进行回收的。lru链表主要用于管理进程空间中使用的内存页。

在 Linux 内核的内存管理中,LRU(Least Recently Used)链表用于高效管理内存页的回收。Linux 采用的方法是维护 2 个双向链表,一个是包含了最近使用页面的 active list,另一个是包含了最近不使用页面的 inactive list。并且在 struct page 的 page flags 中使用了 PG_referenced 和 PG_active 两个标志位来标识页面的活跃程度。

Active List 与 Inactive List 的作用:
在这里插入图片描述

双向链表结构
active list:存储近期活跃页面(PG_active=1),新页面从头部插入,尾部页面可能降级。
inactive list:存储非活跃页面(PG_active=0),尾部页面优先被回收。

PG_active:标记页面所属链表(1=active,0=inactive)。
PG_referenced:标记页面近期是否被访问(1 = 已访问,0 = 未访问)。

// v5.15/source/include/linux/mm_types.h

struct page {
	unsigned long flags;		/* Atomic flags, some possibly
					 * updated asynchronously */
	......
}

页描述符页描述符中对内存回收来说非常必要的标志:
PG_lru:表示页在lru链表中
PG_referenced: 表示页最近被访问(只有文件页使用)
PG_dirty:页为脏页,文件页被修改,以及非文件页加入到swap cache后,就会被标记为脏页。在此页回写前会被清除,但是回写失败时又会被置位
PG_active:页为活动页,配合PG_lru就可以得出页是处于非活动页lru链表还是活动页lru链表PG_private:页描述符中的page->private保存有数据
PG_writeback:页正在进行回写
PG_swapbacked:此页可写入swap分区,一般用于表示此页是非文件页
PG_swapcache:页已经加入到了swap cache中(只有非文件页使用)
PG_reclaim:页正在进行回收,只有在内存回收时才会对需要回收的页进行此标记
PG_mlocked:页被锁在内存中

3.2.1页面状态转换机制:晋升(Promotion)

晋升(Promotion):从 inactive 到 active

触发条件:inactive list 中的页面被访问 2 次(即 PG_referenced=1 时再次被访问)。

操作:
通过activate_page()将页面移至 active list 头部。
设置 PG_active=1,清 0 PG_referenced。

当页面被访问时,内核调用 mark_page_accessed():

// v5.15/source/mm/swap.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 (!PageReferenced(page)) {
		SetPageReferenced(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.
		 */
		if (PageLRU(page))
			activate_page(page);
		else
			__lru_cache_activate_page(page);
		ClearPageReferenced(page);
		workingset_activation(page);
	}
	if (page_is_idle(page))
		clear_page_idle(page);
}
EXPORT_SYMBOL(mark_page_accessed);

mark_page_accessed() 函数负责管理页面的活跃状态标记(PG_referenced 和 PG_active)。
标记页面为“被访问过”,参与 LRU 链表的活跃度管理。
关键行为:
(1)若页面首次被访问,设置 PG_referenced 标志。

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

对应状态转换:inactive,unreferenced → inactive,referenced。

(2)若页面已被访问过(PG_referenced 已设置)且可回收,则将其激活(移到 Active List)。

    if (PageLRU(page))
        activate_page(page);
    else
        __lru_cache_activate_page(page);
    ClearPageReferenced(page);
    workingset_activation(page);
}

逻辑分支:
页面在 LRU 链表上:调用 activate_page() 将其从 Inactive List 移到 Active List。
页面在 PageVec 缓存中:通过 __lru_cache_activate_page() 标记,待后续批量处理。

状态操作:
清除 PG_referenced(因已激活,无需重复标记)。
调用 workingset_activation() 更新工作集统计(用于内存压力评估)。

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

(3)处理不可回收页面(Unevictable)和空闲页面(Idle)的特殊情况。

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

关键设计思想
(1) 二次机会算法(Second Chance)
规则:页面需被访问 两次(首次设 PG_referenced,二次激活)才能晋升到 Active List。
优势:避免短暂访问的页面长期占用 Active List。

(2) 批处理优化
PageVec 机制:不在 LRU 上的页面(如刚分配)暂存到每 CPU 缓存,减少锁争用。
性能权衡:延迟激活操作,提升并发性能。

3.2.2页面状态转换机制:降级(Demotion)

内核定期扫描 Active List,将未被引用的页面降级。

shrink_active_list() 函数扫描 Active LRU 链表,根据页面访问情况决定将其降级到 Inactive List 或保留在 Active List。

// v5.15/source/mm/vmscan.c

/*
 * shrink_active_list() moves pages from the active LRU to the inactive LRU.
 *
 * We move them the other way if the page is referenced by one or more
 * processes.
 *
 * If the pages are mostly unmapped, the processing is fast and it is
 * appropriate to hold lru_lock across the whole operation.  But if
 * the pages are mapped, the processing is slow (page_referenced()), so
 * we should drop lru_lock around each page.  It's impossible to balance
 * this, so instead we remove the pages from the LRU while processing them.
 * It is safe to rely on PG_active against the non-LRU pages in here because
 * nobody will play with that bit on a non-LRU page.
 *
 * The downside is that we have to touch page->_refcount against each page.
 * But we had to alter page->flags anyway.
 */
static void shrink_active_list(unsigned long nr_to_scan,
			       struct lruvec *lruvec,
			       struct scan_control *sc,
			       enum lru_list lru)
{
	unsigned long nr_taken;
	unsigned long nr_scanned;
	unsigned long vm_flags;
	LIST_HEAD(l_hold);	/* The pages which were snipped off */
	LIST_HEAD(l_active);
	LIST_HEAD(l_inactive);
	struct page *page;
	unsigned nr_deactivate, nr_activate;
	unsigned nr_rotated = 0;
	int file = is_file_lru(lru);
	struct pglist_data *pgdat = lruvec_pgdat(lruvec);

	lru_add_drain();

	spin_lock_irq(&lruvec->lru_lock);

	nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold,
				     &nr_scanned, sc, lru);

	__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, nr_taken);

	if (!cgroup_reclaim(sc))
		__count_vm_events(PGREFILL, nr_scanned);
	__count_memcg_events(lruvec_memcg(lruvec), PGREFILL, nr_scanned);

	spin_unlock_irq(&lruvec->lru_lock);

	while (!list_empty(&l_hold)) {
		cond_resched();
		page = lru_to_page(&l_hold);
		list_del(&page->lru);

		if (unlikely(!page_evictable(page))) {
			putback_lru_page(page);
			continue;
		}

		if (unlikely(buffer_heads_over_limit)) {
			if (page_has_private(page) && trylock_page(page)) {
				if (page_has_private(page))
					try_to_release_page(page, 0);
				unlock_page(page);
			}
		}

		if (page_referenced(page, 0, sc->target_mem_cgroup,
				    &vm_flags)) {
			/*
			 * Identify referenced, file-backed active pages and
			 * give them one more trip around the active list. So
			 * that executable code get better chances to stay in
			 * memory under moderate memory pressure.  Anon pages
			 * are not likely to be evicted by use-once streaming
			 * IO, plus JVM can create lots of anon VM_EXEC pages,
			 * so we ignore them here.
			 */
			if ((vm_flags & VM_EXEC) && page_is_file_lru(page)) {
				nr_rotated += thp_nr_pages(page);
				list_add(&page->lru, &l_active);
				continue;
			}
		}

		ClearPageActive(page);	/* we are de-activating */
		SetPageWorkingset(page);
		list_add(&page->lru, &l_inactive);
	}

	/*
	 * Move pages back to the lru list.
	 */
	spin_lock_irq(&lruvec->lru_lock);

	nr_activate = move_pages_to_lru(lruvec, &l_active);
	nr_deactivate = move_pages_to_lru(lruvec, &l_inactive);
	/* Keep all free pages in l_active list */
	list_splice(&l_inactive, &l_active);

	__count_vm_events(PGDEACTIVATE, nr_deactivate);
	__count_memcg_events(lruvec_memcg(lruvec), PGDEACTIVATE, nr_deactivate);

	__mod_node_page_state(pgdat, NR_ISOLATED_ANON + file, -nr_taken);
	spin_unlock_irq(&lruvec->lru_lock);

	mem_cgroup_uncharge_list(&l_active);
	free_unref_page_list(&l_active);
	trace_mm_vmscan_lru_shrink_active(pgdat->node_id, nr_taken, nr_activate,
			nr_deactivate, nr_rotated, sc->priority, file);
}

关键操作:
(1)从 Active List 隔离指定数量的页面(isolate_lru_pages)。

lru_add_drain();  // 确保所有待处理的LRU操作完成
spin_lock_irq(&lruvec->lru_lock);
nr_taken = isolate_lru_pages(nr_to_scan, lruvec, &l_hold, &nr_scanned, sc, lru);
spin_unlock_irq(&lruvec->lru_lock);

lru_add_drain():清空每 CPU 的 LRU 缓存(pagevec),确保后续操作基于最新状态。
隔离页面:从 Active List 取出 nr_to_scan 个页面到临时链表 l_hold,避免长期持有锁。

(2)检查页面的访问状态(page_referenced)。

while (!list_empty(&l_hold)) {
    page = lru_to_page(&l_hold);
    if (page_referenced(page, ...)) {
        if ((vm_flags & VM_EXEC) && page_is_file_lru(page)) {
            nr_rotated++;
            list_add(&page->lru, &l_active);  // 保留在Active
            continue;
        }
    }
    ClearPageActive(page);
    list_add(&page->lru, &l_inactive);  // 降级到Inactive
}

page_referenced():检查页面近期是否被访问(通过反向映射遍历 PTE 的 ACCESSED 位)。
特殊处理可执行文件页:若文件页具有 VM_EXEC 标志(如二进制代码),给予额外活跃周期,提升程序运行性能。
降级逻辑:未被引用的页面清除 PG_active 标志,准备移入 Inactive List。

(3)将页面重新分类到 Active 或 Inactive 链表(move_pages_to_lru)。

spin_lock_irq(&lruvec->lru_lock);
nr_activate = move_pages_to_lru(lruvec, &l_active);
nr_deactivate = move_pages_to_lru(lruvec, &l_inactive);
spin_unlock_irq(&lruvec->lru_lock);

3.3 ANON/FILE LRU 链表

内核维护了四种主要的 LRU 链表,分别针对不同类型的页面进行优化:

LRU 链表 描述 典型页面类型
LRU_ACTIVE_ANON 活跃的匿名页(Anonymous Pages) 进程堆、栈、共享内存等动态分配的内存
LRU_INACTIVE_ANON 非活跃的匿名页 近期未使用的匿名页,可能被换出到 Swap
LRU_ACTIVE_FILE 活跃的文件页(File-backed Pages) 频繁访问的文件缓存(如代码、数据文件)
LRU_INACTIVE_FILE 非活跃的文件页 近期未使用的文件缓存,可直接丢弃

链表定义(mmzone.h):

// 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 2

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
};

struct lruvec {
	struct list_head		lists[NR_LRU_LISTS];
	......
}

typedef struct pglist_data {
	......
	/* Fields commonly accessed by the page reclaim scanner */

	/*
	 * NOTE: THIS IS UNUSED IF MEMCG IS ENABLED.
	 *
	 * Use mem_cgroup_lruvec() to look up lruvecs.
	 */
	struct lruvec		__lruvec;
	......
}

(1) 查看 LRU 链表大小

cat /proc/meminfo | grep -iE 'active|inactive'
Active:          4477068 kB
Inactive:       10577744 kB
Active(anon):       1964 kB
Inactive(anon):  5480488 kB
Active(file):    4475104 kB
Inactive(file):  5097256 kB

(2)链表在 node 的各个 zone 上的分布

cat /proc/zoneinfo
Node 0, zone      DMA
  per-node stats
      nr_inactive_anon 1370098
      nr_active_anon 491
      nr_inactive_file 1274314
      nr_active_file 1119114
      nr_unevictable 143

参考资料

Linux内核技术实战课
https://zhuanlan.zhihu.com/p/70964195


网站公告

今日签到

点亮在社区的每一天
去签到