Linux内存管理 - LRU机制
定义
LRU:Least recently used(最近最少使用)。内核通过LRU链表把内存组织起来,把活跃的页(热页)和不活跃的页(冷页)分类保存到不同列表中。当系统中可用的内存告急的时候,内核线程kswapd被唤醒。kswapd通过查询LRU链表,将不活跃的页释放或者置换后,将物理页回收,用于新的内存分配。
实现逻辑:
- 建立一个双向链表
- 当访问这一页的时候,若这一页已经存在于链表中,则将该节点移动到头节点上。
- 插入的数据时,如果链表已满,就把链表尾部的数据直接删掉(或从内存中置换到disk中)。
数据结构
先看代码总览

struct lruvec
struct lruvec定义在include/linux/mmzone.h(和NUMA node、zone的结构体定义在一个文件中)
每个NUMA node有自己的LRU结构体:

内核把用户空间的内存页分为两大类型:
- 匿名页:进程的堆、栈、还有mmap匿名映射的页。这些页面如果被swap,只能写入swap分区,代价较大。
- 文件页:磁盘上的文件通过mmap映射到内存的页。这些页面如果被换出,因为原始文件就在disk上,直接丢弃即可。如果需要再次访问,从原始文件重新读取即可。
每种文件类型内核都维护两个链表:
- 活跃列表:最近被访问过的页,热页。当一个冷页被访问就会被promoted到活跃列表。
- 不活跃列表:最近没被访问的页,冷页。kswapd回收的主要对象。当热页经过一段时间未被访问,它会被demoted到不活跃列表。
除此之外还有一个不可回收页列表,所以一共有5个LRU链表:

数据结构的示意图就是:

PG_referenced
每个page都有个 PG_referenced 的标志位,表示此page是否被访问过,这个标志位在内存回收过程中起着至关重要的作用。
进程申请一个新的页时:
- 内核会把这个内存页添加到active_list中,并且将 PG_referenced 标志位设置为 0。
这个页被访问时:
- 如果内存页原来处于活跃链表中,那么就会把此内存页的 PG_referenced 设置为 1。
- 如果内存页原来处于非活跃链表中,并且 PG_referenced 为 0。那么将内存页的 PG_referenced 标志位设置为 1。
- 如果内存页原来处于非活跃链表中,并且 PG_referenced 为 1。那么将会把内存页从非活跃链表移动到活跃链表,并且将 PG_referenced 设置为 0。
需要进行内存回收时(匿名页):
- 从 非活跃链表 的尾部开始进行内存淘汰,如果内存页的 PG_referenced 标志位为 1 时,将跳过此内存页,并且将此内存页的 PG_referenced 标志位设置为 0。
- 如果内存页的 PG_referenced 标志位为 0 时,那么将此内存页写入到 交换分区 中,并且将所有与此内存页的映射解除绑定,然后释放此内存页。
- 如果内存页的 PG_referenced 标志位为 1,那么衰退过程将会把此内存页的 PG_referenced 标志位设置为 0。
- 如果内存页的 PG_referenced 标志位为 0,那么衰退过程将会把此内存页移动到 非活跃链表 中。
sturct folio
这个结构体的设计是为了批量管理page的,内核中很多操作(如页面缓存操作、LRU管理)需要频繁地添加、删除或处理单个页面。如果每次操作都获取/释放锁、执行原子操作或进行函数调用,开销会非常大。将这些操作“攒”起来,放到一个数组(即一个“批次”或“向量”)中。当数组填满或达到某个阈值时,再一次性处理整个批次。这极大地减少了锁竞争和间接调用开销,提高了性能。
struct pagevec(前身)
开始内核使用的是struct pagevec结构体:

PAGEVEC_SIZE 通常是 15(即 14 + 1)
struct folio和struct folio_batch
后面内核引入了struct folio,struct folio 是一个代表连续物理内存页的新内核数据结构。它可以表示单个页(4KB),也可以表示一个“大页”(如 2MB 或 1GB)。
- struct page: 代表一本书中的一页纸。
- struct folio: 代表一整章或一整本书(即一叠连续的纸)。
引入folio的目的:
1. 高效的支持大页
2. 减少元数据开销等
所以就也引入了struct folio_batch。它的结构与 pagevec 几乎一模一样,只是核心数组成员从 struct page * 变成了 struct folio *。PAGEVEC_SIZE也增长到了31

此外给每个CPU定义了5个folio_batch

未完待续。。。。
