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

Linux 内存管理之page cache

文章目录

  • 一、page cache
    • 1.1 File-backed pages和Anonymous pages
    • 1.2 page cache/slab cache
    • 1.3 读/写路径
    • 1.4 脏页回写
    • 1.5 drop_caches
    • 1.6 时间局部性与空间局部性
    • 1.7 Page Cache 的两种类型
    • 1.8 关键数据结构
  • 二、Page Cache 的产生
    • 2.1 Buffered I/O(标准 I/O)
    • 2.2 Memory-Mapped I/O(mmap)
  • 三、page cache回收
    • 3.1 回收模式
      • zone:内存回收的基本单位
    • 3.2 回收过程
      • 3.2.1页面状态转换机制:晋升(Promotion)
      • 3.2.2页面状态转换机制:降级(Demotion)
    • 3.3 ANON/FILE LRU 链表
  • 参考资料

一、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.hstruct 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	0x1static __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;      // 所属的inodestruct radix_tree_root page_tree; // 基数树存储page cache页// ...
};struct page {unsigned long flags;     // 页面标志位struct address_space *mapping; // 所属的address_spacepgoff_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.hstruct 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);  // 保留在Activecontinue;}}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 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];......
}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      DMAper-node statsnr_inactive_anon 1370098nr_active_anon 491nr_inactive_file 1274314nr_active_file 1119114nr_unevictable 143

参考资料

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

相关文章:

  • 石家庄建设厅网站首页张掖seo
  • 低成本做网站广告策划
  • 罗田做网站网络培训系统
  • 套用别人产品图片做网站惠州seo整站优化
  • 游戏周边产品 做网站济南seo外包服务
  • 莒县城乡建设局网站营销网络是什么意思
  • LOOP如何让长周期交互LLM代理在复杂环境中实现突破?
  • JSON框架转化isSuccess()为sucess字段
  • DataX(3)—— 核心流程源码
  • 计算机网络 网络层:数据平面(二)
  • 北斗导航 | 基于改进奇偶矢量法的CAT I精密进近RAIM算法
  • 若依框架配置+相关报错
  • [日志收集]
  • [ linux-系统 ] 基础IO
  • 区间求最值问题高效解决方法
  • 仿Apple官网设计风格
  • 基于STM32的智能节能风扇的设计
  • IT运维效率提升: 当IT监控遇上3D可视化
  • Python 爬虫入门:从数据爬取到转存 MySQL 数据库
  • 深度学习入门--(二)感知机
  • VBA技术资料MF329:获得屏幕分辨率
  • 数据库1.0
  • 【请关注】实操mongodb集群部署
  • 迁移学习—基于猫狗数据集
  • SpringCloud系列(37)--搭建SpringCloud Gateway
  • 解释一下黑盒测试和白盒测试的区别?